Skip to content

Typed ProblemSpecs (Plan 2) + learning-loops run ledger L1 (Plan 3) - #212

Merged
aarontrowbridge merged 17 commits into
mainfrom
wip/typed-specs-amicode
Jul 25, 2026
Merged

Typed ProblemSpecs (Plan 2) + learning-loops run ledger L1 (Plan 3)#212
aarontrowbridge merged 17 commits into
mainfrom
wip/typed-specs-amicode

Conversation

@aarontrowbridge

Copy link
Copy Markdown
Member

Two slices of the typed-ProblemSpecs initiative, in one branch because Plan 3 builds directly on Plan 2's hashing.

Green: @amicode/schema 92/92 · @amicode/amico-run 376/376 · extension 769 passed / 6 pre-existing skips. Verified across 4 consecutive full pnpm -r sweeps.

Plan 2 — typed specs reach the TypeScript side

  • problemspec schema vendored from the Julia registries, in two variants: the OSS one emitted from Piccolo (c3884122) and the FULL one from private Piccolissimo (9098f6f). Emitting the full variant from Piccolissimo rather than Piccolo is deliberate — a public Piccolo emission would leak private capability names.
  • hashing.ts is byte-identical to Julia's Piccolo.Specs.structure_hash/problem_hash, pinned against Julia-emitted sidecar fixtures. This works because JS's Number::toString is the ECMAScript algorithm hashes.jl was hand-built to match — please don't substitute another float formatter on either side.
  • solvespec v4: adds problem_spec (oneOf [string, object]), xor with script_path, integer schema_version enum.
  • amico-run routes solvespec.problem_specPiccolo.Specs.solve_spec.
  • ajv ↔ JSONSchema.jl dual-validation on shared fixtures, plus a cross-repo vendoring-drift gate.

Schema-registration gotcha, documented in code: problemspec and ledger-record register in SCHEMAS only, never SUPPORTED_VERSIONS_BY_KIND — both are top-level oneOf shapes with no properties.schema_version, and registering them there crashes the module at load.

Plan 3 — the run ledger (learning loops L1)

Append-only JSONL at ~/.amico/ledger/runs.jsonl. Six record types: solve, verdict, attempt_error, fallback, override, burn. Atomic via PIPE_BUF + O_APPEND, schema-validated on every append.

  • amico-run is the single writer. Every extension-side stanza shells amico ledger append; the extension never writes the JSONL. A failed ledger write never fails a run.
  • L-A retrieval: medians + IQR over source:"user" solves (excluding both replay and simulated), with "verified" := an agree verdict joined on problem_hash, visible provenance, and a mechanical (n, IQR) → confidence rubric.
  • Interim cap: ledger-sourced confidence never reaches high, applied twice. Veloce auto-accepts only high, so without the cap a tight-IQR verified prior would auto-apply the moment L-A shipped, with no per-structure trust gate in place. L-H earns that back.
  • E2E acceptance: two runs of one structure with distinct problem_hash and one agree verdict → "n=2 runs, 1 verified" with confidence capped at medium.

One real defect found and fixed here (e164855)

