-
Notifications
You must be signed in to change notification settings - Fork 1
0.1c — amico-validate CLI + CI fast-tier schema gate (closes #17) #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,19 @@ | ||
| import { build } from 'esbuild' | ||
| import { chmodSync } from 'node:fs' | ||
|
|
||
| // The library is consumed as TS source (main = src/index.ts; consumers bundle it | ||
| // via their own esbuild). We still bundle here as a build-time smoke check that | ||
| // the dep graph (ajv + ajv-formats + the JSON schemas) bundles cleanly into a | ||
| // single ESM module — the same way the extension/CLI will inline it. | ||
| // via their own esbuild). We bundle two artifacts here: | ||
| // - dist/index.js: a smoke check that the dep graph (ajv + ajv-formats + the | ||
| // JSON schemas) bundles cleanly into a single ESM module. | ||
| // - dist/amico-validate.js: the standalone validator CLI (0.1c). | ||
| const common = { bundle: true, platform: 'node', target: 'node20', format: 'esm', sourcemap: true, logLevel: 'info' } | ||
|
|
||
| await build({ ...common, entryPoints: ['src/index.ts'], outfile: 'dist/index.js' }) | ||
|
|
||
| await build({ | ||
| entryPoints: ['src/index.ts'], | ||
| bundle: true, | ||
| platform: 'node', | ||
| target: 'node20', | ||
| format: 'esm', | ||
| outfile: 'dist/index.js', | ||
| sourcemap: true, | ||
| logLevel: 'info', | ||
| ...common, | ||
| entryPoints: ['src/cli.ts'], | ||
| outfile: 'dist/amico-validate.js', | ||
| banner: { js: '#!/usr/bin/env node' }, | ||
| }) | ||
| chmodSync('dist/amico-validate.js', 0o755) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| #!/usr/bin/env bash | ||
| # Thin launcher: resolve node, exec the bundled validator. Committed (exists at | ||
| # install time) so the `amico-validate` bin links on a clean install — the dist | ||
| # bundle is built by `pnpm run build`, not present until then. Mirrors amico-run. | ||
| set -euo pipefail | ||
| SOURCE="${BASH_SOURCE[0]}" | ||
| while [ -h "$SOURCE" ]; do # resolve symlink chains (node_modules/.bin) | ||
| DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)" | ||
| SOURCE="$(readlink "$SOURCE")" | ||
| [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" | ||
| done | ||
| DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)" | ||
| if ! command -v node >/dev/null 2>&1; then | ||
| echo "amico-validate: node >= 20 not found on PATH (install node or fix PATH)" >&2 | ||
| exit 64 | ||
| fi | ||
| BUNDLE="$DIR/../dist/amico-validate.js" | ||
| if [ ! -f "$BUNDLE" ]; then | ||
| echo "amico-validate: bundle missing ($BUNDLE) — run 'pnpm --filter @amicode/schema build'" >&2 | ||
| exit 64 | ||
| fi | ||
| exec node "$BUNDLE" "$@" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| // amico-validate — validate an amico config/artifact file against the shared | ||
| // schema set. A thin CLI over @amicode/schema's validate() — NO parallel | ||
| // validation logic. Exit 0 = valid, 64 = invalid or usage error (mirrors | ||
| // amico-run's config-error exit convention, Q85). | ||
| import { validateFile, kindForFilename, SCHEMA_KINDS, type SchemaKind } from "./index.js"; | ||
|
|
||
| const USAGE = `usage: amico-validate <file> [--schema <kind>] | ||
| <file> role is inferred from its name (run.toml, result.toml, lab.toml, FINISHED); | ||
| pass --schema for solvespec / catalog-entry or any non-standard filename. | ||
| kinds: ${SCHEMA_KINDS.join(", ")} | ||
| exit: 0 valid · 64 invalid or usage error`; | ||
|
|
||
| export function main(argv: string[]): number { | ||
| let file: string | undefined; | ||
| let schema: string | undefined; | ||
| for (let i = 0; i < argv.length; i++) { | ||
| const a = argv[i]; | ||
| if (a === "--help" || a === "-h") { console.log(USAGE); return 0; } | ||
| if (a === "--schema") { | ||
| schema = argv[++i]; | ||
| if (schema === undefined) { console.error(`amico-validate: --schema requires a value\n${USAGE}`); return 64; } | ||
| } else if (a.startsWith("-")) { | ||
| console.error(`amico-validate: unknown flag ${a}\n${USAGE}`); return 64; | ||
| } else if (file !== undefined) { | ||
| console.error(`amico-validate: multiple files given\n${USAGE}`); return 64; | ||
| } else { | ||
| file = a; | ||
| } | ||
| } | ||
| if (file === undefined) { console.error(`amico-validate: no file given\n${USAGE}`); return 64; } | ||
|
|
||
| const inferred = schema ?? kindForFilename(file); | ||
| if (inferred === undefined) { | ||
| console.error(`amico-validate: cannot infer schema for ${file} — pass --schema <${SCHEMA_KINDS.join("|")}>`); | ||
| return 64; | ||
| } | ||
| if (!SCHEMA_KINDS.includes(inferred as SchemaKind)) { | ||
| console.error(`amico-validate: unknown schema "${inferred}" (kinds: ${SCHEMA_KINDS.join(", ")})`); | ||
| return 64; | ||
| } | ||
| const kind = inferred as SchemaKind; | ||
|
|
||
| const r = validateFile(file, kind); | ||
| if (r.ok) { console.log(`OK ${file} (${kind})`); return 0; } | ||
| console.error(`INVALID ${file} (${kind}):`); | ||
| for (const e of r.errors) console.error(` ${e}`); | ||
| return 64; | ||
| } | ||
|
|
||
| process.exit(main(process.argv.slice(2))); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import { describe, it, expect, beforeAll } from "vitest"; | ||
| import { execFileSync } from "node:child_process"; | ||
| import { copyFileSync, mkdtempSync } from "node:fs"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { dirname, join } from "node:path"; | ||
| import { tmpdir } from "node:os"; | ||
|
|
||
| const here = dirname(fileURLToPath(import.meta.url)); | ||
| const pkg = join(here, ".."); | ||
| const BUNDLE = join(pkg, "dist", "amico-validate.js"); | ||
| const validDir = join(here, "fixtures", "valid"); | ||
| const invalidDir = join(here, "fixtures", "invalid"); | ||
| const KINDS = ["run", "result", "lab", "solvespec", "catalog-entry", "finished"]; | ||
|
|
||
| beforeAll(() => { execFileSync("node", [join(pkg, "esbuild.config.mjs")], { cwd: pkg }); }); | ||
|
|
||
| function run(args: string[]): { code: number; stdout: string; stderr: string } { | ||
| try { | ||
| const stdout = execFileSync("node", [BUNDLE, ...args], { encoding: "utf8" }); | ||
| return { code: 0, stdout, stderr: "" }; | ||
| } catch (e) { | ||
| const err = e as { status?: number; stdout?: string; stderr?: string }; | ||
| return { code: err.status ?? 1, stdout: err.stdout ?? "", stderr: err.stderr ?? "" }; | ||
| } | ||
| } | ||
|
|
||
| describe("amico-validate CLI", () => { | ||
| it("valid fixtures → exit 0 (every kind, via --schema)", () => { | ||
| for (const k of KINDS) { | ||
| const r = run([join(validDir, `${k}.toml`), "--schema", k]); | ||
| expect(r.code, `${k}: ${r.stderr}`).toBe(0); | ||
| } | ||
| }); | ||
| it("invalid fixtures → exit 64 + field-precise stderr (every kind)", () => { | ||
| for (const k of KINDS) { | ||
| const r = run([join(invalidDir, `${k}.toml`), "--schema", k]); | ||
| expect(r.code, k).toBe(64); | ||
| expect(r.stderr).toContain("INVALID"); | ||
| } | ||
| }); | ||
| it("a committed WRONG-TYPE fixture fails field-precise (AC7 class matrix is self-contained)", () => { | ||
| const r = run([join(invalidDir, "result-wrongtype.toml"), "--schema", "result"]); | ||
| expect(r.code).toBe(64); | ||
| expect(r.stderr).toContain("/fidelity: must be number"); | ||
| }); | ||
| it("file-role resolution by basename for the fixed-filename schemas (no --schema)", () => { | ||
| expect(run([join(validDir, "run.toml")]).code).toBe(0); | ||
| expect(run([join(validDir, "result.toml")]).code).toBe(0); | ||
| expect(run([join(invalidDir, "result.toml")]).code).toBe(64); // missing schema_version | ||
| }); | ||
| it("FINISHED resolves by exact basename (no extension)", () => { | ||
| const f = join(mkdtempSync(join(tmpdir(), "fin-")), "FINISHED"); | ||
| copyFileSync(join(validDir, "finished.toml"), f); | ||
| expect(run([f]).code).toBe(0); | ||
| }); | ||
| it("a non-filename schema without --schema cannot infer → 64", () => { | ||
| const r = run([join(validDir, "solvespec.toml")]); // solvespec.toml is not a canonical name | ||
| expect(r.code).toBe(64); | ||
| expect(r.stderr).toContain("cannot infer"); | ||
| }); | ||
| it("field-precise stderr names the offending key + path", () => { | ||
| const r = run([join(invalidDir, "lab.toml"), "--schema", "lab"]); | ||
| expect(r.stderr).toContain("/transmon/levels"); | ||
| }); | ||
| it("usage / bad-arg errors exit 64", () => { | ||
| expect(run([]).code).toBe(64); // no file | ||
| expect(run(["a.toml", "b.toml"]).code).toBe(64); // multiple files | ||
| expect(run(["f.toml", "--schema", "bogus"]).code).toBe(64); // unknown schema | ||
| expect(run(["f.toml", "--nope"]).code).toBe(64); // unknown flag | ||
| }); | ||
| it("--help exits 0", () => expect(run(["--help"]).code).toBe(0)); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| schema_version = "1" | ||
| run_id = "r1" | ||
| lab_id = "default" | ||
| fidelity = 0.99 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| status = "halfway" | ||
| exit_code = 0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| schema_version = "1" | ||
| [lab] | ||
| name = "x" | ||
| [transmon] | ||
| omega_GHz = 5.0 | ||
| delta_GHz = 0.2 | ||
| levels = 99 | ||
| drive_max_GHz = 0.2 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| schema_version = "1" | ||
| fidelity = "high" | ||
| iterations = 60 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| fidelity = 0.99 | ||
| iterations = 60 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| schema_version = "1" | ||
| script_path = "/s.jl" | ||
| lab = "default" | ||
| lab_id = "default" | ||
| created_at = "2026-06-15T00:00:00Z" | ||
| orchestrator_version = "0.1.0" | ||
| [julia] | ||
| binary = "julia" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| schema_version = "1" | ||
| script_path = "/s.jl" | ||
| lab_id = "default" | ||
| rogue_key = true |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[important] the gate covers lab/manifest/result/FINISHED but not
solvespecorcatalog-entry— AC5 requires the non-filename schemas via--schema, and the DoD is "all five." Both fixtures are committed; add them:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done — the gate now also validates
solvespec+catalog-entryvia--schema(the committed valid fixtures), so all five are covered (AC5 / DoD).