diff --git a/packages/extension/packs/ee-test-measurement/FIRST-TASK.md b/packages/extension/packs/ee-test-measurement/FIRST-TASK.md new file mode 100644 index 000000000..72f0c7195 --- /dev/null +++ b/packages/extension/packs/ee-test-measurement/FIRST-TASK.md @@ -0,0 +1,24 @@ +# First task — the shakedown exercise (declared; NOT yet run) + +**The task:** one non-QICK instrument class through the strumento device +schema against **mock instruments** — a signal-generator + spectrum-analyzer +fit cycle via the task-record seam: author a device instance for the bench +class (schema extension in scope this pass), run a fit cycle +(`instrumento fit lorentzian`-class) against a mock measurement, record the +task (manifest, progress stream, terminal result), and fold a **T1b, +tier-labeled verdict** (residuals against declared tolerance) end-to-end +through the loop. + +**Exercise level: shakedown** — the loop plumbing end-to-end on synthetic +substrate. It certifies the pack's mechanics (schema → task record → +verdict → fold), not the domain's real capability. Substrate-touching +exercise (a real bench run) is the tracked next milestone, gated on +hardware access that is not wired in this build — stated, not assumed. + +**The polyglot seam rides the same exercise:** the task record the mock run +produces is written by the Python task-record writer and read by a Julia +consumer — one round-trip exercising the contract's shape across languages. + +**Exit:** the run exists with a run ID, a tier-labeled verdict, and the task +directory intact — then this pack is EXERCISED (shakedown), and this +declaration updates to point at the run. diff --git a/packages/extension/packs/ee-test-measurement/PACK.toml b/packages/extension/packs/ee-test-measurement/PACK.toml new file mode 100644 index 000000000..b9658c25e --- /dev/null +++ b/packages/extension/packs/ee-test-measurement/PACK.toml @@ -0,0 +1,53 @@ +# The EE / test-&-measurement domain pack (plan-20260920 step 10; amicode #1328). +# The strumento substrate — device model, task records, calibration store — +# is this domain in quantum clothing; this pack generalizes it to instruments. +# SEEDED, not exercised: the manifest + references resolve; the first task +# (a mock-instrument fit cycle) is the shakedown exercise, the next milestone. +# +# SCOPE BOUND (honest): instruments the device schema actually models. The +# schema today models QICK-class hardware; the first task extends it to the +# bench signal-generator + spectrum-analyzer class (mock). Everything the +# schema does NOT model is out of scope until modeled — stated, never assumed. +schema_version = "1" +id = "ee-test-measurement" +name = "Electrical Engineering / Test & Measurement" +version = 1 +scores = [] +curricula = ["curricula/instrument-bringup/CURRICULUM.md"] +skills = [] # the strumento skill generalizes at exercise time; none shipped yet +payload_schemas = ["PAYLOADS.md"] +benchmarks = ["benchmarks/README.md"] + +[[instruments]] +kind = "strumento device schema (QICK-class, existing)" +coverage = "modeled" + +[[instruments]] +kind = "bench signal generator (mock, schema extension this pass)" +coverage = "modeled-at-first-task" + +[[instruments]] +kind = "spectrum analyzer (mock, schema extension this pass)" +coverage = "modeled-at-first-task" + +[verification] +contract = "VERIFICATION.md" +bindings = [ + "measurement residuals: T1b (deterministic given the recorded artifact; residuals against declared tolerance, honestly noise-bounded)", + "closed-loop recalibration: T1b over the recorded calibration-store chain", + "hardware claims: un-tiered until measured on real instruments (mock runs certify nothing about hardware)", +] + +[first_task] +declaration = "FIRST-TASK.md" +level = "shakedown" + +[onboarding] +primary = "instrument-bringup" +head = "instrument-bringup" + +[corrector] +name = "task-record contract (the strumento seam: the directory is the truth; verdicts re-derived from the record)" +paths = ["FIRST-TASK.md", "VERIFICATION.md"] +integrity = "corrector/integrity.toml" +tier = "open" diff --git a/packages/extension/packs/ee-test-measurement/PAYLOADS.md b/packages/extension/packs/ee-test-measurement/PAYLOADS.md new file mode 100644 index 000000000..41f494c78 --- /dev/null +++ b/packages/extension/packs/ee-test-measurement/PAYLOADS.md @@ -0,0 +1,10 @@ +# EE / test-&-measurement — typed payloads + +The pack's payload vocabulary (additive; the schema package is the arbiter): + +- `measurement-result` — {quantity, value, tolerance, artifact-ref, provenance} — T1b-verified residuals against the recorded artifact +- `calibration-ref-advance` — {ref, parent, config-content-id, per-key-diffs} — the calibration store's content-addressed chain +- `task-record-terminal` — {state, error_kind (script | transport — who fixes them), gates-forwarded, authored_by} — the strumento seam + +Every payload carries `tier` (T1a | T1b | T2 | T3 | un-tiered | pre-tier) — +the enum travels with the claim through every render path. diff --git a/packages/extension/packs/ee-test-measurement/VERIFICATION.md b/packages/extension/packs/ee-test-measurement/VERIFICATION.md new file mode 100644 index 000000000..f78994cbd --- /dev/null +++ b/packages/extension/packs/ee-test-measurement/VERIFICATION.md @@ -0,0 +1,21 @@ +# EE / test-&-measurement — the verification contract + +Tiered and claim-class-bound (spec-20260920-171500 D4): a tier tag asserts +what the instrument establishes, never more. + +| Claim class | Tier | The gate | +|---|---|---| +| Measurement residuals (recorded artifact vs model, declared tolerance) | **T1b** | deterministic given the recorded artifact — the measurement is stochastic, the residual check over the recorded artifact is not; tolerance declared per contract, residuals honestly noise-bounded | +| Closed-loop recalibration (the measure → believe → measure-again chain) | **T1b** | the calibration store's content-addressed chain: each ref advance re-derived from the record, never self-graded | +| Hardware claims (real instruments) | **un-tiered** | mock runs certify NOTHING about hardware — the tier appears only when real instruments are measured; the scope bound holds | +| Bringup state (device model vs manifest) | **T1b** | the strumento device-schema validation (typed errors, `ok: false` carries the error class) | + +**The re-tiering rule:** upgrade on new mechanical evidence (provenance +stamped); downgrade/quarantine when an instrument is invalidated, cascading +to the claims it certified. Historical records render `pre-tier` with +provenance until retro-labeled via the deterministic class→tier table. + +**The task-record contract is the trust boundary:** the directory is the +truth; verdicts forwarded from gates are re-derived from the record before +routing; only declared gates forward at all; forwarding requires a +non-empty `authored_by`. diff --git a/packages/extension/packs/ee-test-measurement/benchmarks/README.md b/packages/extension/packs/ee-test-measurement/benchmarks/README.md new file mode 100644 index 000000000..9cf6c22a5 --- /dev/null +++ b/packages/extension/packs/ee-test-measurement/benchmarks/README.md @@ -0,0 +1,6 @@ +# EE / test-&-measurement — benchmarks + +Declared, not yet populated (honest v1): the benchmark surface lands with +the first-task exercise — the fit-cycle run's measured table (residuals, +wall-clock, retries per fit kind) is the first bench row, cited with its +run ID. Performance and quality claims cite bench rows, never adjectives. diff --git a/packages/extension/packs/ee-test-measurement/corrector/integrity.toml b/packages/extension/packs/ee-test-measurement/corrector/integrity.toml new file mode 100644 index 000000000..db243a1ce --- /dev/null +++ b/packages/extension/packs/ee-test-measurement/corrector/integrity.toml @@ -0,0 +1,6 @@ +# sha256 integrity manifest for the corrector paths (relative to the pack dir). +# Verified at load time (WS1 #369 pattern): a mismatch makes the pack BROKEN. +# Regenerate: shasum -a 256 from the pack dir. +[files] +"FIRST-TASK.md" = "971f100a28f673ccabd73a0662abd58f4dd14c6bcf2d5e4b123febc526891547" +"VERIFICATION.md" = "3d53456c5a60b266cf79e3d0b09e6f634b6bb3439871f415c0730f706d895eb2" diff --git a/packages/extension/packs/ee-test-measurement/curricula/instrument-bringup/CURRICULUM.md b/packages/extension/packs/ee-test-measurement/curricula/instrument-bringup/CURRICULUM.md new file mode 100644 index 000000000..460675001 --- /dev/null +++ b/packages/extension/packs/ee-test-measurement/curricula/instrument-bringup/CURRICULUM.md @@ -0,0 +1,19 @@ +--- +type: curriculum +schema_version: "1" +id: instrument-bringup +version: 1 +description: >- + The skeleton seed for the EE domain's guided path: device-model definition, + mock bringup, the fit cycle, the task-record discipline. Stages are + placeholders until the first-task exercise informs them — a curriculum is + authored from measured practice, not speculation. +stages: [] # deliberately empty: the skeleton exists so the manifest resolves; content lands with exercise +memory_hooks: [] # none yet — hook-card genesis proposes them from recurring questions +--- + +# Instrument bringup (skeleton) + +The guided path from a device description to a verified fit cycle. Authored +from the first-task exercise when it runs — the curriculum grows from what +the loop actually needed, not from a speculative plan. diff --git a/packages/extension/packs/sensing-metrology/FIRST-TASK.md b/packages/extension/packs/sensing-metrology/FIRST-TASK.md new file mode 100644 index 000000000..58145fd6b --- /dev/null +++ b/packages/extension/packs/sensing-metrology/FIRST-TASK.md @@ -0,0 +1,17 @@ +# First task — the shakedown exercise (declared; NOT yet run) + +**The task:** filter-function validation against synthetic noise spectra — +compute the filter response for a representative filter-function family +against declared synthetic spectra, fold the verdict with confidence +intervals (the estimation payload) end-to-end through the loop, tier-labeled +T1b. The synthetic ground truth is declared up front: the harness compares +deterministically over it. + +**Exercise level: shakedown** — loop plumbing on synthetic substrate; it +certifies the pack's mechanics, not the domain's real capability. +Substrate-touching exercise (real NV data) is the tracked next milestone — +the NV testbed is not wired in this build, stated plainly. + +**Exit:** the run exists with a run ID, a tier-labeled verdict with CIs, +and the artifacts intact — then this pack is EXERCISED (shakedown), and this +declaration updates to point at the run. diff --git a/packages/extension/packs/sensing-metrology/PACK.toml b/packages/extension/packs/sensing-metrology/PACK.toml new file mode 100644 index 000000000..933d460db --- /dev/null +++ b/packages/extension/packs/sensing-metrology/PACK.toml @@ -0,0 +1,45 @@ +# The sensing / metrology domain pack (plan-20260920 step 10; amicode #1328). +# The NV-center substrate is the existing exploratory bet (INTENT's +# exploratory tier); this pack seeds the domain: filter functions, noise +# spectroscopy, metrology-grade estimation with confidence intervals. +# SEEDED, not exercised: the first task (filter-function validation against +# synthetic noise spectra) is the shakedown exercise, the next milestone. +schema_version = "1" +id = "sensing-metrology" +name = "Sensing & Metrology" +version = 1 +scores = [] +curricula = ["curricula/noise-spectroscopy/CURRICULUM.md"] +skills = [] # the sensing physics rides the flagship's Lindblad/filter-function material at exercise time +payload_schemas = ["PAYLOADS.md"] +benchmarks = ["benchmarks/README.md"] + +[[instruments]] +kind = "NV-center testbed (data substrate, not wired in this build)" +coverage = "unmodeled-until-hardware" + +[[instruments]] +kind = "synthetic noise spectra (validation substrate)" +coverage = "modeled" + +[verification] +contract = "VERIFICATION.md" +bindings = [ + "filter-function validation vs synthetic spectra: T1b (exit-code + artifact contract — the validation harness over the synthetic ground truth)", + "estimation results (noise spectral density, filter response): T1b with CI folding — the estimation payload carries lo/hi bounds and the method tag", + "real-testbed claims: un-tiered until real data is measured", +] + +[first_task] +declaration = "FIRST-TASK.md" +level = "shakedown" + +[onboarding] +primary = "noise-spectroscopy" +head = "noise-spectroscopy" + +[corrector] +name = "synthetic-spectra ground truth (the validation harness: declared spectra, deterministic comparison)" +paths = ["FIRST-TASK.md", "VERIFICATION.md"] +integrity = "corrector/integrity.toml" +tier = "open" diff --git a/packages/extension/packs/sensing-metrology/PAYLOADS.md b/packages/extension/packs/sensing-metrology/PAYLOADS.md new file mode 100644 index 000000000..6925f95f1 --- /dev/null +++ b/packages/extension/packs/sensing-metrology/PAYLOADS.md @@ -0,0 +1,8 @@ +# Sensing / metrology — typed payloads + +- `estimation-result` — {quantity, value, lo, hi, method, artifact-ref, provenance} — the CI-folding payload (the EstimationResult class) +- `validation-verdict` — {claim, tier (T1b), ground-truth-ref, delta, tolerance} — the synthetic-spectra comparison +- `filter-function-artifact` — {family, parameters, response-array-ref, sha} — the computed response, content-addressed + +Every payload carries `tier` — the enum travels with the claim (un-tiered +for real-testbed claims until measured; pre-tier for historical records). diff --git a/packages/extension/packs/sensing-metrology/VERIFICATION.md b/packages/extension/packs/sensing-metrology/VERIFICATION.md new file mode 100644 index 000000000..8b6d5cf52 --- /dev/null +++ b/packages/extension/packs/sensing-metrology/VERIFICATION.md @@ -0,0 +1,13 @@ +# Sensing / metrology — the verification contract + +Tiered and claim-class-bound (spec-20260920-171500 D4): + +| Claim class | Tier | The gate | +|---|---|---| +| Filter-function validation vs synthetic spectra | **T1b** | exit-code + artifact contract: the validation harness compares computed filter response against the declared synthetic ground truth — deterministic over the declared spectra | +| Estimation results (noise spectral density, filter response, with CI) | **T1b** | the estimation payload carries value + lo/hi bounds + the method tag; the CI folds into the ledger as an EstimationResult-class payload | +| Real-testbed claims (NV hardware) | **un-tiered** | synthetic validation certifies NOTHING about the real testbed; the tier appears only when real data is measured | +| Metrology-grade comparisons (cross-method) | **T1b** | reproducible comparison over recorded artifacts; the better method wins on declared metrics, never vibes | + +**The re-tiering rule** and the `pre-tier`/retro-labeling discipline: as in +the EE contract (spec-20260920 D4, one rule for every domain). diff --git a/packages/extension/packs/sensing-metrology/benchmarks/README.md b/packages/extension/packs/sensing-metrology/benchmarks/README.md new file mode 100644 index 000000000..ddfd7e3a9 --- /dev/null +++ b/packages/extension/packs/sensing-metrology/benchmarks/README.md @@ -0,0 +1,5 @@ +# Sensing / metrology — benchmarks + +Declared, not yet populated (honest v1): the first bench row is the +first-task validation's measured table (filter families × spectra shapes × +deltas vs tolerances), cited with its run ID. diff --git a/packages/extension/packs/sensing-metrology/corrector/integrity.toml b/packages/extension/packs/sensing-metrology/corrector/integrity.toml new file mode 100644 index 000000000..8bdf1679f --- /dev/null +++ b/packages/extension/packs/sensing-metrology/corrector/integrity.toml @@ -0,0 +1,6 @@ +# sha256 integrity manifest for the corrector paths (relative to the pack dir). +# Verified at load time (WS1 #369 pattern): a mismatch makes the pack BROKEN. +# Regenerate: shasum -a 256 from the pack dir. +[files] +"FIRST-TASK.md" = "e75ac866f0a60eaf4852422d0083ab5806b8f66cf1549472b33748f845c94cfa" +"VERIFICATION.md" = "fcfc12df1318989ee034a38d0483a9ca3e36fa3563a989982f550d673f0eba06" diff --git a/packages/extension/packs/sensing-metrology/curricula/noise-spectroscopy/CURRICULUM.md b/packages/extension/packs/sensing-metrology/curricula/noise-spectroscopy/CURRICULUM.md new file mode 100644 index 000000000..40b69584e --- /dev/null +++ b/packages/extension/packs/sensing-metrology/curricula/noise-spectroscopy/CURRICULUM.md @@ -0,0 +1,18 @@ +--- +type: curriculum +schema_version: "1" +id: noise-spectroscopy +version: 1 +description: >- + The skeleton seed for the sensing domain's guided path: filter-function + selection, noise-spectroscopy estimation, metrology-grade comparison. + Stages are placeholders until the first-task exercise informs them — + authored from measured practice, not speculation. +stages: [] +memory_hooks: [] +--- + +# Noise spectroscopy (skeleton) + +The guided path from a sensing question to a CI-bounded estimation. Authored +from the first-task exercise when it runs. diff --git a/packages/extension/src/scores/pack_lint.ts b/packages/extension/src/scores/pack_lint.ts new file mode 100644 index 000000000..e24305b37 --- /dev/null +++ b/packages/extension/src/scores/pack_lint.ts @@ -0,0 +1,133 @@ +// The pack lint (plan-20260920 step 10; amicode #1328) — "seeded" means +// something. The loader (packs.ts) loads and reports; the LINT is the bar the +// acceptance reads: schema validation PLUS reference resolution PLUS the +// domain key set. A pack counts as SEEDED when its manifest validates, every +// path-bearing reference resolves, and it carries the full domain keys +// (curricula, payload schemas, verification contract with claim-class +// bindings, instruments, benchmarks, first task). The flagship quantum pack +// carries no first task by design — it reads FORMALIZED, not seeded (the +// grandfathered status, spec-20260920 D2/D7). +// +// A manifest is cheap; a domain is not (the r3 advisory, verbatim): the +// exercised bar — the first task run end-to-end through the campaign loop +// with a tier-labeled verdict — is a run state, never a manifest state; the +// lint records the declared level (shakedown | substrate) and stops there, +// honestly. +import * as fs from "node:fs"; +import * as path from "node:path"; +import { validateFile } from "@amicode/schema"; +import { parse as parseToml } from "smol-toml"; + +export interface PackLintFinding { + kind: "schema" | "dangling-ref" | "missing-domain-key"; + detail: string; +} + +export interface PackLintResult { + dir: string; + id: string; + seeded: boolean; + exerciseLevel: "shakedown" | "substrate" | undefined; + findings: PackLintFinding[]; +} + +/** The domain key set — required for SEEDED, optional at the schema layer so + * the flagship's WS1 manifest stays valid without them. */ +export const DOMAIN_KEYS = [ + "curricula", + "payload_schemas", + "verification", + "instruments", + "benchmarks", + "first_task", +] as const; + +interface PathBearing { + field: string + p: string +} + +function collectReferences(manifest: Record): PathBearing[] { + const refs: PathBearing[] = [] + const rel = (field: string, v: unknown) => { + if (typeof v === "string") refs.push({ field, p: v }) + } + for (const s of (manifest["scores"] as string[]) ?? []) rel("scores", s) + for (const sk of (manifest["skills"] as { path: string }[]) ?? []) rel("skills", sk.path) + const templates = manifest["templates"] as Record | undefined + if (templates) for (const t of Object.values(templates)) rel("templates", t.path) + const corrector = manifest["corrector"] as { paths: string[]; integrity: string } | undefined + if (corrector) { + for (const c of corrector.paths ?? []) rel("corrector.paths", c) + rel("corrector.integrity", corrector.integrity) + } + for (const c of (manifest["curricula"] as string[]) ?? []) rel("curricula", c) + for (const c of (manifest["payload_schemas"] as string[]) ?? []) rel("payload_schemas", c) + for (const b of (manifest["benchmarks"] as string[]) ?? []) rel("benchmarks", b) + const verification = manifest["verification"] as { contract: string } | undefined + if (verification) rel("verification.contract", verification.contract) + const firstTask = manifest["first_task"] as { declaration: string } | undefined + if (firstTask) rel("first_task.declaration", firstTask.declaration) + return refs +} + +/** Lint one pack dir. Never throws: findings are the interface. */ +export function lintPackDir(dir: string): PackLintResult { + const manifestPath = path.join(dir, "PACK.toml") + const findings: PackLintFinding[] = [] + + if (!fs.existsSync(manifestPath)) { + return { dir, id: "", seeded: false, exerciseLevel: undefined, findings: [{ kind: "schema", detail: "no PACK.toml" }] } + } + + const validation = validateFile(manifestPath, "pack") + if (!validation.ok) { + findings.push({ kind: "schema", detail: validation.errors.join("; ") }) + } + + let manifest: Record = {} + try { + manifest = parseToml(fs.readFileSync(manifestPath, "utf8")) as Record + } catch (e) { + findings.push({ kind: "schema", detail: `unparsable TOML: ${e instanceof Error ? e.message : String(e)}` }) + return { dir, id: "", seeded: false, exerciseLevel: undefined, findings } + } + + // Reference resolution: every path-bearing field resolves, dangling = named. + for (const { field, p } of collectReferences(manifest)) { + const resolved = path.resolve(dir, p) + if (!fs.existsSync(resolved)) { + findings.push({ kind: "dangling-ref", detail: `${field}: ${p} does not resolve (${resolved})` }) + } + } + + // The seeded check: full domain key set AND a clean manifest. + for (const key of DOMAIN_KEYS) { + if (!(key in manifest)) { + findings.push({ kind: "missing-domain-key", detail: `${key} absent — not a seeded domain pack (formalized/legacy packs omit it by design)` }) + } + } + const exerciseLevel = (manifest["first_task"] as { level?: "shakedown" | "substrate" } | undefined)?.level + + return { + dir, + id: String(manifest["id"] ?? ""), + seeded: findings.length === 0, + exerciseLevel, + findings, + } +} + +/** Lint every pack under the ordered roots — the acceptance's validator. */ +export function lintPacks(roots: string[]): PackLintResult[] { + const results: PackLintResult[] = [] + for (const root of roots) { + if (!fs.existsSync(root)) continue + for (const entry of fs.readdirSync(root, { withFileTypes: true })) { + if (!entry.isDirectory()) continue + const dir = path.join(root, entry.name) + if (fs.existsSync(path.join(dir, "PACK.toml"))) results.push(lintPackDir(dir)) + } + } + return results +} diff --git a/packages/extension/test/pack_lint.test.ts b/packages/extension/test/pack_lint.test.ts new file mode 100644 index 000000000..ed088a3f6 --- /dev/null +++ b/packages/extension/test/pack_lint.test.ts @@ -0,0 +1,104 @@ +// The pack-lint tests (plan-20260920 step 10; amicode #1328) — "seeded" +// means something, and these tests are the acceptance's own validator: +// they lint the REAL bundled packs. The EE and sensing packs must be SEEDED +// (clean manifest, every reference resolving, the full domain key set); the +// flagship quantum pack must read FORMALIZED, not seeded (no first task, by +// the grandfathered design) — and its references must still all resolve. +import { describe, it, expect } from "vitest" +import * as fs from "node:fs" +import * as os from "node:os" +import * as path from "node:path" +import { lintPacks, lintPackDir, DOMAIN_KEYS } from "../src/scores/pack_lint.js" + +const bundledPacks = path.resolve(import.meta.dirname, "..", "packs") + +describe("the pack lint", () => { + it("lints the bundled packs: EE and sensing are SEEDED; the flagship is formalized, not seeded", () => { + const results = lintPacks([bundledPacks]) + const byId = new Map(results.map((r) => [r.id, r])) + expect(byId.get("ee-test-measurement")?.seeded).toBe(true) + expect(byId.get("sensing-metrology")?.seeded).toBe(true) + + const flagship = byId.get("quantum-control") + expect(flagship).toBeDefined() + expect(flagship?.seeded).toBe(false) // formalized, not seeded — the grandfathered design + // its missing-domain-key findings are the EXPECTED marker of formalized status… + const missing = flagship?.findings.filter((f) => f.kind === "missing-domain-key") + expect(missing?.length).toBe(DOMAIN_KEYS.length) + // schema-clean against the extended schema (the additive extension does not break the WS1 manifest) + expect(flagship?.findings.filter((f) => f.kind === "schema")).toEqual([]) + // KNOWN REAL FINDING, recorded: the flagship's skill paths are SNAPSHOT-LAYOUT + // paths (../../skills/transmon resolves in the VSIX/extension-snapshot layout + // where all skills coexist — the repo tree carries only the meta skills), so + // in-repo the lint flags them dangling. The step-14 formalization reconciles + // the path layout; until then this is the honest finding, baked into the test + // so the reconciliation cannot land silently. + const dangling = flagship?.findings.filter((f) => f.kind === "dangling-ref") + expect(dangling?.every((f) => f.detail.startsWith("skills:"))).toBe(true) + }) + + it("the seeded packs declare their exercise level honestly (shakedown, not substrate)", () => { + for (const id of ["ee-test-measurement", "sensing-metrology"]) { + const r = lintPacks([bundledPacks]).find((x) => x.id === id) + expect(r?.exerciseLevel).toBe("shakedown") // declared; NOT exercised — a run state, never a manifest state + } + }) + + it("a dangling reference fails the lint, named", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pack-lint-")) + fs.writeFileSync( + path.join(dir, "PACK.toml"), + [ + 'schema_version = "1"', + 'id = "broken"', + 'name = "Broken"', + "scores = []", + "[onboarding]", + 'primary = "none"', + "[corrector]", + 'name = "gate"', + 'paths = ["nope.md"]', + 'integrity = "nope.toml"', + "curricula = []", + "payload_schemas = []", + "instruments = []", + "benchmarks = []", + "[first_task]", + 'declaration = "also-nope.md"', + 'level = "shakedown"', + "", + ].join("\n"), + ) + const r = lintPackDir(dir) + expect(r.seeded).toBe(false) + const dangling = r.findings.filter((f) => f.kind === "dangling-ref") + expect(dangling.some((f) => f.detail.includes("corrector.paths"))).toBe(true) + expect(dangling.some((f) => f.detail.includes("first_task.declaration"))).toBe(true) + }) + + it("a manifest missing the domain keys reads formalized (the seeded-marker findings, not errors)", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pack-lint-2-")) + fs.writeFileSync( + path.join(dir, "PACK.toml"), + [ + 'schema_version = "1"', + 'id = "minimal"', + 'name = "Minimal"', + "scores = []", + "[onboarding]", + 'primary = "none"', + "[corrector]", + 'name = "gate"', + 'paths = ["gate.md"]', + 'integrity = "integrity.toml"', + "", + ].join("\n"), + ) + fs.writeFileSync(path.join(dir, "gate.md"), "# the gate contract\n") + fs.writeFileSync(path.join(dir, "integrity.toml"), "schema_version = \"1\"\n") + const r = lintPackDir(dir) + expect(r.seeded).toBe(false) + // schema-clean and reference-clean — the ONLY findings are the domain-key markers + expect(r.findings.every((f) => f.kind === "missing-domain-key")).toBe(true) + }) +}) diff --git a/packages/schema/schemas/pack.schema.json b/packages/schema/schemas/pack.schema.json index 296a60c0e..f35abf091 100644 --- a/packages/schema/schemas/pack.schema.json +++ b/packages/schema/schemas/pack.schema.json @@ -2,33 +2,51 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://amico.harmoniqs.co/schema/pack/v1", "title": "amico pack manifest", - "description": "The unit of generality for a domain pack (autoresearch studio WS1, #369): one manifest per domain bundling skills, a template registry, the corrector with its integrity manifest, the catalog schema, the onboarding score, and an eval corpus pointer. Owned by the schema package; one validator consumed by the extension, the CLI, and CI. A score is one FIELD of a pack, not a peer of it. The per-entry tier (open vs an entitlement bundle id) encodes the open-core boundary: flowing a feature back to open-source is a manifest edit plus a content move, not a migration. Pack ids are deliberately NOT an enum — the taxonomy is manifest data, revisable without a schema version bump.", + "description": "The unit of generality for a domain pack (autoresearch studio WS1, #369): one manifest per domain bundling skills, a template registry, the corrector with its integrity manifest, the catalog schema, the onboarding score, and an eval corpus pointer. Owned by the schema package; one validator consumed by the extension, the CLI, and CI. A score is one FIELD of a pack, not a peer of it. The per-entry tier (open vs an entitlement bundle id) encodes the open-core boundary: flowing a feature back to open-source is a manifest edit plus a content move, not a migration. Pack ids are deliberately NOT an enum \u2014 the taxonomy is manifest data, revisable without a schema version bump.", "type": "object", "additionalProperties": false, - "required": ["schema_version", "id", "name", "scores", "onboarding", "corrector"], + "required": [ + "schema_version", + "id", + "name", + "scores", + "onboarding", + "corrector" + ], "properties": { - "schema_version": { "enum": ["1"] }, + "schema_version": { + "enum": [ + "1" + ] + }, "id": { "type": "string", "pattern": "^[a-z0-9-]+$", "description": "pack id, kebab-case, free-form (quantum-control, qec, calibration, ...)" }, - "name": { "type": "string", "minLength": 1 }, + "name": { + "type": "string", + "minLength": 1 + }, "version": { "type": "integer", "minimum": 1, - "description": "pack content version — bump when content moves between tiers" + "description": "pack content version \u2014 bump when content moves between tiers" }, "scores": { "type": "array", - "minItems": 1, - "items": { "type": "string", "minLength": 1 }, - "description": "score dirs this pack contributes, relative to the pack dir" + "items": { + "type": "string", + "minLength": 1 + }, + "description": "Score/curriculum dirs (paths relative to the pack dir). A seeded domain pack may carry none yet \u2014 the curriculum skeleton rides `curricula` and composed interviews come at maturity." }, "onboarding": { "type": "object", "additionalProperties": false, - "required": ["primary"], + "required": [ + "primary" + ], "properties": { "primary": { "type": "string", @@ -44,32 +62,48 @@ }, "skills": { "type": "array", - "items": { "$ref": "#/$defs/tieredPath" }, + "items": { + "$ref": "#/$defs/tieredPath" + }, "description": "skill dirs this pack contributes, relative to the pack dir" }, "templates": { "type": "object", - "additionalProperties": { "$ref": "#/$defs/tieredPath" }, - "description": "template registry: name → tiered path (e.g. solve → the vetted template)" + "additionalProperties": { + "$ref": "#/$defs/tieredPath" + }, + "description": "template registry: name \u2192 tiered path (e.g. solve \u2192 the vetted template)" }, "corrector": { "type": "object", "additionalProperties": false, - "required": ["name", "paths", "integrity"], + "required": [ + "name", + "paths", + "integrity" + ], "properties": { - "name": { "type": "string", "minLength": 1 }, + "name": { + "type": "string", + "minLength": 1 + }, "paths": { "type": "array", "minItems": 1, - "items": { "type": "string", "minLength": 1 }, - "description": "the gate artifacts — must resolve OUTSIDE agent-editable trees (a load-time property, not a convention)" + "items": { + "type": "string", + "minLength": 1 + }, + "description": "the gate artifacts \u2014 must resolve OUTSIDE agent-editable trees (a load-time property, not a convention)" }, "integrity": { "type": "string", "minLength": 1, "description": "path to the sha256 integrity manifest covering every named path, relative to the pack dir" }, - "tier": { "$ref": "#/$defs/tier" } + "tier": { + "$ref": "#/$defs/tier" + } } }, "catalog_schema": { @@ -81,20 +115,118 @@ "type": "string", "minLength": 1, "description": "pointer to the pack's eval corpus (e.g. a Prova corpus ref)" + }, + "curricula": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Curriculum paths (relative to the pack dir). The curricula substrate is core; the content is pack-declared." + }, + "payload_schemas": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Typed-payload schema references (paths, relative to the pack dir)." + }, + "verification": { + "type": "object", + "required": [ + "contract", + "bindings" + ], + "properties": { + "contract": { + "type": "string", + "description": "Path to the per-domain verification contract (claim-class bindings, tier assignments)." + }, + "bindings": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Claim-class bindings in short form (e.g. 'simulation: model-trajectory claims at T1a; hardware claims un-tiered') \u2014 the full contract doc carries the details." + } + }, + "description": "The verification-contract reference with claim-class bindings (the tiering is honest: tiers assert what the instrument establishes)." + }, + "instruments": { + "type": "array", + "items": { + "type": "object", + "required": [ + "kind" + ], + "properties": { + "kind": { + "type": "string" + }, + "coverage": { + "type": "string", + "description": "modeled | modeled-at-first-task | unmodeled \u2014 the scope bound, stated not assumed" + } + } + }, + "description": "The instrument list with the scope bound: instruments the schema actually models \u2014 each entry states its coverage." + }, + "benchmarks": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Benchmark paths (relative to the pack dir), each resolving at lint time." + }, + "first_task": { + "type": "object", + "required": [ + "declaration", + "level" + ], + "properties": { + "declaration": { + "type": "string", + "description": "Path to the first-task declaration doc." + }, + "level": { + "type": "string", + "enum": [ + "shakedown", + "substrate" + ], + "description": "shakedown = the loop plumbing end-to-end (mock/synthetic); substrate = touching real domain substrate. The exercised-to-maturity ladder is tracked per manifest." + } + }, + "description": "The first-task declaration \u2014 a pack is exercised when it has run end-to-end with a tier-labeled verdict." } }, "$defs": { "tier": { - "anyOf": [{ "const": "open" }, { "type": "string", "pattern": "^[a-z0-9-]+$" }], + "anyOf": [ + { + "const": "open" + }, + { + "type": "string", + "pattern": "^[a-z0-9-]+$" + } + ], "description": "open (default when absent), or the entitlement bundle id gating this entry" }, "tieredPath": { "type": "object", "additionalProperties": false, - "required": ["path"], + "required": [ + "path" + ], "properties": { - "path": { "type": "string", "minLength": 1 }, - "tier": { "$ref": "#/$defs/tier" } + "path": { + "type": "string", + "minLength": 1 + }, + "tier": { + "$ref": "#/$defs/tier" + } } } } diff --git a/packages/schema/test/pack.test.ts b/packages/schema/test/pack.test.ts index a214d8727..d5734047a 100644 --- a/packages/schema/test/pack.test.ts +++ b/packages/schema/test/pack.test.ts @@ -51,8 +51,12 @@ describe("the pack kind", () => { it("requires the onboarding primary score — a score is a FIELD of a pack", () => { expect(validate(pack({ onboarding: { head: "overture" } }), "pack").ok).toBe(false); }); - it("requires a non-empty scores list", () => { - expect(validate(pack({ scores: [] }), "pack").ok).toBe(false); + it("allows an empty scores list — the seeded-domain-pack contract (plan-20260920 step 10)", () => { + // A seeded domain pack legitimately carries no composed scores yet: the + // curriculum skeleton rides `curricula` and composed interviews come at + // maturity. The flagship keeps its non-empty list; minItems was the + // quantum-era single-pack contract, relaxed deliberately for domains. + expect(validate(pack({ scores: [] }), "pack").ok).toBe(true); }); it("requires corrector.integrity — the threshold condition is a load-time property, not a convention", () => { const c = { ...(pack().corrector as object) } as Record;