structure_hash covers a problem's type skeleton, not its task — it never includes goal.gate, so a CZ and an X gate on the same system/template/solver hash identically. That's correct for warm-pool routing (the gate doesn't change the Julia type) but wrong for priors: neither the primary nor the fallback retrieval key discriminated the goal, while summary.goal was recorded and even required. A hard CZ's median Q/max_iter were being recommended for a trivially easier X gate.

Fixed in the retrieval key, not the hash — the hash's coarseness is load-bearing for routing and precompilation. Omitting the goal stays legal but the provenance then reads goal not keyed rather than implying a tighter key than was used.

Divergences from the plan, all forced by reality

  • @amicode/schema's structureHash is unreachable from amicode_tools.ts — its Bun plugin runtime resolves only relative sibling imports, not bare package specifiers. So structure_hash is read back from the prior run's result.toml (the value settle() already stamped) instead of recomputed. Arguably better: one writer of that hash, no chance of two implementations drifting.
  • action:"query" needs ≥1 completed run in a workspace — there's no propose-time spec-assembly pipeline yet. Matches L-A's intent (priors are for a repeat formulation) but worth a human's confirmation.
  • attempt_error/fallback had no in-process hook — both happen inside a bash-invoked amico run. Rather than invent one, this adds two bookkeeping tools, amicode_report_attempt_error and amicode_report_fallback, following amicode_recommend's record-what-happened-elsewhere pattern. verdict had a real call site and is wired into amicode_verify. This is new tool surface — a design call, flagged rather than buried.
  • platform doesn't exist in problemspec.schema.json, so settle() derives it from system.template (MultiTransmonSystemmulti_transmon). Informational only, but the fallback retrieval key keys on it, so the heuristic is load-bearing there.

Known caveats

  • The vendoring-drift HARD gate soft-skips until the Piccolo/Piccolissimo emission commits are pushed, because the sidecar shas are local-only. It auto-hardens once they land (Piccolo side is Piccolo.Specs Phase 1 — declarative ProblemSpec wire format + interpreter (call-surface neutral) Piccolo.jl#258).
  • Pre-split hazard: packages/schema registers the FULL problemspec.schema.json as the shipped problemspec kind, with the OSS variant vendored alongside "for package-access staging." Only a comment says which to ship. An OSS extraction that keeps this package as-is would expose six private-only enum values (altissimo backend, continuation/staged strategies, hermite_bending_energy/hermite_c2 objectives, robust wrapper). Harmless today since this repo is private — but it wants a build-time gate before the open-core split ships.
  • Piccolo.jl#259 (Phase 1b) changes the OSS schema additively (allOf 4→7). Hash sidecars are unaffected (they hash spec data, not the schema), but both vendored schema variants will need re-staging after it merges.
  • amico-run's bundle-and-spawn tests are load-sensitive under pnpm -r: two flakes were observed and then failed to reproduce across 4 consecutive green sweeps (a different test each time, neither touching changed code paths). Pre-existing fragility; will eventually bite CI.

🤖 Generated with Claude Code

Aaron Trowbridge and others added 17 commits July 24, 2026 12:34
…issimo)

Vendor both emitted variants into packages/schema/schemas/: the FULL variant as
problemspec.schema.json (Piccolissimo.jl @ 9098f6f) and the OSS variant as
problemspec.oss.schema.json (Piccolo.jl @ c3884122). Provenance shas live in
*.schema.json.sha sidecars, kept OUT of the JSON so the vendored files stay
byte-identical to the emitted schemas for the Task 9 cross-repo vendoring-drift
gate.

Register the FULL variant as the `problemspec` kind in SCHEMAS ONLY (NOT
SUPPORTED_VERSIONS_BY_KIND — it is a top-level oneOf with an INTEGER
schema_version enum [1] inside each branch, so it has no top-level
properties.schema_version; plan review correction #6). Add a kindForFilename
rule (problem.toml -> problemspec). Widen the version-map type to exclude
problemspec alongside finished.

TDD: test/problemspec.test.ts asserts the kind is registered, a valid control
spec validates ok, an unknown template and an unknown top-level key are
rejected, and problem.toml routes to problemspec. Add the valid golden fixture
and update the SCHEMA_KINDS exact-set assertion. Full @amicode/schema suite +
typecheck green (50 tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…formance

Task 4 (Plan 2). Adds 5 valid problemspec fixtures (x-control, energy-polish,
min-time, free-phase, rollout) and 5 invalid (unknown-field, free-dt-bare-true,
free-phase+bilinear, template<->pulse mismatch, bad-objective-kind). Extends the
ajv sweep (test/problemspec.test.ts) and the JSONSchema.jl lane (julia/runtests.jl
KINDS + a dedicated dual-validation testset) so both validators must agree on
accept/reject for every fixture. Routes problem.toml -> problemspec in validate.jl.

Verified: JSONSchema.jl v1.5.0 draft-07 oneOf + if/then matches ajv on all fixtures
(no divergence). The plan's "R_ddu on SplinePulseProblem" negative is NOT
schema-catchable (R_ddu is a plain optional number valid on every template; the
applicability rule is a materialize-layer trait) — substituted bad-objective-kind
and flagged the gap to Plan 1 emit_schema.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…guage)

Task 5 (Plan 2). src/hashing.ts mirrors Piccolo's src/specs/hashes.jl byte-for-byte:
canonicalJson (sorted ASCII keys, JSON string escaping, ECMAScript Number::toString
via String(x) — the reference algorithm hashes.jl was hand-built to match) plus
fullDict/structureFields, which apply the SAME parse_spec defaults + wire projection
the Julia structs carry so smol-toml (int) and TOML.jl (float) parses hash the same
logical spec. structureHash/problemHash are sha256hex over the canonical bytes.

test/hashing.test.ts asserts both hashes equal the Julia-emitted sidecars for every
fixture, and checks the landmine's canonical JSON is byte-identical to Julia's
full_dict / structure_fields output (T=100.0 float -> bare "100").

Verified byte-for-byte, e.g. landmine.toml:
  structure_hash = ff3a6210ca7b094c2188e87bbc9c24e4eaac770d39b0067ab9afeb5756485b0d
  problem_hash   = b7ec369a0952b7365bd182889a9ad5cacf427d48bef5aa4095c784d4b6d3c1c3

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend ac6_drift_check.sh with a problemspec perturbation case: canary the
control branch's required (oneOf[0]) so a control fixture matches zero
branches, and assert BOTH the TS amico-validate bin AND julia validate.jl
flip accept->reject, then revert. Refactored the run case + new case into a
shared assert_flip helper; the trap now reverts the whole schemas dir.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump solvespec to v4: $id .../solvespec/v4, schema_version enum
["1","2","3","4"], new `problem_spec` property (oneOf string|object), and a
top-level oneOf enforcing exactly one of {script_path, problem_spec}. lab_id
stays required; additionalProperties:false stays strict. problem_spec is the
typed Piccolo ProblemSpec runner target that amico-run routes to
Piccolo.Specs.solve_spec (Task 8).

Per review correction #4, SUPPORTED_VERSIONS_BY_KIND auto-derives from the
enum, so no schemas.ts edit is needed; fixed the stale index.ts header
comment. TDD: problem_spec-only (string+object) validate, both/neither fail,
lab_id required, strict-unknown holds. TS 79/79, Julia round-trip 35/35.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…spec

A v4 solvespec may carry problem_spec (path or inline object) instead of
script_path. Wire the scriptless path end-to-end (review correction #2):

- types.ts: add problem_spec to SpecStamp; Executor.submit scriptPath optional
- launch.ts: parse --spec up front, detect problem_spec, stand down the
  no-script guard + script read; stamp problem_spec onto opts.spec
- gate.ts: skip the import scan for a scriptless spec (the ProblemSpec,
  validated against Piccolo's registries in step 1, is the entitlement surface)
- subcommands.ts: estimate rejects a scriptless spec cleanly (no crash)
- local_executor.ts: scriptPath optional; spawn
  `julia -e 'using Piccolo; Piccolo.Specs.solve_spec(ARGS[1]; run_dir=pwd())' <path>`;
  an inline object is serialized to <runDir>/problem.toml AFTER the manifest
  (run-dir contract: manifest FIRST, then problem.toml, solvespec.json, index,
  latest); a string path is passed straight through
- remote_executor.ts: reject problem_spec (local-only until Phase 4)

TDD (julia spawn mocked via fakeJulia): problem_spec path + inline object
route to solve_spec with run.toml written first and ITER/DONE classified; a
script_path spec still runs the script directly. amico-run 345/345, typecheck
clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend the schema-roundtrip job for problemspec (Task 9):
- the existing runtests.jl lane already runs the problemspec dual-validation
  sweep (ajv == JSONSchema.jl) and the AC6 step now covers the problemspec
  conditional schema — update the job comment to reflect both.
- add a cross-repo vendoring-drift gate: scripts/vendoring_drift_check.sh
  git-fetches each vendored problemspec schema's source at the sha recorded in
  its *.sha sidecar and cmp's byte-for-byte — it NEVER re-runs regenerate.jl
  (correction #1). The OSS variant is checked against public Piccolo
  tokenlessly (a reachable-but-differing source reds CI); the FULL variant
  (private Piccolissimo) is checked only when PICCOLISSIMO_SCHEMA_TOKEN is set,
  so amicode CI carries no mandatory private dep — its authoritative gate lives
  in Piccolissimo's own CI.

Failure model: reachable source + byte mismatch → hard fail; source not
reachable at the pinned sha → warn + skip (emission side Tasks 1-2 not pushed
yet), so the gate auto-hardens once the source lands. Added a `path` field to
the sidecars so the script reads the source path (no hardcoding), and a
VENDOR_DRIFT_SRC_BASE local-test seam.

Validated locally: YAML parses (11 steps); drift script exits 0 on identical
source, 1 on drift, and soft-skips the currently-unreachable pinned source.
The vendoring-drift step is push-gated on CI and auto-hardens when Piccolo
Task 1 / Piccolissimo Task 2 are pushed and the sidecar shas point at real
commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan 3 (learning-loops L1) Task 1. ledgerPath() = $AMICO_LEDGER ||
~/.amico/ledger/runs.jsonl; appendRecord/readRecords over JSONL; the six
discriminated record kinds (solve|verdict|attempt_error|fallback|override|burn).
O_APPEND single-writer atomicity guarded by a per-record PIPE_BUF size ceiling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan 3 (learning-loops L1) Task 2. draft-07 oneOf discriminated on `type` over
the six record kinds (solve|verdict|attempt_error|fallback|override|burn);
source enum user|replay|simulated (simulated = Prova isolation bridge); override
carries auto_accepted. Registered in SCHEMAS ONLY (never SUPPORTED_VERSIONS_BY_KIND
— it is a top-level oneOf with no properties.schema_version, which would crash the
version-map builder at load, same as problemspec — review correction #1).
appendRecord now validates on write and throws on invalid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan 3 (learning-loops L1) Task 4. bucketN/bucketT (documented edges); primary key
structure_hash x (n_bucket, t_bucket), fallback (platform, template, trajectory,
levels, buckets) below K_MIN; source=user filter (excludes replay AND simulated);
solve-verdict join on problem_hash; verified = count of matched solves whose
problem_hash has an agree verdict; medians+IQR per numeric param (Q,R,du_bound,N,
max_iter) + integrator mode; honest provenance 'n=<total> runs, <verified>
verified'; mechanical confidence with the interim medium cap (any unverified run
bars high — keeps ledger-sourced auto-apply unreachable until L-H).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan 3 (learning-loops L1) Task 3. `amico ledger append` (--json or stdin →
validate → appendRecord; extension stanzas shell into this, never touch runs.jsonl
directly) + `amico ledger query --structure-hash --n --t` (delegates to
ledger_query.ts). Registered in SPINE_VERBS so CLI + MCP facade + --help are free.
Bundle test spawns 24 concurrent subprocess appenders proving cross-process
O_APPEND atomicity (no interleaving).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LocalExecutor.settle() now derives + appends one `solve` ledger record
after writeFinished: structure_hash/problem_hash/versions/converged come
from result.toml's [params]; the base summary AND the recommendable
knobs (Q/R/du_bound/max_iter/integrator) come from the solvespec (the
typed ProblemSpec run.toml's script_path resolves to) — without the
knobs, ledger_query's medians are permanently empty, silently breaking
Tasks 6/9. source defaults to "user", overridable via
AMICO_LEDGER_SOURCE for L-I replay. Any read/parse/validation failure
is caught and logged — a ledger hiccup must never fail a run.

Plan 3 (learning-loops L1) Task 5.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…amping (L-A)

amicode_recommend gains action="query": resolves the active workspace's
most recent structure_hash (+ N/T) from its last run's result.toml /
solvespec, shells `amico ledger query`, and returns medians/IQR with
honest "n runs, m verified" provenance via a new ledger_client.ts. All
ledger I/O shells to the `amico` CLI (single-writer discipline —
opencode-plugin's Bun runtime can only resolve relative sibling
imports, so @amicode/amico-run/@amicode/schema aren't reachable here
regardless; structure_hash is read back from Task 5's result.toml
stamp rather than recomputed).

propose/outcome events are now stamped with structure_hash; an
outcome="overridden" call additionally appends an `override` ledger
stanza (recommended value recovered from the matching `proposed`
event). Ledger-sourced confidence is clamped to at most "medium" —
belt-and-suspenders on top of ledger_query.ts's own interim cap, so
veloce (which auto-accepts only "high") can never auto-apply an
unverified prior.

Also re-exports structureHash/problemHash from @amicode/schema's
package root (packages/schema/src/hashing.ts) — needed by this task,
previously only reachable via a package-internal relative path.

Plan 3 (learning-loops L1) Task 6.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Adds three pure, tested stanza builders to ledger_client.ts
(attemptErrorStanza, fallbackStanza, verdictStanza) plus
resolveRunHashes (reads a run dir's result.toml [params] for the
structure_hash/problem_hash Task 5 already stamps there). All route
through appendStanza → `amico ledger append` — the extension never
writes runs.jsonl directly.

Wires two new bookkeeping tools, amicode_report_attempt_error and
amicode_report_fallback: neither a spec-validation failure nor a tier
fallback has an in-process hook in the extension today (both happen
inside a bash-invoked `amico run --spec`), so — mirroring
amicode_recommend's own "record what happened elsewhere" doctrine —
the agent reports what it observed in the CLI's output. Wires the
`verdict` stanza into the existing amicode_verify tool (a real call
site: agree/disagree + both fidelities are already its args), joined
to the run's `solve` record via problem_hash.

Plan 3 (learning-loops L1) Task 7.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Adds the `ledger` provenance source to confidence-rubric.md's
resolution order (between own-precedent and demo) with the mechanical
(n, IQR, verified-fraction) → high|medium|low table and the interim
`medium` cap note (applied twice: ledger_query.ts's rankConfidence and
again at the amicode_recommend tool boundary).

Writes packages/amico-run/docs/ledger.md: single-writer discipline
(and why the extension's Bun-runtime import constraints make shelling
`amico ledger append` structural, not stylistic); ops-data-vs-vault-
knowledge; why only source="user" feeds priors (replay = L-I nightly
fleet, simulated = Prova's isolation bridge); no-ML/mechanical-only
doctrine; and the charter/16 Tier-2-substrate + charter/18 severed-
flywheel reconnection this ledger grounds.

Plan 3 (learning-loops L1) Task 8.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Cross-package integration test (packages/extension/test/ledger_e2e.test.ts):
two REAL LocalExecutor solves of the same structure (same structure_hash,
distinct problem_hash — review correction #3), one gets an `agree`
verdict via the real built `amico` CLI, and ledger_client.ts's
queryLedger + selectRecommendations (the logic behind
amicode_recommend action="query") return "n=2 runs, 1 verified"
provenance with confidence capped at medium — spec success criteria 1
and 5, exercised end-to-end rather than mocked.

CI: adds a ledger-record.json fixture + gates it through amico-validate
in ci.yml's fast job. Confirmed (not just asserted) that the ledger
suites are already swept: `pnpm -r run test` across schema (92/92),
amico-run (374/374), and extension (766/766, 6 pre-existing skips) all
green together.

Plan 3 (learning-loops L1) Task 9 — L1 complete.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
structure_hash covers a problem's TYPE skeleton, deliberately not its task:
structureFields() takes system/trajectory/pulse/template/goal_treatment/
free_dt/free_phase/objective_kinds/integrator/wrappers/solver but never
goal.gate. So a CZ and an X gate on the same system, template and solver
hash identically. That is correct for warm-pool routing -- the gate does
not change the Julia type, which is the whole point of the hash -- but
wrong for priors: neither the primary key (structure_hash x N-bucket x
T-bucket) nor the fallback (platform/template/trajectory/levels + buckets)
discriminated the goal, while summary.goal was recorded and even required.

Net effect before this change: a hard CZ's median Q/R/du_bound/max_iter
were recommended for a trivially easier X gate and vice versa, averaging
two difficulty populations behind honest-looking provenance.

The fix belongs in the retrieval key, NOT in structure_hash -- the hash's
coarseness is load-bearing for routing and precompilation ("same
structure_hash => same concrete types"). The two jobs the hash serves want
different granularities; retrieval is where that difference is paid.

- QueryKey gains `goal?`, applied to BOTH primaryMatch and fallbackMatch.
- Omitting it stays legal and coarse, but provenance then reads "goal not
  keyed" instead of implying a tighter key than was used -- L-A's doctrine
  is visible provenance, so silent discrimination would make it lie.
- `amico ledger query --goal <g>`; usage + synopsis updated.
- resolveWorkspaceSpecContext extracts goal from the run's solvespec,
  mirroring settle()'s derivation exactly (goal.gate, else goal.kind), since
  goal is never stamped into result.toml's [params] -- without that read the
  key would be silently inert in production.
- queryLedger passes --goal through; amicode_recommend action:"query" supplies it.
- confidence-rubric.md + docs/ledger.md document the goal leg and why the
  hash is not the retrieval key.

Tests: amico-run 376 (+2: CZ/X separation on one structure_hash, incl. the
mixed-query median made visible; goal applied on the fallback path too),
extension 769 (+3: goal extracted from [goal].gate, [goal].kind fallback,
absent-goal stays undefined). schema 92 unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant