dojocal

package
v0.47.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 4, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Overview

Package dojocal is the dojo-RSI loop's PURE proposer + self-scoring rung — the genuinely-safe autonomous slice that MUTATES NOTHING (Phase 1 of docs/fak/dojo-rsi-loop.md, issue #1023). It is the twin of internal/guardrsi pointed at the dojo's calibration journal instead of the guard's verdict journal: where guardrsi folds a verdict journal and proposes the worst-bucket honesty hole, dojocal folds a dojo report's scored episodes (reusing dojo.BoardFromEpisodes) and proposes the worst-calibrated MEASURED, NON-DENY-LISTED, NON-FLOOR (lever, metric) cell to recalibrate.

What makes the loop safe to run unattended is what it does NOT do: it never opens a worktree, never rewrites a claim literal, never touches a file. It PROPOSES a recalibration (swap the claimed number to the corpus mean) and SELF-SCORES it by replaying dojo.FoldCalibrable over the same episodes with the candidate claim swapped in — a pure, deterministic, CI-able re-fold. The KEEP bit is non-forgeable and mirrors guardrsi.RunIteration exactly: KEEP iff measured-rows>0 AND the folded calibrable metric STRICTLY drops AND a per-lever sample threshold is met AND an external `--witness {"ok":true}` is supplied.

The structural honesty guarantee comes from dojo.FoldCalibrable, not from a check here: an INTENTIONAL FLOOR (false_warm_rate must stay 0.0) folds by its breach, not its calib_err, so "recalibrating" a floor up to its empirical rate RAISES the fold and is reverted. The candidate picker refuses to target a floor at all (it is routed, never recalibrated). A small NeverRecalibrate deny-list repeats that safety rule independently of the episode's IntentionalFloor bit, and dojo.FoldCalibrable refuses to fold an UNMEASURED episode — so the ways the loop could optimise itself into dishonesty are closed by construction, the same way guardrsi can only ever repair an honesty hole and never invent a row.

Index

Constants

View Source
const (
	// ProposeSchema tags the proposal envelope.
	ProposeSchema = "fak-dojo-rsi.propose/1"
	// IterationSchema tags one replayed self-scoring iteration.
	IterationSchema = "fak-dojo-rsi/1"
	// DefaultMinSample is the per-lever sample floor a RECALIBRATE candidate must
	// clear before its proposed claim is trusted enough to KEEP: a recalibration
	// fitted to one or two boundaries is noise, not a corpus central tendency. The
	// guard's worst-bucket loop has no analogue (a journal row is its own witness);
	// the dojo's claim is a population estimate, so it owes a sample floor.
	DefaultMinSample = 3
	// SparseLeverMinSample is the stricter floor for sparse, high-variance dojo
	// levers whose corpus has fewer natural boundaries than resume-posture.
	SparseLeverMinSample = 6
)
View Source
const (
	// JournalSchema tags committed dojo-RSI journal rows.
	JournalSchema = "fak-dojo-rsi-journal/1"
	// TrendSchema tags a folded KEEP/REVERT trend over the journal.
	TrendSchema = "fak-dojo-rsi-trend/1"
	// DefaultJournalRel is the committed dojo-RSI ledger the loop and CI feed read.
	DefaultJournalRel = "docs/dojo/rsi-journal.jsonl"
	// DefaultCellRecheckDays is the staleness horizon used by the Phase-3 selector.
	DefaultCellRecheckDays = 7
)
View Source
const ClaimsRelPath = "internal/dojo/claims.go"

ClaimsRelPath is the module-relative path of the dojo claim registry — the one file the RSI loop's RECALIBRATE arm rewrites. Named here so the worktree harness and the rewrite verifier agree on one literal, exactly as rsiloop pins tunableRelPath. Forward slashes; the command layer joins it onto the module dir.

Variables

This section is empty.

Functions

func AppendJournalLine

func AppendJournalLine(row JournalRow) (string, error)

AppendJournalLine renders a journal row as one JSONL line.

func CheckIteration

func CheckIteration(it Iteration) []string

CheckIteration re-derives the KEEP invariants from the iteration's own fields — the dojo twin of guardrsi.CheckIteration. A kept iteration that cannot defend its keep bit (no rows, no strict drop, too few samples, no witness, or a routed candidate) returns the violations, so a fabricated KEEP is caught at the gate.

func ClaimChangeLine

func ClaimChangeLine(src []byte, lever, metric string, newClaimed float64) (lineNo int, before, after string, err error)

ClaimChangeLine reports the single source line that RewriteClaim would change for (lever, metric): its 1-based line number and the before/after text, for a legible preview diff. It re-runs RewriteClaim internally so the preview is exactly what an apply would write (never a hand-rendered approximation). A rewrite that changes no line (impossible once RewriteClaim succeeds) returns ok=false.

func EffectiveMinSample added in v0.37.0

func EffectiveMinSample(lever, metric string, requested int) int

EffectiveMinSample returns the sample floor for a candidate cell. The caller's requested floor is a global lower bound; per-cell policy can only raise it.

func MarshalTrendText

func MarshalTrendText(tr JournalTrend) string

MarshalTrendText renders the trend as a compact text card for Slack/CI.

func MinSampleOverride added in v0.37.0

func MinSampleOverride(lever, metric string) (int, bool)

MinSampleOverride reports a stricter per-(lever,metric) sample floor. Absence means the caller's requested floor (or DefaultMinSample) is sufficient.

func NeverRecalibrateReason added in v0.37.0

func NeverRecalibrateReason(lever, metric string) (string, bool)

NeverRecalibrateReason reports metrics that must never be converted into a RECALIBRATE claim swap, even if a malformed report omits IntentionalFloor.

func NewWorktreeHarness added in v0.38.0

func NewWorktreeHarness(cfg WorktreeConfig) rsiloop.Harness

NewWorktreeHarness wires a rsiloop.Harness to the real dojo worktree/probe/ suite/truth impls. It is the dojo twin of rsiloop.NewWorktreeHarness: the baseline measures FoldCalibrable over a two-shard corpus in a pinned-main worktree, the candidate rewrites one claim literal and re-measures, and the keep-bit reads a real go-suite plus the two-shard gate plus treeChangedOnly.

func ProjectionPaths

func ProjectionPaths(lever, metric string) []string

ProjectionPaths returns the conservative declared path set for an agent REPROJECT candidate. The command layer verifies actual changed paths stay within this set before accepting any re-measurement.

func ReadClaim

func ReadClaim(src []byte, lever, metric string) (float64, error)

ReadClaim returns the literal claim value currently registered for (lever, metric) in `src`, without rewriting anything. The worktree harness uses it to read the pinned-baseline literal a candidate forks from; the preview command uses it to show the before value. Same fail-closed anchoring as RewriteClaim (not-found / ambiguous are errors, never a silent zero).

func RewriteClaim

func RewriteClaim(src []byte, lever, metric string, newClaimed float64) ([]byte, float64, error)

RewriteClaim re-points the single registered claim literal for (lever, metric) in the claims.go source `src` to newClaimed, returning the rewritten bytes and the OLD value it replaced. It is the pure rewrite the worktree arm applies inside a throwaway worktree before re-measuring FoldCalibrable.

It fails closed on anything that would make the rewrite ambiguous or empty:

  • the cell's `claim(`/`floor(` anchor is not found (an unregistered or moved cell — the rewrite contract is broken, never a silent no-op),
  • the anchor matches more than once (the registry's keys are unique; a double match means the source is not the registry this targets),
  • the new value formats identically to the old (a no-op is not a real recalibration — the same "changed nothing fails closed" rule treeChangedOnly enforces on the worktree side).

Only the float is rewritten; the basis string (which may quote the same number), a floor's trailing bool, and every other cell are byte-identical in the output.

func Round3

func Round3(v float64) float64

Round3 is re-exported for callers that fold dojocal numbers without importing mathx directly. It rounds to three decimals, matching the dojo's reporting.

func TreeChangedWithin

func TreeChangedWithin(changed, declared []string) bool

TreeChangedWithin is the REPROJECT path gate: every changed path must be within the candidate's declared path set. Entries ending in "/" or "/**" are treated as prefixes; other entries are exact files.

Types

type Iteration

type Iteration struct {
	Schema         string          `json:"schema"`
	Goal           string          `json:"goal"`
	Candidate      Recal           `json:"candidate"`
	BaselineFold   dojo.FoldResult `json:"baseline_fold"`
	ReplayedFold   dojo.FoldResult `json:"replayed_fold"`
	BaselineValue  float64         `json:"baseline_value"`
	ReplayedValue  float64         `json:"replayed_value"`
	MeasuredDelta  float64         `json:"measured_delta"` // baseline - replayed; positive = the fold dropped
	MinSample      int             `json:"min_sample"`
	Witness        map[string]any  `json:"witness,omitempty"`
	Kept           bool            `json:"kept"`
	Reason         string          `json:"reason"`
	KeepRevertRule string          `json:"keep_revert_rule"`
}

Iteration is one replayed self-scoring tick — the dojo twin of guardrsi.Iteration. It carries the candidate, the calibrable fold BEFORE and AFTER the proposed claim swap (both DERIVED by re-folding, never asserted), the strict measured delta, and the non-forgeable KEEP bit + reason.

func RunIteration

func RunIteration(r dojo.Report, candidate Recal, minSample int, witness map[string]any) Iteration

RunIteration replays dojo.FoldCalibrable over the report's episodes with the candidate's claim SWAPPED IN, and decides KEEP/REVERT — the pure, deterministic, CI-able dojo twin of guardrsi.RunIteration, taking `--witness {"ok":true}` exactly as guard-verdict-rsi does.

The replay re-scores every episode of the candidate's (lever, metric) cell against the proposed NewClaimed (using the same dojo.Score + dojo.DefaultCalibBand the live builders use), leaving every other episode untouched, then re-folds. Nothing on disk changes; the swap is a value substitution in a copy of the episodes. The KEEP bit mirrors guardrsi exactly:

KEEP iff measured-rows>0 AND the folded calibrable Value STRICTLY drops
     AND the candidate's per-lever sample >= minSample AND a green witness.

A floor target can never KEEP: FoldCalibrable folds a floor by its breach, so closing the gap RAISES Value (negative delta) and REVERTs — the structural guarantee, surfaced here as a normal no-strict-gain revert. An UNMEASURED-routed or floor-routed candidate is refused before any replay (it carries no swap). minSample<=0 falls back to DefaultMinSample; per-cell policy can only raise it.

type JournalRow

type JournalRow struct {
	Schema        string    `json:"schema"`
	Tick          int       `json:"tick"`
	Date          string    `json:"date"`
	GeneratedAt   string    `json:"generated_at"`
	Commit        string    `json:"commit,omitempty"`
	Lever         string    `json:"lever"`
	Metric        string    `json:"metric,omitempty"`
	Kind          RecalKind `json:"kind"`
	Decision      string    `json:"decision"`
	Kept          bool      `json:"kept"`
	AgentArm      bool      `json:"agent_arm,omitempty"`
	Baseline      float64   `json:"baseline_value"`
	Replayed      float64   `json:"replayed_value"`
	MeasuredDelta float64   `json:"measured_delta"`
	BreakerCount  int       `json:"breaker_nonkeeps"`
	WakeupAt      string    `json:"wakeup_at,omitempty"`
	Reason        string    `json:"reason,omitempty"`
}

JournalRow is one durable dojo-RSI tick. It stores the measured keep/revert facts and enough routing context for the CI feed to trend mechanical KEEPs, agent-route REVERTs, and floor ESCALATEs without re-running a corpus.

func NewJournalRow

func NewJournalRow(tick int, it Iteration, decision string, breaker int, now time.Time, commit string, wake Wakeup) JournalRow

NewJournalRow projects one iteration into the durable row schema.

func ParseJournal

func ParseJournal(content string) []JournalRow

ParseJournal parses committed dojo-RSI JSONL, skipping torn or foreign rows.

type JournalTrend

type JournalTrend struct {
	Schema          string      `json:"schema"`
	GeneratedAt     string      `json:"generated_at"`
	Rows            int         `json:"rows"`
	Keep            int         `json:"keep"`
	Revert          int         `json:"revert"`
	Escalate        int         `json:"escalate"`
	MechanicalKeep  int         `json:"mechanical_keep"`
	AgentRoutes     int         `json:"agent_routes"`
	ReprojectRoutes int         `json:"reproject_routes"`
	HarvestRoutes   int         `json:"harvest_routes"`
	FloorEscalates  int         `json:"floor_escalates"`
	Latest          *JournalRow `json:"latest,omitempty"`
	Summary         string      `json:"summary"`
}

JournalTrend folds the committed journal into the CI feed payload.

func FoldTrend

func FoldTrend(rows []JournalRow, now time.Time) JournalTrend

FoldTrend summarizes the KEEP/REVERT journal for the CI feed.

type ProposePayload

type ProposePayload struct {
	Schema     string          `json:"schema"`
	Baseline   dojo.FoldResult `json:"baseline"`
	Board      dojo.Board      `json:"board"`
	Candidates []Recal         `json:"candidates"`
	Worst      Recal           `json:"worst"`
}

ProposePayload is the proposal envelope: the worst-first candidates, the folded calibrable baseline they were drawn from, and the single worst candidate the loop would act on first (mirroring guardrsi's WorstBucket-led FoldPayload).

func ProposeRecals

func ProposeRecals(r dojo.Report) ProposePayload

ProposeRecals folds a dojo report's scored episodes into the worst-first list of recalibration candidates — the twin of guardrsi.WorstBucket, reusing dojo.BoardFromEpisodes for the cross-lever board the candidates ride alongside.

It is pure and total. For each MEASURED, NON-FLOOR (lever, metric) cell it proposes a RECALIBRATE re-pointing the claim at the cell's measured mean; an explicit NeverRecalibrate entry or INTENTIONAL FLOOR cell is emitted as a ROUTE_FLOOR (never a claim swap — a floor breach is a bug to escalate, the design's structural anti-gaming rule); a lever whose episodes were ALL UNMEASURED contributes a ROUTE_UNMEASURED so the floor of the honesty constraint is visible rather than silently dropped. Candidates are sorted worst-first by calib_err (a measured recalibration always ahead of a routed one), then lever, then metric, for determinism.

type Recal

type Recal struct {
	Lever      string    `json:"lever"`
	Metric     string    `json:"metric"`
	Kind       RecalKind `json:"kind"`
	OldClaimed float64   `json:"old_claimed"`
	NewClaimed float64   `json:"new_claimed"`
	// MeasuredMean is the mean realized value over the cell's measured episodes —
	// the corpus central tendency a RECALIBRATE re-points the claim at.
	MeasuredMean float64 `json:"measured_mean"`
	// Sample is how many measured episodes stand behind MeasuredMean.
	Sample int `json:"sample"`
	// MinSample is the effective sample floor this cell must clear to KEEP.
	MinSample int `json:"min_sample"`
	// Verdict is the worst measured episode's verdict for this cell (OVER_CLAIM /
	// UNDER_CLAIM / CALIBRATED), carried for the operator's context.
	Verdict string `json:"verdict"`
	// CalibErr is the worst measured calib_err for the cell — the size of the gap
	// the recalibration would close.
	CalibErr float64 `json:"calib_err"`
	// IntentionalFloor is mirrored from the episode so a reader can see at a glance
	// why a ROUTE_FLOOR candidate is routed rather than recalibrated.
	IntentionalFloor bool `json:"intentional_floor"`
	// NeverRecalibrate marks a safety-critical deny-list entry whose claim may not
	// be swapped even if the input report accidentally drops IntentionalFloor.
	NeverRecalibrate bool `json:"never_recalibrate,omitempty"`
	// Reason is the one-line rationale (why this cell, why this kind).
	Reason string `json:"reason"`
	// DeclaredPaths is the path allow-list the agent arm must stay within for a
	// REPROJECT candidate. The command layer verifies changed paths against this
	// list before any re-measured FoldCalibrable gain can be trusted.
	DeclaredPaths []string `json:"declared_paths,omitempty"`
}

Recal is one proposed recalibration targeting exactly one (lever, metric) cell of the worst-first board. It mirrors the design doc's Recal: the loop writes the proposed NewClaimed (the corpus mean) but the KEEP decision is derived by re-folding, never asserted here.

type RecalKind

type RecalKind string

RecalKind names what a proposal asks for, mirroring the design doc's split (docs/fak/dojo-rsi-loop.md "The candidate"). Only RECALIBRATE is self-KEEPable here; the rest are ROUTED to a human/agent arm and never KEPT by this pure loop.

const (
	// RecalibrateKind re-points a genuine ESTIMATE claim at its corpus central
	// tendency — the mechanical, self-keepable win.
	RecalibrateKind RecalKind = "RECALIBRATE"
	// ReprojectKind routes a projection-code fix: the claim stays pinned, and an
	// agent proposes a patch that moves realized behavior toward it. It is never
	// self-KEPT by the pure recalibration loop.
	ReprojectKind RecalKind = "REPROJECT"
	// HarvestKind routes a real under-claimed saving to the issue/harvest arm. A
	// human decides whether to build it; the pure loop never auto-lands it.
	HarvestKind RecalKind = "HARVEST"
	// RouteFloor marks a candidate whose target cell is an INTENTIONAL FLOOR: a
	// guard the dojo defends, never a recalibration. A breach is a belief-code bug
	// to escalate, so the loop routes it and never proposes a claim swap.
	RouteFloor RecalKind = "ROUTE_FLOOR"
	// RouteUnmeasured marks a candidate the loop cannot score: the worst lever had
	// no measured episode (UNMEASURED is uncandidatable by construction — the floor
	// the honesty constraint routes to).
	RouteUnmeasured RecalKind = "ROUTE_UNMEASURED"
)

type ScoredCell

type ScoredCell struct {
	Candidate    Recal   `json:"candidate"`
	Score        float64 `json:"score"`
	LastTouched  string  `json:"last_touched,omitempty"`
	NextEligible string  `json:"next_eligible,omitempty"`
	AgeDays      float64 `json:"age_days"`
	Novelty      float64 `json:"novelty"`
	ValueWeight  float64 `json:"value_weight"`
	Staleness    float64 `json:"staleness"`
	Saturated    bool    `json:"saturated,omitempty"`
	Reason       string  `json:"reason"`
}

ScoredCell is one candidate scored by novelty x value x staleness.

func NextCandidate

func NextCandidate(ranked []ScoredCell) (ScoredCell, bool)

NextCandidate returns the highest-ranked non-saturated cell.

func RankCandidates

func RankCandidates(candidates []Recal, rows []JournalRow, opts SelectOptions) []ScoredCell

RankCandidates reuses nightrun's selector shape for dojo cells: novelty (never touched), value (calibration gap), and staleness (last touch age) are blended into one deterministic priority score. A fresh touched cell is marked Saturated so a loop can stop instead of thrashing the same row.

type SelectOptions

type SelectOptions struct {
	Now         time.Time
	RecheckDays int
}

SelectOptions parameterizes candidate ranking.

type Wakeup

type Wakeup struct {
	At      string `json:"at"`
	DelayS  int64  `json:"delay_seconds"`
	Reason  string `json:"reason"`
	Pending bool   `json:"pending"`
}

Wakeup is the loop's next self-pacing decision. Command layers can translate it to an MCP ScheduleWakeup call; pure code only computes the time.

func ScheduleWakeup

func ScheduleWakeup(ranked []ScoredCell, now time.Time) Wakeup

ScheduleWakeup returns now when there is runnable work, or the earliest next-eligible time when every candidate is saturated.

type WorktreeCandidate added in v0.38.0

type WorktreeCandidate struct {
	Lever      string
	Metric     string
	NewClaimed float64
}

WorktreeCandidate is the one recalibration the worktree arm measures: the (lever, metric) cell to re-point and the corpus-mean claim to swap in. It is the Payload a rsiloop.Candidate carries; the command layer picks the worst RECALIBRATE from `fak dojo-rsi propose` and builds this from it.

type WorktreeConfig added in v0.38.0

type WorktreeConfig struct {
	// Repo is a path inside the working copy (the module root is fine; git finds
	// the repo from there).
	Repo string
	// BaselineRef is the ref the baseline + the candidate fork from ("main"). The
	// harness resolves it to a SHA ONCE and pins it, so before/after are measured
	// on the identical tree even if main advances mid-run.
	BaselineRef string
	// Corpus is the directory of .jsonl transcripts `fak dojo run --corpus` scores
	// against. It is split into two disjoint shards for the two-shard gate.
	Corpus string
	// Candidate is the single recalibration to measure.
	Candidate WorktreeCandidate
	// SuiteCmds is the suite-green gate; ALL must exit 0. Default: build + vet.
	SuiteCmds [][]string
	// SuitePkgs is the package pattern for the default suite gate ("./...").
	SuitePkgs string
	// DojoArgs are extra args appended to `fak dojo run` (e.g. --ttl 1h, --lever).
	DojoArgs []string
	// DojoRun, when non-nil, overrides the real `fak dojo run --json` invocation.
	// A test injects a fake that returns a fixed report for a corpus dir; the
	// production harness leaves it nil so the real exec runs.
	DojoRun func(moduleDir, corpusDir string) (dojo.Report, error)
	// ScratchDir is the parent for ephemeral worktrees + shard dirs ("" => os.TempDir).
	ScratchDir string
}

WorktreeConfig parameterizes the real dojo worktree harness.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL