From 7ffd0d56187594a8bdac21b2bacde8645f08da1f Mon Sep 17 00:00:00 2001 From: iperev Date: Sun, 26 Jul 2026 17:47:26 +0200 Subject: [PATCH] fix: close audit remediation gaps --- .github/workflows/ci.yml | 14 + .github/workflows/release.yml | 30 +- ADOPTION.md | 13 +- README.md | 109 +- cmd/agentic-proofkit/main.go | 12 +- .../audit-remediation-design.md | 3182 ++++ docs/implementation/audit-remediation-plan.md | 4077 +++++ docs/proofkit-contract-map.md | 5 +- docs/release-process.md | 11 +- .../overview.md | 3 +- .../requirements.v1.json | 2 +- .../proofkit-package-boundary/overview.md | 63 +- .../requirements.v1.json | 18 +- .../proofkit-spec-proof-core/overview.md | 48 +- .../requirements.v1.json | 26 +- .../proofkit-supply-chain-quality/overview.md | 122 +- .../requirements.v1.json | 43 +- internal/app/adoption_commands.go | 39 +- internal/app/agent_route_command.go | 7 +- internal/app/app.go | 45 +- internal/app/app_test.go | 28 + internal/app/cli_abi_test.go | 1077 +- internal/app/cli_contract_test.go | 662 +- .../app/cli_output_witness_contract_test.go | 510 + internal/app/command_contract_generated.go | 92 + internal/app/command_coverage_test.go | 2 +- internal/app/command_descriptors.go | 46 +- internal/app/command_family_catalog.go | 9 +- internal/app/command_family_catalog_test.go | 84 + internal/app/command_help.go | 46 +- internal/app/command_registry.go | 11 +- internal/app/envelope_commands.go | 15 +- internal/app/invocation_profile_test.go | 889 ++ internal/app/planning_changed_command.go | 4 + internal/app/requirement_commands.go | 205 +- internal/app/requirement_context_cli_test.go | 320 +- internal/app/stack_command.go | 3 +- internal/app/usage.go | 13 +- .../adoptioncontract/adoptioncontract.go | 49 +- .../command/adoptiondoctor/adoptiondoctor.go | 45 +- .../adoptiondoctor/adoptiondoctor_test.go | 91 + .../adoptionworkflow/adoptionworkflow.go | 83 +- internal/command/agentroute/agentroute.go | 21 +- internal/command/agentroute/envelope.go | 10 +- .../branchauthority/branchauthority.go | 27 +- .../branchauthority/branchauthority_test.go | 10 +- .../command/changedpathset/changedpathset.go | 28 +- .../changedpathset/changedpathset_test.go | 28 +- .../deployment_evidence_admission.go | 34 +- .../externalconsumer/externalconsumer.go | 22 +- .../externalconsumer/externalconsumer_test.go | 14 +- internal/command/gradualadoption/bootstrap.go | 26 +- .../gradualadoption/bootstrap_envelope.go | 13 +- .../bootstrap_materialization.go | 13 +- .../gradualadoption/gradualadoption_test.go | 111 +- .../package_runtime_dependency_test.go | 32 +- .../packageruntimedependency.go | 36 +- .../command/pilotadmission/pilotadmission.go | 26 + .../pilotadmission/pilotadmission_test.go | 71 + .../projectstructure/materialization.go | 8 +- .../projectstructure/projectstructure_test.go | 51 + internal/command/projectstructure/scaffold.go | 23 +- internal/command/publicapi/public_api.go | 319 +- internal/command/publicapi/public_api_test.go | 341 +- .../readinesscloseout/readinesscloseout.go | 52 +- .../readinesscloseout_test.go | 115 +- .../registryconsumer/registryconsumer.go | 24 +- .../registryconsumer/registryconsumer_test.go | 27 +- .../registry_consumer_input_compose.go | 24 +- .../registry_consumer_input_compose_test.go | 13 +- .../repo_profile_admission.go | 8 +- .../repo_profile_admission_test.go | 65 +- .../requirementbinding/requirementbinding.go | 81 +- .../requirementbrowser/assets/workspace.css | 279 +- .../requirementbrowser/assets/workspace.js | 169 +- .../requirementbrowser/handoff_bounds_test.go | 2 +- .../requirementbrowser/http_handler.go | 30 +- .../requirementbrowser/requirementbrowser.go | 2 +- .../requirementbrowser_test.go | 19 + internal/command/requirementbrowser/server.go | 30 +- .../command/requirementbrowser/server_test.go | 47 + .../command/requirementbrowser/v1_adapter.go | 5 + .../command/requirementbrowser/workspace.go | 67 +- .../requirementbrowser/workspace_test.go | 195 +- .../command/requirementcontext/compose.go | 20 +- internal/command/requirementcontext/model.go | 103 +- .../requirementcontext_test.go | 99 +- .../requirementcontext/slice_closure_test.go | 2 +- .../command/requirementcontext/v1_adapter.go | 46 + .../requirementcoverageview/envelope.go | 6 +- .../requirementcoverageview.go | 2 +- .../command/requirementcoverageview/types.go | 2 + .../requirementdiff/output_admission.go | 29 +- .../requirementdiff/requirementdiff.go | 45 +- .../requirementdiff/requirementdiff_test.go | 97 +- .../command/requirementdiff/v1_adapter.go | 53 + .../requirementgraph/requirementgraph_test.go | 27 +- .../specproofbundleadmission_test.go | 30 +- .../stackpreset/preset_ids_generated.go | 6 + internal/command/stackpreset/stackpreset.go | 50 +- .../command/stackpreset/stackpreset_test.go | 5 + internal/kernel/cliexec/cliexec.go | 97 +- internal/kernel/cliexec/cliexec_test.go | 189 +- .../testsupport/browserfixture/fixture.go | 6 +- .../browserproofverify/artifact_paths.go | 10 + .../browserproofverify/artifact_paths_test.go | 31 + internal/tools/browserproofverify/main.go | 5 +- .../commandcontractgen/condition_model.go | 131 + internal/tools/commandcontractgen/main.go | 967 ++ .../tools/commandcontractgen/main_test.go | 786 + internal/tools/coveragemetrics/main.go | 377 +- internal/tools/coveragemetrics/main_test.go | 363 +- .../tools/packageartifactrecord/record.go | 26 +- .../packageartifactrecord/record_test.go | 27 + internal/tools/packagebuild/main.go | 5 + internal/tools/packagebuild/main_test.go | 11 + internal/tools/packageverify/main.go | 1779 ++- internal/tools/packageverify/main_test.go | 842 +- .../tools/pythonpackage/continuation_test.go | 89 + .../pythonpackage/exec_lifecycle_test.go | 45 +- internal/tools/pythonpackage/metadata_test.go | 110 + internal/tools/pythonpackage/verify.go | 353 + internal/tools/releasechange/record.go | 108 +- internal/tools/releasechange/record_test.go | 335 +- .../tools/releasecloseoutinput/main_test.go | 8 +- internal/tools/releasesbom/main.go | 314 +- internal/tools/releasesbom/main_test.go | 102 + package-lock.json | 49 +- package.json | 13 +- playwright.config.mjs | 8 +- proofkit/cli-contract.v2.json | 12853 +++++++++++++++- proofkit/receipt-producer-policy.json | 2 +- proofkit/requirement-bindings.json | 974 +- proofkit/witness-plan.json | 68 +- python/agentic_proofkit/cli.py | 10 +- release/change-record.v1.json | 49 - release/change-record.v2.json | 130 + scripts/browser-proof-inputs.test.mjs | 378 + scripts/source-hygiene.mjs | 21 +- scripts/source_hygiene_test.go | 74 +- scripts/validate-self-hosting-receipts.go | 21 +- .../validate-self-hosting-receipts_test.go | 262 +- .../workflow_browser_runtime_oracle_test.go | 98 + scripts/workflow_oracle_support_test.go | 538 + scripts/workflow_package_gate_oracle_test.go | 1945 ++- .../workflow_runtime_preconditions_test.go | 38 + .../workflow_security_scanner_oracles_test.go | 346 + scripts/workflow_source_oracles_test.go | 437 + tests/browser/axe-harness.mjs | 75 + tests/browser/workspace.spec.mjs | 1315 +- 150 files changed, 39928 insertions(+), 2185 deletions(-) create mode 100644 docs/implementation/audit-remediation-design.md create mode 100644 docs/implementation/audit-remediation-plan.md create mode 100644 internal/app/cli_output_witness_contract_test.go create mode 100644 internal/app/command_contract_generated.go create mode 100644 internal/app/invocation_profile_test.go create mode 100644 internal/command/requirementbrowser/requirementbrowser_test.go create mode 100644 internal/command/requirementbrowser/v1_adapter.go create mode 100644 internal/command/requirementcontext/v1_adapter.go create mode 100644 internal/command/requirementdiff/v1_adapter.go create mode 100644 internal/command/stackpreset/preset_ids_generated.go create mode 100644 internal/tools/commandcontractgen/condition_model.go create mode 100644 internal/tools/commandcontractgen/main.go create mode 100644 internal/tools/commandcontractgen/main_test.go create mode 100644 internal/tools/pythonpackage/continuation_test.go delete mode 100644 release/change-record.v1.json create mode 100644 release/change-record.v2.json create mode 100644 scripts/workflow_browser_runtime_oracle_test.go create mode 100644 scripts/workflow_oracle_support_test.go create mode 100644 scripts/workflow_runtime_preconditions_test.go create mode 100644 scripts/workflow_security_scanner_oracles_test.go create mode 100644 scripts/workflow_source_oracles_test.go create mode 100644 tests/browser/axe-harness.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a72a148..fb3af76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,6 +81,9 @@ jobs: - name: Verify Go formatting run: npm run go:fmt + - name: Verify generated command contracts + run: npm run command-contract:check + - name: Verify generated command family catalog run: npm run command-family:check @@ -188,6 +191,17 @@ jobs: - name: Run browser proof run: npm run browser:check + - name: Upload browser failure diagnostics + if: ${{ failure() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: browser-runtime-diagnostics-${{ github.sha }}-${{ github.run_attempt }} + path: | + artifacts/browser-run-*/playwright-report.json + artifacts/browser-run-*/test-results + if-no-files-found: error + retention-days: 14 + - name: Upload browser proof uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8157460..fb11ee8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -976,23 +976,13 @@ jobs: --notes-file artifacts/release/release-notes.md existing_dir="$(mktemp -d)" gh release view "$GITHUB_REF_NAME" --json assets --jq '.assets[].name' | sort > /tmp/proofkit-existing-release-assets.txt - mapfile -t existing_asset_names < /tmp/proofkit-existing-release-assets.txt - comm -13 \ - <(printf '%s\n' "${expected_asset_names[@]}") \ - <(printf '%s\n' "${existing_asset_names[@]}") \ - >/tmp/proofkit-extra-release-assets.txt - if [ -s /tmp/proofkit-extra-release-assets.txt ]; then - cat /tmp/proofkit-extra-release-assets.txt >&2 - echo "release $GITHUB_REF_NAME contains unexpected assets" >&2 + if ! cmp -s /tmp/proofkit-expected-release-assets.txt /tmp/proofkit-existing-release-assets.txt; then + diff -u /tmp/proofkit-expected-release-assets.txt /tmp/proofkit-existing-release-assets.txt >&2 || true + echo "release $GITHUB_REF_NAME asset set differs from the candidate asset set" >&2 exit 1 fi - missing_assets=() for asset in "${expected_assets[@]}"; do asset_name="$(basename "$asset")" - if ! grep -Fxq "$asset_name" /tmp/proofkit-existing-release-assets.txt; then - missing_assets+=("$asset") - continue - fi gh release download "$GITHUB_REF_NAME" --pattern "$asset_name" --dir "$existing_dir" existing_asset="$existing_dir/$asset_name" if ! cmp -s "$asset" "$existing_asset"; then @@ -1001,9 +991,6 @@ jobs: exit 1 fi done - if [ "${#missing_assets[@]}" -gt 0 ]; then - gh release upload "$GITHUB_REF_NAME" "${missing_assets[@]}" - fi gh release view "$GITHUB_REF_NAME" --json tagName,name,body,isDraft,isPrerelease,assets >/tmp/proofkit-release-view.json go run ./internal/tools/releasepreflight github-release \ --release-file /tmp/proofkit-release-view.json \ @@ -1012,17 +999,6 @@ jobs: --asset-names-file /tmp/proofkit-expected-release-assets.txt cp /tmp/proofkit-release-view.json artifacts/release/github-release.json go run ./internal/tools/releasepreflight retained-evidence --artifact-root artifacts - final_dir="$(mktemp -d)" - for asset in "${expected_assets[@]}"; do - asset_name="$(basename "$asset")" - gh release download "$GITHUB_REF_NAME" --pattern "$asset_name" --dir "$final_dir" - final_asset="$final_dir/$asset_name" - if ! cmp -s "$asset" "$final_asset"; then - echo "release $GITHUB_REF_NAME final asset $asset_name differs from the candidate artifact" >&2 - sha256sum "$asset" "$final_asset" >&2 || true - exit 1 - fi - done echo "release $GITHUB_REF_NAME already exists and matches the candidate artifacts" >&2 exit 0 fi diff --git a/ADOPTION.md b/ADOPTION.md index 42eff4a..0bbaca3 100644 --- a/ADOPTION.md +++ b/ADOPTION.md @@ -30,18 +30,19 @@ Publisher and post-publish registry identity are admitted. GitHub Release assets are archive and provenance lookup, not package-manager dependency authority. -Consumers may choose npm or Bun for installation. Both consume the same npm -registry package identity: +Consumers install the exact npm registry package identity: ```bash -npm install -D @research-engineering/agentic-proofkit -bun add -d @research-engineering/agentic-proofkit +npm install --save-dev --save-exact @research-engineering/agentic-proofkit +npm exec --offline -- agentic-proofkit help ``` Release evidence still uses npm as the registry-authority CLI because Proofkit records npm-specific package identity, `dist.integrity`, `dist.shasum`, `npm -pack`, and root-only registry install proof. Bun is not used as a substitute -for those release-authority facts. +pack`, and root-only registry install proof. A bare `agentic-proofkit` command +is valid only when an installed package binary is already on `PATH`. +Equivalent exact-tarball Bun execution has not been admitted, so Bun execution +remains a non-claim. Stable authority channel ids: diff --git a/README.md b/README.md index 0b78e0c..1c85fc6 100644 --- a/README.md +++ b/README.md @@ -24,25 +24,55 @@ packets without copying verifier logic between projects. The canonical registry identity is npm: ```bash -npm install -D @research-engineering/agentic-proofkit +npm install --save-dev --save-exact @research-engineering/agentic-proofkit ``` -Bun consumers may install the same npm registry package with Bun: +Pre-1.0 releases may contain owner-declared breaking changes, so npm consumers +must retain the exact saved version instead of replacing it with a version +range. + +The canonical local invocation resolves only the already-installed dependency: ```bash -bun add -d @research-engineering/agentic-proofkit +npm exec --offline -- agentic-proofkit help ``` npm remains the release-authority toolchain because release proof records npm registry identity, `dist.integrity`, `dist.shasum`, `npm pack`, and root-only -registry install evidence. Bun is a supported consumer/developer package -manager path, not a replacement for npm release evidence. +registry install evidence. A bare `agentic-proofkit` command is valid when the +package manager, script runner, or activated environment has already placed the +installed binary on `PATH`; it is not the canonical copy-and-paste route. +Equivalent exact-tarball Bun execution has not been admitted, so this README +does not claim a Bun execution route. Python consumers use the Python package as a runner wrapper over the same Go CLI, not as a Python SDK. Python projects should still treat CLI/JSON records, exit codes, and package metadata as the public contract. -Published Darwin binary distributions require macOS 12.0 or later. + +Supported binary targets are macOS 12 or later on arm64 or x64. +Linux manylinux 2.17 or later is supported on arm64 or x64. Windows is unsupported. The Python +runner requires Python 3.9 or later and wraps the same Go CLI; it is not a +Python SDK. + +After an exact Python package version is available from an admitted channel, +use one complete package-manager chain: + +```bash +python -m pip install agentic-proofkit== +python -m agentic_proofkit help +``` + +or: + +```bash +uv add --dev agentic-proofkit== +uv run agentic-proofkit help +``` + +These conditional commands do not claim that any current version is available +on PyPI. + ## Project Boundary @@ -148,10 +178,10 @@ make generated invariants authoritative by itself. Use the CLI help route before reading source: ```bash -agentic-proofkit help -agentic-proofkit init -agentic-proofkit help repo-profile-admission -agentic-proofkit repo-profile-admission --help +npm exec --offline -- agentic-proofkit help +npm exec --offline -- agentic-proofkit init +npm exec --offline -- agentic-proofkit help repo-profile-admission +npm exec --offline -- agentic-proofkit repo-profile-admission --help ``` Command-specific help is derived from the private command descriptor table and @@ -175,8 +205,60 @@ value with lower transport overhead by placing the process option before the command: ```bash -agentic-proofkit --json-layout compact requirement-context-slice --input slice-input.json +npm exec --offline -- agentic-proofkit --json-layout compact requirement-context-slice --input slice-input.json +``` + +### First Valid Input + +The following marker-bounded record is a complete minimal requirement-source +input. Its example IDs, paths, owner, invariant, and non-claims are +caller-replaceable examples, not Proofkit-owned product meaning. + + +```bash +npm exec --offline -- agentic-proofkit requirement-source-admission --input - +``` + +```json +{ + "schemaVersion": 1, + "sourceId": "example.requirements", + "specPackagePath": "docs/specs/example", + "overviewPath": "docs/specs/example/overview.md", + "requirementsPath": "docs/specs/example/requirements.v1.json", + "nonClaims": [ + "This example does not approve merge or release." + ], + "requirements": [ + { + "requirementId": "REQ-EXAMPLE-001", + "ownerId": "example.owner", + "invariant": "The example owner must replace this sentence with an admitted product invariant.", + "claimLevel": "blocking", + "riskClass": "medium", + "proofBindingRefs": [ + "proofkit/requirement-bindings.json" + ], + "nonClaimRefs": [], + "nonClaims": [ + "This example does not execute or authenticate a native witness." + ], + "lifecycle": { + "state": "active", + "replacementRequirementIds": [], + "evidenceRefs": [] + }, + "deferral": null, + "updatePolicy": { + "reviewOwnerId": "example.owner", + "requiresImpactDeclaration": true, + "requiresProofBindingReview": true + } + } + ] +} ``` + Use `secret-scan` only when the caller provides an explicit file inventory with content. It is a dedicated secret-like text detector for admitted inventory @@ -187,7 +269,7 @@ For TypeScript consumers that want a small wrapper instead of hand-written child-process code: ```bash -agentic-proofkit json-report-cli-adapter-source --language typescript --format json +npm exec --offline -- agentic-proofkit json-report-cli-adapter-source --language typescript --format json ``` The generated adapter remains caller-owned after materialization. It must be @@ -197,10 +279,7 @@ contract; it does not become a separate public SDK or proof authority. | Need | Owner | |---|---| | Human orientation | This README | -| Coding-agent startup | `AGENTS.md` | | Adoption and release-channel model | `ADOPTION.md` | -| Active work ledger | `BACKLOG.md` | -| Contribution rules | `CONTRIBUTING.md` | | Vulnerability reporting boundary | `SECURITY.md` | | Explicit boundary denials | `NON_CLAIMS.md` | | `LICENSE` | MIT license | diff --git a/cmd/agentic-proofkit/main.go b/cmd/agentic-proofkit/main.go index 12242e9..d265fde 100644 --- a/cmd/agentic-proofkit/main.go +++ b/cmd/agentic-proofkit/main.go @@ -2,11 +2,21 @@ package main import ( "context" + "fmt" "os" "github.com/research-engineering/agentic-proofkit/internal/app" + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" ) func main() { - os.Exit(app.Run(context.Background(), os.Args[1:], os.Stdin, os.Stdout, os.Stderr)) + renderer, err := cliexec.AdmitLauncherProfile( + os.Getenv(cliexec.LauncherProfileEnvironment), + os.Getenv(cliexec.PythonExecutableEnvironment), + ) + if err != nil { + _, _ = fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + os.Exit(app.RunWithRenderer(context.Background(), os.Args[1:], os.Stdin, os.Stdout, os.Stderr, renderer)) } diff --git a/docs/implementation/audit-remediation-design.md b/docs/implementation/audit-remediation-design.md new file mode 100644 index 0000000..c031a16 --- /dev/null +++ b/docs/implementation/audit-remediation-design.md @@ -0,0 +1,3182 @@ +# Audit Remediation Design + +Status: C-124 implementation candidate; C-01 through C-124 +corrections are present; provider validation is invalidated by the C-124 +semantic delta; unaffected prior validations remain historical evidence. + +Owner: `proofkit`. + +Target baseline: `3d86b6d0e4ec4a6c6a7f7a35ff2787011771aa64`. + +Review history: + +- cycle 1: `REVISE` by proof/security, contract/release, and UX/accessibility + reviewers; resolved by explicit `0.2.0` migration, versioned downstream + envelopes, deterministic race barriers, single-owner generated CLI + projections, strict entity grammar, exact proof routing, read-only release + allowlists, and state-specific browser oracles; +- cycle 2: `REVISE`; resolved by executable selector alignment, symmetric + closed input/output ABI trees, an explicit change-record v2 path, split proof + ownership, exact generator checks, relative/absolute symlink compatibility, + a legacy-term boundary, and a canonical whole existing-release block; +- cycle 3: two `APPROVE`, one `REVISE`; resolved by aligning the output-writer + selector and making generated CLI-contract freshness a mandatory + `npm run check` step; +- cycle 4: unanimous `APPROVE`; no P0-P2 design contradiction remains. +- cycle 5: unanimous `APPROVE` after correcting the generated stack-preset + topology; the single authored owner remains unchanged. +- implementation cycle 1: three independent `APPROVE` verdicts after + corrections C-01 through C-20, exact proof-selector closure, dead-helper + cleanup, and a green full worktree gate; no confirmed P0-P2 finding remains. +- committed-candidate cycle 1: two `REVISE` verdicts found a job-level + `continue-on-error` expression bypass and three untested per-view loading + states; C-21 and C-22 plus primary-owner projections closed both findings, + and the focused correction review returned three `APPROVE` verdicts. +- committed-candidate cycle 2: two `APPROVE` verdicts and one `REVISE` exposed + ignored inherited and step-level execution controls plus partial + `continue-on-error` evaluation in the generic workflow oracle; C-23 closed + workflow/job/step inheritance, exact environment entries, nullable scalar + key presence, required leaf jobs, and the aggregate, after which the focused + correction review returned three `APPROVE` verdicts. +- exact-commit cycle 1: proof/security and UX/architecture returned `APPROVE`; + contract/release returned `REVISE` because the `0.2.0` machine change record + omitted the intentional `adoption-doctor` blocked-state and exit-code + migration; C-24 closed the machine record, migration, rendered-note + falsifier, and proof binding, after which focused correction review returned + three `APPROVE` verdicts. +- exact-commit cycle 2: proof/security and UX/architecture returned `APPROVE`; + contract/release returned `REVISE` because the record and top-level-only + semantic test omitted the intentional non-enforced advisory rule transition + from `passed` to `skipped`; the first focused review exposed the same + undeclared transition outside the touched selection in `enforce-touched`. + C-25 declares the complete migration, strengthens both adjacent-rule oracles, + and binds them; the second focused correction review returned three + `APPROVE` verdicts. +- provider cycle 1: the exact candidate passed locally but two provider browser + attempts failed at the same 30-second Firefox cap on different tests while + Chromium and WebKit passed; the run lifecycle then deleted the promised + retained traces and the success-only upload skipped them. C-26 preserves + attempt-scoped failure diagnostics without creating or admitting passed + proof so the moving Firefox stall can be diagnosed rather than guessed; the + focused correction review returned three `APPROVE` verdicts after routing + both selectors exclusively to QUALITY-022. +- provider cycle 2: the first post-C-26 exact local gate reproduced the moving + Firefox timeout in two different state-matrix rows; both retained traces end + at the first `AxeBuilder.analyze()` evaluate of the 1,287,127-character + default axe source. A first minified-source candidate passed two full runs + but was falsified by a controlled repeated-audit run: minified and default + sources both stall under continuous trace screenshots, while the default + source completes 13 two-audit cycles without tracing and with action, DOM, + network, and source tracing when screenshots are disabled. A third full run + then falsified screenshot removal alone by stalling at the same large + builder evaluate. C-27 combines both necessary controls: pinned axe + initialization through the browser-context script channel, constant builder + loaders, and screenshot-free traces with one bounded best-effort post-failure + screenshot; the combination completed 30 consecutive two-audit Firefox + cycles. +- provider correction review: proof and UX returned `APPROVE`; contract + returned `CONDITIONAL APPROVE` after C-27 also closed the exact development + dependency allowlist, owner-to-binding non-claims, and constant-loader + reachability mutant. Two consecutive frozen-byte 72-test browser proofs and + direct proof verification passed without an intervening browser-input edit, + discharging the sole condition before final committed-object revalidation. +- final committed-object review: proof/security found that C-13's pinned-parent + publication could still follow a parent moved outside the repository after + the last route check. C-28 keeps temporary-file creation and cleanup on the + pinned parent but binds the irreversible publication to full source and + destination routes through the repository root; the exact pre-publication + falsifier covers outside-root and in-root replacements. +- final correction review: all three reviewers rejected C-28 because + `os.Root.Rename` resolves its source and destination routes separately and a + replacement can substitute the discoverable temporary basename. C-29 moves + the temporary object to the repository root, admits its identity and the + destination parent after the exact barrier, publishes through the repository + root, and narrows the owner contract to the strongest cross-platform claim + the implementation can prove. Adversarial same-user namespace mutation after + final identity admission is explicit rather than hidden behind another + check-then-use implication. +- C-29 correction review: the root-level candidate was rejected because it + regressed writable-child and nested-filesystem outputs, object identity did + not admit content, its compatible public contract was missing from the + release record, and final PR-body validation did not machine-check all + mandatory closeout facts. C-30 restores same-parent staging/publication under + the explicit concurrency boundary and admits identity, exact mode, and + digest; C-31 + projects the compatible addition into release notes; C-32 adds a canonical + closeout record validated in both reviewed and server-side body bytes. +- C-30 through C-32 correction review found that permission-only mode + comparison discarded setuid, setgid, and sticky bits, while C-32 validated + mutable artifact paths and later reread them for projection. C-30 now admits + the complete file mode exactly; C-33 copies each local evidence object once + into a private snapshot and validates and projects those same bytes. +- C-33 correction review found that snapshot byte identity does not admit + fields absent from its predicate. C-34 removes unvalidated artifact digests + from closeout so every projected local value is explicitly admitted. +- C-34 self-review found that streaming `jq -e` accepts a forged first JSON + document when a valid second document determines the final exit status. C-35 + slurps each snapshot, requires exactly one document, and admits the exact + browser-project inventory. +- The independent maximum-reasoning audit reproduced dynamically false + conditions on a required CI test step and the release candidate job that the + literal `false`/`0` deny-list admitted. C-36 closes both workflow inventories + with exact absent-or-owner condition projections. +- The C-36 correction review replaced the macOS smoke with an Ubuntu no-op + while retaining a successful job ID and reproduced vacuous bound negative + selectors on an invalid owner workflow. C-37 binds exact CI check names, + runners, and the platform-smoke command, rejects reusable-job substitution, + and requires positive owner admission inside the bound selectors. +- The C-37 correction review inserted a semantic shadow step before the exact + platform command, passed a mixed-type runner list through lossy + normalization, demonstrated that QUALITY-013 still omitted its positive CI + and release package-gate owners, and found a malformed closeout `jq` filter. + C-38 closes the ordered platform step inventory and package-script owner, + requires exact scalar runners, binds both positive package-gate owners, and + restores the executable singleton filter. +- The C-38 correction review moved the semantic shadow into the repository-local + setup action and added an otherwise-empty `run` key beside `uses`; both + survived path/value-only comparison. C-39 admits the exact local-action bytes + and makes `run`, `uses`, and `with` key presence part of the ordered step + inventory. +- The C-39 correction review returned three independent `APPROVE` verdicts + after reproducing the nested local-action shadow and null, empty, whitespace, + and dual execution-key mutants; no P0-P2 finding remains. +- A subsequent independent maximum-reasoning audit approved the exact C-39 + commit at 97/100 with no P0-P2 finding, but confirmed two P3 hardening gaps: + the closed selector inventory omitted the QUALITY-011 and QUALITY-013 + anti-vacuity scenarios, and README command extraction used whitespace + splitting that changed safe quoted Bash words. C-40 reopens exact-object + validation for both findings. +- C-40 focused review reproduced an owner-transfer mutant: changing only a + critical scenario's `requirementId` preserved its exact selectors and passed + the scenario-keyed inventory. C-41 keys every protected inventory by the + exact requirement/scenario pair and independently falsifies owner transfer. +- The same focused cycle reproduced complete selector deletion through the + generic empty-selector early return and NUL admission through the bounded + lexer, then found a plan threshold weaker than the final completion + criterion. C-42 moves exact-set admission before the empty path, rejects NUL, + and requires every final reviewer to report no unresolved confirmed finding. +- Continued boundary review found Unicode trimming changed literal Bash argv + and could erase invalid JSON NBSP bytes, escaped NUL bypassed the top-level + lexer check, and interactive Bash history expansion changed an admitted + double-quoted `!`. C-43 preserves exact non-IFS and JSON fence bytes, closes + escaped NUL, and rejects unescaped double-quoted history expansion. +- C-43 focused review then showed that symmetric space/tab trimming consumed + a valid trailing escaped delimiter before lexing. C-44 trims only leading + unescaped Bash delimiters and preserves trailing escaped space and tab. +- The first complete package-level run after C-44 showed that the pre-existing + missing-function mutant now stopped earlier at the new exact-set boundary. + C-45 moves that mutant to an unprotected binding so exact-set and generic + function-existence failures remain independently reachable. +- Continued review reproduced quoted and unquoted even-backslash history + literals rejected after pair collapse, one remaining P0-P2-only preparation + threshold, and repeated generic I/O during pure inventory mutants. C-46 + consumes complete backslash runs before `!` with Bash-equivalent quoted and + unquoted projection; C-47 aligns the last threshold; C-48 separates pure + inventory admission from generic AST and `go list` validation. +- Adversarial complexity review then showed that the first C-46 helper rescanned + a non-history backslash suffix after every collapsed pair, producing + quadratic work on a package-bounded README line. C-49 consumes every + backslash run once and dispatches its terminal byte under the same bounded + quoted or unquoted rules. +- The next complete package-level run showed that isolated generic + executability fixtures now stopped at the intentionally earlier global + inventory phase. C-50 exposes the two existing validation phases as separate + local functions: production composes both, while each fixture calls the + owner of the error it falsifies. +- Frozen C50 proof-routing review found that the generic missing-function and + invalid-signature falsifiers were not selectors of the QUALITY-010 + executability scenario. C-51 binds both and protects that complete + four-selector scenario with the same exact owner/set inventory. +- Frozen C51 review reproduced the same selective-route gap for the + QUALITY-013 permission-floor falsifier. A closed-world pass over the typed + package-gate oracle found ten owner-relevant tests outside its seven-selector + binding. C-52 binds the complete seventeen-test owner surface and protects it + with exact set equality. +- The frozen C52 correction review returned three independent `APPROVE` + verdicts with no confirmed P0-P3 finding. The reviewers independently + checked exact QUALITY-010, QUALITY-011, and QUALITY-013 inventories, bounded + README lexer semantics and linearity, JSON byte preservation, owner + separation, documentation parity, and absence of business-logic drift. +- The next exact-commit review confirmed two omitted boundary cases. The + intentional readiness-closeout character-reference verdict change was absent + from the closed release record, and the selector validator rejected an + executable Go test with an unnamed `*testing.T` parameter. C-53 closes the + release declaration, migration, rendered-note witness, selector grammar, and + exact QUALITY-010 and QUALITY-024 inventories. +- The repeated exact-commit review confirmed five further P2 gaps: entity + decoding preceded Markdown pipe parsing, the reachable specifications + no-match state was absent from the browser matrix, the removed synthetic + Arrow-key contract lacked migration disclosure, and the compatible pilot-all + envelope plus optional witness-selector I/O were absent from the release + record. C-54 closes all five at their existing owners. +- The next exact-commit UX review reproduced a discontinuous npm onboarding + route: root help displayed a bare executable while the installed consumer + admitted only local offline npm resolution, and the witness ignored the + displayed command. C-55 projects the copyable npm route and executes argv + parsed from that exact stdout while rejecting the bare-route mutant. +- C-55 correction review then reproduced Unicode-whitespace normalization: + leading or trailing NBSP around the displayed route passed the witness even + though Bash does not treat that byte as an IFS delimiter. C-56 removes only + the authored leading space/tab indentation and preserves both NBSP mutants. +- The next exact-commit review proved that the installed trace still + hard-coded every transition after root help, that the release record + omitted the onboarding addition and misclassified removed installed + governance paths, that absolute-symlink migration omitted manifest + ancestors, and that merge-critical workflow jobs admitted semantic shadow + steps. C-57 through C-59 close the displayed onboarding chain, exact release + declarations, migration scope, and complete ordered step inventories. +- The same review falsified A-04: the 3,025-line workflow oracle mixed five + independent requirement owners and the concentration had already hidden a + shadow-step gap. C-60 records a closed size ledger, splits peripheral owners + and neutral support, preserves the inseparable QUALITY-011/013 selector + cluster, and protects every moved binding by exact selector inventories and + stale-path mutants. +- The C-60 correction review confirmed six residual proof gaps: the closed + step projection omitted `id` and `timeout-minutes`; exact selectors did not + preserve their witness path; candidate staging could omit the five new + untracked owners; one ledger byte count was stale; the extracted scanner + selectors were not requirement-bound; and exact-tarball onboarding still + admitted bare invocation copy for ordinary leaves. C-61 through C-66 close + these gaps with presence-aware step fields, exact witness paths, closed + staging and size inventories, scanner owner bindings, and execution of every + displayed leaf-help route and installed invocation. +- Candidate-command rehearsal then showed that cleanup-bearing temporary-file + staging was not executable under the repository's destructive-action guard. + C-67 replaces temporary files and cleanup with in-memory exact inventories + and stdin pathspec admission while preserving every equality predicate. +- The next focused proof review removed the workflow permission floor and + added a surplus provider write scope without tripping the scanner selector; + its contract review also placed the installed block before Usage and used a + command-token prefix collision without tripping the onboarding verifier. + C-68 and C-69 close exact permission sets plus explicit inheritance and exact + Usage order, token boundary, and installed-byte equality. +- The C-69 review then deleted its new falsifier while all bound QUALITY-019 + selectors remained green. C-70 adds the selector to the owner requirement, + exact selector set, exact witness path, and deletion/surplus/owner/relocation + mutation inventory. +- The final C-61 through C-70 focused correction review returned three + independent `APPROVE` verdicts with no confirmed P0-P3 finding on one + unchanged frozen snapshot. Exact committed-object validation remains the + final publication precondition. +- Final provider-closeout rehearsal then proved that its browser snapshot + predicate still required the superseded 24-tests-per-project matrix while + the exact committed gate produced 25 per project. C-71 aligns the executable + closeout predicate with the current 75-test owner result before repeating + committed-object validation. +- The repeated exact-commit architecture review then reproduced overlapping + adoption output conditions: `--mode bootstrap` also described the agent and + materialization routes while the generator compared only condition text. + C-72 introduces one optional bounded machine condition model, closes its + finite normalized option space, binds concrete argv to exact conditions and + variants, and rejects repeated single-value selectors instead of retaining + last-write-wins ambiguity. +- C-72 decomposition then falsified the candidate-staging closure: moving the + pure condition algorithm out of the generator I/O owner created a sixth + decomposition-owner file, while P12.2 still admitted only the five earlier + workflow owners. C-73 closes that six-file owner subset and keeps exact + staged-path equality plus empty unstaged and untracked remainders. +- Direct argv falsification then showed that `--pilot ""` set the raw flag but + normalized to the same empty native value as omission, so the ABI condition + incorrectly reported `--pilot=absent` and selected the default first pilot. + C-74 rejects the empty valued selector, binds its exact diagnostic, and + includes the intentional rejection in requirements and migration guidance. +- Staging rehearsal then proved that a baseline-relative added-path inventory + is not the current untracked inventory after an earlier candidate commit: + five workflow files already exist in `HEAD`, while only the condition-model + owner remains untracked in the C-72 through C-74 amend. C-75 proves the + one-file current set separately and requires the six-file decomposition + subset to survive staging. +- Condition-closure review then showed that the 80-state test duplicated the + native mode and pilot literals. C-76 derives immutable test domains from the + same internal native lists that build `ValidateOptions` admission maps, while + retaining the exact current 80-combination and twelve-valid-state predicates. +- Claim review then showed that generic condition syntax does not imply generic + native-option closure: only the adoption output owner has the required + finite-domain and argv witnesses. C-77 admits that exact definition as the + only current condition-model owner and requires any later owner to add its + own native-closure proof before generator admission. +- Baseline-diff rehearsal then showed that the six decomposition owners are + only a subset of all added files: the candidate already contains seventeen + added files relative to the reviewed baseline and the condition owner makes + eighteen. C-78 closes the complete baseline-relative added-path inventory + independently from the current untracked set and owner subset. +- Independent C-78 review then found two proof escapes and one stale + architecture fact: the guidance mode/scope failure emitted JSON without an + exact output condition and variant assertion; an alias command or direction + could reuse the admitted definition; and the C-73 prose retained superseded + exact line counts. C-79 through C-81 bind the JSON error route, close the + command/direction/definition triple, and remove volatile inline measurements. +- C-79 correction review then removed both route coordinates from only the + guidance failure while the shared guidance condition kept the global count + green. C-82 makes JSON assertion and both route coordinates a biconditional + per case, eliminating that false-green path. +- C-82 review then removed the JSON assertion together with both coordinates; + the fixture biconditional remained true while runtime still emitted unchecked + JSON. C-83 binds the expectation to observed non-empty JSON stdout and closes + the exact fourteen-case JSON inventory. +- Final committed-object decomposition review then found that + `condition_model.go` duplicated the generator package's existing generic + sorted-map-key helper. C-84 reuses the same-package owner and removes the + redundant algorithm and import. +- Final committed-object proof review then found that the critical Mach-O + byte-compatibility scenario selected only a README projection test. C-85 + binds the exact negative, boundary-positive, truncated-parser, and legacy + parser witnesses and protects their selector/path inventory against + deletion, surplus, transfer, and relocation. +- The repeated exact-object review then found the same semantic-reachability + escape in the Python wheel-platform and one-shot browser cleanup scenarios: + both selected tests passed while their named operations had zero coverage. + C-86 binds the wheel owner/projection/verifier tests and the three cleanup + concurrency tests, then closes both selector and path inventories. +- Exhaustive review of all 105 candidate-added selector rows then found one + remaining semantic false route: the mutable-release-facts scenario selected + only package reference closure. C-87 binds the existing ten-case stale-fact + falsifier and closes its exact selector/path inventory. +- The independent Sol/max audit then found that exact permission maps on named + scanner jobs did not close the workflow job inventory: an unclassified job + with write authority remained admissible. C-88 requires each scanner + workflow's jobs to equal the advisory/provider union and preserves a surplus + write-job falsifier. +- Terminal UX review then executed the exact first command emitted by a stack + preset in the installed npm consumer and received `command not found`. + Initial C-89 review rejected a global npm renderer because the same binary is + shipped in the Python wheel. C-89 therefore admits one explicit immutable + invocation profile at the launcher boundary, renders npm, Python-module, and + direct-path continuations separately, and proves both installed channels. +- Terminal contract review then replaced the pilot aggregate output with an + object while its declared output witness still passed, and observed that + self-check's output witness asserted empty stdout on an input error. Review + also identified the root-distinct adoption aggregate and the app-owned pilot + union constructor. C-90 closes all three selector tuples, native-source + ownership, requirement bindings, and substitution falsifiers. +- Frozen implementation review then found that the Python executable could + carry report-visible secret-like or control content, and that help, + structured agent-route/workflow/coverage argv, project workflow identity, and + the installed wheel route chain remained outside the C-89 closed inventory. + C-91 closes launcher value admission; C-92 closes every owned display/argv + route while proving caller-owned argv preservation and direct-argv execution. +- Provider exact-object review then exposed two test-oracle portability gaps. + Linux could immediately reuse the inode of a removed temporary file, so the + writer correctly rejected the substitution as a mode change while the + identity mutant demanded a platform-dependent diagnostic. Separately, the + retained Firefox trace proved a fully rendered graph before one page-realm + bulk evaluation consumed the remaining 29 seconds without returning. C-93 + substitutes a pre-existing live file whose identity must differ while both + files coexist; C-94 replaces the bulk evaluation with retryable count plus + indexed-attribute assertions that are logically equivalent to exact ordered + array equality, without retries, timeout expansion, test splitting, or + assertion weakening. Independent architecture and UX review approved both + repair classes. +- Final measurement review found that the last static-analysis cleanup changed + `agentroute.go` after the threshold ledger was frozen. C-95 refreshes the + complete final ledger after every correction instead of treating a prior + exact snapshot as current evidence. +- The first exact-object Sol/max audit then falsified input-grammar closure: + typed workflow decoding silently discarded job-level execution controls, and + source hygiene omitted the shipped tracked CSS language. C-96 admits every + tracked workflow through closed raw workflow/job/step mappings with two + exact release-environment exceptions before typed semantics. C-97 derives + browser-asset extension mutants from the tracked owner inventory and adds + CSS without changing identifier-boundary matching. +- Dependency pre-merge validation then reproduced the Firefox 30-second stall + twice on the same immutable branch while the failing test moved between the + two selection scenarios. C-98 removes their repeated page-realm range + synthesis: collapse uses Playwright `selectText` and click actions, while the + Unicode case performs one locator-scoped exact-range operation with + independently computed strict bounds, without retries, a larger timeout, or + production hooks. +- The next exact-object Sol/max audit returned `REVISE` with four P2 findings + and no P0, P1, or P3 finding. It proved that publication commands conflated + the historical audit baseline with the current integration base, the + closeout predicate retained a stale scenario count, launcher admission + accepted bidi format controls, and `pilot-admission` exposed one undeclared + alias route plus last-write-wins selectors. C-99 through C-102 separate Git + identities, bind the exact final coverage count, close Unicode `Cc` and `Cf` + admission, and make every accepted pilot route declared and unambiguous. +- C-99 through C-102 review cycle 1 returned one `APPROVE` and two `REVISE` + verdicts; it required an exhaustive `Cc`/`Cf` oracle, two-phase Git/PR + identity, applicable closeout completion criteria, singleton-parent proof, + durable QUALITY-004 ownership, and compatibility declarations. Cycle 2 + returned one `APPROVE` and two `REVISE` verdicts because the migration text + incorrectly required exactly one pilot selector and hid the valid omitted + default-first route. Cycle 3 returned three independent `APPROVE` verdicts + on one frozen diff with no confirmed P1-P3 finding. +- The mandatory final exact-object Sol/max audit of + `e55bfc6e5641aed906d9a3c02e56a431bc0ca4b5` returned `REVISE` with one P2 + and no P0, P1, or P3 finding. The current release-record witness admitted + only a manually selected subset of the machine record, so semantic deletion + or a structurally valid surplus could remain green after regeneration. + C-103 closes the complete breaking, addition, migration, and rendered-note + inventories under the existing QUALITY-024 owner. +- C-103 review cycle 1 returned one `APPROVE` and two `REVISE` verdicts; + exact machine ID/order mutants and note-projection closure were added. + Cycle 2 returned two `APPROVE` verdicts and one `REVISE` because + section-local equality still admitted appended surplus, duplicate, or second + owned sections. Cycle 3 returned three independent `APPROVE` verdicts on one + frozen diff after complete ordered machine equality and one independently + authored byte-exact full-note projection closed every confirmed escape; no + confirmed P1-P3 finding remains. +- Publication rehearsal after the first C-103 terminal approval reproduced a + zsh refspec-expansion failure in the plan itself: `"$final_sha:refs/..."` + treats `:r` as a parameter modifier instead of a literal separator. C-104 + braces the variable before the adjacent colon and rejects every remaining + unbraced variable-colon occurrence in the tracked plan. +- C-104 focused review returned two `APPROVE` verdicts and one `REVISE`: the + exact braced refspec was correct, but the plan retained the lease preceding + the already successful first publication. C-105 preserves that value as + history and binds the next correction publication to the exact current + remote head. +- The mandatory new post-C-105 exact-object Sol/max audit returned `REVISE` + with one P2 and no other confirmed P0-P3 finding. The baseline-relative + added-file inventory still contained 18 paths even though three later + owner-test files increased the final set to 21. C-106 refreshed the 21-path + set at the C-106 freeze and made that epoch's two-file amend staging + sequence executable. +- The mandatory new post-C-106 exact-object Sol/max audit of + `4a828d1be9e3f9cab0e93d4ef5991fef0d2cd475` returned `REVISE` with two P2, + one P3, and no other confirmed P0-P3 finding. C-107 closes the public + Scorecard action's exact output-input set and adds the surviving surplus + mutant. C-108 restores the exact 30-requirement P10 invariant delta. C-109 + completes C-81 by removing all four volatile line-count qualifiers from the + reverse-decomposition rationale. +- C-107 through C-109 focused review returned one `APPROVE` and two `REVISE` + verdicts. C-110 rejects a string that the generic expression normalizer + previously converted into boolean truth and binds that exact substitution + mutant. C-111 time-indexes the two-file staging predicate to the C-106 epoch + so it cannot contradict the then-current C-107 through C-109 three-file + correction set. +- C-110 through C-111 focused review returned two `APPROVE` and one `REVISE` + verdict. The remaining design-history sentence still described C-106 as the + active freeze without a historical qualifier. C-111 now time-indexes that + sentence to the same C-106 freeze without changing either inventory. +- C-111 focused review cycle 3 returned two `APPROVE` and one `REVISE`. + C-112 closes the full Scorecard-action subset after a second differently + named action with surplus authority-bearing input survived the named-step + selector. +- C-112 focused review cycle 4 returned three `REVISE` verdicts. C-113 aligns + Scorecard repository identity with GitHub's case-insensitive owner/repository + semantics after a mixed-case second action survived the lowercase-prefix + classifier. +- C-113 focused review cycle 5 returned three `REVISE` verdicts. C-114 makes + repository admission explicitly ASCII before case folding after Unicode + simple-fold long-s aliases exceeded the provider identity domain. +- The first exact-provider attempt for + `81e2c7d570e1982ffe4a9f1e5a43150438017b41` passed source and macOS quality + but failed `quality / browser runtime` with 74 of 75 tests passing, which + caused `quality / required aggregate` to fail. The retained Firefox trace + proves that the rendered state and heading assertions completed before an + auxiliary `page.evaluate` version probe remained outstanding until the + unchanged 30-second test deadline; axe analysis had not started. Earlier + retained attempts timed out at distinct page-realm and locator operations, + evidence consistent with but not proof of one moving engine-level stall. + The exact Firefox/Juggler cause remains unverified. C-115 therefore treats + the provider failure as a falsification of C-27's bounded first-attempt + claim and makes removal of source-proven avoidable wrapper exposure the first + controlled correction hypothesis. +- C-115 design review cycles 1 through 5 returned `REVISE` while narrowing + causal claims, closing the combined default-rule and target-size falsifier, + naming every owner, preserving exact branding, adding frame/version/result + and operation-topology mutants, defining the empirical overturn condition, + and closing run-options plus test-engine identity. Cycle 6 returned three + independent `APPROVE` verdicts with no confirmed design gap. +- C-115 implementation review separated attempted from completed context and + page states, closed pending, failed, concurrent, and zero-retry mutants, + replaced an unsound source scanner with one fresh-page fixture, and removed + one redundant state assertion. Three independent reviewers approved exact + diff SHA-256 `4bf170fc8f5ea50619bc414badd88cd0997198e0419e2739357760c0e577d33f` + with no P0-P3 finding. +- The first immutable C-115 falsifier epoch used input digest + `sha256:8099d7060ba9033c1e8317b6032a8776ef21c879b371edda9a460732f66281f4`. + Firefox iterations 1 through 14 each passed 25 of 25 tests. Iteration 15 + passed 24 of 25 and stopped at the unchanged 30-second deadline in + `Locator.boundingBox()` after the locator had resolved a visible graph. + C-116 preserves the graph contract while removing that raw geometry call, + which had no narrower per-call bound and consumed the remaining test budget, + and two later raw operations that the failed attempt did not reach. +- C-116 design review cycle 1 returned three `REVISE` verdicts. The correction + narrows the theorem from effective raster usability to the local SVG and + owned-viewport contract, derives expected identities from the admitted HTTP + response rather than circular DOM metadata, closes descendant visibility, + alpha, and degenerate-edge mutants, removes a redundant production CSS + floor, defines exact CSS properties, separates review-byte freshness from + runtime-input freshness, and makes toolchain A/B explicitly conditional. +- C-116 design review cycle 2 returned one `APPROVE` and two `REVISE` + verdicts. The correction arms exact request/response observers before UI + activation and proves the single rendered response, names the complete data + and geometry owner chain, closes hidden rectangle/text paint, and excludes + cross-engine-incompatible line visibility assertions. +- C-116 design review cycle 3 returned two `REVISE` verdicts while the third + review was invalidated by a concurrent correction. Effective-alpha mutants + preserved opaque computed colors while setting node fill or stroke opacity + to zero, and an overlapping-node mutant preserved every identity, label, + paint, and edge predicate. The node contract now admits each opacity factor, + exact geometry, direct-child order, and local transform independently. +- C-116 design review cycle 4 returned three `REVISE` verdicts. CSS geometry, + individual-transform, motion-path, text-offset, and empty-label mutants + preserved exact SVG attributes or the singular `transform` property. The + contract now closes used rectangle geometry, the complete current transform + family, absent text-offset attributes, exact visible-label projection, and + positive font size. +- C-116 design review cycle 5 returned one `APPROVE` and two `REVISE` + verdicts. Hidden-overflow, individual zoom, content-visibility, and zero-dash + mutants survived the otherwise closed local predicates. The owned viewport + now preserves exact scrolling, and every admitted local graph element closes + zoom, content visibility, local effects, and edge dash paint. +- C-116 design review cycle 6 returned `REVISE` after text-anchor, baseline, + and font-adjustment mutants changed or erased visible glyph layout while + retaining content, coordinates, font size, and paint. Generic visibility + and viewport-intersection alternatives were independently falsified across + all pinned engines. The bounded text contract now admits the exact current + layout serializations directly. +- C-116 design review cycle 7 returned three `REVISE` verdicts. Alignment + baseline and hidden-`tspan` mutants survived; several retained text + conjunctions lacked independent falsifiers; `text-indent` was empirically + inert; and the generic-matcher counterexample used an overbroad quantifier. + The correction closes each effective text property independently, removes + the inert property, requires direct text-only labels, and states the exact + existential cross-engine failure. +- C-116 design review cycle 8 returned one `APPROVE` and two `REVISE` + verdicts. Text-security substitution and SMIL animation preserved base + content or geometry while changing glyphs or animated SVG values. The + correction closes text security, root and leaf element topology, and + animated-geometry surplus with isolated mutants. +- C-116 design review cycle 9 returned three `REVISE` verdicts. Fixture-equal + cached rendering survived response/DOM equality; independent retrying CSS + assertions admitted a phase-split animation with no jointly valid state; + and three new childlessness predicates lacked independent falsifiers. The + correction adds a response intervention sentinel, closes local CSS animation + and transition activity, names the temporal non-claim, and gives every leaf + topology predicate its own mutant. +- C-116 design review cycle 10 returned three `REVISE` verdicts. A positive + zero-duration transition delay, an animation on an ancestor that changed + inherited graph paint, and an external SMIL target all survived local + element checks; the exact group and root ordering predicates also lacked + independent reorder falsifiers. The correction closes the complete owned + ancestor chain, both transition dimensions, local SVG addressability, and + both order predicates with isolated mutants. +- C-116 design review cycle 11 returned one `APPROVE` and two `REVISE` + verdicts. Hidden graph tables preserved response/text equality while + removing the only visible authority, currentness, verification, and state + fields. Conversely, `xml:id` did not create an addressable SVG target in any + pinned engine and had no independent counterexample. The correction adds a + bounded local visibility and temporal contract for both graph tables and + removes that unjustified conjunct while retaining the reproduced plain-ID + external-SMIL falsifier. +- C-116 design review cycle 12 returned three `REVISE` verdicts. Transparent + text color, local filter/clip/mask effects, and Firefox external SMIL through + a table-cell ID preserved the first table contract while erasing trust-state + glyphs; static captions and headers and both transition dimensions also + lacked independent falsifiers. The correction closes local table paint and + addressability, exact static semantics, and every retained table conjunct + with separate mutants. +- C-116 design review cycle 13 returned two `REVISE` verdicts while the third + review was invalidated by the correction. Table font adjustment, zoom, + content visibility, and several order/count predicates lacked independent + mutants. More importantly, Firefox SMIL could target the existing + `#workspace-content` ancestor, proving that local ID bans were the wrong + boundary. The correction removes those bans, excludes declarative SVG + animation at the document boundary, and makes local topology falsifiers + inert and independent. +- C-116 design review cycle 14 returned three independent `APPROVE` verdicts + on git blob `598dfb89b7567df269b41491b15c7fe527248b3d` with no confirmed + P0-P3 finding. C-116 is approved for planning; no runtime success follows + until implementation and the fresh immutable 30-process epoch pass. +- The C-116 implementation candidate at + `0c67de58b0b9837d714e417f64758a76368f3efa` passed the complete immutable + replacement epoch. All 30 separate Firefox processes passed exactly 25 of + 25 tests with one worker, zero retries, zero skipped, unexpected, or flaky + tests, and `exited` watchdog status. Every record used input digest + `sha256:ec3d79218e20831e726bf45e171b1d0276fdf22a04790a13f1e72e6df8dbee0d` + and historical test-ID digest + `sha256:f7b80cd6ea950cad6693a7b11020f746581d6eba4f2b7314700e4161448a554c`; + the 30-record JSONL SHA-256 is + `e38754615878a012358d2fe75fd4af031107450a7ec2bc6d70db6bc89c543051`. + Both subsequent full browser proofs passed 75 of 75, the composite browser + gate passed 21 static tests and 75 runtime tests, and the final full + `npm run check` passed. All five outer watchdog records were `exited` with + leader exit code zero and empty signal and group-probe error sets. +- Provider run `30297044766`, attempt 1, reported pull-request head + `26e44b79a90b41494f9971b84f66e4b737bc9baa` and checked out synthetic merge + commit `da27a7a1b3e17a901a47621a31ca8ae3432f9901`. Both objects have tree + `ae3b0b16efc3d185425a91488b1f902eee630c2f`, so the executed bytes equal the + head bytes even though the commit identities differ. That run falsified the + implication from the local epoch to provider liveness. Firefox timed out + after entering Playwright 1.61.1 `evalOnSelectorAll` for the focus negative + control. Retained artifact `8665124396` has GitHub digest + `sha256:db3179664637de3b053bde5efce6b0e2e8b44e3d96c5b7bf07032a270b2b46b5`; + its report SHA-256 is + `3498361d22679cc87c6560c055750bb3c782bb1d8761b28e5287499e0486a4d2`, + its trace ZIP SHA-256 is + `b4f5560b9e0e240dab35e631d9b848a6f18817b02ca6c03c2254a32ba989328d`, + and the inner `0-trace.trace` SHA-256 is + `86a30f0e21dc41a9961d26506f262e18a1cfd8832cca24d9c70a1504864dc0a4`. + The trace contains the call's `before` record and no `after` record. + Chromium and WebKit passed, as did source, macOS smoke, CodeQL, OSV, and + semantic diff. This is not evidence of a product or focus-contract failure. + It activates the already approved isolated Playwright 1.61.1-versus-1.62 + A/B overturn condition. Bot PR 80 run `30250528617`, against base + `3d86b6d0e4ec4a6c6a7f7a35ff2787011771aa64`, checked out merge + `3367101eaf48fd664f1c1975181c15d047d7fac2`; browser artifact `8646807702` + has GitHub digest + `sha256:57de6b30f9a3a82ca33b4ad18f9f36c2b47dbc55dac301c4f11669040b7a4ae1`. + Its 1.62 provider browser job passed only the older six tests per engine and + therefore did not exercise the current focus negative control; its source + job failed package verification because it changed only the manifest and + lock. The admitted A/B therefore changes exactly the manifest, lock, + package-verifier pin, and verifier fixture together; it does not change + runtime tests, retries, timeouts, production code, or business logic. +- The first Playwright 1.62 post-epoch full gate exposed C-117 under a retained + `TMPDIR` whose group was `wheel`, outside the caller's supplementary groups. + The `mode-setgid` test mutation treated successful `chmod` return as proof + that the bit materialized. Darwin instead returned success while clearing + setgid, so the writer correctly observed unchanged mode `0644` and published. + The same subtest passed 100 of 100 under the default `staff`-group temp root + and failed 100 of 100 under the retained `wheel`-group root. This confirms a + test-oracle precondition defect, not a production-writer defect. +- Final exact-object review found that C-119 introduced terminal HTTP-response + and exact accessible-name guards without guard-specific falsifiers. Removing + either guard left the 81-test matrix green. C-120 adds owner-local open and + reload 503 counterexamples plus a substring-preserving heading-name drift; + all three engines must reject those mutants before provider validation + repeats. +- Provider attempt 1 for exact source commit + `dcc824b31f858ab8fea5be683e5d81f12f039279` falsified C-119's remaining + `page.goto(..., {waitUntil: "commit"})` dependency. Firefox received the + main document with status 200, loaded every local asset and API response, and + rendered the initialized workspace, but the runner never resolved the + navigation call. C-121 replaces provider-falsified lifecycle waiting with a + pre-armed exact-URL navigation-response observation, exact trigger token, + successful-response admission, and the existing exact semantic heading + assertion. A same-URL non-navigation 503 decoy makes the navigation + classifier executable rather than structural. +- Final Sol/max review of exact candidate + `c2315fdf28be95eab08089008773d7dd234d9c96` found two false-green + candidates. C-122 reconciles P12.2's correction inventory so its + declaration and executable post-`c2315fd` three-path set have one value; + the preceding four-path staging set is historical. C-123 adds a + negative truth table for every raw base-URL admission clause; source presence + alone did not prove that the runtime witness depended on those clauses. +- The same review then reproduced C-124: deleting exact trigger-token + admission and pending-waiter abort/consumption left static 22/22 and runtime + 93/93 green. One deterministic injected pending waiter now distinguishes the + exact token, signal abort, and explicit rejection-consumption paths without + adding a browser lifecycle or production seam. + +## Purpose + +This document defines the smallest owner-valid repair for every finding that +survived independent reproduction and adjudication of the July 2026 +architecture, proof, security, release, and usability audits. + +It is temporary change authority, not a new product specification. Existing +requirements, machine contracts, code owners, and executable falsifiers remain +the durable owners. + +## Scope + +In scope: + +- false-positive proof states and false-green local oracles; +- filesystem confinement and immutable-read boundaries; +- public CLI input, output, exit, diagnostic, and discovery contracts; +- release SBOM semantics and pre-1.0 consumer compatibility; +- package and self-hosting negative-test closure; +- install-to-first-success onboarding; +- browser error handling, accessibility, responsive layout, and contrast; +- package-public documentation closure; +- exact requirement, binding, witness, and non-claim updates needed by these + repairs. + +Out of scope: + +- provider-side publication, branch protection, Trusted Publisher, registry + identity, or production rollout; +- new commands, remote services, SDKs, retry systems, or policy engines; +- general rewrites of command packages; +- decomposition based only on line count; +- full GitHub Actions interpretation; +- full CommonMark parsing; +- a claim of complete WCAG 2.2 AA conformance. + +## Authority + +The repair is bounded by: + +1. `AGENTS.md`, especially evidence-class separation, immutable admission, + package boundaries, and closeout; +2. `BACKLOG.md` for active, blocked, and deferred claims; +3. `NON_CLAIMS.md` and `ADOPTION.md`; +4. `docs/release-process.md` and `docs/proofkit-contract-map.md`; +5. the five `docs/specs/*/requirements.v1.json` sources; +6. `proofkit/cli-contract.v2.json`, + `proofkit/command-families.v1.json`, + `proofkit/requirement-bindings.json`, and `proofkit/witness-plan.json`; +7. command-local admission code and native tests for their exact behavior. + +When these surfaces conflict, the repair fails closed and updates the durable +owner rather than preserving a convenient implementation. + +## Retirement + +This document retires only after all accepted rows below are represented by +durable requirements or contracts plus executable falsifiers, the final +committed implementation passes `npm run check` and exact-object review, the +validated branch and an open, unmerged pull request identify that same object +against the same reviewed base commit, and the following closed source-owned +provider-check inventory is present for that head/base pair in the workflow +run for pull request `#78`, provider attempt `1`, with `success` conclusions: + +- `ci` / `quality / source`; +- `ci` / `quality / platform smoke / macos-15`; +- `ci` / `quality / browser runtime`; +- `ci` / `quality / required aggregate`. + +Every other check run and legacy status context observed for the object in two +consecutive final provider passes must have an accurately reported terminal +disposition. Retrospective routing must then be complete, and the final +pull-request body must record those facts and residual non-claims. A final +bounded readback must verify the remote head and base SHAs and pull-request +identity before, between, and after two reads of provider conclusions from +literal-SHA workflow-run, job, check-run, and commit-status endpoints, and must +compare the two canonical provider projections byte for byte. The reviewed +body must contain exactly one record of that projection's SHA-256 and exactly +one sentinel-delimited canonical JSON projection equal to the observed bytes; +any prose is non-authoritative. The server-side body must equal the reviewed +local body byte for byte. The inventory is owned by the exact required job set +in +`.github/workflows/ci.yml` and its typed workflow oracle; it is a closeout +requirement, not a claim about provider-side branch-protection settings, +an atomic provider snapshot across endpoints, concurrent PR-identity history +between bounded observations, or provider immutability after the final +response. Until every condition holds, this document remains temporary +closeout authority. After retirement it must not be cited as runtime, release, +or merge authority. + +## Formal quality model + +Let: + +- `A(x)` mean caller input `x` was structurally admitted; +- `S(x)` mean a semantic report was produced from admitted input; +- `B(x)` mean an unresolved blocked precondition is present; +- `C(p, h)` mean path `p` was opened through confined root handle `h`; +- `I(f0, f1)` mean two observations refer to the same opened file identity; +- `G(e)` mean a workflow guard is one exact owner-admitted expression; +- `R(j)` mean required leaf job `j` completed with `success`; +- `Reach(s0, sn, w)` mean witness trace `w` executes every transition from + installed state `s0` to first-success state `sn` with the same artifact; +- `X(v)` mean UI state `v` has an executable accessibility and reflow oracle. + +The repaired system must satisfy: + +```text +I-01 B(x) -> report.state = blocked and exit != 0 +I-02a not A(x) and not envelope_mode -> exit = 1 and stdout = empty and stderr != empty +I-02b not A(x) and envelope_mode -> exit = 1 and stdout = one invalid-input envelope and stderr = empty +I-03 A(x) and S(x) -> stdout = one admitted JSON value and stderr = empty +I-04 read(p) -> exists h,f: C(p,h) and bytes = read(f) and I(f_pre,f_post) +I-05 publish(p) -> every parent, temporary file, and rename is rooted at one h +I-06 workflow_guard_accepted(e) <-> G(e) +I-07 aggregate_success <-> for all required jobs j: R(j) +I-08 one_commonmark_entity_decode(a) = b -> phrase_class(a) = phrase_class(b) +I-09 exists w: Reach(local_install, first_valid_input, w) +I-10 stable_browser_state(v) -> X(v) +I-11 release_dependency_edge(a,d) -> d is required by artifact a +I-12 breaking_pre_1_0_release -> exact_pin_policy and non-patch version change +I-13 blocking_conjunction(f1..fn) -> every fi has an independent falsifier +I-14 public_contract_change -> canonical compatibility projection changes +``` + +No row is complete merely because a positive test passes. Each row requires a +negative case that accepts the current wrong implementation and rejects the +repair's forbidden alternative. + +## Adjudicated finding ledger + +### Accepted findings + +| ID | Severity | Finding | Durable owner | Required outcome | +|---|---:|---|---|---| +| R-01 | P1 | `adoption-doctor` reports blocked prerequisites as passed outside enforcing modes | `REQ-PROOFKIT-RETIRE-008` | Blocked evidence is always blocked; ordinary advisory gaps retain non-enforcing behavior | +| R-02 | P1 | CycloneDX root dependency edges include tool/build modules not required by shipped artifacts | `REQ-PROOFKIT-QUALITY-002` | Artifact dependency edges contain only evidenced runtime requirements; retained inventory is explicitly excluded; digest and build information come from one immutable byte snapshot read through an identity-checked pinned descriptor | +| R-03 | P1 | TypeScript public API scanner validates a pathname and later reopens it | `REQ-PROOFKIT-PACKAGE-002` | A confined repository root opens a pinned package-root handle before its manifest and sources; pinned file handles own identity, bytes, and cache keys | +| R-04 | P1 | Repository-relative output checks parents by pathname before independent create and rename operations | `REQ-PROOFKIT-SPEC-009` | Parent creation remains repository-root-confined; temporary write, cleanup, and atomic publication use one pinned destination parent after final parent-route plus temporary-object identity, exact mode, and content admission | +| R-05 | P1 | Workflow guard oracles accept expected substrings inside semantically false expressions or drop execution-control fields that alter an exact command | `REQ-PROOFKIT-QUALITY-013` | Trust-significant expressions use exact whole-expression allowlists and merge-critical jobs and steps admit only the modeled safe execution controls | +| R-06 | P1 | CI aggregate oracle accepts expected shell tests inside dead or neutralized code or under an inherited/local execution override | `REQ-PROOFKIT-QUALITY-011` | The exact required job set, execution controls, and canonical aggregate script are admitted | +| R-07 | P1 | Nine commands encode structural admission failure as report JSON and leave `stderr` empty | `REQ-PROOFKIT-QUALITY-004` and CLI process contract | Ordinary malformed input is an error; admitted semantic failures remain reports | +| R-08 | P2 | Readiness phrase scanning misses single-decoded HTML character references | `REQ-PROOFKIT-PACKAGE-002` | Phrase comparison uses one semantic-text decode after structural row parsing | +| R-09 | P2 | The CLI compatibility hash omits input and output contract semantics | `REQ-PROOFKIT-QUALITY-004` | Every required-input command has a bounded contract projection included in ABI compatibility | +| R-10 | P2 | A breaking pre-1.0 patch remains range-compatible for caret consumers | release change record and `REQ-PROOFKIT-QUALITY-024` | Consumer docs require exact pins and release validation rejects an incompatible patch policy | +| R-11a | P2 | Self-hosting report verdict lacks isolated falsifiers | `REQ-PROOFKIT-PACKAGE-004` | A pure verdict boundary rejects nonzero, invalid JSON, and non-passed reports | +| R-11b | P2 | Wheel version, identity, and SHA checks lack isolated falsifiers | `REQ-PROOFKIT-PACKAGE-006`, `REQ-PROOFKIT-QUALITY-023` | Each wheel integrity predicate has its own negative case | +| R-11c | P2 | Root tarball deny-list decisions lack operation-boundary falsifiers | `REQ-PROOFKIT-PACKAGE-001` | A complete tarball plus one forbidden entry fails for every denied class | +| R-11d | P2 | Local and CI receipt identity lacks an isolated falsifier | `REQ-PROOFKIT-PACKAGE-004` | Local and CI identities cannot collapse into the same receipt | +| R-12 | P2 | Four command-route closure conjuncts lack isolated falsifiers | `REQ-PROOFKIT-QUALITY-010` | Each closure field has its own negative case | +| R-13 | P2 | External action SHA pinning is correct but unguarded | `REQ-PROOFKIT-QUALITY-025` | Every non-local `uses` value is exactly a 40-lowercase-hex commit | +| R-14 | P2 | Installed README commands assume a globally resolvable executable | `REQ-PROOFKIT-PACKAGE-003`, `REQ-PROOFKIT-QUALITY-019` | Canonical npm onboarding uses offline local package resolution | +| R-15 | P2 | Stack preset IDs are not projected into direct help or the machine contract | `REQ-PROOFKIT-SPEC-018` | Runtime, help, diagnostics, and contract share one preset inventory owner | +| R-16 | P2 | No copyable first valid requirement input is routed from Start Here | `REQ-PROOFKIT-SPEC-001` | A marker-bounded shipped example is executed by a test | +| R-17 | P3 | Raw pipe characters break the first contract-map decision row | contract map | Every decision row renders as exactly three GFM cells | +| R-18 | P2 | Axe runs in only one terminal workspace state and does not execute `target-size` | `REQ-PROOFKIT-QUALITY-022` | A stable-state matrix runs default axe plus explicit target-size | +| R-19 | P2 | Initial workspace markup uses invalid or unjustified ARIA roles | `REQ-PROOFKIT-SPEC-021` | Native list/article semantics replace the synthetic tree; handoff output has a labelled region | +| R-20 | P2 | Workspace produces document-level overflow at 320 CSS pixels | `REQ-PROOFKIT-QUALITY-022` | The document reflows; only graph/table viewports scroll internally | +| R-21 | P2 | Native control colors do not prove text, boundary, and focus contrast | `REQ-PROOFKIT-QUALITY-022` | Explicit light/dark tokens and computed contrast oracles cover pinned engines | +| R-22 | P3 | Root help does not reveal command-family discovery | `REQ-PROOFKIT-SPEC-018` | Root help contains the one opt-in `help families` route | +| R-23 | P2 | Workspace has no bounded visible bootstrap failure state | `REQ-PROOFKIT-SPEC-021` | Loading and sanitized terminal failure states exist before and after manifest admission | +| R-24 | P2 | `baselineVerification` overstates caller-expected digest coverage | `REQ-PROOFKIT-SPEC-019`, `021`, `022`, and `023` | Schema v2 calls the field `expectedDigestCoverage`; every context consumer owns its v2 projection, and v1 is admitted only through an explicit adapter | +| R-25 | P2 | Python and supported-platform onboarding is incomplete | `REQ-PROOFKIT-PACKAGE-006`, `REQ-PROOFKIT-QUALITY-016`, `REQ-PROOFKIT-QUALITY-023` | Docs project package metadata without claiming registry availability | +| R-26 | P2 | Package-public docs reference `BACKLOG.md`, but the npm artifact omits it | `REQ-PROOFKIT-PACKAGE-001` | Contributor-only files and backlog routes are removed from the npm artifact | +| R-27 | P3 | The browser render diagnostic omits the supported `workspace` view | `REQ-PROOFKIT-SPEC-009` and `021` | Runtime and app diagnostics enumerate the same admitted view vocabulary | +| R-28 | P1 | Existing GitHub Releases can receive missing assets through a backfill branch | `REQ-PROOFKIT-QUALITY-025` | Existing release topology is validated without provider mutation | +| R-29 | P2 | `SPEC-021` says no command execution while `--open` invokes a fixed OS browser launcher | `REQ-PROOFKIT-SPEC-021` | The owner distinguishes fixed loopback launch from caller-supplied or witness execution | + +### Reclassified or rejected hypotheses + +| ID | Disposition | Reason | +|---|---|---| +| A-01 | Rejected as a global defect | Duplicate canonicalization and strict sorted-unique admission are different contracts. Replacing every local helper would change valid set semantics without a field-level owner proof. | +| A-02 | Reclassified from P1 security to R-24 P2 naming | Caller-authored expected/current digest equality proves self-consistency only. Existing owners already deny freshness and provenance; the term, not an authentication boundary, is wrong. | +| A-03 | Rejected | No `jsonNumber` exploit or divergent admitted value was reproduced. | +| A-04 | Confirmed and remediated | `scripts/workflow_package_gate_oracle_test.go` was 3,025 LOC / 94,323 bytes and mixed five independent requirement owners; the missed semantic-shadow step proves material review harm. C-60 separates browser, runtime-precondition, workflow-source, security-scanner, and neutral-support surfaces while retaining the logically inseparable QUALITY-011/013 cluster. | +| A-05 | Rejected | No prematurely decomposed package was proven. Kernel dependency direction is acyclic and command packages own distinct public routes. | +| A-06 | Rejected | Current action references are already exact commit SHAs. Only the missing falsifier remains as R-13. | +| A-07 | Rejected | A producer marker embedded in a caller-computable snapshot ID cannot create provenance and would add false authority. | +| A-08 | Rejected | Current-build self-consistency cannot alone become merge-critical proof; no repair may introduce this implication. | + +## Design decisions + +### D-01: Evidence state classification + +Problem: + +`adoption-doctor` derives both record state and rule state from policy-enforced +gaps, erasing the epistemic fact that a prerequisite is blocked. + +Chosen owner boundary: + +Keep gap discovery in `adoptiondoctor`; add a classification function that +separates unconditional blocked gaps from policy-enforced advisory gaps. + +Design: + +```text +blocked = gaps where kind is blocked_precondition or child_report_blocked +enforced = policy-selected non-blocked gaps + +if blocked is non-empty: state=blocked, exit=1 +else if enforced is non-empty: state=failed, exit=1 +else: state=passed, exit=0 +``` + +Blocked rule rows are always `blocked`. Other non-enforced rows use +`adoptionmode.NonEnforcingStatus`. Promotion readiness consumes both sets. + +Rejected lower-cost alternative: + +Changing only the rule status leaves the top-level machine state false. + +Proof invariant: + +`blocked gap -> blocked report`, while an observe-mode advisory candidate +remains `passed` with a `skipped` rule. + +Non-claims: + +The report does not authenticate the prerequisite, execute evidence, or approve +enforcement. + +Rollback or overturn condition: + +Only a durable owner explicitly redefining blocked as a policy warning may +overturn this decision. + +Why this avoids accidental complexity: + +It adds one classification boundary and reuses the existing mode vocabulary. + +Why this avoids premature over-decomposition: + +The logic remains in the command owner. + +### D-02: Artifact-honest SBOM + +Problem: + +`go list -m all` is a source/build inventory, not proof that each module is a +runtime dependency of every package, wheel, and binary. + +Chosen owner boundary: + +`internal/tools/releasesbom` continues to own deterministic release SBOM +generation. Release files remain subject components. Go module inventory is +retained only as explicitly excluded build inventory unless an individual +release binary provides a runtime dependency edge. + +Design: + +- add CycloneDX `scope`; +- mark `go list` module inventory as `excluded`; +- add `proofkit:evidence-class=source_build_inventory`; +- do not include excluded modules in the root `dependsOn`; +- retain release-file components as release subjects/representations, not + runtime dependencies, and keep them out of root `dependsOn`; +- emit artifact-specific edges from each binary BOM reference to only the + runtime modules recovered from that binary's build information; +- when a runtime module also exists in source inventory, deterministically + deduplicate by package URL and promote the component to `required`; source + evidence remains a property rather than a second conflicting BOM reference; +- package and wheel components receive no runtime edge without their own + artifact-derived evidence; +- if `debug.BuildInfo` or `go version -m` exposes runtime dependencies for a + future non-stripped artifact, emit those through a separately tested runtime + inventory function rather than inferring them from the module graph; +- test that tool-only modules can never become root runtime edges. + +Rejected lower-cost alternative: + +Deleting all Go module records hides useful supply-chain inventory. Keeping +them unscoped preserves the false dependency claim. + +Proof invariant: + +Every binary dependency edge has evidence from that binary; source/tool +inventory and distribution representations have no runtime dependency edge. + +Non-claims: + +The SBOM does not prove vulnerability absence, license approval, reachability, +or provider attestation ingestion. + +Rollback or overturn condition: + +A reviewed release owner may replace excluded inventory with artifact-derived +runtime edges after adding cross-platform falsifiers. + +Why this avoids accidental complexity: + +It corrects the evidence class without introducing a linker graph framework. + +Why this avoids premature over-decomposition: + +All logic stays in the current SBOM tool. + +### D-03: Handle-anchored filesystem operations + +Problem: + +Containment checks and later pathname operations do not imply object identity. + +Chosen owner boundary: + +Use Go's `os.Root` independently in the public API scanner and the output +writer. Do not create a generic filesystem abstraction because the read and +publication contracts differ. + +Scanner design: + +- admit lexical repository-relative paths; +- open the repository once with `os.OpenRoot`; +- open each referenced package as a confined pinned sub-root before reading + its package manifest, then read every source for that package through the + same sub-root; +- open each admitted lexical path through its owning root and pin its file + handle; +- resolve its canonical in-root path, open that path through the same root, and + require `os.SameFile` between the two pinned handles before accepting the + canonical extension; this preserves safe in-root symlinks without a + check-then-reopen implication; +- relative symlinks whose targets remain inside the root are preserved; + absolute symlink targets are rejected by `os.Root` even when they point back + inside the repository. This is an intentional `0.2.0` security-hardening + change recorded in the CLI contract, release change record, migration text, + and positive relative-link/negative absolute-link tests; +- bind pre/open/post identity and size around the bounded read, cache each + lexical admission immutably, and bind every alias of one canonical source + route to the identity and digest of its first admission; reject any later + alias whose identity or digest differs; +- derive extension and package directory from the admitted object and pinned + sub-root, never by a second unconfined open. + +The scanner implementation exposes a private operation seam used only by +same-package tests. A staged barrier deterministically pauses after the legacy +path check but before the legacy reopen, proving the current redirect +counterexample without time-based race loops. The repaired path is then tested +with the same barrier and pinned handles. + +Writer design: + +- open the current repository root once; +- create/check parents with root-relative operations; +- pin the admitted destination parent; +- create a random temporary file through that pinned parent using + `O_CREATE|O_EXCL`; +- write and chmod to `0644` through the open temporary file handle, close it, + and retain its admitted file identity; +- immediately before publication, expose a deterministic test-only object + barrier, re-admit the non-symlink temporary entry's identity, exact mode, and + content digest, expose a second test-only barrier, then re-admit the current + destination-parent route at the irreversible rename boundary; +- rename the temporary source to the destination through the same pinned + parent, preserving baseline writable-child and cross-filesystem behavior; +- clean up the temporary route through the same pinned parent; +- reject symlink or directory destinations without relying on the check for + confinement. + +The writer receives the same style of same-package operation seam: a test +barrier performs the parent substitution before temporary-file creation, +before object admission, and after object admission at the irreversible rename +boundary, while a sibling case replaces the temporary route, rewrites its +content, or changes its permission or special mode bits before final object +admission. External and in-root replacement sentinels, stable-path bytes/mode, +temporary identity, exact-mode, and content rejection, absence of published +output, and absence of temporary residue are asserted. Polling races and +probabilistic swap loops are forbidden as proof. + +Rejected lower-cost alternative: + +Additional `Lstat`, `EvalSymlinks`, or string-prefix checks leave a race window. + +Proof invariant: + +Concurrent parent or source replacement cannot read or mutate an external +sentinel. + +Non-claims: + +The writer does not promise protection from adversarial concurrent content or +namespace mutation by the same operating-system user during the operation, +fsync durability, or a repository-wide transaction. +The scanner does not prove checkout freshness. + +Rollback or overturn condition: + +Unsupported `os.Root` platform behavior or an owner-required symlink workflow +must be resolved with an equally strong descriptor-based implementation, never +with pathname rechecks. + +Why this avoids accidental complexity: + +It uses the standard confined-root API and command-local helpers. + +Why this avoids premature over-decomposition: + +No new shared package is admitted. + +### D-04: Exact workflow source oracles + +Problem: + +Substring recognition is not semantic implication for either expressions or +shell programs. + +Chosen owner boundary: + +The fixed repository workflows are checked by exact, owner-reviewed source +forms. `actionlint` retains syntax/expression validation. + +Design: + +- normalize only layout whitespace outside quoted expression literals; +- compare every trust-significant `if` against a complete allowed-expression + set for its specific job or step; +- require exact required-job `needs`; +- require the aggregate workflow to retain exact `bash` run defaults without + an inherited working-directory override or environment entries; +- require every required leaf job and the aggregate job to omit job defaults + plus job-level `continue-on-error` and have no job environment entries; +- require every step in those jobs to omit shell, working-directory, and + `continue-on-error` and have no step environment entries; +- require the aggregate job to have exact `always()` admission and one run step + with no step-level `if` or `uses`; +- for generic package-gate workflows, admit only absent or exact safe workflow + run defaults, exact owner-reviewed workflow and step environments, no + gate-job defaults or environment, and no execution override on any step in + the gate job; +- compare the whole aggregate shell program to a canonical constant; +- require every external `uses` reference to match a 40-lowercase-hex SHA, + exempting repository-local `./` actions; +- give this supply-chain property a dedicated requirement rather than + overloading the actionlint requirement, which owns syntax and expression + validity but does not claim external action safety. + +Rejected lower-cost alternative: + +Growing deny-lists cannot close expression or shell grammar. + +Proof invariant: + +`|| true`, dead branches, quoted predicate text, early success, background +tests, inherited or local environment, shell, and working-directory overrides, +unexpected environment entries, and any schema value including explicit YAML +`null` presence for the forbidden scalar job/step controls all fail the oracle. + +Non-claims: + +The local source oracle is not a GitHub Actions interpreter and does not prove +provider execution or branch-protection configuration. + +Rollback or overturn condition: + +Any workflow expression change requires an explicit owner review and an +allowlist update with a new negative case. + +Why this avoids accidental complexity: + +Exact forms are stronger and smaller than a partial evaluator. + +Why this avoids premature over-decomposition: + +The existing workflow test remains the owner. + +### D-05: Structural admission versus semantic failure + +Problem: + +Some builders translate structural errors into ordinary report records, +contradicting the CLI process contract. + +Chosen owner boundary: + +Command packages return admission errors. The app layer owns channel routing. +Only explicitly requested agent-envelope modes convert an admission error to a +JSON invalid-input envelope. + +Design: + +```text +ordinary structural error: + exit 1; stdout empty; sanitized stderr diagnostic + +admitted semantic failed or blocked report: + nonzero exit; stdout one JSON report; stderr empty + +explicit agent-envelope invalid input: + exit 1; stdout one invalid-input envelope; stderr empty +``` + +The nine reproduced commands receive a table-driven ABI falsifier. Builder +signatures are changed only where needed to preserve this split. + +Rejected lower-cost alternative: + +Duplicating the same diagnostic in both channels makes machine composition +ambiguous. Detecting synthetic `invalid-input` report IDs in the app preserves +the wrong owner direction. + +Proof invariant: + +Every required-input command satisfies the same channel algebra for `{}` and +for one admitted semantic failure. + +Non-claims: + +Human diagnostic bytes are stable only where explicitly included in the ABI +corpus. + +Rollback or overturn condition: + +A versioned CLI process contract may add another explicit projection; command +packages may not silently invent one. + +Why this avoids accidental complexity: + +It removes a translation layer instead of adding one. + +Why this avoids premature over-decomposition: + +Channel behavior stays centralized in `internal/app`. + +### D-06: Machine-readable CLI compatibility + +Problem: + +The public contract claims input ownership but 59 required-input commands have +no input contract, and the ABI projection ignores both input and output +contracts. + +Chosen owner boundary: + +`proofkit/cli-contract.v2.json` is the single authored machine owner. A +deterministic checked generator projects contract metadata into private Go +tables used by descriptors and help. Command admission code remains the +implementation and must be linked to named native admission witnesses. + +The generator is `internal/tools/commandcontractgen`; it writes two +owner-derived private projections: + +- `internal/app/command_contract_generated.go` for app descriptors/help; +- `internal/command/stackpreset/preset_ids_generated.go` for the lower-level + preset package. + +The second output prevents `stackpreset` from importing `internal/app` and +therefore prevents an import cycle. Both files are generated from the same +authored CLI contract in one invocation and have no independently editable +vocabulary. Freshness is checked by: + +```bash +npm run command-contract:check +go test ./internal/tools/commandcontractgen ./internal/app -run CLIContract +``` + +`package.json` defines `command-contract:check` as +`go run ./internal/tools/commandcontractgen --check` and includes it in the +mandatory `npm run check` chain before Go/package closeout. + +Design: + +- use a deliberately bounded `root_shape_only` definition grammar: each + direction declares sorted, condition-complete variants with a root kind + (`object`, `array`, or explicitly unconstrained `json_value`), exact + top-level allowed/required fields for object roots, and exact CLI flag/mode + conditions; a direction with multiple bounded root kinds declares + `rootType=union`, while `json_value` is forbidden as a union escape hatch; +- the adoption output definition may opt into + `cli_flag_conjunction_v1`; every condition then uses one canonical + ASCII-space-separated conjunction over the same sorted flag dimensions, + every dimension has either exact absent/present states or exact literal + values, every dimension is an allowed command flag, and assignments owned by + different variants are pairwise disjoint; a second definition is rejected + until its own native-domain and argv-closure witness is admitted; +- `internal/tools/commandcontractgen/condition_model.go` owns that pure grammar + and disjointness algorithm; generator I/O and projection remain in `main.go`; +- every required-input command receives an `inputContract` containing: + `schemaVersion`, the bounded root-shape definition and canonical digest, + owner refs, exact native admission witness selector, and an explicit + non-claim for nested fields, scalar types, collection cardinality, + nullability, and cross-field semantics; +- every JSON output command receives an `outputContract` whose variants cover + every supported JSON-producing flag/mode route, including object/array + unions and agent-envelope projections; +- the generated private Go projection supplies the bounded direct-help summary + and flag choices; these values are not manually repeated in descriptors; +- the canonical ABI projection includes each bounded input/output definition, + canonical digest, scope, flags, flag choices, and output modes; +- generator checks require complete required-input and JSON-output coverage, + generated-file freshness, sorted and unique root variants, non-empty exact + conditions, valid root-kind/field combinations, and an executable native + witness selector; +- native source digests remain conservative freshness sentinels only: they + deliberately force review after owner-package code changes but are not + semantic-equivalence proof; +- direct public-CLI tests exercise every high-risk multi-mode route and assert + its root kind and exact root keys; command-local witnesses remain the + evidence for individual native admission and output behavior; +- the adoption output condition oracle enumerates the finite + `5 modes x 2 agent states x 2 materialization states x 4 pilot states` + domain derived from immutable copies of the same internal native value lists + that build option-admission maps, requires native option admission to accept + exactly the same twelve assignments declared by the condition model, derives + each exercised condition from parsed argv, and rejects repeated `--mode` or + `--pilot` selectors before they can change the selected assignment; +- contract mutation tests cover root kind, allowed/required fields, variant + conditions, schema versions, deleted definitions, digests, selectors, and + either generated output becoming stale; +- the contract does not become a second runtime validator: owner-native + positive/negative admission tests remain required evidence, and the public + contract explicitly denies nested, typed, cardinality, nullability, and + cross-field semantic parity. + +Rejected lower-cost alternative: + +A source-file hash alone detects irrelevant refactors and still does not state +the contract. The first implementation's inferred nested record graph is also +rejected: it attached records by field-name heuristics and produced +demonstrably false scalar-to-record associations. A generic owner string does +not expose compatibility. Merely adding `without` prose to overlapping +conditions is also rejected because free text cannot prove dimension closure, +canonicality, or pairwise disjointness. A general SAT or CLI grammar is +unnecessary: the optional four-dimension conjunction model closes the only +confirmed machine-selection boundary. + +Proof invariant: + +Changing a declared root kind, top-level field, requiredness rule, schema +version, or JSON-producing flag/mode variant changes the public ABI projection +and fails the golden until reviewed. Changing native owner-package source also +forces review without being mislabeled as semantic proof. + +Non-claims: + +The summary is not JSON Schema. It does not describe nested fields, scalar +types, array item types or cardinality, nullability, or cross-field semantics, +and it does not replace command admission tests. + +Rollback or overturn condition: + +A future versioned JSON Schema surface may supersede these closed machine +projections after demonstrating lower duplication and exact parity. + +Why this avoids accidental complexity: + +It uses one bounded root-shape registry and deterministic projection generator +instead of a false full-schema model, two manually authored public/private +schema copies, or a schema service. + +Why this avoids premature over-decomposition: + +The public contract is authored once; generated Go metadata is derived and +freshness-checked. + +### D-07: Pre-1.0 compatibility + +Problem: + +A breaking `0.1.x` patch is selected by a common caret range. + +Chosen owner boundary: + +Release change admission and package-public install documentation jointly own +the policy. + +Design: + +- canonical npm installation uses `--save-exact`; +- README and generated release notes state that pre-1.0 consumers must + exact-pin; generated npm install and rollback commands include + `--save-exact` and the rollback route names the literal admitted previous + version; +- replace `release/change-record.v1.json` with + `release/change-record.v2.json`; update `releasechange.RecordPath`, release + input composers, manifests, closeout, workflow paths, bindings, package + checks, and documentation in the same slice; +- change-record schema v2 gains exact `previousVersion` and closed + `changeClass=compatible|breaking`; +- admission derives that non-empty `breakingChanges` or required migration + requires `changeClass=breaking`; +- release validation rejects non-empty breaking changes or required migration + when the new version is only a patch over the previous pre-1.0 version; +- a future breaking pre-1.0 release must increment the minor version; +- no v1 adapter is added because no durable consumer requires one; the + version-bound record is a repository release input, and retaining two active + paths would create ambiguous authority; +- this change advances all synchronized package, Python, release record, + manifest, notes, and contract metadata from `0.1.160` to `0.2.0` with + `previousVersion=0.1.160`; +- the already published `v0.1.160` is not changed, republished, or backfilled. + +Rejected lower-cost alternative: + +Documentation alone does not prevent a future incompatible patch. A validator +alone does not protect existing range consumers. + +Proof invariant: + +For `0.m.p -> 0.m.(p+1)`, breaking changes or required migration fail release +admission; `0.1.160 -> 0.2.0` is admitted as breaking; install and rollback +release-note projections both preserve exact npm dependency pins. + +Non-claims: + +This does not prove registry publication, downstream lockfile use, or adoption. + +Rollback or overturn condition: + +Only a versioned release policy with an equally strong consumer-compatibility +proof may replace exact pins and minor bumps. + +Why this avoids accidental complexity: + +It adds two version facts and one semver comparison to the existing owner. + +Why this avoids premature over-decomposition: + +No new versioning package is needed unless another owner reuses the algorithm. + +### D-08: Independent blocking falsifiers + +Problem: + +Aggregate positive fixtures allow individual blocking conjuncts to become +inert without failing tests. + +Chosen owner boundary: + +Existing self-hosting, coverage, package verification, and release tests. + +Design: + +- extract pure verdict functions only where subprocess coupling currently + prevents testing; +- mutate one predicate per table row; +- cover self-hosting report state, wheel SHA, version match, local/CI receipt + identity, tarball root deny-list, command-route closure, and linkage + dead-zone fields; +- retain route-only metrics as non-claims where the owner explicitly makes + them non-blocking; +- do not convert line coverage into semantic proof. + +Rejected lower-cost alternative: + +One fixture with every field wrong cannot prove that every conjunct matters. + +Proof invariant: + +Removing or inverting any single blocking predicate fails at least its named +negative case, while the neighboring positive fixture remains green. + +Non-claims: + +These tests do not prove exhaustive input coverage or provider state. + +Rollback or overturn condition: + +A predicate may lose its falsifier only if the durable requirement removes it +from the blocking conjunction. + +Why this avoids accidental complexity: + +Pure helpers are admitted only at existing side-effect boundaries. + +Why this avoids premature over-decomposition: + +No test utility package is added for one-use helpers. + +### D-09: Continuous onboarding + +Problem: + +The shipped path breaks between local install, executable resolution, family +discovery, preset vocabulary, and first valid input. + +Chosen owner boundary: + +README, direct/root help, descriptors, stack preset inventory, package smoke, +and one marker-bounded example. + +Design: + +- npm install uses `--save-exact`; +- commands use `npm exec --offline -- agentic-proofkit`; +- Bun is not presented as a verified canonical onboarding route in this + change; adding it later requires an exact-pin artifact smoke equivalent to + the npm witness; +- root help projects the `help families` route; +- the projected route is the copyable + `npm exec --offline -- agentic-proofkit help families` command used by the + installed consumer, and its witness executes argv parsed from those exact + displayed bytes; +- the authored CLI contract is the sole editable preset-ID owner; + `stackpreset.IDs()` returns a defensive copy of its generated lower-package + projection, while direct help and diagnostics use that API and app metadata + uses the sibling generated projection; +- README includes one minimal valid requirement source and a tested command; +- package verification installs the exact tarball and executes one continuous + offline witness trace through the installed artifact: root help, family + discovery, every stack preset ID, extraction of the marker-bounded example + from the installed README, and successful admission of that example; +- one immutable `cliexec` renderer owns shell quoting and a previously admitted + invocation prefix; it has exactly three profiles: + `npm_offline` renders `npm exec --offline -- agentic-proofkit`, + `python_module` renders the absolute admitted interpreter followed by + `-m agentic_proofkit`, and `path` renders `agentic-proofkit`; +- the npm shell wrapper and Python wrapper overwrite private launcher + environment fields `AGENTIC_PROOFKIT_LAUNCHER_PROFILE` and + `AGENTIC_PROOFKIT_PYTHON_EXECUTABLE` before `exec`; the closed admission + matrix is `("", "")` or `("path", "")` to `path`, + `("npm_offline", "")` to `npm_offline`, and + `("python_module", )` to + `python_module`; every unknown profile, relative or empty Python executable, + secret-like, control-bearing, or format-bearing executable, or executable + field supplied with another profile is rejected without disclosing the + rejected value; the Go process boundary admits those + fields once and passes the renderer explicitly through app and command + builders, with no package-manager, executable, `PATH`, or repository-state + autodetection; +- the exact current Proofkit-owned generated-command field inventory is closed + over: + `$.diagnostics[?key=preset].value.suggestedCommands[*]` for stack-preset; + `$.nextCommands[*]`, + `$.agentActionPlan[?phase=verify].commands[*]`, + `$.payloads.adoptionGuidance.agentGuidance.commands[callerCommandCount:]`, + `$.report.diagnostics[?key=agentActionPlan].value[?phase=verify].commands[*]`, + and `$.report.diagnostics[?key=nextCommands].value[*]` for bootstrap JSON; + `$.commands[*].command` for its agent envelope; + `$.nextCommands[*]` plus decoded + `$.files[?payloadKey=adoptionGuidance].content::$.agentGuidance.commands[callerCommandCount:]` + for its materialization manifest; + the same bootstrap display-command locations below `$.bootstrapReport`, plus + `$.materializationManifest.nextCommands[*]` and decoded + `$.materializationManifest.files[?purpose=caller-owned gradual adoption + guidance input].content::$.agentGuidance.commands[callerCommandCount:]` + for project-structure JSON; and `$.commands[*].command` for the + project-structure agent envelope; adding, removing, or relocating a producer + or field requires the same inventory, requirement, and witness update; +- the structured-argv inventory is separately closed over + `$.nextCommands[*].argv`, agent-envelope `$.commands[*].argv`, and exact + agent-envelope `$.commands[*].display == cliexec.DisplayArgv(argv)` for + agent-route; release-phase `$.phases[?phase=release].commands[*].argv` for a + direct and aggregate adoption workflow; `$.commands[*].argv` plus exact + `command == cliexec.DisplayArgv(argv)` for their agent envelopes; + failure-rerun `$.commands[*].argv` for requirement coverage; and project + `$.adoptionWorkflowPlan.phases[?phase=profile].commands[*].argv`, + `$.adoptionWorkflowPlan.phases[?phase=bootstrap].commands[*].argv`, and + `$.adoptionWorkflowPlan.phases[?phase=bind].commands[*].argv` with exact + counts `2`, `2`, and `3`; project source-report identity is derived from the + same renderer-owned workflow record; +- the textual help inventory is closed over the root family-discovery route, + every family route, every family-to-leaf route, every descriptor's installed + invocation, the help descriptor's exact authored help forms, and every + stack-preset copyable route; path, npm-offline, and Python-module profiles + must render every slot exactly once; +- caller-owned bootstrap `commands` bytes remain unchanged and are proved by + `TestBootstrapPreservesCallerDisplayCommandInGuidancePayload`; specifically, + the prefix + `$.payloads.adoptionGuidance.agentGuidance.commands[0:callerCommandCount]` + and its decoded materialization copies remain caller-owned while only the + suffix is renderer-owned; +- caller-owned native-witness argv below the project bootstrap report remains + byte-for-byte equal to the admitted bootstrap input and is excluded from the + Proofkit-owned structured-argv inventory; +- the installed npm trace parses every preset's exact generated command + strings through the bounded literal-word boundary, requires the exact npm + prefix on every string, and re-executes a self-continuation from those exact + JSON bytes; +- the installed wheel trace invokes a preset through the installed Python + module, requires every emitted command to use its exact venv interpreter and + `-m agentic_proofkit`, directly re-executes a self-continuation, traverses + root help through every family and leaf help route, emits an agent-route argv + with the same immutable prefix, and directly executes that argv with npm + absent from `PATH`; route extraction admits only exact authored four-space + indentation and one canonical lower-case command operand, rejects whitespace + and shell-expansion mutants, and never turns generated stdout into shell + authority; +- Python docs state `python -m agentic_proofkit` and `uv run + agentic-proofkit`, supported targets, Python minimum, wrapper-not-SDK, and + explicit registry-availability non-claims; +- a marker-bounded platform block is projected exactly from + `releaseplatform.Targets()`, macOS minimum 12, manylinux 2.17 arm64/x64, + Python `>=3.9`, and explicit Windows non-support; a docs test compares every + row with the private owners; +- Python examples include the complete conditional install-to-invoke chain: + `python -m pip install agentic-proofkit==` then + `python -m agentic_proofkit`, and + `uv add --dev agentic-proofkit==` then + `uv run agentic-proofkit`, without implying that a current registry version + exists; +- remove contributor-only `AGENTS.md` and `CONTRIBUTING.md` from the npm + package, remove the active-backlog route from package-public README, and add + a field-aware package-reference-closure falsifier; +- update or exclude package projections such as + `receipt-producer-policy.local.developer` that cite contributor-only files, + and classify self-hosting witness selectors that name `AGENTS.md`, + `BACKLOG.md`, or `CONTRIBUTING.md` as source-checkout-only rather than + package-consumer routes; +- `BACKLOG.md` remains a source-checkout owner and is not shipped as + version-specific consumer documentation. + +Exact C-89 proof routes: + +| Requirement | Scenario | Witness path | Selector | Executable command | +| --- | --- | --- | --- | --- | +| `REQ-PROOFKIT-PACKAGE-002` | `proofkit.package-boundary.launcher-profile-admission` | `internal/kernel/cliexec/cliexec_test.go` | `TestLauncherProfileAdmissionMatrix` | `go test ./internal/kernel/cliexec -run '^TestLauncherProfileAdmissionMatrix$'` | +| `REQ-PROOFKIT-PACKAGE-002` | `proofkit.package-boundary.generated-command-field-inventory` | `internal/app/invocation_profile_test.go` | `TestGeneratedCommandInvocationProfileFieldInventory` | `go test ./internal/app -run '^TestGeneratedCommandInvocationProfileFieldInventory$'` | +| `REQ-PROOFKIT-PACKAGE-002` | `proofkit.package-boundary.generated-command-field-inventory` | `internal/app/invocation_profile_test.go` | `TestGeneratedCommandInvocationProfileRouteClosure` | `go test ./internal/app -run '^TestGeneratedCommandInvocationProfileRouteClosure$'` | +| `REQ-PROOFKIT-PACKAGE-002` | `proofkit.package-boundary.generated-command-caller-preservation` | `internal/command/gradualadoption/gradualadoption_test.go` | `TestBootstrapPreservesCallerDisplayCommandInGuidancePayload` | `go test ./internal/command/gradualadoption -run '^TestBootstrapPreservesCallerDisplayCommandInGuidancePayload$'` | +| `REQ-PROOFKIT-PACKAGE-003` | `proofkit.package-boundary.outside-consumer-artifact` | `internal/tools/packageverify/main_test.go` | `TestExactTarballOnboardingTrace` | `go test ./internal/tools/packageverify -run '^TestExactTarballOnboardingTrace$'` | +| `REQ-PROOFKIT-PACKAGE-006` | `proofkit.package-boundary.python-wheel-generated-continuation` | `internal/tools/pythonpackage/continuation_test.go` | `TestInstalledWheelContinuationUsesExactPythonModuleProfileWithoutNPM` | `go test ./internal/tools/pythonpackage -run '^TestInstalledWheelContinuationUsesExactPythonModuleProfileWithoutNPM$'` | +| `REQ-PROOFKIT-PACKAGE-006` | `proofkit.package-boundary.python-wheel-generated-continuation` | `internal/tools/pythonpackage/continuation_test.go` | `TestExactDisplayedRouteOperandsRejectsWhitespaceAndExpansionMutants` | `go test ./internal/tools/pythonpackage -run '^TestExactDisplayedRouteOperandsRejectsWhitespaceAndExpansionMutants$'` | +| `REQ-PROOFKIT-QUALITY-019` | `proofkit.supply-chain-quality.installed-package-json-abi-smoke` | `internal/tools/packageverify/main_test.go` | `TestExactTarballOnboardingTrace` | `go test ./internal/tools/packageverify -run '^TestExactTarballOnboardingTrace$'` | +| `REQ-PROOFKIT-QUALITY-024` | `proofkit.supply-chain-quality.release-change-record-projection` | `internal/tools/releasechange/record_test.go` | `TestCurrentChangeRecordNamesReviewedSemanticChanges` | `go test ./internal/tools/releasechange -run '^TestCurrentChangeRecordNamesReviewedSemanticChanges$'` | + +The coverage owner admits these rows as an exact critical inventory and rejects +empty, missing, surplus, selector substitution, witness relocation, executable +command drift, requirement transfer, and scenario transfer. + +Rejected lower-cost alternative: + +Global installation, bare `npx`, or network fallback changes package identity. +Printing all allowed keys on every malformed input is noisy and does not create +a successful first route. + +Proof invariant: + +For the installed `npm_offline` and `python_module` channels and every field in +the closed generated-command inventory, the emitted command resolves the same +candidate artifact without network fallback; both temporary consumers can +execute an exact emitted self-continuation. The `path` profile preserves only +the canonical bare executable token and caller-owned resolution, without an +artifact-identity claim. Caller-owned display bytes are preserved in every +profile. + +Non-claims: + +The docs do not prove npm or PyPI publication, Bun support execution, shell +portability outside supported package targets, or consumer adoption. A direct +binary uses the `path` profile and therefore still requires the caller to make +`agentic-proofkit` resolvable. + +Rollback or overturn condition: + +If a documented package channel is removed from durable release owners, its +route and test must be removed together. + +Why this avoids accidental complexity: + +It projects existing owners and adds no command. + +Why this avoids premature over-decomposition: + +The only shared field is descriptor flag-value choices. + +### D-10: Browser state and narrow accessibility proof + +Problem: + +Initial markup, terminal failures, 320-pixel layout, controls, and axe coverage +do not satisfy a coherent stable-state contract. + +Chosen owner boundary: + +Existing workspace HTML/assets and Playwright witness. + +Design: + +- server HTML contains an initial loading status; +- initialization catches manifest failure and renders a sanitized alert; +- request failures use the same terminal state vocabulary; +- native list and article semantics replace the unjustified ARIA tree; +- handoff output uses a visible heading and labelled region; +- active view controls expose `aria-current`; +- grid children use `min-width: 0`, text can wrap, navigation wraps, and + graph/table overflow is confined to labelled internal viewports; +- explicit light/dark control tokens preserve forced-colors adaptation; +- axe runs on bootstrap loading, bootstrap failure, specifications loading, + specifications, specifications no-match, diff loading, diff, graph loading, + graph, unavailable, failed, and handoff-result states; +- the Playwright matrix declares for each state its deterministic route + interception or deferred-response barrier, exact body and content + `data-state` where applicable, heading, and applicable axe/reflow checks; the + observed state identity is asserted before every oracle; +- bootstrap loading is held by a deferred manifest response, and each + specifications, diff, and graph loading state is independently held by its + own deferred view response; every barrier is released only after its complete + row oracle, and bootstrap, view, and handoff failures are separate rows; +- `target-size` is explicitly enabled, applies to representative controls, + has zero violations, and an undersized-control negative fixture proves that + the rule would fail; +- a 320 by 800 viewport asserts no document-level horizontal overflow after + each view transition; +- computed contrast checks read actual rendered controls, adjacent + backgrounds, border colors, opacity, and focused outline styles in pinned + engines and light/dark schemes rather than merely checking token values; +- replacing the synthetic tree intentionally removes its ArrowUp/ArrowDown + roving-focus contract; tests preserve standard Tab/Shift+Tab traversal, + Enter/Space activation, selection, and handoff semantics. + +Rejected lower-cost alternative: + +`overflow-x: hidden` hides data. Keeping a synthetic tree adds an unsupported +keyboard contract. A single final-state axe run does not prove initial or +failure states. + +Proof invariant: + +Every stable state is non-empty, has no default axe violation, executes the +target-size rule with no violation, and reflows without document overflow +where applicable. + +Non-claims: + +The witness does not establish complete WCAG conformance, branded Safari +behavior, screen-reader interoperability, all OS themes, or 400-percent zoom. + +Rollback or overturn condition: + +New stable UI states must enter the state matrix or be explicitly classified +as transient and inaccessible to users. + +Why this avoids accidental complexity: + +It removes an ARIA widget and centralizes one test helper. + +Why this avoids premature over-decomposition: + +CSS and rendering remain in the existing asset owner. + +### D-11: Honest digest-coverage naming + +Problem: + +`baselineVerification=verified` sounds like provenance even though it means +only that all caller-provided expected digests match current bytes. + +Chosen owner boundary: + +Versioned requirement-context snapshots and downstream diff/browser +projections. + +Design: + +- snapshot schema v2 emits `expectedDigestCoverage: none|partial|all`; +- v1 input is first fully admitted under the complete v1 contract, then + normalized by an explicit legacy adapter: + `unverified -> none`, `partially_verified -> partial`, `verified -> all`; +- all producers emit v2; +- semantic-diff input/output, workspace manifest, and affected HTTP + projections advance to their own schema v2 envelopes and use the new name; +- each affected v1 envelope has a strict v1 adapter, mixed v1/v2 keys are + rejected, and migration tests cover v1 admission, normalized v2 equality, + v2 production, and stable rejection of malformed legacy data; +- UI says `Expected-digest coverage`, never `Baseline verified`; +- requirements and non-claims state that coverage does not authenticate a + producer, baseline, checkout, or freshness. + +Rejected lower-cost alternative: + +A producer marker inside caller-computable data creates no provenance. +Changing UI text alone leaves the wire contract misleading. + +Proof invariant: + +Self-consistent caller data remains admissible. The legacy verification term +may appear only inside the strict v1 adapter, migration fixtures, and +compatibility diagnostics; no v2 output, current contract, direct help, or UI +calls digest coverage verified. + +Non-claims: + +No signatures, trusted producer, repository freshness, or merge authority are +added. + +Rollback or overturn condition: + +A future authenticated snapshot format may introduce a separate provenance +field with its own trust root; it must not reuse digest coverage. + +Why this avoids accidental complexity: + +One versioned rename removes a false semantic implication. + +Why this avoids premature over-decomposition: + +The legacy adapter stays in the snapshot model owner. + +### D-12: Semantic Markdown phrase equivalence + +Problem: + +Readiness overclaim scanning compares source bytes that can contain one +semicolon-terminated CommonMark character reference equivalent to a direct +policy phrase. + +Chosen owner boundary: + +The readiness command keeps structural table parsing and phrase policy local. + +Design: + +- parse rows and cells from original Markdown bytes; +- decode exactly one strict semicolon-terminated named, decimal, or hexadecimal + CommonMark character reference only in extracted textual segments before + phrase normalization; +- use a bounded recognizer around the standard entity table rather than + applying permissive HTML decoding to arbitrary ampersand text; +- normalize policy phrases through the same helper; +- do not decode before pipe parsing; +- do not recursively decode double-encoded values. + +Rejected lower-cost alternative: + +Rejecting every ampersand breaks legitimate Markdown and still does not state +visible-text semantics. + +Proof invariant: + +Semicolon-terminated named, decimal, and hexadecimal references classify +identically to direct text; missing-semicolon and double-encoded references +remain literal after one pass. + +Non-claims: + +This is not a full Markdown parser or extraction-completeness proof. + +Rollback or overturn condition: + +A full admitted Markdown AST may supersede the helper only with equivalence +falsifiers. + +Why this avoids accidental complexity: + +One standard-library decode at the semantic boundary is sufficient. + +Why this avoids premature over-decomposition: + +The helper remains command-local until reused by another policy owner. + +## Documentation topology + +The closed size audit uses a deterministic suspicion threshold: +`LOC >= 1000 or bytes >= 65536`. Crossing the threshold is necessary only for +this ledger, not sufficient for a god-file verdict. A proven god file also +requires at least two independent semantic owners and observed or reproducible +material harm from their concentration. + +Candidate snapshot ledger: + +| Path | LOC | Bytes | Disposition | Owner proof | +|---|---:|---:|---|---| +| `proofkit/cli-contract.v2.json` | 14,983 | 537,788 | Suspicious size; not god | One generated public CLI-contract projection with freshness and ABI oracles | +| `docs/implementation/audit-remediation-plan.md` | 4,077 | 187,518 | Temporary oversized execution document | One reviewed implementation graph; retirement is required by the closeout predicate | +| `proofkit/requirement-bindings.json` | 3,507 | 142,191 | Suspicious size; not god | One canonical binding registry whose global order and linkage closure require one record | +| `scripts/workflow_package_gate_oracle_test.go` | 2,606 | 80,168 | Remediated god-file; residual suspicious cluster | Five-owner form was split; remaining QUALITY-011/013 scenarios share an exact selector and single-path binding contract | +| `internal/tools/packageverify/main.go` | 2,590 | 92,501 | Suspicious size; not proven god | One npm artifact admission boundary; helper extraction requires a second durable consumer or independent change reason | +| `docs/implementation/audit-remediation-design.md` | 3,182 | 248,568 | Temporary oversized design document | One adjudicated correction ledger; retirement is required after durable-owner closeout | +| `internal/app/cli_abi_test.go` | 2,316 | 112,885 | Suspicious size; not god | One public CLI ABI corpus and golden identity | +| `internal/app/cli_contract_test.go` | 2,096 | 87,274 | Suspicious size; not god | One CLI contract-admission and native-source parity corpus | +| `internal/tools/releasecloseoutinput/main_test.go` | 1,757 | 72,617 | Suspicious size; not god | One release-closeout input anti-corruption boundary | +| `internal/tools/packageverify/main_test.go` | 1,752 | 64,724 | Suspicious size; not god | One npm artifact verifier corpus | +| `internal/command/agentroute/agentroute_test.go` | 1,683 | 55,743 | Suspicious size; not god | One command owner and its complete behavioral corpus | +| `internal/tools/releasecloseoutinput/main.go` | 1,674 | 63,256 | Suspicious size; not god | One closeout projection owner | +| `proofkit/witness-plan.json` | 1,440 | 37,233 | Suspicious size; not god | One generated global witness plan | +| `internal/command/testevidenceinventory/testevidenceinventory_test.go` | 1,310 | 52,773 | Suspicious size; not god | One test-evidence inventory command corpus | +| `internal/command/repoprofileadmission/repo_profile_admission.go` | 1,280 | 42,499 | Suspicious size; not god | One repository-profile admission state machine | +| `internal/command/requirementcoverageview/requirementcoverageview_test.go` | 1,276 | 54,939 | Suspicious size; not god | One requirement-coverage view corpus | +| `internal/app/app_test.go` | 1,266 | 50,345 | Suspicious size; not god | One top-level command dispatcher and process-channel corpus | +| `internal/command/requirementbinding/requirementbinding.go` | 1,155 | 37,562 | Suspicious size; not god | One requirement-binding admission owner | +| `internal/command/pilotadmission/pilotadmission.go` | 1,154 | 43,660 | Suspicious size; not god | One pilot-admission command owner | +| `internal/command/agentroute/agentroute.go` | 1,121 | 51,789 | Suspicious size; not god | One agent-route command owner | +| `tests/browser/workspace.spec.mjs` | 1,416 | 60,370 | Suspicious size; not proven god | One end-to-end browser contract corpus; helper extraction would split shared state and add a one-consumer abstraction | +| `internal/command/workspaceregistry/workspaceregistry.go` | 1,096 | 34,838 | Suspicious size; not god | One workspace-registry command owner | +| `internal/command/bindingpartition/bindingpartition.go` | 1,086 | 39,825 | Suspicious size; not god | One binding-partition command owner | +| `internal/command/releaseauthority/releaseauthority.go` | 1,083 | 39,167 | Suspicious size; not god | One release-authority command owner | +| `.github/workflows/release.yml` | 1,069 | 49,849 | Suspicious size; not god | One event/needs release state machine; splitting jobs into reusable workflows would change trust and permission semantics | +| `internal/command/testevidenceinventory/testevidenceinventory.go` | 1,033 | 35,981 | Suspicious size; not god | One test-evidence inventory command owner | +| `internal/command/capabilitymapadmission/capability_map_admission.go` | 1,015 | 34,953 | Suspicious size; not god | One capability-map admission command owner | +| `internal/command/readinesscloseout/readinesscloseout.go` | 1,014 | 33,435 | Suspicious size; not god | One readiness-closeout command owner | +| `internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source.go` | 1,008 | 34,655 | Suspicious size; not god | One JSON-report CLI-adapter source owner | +| `internal/tools/releasemanifest/main.go` | 1,006 | 36,543 | Suspicious size; not god | One release-manifest construction boundary | +| `internal/command/conformanceprofile/conformanceprofile.go` | 1,000 | 35,766 | Suspicious size; not god | One conformance-profile command owner | + +C-60 decomposes the confirmed concentration as follows: + +- `workflow_package_gate_oracle_test.go` retains the inseparable + QUALITY-011/013 merge/package-gate proof cluster; +- `workflow_oracle_support_test.go` contains shared typed YAML and neutral + helpers and contains no `Test*` selector; +- browser runtime, runtime preconditions, workflow source policy, and security + scanner policy each have their own test file; +- PACKAGE-005, QUALITY-022, and QUALITY-025 bindings point to the new semantic + owners and exact selector inventories reject deletion, surplus, + owner-transfer, or stale-path substitution. +- QUALITY-005, QUALITY-006, and QUALITY-007 bind the scanner-policy selectors + to their extracted owner, and the same exact selector-and-path inventory + rejects deletion, surplus, owner transfer, or relocation. + +The reverse decomposition audit found no proven merge. It inspected every Go +file at or below 40 LOC and every package with at least eight Go files. +The strongest candidates were rejected for explicit boundaries: + +- `requirementbrowser/v1_adapter.go` owns a retireable wire-version adapter; +- generated preset IDs are generator-owned output in a different Go package; +- `cmd/agentic-proofkit/main.go` is the executable boundary; +- browser `assets.go` is the embed boundary; +- small app command wrappers preserve command-route and native-source review + identities, while merging them would not remove a dependency or duplicate + algorithm. + +Reopen a split or merge only when owner evidence proves an independent change +reason, duplicated algorithm, dependency-cycle reduction, or measurable review +harm. File count, line count, and aesthetic preference alone are insufficient. +New helpers remain admissible only when they isolate a pure predicate, project +one private owner, bind a confined handle, or serve the shared workflow +anti-corruption boundary demonstrated above. + +Implementation-only documents stay outside `package.json.files`. Contributor +governance and `BACKLOG.md` also remain source-checkout surfaces: an installed +runtime dependency must not expose incomplete repository-governance routes or +version-specific work rows. + +### D-13: Immutable release topology and exact browser side effects + +Problem: + +The release workflow can add missing assets to an existing GitHub Release even +though the release owner calls historical release evidence immutable. The +browser owner separately uses an over-broad prohibition that appears to ban its +own fixed `--open` launcher, and one renderer diagnostic omits `workspace`. + +Chosen owner boundary: + +The release workflow may validate but never mutate an existing release. The +browser server may invoke only a fixed platform launcher with its own admitted +loopback URL. View vocabulary remains local to the browser command and app +parity tests. + +Design: + +- remove the existing-release missing-asset upload branch; +- existing releases must have the exact expected asset names and bytes or fail + with a terminal nonzero result before any provider mutation; +- admit a closed read-only provider command set for the existing-release path + (`gh release view` and `gh release download` in exact owner-approved forms); +- compare the entire existing-release shell block to one canonical + owner-reviewed source form, including every local and provider operation; + any `curl`, alternate network client, shell indirection, additional command, + or other source change fails the oracle until a new owner-reviewed form and + negative case are admitted; +- historical exceptions may be recorded outside successful release evidence + but never authorize upload or a passing release result; +- refine `SPEC-021` to prohibit caller-supplied and native-witness command + execution while permitting the fixed browser launcher; +- inject the launcher operation for tests and require fixed executable/argv + forms plus an admitted loopback URL; +- update the renderer diagnostic to include `workspace` and add app/runtime + vocabulary parity. + +Rejected lower-cost alternative: + +Add-only backfill still mutates historical evidence topology. Removing +`--open` breaks the admitted one-shot workflow. A new shared enum package for +two local projections is premature. + +Proof invariant: + +An incomplete existing release terminates nonzero before any provider +mutation, and no caller value can select the browser executable or add launcher +arguments. + +Non-claims: + +Source tests do not prove provider immutability, actual release assets, OS +browser profile identity, or that the browser rendered successfully. + +Rollback or overturn condition: + +Only a versioned release owner may define a mutable evidence class. A future +launcher expansion requires a new trust-boundary contract. + +Why this avoids accidental complexity: + +It deletes a mutation path and adds narrow injected-operation tests. + +Why this avoids premature over-decomposition: + +Release and browser vocabularies remain with their current owners. + +## Compatibility and business-logic proof + +Expected intentional public changes: + +- blocked prerequisites can no longer return a successful adoption report; +- readiness closeout decodes one strict semicolon-terminated CommonMark or HTML + character reference pass before policy phrase matching, so a forbidden + phrase hidden by one such reference now fails closed; +- malformed input for the nine affected commands moves from JSON `stdout` to + diagnostic `stderr`; +- CLI contract compatibility projection expands; +- `pilot-admission --pilot all` contract envelopes admit one strict first and + stack-diverse input pair and return those ordered pilot reports; +- requirement binding admission and output preserve optional + `witnessSelectors` selector-and-command records; +- requirement-context, semantic-diff, workspace manifest, and affected HTTP + projection outputs advance to v2 with strict v1 input adapters; +- package/release metadata advances from `0.1.160` to `0.2.0`; +- future breaking pre-1.0 patch releases are rejected; +- absolute TypeScript source/package symlink targets are rejected; relative + in-root symlinks remain supported; +- help and docs expose existing routes and preset values; +- synthetic ArrowUp/ArrowDown tree focus behavior is removed with the + unjustified ARIA tree; standard Tab/Shift+Tab and Enter/Space behavior is + preserved. + +Preserved behavior: + +- valid admitted inputs retain the same semantic decisions except for the + explicitly declared readiness-closeout normalization and migrations above; +- semantic failed reports remain JSON reports; +- observe/warn modes remain advisory for non-blocked gaps; +- stack preset IDs and profiles do not change; +- output bytes and mode on a stable safe path remain deterministic; +- scanner export grammar and comparison do not expand; +- browser projections remain presentation-only; +- package artifacts remain candidate evidence until provider and registry proof + exists. + +For every intentional public change, the change record, contract projection, +requirements, bindings, and migration text must agree before closeout. + +## Durable proof routing + +Every row below is an implementation obligation. Proposed new scenario and +witness IDs become exact binding identities in +`proofkit/requirement-bindings.json`; existing identities are retained where +they already own the boundary. Test selectors are fixed before production +edits. + +| Finding | Exact requirements | Binding scenario and witness | Exact path, selector, and command | Required non-claim delta | +|---|---|---|---|---| +| R-01 | `REQ-PROOFKIT-RETIRE-008`, `REQ-PROOFKIT-QUALITY-024` | extend `proofkit.consumer-infra-retirement.adoption-doctor-enforcement` / `proofkit.adoption-doctor.enforcement-and-envelope` and `proofkit.supply-chain-quality.release-change-record-projection` / `proofkit.release-change.versioned-projection-falsifier` | `internal/command/adoptiondoctor/adoptiondoctor_test.go`, `TestBuildReportsObserveAndWarnWithoutBlockingAdvisoryGaps`, `TestBuildEnforceTouchedSkipsGapsOutsideTouchedSelection`, `TestBuildBlocksEveryModeForExternalPreconditions`; `internal/tools/releasechange/record_test.go`, `TestCurrentChangeRecordNamesReviewedSemanticChanges`; `go test ./internal/command/adoptiondoctor ./internal/tools/releasechange` | Advisory mode does not authenticate or satisfy blocked evidence; a local record does not prove consumer migration | +| R-02 | `REQ-PROOFKIT-QUALITY-002` | extend `proofkit.supply-chain-quality.release-sbom` / `proofkit.release-sbom.deterministic-inventory` | `internal/tools/releasesbom/main_test.go`, `TestArtifactSpecificRuntimeEdgesAndExcludedInventory`, `TestReleaseFileEvidenceRejectsDeterministicIdentitySwap`, `TestReleaseFileEvidenceRejectsDeterministicInPlaceMutation`, `go test ./internal/tools/releasesbom` | Required scope and edges do not prove reachability, vulnerability absence, or license approval | +| R-03 | `REQ-PROOFKIT-PACKAGE-002` | extend `proofkit.package-boundary.typescript-explicit-scan-topology` / `proofkit.typescript-public-api.explicit-scan-topology` | `internal/command/publicapi/public_api_test.go`, `TestScanCacheBindsBytesToFirstCanonicalIdentityAcrossSymlinkRetarget`, `TestCanonicalSourceSnapshotRejectsChangedCrossAliasAdmission`, `TestVerifyRejectsDeterministicSymlinkSwap`, `TestVerifyPinsPackageRootAcrossInRootSiblingSwap`, `go test ./internal/command/publicapi` | Confined read does not prove checkout freshness or compiler provenance | +| R-04 | `REQ-PROOFKIT-SPEC-009` | extend `proofkit.spec-proof-core.requirement-spec-tree-view-cli-output` / `proofkit.requirement-spec-tree-view.cli-output-path-falsifier` | `internal/app/cli_abi_test.go`, `TestOutputWriterRejectsDeterministicParentSwap`, `go test ./internal/app -run OutputWriter` | Confined atomic rename does not prove protection from same-user content or namespace mutation during the operation or fsync durability | +| R-05 | `REQ-PROOFKIT-QUALITY-013` | extend `proofkit.supply-chain-quality.workflow-package-gate-oracle` / `proofkit.workflow-package-gate.typed-oracle` | `scripts/workflow_package_gate_oracle_test.go`, `TestCIWorkflowDeclaresFailClosedRequiredAggregate`, `TestPackageGateWorkflowOracleAcceptsOwnerCIAndReleaseWorkflows`, `TestWorkflowGuardExpressionsRejectNeutralization`, `TestPackageGateWorkflowOracleRejectsDisabledAndShadowedEvidence`, `TestPackageGateWorkflowOracleRejectsExecutionOverrides`, `TestPackageGateWorkflowOracleRejectsRequiredPriorExecutionOverride`, `TestPackageGateWorkflowOracleRejectsUnusedAllowedStepEnvironment`, `go test ./scripts -run 'CIWorkflowDeclaresFailClosedRequiredAggregate|WorkflowGuard|PackageGateWorkflowOracle'` | Exact source forms do not prove provider execution | +| R-06 | `REQ-PROOFKIT-QUALITY-011` | extend `proofkit.supply-chain-quality.ci-required-aggregate-exactness` / `proofkit.ci.required-aggregate-neutralization-falsifier` | `scripts/workflow_package_gate_oracle_test.go`, `TestCIWorkflowDeclaresFailClosedRequiredAggregate`, `TestCIRequiredAggregateRejectsNeutralizedScript`, `TestCIRequiredAggregateRejectsExecutionOverrides`, `TestCIRequiredAggregateRejectsPlatformSmokeSubstitution`, `go test ./scripts -run 'CIWorkflowDeclaresFailClosedRequiredAggregate|CIRequiredAggregate'` | Local source admission does not prove branch protection | +| R-07 | `REQ-PROOFKIT-QUALITY-004` | extend `proofkit.supply-chain-quality.cli-abi-golden` / `proofkit.cli-abi.golden-corpus` | `internal/app/cli_abi_test.go`, `TestRequiredInputCommandsRouteStructuralErrorsByMode`, `go test ./internal/app -run RequiredInputCommandsRoute` | Diagnostics outside the declared corpus are not byte-stable | +| R-08 | `REQ-PROOFKIT-PACKAGE-002`, `REQ-PROOFKIT-QUALITY-024` | extend `proofkit.package-boundary.readiness-closeout-overclaim-grammar` / `proofkit.readiness-closeout.overclaim-grammar` and `proofkit.supply-chain-quality.release-change-record-projection` / `proofkit.release-change.versioned-projection-falsifier` | `internal/command/readinesscloseout/readinesscloseout_test.go`, `TestPhraseScanDecodesOneStrictCharacterReference`; `internal/tools/releasechange/record_test.go`, `TestCurrentChangeRecordNamesReviewedSemanticChanges`; `go test ./internal/command/readinesscloseout ./internal/tools/releasechange` | The bounded decoder is not a complete Markdown AST; a local change record does not prove consumer migration | +| R-09 | `REQ-PROOFKIT-QUALITY-004`, `REQ-PROOFKIT-PACKAGE-002` | extend `proofkit.supply-chain-quality.cli-contract-topology` / `proofkit.cli-contract.descriptor-parity` | `internal/app/cli_contract_test.go`, `TestCommandDescriptorContractParityRejectsMutations`; `internal/tools/commandcontractgen/main_test.go`, `TestRenderRejectsIncompleteAndStaleCommandContracts`; `npm run command-contract:check`; `go test ./internal/tools/commandcontractgen ./internal/app -run 'CommandDescriptorContractParity|RenderRejectsIncomplete'` | Machine declaration plus native witnesses does not prove every cross-field semantic constraint | +| R-10 | `REQ-PROOFKIT-QUALITY-024` | extend `proofkit.supply-chain-quality.release-change-record-projection` / `proofkit.release-change.versioned-projection-falsifier` | `internal/tools/releasechange/record_test.go`, `TestAdmitEnforcesVersionedChangeClass`, `go test ./internal/tools/releasechange` | Version admission does not prove registry history | +| R-11a | `REQ-PROOFKIT-PACKAGE-004` | add `proofkit.package-boundary.self-hosting-report-verdict` / `proofkit.self-hosting.report-verdict-falsifier` | `scripts/validate-self-hosting-receipts_test.go`, `TestRunProofkitVerdictCases`, `go test ./scripts -run RunProofkit` | Injected operation tests do not prove provider producer identity | +| R-11b | `REQ-PROOFKIT-PACKAGE-006`, `REQ-PROOFKIT-QUALITY-023` | extend `proofkit.package-boundary.python-wheel-candidate` / `proofkit.python-package.boundary` | `scripts/validate-self-hosting-receipts_test.go`, `TestPythonArtifactRefsRejectEachWheelIdentityDefect`, `go test ./scripts -run PythonArtifactRefs` | Local wheel bytes do not prove PyPI bytes | +| R-11c | `REQ-PROOFKIT-PACKAGE-001` | extend `proofkit.package-boundary.root-export-and-deep-import-denial` / `proofkit.package-artifact.boundary` | `internal/tools/packageverify/main_test.go`, `TestVerifyRootPackageRejectsEachForbiddenRootEntry`, `go test ./internal/tools/packageverify` | Local tarball proof does not prove registry tarball identity | +| R-11d | `REQ-PROOFKIT-PACKAGE-004` | extend `proofkit.package-boundary.ci-receipt-anchor` / `proofkit.ci.receipt-anchor` | `scripts/validate-self-hosting-receipts_test.go`, `TestReceiptIDKeepsLocalAndCIIdentitiesDistinct`, `go test ./scripts -run ReceiptID` | Receipt naming does not authenticate a producer | +| R-12 | `REQ-PROOFKIT-QUALITY-010` | extend `proofkit.supply-chain-quality.coverage-metrics` / `proofkit.coverage-metrics.linkage-report` | `internal/tools/coveragemetrics/main_test.go`, `TestEachCommandRouteClosureConjunctHasIndependentFalsifier`, `TestEachLinkageDeadZoneConjunctHasIndependentFalsifier`, `go test ./internal/tools/coveragemetrics` | Static route closure does not satisfy semantic falsifier coverage | +| R-13 | `REQ-PROOFKIT-QUALITY-025` | extend `proofkit.supply-chain-quality.workflow-source-oracles` / `proofkit.workflow.exact-source-oracle-falsifiers` | `scripts/workflow_source_oracles_test.go`, `TestWorkflowExternalActionsUseFullCommitSHAs`, `go test ./scripts -run ExternalActions` | A commit pin does not prove action safety or tag equivalence | +| R-14 | `REQ-PROOFKIT-PACKAGE-003`, `REQ-PROOFKIT-QUALITY-019` | extend `proofkit.package-boundary.outside-consumer-artifact` / `proofkit.package-artifact.outside-consumer` | `internal/tools/packageverify/main_test.go`, `TestExactTarballOnboardingTrace`, `go test ./internal/tools/packageverify -run OnboardingTrace` | Local artifact execution does not prove registry publication | +| R-15 | `REQ-PROOFKIT-SPEC-018` | extend `proofkit.spec-proof-core.command-family-help-compatibility` / `proofkit.command-family-navigation.help-compatibility-falsifier` | `internal/app/command_family_catalog_test.go`, `TestStackPresetVocabularyProjectsFromOneOwner`, `go test ./internal/app -run StackPresetVocabulary` | Presets remain suggestions, not consumer policy | +| R-16 | `REQ-PROOFKIT-SPEC-001`, `REQ-PROOFKIT-QUALITY-019` | add `proofkit.spec-proof-core.installed-readme-first-input` / `proofkit.packageverify.installed-readme-input-falsifier` | `internal/tools/packageverify/main_test.go`, `TestExactTarballOnboardingTrace`, `go test ./internal/tools/packageverify -run OnboardingTrace` | Example validity does not prove requirement meaning | +| R-17 | `REQ-PROOFKIT-PACKAGE-001` | add `proofkit.package-boundary.contract-map-table-shape` / `proofkit.contract-map.table-shape-falsifier` | `internal/app/cli_contract_test.go`, `TestContractMapDecisionTreeHasThreeCells`, `go test ./internal/app -run ContractMap` | Cell-count proof is not full GFM rendering | +| R-18 | `REQ-PROOFKIT-QUALITY-022`, `REQ-PROOFKIT-SPEC-021` | extend `proofkit.supply-chain-quality.browser-static-and-runtime-proof` / `proofkit.requirement-browser.static-runtime-proof-falsifier` | `tests/browser/workspace.spec.mjs`, `workspace state matrix passes axe and target-size`, `npm run browser:check` | Narrow automated rules do not establish full WCAG conformance | +| R-19 | `REQ-PROOFKIT-SPEC-021` | extend `proofkit.spec-proof-core.requirement-browser-rendered-runtime` / `proofkit.requirement-browser.rendered-runtime-falsifier` | `tests/browser/workspace.spec.mjs`, `specifications use native semantics and keyboard activation`, `npm run browser:check` | The flat list does not claim hierarchical tree navigation | +| R-20 | `REQ-PROOFKIT-QUALITY-022` | extend `proofkit.supply-chain-quality.browser-static-and-runtime-proof` / `proofkit.requirement-browser.static-runtime-proof-falsifier` | `tests/browser/workspace.spec.mjs`, `workspace states reflow at 320 CSS pixels`, `npm run browser:check` | The test is not a complete zoom/device audit | +| R-21 | `REQ-PROOFKIT-QUALITY-022` | extend `proofkit.supply-chain-quality.browser-static-and-runtime-proof` / `proofkit.requirement-browser.static-runtime-proof-falsifier` | `tests/browser/workspace.spec.mjs`, `rendered controls meet narrow contrast thresholds`, `npm run browser:check` | Pinned engine/scheme checks do not cover every OS theme | +| R-22 | `REQ-PROOFKIT-SPEC-018` | extend `proofkit.spec-proof-core.command-family-help-compatibility` / `proofkit.command-family-navigation.help-compatibility-falsifier` | `internal/app/command_family_catalog_test.go`, `TestRootHelpDiscoversFamiliesWithoutExpandingThem`, `go test ./internal/app -run CommandFamily` | Root help does not recommend a product decision | +| R-23 | `REQ-PROOFKIT-SPEC-021`, `REQ-PROOFKIT-QUALITY-022` | extend workspace rendered-runtime and static-runtime witnesses | `tests/browser/workspace.spec.mjs`, `bootstrap and request failures are visible and sanitized`, `npm run browser:check` | No retry, telemetry, or offline policy is added | +| R-24 | `REQ-PROOFKIT-SPEC-019`, `REQ-PROOFKIT-SPEC-021`, `REQ-PROOFKIT-SPEC-022`, `REQ-PROOFKIT-SPEC-023`, `REQ-PROOFKIT-QUALITY-004` | extend context compose, semantic diff, graph consumer, browser workspace, and CLI schema-evolution scenarios | context, diff, graph, browser, and app test files; selectors `TestV1DigestCoverageAdapters`, `TestDigestCoverageAdaptersPreserveSemanticDiffV2`, `TestBuildConsumesNormalizedV1AndV2ContextSnapshots`, `TestV2DigestCoverageProjections`, and `TestLegacyDigestVocabularyConfinedToV1AdaptersAndFixtures`; `go test ./internal/command/requirementcontext ./internal/command/requirementdiff ./internal/command/requirementgraph ./internal/command/requirementbrowser ./internal/app` | Digest coverage does not authenticate producer, baseline, or freshness | +| R-25 | `REQ-PROOFKIT-PACKAGE-006`, `REQ-PROOFKIT-QUALITY-016`, `REQ-PROOFKIT-QUALITY-023` | extend Python wheel and platform scenarios / platform-doc and release-platform-parity falsifiers | `internal/tools/pythonpackage/metadata_test.go`, `TestREADMEPlatformAndPythonProjection`, `TestReleaseTargetsProjectExactPythonWheelMetadata`, `TestVerifyWheelContentsRequiresExactWheelMetadata`, `go test ./internal/tools/pythonpackage ./internal/kernel/releaseplatform` | Docs do not claim a current PyPI version or Windows support | +| R-26 | `REQ-PROOFKIT-PACKAGE-001`, `REQ-PROOFKIT-PACKAGE-007` | extend package reference closure and mutable-release-fact scenarios / their separate falsifiers | `internal/tools/packageverify/main_test.go`, `TestPackagePublicReferenceClosure`, `TestVerifyNoStalePackageDocsRejectsMutableReleaseFactsInMarkdown`, `go test ./internal/tools/packageverify` | The bounded destination and exact code-span classifications are not a complete Markdown parser; the npm artifact is not a source checkout | +| R-27 | `REQ-PROOFKIT-SPEC-009`, `REQ-PROOFKIT-SPEC-021`, `REQ-PROOFKIT-QUALITY-004` | extend browser route and CLI ABI scenarios | `internal/command/requirementbrowser/requirementbrowser_test.go`, `TestInvalidViewListsEverySupportedView`, `go test ./internal/command/requirementbrowser ./internal/app` | Diagnostic parity does not prove browser runtime | +| R-28 | `REQ-PROOFKIT-QUALITY-025` | extend `proofkit.supply-chain-quality.workflow-source-oracles` / `proofkit.workflow.exact-source-oracle-falsifiers` | `scripts/workflow_source_oracles_test.go`, `TestExistingReleasePathIsReadOnlyAndFailsOnDrift`, `go test ./scripts -run ExistingRelease` | Source allowlist does not prove provider asset state | +| R-29 | `REQ-PROOFKIT-SPEC-021`, `REQ-PROOFKIT-PACKAGE-002` | extend fixed-launcher and one-shot-cleanup scenarios / their separate falsifiers | `internal/command/requirementbrowser/server_test.go`, `TestOpenBrowserUsesFixedLauncherAndLoopbackURL` plus the three `TestServeOneShot*` cleanup selectors, `go test ./internal/command/requirementbrowser` | Launcher success does not prove cleanup or browser rendering; cleanup does not prove browser profile identity | + +This change makes no selective-gate sufficiency claim. The repository has no +current aggregate producer that can derive a complete +`selective_gate_plan_input` from Git plus the binding graph, and inventing one +inside an audit-remediation PR would be a new feature. The exact closeout gate +is therefore always: + +```bash +git diff --check +npm run check +``` + +If an optional caller-owned selective input is materialized during +implementation, it is admitted with: + +```bash +go run ./cmd/agentic-proofkit selective-gate-plan --input /selective-gate-plan-input.json > /selective-gate-plan.json +jq -e '.unknownEdges == [] and .failures == []' /selective-gate-plan.json +``` + +Any non-empty `unknownEdges` or `failures` blocks closeout until the binding or +owner route is corrected. Running the full gate does not convert an unresolved +edge into success. Absence of an optional selective input remains an explicit +non-claim, not a skipped success. + +## Proof matrix + +| Repair | Current wrong implementation accepted by falsifier | Required narrow gate | +|---|---|---| +| R-01 | Observe-mode blocked prerequisite returns `passed/0` | `go test ./internal/command/adoptiondoctor ./internal/kernel/adoptionmode` | +| R-02 | Tool-only module appears as required root dependency | `go test ./internal/tools/releasesbom` | +| R-03 | Source, ancestor, or manifest-owning package swap can redirect the post-check open | `go test ./internal/command/publicapi` | +| R-04 | Output parent swap can redirect create or rename | `go test ./internal/app -run OutputWriter` | +| R-05 | Expected expression plus `|| true`, a shell override, a working-directory override, or any merge-critical `continue-on-error` field is admitted | `go test ./scripts -run 'WorkflowGuard|PackageGateWorkflowOracle'` | +| R-06 | Expected tests inside `if false` or under inherited/job/step execution overrides are admitted | `go test ./scripts -run RequiredAggregate` | +| R-07 | `{}` yields JSON stdout and empty stderr | `go test ./internal/app -run RequiredInputCommandsRoute` | +| R-08 | Entity-obfuscated visible overclaim passes | `go test ./internal/command/readinesscloseout` | +| R-09 | Input or output schema change leaves ABI hash unchanged | `npm run command-contract:check` and `go test ./internal/tools/commandcontractgen ./internal/app -run CLIContract` | +| R-10 | Breaking pre-1.0 patch is admitted | `go test ./internal/tools/releasechange ./internal/tools/releasepreflight` | +| R-11 | One self-hosting or package predicate can be inverted | Exact R-11a through R-11d gates in durable proof routing | +| R-12 | One closure field can be cleared without failure | `go test ./internal/tools/coveragemetrics` | +| R-13 | Floating external action ref is accepted | `go test ./scripts -run ExternalActions` | +| R-14 | Local install requires ambient PATH | `go test ./internal/tools/packageverify -run OnboardingTrace` | +| R-15 | Preset owner and help/contract diverge | `go test ./internal/command/stackpreset ./internal/app` | +| R-16 | Installed README example becomes invalid | `go test ./internal/tools/packageverify -run OnboardingTrace` | +| R-17 | Decision row has more than three cells | `go test ./internal/app -run ContractMap` | +| R-18..R-23 | Initial, failure, narrow, or alternate state escapes browser oracle | `npm run browser:check` with the named state-matrix rows | +| R-24 | Caller consistency is still called verified | `go test ./internal/command/requirementcontext ./internal/command/requirementdiff ./internal/command/requirementbrowser ./internal/app` | +| R-25 | Docs drift from Python/platform owners | `go test ./internal/tools/pythonpackage ./internal/kernel/releaseplatform` | +| R-26 | Installed artifact exposes a dangling source-checkout route | `go test ./internal/tools/packageverify -run PackagePublicReferenceClosure` and `npm run package:artifact` | +| R-27 | Renderer and app disagree on admitted views | `go test ./internal/command/requirementbrowser ./internal/app -run InvalidView` | +| R-28 | Existing release can enter `gh release upload` | `go test ./scripts -run ExistingRelease` | +| R-29 | Launcher can receive caller executable, arguments, or non-loopback URL | `go test ./internal/command/requirementbrowser -run OpenBrowser` | + +Final proof: + +```bash +git diff --check +npm run check +``` + +The final committed object must pass the full gate. Provider publication and +registry checks remain separately unverified unless external evidence is +available. + +## Implementation correction epoch + +Independent implementation review may falsify a design assumption without +expanding product scope. The following corrections are admitted because each +strengthens an existing invariant at its existing owner: + +| Correction | Falsified implication | Required correction | +|---|---|---| +| C-01 | `hash(path) = h` and `buildInfo(path) = b` do not imply that `h` and `b` describe the same bytes under a pathname swap or same-inode rewrite | Derive hash and build information from one immutable byte snapshot read through a pinned release-file descriptor, then prove same-handle content, pre/post descriptor, and current-path identity | +| C-02 | Cached bytes from read `A` plus identity from later stat `B`, or a parsed-export cache keyed only by canonical route, do not imply one immutable source admission across lexical aliases | Build each TypeScript lexical admission's bytes, digest, and identity from one pinned descriptor, bind every canonical source route to its first admitted identity/digest/parsed exports, and reject deterministic same-lexical or cross-alias drift | +| C-03 | A command-level native-source digest does not expose the required input/output compatibility surface | Initially require a recursively resolved field tree, then treat that attempted correction as falsified and superseded by C-09 after field-name inference produced false nested associations | +| C-04 | An AST function declaration with a matching name does not imply that `go test -run` can discover or execute it | Admit only Go test names and signatures using `*testing.T`, including valid import aliases, and require the witness to be an active `_test.go` file in the current `go list` projection | +| C-05 | A breaking version increment does not imply a `.0` target | Admit every major increase and every pre-1.0 minor increase while rejecting only breaking patch-class changes | +| C-06 | Updating the context producer alone does not own the wire contract of semantic diff and graph consumers | Include `SPEC-022` and `SPEC-023` as direct R-24 owners and proof routes | +| C-07 | A prohibited identifier appearing as an arbitrary substring of a content digest does not imply organization-policy leakage | Match prohibited identifiers at identifier boundaries and retain staged-blob plus worktree falsifiers | +| C-08 | A hard-coded receipt-kind mismatch does not remain a negative case after proof bindings legitimately gain that command | Derive the mismatch fixture from the current complement of the selected requirement's command set and require the complement to be non-empty | +| C-09 | Internally consistent inferred record graphs and executable test names do not imply semantic CLI-contract parity | Replace the false nested graph with an honest `root_shape_only` variant grammar, enumerate every supported JSON root/mode, use exact `union` roots instead of an unconstrained scalar-capable aggregate, cover omitted flag defaults as first-class conditions, add direct high-risk CLI root oracles, and deny nested/type/cardinality/nullability parity | +| C-10 | A repository-root-confined reopen does not preserve the package directory admitted with its manifest when an in-root sibling is substituted | Open the package boundary as a confined pinned sub-root before reading its manifest, then admit all package sources through that same handle | +| C-11 | A versioned breaking-release policy does not imply exact consumer pinning when generated release notes omit npm's exact-save flag | Render install and rollback commands with `--save-exact`, include the literal previous version, and falsify both generated routes | +| C-12 | A design table naming a selector or witness does not imply that the durable proof graph contains it | Bind every exact design selector and witness identity, then run binding admission and selector-executability closure | +| C-13 | A repository-root-confined output route does not preserve the admitted parent when that parent is replaced by an in-root sibling symlink | Open and pin the admitted parent as a confined sub-root, bind the route and handle with `SameFile`, keep temporary creation and cleanup on that handle, and recheck the current route; C-28 supersedes descriptor-relative publication | +| C-14 | Counting one externally blocked gap as also enforced does not imply disjoint adoption-doctor disposition classes | Derive blocked and enforceable gap sets independently, exclude blocked gaps from enforcement counts in every mode, and let unresolved blocked evidence determine top-level blocked state | +| C-15 | One native-source digest does not imply complete ownership when an output is assembled by two command packages | Admit exactly one of `nativeSource` or a sorted, non-empty, unique `nativeSources` list and require every declared source digest to be fresh | +| C-16 | A scanner parse failure represented as a synthetic semantic report does not imply structural-error channel parity | Return scanner admission errors structurally so malformed deployment input exits non-zero with empty `stdout` and a bounded `stderr` diagnostic | +| C-17 | Server cleanup and one-shot lifecycle tests do not imply that every terminal one-shot output has a declared public root shape | Declare distinct submitted and terminal variants and execute both through package and public-CLI oracles | +| C-18 | Variant-local root checks do not imply globally unambiguous conditions, default-flag coverage, or a reachable aggregate mode | Reject duplicate conditions across variants, exercise explicit and omitted defaults directly, and admit the two-input pilot aggregate through one strict envelope | +| C-19 | A worktree snapshot derived from tracked and untracked paths does not imply that an unstaged tracked deletion affects artifact identity | Subtract `git ls-files --deleted` from the snapshot and prove staging the same deletion cannot change the candidate snapshot | +| C-20 | Rejecting forbidden root entries does not imply that the canonical root package is retained by the test fixture | Include the valid root package entry in the positive artifact fixture, then mutate each forbidden root entry independently | +| C-21 | Recognizing literal `true` does not imply that a dynamic or constant GitHub expression cannot enable job-level `continue-on-error` | Require the fail-closed aggregate job to omit `continue-on-error` entirely and falsify literal, expression, and explicitly false field presence | +| C-22 | Bootstrap and terminal-state coverage does not imply coverage of per-view loading states that remain visible under a delayed request | Hold each specifications, diff, and graph request behind a deterministic barrier and run the complete accessibility, target-size, reflow, contrast, packet, and tab-order oracle before release | +| C-23 | Exact run text and actionlint success do not imply the same execution semantics when a typed oracle drops inherited or step-level environment, shell, and working-directory controls, partial truth evaluation does not prove `continue-on-error` absence, and ordinary nullable decoding does not preserve forbidden-key presence | Model workflow, job, and step run controls with presence-aware scalar admission; require exact safe workflow defaults and exact owner-reviewed environment entries where present plus exact bash defaults for the CI aggregate; forbid job defaults, unexpected environment entries, shell, working-directory, or any `continue-on-error` presence on all required leaf and aggregate jobs and on every step in each merge-critical job; falsify each inheritance level and explicit YAML `null` for forbidden scalar controls independently | +| C-24 | A release change record that correctly declares some breaking changes does not imply that it declares every intentional public state/exit change approved by the design | Declare the `adoption-doctor` blocked-prerequisite state and nonzero-exit change plus its consumer migration step in the versioned record, and bind a direct record-to-rendered-notes falsifier | +| C-25 | A declared top-level state/exit migration does not imply declaration of nested public rule-status changes, and an observe-only test does not prove every non-enforced rule path | Declare the non-enforced advisory rule transition from `passed` to `skipped` plus its consumer migration, require exact `observe=skipped` and `warn=warning` rule statuses, prove gaps outside an `enforce-touched` selection remain top-level `passed/0` with `skipped` rules, and prove the declaration reaches rendered notes | +| C-26 | `trace: retain-on-failure` and an error naming a run directory do not imply that diagnostics survive process cleanup or reach provider review | Under the QUALITY-022 browser artifact-confinement and failure-diagnostics-retention witnesses, clean successful browser runs, retain failed attempt directories, upload only the attempt-scoped report and test-results paths under an exact failure condition, and keep authoritative proof upload success-only | +| C-27 | `retain-on-failure` plus a pinned axe package does not imply bounded Firefox execution: neither removing continuous screenshots alone nor shrinking the repeated megabyte-scale builder source alone prevents the stall | Initialize the pinned version-identical minified axe distribution through the browser-context script channel, let every builder evaluate only a constant no-op loader, retain action, DOM, network, and source trace data with continuous screenshots disabled, request one bounded best-effort screenshot after failure, preserve all default and target-size rule semantics, and require clean first-attempt execution in every pinned engine without retries or a larger timeout | +| C-28 | A pinned parent handle plus current-route checks does not imply that descriptor-relative publication stays repository-confined when the admitted parent moves after the last check | Keep temporary-file creation and cleanup through the pinned parent, publish through the repository root with full temporary-source and destination routes, and use an exact `before_publish` barrier to prove outside-root and in-root replacement sentinels remain unchanged with no temporary residue | +| C-29 | Two root-confined pathname operands do not imply that `os.Root.Rename` publishes the admitted temporary object into the admitted parent: the source and destination routes are resolved separately, and no additional pre/post pathname check closes the remaining same-user namespace race | Stage the temporary object directly under the repository root, retain its identity, re-admit both temporary identity and destination-parent route after the exact barrier, publish through the repository root, and replace the unprovable stable-parent concurrency promise with an explicit same-user post-admission namespace-mutation non-claim; this narrows only an unmerged audit hardening claim absent from the baseline contract | +| C-30 | Root-level staging does not preserve baseline writable-child or nested-filesystem behavior; `SameFile` does not imply immutable temporary content or mode, that a followed symlink is the directory entry being renamed, or that a parent admitted before hashing remains current at publication; and `FileMode.Perm() == 0644` does not exclude setuid, setgid, or sticky bits | Under the explicit same-user concurrency non-claim, stage, validate, publish, and clean through the pinned destination parent; after the exact barrier re-admit the non-symlink temporary entry identity, complete mode exactly equal to `0644`, and content digest, then re-admit the parent route at the irreversible rename boundary, and falsify object replacement, permission and every special-mode-bit change, in-place rewrite, symlink aliasing, and parent replacement before publication | +| C-31 | A compatible public requirement addition does not reach consumers merely because its source, overview, binding, and tests agree | Add the repository-confined same-parent atomic output guarantee to the versioned machine release record and require its exact summary in rendered notes | +| C-32 | Byte equality and a valid provider projection do not imply that the final PR body includes the promised final tree, diff counts, local gates, residual non-claims, and retrospective | Build one canonical closeout record from exact repository and admitted local-evidence facts, embed it with a digest and unique sentinels, and validate it independently in both the reviewed snapshot and final server body | +| C-33 | Validating a mutable local artifact path and later rereading that path for projection does not imply that the projected bytes were admitted; permission-only file-mode equality also does not imply exact mode equality | Copy each local evidence object once into a private snapshot, validate and project only those same bytes, recheck final `HEAD` and tracked-tree cleanliness after record construction, compare the complete temporary-file mode to exactly `0644`, and falsify each special mode bit independently | +| C-34 | Snapshot byte identity does not admit a field that its validation predicate ignores | Project only exact validated local fields into closeout; omit the browser input digest and package/coverage snapshot digests because this closeout has no owner-valid predicate for their semantics | +| C-35 | For a multi-document JSON stream, `jq -e predicate` returns the truth status of the last output and therefore does not imply that the first document later selected by `--slurpfile` passed | Slurp each local snapshot during validation, require exactly one document, apply the predicate to that document, and admit the exact sorted Chromium/Firefox/WebKit project inventory | +| C-36 | Rejecting only literal `false` or `0` does not imply that a trust-significant workflow job or step is reachable; a dynamically false CI step can leave its containing job successful and satisfy the aggregate, while nullable decoding can confuse explicit `if: null` with absence | Require an exact closed job inventory, presence-aware condition decoding, and exact absent-or-owner condition for every job and step in required CI and release workflows; retain exact named conditional exceptions only; and falsify dynamic false plus explicit-null conditions on required CI and release routes | +| C-37 | A successful job whose identifier is `platform-smoke` does not imply macOS platform execution, and rejection-only binding selectors can pass vacuously when their owner workflow is already invalid | Bind each required CI job to its exact provider-check name and runner, require the exact fail-closed platform-smoke command, reject reusable-job and no-op substitution, assert positive owner admission before every bound mutation table, and route the positive inventory oracle directly through QUALITY-011 and QUALITY-013 | +| C-38 | An exact final platform command does not imply that an earlier step did not rewrite its package-script owner; lossy runner-label normalization does not prove an exact scalar; negative package-gate selectors do not imply that the real CI and release owners pass; and an unparsed closeout snippet can regress its singleton predicate | Close the ordered five-step macOS inventory and exact package-script owner command, compare each runner as an exact string scalar, bind a positive oracle that admits both real package-gate workflows, restore `length == 1`, and execute the affected selectors and closeout filter | +| C-39 | An exact local-action path does not imply exact repository-controlled action semantics, and zero-value decoding does not distinguish an absent step key from `null`, empty, or whitespace-bearing dual execution syntax | Admit the exact setup-action bytes by digest, reject a nested semantic-shadow mutation, track `run`, `uses`, and `with` key presence, compare the exact YAML block value including its terminal newline, and reject dual or explicit-empty execution keys | +| C-40 | Valid selector functions and commands do not imply that the complete anti-vacuity proof set remains bound when a critical selector is removed; whitespace field splitting does not imply Bash-to-direct-exec argv equivalence for quoted literal words | Extend the existing exact selector inventory to the QUALITY-011 and QUALITY-013 scenarios and independently falsify missing and surplus selectors; replace whitespace splitting only at the README boundary with a bounded expansion-free literal shell-word lexer that preserves safe quotes, escapes, and concatenation while rejecting operators, expansion, globbing, line continuation, and malformed quotes | +| C-41 | An exact selector inventory keyed only by scenario identity does not imply that the scenario remains bound to its owning requirement | Key every protected selector inventory by the exact `(requirementId, scenarioId)` pair and falsify owner-only transfer independently for each newly protected anti-vacuity scenario | +| C-42 | Marking a required scenario seen before an empty-selector early return does not imply exact-set admission; a lexer accepting NUL cannot preserve direct-exec argv; and a P0-P2-only review threshold does not imply the plan's stronger no-unresolved-finding completion criterion | Compare every required selector set before the generic empty path and falsify complete deletion for each critical scenario; reject NUL in every lexer state and preserve the mutant; align all final review thresholds to no unresolved confirmed finding | +| C-43 | Unicode whitespace trimming does not preserve Bash IFS or JSON whitespace semantics; a top-level NUL check does not inspect a byte consumed as an escape lookahead; and double quotes do not suppress interactive Bash history expansion | Trim only Bash space/tab delimiters around the command, preserve all other literal command bytes and exact JSON fence bytes, reject escaped NUL, reject unescaped `!` inside double quotes, and preserve Bash-equivalent `\\!` as a literal backslash-plus-exclamation argument | +| C-44 | Symmetric trimming of Bash delimiters does not distinguish an unescaped trailing separator from a trailing separator escaped into the final argv word | Trim only leading space/tab before the fixed command prefix, let the lexer discard unescaped trailing delimiters, and preserve escaped trailing space and tab with direct Bash-equivalent argv mutants | +| C-45 | A missing-function mutant on a newly exact-set-protected scenario does not imply that generic function-existence validation remains independently exercised because exact-set admission now rejects it first | Route the existing missing-function mutant through an unprotected selector-bearing binding while retaining the separate exact-set missing, empty, surplus, and owner-transfer mutants | +| C-46 | Consuming one escaped `!` does not preserve Bash semantics for two or more preceding backslashes because history suppression precedes quoted or unquoted backslash collapse | Consume the complete backslash run before `!`, project `ceil(n/2)` backslashes when double quoted and `floor(n/2)` when unquoted, retain a literal `!`, and preserve mutants for both states at run lengths one through four | +| C-47 | Aligning final committed-object review thresholds does not prevent an earlier candidate-preparation P0-P2 cutoff from dropping a confirmed P3 before those reviews | Require exact evidence for every remaining confirmed objection and finding at candidate preparation | +| C-48 | A correct pure inventory mutant does not justify repeating generic AST parsing and `go list` discovery before reaching that mutant, and repeated I/O obscures the boundary being tested | Run exact requirement/scenario/selector inventory admission as a complete pure first phase, then run the existing generic selector-function and active-file validation once for the positive object | +| C-49 | Scanning the whole remaining backslash run only when looking for a history marker, then advancing by one pair when no marker exists, yields `n + (n-2) + ... = O(n^2)` work on a package-bounded command line | Consume each complete backslash run exactly once, dispatch its terminal byte with the C-46 quoted/unquoted semantics, and preserve a 128-KiB non-history run regression | +| C-50 | Moving global inventory admission before generic executability does not imply that partial generic fixtures contain the repository-global inventory, and forcing them to do so would couple local error tests to unrelated owners | Keep one production composition function, expose one pure exact-inventory phase and one generic executability phase locally, and route partial fixtures only to the phase whose error they falsify | +| C-51 | Source presence and full-package execution of generic missing-function and invalid-signature tests do not imply that selective QUALITY-010 proof routing invokes those two executability conjuncts | Bind both selectors to the QUALITY-010 executability scenario, close its exact four-selector inventory, and run empty, missing, surplus, and owner-transfer mutants against that scenario too | +| C-52 | Binding a representative subset of typed workflow-oracle tests does not imply selective proof for permission floors, prior-step order and command identity, duplicate names, need-success bypasses, admitted optional paths, or typed needs normalization | Classify every owner-relevant test in `workflow_package_gate_oracle_test.go`, bind the complete seventeen-selector QUALITY-013 surface, and make that exact set part of the protected requirement/scenario inventory | +| C-53 | A source-level intentional semantic change does not imply inclusion in the closed release record, and requiring one named Go-test parameter rejects the toolchain-valid unnamed form | Declare the readiness-closeout character-reference behavior as a breaking change with migration guidance and an exact rendered-note witness; admit zero or one Go-test parameter name, bind the unnamed-parameter regression, expand QUALITY-010 executability to five exact selectors, and protect the three-selector QUALITY-024 release-record inventory | +| C-54 | Decoding before structural Markdown parsing can create a false cell boundary; a body-level UI state does not imply coverage of a distinct stable content substate; and generic release prose does not declare specific runtime additions or a removed keyboard contract | Split original Markdown bytes before one-pass text decoding and falsify an encoded pipe; add specifications no-match to the full browser matrix with exact content-state identity; declare Arrow-key removal with migration guidance plus pilot-all and witness-selector additions, project every exact record entry into rendered notes, and refresh the readiness native-source, generated CLI-contract, and public ABI golden projections | +| C-55 | Showing a bare family-discovery executable in root help does not imply that an exact-tarball npm consumer can execute it, and searching stdout before running a separately hard-coded argv is a false-green continuity witness | Project the copyable offline npm exec route, parse and execute the displayed route through the installed consumer, reject the bare-route mutant before family discovery, and refresh the app native-source, generated CLI-contract, and public ABI golden projections | +| C-56 | Unicode whitespace normalization before parsing a displayed shell command does not preserve copied Bash argv because NBSP is not an IFS delimiter | Remove only authored leading ASCII space/tab indentation, preserve all trailing bytes, and reject leading and trailing NBSP mutants | +| C-57 | Executing one displayed root-help command does not imply a continuous installed-user route when family, leaf, preset, and README transitions are rediscovered or hard-coded by the witness | Display a copyable offline npm command at every help transition, parse and execute those exact bytes, execute every contract-owned preset route, discover the installed README path from help, and reject bare or missing intermediate routes | +| C-58 | Source implementation and generic release prose do not imply a closed, correctly classified consumer change record | Protect the exact onboarding addition, classify removal of installed `AGENTS.md` and `CONTRIBUTING.md` as breaking with migration guidance, and extend absolute-symlink migration to manifest ancestors plus source paths | +| C-59 | Exact required step identities do not imply that an earlier unreviewed step cannot rewrite their npm-script owners | Hash the complete ordered semantic step inventory for CI `source-quality`, CI `browser-runtime`, and release `candidate`, including execution-key presence and values, and reject inserted script-shadow steps in all three jobs | +| C-60 | File size alone does not prove a god file, but five independent owners plus a demonstrated missed invariant do; conversely, one physical witness path shared by exact QUALITY-011 and QUALITY-013 selector inventories prevents a sound one-owner-per-file split | Inventory every tracked file at or above 1,000 LOC or 65,536 bytes; separate browser, runtime-precondition, source-policy, scanner-policy, and neutral support files; retain the QUALITY-011/013 cluster; update exact binding paths and protect moved selectors against deletion, owner transfer, surplus, and stale-path substitution | +| C-61 | A hash over selected step fields does not imply exact semantic step identity when valid GitHub step fields are omitted | Include `id` and `timeout-minutes` values and key presence in every closed step projection, and reject each independently in CI source quality, CI browser runtime, and the release candidate | +| C-62 | Exact requirement, scenario, and selector equality does not imply that selectors remain in the file reviewed as their owner | Add one exact `witnessPath` inventory for every protected selector set and reject relocation independently from missing, surplus, and owner-transfer mutations | +| C-63 | A reviewed dirty diff does not imply that candidate staging contains new untracked owner files | Close the untracked candidate inventory to the five extracted workflow files, stage the complete reviewed path union, and require staged-path equality plus an empty unstaged and untracked remainder | +| C-64 | A threshold-complete path set does not imply that recorded file measurements are exact after surrounding edits | Recompute LOC and bytes from the final candidate snapshot, correct the stale `internal/app/app_test.go` byte count, and require exact final ledger equality before review | +| C-65 | Extracting scanner tests into a cohesive file does not imply selective proof ownership when no requirement binding names the new selectors | Bind CodeQL, OSV, and Scorecard permission/publication invariants to the extracted scanner owner, admit their exact selectors and path, and falsify deletion, surplus, owner transfer, and relocation | +| C-66 | Adding an installed invocation to generated help does not imply that every shipped leaf exposes valid copyable bytes in the exact tarball | Traverse every displayed family and leaf route from the installed artifact, execute each exact help route, compare each installed invocation to its exact bare usage, and retain preset and README transitions as additional end-to-end witnesses | +| C-67 | A logically correct staging predicate is not an executable plan when its temporary-file cleanup is rejected by the active destructive-action guard | Hold the reviewed, expected-untracked, actual-untracked, and staged path inventories in memory; pass the exact untracked inventory to `git add` through stdin; preserve equality and empty-remainder checks without temporary paths or cleanup | +| C-68 | Absence of an observed literal write scope does not imply an explicitly declared read-only scanner floor, and one required provider write scope does not exclude surplus write authority | Require exact workflow, advisory-job, and provider-job permission maps for CodeQL, OSV, advisory Scorecard, and public Scorecard; preserve intentional inheritance explicitly; reject a missing workflow floor, advisory write authority, and surplus provider write authority independently | +| C-69 | Comparing Installed invocation while scanning lines does not imply comparison with the final unique Usage line, and raw string prefix does not preserve the command-token boundary | Collect the unique Usage and Installed invocation before comparison, require their exact authored order, admit only exact command token or token-plus-space, compare full installed bytes afterward, and reject installed-before-Usage plus prefix-collision mutants | +| C-70 | A full package test containing C-69 does not imply that selective QUALITY-019 proof preserves or executes that falsifier | Add C-69 to the exact installed-package selector set and witness path, strengthen the owner requirement to name every leaf and exact ordered command-token binding, and reject selector deletion, surplus, owner transfer, and path relocation | +| C-71 | A passing current browser matrix does not imply that the final provider-closeout predicate admits that matrix when its per-project count is stale | Bind the executable closeout snapshot predicate to the current owner count of 31 passed tests for each of Chromium, Firefox, and WebKit before publishing or reporting provider closure | +| C-72 | Unique condition strings do not imply mutually exclusive machine-selectable CLI variants, and raw argv does not imply one selector state while repeated single-value flags use last-write-wins | Admit an optional canonical complete CLI-flag conjunction model; require equal allowed dimensions and disjoint assignments; close all twelve valid adoption output states against native option admission; derive every ABI condition from parsed argv; reject repeated `--mode` and `--pilot`; and project the intentional behavior change through the release record | +| C-73 | An exact decomposition-owner inventory remains exact only for the snapshot from which it was derived; a later owner-aligned split can add a required file without changing the earlier five-file predicate | Extend the decomposition-owner subset to the six files including `internal/tools/commandcontractgen/condition_model.go`, and retain exact staged-path equality plus empty unstaged and untracked remainders | +| C-74 | A valued flag being present in raw argv does not imply a literal condition state when its parser admits the empty string and native normalization uses empty as the omission sentinel | Reject an explicitly empty `--pilot` value before option construction, bind the exact CLI diagnostic and condition non-projection, and disclose the intentional rejection with migration guidance | +| C-75 | The decomposition-owner subset does not imply the set currently untracked after an earlier candidate commit has already admitted five members of that subset | In the correction amend, require the current untracked set to contain only `condition_model.go`, stage the exact reviewed correction-path union, and independently require all six decomposition owners to be present in the baseline-relative additions | +| C-76 | Enumerating hard-coded native mode and pilot literals in a test does not imply future closure against the native admission domain when that owner changes and its source digest is refreshed | Build native admission maps and immutable test projections from one internal mode list and one pilot list; derive the closure loops from those projections and retain exact 80-combination and twelve-valid-state assertions | +| C-77 | A generic canonical/disjoint condition grammar does not imply generic finite native-option or argv closure when only one current definition owns those executable witnesses | Admit `cli_flag_conjunction_v1` only on the adoption output definition, reject an unowned second opt-in, and require a future owner to extend generator admission together with its own native-domain and argv proof | +| C-78 | A complete owner-specific subset of added files does not imply the complete candidate added-file inventory relative to the reviewed baseline | Require exact equality with the complete baseline-relative added-path inventory after final correction freeze and separately prove that the six decomposition owners are a subset | +| C-79 | Covering all twelve valid normalized assignments does not imply that every exercised argv which emits JSON is bound to its exact condition and root variant | Bind the reachable guidance mode/scope failure argv to the guidance-report condition and `06-guidance-report` root before asserting its failure body | +| C-80 | Restricting a condition model by definition ID does not imply that another command or direction cannot reference that admitted definition | Require the exact `(adoption-contract-envelope, output, adoption-output-definition)` triple and reject command, direction, and definition aliases independently | +| C-81 | Exact source sizes recorded in explanatory prose do not remain exact after later corrections, even when the threshold ledger is refreshed | Remove volatile inline line counts from the decomposition rationale and keep snapshot-specific measurements only in the deterministically validated ledger | +| C-82 | Adding condition and variant coordinates to one JSON test case does not imply their retention when conditional assertions let both coordinates be deleted and a duplicate condition preserves the global count | Require `assertJSON` if and only if both route coordinates are non-empty for each case, then run argv-condition and variant-root assertions unconditionally on every JSON case | +| C-83 | A biconditional among mutable fixture fields does not imply that the fixture matches actual runtime emission, and deleting the whole case evades per-case checks | Require observed non-empty stdout exactly when a JSON assertion is declared, parse every observed stdout, and preserve the exact sorted fourteen-case JSON inventory | +| C-84 | A cohesive file boundary does not justify duplicating an identical generic algorithm already owned by the same Go package | Replace all condition-model calls with the existing `sortedKeys` helper and delete the duplicate helper plus unused import | +| C-85 | A critical binding whose selector exists and executes does not imply that the selected test reaches the claimed byte-admission invariant | Replace the README-only selector on the Mach-O compatibility scenario with the exact four-test negative/positive/parser inventory, bind its exact witness path in the coverage owner, and apply the existing empty, missing, surplus, owner-transfer, and relocation mutations | +| C-86 | Closing one semantic false route does not imply that other exact selectors reach the operation named by their scenario | For Python wheel-platform parity, bind README ownership, exact all-target wheel metadata/filename projection, and verifier rejection; for browser one-shot cleanup, replace the fixed-launcher selector with the exact three cleanup/concurrency tests while retaining the launcher in its separate package-boundary scenario; protect both exact selector/path inventories with the existing five mutation classes | +| C-87 | A package-reference closure test can pass while every mutable-release-fact detector remains unreachable | Replace the PACKAGE-007 selector with the existing ten-class stale-fact falsifier, retain reference closure only in its PACKAGE-001 scenario, and protect the exact requirement/scenario/selector/path tuple with empty, missing, surplus, owner-transfer, and relocation mutations | +| C-88 | Exact permissions for every expected scanner job do not imply that no unclassified write-capable job exists | Require the workflow job count to equal the disjoint advisory/provider expectation count before validating each expected job, and reject an added unclassified `contents: write` job for CodeQL, OSV, and Scorecard | +| C-89 | A canonical installed help route does not imply that generated continuations resolve the artifact in every supported launcher channel | Admit one immutable `npm_offline`, `python_module`, or `path` invocation profile at the launcher boundary; thread one renderer through the exact generated-field inventory; preserve caller-owned strings; execute exact npm and wheel continuations; reject wrong-profile, bare, missing, surplus, and caller-rewrite mutants; bind `PACKAGE-002`, `PACKAGE-003`, `PACKAGE-006`, `QUALITY-019`, and the public-string migration under `QUALITY-024` | +| C-90 | An executable native-output selector does not imply that the selected test observes the declared output root or its complete native owner | Close the root-distinct output inventory over adoption-contract-envelope, pilot-admission, and self-check; bind them respectively to `internal/app/cli_abi_test.go` selectors `TestAdoptionContractEnvelopeCLIABI`, `TestStandaloneMultiVariantCommandsUseExactRootShapes`, and `TestSelfCheckOutputUsesExactRootShape`; require exact native source sets adoption=`{internal/command/adoptioncontract}`, pilot=`{internal/app, internal/command/pilotadmission}`, self-check=`{internal/app}`; protect source sets and exact command/direction/path/test/executable-command/requirement/scenario tuples with empty, missing, surplus, substitution, relocation, direction-transfer, scenario-transfer, command-drift, and owner-transfer mutants under `PACKAGE-002`, `QUALITY-004`, and `SPEC-011` | +| C-91 | An absolute launcher path does not imply that the value is safe to project into generated commands or diagnostics | Reject report-visible secret-like and Unicode control content at Python launcher admission, use field-only errors that never disclose the value, and exercise the shared redaction corpus plus control mutants | +| C-92 | Closing continuation string fields does not imply closure of help display routes, structured argv, project workflow identity, or the installed wheel's complete next-step chain | Bind every owned help/display/argv sink to one immutable renderer, preserve caller-owned native-witness argv, build project workflow plans and source-report hashes with that renderer, bind `TestGeneratedCommandInvocationProfileRouteClosure`, traverse every installed wheel family/leaf route, execute exact emitted agent-route argv directly, and reject indentation, Unicode-whitespace, expansion, missing, surplus, relocation, and wrong-profile mutants | +| C-93 | Removing a temporary file and immediately recreating its pathname does not imply a different `os.SameFile` identity because Linux may reuse the released inode | Pre-create a replacement with the exact expected bytes and mode while the writer's temporary object is still live, assert that the two coexisting objects have distinct identities, then remove the temporary entry and rename that replacement into its pathname at the deterministic barrier; identity is the only changed dimension and the exact diagnostic becomes platform-independent | +| C-94 | A fully rendered DOM and prior successful locator reads do not imply that a Firefox page-realm `evaluateAll` returns within the bounded test budget | Express ordered graph/table equality as retryable row-count equality plus an indexed `data-identity` equality for every expected element; reject missing, surplus, reordered, duplicated, substituted, and absent identities while retaining one worker, zero retries, and the 30-second timeout | +| C-95 | An exact size ledger for one frozen snapshot does not remain exact after a later static-analysis or provider correction | Recompute the complete `LOC >= 1000 or bytes >= 65536` ledger only after final source and document bytes freeze, then require exact path, LOC, and byte equality before committed-object review | +| C-96 | Typed YAML decoding does not imply that execution-affecting or unknown workflow keys were absent, because unmodeled keys can be discarded before semantic validation | Admit every tracked workflow through a raw `yaml.Node` closed-key oracle at workflow, job, and step scope; permit only the two exact owner-reviewed release environments; reject unknown, duplicate, merge, container, strategy, service, output, job-concurrency, reusable-job, and step-execution-control mutants before typed decoding | +| C-97 | A source-hygiene scanner that covers several text extensions does not imply closure over the authored text languages actually shipped as embedded browser assets | Add CSS to the admitted text extension set and derive staged-blob and worktree mutants from every unique extension in the exact tracked browser-asset inventory, while leaving identifier boundaries and digest-substring admission unchanged | +| C-98 | Removing one bulk Firefox page-realm evaluation does not imply that repeated manual `waitForFunction` plus `evaluate` range synthesis in adjacent selection scenarios is bounded; two immutable provider attempts stalled on different selection tests | Create and collapse ordinary native selection through Playwright `selectText` and click actions; for the nonzero Unicode range, compute strict UTF-16 and code-point bounds independently in the test runner and perform one locator-scoped exact-range operation without query scanning, clamping, or manual event dispatch; delete the shared helper without changing production code, test identities, retries, worker count, or timeout | +| C-99 | The historical audit baseline does not imply the current Git parent, remote `main`, pull-request base, provider base, or feature lease after independently reviewed dependency merges and correction publications | Preserve `3d86b6d0e4ec4a6c6a7f7a35ff2787011771aa64` only as `audit_baseline_sha`; require the singleton parent and remote `main` to equal integration base `0df4c28bac9737f476f7dc66030363b8b40d5417`; bind each publication to its exact observed feature lease, beginning with `1a681c47911680d101d36b48ce818ea1905a7148` for the first publication; after every push and before PR-body or provider mutation require exact PR author, head, base, open state, and non-merge state; use the integration base for PR diff, closeout, and provider projections | +| C-100 | A previously valid coverage count does not imply the final regenerated artifact retains that count after new bound scenarios | Require the final coverage snapshot to contain exactly 69 requirements, 69 bound requirements, 173 scenarios, and 78 commands; reject the stale value 167 and adjacent 172/174 counterexamples | +| C-101 | `unicode.IsControl` rejection does not imply rejection of report-visible Unicode format controls because bidi controls are category `Cf`, not `Cc` | Reject both Unicode `Cc` and `Cf` at the existing Python launcher boundary, retain field-only diagnostics, and exhaustively equate the admitted category predicate and rejection/non-disclosure oracle with every Unicode scalar in `Cc or Cf`, including non-bidi `Cf` values | +| C-102 | Listing a public alias in `allowedFlags` does not imply that every accepted alias composition has an exact input/output condition, and sequential parsing does not make repeated selectors unambiguous | Retain the compatible `--stack-diverse` alias, declare `--contract-envelope --stack-diverse` in the exact pilot input/output variants, execute that route through the public CLI oracle, reject repeated or mixed `--pilot`/`--stack-diverse` selectors in either order before reading input, and project the newly rejected formerly accepted argv through QUALITY-004 plus the breaking change record and migration | +| C-103 | A bound test that checks selected release-record entries does not imply that the complete reviewed machine declaration is closed; structural admission and regenerated notes can remain consistent after semantic deletion or surplus | Require ordered equality for every current breaking change ID/summary, addition ID/summary, and migration step plus byte-for-byte equality with one independently authored complete current release-note projection; reject per-entry deletion plus ID/summary substitution, every adjacent reorder, one valid machine surplus per inventory, reordered or relocated bullets, in-block surplus, and appended surplus, duplicate, or second owned sections through the existing QUALITY-024 selector | +| C-104 | Quoting a shell variable does not imply that an immediately adjacent colon is literal in zsh; `"$final_sha:refs/..."` expands a modified parameter and makes the reviewed publication step non-executable | Use `"${final_sha}:refs/..."`, scan the complete tracked plan for unbraced variable-colon forms, and bind publication to the corrected literal refspec plus the existing exact remote lease; add no repository-wide shell abstraction or permanent product gate | +| C-105 | A feature lease admitted before one successful force-with-lease publication does not remain current after that publication or a later amend | Preserve `1a681c47911680d101d36b48ce818ea1905a7148` as the historical first-publication lease, require the next correction publication lease to equal current remote head `90090a5c712efa70b900fed0e115274cfa4773f0`, and reject any mismatch before mutation; retain one literal final SHA as the new publication result | +| C-106 | An exact baseline-relative added-file inventory does not remain complete when later correction epochs add owner tests after the inventory snapshot, and a historical untracked-file predicate does not describe the current amend | Recompute the final inventory as 21 exact paths, add the CLI output-witness, invocation-profile, and Python continuation tests, require the C-106 correction path set to equal only the two reviewed design/plan files with no untracked remainder, and retain the six-file decomposition subset proof | +| C-107 | Checking each required Scorecard output input value does not imply that the complete `with` input set is exact; an added authority-bearing input can preserve every positive assertion | Require the exact three-key input set and values for `publish_results`, `results_file`, and `results_format`, and make the same selector reject a surplus `repo_token` mutant | +| C-108 | A manually listed requirement-delta claim does not imply parity with the actual base-to-candidate invariant map | Recompute the exact changed invariant IDs, restore `SPEC-011` and `QUALITY-005`, `QUALITY-006`, and `QUALITY-007`, and require the P10 declaration to contain all 30 changed IDs with no surplus | +| C-109 | Declaring that volatile inline measurements were removed does not imply that the decomposition rationale contains no stale numeric qualifiers | Remove the numeric qualifiers from all four small-file rationale bullets and retain snapshot-specific sizes only in the threshold ledger | +| C-110 | A generic truth-expression normalizer does not prove exact YAML boolean identity; the string `true }}` can normalize to truth while changing the owned action input | Require `publish_results` to decode as the literal boolean `true`, remove the unused generic helper, and make the same selector reject the string-substitution mutant | +| C-111 | A correction set described as current or latest without an epoch qualifier does not remain current after a later correction changes another owner file | Time-index every historical two-file and 21-path statement to the C-106 freeze and reserve that epoch's correction authority for P12.2's then-current exact three-file C-107 through C-114 set | +| C-112 | Exact inputs on one named Scorecard step do not imply that no second Scorecard action carries parallel publication authority | Require the selected named step to be the sole `ossf/scorecard-action` step and make the same boundary predicate reject a differently named second-action mutant with surplus `repo_token` input | +| C-113 | A lowercase literal prefix does not classify the complete Scorecard-action subset because GitHub owner/repository identity is case-insensitive | Split one action reference at its single `@`, compare only the owner/repository portion with ASCII case-insensitive equality, preserve the ref bytes, and reject a mixed-case second-action mutant while excluding distinct repositories, subpaths, and malformed references | +| C-114 | Unicode simple case folding is strictly broader than ASCII provider repository identity | Reject any non-ASCII repository byte before case-insensitive comparison and make a long-s repository mutant fail while retaining valid ASCII case variants | +| C-115 | Context initialization plus a constant wrapper loader does not imply bounded execution when source inspection proves that the wrapper still performs avoidable page-realm evaluations, creates a temporary page, and re-executes the context init script for each audit; deleting only the auxiliary version probe would preserve that higher-exposure topology | Remove `@axe-core/playwright`; initialize the exact pinned `axe-core` source once per test context; fail closed unless `page.frames()` is exactly the singleton `[page.mainFrame()]`; in one direct evaluation preserve the wrapper's same-origin and `playwright` branding configuration, require the pinned pre-run version, and run default rules plus explicitly enabled `target-size` through an exact closed options object; require returned `testEngine` name `axe-core` and the pinned version; preserve rule applicability and add one combined negative fixture that requires both a `target-size` violation on an undersized named control and a default `button-name` violation on a normal-sized unnamed control; isolate zero-frame, child-frame, absent/wrong pre-run version with no run, exact configure and run-options closure, absent/wrong result name or version, one-evaluation, and no-temporary-page mutants; preserve one worker, zero retries, the 30-second timeout, and the existing diagnostic policy | +| C-116 | Fourteen preceding passes do not imply bounded first-attempt graph proof: on immutable iteration 15 the visible SVG locator resolved but `boundingBox()` never returned before the 30-second deadline; moreover, DOM-derived expected identities, viewBox, visibility, minimum width, non-`none` first-edge stroke, and a PNG byte threshold do not imply the admitted local SVG contract because fixture-equal cache, omission, opacity-zero, hidden-descendant, hidden-overflow, hidden trust-state tables, one-pixel-height, geometry-override, individual-transform, phase-split animation, delayed transition, external SMIL, zoom, content-visibility, text-security, empty-label, text-layout, zero-dash, and degenerate-edge mutants survive weaker or circular oracles | Remove pass-path `boundingBox`, raw computed-style evaluation, and element screenshot calls; intercept the exact one app-issued same-origin graph POST, inject a safe response sentinel absent upstream, observe that response, and require its frozen ordered node and edge records plus sentinel to equal graph metadata, exact leaf topology, SVG children, and visible opaque graph tables; independently compute the viewBox, node geometry, labels, and non-degenerate edge coordinates from response primitives and the restated layout formula; require visible owned viewport and SVG, exact viewport scrolling, explicit local display, visibility, opacity, complete transform-family, no CSS animation or transition time on the owned ancestor chain, no declarative SVG animation in the document, zoom, content visibility, filter, clip-path, and mask values, `800px` minimum width, computed height of at least `180px`, exact visible and opaque node shape/label attributes, computed geometry, direct text, text security, and bounded text-layout serializations, and positive visible opaque non-dashed `1.5px` stroke on every childless edge without applying a bounding-box visibility matcher to zero-thickness SVG lines; retain failure-only diagnostics, one worker, zero retries, and 30 seconds; restart the complete immutable 30-process Firefox epoch from iteration 1 | +| C-117 | A successful `chmod` call does not imply that a setgid mutant materialized when Darwin clears the bit for a file whose group is outside the caller's groups | Normalize only the setgid test root to the effective group before temp creation, require exact post-`chmod` mode materialization for every mode mutant, and leave the correctly fail-closed writer unchanged | +| C-118 | A handoff submitted from one rendered view does not imply that its later response still owns the workspace view-state projection after the user opens a newer view | Capture the active view request identity at submission; always render the admitted handoff result in its independent packet region, but update the global view-state projection only when that view identity is still current; preserve server submission and do not pretend client abort can revoke an already committed handoff | +| C-119 | A fully rendered document with complete successful local resources does not imply that Playwright Firefox will report either `load` or `domcontentloaded` before the unchanged test deadline | Route every workspace open and reload through one web-first readiness policy that waits only for navigation commit, requires a successful main-resource response and the exact visible server-owned workspace heading, and leaves the existing state, accessibility, and API assertions as the behavior oracles; retain the raw `about:blank` negative-control navigation and add no retry or timeout increase | +| C-120 | A guard present in a browser helper does not imply that the executable proof depends on it: deleting C-119's HTTP-response guard or weakening its exact heading matcher leaves an all-success navigation matrix green | Add independent main-document 503 falsifiers for both open and reload plus a substring-preserving accessible-name drift falsifier; require every pinned engine to reject each mutant while leaving production code, retry policy, and timeouts unchanged | +| C-121 | A main document received with status 200 and a fully initialized visible workspace does not imply that Playwright resolves even its `commit` lifecycle wait | Arm one exact-URL navigation-response waiter before scheduling location assignment or reload; admit an exact trigger token, successful response, and exact visible heading; exclude same-URL non-navigation responses with an executable 503 decoy; statically exclude provider-falsified direct lifecycle waits from the workspace corpus | +| C-122 | One current correction inventory stated in prose does not equal a later executable inventory merely because both appear in P12.2, and an inventory does not remain current across an amend | Time-index the four-path C-121 staging set to its `c2315fd` epoch and make both current P12.2 owner surfaces name the same exact three post-`c2315fd` correction paths before staging | +| C-123 | Rejecting raw base-URL drift in source does not imply that browser proof depends on every local-origin clause | In the existing navigation test, admit the configured URL and reject non-string input, HTTPS, hostname drift, missing port, username, password, path, query, and fragment independently before any navigation | +| C-124 | Exact trigger-token admission and pending-waiter cleanup present in source do not imply executable dependence when every trigger succeeds and every failure follows a settled response | Inject one deterministic pending response into the existing navigation test; return a wrong token; require the exact token error, observed abort signal, and observed consumption of the waiter rejection; make the fallback response fail distinctly if token admission is deleted | + +Exact C-90 binding mappings use +`proofkit.package-boundary.cli-output-root-witnesses` for all three selectors, +`proofkit.supply-chain-quality.cli-abi-golden` for all three selectors, and +`proofkit.spec-proof-core.adoption-contract-envelope-cli-abi` for the adoption +selector. The independent tuple-closure oracle is owned by +`proofkit.supply-chain-quality.cli-output-witness-contract` at +`internal/app/cli_output_witness_contract_test.go`, selector +`TestRootDistinctOutputWitnessBindingsAreExact`; it is intentionally separate +from the general CLI topology corpus. Every row has direction `output`. The +selector/source oracle rejects +missing, surplus, or substituted native source paths independently of generic +digest freshness, including conversion between incomplete `nativeSource` and +`nativeSources` forms. + +These corrections do not add commands, evidence classes, provider claims, or +general-purpose abstractions. They are overturned only by an owner-approved +contract that removes the corresponding identity, ABI, executability, SemVer, +or wire-consumer invariant. + +### C-40 decision record + +```text +problem: + Exact proof bindings could silently lose a critical anti-vacuity selector, + and safe quoted README words could be executed as different argv. +chosen owner boundary: + REQ-PROOFKIT-QUALITY-010 owns selector-inventory admission; + REQ-PROOFKIT-SPEC-001 and REQ-PROOFKIT-QUALITY-019 own the installed README + command-to-current-product trace. +rejected lower-cost alternative: + Documentation-only quoting restrictions leave an executable false reject; + checking only selector existence leaves removal of a valid critical + selector invisible. +proof invariant: + Both critical scenarios equal their closed selector sets, and every admitted + README literal word maps to the same byte argument under bounded Bash + quoting and direct execution. +non-claims: + This is not a complete shell or Markdown parser and admits no expansion, + substitution, control operator, glob, multiline command, or arbitrary npm + command. +rollback or overturn condition: + An owner-approved structured argv field supersedes the README command line, + or the owning quality requirements remove the exact anti-vacuity scenarios. +why this avoids accidental complexity: + It extends one existing map and adds one boundary-local lexer with no + dependency, process invocation, expansion, or reusable parsing layer. +why this avoids premature over-decomposition: + The lexer has one consumer and remains adjacent to that trust boundary; + extraction into a package would invent ownership without a second consumer. +``` + +C-41 retains the C-40 owner boundary, non-claims, rollback condition, and +complexity analysis. Its proof invariant strengthens set equality to exact +triple equality: requirement owner, scenario identity, and selector set. +C-42 retains those boundaries and makes the planned closeout threshold equal +to the design's actual retirement predicate. +C-43 remains inside the same README extraction anti-corruption boundary and +adds no general shell, JSON, or Markdown parsing claim. +C-44 narrows preprocessing further; it adds no parser state or abstraction. +C-45 changes only oracle routing and no production behavior. +C-46 remains inside the bounded lexer and does not add general history +expansion. C-47 changes only closeout policy. C-48 removes repeated work without +weakening the positive integration proof. +C-49 makes the lexer linear in command bytes and adds no new accepted syntax. +C-50 names two already distinct invariants and creates no reusable package or +public abstraction. +C-51 changes only durable proof routing and its anti-deletion oracle. +C-52 leaves tests owned by QUALITY-022, QUALITY-025, and unrelated workflow +requirements in their existing scenarios; it does not claim every test in the +shared file belongs to QUALITY-013. +C-53 changes no readiness runtime behavior: it makes the existing intentional +change discoverable to consumers and aligns selector admission with the Go +toolchain. The release record remains the existing closed owner, and the +unnamed-parameter regression remains inside the existing coverage validator. +C-54 changes no public runtime semantics beyond fixing the confirmed +readiness false green. It adds proof for an already reachable browser state and +documentation for already implemented public changes; it creates no new +parser, UI state, command, or release evidence class. +C-55 changes only the copyable presentation of an existing help route and +binds the installed witness to those displayed bytes. C-56 narrows +preprocessing to Bash-compatible indentation and adds no accepted syntax, +command, resolver, or shell abstraction. +C-57 extends that same route-byte invariant through already existing commands +and the already shipped first-input example; it adds no command or product +policy. C-58 changes only consumer disclosure and migration accuracy. +C-59 closes existing workflow source owners rather than inventing another +evidence class. C-60 follows executable owner boundaries: the two scenarios +that share one exact selector and one single-path binding remain together, +while independently changing peripheral owners move without selector renames. +C-61 extends the existing closed step record without adding a workflow model. +C-62 strengthens the existing selector inventory with its already authoritative +path. C-63 changes only candidate admission, and C-64 changes only exact +measurement evidence. C-65 restores selective routing for existing scanner +requirements without creating a new requirement class. C-66 applies the +existing installed-route invariant uniformly to every leaf; it adds no command, +resolution mechanism, or public input behavior. +C-67 changes no candidate membership or repository bytes; it removes an +execution-environment contradiction from the existing candidate-admission +proof and adds no script or reusable abstraction. +C-68 narrows existing source permission claims to exact checked maps and adds +no provider authority. C-69 changes only installed-help admission and no +command dispatch, JSON shape, exit code, or runtime input semantics. +C-70 changes only durable proof routing and requirement precision. +C-71 changes only the executable final-closeout predicate and adds no provider +claim. +C-72 changes only the public root-variant selection contract and two formerly +ambiguous repeated-flag inputs; it adds no runtime schema validator, general +condition language, command, evidence class, or provider authority. +C-73 changes no runtime or public contract. It repairs only candidate +admission after the C-72 owner split. Keeping the cohesive pure condition +algorithm separate is lower-cost than returning its grammar and disjointness +logic to the generator I/O owner, while splitting that algorithm further would +be premature over-decomposition. +C-74 changes only one formerly ambiguous empty-valued invocation. Reusing the +existing parser error path and ABI corpus is lower-cost than adding a second +normalization layer or another condition state. +C-75 changes no repository contract. It keeps current worktree state and +baseline-relative candidate identity as distinct evidence classes instead of +adding a stateful staging abstraction. +C-76 changes no accepted option or public package surface. Two clone-returning +accessors in an `internal` package remove duplicated test authority and remain +cohesive with the native option owner. +C-77 removes an unproven extension point rather than adding a framework. A +future second owner can overturn the one-definition admission only with the +proof whose absence currently justifies rejection. +C-78 changes no code or contract. It separates whole-candidate identity from +one semantic owner subset using the existing in-memory staging predicates. +C-79 and C-80 add no product behavior: they close existing ABI and generator +proof paths. C-81 removes redundant volatile evidence instead of adding +another measurement owner. +C-82 strengthens only test anti-vacuity and introduces no new abstraction or +runtime branch. +C-83 adds only observed-output and exact-inventory conjuncts to the same ABI +oracle; it changes no command behavior. +C-84 reduces the implementation by one identical helper and does not change +condition ordering or package ownership. +C-85 changes only selective proof routing and its anti-deletion oracle; it +adds no package behavior, platform claim, test implementation, or evidence +class. +C-86 adds one boundary-local projection test and otherwise reuses existing +tests and validator machinery. It separates launcher and cleanup semantics, +changes no runtime behavior, and avoids a generic coverage-threshold framework +whose line coverage would not prove semantic adequacy. +C-87 changes only proof routing and reuses an existing semantic test; it adds +no runtime branch, file, abstraction, or release evidence class. +C-88 strengthens the existing scanner source oracle with one set-cardinality +conjunct and one mutation case; it adds no workflow model, runtime dependency, +provider claim, or production behavior. +C-89 adds one value renderer because three existing generated-command producers +and two installed channels need the same quoting invariant. It preserves +caller-owned display text, admits launcher identity once, adds no ambient +resolver or runner autodetection, and records the supported-channel public +string change plus migration. The wrapper fields are an anti-corruption +boundary, not a public command or package-manager framework. +C-90 changes source-checkout proof routing, pilot native-source closure, and +anti-substitution oracles only. Its focused self-check witness reuses the +existing contract-derived root assertion. It adds no public runtime schema +field, output-root change, consumer migration, parser, or general source-level +test analyzer. +C-91 adds one bounded value-admission conjunct at the existing launcher +boundary and reuses the shared redaction corpus; it adds no credential model or +secret scanner. +C-92 extends the existing renderer and exact inventory instead of introducing +a second route model. It executes admitted argv directly, retains caller-owned +argv, changes no public JSON field, and records the already-breaking display +and argv prefix migration without adding a shell parser or ambient resolver. +C-93 changes only a negative-test fixture: one pre-existing regular file with +the same content and exact mode provides a cross-platform distinct identity +without adding production synchronization or weakening the writer's mode and +content checks. +C-94 re-expresses the same equality theorem through Playwright's retryable web +assertions. It removes both page-realm callbacks, adds no retry or timeout +policy, does not split the end-to-end flow, and preserves every negative case +implied by exact ordered equality. +C-95 changes only snapshot evidence and prevents explanatory measurements from +outliving the bytes they describe. +C-96 closes an existing source grammar before lossy decoding. It adds no +GitHub Actions schema framework, recursively models no dynamic `on`, `env`, +`with`, or permission vocabulary, and changes no workflow bytes. +C-97 changes only language reachability in the existing source scanner. It +adds no MIME inference or binary classifier and leaves the token predicate +byte-for-byte unchanged. +C-98 changes only how the browser test performs selection. The exact emoji +range retains a nonzero expected start, a two-code-unit DOM span, and a +one-code-point output span, while click collapse preserves the same visible +authority transitions. No test-only production surface or browser-specific +branch is admitted. +C-99 changes temporary closeout authority only. It keeps the original audit +baseline as historical evidence while preventing it from impersonating the +current PR base. The two phases reflect observable state: the old PR base is +not required to equal the new integration base until the new head is pushed; +no repository product behavior or provider result is changed. +C-100 changes one exact artifact predicate after regeneration and introduces +no inferred coverage implication. +C-101 adds one Unicode general-category conjunct at the existing launcher +admission boundary. It adds no normalization, credential model, shell parser, +or alternate display renderer. Python-module paths containing `Cf` change from +accepted to rejected and are therefore declared in the breaking change record. +C-102 preserves all previously valid single-selector pilot routes and the +documented alias. It removes only ambiguous argv sequences, extends the +existing root-shape condition owner, and adds no new command, mode, or output +shape. The repeated-selector rejection is declared as a breaking compatibility +change with an exact migration step. +C-103 strengthens only the existing source-checkout release-declaration +witness and its durable owner projection. It adds no runtime branch, release +record field, second registry, or inferred source-diff completeness claim. +C-104 corrects one temporary execution-owner command and adds no runtime, +public-contract, package, workflow, or business-logic behavior. Its proof is +the exact absence of unbraced variable-colon forms plus successful publication +under the pre-existing remote lease. +C-105 corrects only the temporary publication epoch identity. It adds no +product, provider, branch-protection, or merge authority and does not weaken +the existing exact lease. +C-106 updates only candidate-staging and inventory evidence after owner files +already admitted by earlier corrections. It adds no source file, runtime +behavior, package contract, or new decomposition boundary. +C-107 strengthens one existing QUALITY-007 test predicate without changing +workflow bytes, permissions, or publication behavior. +C-108 corrects temporary plan parity evidence and changes no requirement, +binding, or product behavior. +C-109 removes stale explanatory measurements and changes no decomposition +boundary or source behavior. +C-110 narrows only the QUALITY-007 test oracle to the exact decoded type and +changes no workflow input or runtime behavior. +C-111 corrects temporal language in both review history and temporary closeout +authority and changes no staging operation beyond the already reviewed +three-file set. +C-112 closes the Scorecard-action subset inside the existing QUALITY-007 test +owner and adds no full step inventory, workflow byte, or publication behavior. +C-113 aligns the same bounded classifier with provider repository identity and +adds no general action parser, network dependency, or ref normalization. +C-114 narrows that classifier to its declared ASCII domain and adds no +normalization, Unicode mapping, or provider lookup. +C-115 removes a source-only wrapper dependency and replaces two wrapper audits +per state with one direct call to the already pinned `axe-core` distribution. +Let `D` be the default-enabled axe rules and `t` be default-disabled +`target-size`. Let exact options `O` omit `runOnly`, omit overrides for every +`r in D`, and contain only `rules.target-size.enabled = true`. Axe's selected +rule set under `O` is then `D union {t}` while the check options for each +`r in D` remain unchanged. The old admitted predicate was +`Violations(D) = empty and Violations({t}) = empty and Applicable(t) and +Incomplete(t) = empty`. The new admitted predicate over the one combined +result `R = axe.run(document, O)` is +`R.violations = empty and Applicable(R, t) and Incomplete(R, t) = empty`. +These predicates are equivalent at the per-rule outcome projection because +only `t` changes from unselected to selected; no equality of complete result +objects is claimed. A combined negative fixture must defeat a +`runOnly: target-size` mutant by requiring both a `target-size` violation on an +undersized named control and the default `button-name` violation on a +normal-sized unnamed button in the same result. + +The topology oracle must admit `O` by exact closed equality: the top level has +only `rules`; `rules` has only `target-size`; and that record has only the +literal boolean `enabled: true`. Missing, wrong, or surplus keys or values, +including `runOnly: ["target-size", "button-name"]` and any default-rule +override, must fail even though the two-defect fixture could still observe both +named violations. + +Inside the direct evaluation, capture `axe`, require the exact pre-run version +and callable `configure` and `run`, apply the wrapper-equivalent closed +configuration `allowedOrigins = [""]` and +`branding.application = "playwright"`, and only then run. Full result-byte, +timestamp, array-order, tool-option, help-URL, and two-run-to-one-run equality +remain explicit non-claims; QUALITY-022 admits only the pinned engine identity +and accessibility verdict projections named above. Isolated falsifiers must +prove: zero frames and any child frame reject before evaluation; +`page.frames()` equals exactly `[page.mainFrame()]` on admission; an absent or +wrong pre-run version invokes neither `configure` nor `run`; missing +`configure` or `run` fails closed; missing, wrong, or surplus configuration +keys fail the closed topology oracle; an absent or wrong returned +`testEngine.name` or `testEngine.version` rejects unless the pair is exactly +`("axe-core", axeDistributionVersion)`; and each audit performs exactly one +page evaluation and never creates or accesses a temporary page. + +The matrix and combined negative control use one dedicated Playwright fixture +that owns a fresh `Page` and its `BrowserContext` for one test state. The +fixture initializes exactly once before the test body and its teardown admits +the test only if that page completed exactly one audit. The harness rejects a +second initialization before another registration and a second audit before +another evaluation, including concurrent attempts; a failed init registration +releases ownership for one retry, while an audit attempt is fail-closed under +the existing zero-retry policy. Reusing one `Page` lifetime for multiple +audited states is not admitted by C-115 and requires a separately owned fixture +topology rather than a caller-invented state token. + +The exact QUALITY-022 runtime and topology owners are +`tests/browser/axe-harness.mjs`, `tests/browser/workspace.spec.mjs`, and +`scripts/browser-proof-inputs.test.mjs`. The exact source-only dependency +inventory owners are `package.json`, `package-lock.json`, +`internal/tools/packageverify/main.go`, and +`internal/tools/packageverify/main_test.go`. The durable statement is updated +only in `docs/specs/proofkit-supply-chain-quality/requirements.v1.json` and its +human projection +`docs/specs/proofkit-supply-chain-quality/overview.md`. Existing Proofkit +binding paths and witness identities remain unchanged; no new proof-like owner +is introduced. + +The single-main-frame precondition makes removal of the wrapper's recursive +frame topology explicit and fail-closed. A future child frame overturns this +design and requires a separately owned iframe-aware witness. Increasing the +timeout, adding retries, skipping Firefox, or upgrading the browser toolchain +without a causal falsifier are rejected because none proves bounded +first-attempt execution. Removing only the observed version probe is the +lower-byte alternative, but it is rejected because prior exact traces moved +the stall to other operations and the wrapper would retain all other avoidable +page-realm calls and temporary pages. + +C-115 is a controlled topology hypothesis, not a proven Firefox root-cause +repair. If a bounded repeated full Firefox project or the exact-head provider +attempt 1 again times out on any Playwright page, locator, or evaluation call, +the hypothesis is falsified and closeout remains blocked. Re-adjudicate the +retained trace before choosing another repair. Only if the new evidence again +localizes the failure to cross-operation engine-level liveness should a pinned +Playwright 1.62 toolchain and browser-lifetime isolation be tested as separate +candidate A/B epochs. Do not combine those changes with C-115 before this +falsifier, because that would erase causal attribution. No C-115 evidence +claims a product or UX defect, a specific browser crash, or the exact +Playwright/Firefox mechanism. + +C-116 is the required adjudication of that overturn condition. Let `F(i)` mean +that every test in independent Firefox process `i` terminates successfully +within the unchanged per-test bound. The first epoch observed +`F(1) through F(14)` and `not F(15)`. Therefore the universal claim +`for every i in 1..30, F(i)` is false; the fourteen passes remain diagnostic +evidence but cannot admit the candidate. The retained trace narrows the failed +boundary: `Locator.boundingBox()` began about 1.5 seconds into the test, its +internal locator resolved the visible SVG in about 3.5 milliseconds, and no +successful return was recorded before the deadline. The trace does not +distinguish the later Firefox content-quad response from handle disposal, so +the exact Firefox or Juggler cause remains a non-claim. The preserved page and +failure screenshot show a rendered graph, so no product or UX failure follows +from this tooling-liveness counterexample. The retained trace SHA-256 is +`a08498b0ce74c39a714856c855d7757f0907a8a67df3d06b1cdab3b652904fc7`; +the machine report SHA-256 is +`2e5475d20b3161d6d0128b9097288302ad70a94de57b90c6e569533c114383a9`. + +The old graph smoke test also lacked a useful raster theorem. A transparent +SVG can be Playwright-visible and produce a PNG larger than 1,000 bytes, while +an explicit one-pixel height can preserve its viewBox, computed minimum width, +and non-`none` stroke. Thus PNG size does not imply meaningful visible pixels, +and viewBox plus visibility does not imply the prior physical-height floor. +C-116 does not infer a broader raster theorem. It admits one bounded local SVG +structure and computed-style contract under the current owned viewport. + +Before activating Traceability, the witness arms an exact same-origin POST +route and request/response observers for `/api/v1/graph`; it does not issue an +independent API request. The route fetches the real upstream response, requires +at least one node, replaces only the first node label with one safe +deterministic sentinel proven absent from the upstream projection, and fulfills +that one app-issued request. The witness then activates the view, admits the +successful response linked to that request, deep-copies and freezes its +projection records, waits for the stable graph, and requires exactly one +matching request, route interception, and response over that window. The +expected node/edge order, count, labels, and endpoint relations come from that +single observed response, never from DOM datasets. The witness separately +equates those records with the graph dataset, the ordered SVG groups and lines, +and both ordered tables, and requires the sentinel in both the first node's +full title and truncated visible text. This rejects a renderer that +consistently omits a record from every derived DOM surface, compares against a +different response, or renders an unchanged fixture-equal cache. The one-field +intervention proves dependency on this response channel; it does not prove +independent causal consumption of every response field. + +The retryable style contract requires both `.graph-viewport` and its direct +SVG child to be visible with their exact current `display`, `visibility`, +`opacity`, transform-family, `filter`, `clip-path`, and `mask-image` values. The +closed transform family is `transform`, `translate`, `rotate`, `scale`, and +`offset-path`, each with its exact current `none` value. The viewport must +retain exact computed `overflow-x: auto` and `overflow-y: auto`; an isolated +`hidden` or `clip` mutant must fail. Every element on the exact owned chain +`html > body > main > #workspace-content > .graph-viewport`, the direct SVG, +and every admitted node group, rectangle, text, and edge must have computed +`animation-name: none`, `transition-duration: 0s`, and +`transition-delay: 0s`. The viewport, SVG, and admitted graph elements also +have computed `zoom: 1` and `content-visibility: visible`; every graph element +retains exact `filter: none`, `clip-path: none`, and `mask-image: none`. +Isolated zoom, content-visibility, positive-duration, zero-duration +positive-delay, local phase-split animation, and ancestor inherited-paint +phase-split animation mutants must fail. Closing CSS animation and both +transition time dimensions on the owned ancestor chain prevents retrying +property assertions from admitting either confirmed alternating CSS mutant; +simultaneous stability under script-driven mutation or the Web Animations API +remains an explicit non-claim. The SVG requires its `800px` computed minimum +width and a computed height matched +by the explicit numeric CSS serialization for values at least 180 pixels. It +requires every node group, rectangle, and text label to have non-`none` local +display, visible visibility, opacity equal to one, and the owned positive +opaque fill/stroke where applicable. Rectangle and text fill opacity and +rectangle stroke opacity must each equal one; isolated zero-value mutants for +all three properties must fail. Each node also has exactly one accessible +title with the response-derived label. For response-order index `i`, the +independent formula computes `x = 28 + (i mod 2) * 390` and +`y = 28 + floor(i / 2) * 76`; the direct children must be exactly +`title, rect, text`, the rectangle must have exact +`x, y, width = 350, height = 48, rx = 4`, the text must have exact +`x + 10, y + 29`, and the group, rectangle, and text must satisfy the complete +closed transform family above. An isolated swap of rectangle and text must +fail while preserving their membership, attributes, and content. Computed +rectangle `x`, `y`, `width`, `height`, `rx`, and `ry` must equal the current +exact pixel serializations, with `ry = auto`, so CSS geometry cannot override +exact attributes. Text must omit +`dx`, `dy`, `textLength`, `lengthAdjust`, and attribute-level `rotate`. Its +visible content is exactly `evidencePlane + ": " + label`, truncated only +when longer than 48 Unicode code points to the first 47 code points plus +`"..."`. Its computed text contract is exactly `font-size: 13px`, +`font-size-adjust: none`, `text-anchor: start`, `direction: ltr`, +`writing-mode: horizontal-tb`, `dominant-baseline: auto`, +`alignment-baseline` in the complete pinned set `auto` or `baseline`, +`letter-spacing: normal`, `word-spacing: 0px`, `text-transform: none`, and +`baseline-shift` in the complete pinned +serialization set `0px` or `baseline`. Independent zero-size, +geometry-override, individual-translate, motion-path, text-offset, empty-label, +zero-font-size, zero-font-adjustment, end-anchor, right-to-left direction, +vertical-writing, hanging dominant baseline, shifted alignment baseline, large +baseline shift, extreme-letter-spacing, extreme-word-spacing, and uppercase +transformation mutants must fail. Computed `-webkit-text-security` must equal +`none`, and an isolated `disc` mutant must fail. The text and title elements +must have no direct element children. Separate hidden-`tspan` mutants under +text and title must each fail while preserving the exact direct text bytes. +Generic +`toBeVisible()` and `toBeInViewport()` are rejected as replacement oracles: +for each matcher at least one harmful mutant/engine pair survives, while the +viewport matcher also rejects unscrolled baseline rows and would add a new +scroll operation. Neither is therefore one uniform cross-engine oracle. The +positive display predicate admits the pinned engines' `inline` or `block` +serialization but rejects `none`. Every edge is bound to independently +computed, response-derived non-degenerate endpoint coordinates and must have +non-`none` local display, visible visibility, opacity and stroke opacity equal +to one, a positive opaque computed stroke, `1.5px` stroke width, +`stroke-dasharray: none`, and the same closed transform family. Every line and +rectangle must have zero direct element children. The SVG root's direct element +children must equal exactly all response-derived lines in edge order followed +by all response-derived groups in node order, with no surplus. Separate inert +line-child, inert rectangle-child, and root-surplus element mutants plus a +zero-dash mutant must fail while preserving geometry and paint. A separate +root-order mutant moves the +first group before the last line while preserving both subsets' relative +order, identities, and counts. The complete document must contain zero SVG +declarative animation elements from the exact set `animate`, `animateColor`, +`animateMotion`, `animateTransform`, `discard`, and `set`. Separate hidden +external-SVG mutants must target a graph line coordinate and the existing +`#workspace-content` ancestor color; both must fail at this document boundary. +This direct source exclusion replaces local `id` and `xml:id` bans: the former +did not close ancestor targets and the latter did not create an addressable +target in any pinned engine. It intentionally does not use `toBeVisible()` on +an SVG line: horizontal or vertical zero-thickness line boxes are reported +differently by the pinned engines. The property `clip-path: none` is exact; +normal SVG viewport overflow clipping is neither rejected nor generalized into +a no-clipping claim. No production CSS floor is added: the used-height +assertion directly rejects the one-pixel mutant at the lower owner cost. + +The two graph table viewports, their tables, captions, section groups, rows, +headers, and response-derived cells form the visible trust-state projection. +Their captions are exactly `Admitted traceability nodes` and +`Admitted traceability edges`. Their ordered headers are exactly the two +inventories constructed by `workspace.js`; their exact ordered body-cell text +comes from the same frozen response records, not from the DOM. Exact visible +header, row, and cell counts close omission and surplus. Independent caption +substitution or emptying, header substitution or surplus, body-row reorder, +omission or surplus, and body-cell substitution mutants must fail. + +Each local table element must satisfy the retryable visible predicate, +computed opacity `1`, `filter: none`, `clip-path: none`, `mask-image: none`, +`content-visibility: visible`, `zoom: 1`, `animation-name: none`, +`transition-duration: 0s`, and `transition-delay: 0s`. Each text-bearing +caption, header, and body cell also requires the current exact positive opaque +computed color `rgb(23, 32, 51)`, positive `16px` font size, +`font-size-adjust: none`, `-webkit-text-security: none`, and +`text-transform: none`. Each table's exact response-derived row and cell +counts must equal the count of matching visible locators. This closes hidden +ancestors, hidden individual rows or cells, local alpha and glyph loss, local +effects, and phase-split table animation without adding raw page evaluation. +Separate +hidden-viewport, hidden-row, opacity-zero-cell, transparent header and data +cell color, filter-opacity, complete-clip, transparent-mask, zero-font-size, +zero-font-adjustment, text-security, text-transform, zero-zoom, +hidden-content-visibility, phase-split-cell animation, positive-duration, and +zero-duration positive-delay mutants must fail. Separate header reorder and +body-cell reorder, body-cell-only omission, and body-cell-only surplus mutants +must preserve every unrelated count and order dimension and fail their sole +predicate. Exact initial page-viewport intersection, pixel layout, ancestor +effective compositing outside the owned table viewport, font rasterization, +and raster equality remain non-claims. + +`internal/testsupport/browserfixture/fixture.go` owns the raw graph fixture; +`internal/command/requirementgraph/requirementgraph.go` constructs normalized +records and identities; `internal/command/requirementgraph/output_admission.go` +admits them; `internal/command/requirementbrowser/workspace.go` owns +snapshot/session closure; and +`internal/command/requirementbrowser/http_handler.go` owns windowing and the +HTTP envelope. `internal/command/requirementbrowser/assets/workspace.js` owns +SVG construction and the geometry formula. The witness independently restates +that formula and computes viewBox height plus edge coordinates from the +response order and endpoint relations; it never reads expected geometry from +the DOM or calls production code. The unchanged +`internal/command/requirementbrowser/assets/workspace.css` owns style, and +`tests/browser/workspace.spec.mjs` is the runtime witness. Existing +`REQ-PROOFKIT-SPEC-021` and +`REQ-PROOFKIT-QUALITY-022` bindings already own visible traceability UX and +the same non-empty passed browser identities, so C-116 changes no requirement, +public API, packet, route, or business rule. Pixel-perfect raster equality, +anti-aliasing, effective compositing, ancestor occlusion, useful pixels, +exact screenshots, simultaneous stability under script-driven mutation or the +Web Animations API, a general ban on necessary page evaluation, independent +causal consumption of every response field, and the exact engine defect remain +non-claims. Increasing timeout or retries, retaining any of the three raw +pass-path operations, accepting 14 of 15, or merging a toolchain upgrade into +the same repair are rejected because they either preserve the counterexample +or destroy causal attribution. Any change in the resolved browser-runtime +input set creates a new input digest and restarts the full 30-process epoch; a +design-only byte change restarts design review but does not imply a new runtime +digest. The failed epoch cannot be resumed. + +A pinned Playwright 1.61.1 versus 1.62 or browser-lifetime A/B is diagnostic +and nonblocking if the fresh C-116 epoch passes all 30 processes. It becomes +blocking only if the replacement web-first path times out again or if an exact +engine or browser-lifetime cause is claimed. Running it before the lower-cost +raw pass-path operation-removal/replacement falsifier would mix variables +without closing the independent oracle defects. + +### Playwright 1.62 A/B decision record + +Problem: the immutable Playwright 1.61.1 C-116 epoch passed locally, but the +first provider execution later stalled inside a different raw Playwright page +operation. Therefore local bounded reliability does not imply provider +liveness: `L and not P` falsifies `L -> P`. + +Chosen owner boundary: update exactly the root manifest pin, lock resolution, +package-verifier exact dev-dependency admission, and its fixture from 1.61.1 to +1.62.0. These are the four existing owners of the same source-only browser +proof toolchain version. The root dev-dependency pin is an intentional, +nonbreaking public package-metadata change, while the lock records the +toolchain dependency's Node `>=20` engine floor. That floor governs source-only +development proof execution, not the shipped package runtime. The admission +oracle changes only to preserve exact metadata parity. No shipped CLI +JSON/exit-code behavior, product runtime, business rule, browser test, retry, +timeout, worker, or report-semantics byte changes. + +Rejected lower-cost alternatives: rerunning or republishing the failed 1.61.1 +object cannot distinguish an intermittent provider stall from a version +effect; increasing timeout or retries weakens the falsifier; deleting the +negative control removes coverage; and changing browser lifetime or another +raw operation in the same epoch mixes causal variables. The already completed +raw graph pass-path removal was the lower-cost prior intervention and did not +establish provider liveness. + +Proof invariant: the version change creates a new resolved browser-runtime +input digest. The candidate is admissible only after a new immutable +30-process Firefox epoch passes from iteration 1, two subsequent full browser +proofs pass, the composite and full gates pass, committed-object reviewers +approve, and the first fresh provider attempt passes every required check. +All Firefox projections must retain the historical 25-test identity digest, +one worker, zero retries, and 30-second per-test timeout. + +Non-claims: a successful 1.62 epoch does not prove the exact 1.61.1 engine +defect, universal browser liveness, registry publication, provider attestation, +production readiness, or a general absence of browser-runner defects. The A/B +does not preserve byte-identical package metadata, change the consuming +runtime Node floor, or claim compatibility for unsupported contributor +environments below the development toolchain's Node floor. + +Rollback or overturn condition: revert all four version-owner changes together +if 1.62 changes a verdict or test identity, fails package admission, fails any +immutable local gate, or introduces a security or platform incompatibility. +If the exact 1.62 provider attempt stalls again while other required jobs pass, +retain diagnostics and reject a version-only causal conclusion; the next +admissible intervention is a separately reviewed browser-lifetime or remaining +raw-operation experiment with a fresh input digest and complete epoch. A green +provider attempt admits the 1.62 candidate but does not establish that the +upgrade alone caused the difference. + +Accidental-complexity and decomposition argument: synchronizing four existing +owners is the minimum change that preserves exact package admission. A new +adapter, retry layer, test file, configuration surface, or version registry +would add an owner without a second independent consumer. Keeping the decision +in this temporary remediation authority avoids both a permanent one-off +abstraction and premature file decomposition. + +### C-117 setgid-mutant decision record + +Problem: the test assumed `chmod returned nil -> requested setgid mode exists`. +Darwin provides a counterexample when the file group is outside the caller's +groups: `chmod` returns nil but clears setgid. The writer then sees its expected +`0644` mode, so publication is correct and an expected writer error is a false +oracle conclusion. + +Chosen owner boundary: change only `internal/app/cli_abi_test.go`. Before the +setgid temporary object is created, normalize the test root to the caller's +effective group. After every mode mutation, require an immediate `Lstat` mode +equality before the barrier returns. This preserves independent permission, +setuid, setgid, and sticky-bit falsifiers. + +Rejected lower-cost alternative: rerunning under the default temp root hides +the environmental counterexample; skipping or deleting setgid removes an +independent security falsifier; accepting nil without a materialization +postcondition preserves the false implication; and changing the writer would +reject a state that did not actually change. + +Proof invariant: both default-group and foreign-group `TMPDIR` executions must +materialize the requested setgid mode and make the unchanged writer reject it +100 of 100 times. The complete full gate must then pass. Because this test file +is outside the resolved browser input set, its correction must leave the +Playwright 1.62 epoch digest unchanged. + +Non-claims: this correction does not assert Windows or Plan 9 support, change +output semantics, strengthen a caller privilege boundary, or prove arbitrary +filesystem special-mode behavior. + +Rollback or overturn condition: revert the test-only correction if group +normalization changes production bytes, fails on a supported Darwin or Linux +platform, or the explicit postcondition cannot materialize the mutant. Such a +counterexample requires a platform-specific test-fixture design, not a weaker +writer assertion. + +Accidental-complexity and decomposition argument: two local setup and +postcondition checks repair the existing oracle. A platform adapter, new test +file, production hook, or reusable abstraction has no second consumer and +would add unjustified ownership. + +### C-118 browser-state decision record + +Problem: the handoff panel and the active workspace view are independent +presentation regions, but a late handoff response writes the same global state +projection used by a newer view. + +Chosen owner boundary: change only +`internal/command/requirementbrowser/assets/workspace.js` and its existing +browser owner test. Capture the active view request identity when a handoff is +submitted and condition only the global state write on that identity. + +Rejected lower-cost alternatives: aborting a handoff on view navigation cannot +revoke a server-side terminal commit and could hide a valid submitted packet; +discarding the late packet conflates view navigation with handoff cancellation. + +Proof invariant: after a handoff begins, a newer Diff or Graph view remains the +global workspace state when the handoff succeeds or fails, while the handoff +region still reports its own terminal result. + +Non-claims: view navigation does not cancel or roll back a submitted handoff; +the packet region is not hidden; no new persistence behavior or +provider-liveness claim is introduced. + +Rollback or overturn condition: revert C-118 if the handoff packet is no longer +observable after a valid submission or if one-shot terminal semantics change. + +Accidental-complexity and decomposition argument: one captured scalar and one +conditional state write separate two existing lifecycles without a controller, +queue, or new state machine. The correction stays in the existing browser owner +and existing test file. + +### C-119 browser-navigation decision record + +Problem: provider attempt 1 for the C-118 candidate timed out twice in Firefox +while `page.goto("/")` waited for `load`. Both retained traces show every local +HTML, CSS, JavaScript, manifest, and requirements response completed +successfully in milliseconds, and both failure snapshots show the initialized +workspace. The failures occurred before test-specific behavior. Evidence is CI +run `30334467601`, job `90196309721`, artifact `8678666391`, report SHA-256 +`50e45237d4d41ad221cd9320a37ed3a96ca413af2caa4db790309976e38a533d`, +and trace SHA-256 values +`66cf9aef659dc4bf93ba97c192e9fb6d5cd63fa67e012df1acf862fa848bddd7` +and `8f817656fe74b2920c559014216a598a7d4409571a775b8a01c0f4ad5340bf6f`. +The first C-119 candidate replaced `load` with `domcontentloaded`; provider +attempt 1 then reproduced two fully rendered Firefox timeouts at that earlier +lifecycle event. That evidence is CI run `30335313301`, job `90198820640`, +artifact `8678963003`, report SHA-256 +`e1c495fd99d35a972ae9301b5c4799369409186a4c7445d1f720e9a9f0ba1c61`, +and trace SHA-256 values +`f03f9fbf5b5e15f1d5bf3b54a5c7543f04a3c8c64fdae834164571f76d74a853` +and `302cc38a72aa7b8399e5fa36f0de31e6f19900c4c412ebef2f41e047581451d4`. + +Chosen owner boundary: change only the existing browser witness. Route all +then-current workspace opens and reloads through owner-local helpers that wait +only for navigation commit, reject a missing or unsuccessful main-resource +response, and require the visible server-owned workspace heading through +Playwright's exact accessible-name matcher before returning. Keep the +`about:blank` axe negative control unchanged. C-121 supersedes the lifecycle +mechanism while preserving these response and semantic admission obligations. + +Rejected lower-cost alternatives: a rerun would not satisfy attempt-1 proof; +waiting for `domcontentloaded` was empirically falsified by the next provider +attempt; raising the 30-second timeout would hide rather than remove the +irrelevant lifecycle dependency; `networkidle` would add a discouraged +ambient-network heuristic; production changes cannot repair a test-runner +lifecycle signal after the application is already rendered. + +Proof invariant: browser tests admit the new main document through its +successful navigation response and exact visible server-owned heading, while +each existing state, accessibility, and API assertion still proves the +behavior it owns. Navigation HTTP failure remains terminal. + +Non-claims: navigation commit and the heading did not themselves prove module +completion, application behavior, visual correctness, API completion, +provider reliability, or a generally flake-free browser engine. No retry, +timeout increase, server change, or product behavior change is introduced. + +Rollback or overturn condition: replace this policy only if the application +requires a resource whose correctness is not covered by the existing semantic +assertions and whose completion needs a separately owned readiness signal. + +Accidental-complexity and decomposition argument: two small owner-local helpers +centralized one repeated navigation policy without a new module, fixture, +retry controller, production hook, or general navigation abstraction. + +### C-120 navigation-guard falsifier decision record + +Problem: C-119's ordinary success paths exercise navigation through the new +helpers but do not distinguish an implementation with the response or exact +accessible-name guard removed. Therefore `allSuccess -> guardRequired` is +false for the 81-test matrix, even though both guards own admission decisions. + +Chosen owner boundary: add three cases to the existing browser contract corpus. +Intercept the main document, preserve its original body, and return status 503 +for open and reload independently. In a third case, preserve successful +navigation and change only the server heading to +`browser.fixture.workspace drift`, which retains the weaker substring. Admit +the original 200 response, byte-changing substitution, and completed route +fulfillment out of band before accepting the helper rejection. + +Rejected lower-cost alternative: source inspection proves guard presence but +not executable dependence. One 503 case cannot prove both duplicated operation +paths. A heading-absence mutant would not distinguish exact from substring +matching. A new fixture, module, production hook, retry, or timeout increase +adds no proof. + +Proof invariant: for every pinned engine, deleting the shared HTTP-response +guard or weakening the accessible-name matcher makes its owning negative +control fail; both open and reload operation paths reject their independently +injected 503 response; a route-handler or fixture-precondition failure cannot +satisfy the heading oracle; the unchanged C-121 implementation passes 31 tests +per engine. Controlled Chromium rehearsal deletes the shared response guard +and changes the exact matcher to substring matching; the owning falsifiers +must fail because the helper resolves instead of rejecting. + +Non-claims: these falsifiers do not prove arbitrary navigation failures, +browser-engine liveness, remote networking, or document integrity beyond the +admitted response and exact heading. + +Rollback or overturn condition: consolidate the two operation cases only if a +single counterexample still proves both open and reload routing through the +shared response-admission owner. + +Accidental-complexity and decomposition argument: three declarative tests reuse +the existing helpers and route API. No production state, abstraction, fixture, +or file is introduced. + +### C-121 provider-falsified navigation-lifecycle decision record + +Problem: provider attempt 1 for exact source +`dcc824b31f858ab8fea5be683e5d81f12f039279` timed out in Firefox while +`page.goto(..., {waitUntil: "commit"})` remained pending. The retained trace +records the main document status 200, complete local asset and API responses, +and a fully initialized workspace screenshot. Thus +`response200 and renderedWorkspace -> gotoCommitResolved` is false. The exact +evidence is run `30337477288`, job `90205431977`, diagnostic artifact +`8679825289`, GitHub artifact digest +`sha256:274cb4bbe4ef2bcbb55f476ed21414287f5d1d80632f051304ea07d0c7e94cba`, +report SHA-256 +`da78d940df3e969252c612bd89e90818b4ee5154dd81d05d74761d928164f953`, +and trace SHA-256 +`3e75d3fffd297652d30cc3c7f17b1252f92214e43990f4830e6350291e145cb4`. +The provider checked out synthetic merge +`30e9f1892d95b5aa886a360f5b14f28372c15862`; the exact source revision and +tree identity remain the source-owned comparison surface. + +Chosen owner boundary: change only the existing browser witness. Admit the +configured local base URL before use. Capture its current main frame, arm one +exact-URL main-frame navigation-response waiter, then schedule location +assignment or reload from the page and require the exact trigger token. Admit +only a successful response and the exact visible server-owned heading. Abort +and consume the waiter if the trigger or later admission fails. The current +inventory is 27 open-helper calls, two reload-helper calls, and one +classifier-only reload trigger. + +Rejected lower-cost alternatives: rerun-only acceptance violates attempt-1 +proof; another lifecycle event preserves the falsified dependency; a longer +timeout or retry hides it; `networkidle` adds ambient-network authority; +unawaited navigation creates an unowned rejection; fetch plus `setContent` +does not execute browser navigation semantics; production readiness hooks or +dependency churn change a non-owner surface. + +Proof invariant: the response waiter exists before the timer-backed trigger; +the trigger synchronously returns one exact scheduling token; only an +exact-URL main-frame navigation response can satisfy the waiter; status 503 is +terminal; and the exact visible heading remains the semantic readiness oracle. +A pure classifier truth table admits the main-frame navigation and rejects a +same-URL main-frame fetch, same-URL child-frame navigation, and foreign URL. +The live same-URL 503 fetch decoy proves operation-level classifier wiring. +Open and reload 503 cases prove shared response admission, heading drift proves +exact semantic admission, and a static source oracle excludes every direct +workspace lifecycle wait except the isolated `about:blank` axe control. The +unchanged corpus must pass 31 tests in each pinned engine with zero retries. +Controlled Chromium rehearsals removed the URL, navigation, and frame clauses +independently; each corresponding truth-table row failed. Deleting the shared +response guard failed both 503 cases, and weakening exact heading admission +failed the substring-preserving drift case. + +Non-claims: the classifier does not prove child-frame readiness, arbitrary +navigation behavior, remote-network reliability, browser-engine liveness, or +product behavior beyond the downstream assertions. A response and heading do +not replace the existing state, accessibility, API, or mutation oracles. + +Rollback or overturn condition: replace the response-event policy only if +provider evidence falsifies it or a separately owned application readiness +contract offers a lower-cost deterministic signal. Any new direct lifecycle +wait must first falsify the static source oracle and receive an owner-specific +counterexample. + +Accidental-complexity and decomposition argument: one pure three-clause +classifier and one shared observer serve both existing open and reload helpers. +The truth table closes the only added classification branch. A new module, +fixture, retry state machine, production hook, or generic navigation framework +would add ownership without another consumer. + +### C-122 correction-inventory parity decision record + +Problem: before `c2315fd`, P12.2 first declared the current correction +inventory as design, plan, and workspace test, but its executable equality +predicate also required `scripts/browser-proof-inputs.test.mjs`. After that +four-path correction was amended, the next correction epoch changed only +design, plan, and workspace test. Thus both the pre-amend three-vs-four +contradiction and the post-amend claim that four remained current were false. + +Chosen owner boundary: time-index the four-path set to the `c2315fd` staging +epoch and make both current P12.2 surfaces consume the exact same three sorted +post-`c2315fd` paths. + +Rejected lower-cost alternative: retaining the static owner would make the +current executable predicate fail because that file is unchanged from HEAD. +Deleting it from the historical `c2315fd` account would omit a real C-121 +correction. + +Proof invariant: for the current epoch, P12.2 has one exact three-path +correction set, the worktree inventory equals it before staging, and the staged +inventory equals it after staging. Earlier sets are explicitly epoch-bound. + +Non-claims: this parity does not prove the separate baseline-relative +150-file diff or 21-file addition inventory; their existing predicates remain +independent. + +Rollback or overturn condition: change the list whenever a later correction +epoch changes an owner file or an amend absorbs one, and update the epoch +statement, declaration, and predicate in the same reviewed edit. + +Accidental-complexity and decomposition argument: one added list row removes +contradictory authority. A generator or second inventory file would increase +cost without a durable consumer. + +### C-123 base-URL admission falsifier decision record + +Problem: C-121 added raw base-URL rejection clauses, but all passing browser +calls supplied the configured valid URL. Therefore +`allBrowserTestsPass -> everyBaseURLClauseRequired` was false. + +Chosen owner boundary: extend the existing navigation-classifier test with one +positive configured-URL row and independent negative rows for non-string +input, protocol, hostname, port, username, password, path, query, and fragment. +Run this table before navigation so an admission failure cannot be hidden by a +later response or heading failure. + +Rejected lower-cost alternative: source inspection proves clause presence but +not behavioral dependence. A single malformed URL cannot distinguish the +clauses. A new test file, exported production helper, or general URL policy +adds no owner. + +Proof invariant: the exact configured `http://127.0.0.1:/` bytes +normalize to their own canonical URL; changing any admitted authority +dimension makes its corresponding row reject with the owner error. Username +and password have separate counterexamples, including an empty-username, +non-empty-password URL. Controlled Chromium rehearsal removed the root-path +clause and the path-drift row failed because the helper admitted its mutant. + +Non-claims: the table does not claim DNS confinement, remote URL admission, +network isolation outside the test server, or general URL validation. + +Rollback or overturn condition: broaden the URL domain only when the browser +server contract admits another origin and adds an owner-specific positive and +negative proof. + +Accidental-complexity and decomposition argument: one data table exercises the +existing pure helper in its sole runtime owner. It adds no abstraction, +fixture, browser count, production hook, or retry path. + +### C-124 trigger-and-cleanup falsifier decision record + +Problem: every real trigger returned the expected token, and the 503 and +heading failures occurred after the response waiter settled. Deleting token +admission and `controller.abort()` therefore left static 22/22 and runtime +93/93 green. The source guards did not imply behavioral dependence. + +Chosen owner boundary: in the existing navigation test, inject one page-shaped +object whose response waiter remains pending, rejects when its supplied abort +signal fires, and otherwise resolves shortly to a distinct unsuccessful +response. Return a wrong trigger token and require the exact token error, +observed signal abort, and observed invocation of the waiter's rejection +consumer. + +Rejected lower-cost alternative: source inspection is the false-green being +repaired. A real network stall adds timing and browser authority. Removing the +guards would allow the trigger/observer handshake to drift and leave a pending +waiter rejection unowned. + +Proof invariant: wrong token is terminal before response admission; every +failure aborts a still-pending response observation; and its rejection is +explicitly consumed before the original error escapes. If token admission is +deleted, the distinct fallback response error appears. If abort or consumption +is deleted, its exact observation remains false. The injected operation +requires the exact event order `waiter-armed`, `trigger-called`, +`waiter-aborted`, `waiter-consumed`, which also makes pre-arm ordering +executable. + +Non-claims: the injected object is not a general Playwright mock, does not +prove every AbortController behavior, and does not replace real-browser +response, heading, or decoy cases. + +Rollback or overturn condition: remove the seam only if the observer API no +longer creates a pending rejection or another owner provides equally +deterministic token and cleanup falsifiers. + +Accidental-complexity and decomposition argument: one local object exercises +the two failure-only branches of the existing helper. It adds no exported +helper, fixture, file, retry, production state, or test identity. + +The C-27/C-115 axe harness remains an anti-corruption boundary, not a product +layer. C-115 removes the wrapper, not the boundary: exact source initialization, +runtime version admission, rule selection, frame scope, and returned-result +identity stay centralized. Remove the harness only after those obligations +move to a lower-cost owner with equivalent mutation and browser-runtime proof. + +## Review acceptance criteria + +The design is ready for implementation only when independent reviewers agree: + +1. every accepted row has one owner, counterexample, repair invariant, and + non-claim; +2. every rejected row states why its conclusion does not follow; +3. no repair changes valid-input business semantics without an explicit + compatibility record; +4. no new shared abstraction lacks two real consumers; +5. no documentation-only repair substitutes for a runtime or proof defect; +6. every stable browser state enters the executable state matrix; +7. the implementation plan orders owner and contract changes before dependent + projections and full closeout. diff --git a/docs/implementation/audit-remediation-plan.md b/docs/implementation/audit-remediation-plan.md new file mode 100644 index 0000000..77d428a --- /dev/null +++ b/docs/implementation/audit-remediation-plan.md @@ -0,0 +1,4077 @@ +# Audit Remediation Implementation Plan + +Status: C-124 implementation candidate; C-01 through C-124 +corrections are present; provider validation is invalidated by the C-124 +semantic delta; unaffected prior validations remain historical evidence. + +Owner: `proofkit`. + +Design input: +`docs/implementation/audit-remediation-design.md`. + +Target baseline: `3d86b6d0e4ec4a6c6a7f7a35ff2787011771aa64`. + +Branch: `fix/audit-remediation`. + +Review history: + +- cycle 1: three `REVISE` verdicts; corrections applied for owner-first + sequencing, the omitted SBOM tranche, artifact dependencies, committed-object + closeout, exact selector/workflow inventories, race falsifiers, onboarding + continuity, and accessibility oracles; +- cycle 2: three `REVISE` verdicts; corrections applied for exact requirement + ownership, artifact-derived SBOM edges, generated preset topology, + source-only witness classification, branch/PR commands, help continuity, + and route-bearing README code spans; +- cycle 3: three `REVISE` verdicts; corrections applied for the exact two-file + generated-artifact exception and mutation-free branch preflight; +- cycle 4: two `APPROVE`, one `REVISE`; corrections applied for pre-switch + target-ref identity and immutable validated-SHA publication binding; +- cycle 5: unanimous `APPROVE`; no P0-P2 plan contradiction remains. +- implementation cycle 1: proof/security, contract/release, and + UX/accessibility reviewers returned `APPROVE` after C-01 through C-20 and + static-analysis cleanup; the complete worktree `npm run check` passed. +- committed-candidate cycle 1: two `REVISE` findings added C-21 and C-22 for + aggregate job bypass-field absence and the three held per-view loading + states; focused correction review returned three `APPROVE` verdicts. +- committed-candidate cycle 2: two `APPROVE` verdicts and one `REVISE` added + C-23 for ignored workflow/job/step execution controls and partial + `continue-on-error` evaluation; exact environment entries, nullable scalar + key presence, required leaf jobs, and the aggregate were closed, and focused + correction review returned three `APPROVE` verdicts. +- exact-commit cycle 1: proof/security and UX/architecture returned `APPROVE`; + contract/release returned `REVISE` because the `0.2.0` machine change record + omitted the intentional `adoption-doctor` blocked-state and exit-code + migration; C-24 closed the machine record, migration, rendered-note + falsifier, and proof binding, and focused correction review returned three + `APPROVE` verdicts. +- exact-commit cycle 2: proof/security and UX/architecture returned `APPROVE`; + contract/release returned `REVISE` because the record and top-level-only + semantic test omitted the intentional non-enforced advisory rule transition + from `passed` to `skipped`; the first focused review exposed the same + undeclared transition outside the touched selection in `enforce-touched`. + C-25 declares the complete migration, strengthens both adjacent-rule oracles, + and binds them; the second focused correction review returned three + `APPROVE` verdicts. +- provider cycle 1: the exact candidate passed locally but two provider browser + attempts failed at the same 30-second Firefox cap on different tests while + Chromium and WebKit passed; the run lifecycle then deleted the promised + retained traces and the success-only upload skipped them. C-26 preserves + attempt-scoped failure diagnostics without creating or admitting passed + proof so the moving Firefox stall can be diagnosed rather than guessed; the + focused correction review returned three `APPROVE` verdicts after routing + both selectors exclusively to QUALITY-022. +- provider cycle 2: C-26 retained the next locally reproduced Firefox traces; + both failures terminate at the first default axe-source evaluate rather than + an application action or assertion. A minified-source candidate passed two + full runs but a controlled falsifier made both minified and default sources + stall under continuous trace screenshots; the default source completed 13 + two-audit cycles without tracing and with screenshot-free action, DOM, + network, and source tracing. A third full run falsified screenshot removal + alone at the same large builder evaluate. C-27 therefore combines + browser-context axe initialization, constant builder loaders, screenshot-free + traces, one bounded best-effort post-failure screenshot, zero retries, and + the existing timeout; the combination passed 30 consecutive two-audit + Firefox cycles and still requires repeated clean first-attempt runtime proof + before exact-object closeout. +- provider correction review: proof and UX returned `APPROVE`; contract + returned `CONDITIONAL APPROVE` after the exact package allowlist, + owner-to-binding non-claims, and deterministic constant-loader reachability + oracle were aligned. Two consecutive frozen-byte 72-test browser proofs and + direct proof verification passed without an intervening browser-input edit, + discharging the only remaining review condition before final + committed-object revalidation. +- final committed-object review: proof/security found that a descriptor-relative + rename could follow the admitted output parent after it moved outside the + repository between the last route check and publication. C-28 binds the + irreversible rename to full routes through the repository root while keeping + temporary-file cleanup on the pinned parent, and adds exact outside-root and + in-root pre-publication falsifiers. +- final correction review: all three reviewers rejected C-28 because + `os.Root.Rename` resolves mutable source and destination routes separately and + a replacement can substitute the visible temporary basename. C-29 stages + temporary bytes at the repository root, re-admits their identity and the + destination-parent route after the exact barrier, keeps publication and + cleanup root-confined, and records adversarial same-user namespace mutation + after final admission as the unavoidable cross-platform non-claim. +- C-29 correction review: the root-level candidate was rejected for + writable-child and nested-filesystem regressions, missing content admission, + an omitted compatible release-record entry, and PR-body checks that did not + machine-validate every promised closeout fact. C-30 restores pinned + same-parent staging and publication with identity-plus-mode-plus-content + admission; + C-31 closes the release projection; C-32 adds a canonical closeout-body + record. The next correction review found that permission-only mode checks + omitted special bits and that local artifacts were reread from mutable paths; + C-33 requires complete-mode equality and one private byte snapshot per local + evidence object. C-34 removes artifact fields not admitted by the exact local + predicates. C-35 rejects multi-document JSON substitution and admits the + exact browser-project inventory. The independent maximum-reasoning audit then + reproduced dynamically false CI-step and release-candidate conditions; C-36 + closes both workflow condition inventories. Its correction review then + replaced the macOS smoke with an Ubuntu no-op while preserving aggregate + success and reproduced vacuous negative selectors; C-37 binds exact CI check + names, runners, and platform-smoke execution and requires positive owner + admission inside bound falsifiers. The next review inserted a semantic shadow + step, passed a mixed-type runner list, reproduced missing positive + package-gate owners, and found a malformed closeout filter; C-38 closes the + ordered platform topology and package-script owner, exact scalar runners, + positive CI/release package-gate admission, and executable singleton filter. + The next review moved the shadow into the repository-local setup action and + added an empty `run` key beside `uses`; C-39 binds exact local-action bytes + and makes execution-key presence part of the exact step inventory. +- The C-39 correction review returned three independent `APPROVE` verdicts + after reproducing the nested local-action shadow and null, empty, whitespace, + and dual execution-key mutants; no P0-P2 finding remains. +- The next independent maximum-reasoning audit approved the exact C-39 commit + at 97/100 with no P0-P2 finding and confirmed two P3 hardening gaps. C-40 + closes exact QUALITY-011/013 selector-set immutability and bounded + expansion-free README shell-word parsing; all exact-object gates and reviews + must therefore be repeated. +- C-40 focused review reproduced owner-only transfer of a protected scenario + while its exact selector set remained unchanged. C-41 binds every protected + inventory to the exact requirement/scenario pair and adds the missing + owner-transfer falsifier. +- The same cycle reproduced all-selector deletion through an empty-selector + early return, NUL admission that direct execution cannot preserve, and a + stale P0-P2-only final-review threshold. C-42 closes all three contradictions. +- Continued focused review reproduced Unicode-trim drift in both command and + JSON fence bytes, escaped-NUL lookahead bypass, and double-quoted interactive + Bash history expansion. C-43 preserves exact boundary bytes and closes each + lexer mutant without widening the grammar. +- C-43 review then reproduced a trailing escaped-space/tab false reject caused + by symmetric delimiter trimming. C-44 restricts preprocessing to leading + delimiters and preserves both mutants. +- The first complete package-level run then exposed that the generic + missing-function mutant was shadowed by the new exact-set check. C-45 routes + it through an unprotected binding and keeps both failure classes reachable. +- Continued review reproduced even-backslash history-literal false rejects, + one remaining P0-P2-only preparation threshold, and repeated generic I/O in + every pure inventory mutant. C-46 through C-48 close all three without + widening product scope. +- Complexity review then found quadratic rescanning of long non-history + backslash runs in the first C-46 helper. C-49 consumes each run once and + retains a large-run regression. +- The next complete package-level run showed partial generic fixtures stopping + at the earlier global inventory phase. C-50 keeps production composition but + routes isolated fixtures directly to the validation phase they own. +- Frozen C50 review then found two generic executability falsifiers absent from + selective QUALITY-010 routing. C-51 binds both and closes the scenario's + exact four-selector inventory. +- Frozen C51 review reproduced the same gap for QUALITY-013's permission-floor + falsifier. A closed-world owner pass found ten omitted typed-oracle tests; + C-52 binds and protects the complete seventeen-selector owner surface. +- The frozen C52 correction review returned three independent `APPROVE` + verdicts with no confirmed P0-P3 finding after exact inventory, README + lexer, JSON byte, owner-separation, documentation-parity, and business-logic + checks. The implementation is therefore ready for final committed-object + validation. +- The next exact-commit review confirmed that readiness-closeout's intentional + character-reference verdict change was absent from the closed release record + and that selector admission rejected a toolchain-valid unnamed + `*testing.T` parameter. C-53 adds the release declaration, migration, + rendered-note witness, Go grammar regression, and exact QUALITY-010 and + QUALITY-024 selector inventories before committed-object validation repeats. +- The repeated exact-commit review confirmed five P2 gaps: decoding before + Markdown pipe parsing, an untested stable specifications no-match state, + undisclosed removal of synthetic Arrow-key navigation, and undisclosed + compatible pilot-all envelope plus witness-selector I/O additions. C-54 + repairs the parser order, adds the complete browser row, and projects all + three public changes through the closed release record and rendered notes. +- The next exact-commit UX review confirmed that root help displayed a bare + executable unavailable to a normal npm consumer while the installed witness + executed separately hard-coded argv. C-55 makes the displayed route the + copyable offline npm exec command, parses and executes those exact bytes, and + rejects the bare-route mutant. +- C-55 correction review confirmed that Unicode `TrimSpace` admitted leading + or trailing NBSP around the displayed command even though Bash does not + treat NBSP as an IFS delimiter. C-56 removes only the authored leading + ASCII space/tab indentation and keeps both NBSP falsifiers. +- The next exact-commit review confirmed a discontinuous onboarding witness, + incomplete and misclassified release declarations, under-scoped symlink + migration, merge/release semantic shadow steps, and a five-owner workflow + oracle concentration. C-57 through C-60 close the full displayed route, + release classification and migration, complete step inventories, exact + large-file ledger, owner-aligned split, and moved-selector anti-deletion + proof. +- The C-60 decomposition review approved one minimal owner-cluster split and + rejected one-requirement-per-file decomposition because QUALITY-011 and + QUALITY-013 share an exact selector while each binding admits one + `witnessPath`. The implementation preserves that cluster and separates only + independently changing owners plus neutral support. +- The C-60 correction review confirmed six residual gaps in step-field + closure, witness-path ownership, untracked-file staging, exact size evidence, + scanner selective routing, and ordinary leaf invocation UX. C-61 through + C-66 add independent mutants and owner-bound repairs before candidate + creation. +- Candidate-command rehearsal rejected temporary-file cleanup under the active + destructive-action guard. C-67 keeps the exact staging predicates but uses + in-memory inventories and stdin pathspec admission, so the documented + sequence is executable without cleanup authority. +- The next focused review admitted a missing scanner permission floor, a + surplus provider write scope, an installed block before Usage, and a + command-token prefix collision. C-68 and C-69 add exact permission-map and + post-parse invocation predicates with independent mutants. +- C-69 proof review then deleted the new test while every bound QUALITY-019 + selector remained green. C-70 adds the falsifier to the exact owner selector + and path inventory. +- The final C-61 through C-70 focused correction review returned three + independent `APPROVE` verdicts with no confirmed P0-P3 finding on one + unchanged frozen snapshot. Candidate staging and exact committed-object + validation may proceed. +- Final provider-closeout rehearsal then rejected the current 75-test browser + matrix because its executable predicate still required 24 rather than 25 + tests per project. C-71 aligns that predicate with the immutable gate output + before repeating the entire committed-object epoch. +- The repeated exact-commit architecture review then reproduced overlapping + adoption output conditions and showed that last-write-wins repeated + `--mode`/`--pilot` argv prevented a unique raw-argv selector projection. + C-72 adds the bounded condition model, finite native-option closure, + argv-derived ABI binding, duplicate-selector rejection, and release migration + before the committed-object epoch repeats again. +- Splitting the C-72 pure condition algorithm from generator I/O then created + a sixth decomposition owner while P12.2 still required the earlier exact + five-file workflow inventory. C-73 extends that owner subset to six without + weakening staged-path equality or the empty-remainder predicates. +- Raw-argv falsification then admitted `--pilot ""`: the parser marked the flag + present but native normalization and the ABI condition both treated its empty + value as omission. C-74 rejects the empty valued selector and projects that + intentional behavior through the exact ABI, requirements, release record, + and migration. +- Staging rehearsal then rejected comparison of that six-file + decomposition subset with the current untracked set: the earlier candidate + already owns the five workflow files, so the correction amend has exactly one + untracked condition-model owner. C-75 proves the current set independently + and retains the owner subset after staging. +- Condition-closure review then found that the 80-state test duplicated native + mode and pilot literals. C-76 derives immutable domains from the same + internal lists that build native admission maps and preserves the exact + 80-combination and twelve-valid-state assertions. +- Claim review then found that the generic condition grammar did not prove + native-domain or argv closure for an arbitrary future definition. C-77 + admits only the current adoption output owner and makes a second opt-in fail + until its own executable closure witness is added. +- Baseline-diff rehearsal then found seventeen pre-existing added paths, so the + six decomposition owners were not the complete baseline-relative inventory. + C-78 requires all eighteen final added paths exactly and proves the six-owner + set only as a subset. +- Independent C-78 review then found that one guidance failure emitted JSON + without exact condition/variant binding, the admitted definition could be + rebound through another command or direction, and explanatory prose retained + stale line counts. C-79 through C-81 close the JSON route, exact owner triple, + and snapshot-measurement ownership. +- C-79 correction review then deleted both guidance-failure route coordinates + while the duplicate successful guidance condition preserved the global count. + C-82 requires JSON assertion and both route coordinates together per case. +- C-82 review then deleted the JSON assertion with both coordinates while + runtime still emitted unchecked JSON. C-83 equates fixture expectation with + observed stdout and protects the exact fourteen-case JSON inventory. +- Final committed-object decomposition review then found an identical generic + sorted-key helper duplicated inside the same Go package. C-84 reuses the + existing owner and deletes the redundant helper and import. +- Final committed-object proof review then found that the critical Mach-O + byte-compatibility scenario selected only a README projection test. C-85 + binds the exact four byte-admission/parser witnesses and protects their + selector and path inventory. +- Repeated exact-object review then found two analogous semantic false routes: + Python wheel-platform parity did not reach wheel projection or verification, + and browser one-shot cleanup selected only the fixed launcher. C-86 binds + the named operations directly and closes both exact inventories. +- Exhaustive review of all candidate-added selector rows then found one final + analogous route: mutable-release-fact policy selected only reference closure. + C-87 binds its existing ten-case stale-fact test and exact inventory. +- The independent Sol/max audit then found a scanner authority escape outside + selector routing: exact permissions on named jobs did not reject an + unclassified write-capable job. C-88 closes the exact advisory/provider job + union and adds the surplus-job falsifier. +- Terminal UX review then executed a preset's exact generated continuation in + the installed npm consumer and reproduced a bare-binary `command not found`. + Initial review rejected a global npm renderer because it broke the Python + wheel. C-89 now uses an explicit immutable launcher profile and proves exact + continuations in both installed channels. +- Terminal contract review then proved that adoption, pilot, and self-check + output selectors could pass without observing their declared output roots, + and that pilot output omitted its app-layer union constructor from native + ownership. C-90 closes all three exact selector/binding/source tuples. +- Frozen implementation review then found secret/control launcher values and + unbound help, structured argv, project workflow identity, and installed-wheel + route sinks. C-91 hardens admission; C-92 closes and binds those sinks while + preserving caller-owned argv. +- Provider exact-object review then reproduced Linux inode reuse inside the + output-writer identity mutant and retained a Firefox trace that stalled only + in the graph table's page-realm `evaluateAll` after the page was fully + rendered. C-93 makes the identity substitution deterministic with a + pre-existing live replacement; C-94 proves the same exact ordered table + equality through retryable count and indexed-attribute assertions, retaining + zero retries and the 30-second timeout. Independent focused architecture and + UX reviews approved both repair classes. +- Final ledger review found the `agentroute.go` measurement stale after the + last static-analysis cleanup. C-95 requires one more complete threshold + measurement after all correction bytes freeze. +- The first exact-object Sol/max audit then reproduced two input-closure gaps: + typed workflow decoding ignored valid execution controls, and CSS was absent + from source hygiene. C-96 adds a raw closed-key oracle over the exact tracked + workflow inventory with only exact release-environment exceptions. C-97 adds + CSS and derives both staged and worktree language mutants from tracked + browser-asset extensions without changing the token matcher. +- Dependency pre-merge validation then produced two current-base Firefox + failures on one immutable dependency branch; the timeout moved between the + collapsed and Unicode selection scenarios. C-98 replaces their repeated + manual page-realm range synthesis with Playwright `selectText` and click for + collapse plus one locator-scoped exact-range operation for the nonzero + Unicode selection while preserving one worker, zero retries, 30 seconds, the + exact test identities, and Unicode code-point proof. +- The next exact-object Sol/max audit returned four P2 findings and no P0, P1, + or P3 finding. C-99 separates the historical audit baseline from the current + integration, PR, and provider base; C-100 admits the final 173-scenario + coverage artifact; C-101 closes launcher admission over Unicode `Cc` and + `Cf`; and C-102 declares the accepted contract-envelope stack alias while + rejecting repeated or mixed pilot selectors. +- C-99 through C-102 review cycle 1 returned one `APPROVE` and two `REVISE` + verdicts and closed exhaustive Unicode classification, two-phase identity, + singleton-parent, temporary-closeout, durable-owner, and compatibility + omissions. Cycle 2 returned one `APPROVE` and two `REVISE` verdicts because + its migration wording excluded the valid omitted default-first pilot route. + Cycle 3 returned three independent `APPROVE` verdicts on the same frozen diff + with no confirmed P1-P3 finding. +- The mandatory final Sol/max audit of exact commit + `e55bfc6e5641aed906d9a3c02e56a431bc0ca4b5` returned one P2 and no P0, P1, + or P3 finding: the current release-record witness checked only 10 of 12 + breaking changes and 7 of 11 additions and admitted semantic deletion or + surplus after regeneration. C-103 closes the entire record and rendered-note + inventories at the existing QUALITY-024 selector. +- C-103 review cycle 1 returned one `APPROVE` and two `REVISE` verdicts and + added exact machine ID/order mutants plus note-projection closure. Cycle 2 + returned two `APPROVE` verdicts and one `REVISE` because section-local + equality still admitted appended surplus, duplicate, or second owned + sections. Cycle 3 returned three independent `APPROVE` verdicts on one + frozen diff after complete ordered machine equality and one independently + authored byte-exact full-note projection closed every confirmed escape; no + confirmed P1-P3 finding remains. +- Publication rehearsal after the first C-103 terminal approval reproduced + the plan's unbraced zsh variable-colon refspec failure. C-104 uses an + unambiguous braced variable, requires a complete tracked-plan scan for the + rejected form, and repeats the exact-object and provider epoch. +- C-104 focused review returned two `APPROVE` verdicts and one `REVISE` + because the plan's lease still preceded the already successful first + publication. C-105 preserves the first lease as history and binds the next + correction publication to exact current remote head `90090a5c...`. +- The mandatory new post-C-105 Sol/max audit returned one P2 and no other + confirmed P0-P3 finding: P12.2 still declared 18 baseline-relative additions + after three later owner-test additions made the exact final set 21. C-106 + refreshes that closed set and the then-current C-106 two-file amend staging + predicate. +- The mandatory new post-C-106 exact-object Sol/max audit of + `4a828d1be9e3f9cab0e93d4ef5991fef0d2cd475` returned two P2 findings, one P3 + finding, and no other confirmed P0-P3 finding. C-107 closes the exact + Scorecard output-input set with a surplus mutant; C-108 restores the complete + 30-ID requirement-invariant delta; C-109 removes the four stale + reverse-decomposition line-count qualifiers. +- C-107 through C-109 focused review returned one `APPROVE` and two `REVISE` + verdicts. C-110 replaces generic truth normalization with exact decoded + boolean admission and adds the surviving string-substitution mutant. C-111 + time-indexes the historical two-file set to C-106 while preserving P12.2's + then-current three-file set. +- C-110 through C-111 focused review returned two `APPROVE` and one `REVISE` + verdict because one design-history sentence still called C-106 the latest + current epoch. C-111 now time-indexes every historical 21-path and two-file + assertion to the C-106 freeze. +- C-111 focused review cycle 3 returned two `APPROVE` and one `REVISE` because + a differently named second Scorecard action could bypass the selected-step + input predicate. C-112 closes the complete Scorecard-action subset and adds + the surviving second-action mutant. +- C-112 focused review cycle 4 returned three `REVISE` verdicts because the + Scorecard subset classifier was case-sensitive while GitHub + owner/repository identity is not. C-113 admits the repository portion with + case-insensitive equality, preserves ref bytes, and adds the surviving + mixed-case second-action mutant. +- C-113 focused review cycle 5 returned three `REVISE` verdicts because Unicode + simple case folding was broader than the declared ASCII repository domain. + C-114 rejects non-ASCII repository bytes and adds the surviving long-s + mutant. +- Exact provider run `30275221625` for + `81e2c7d570e1982ffe4a9f1e5a43150438017b41` passed source and macOS quality, + but `quality / browser runtime` passed only 74 of 75 tests and failed, which + caused `quality / required aggregate` to fail. The retained Firefox trace + stops at the auxiliary pre-audit version evaluation after the page state was + rendered; it does not prove the underlying Firefox/Juggler cause. +- C-115 design review required five correction cycles before cycle 6 returned + three independent `APPROVE` verdicts. The approved controlled hypothesis + removes the + source-only axe wrapper, combines the default and explicit target-size + projections in one direct pinned-engine run, closes rule/config/frame/result + and call-topology mutants, and reopens adjudication if a bounded full + Firefox or exact-provider attempt times out again. +- C-115 plan review cycles 1 through 6 returned at least one `REVISE` verdict + while closing the exact dependency-inventory selector, materialized and root + digest remeasurement, canonical Firefox identity parity, one-shell + lifecycle, bounded server termination, per-process wall-clock deadlines, + leader-versus-descendant process-group cleanup, pre-spawn wrapper-signal + forwarding, residual-process non-claims, and exact signal/probe forensic + records. Cycle 7 returned three independent `APPROVE` verdicts on SHA-256 + `bb8eeb2317018030fbb273e1f18e081c33e1a022c78223947b3a8f989e626d44` + with no confirmed plan gap. +- C-115 implementation review returned three independent `APPROVE` verdicts + on exact diff SHA-256 + `4bf170fc8f5ea50619bc414badd88cd0997198e0419e2739357760c0e577d33f`; + direct axe topology, dependency closure, rollback, concurrency, and + zero-retry mutants were green. +- The immutable C-115 epoch used input digest + `sha256:8099d7060ba9033c1e8317b6032a8776ef21c879b371edda9a460732f66281f4`. + Iterations 1 through 14 passed 25 of 25 Firefox tests. Iteration 15 passed + 24 of 25 and timed out in the graph test after its visible SVG locator + resolved inside `Locator.boundingBox()`. The epoch stopped; trace SHA-256 + `a08498b0ce74c39a714856c855d7757f0907a8a67df3d06b1cdab3b652904fc7` + and report SHA-256 + `2e5475d20b3161d6d0128b9097288302ad70a94de57b90c6e569533c114383a9` + are diagnostic evidence, not product-failure or exact-engine-cause proof. +- C-116 design review cycles 1 through 13 returned at least one `REVISE` + verdict while closing response causality, local SVG structure and paint, + graph-table trust-state visibility, CSS and SMIL temporal escapes, exact + falsifier independence, and cross-engine serialization. Cycle 14 returned + three independent `APPROVE` verdicts on git blob + `598dfb89b7567df269b41491b15c7fe527248b3d` with no confirmed P0-P3 + finding. +- C-116 plan review cycle 1 returned one `APPROVE` and two `REVISE` verdicts. + The correction makes route/observer teardown an awaited fail-closed state + machine, makes one exact assertion plan observable through an injectable + branch-free executor, and binds every new epoch to the historical 25-ID + digest derived from the hash-verified C-115 failure report. +- C-116 plan review cycle 2 returned one `REVISE`, one `APPROVE`, and one + invalidated review. The correction re-admits the historical 25-ID bytes and + digest after the second full browser run, composite browser gate, and final + full gate rather than checking only the first full projection. +- C-116 plan review cycle 3 returned three independent `APPROVE` verdicts on + git blob `e04c4bdd8859ac71b0d02fde06e7c025813ae459` with no confirmed + P0-P3 finding. C-116 is approved for implementation. +- The complete C-116 executable epoch passed on runtime candidate + `0c67de58b0b9837d714e417f64758a76368f3efa`. Its 30 separate Firefox + processes passed 750 of 750 tests with one worker, zero retries, zero + skipped, unexpected, or flaky tests, and one immutable input digest + `sha256:ec3d79218e20831e726bf45e171b1d0276fdf22a04790a13f1e72e6df8dbee0d`. + Every iteration retained historical test-ID digest + `sha256:f7b80cd6ea950cad6693a7b11020f746581d6eba4f2b7314700e4161448a554c`; + the records JSONL SHA-256 is + `e38754615878a012358d2fe75fd4af031107450a7ec2bc6d70db6bc89c543051`. + Both following full browser proofs passed 75 of 75, the composite browser + gate passed 21 static and 75 runtime tests, and the final `npm run check` + passed. The build, two full-browser, composite, and full-check watchdogs all + recorded `exited`, leader exit code zero, no leader signal, and empty signal + and process-group probe error sets. +- Provider run `30297044766`, attempt 1, reported head + `26e44b79a90b41494f9971b84f66e4b737bc9baa` but checked out synthetic merge + commit `da27a7a1b3e17a901a47621a31ca8ae3432f9901`; their trees are both + `ae3b0b16efc3d185425a91488b1f902eee630c2f`. It later timed out in the + Firefox focus negative control after Playwright 1.61.1 entered + `evalOnSelectorAll`. Artifact `8665124396` has GitHub digest + `sha256:db3179664637de3b053bde5efce6b0e2e8b44e3d96c5b7bf07032a270b2b46b5`; + its report, trace ZIP, and inner trace SHA-256 values are respectively + `3498361d22679cc87c6560c055750bb3c782bb1d8761b28e5287499e0486a4d2`, + `b4f5560b9e0e240dab35e631d9b848a6f18817b02ca6c03c2254a32ba989328d`, + and `86a30f0e21dc41a9961d26506f262e18a1cfd8832cca24d9c70a1504864dc0a4`. + The inner trace has no matching return. Chromium, WebKit, source, macOS + smoke, CodeQL, OSV, and semantic diff passed. This falsifies provider + liveness, not product behavior, and activates the plan's isolated 1.62 A/B. + Change only `package.json`, `package-lock.json`, the package-verifier exact + dev-dependency pin, and its fixture. Preserve all tests, one worker, zero + retries, 30-second tests, production code, and business logic. Require + package-verifier tests, then a new input digest and immutable 30-process + Firefox epoch from iteration 1, two full browser proofs, browser static 21 + of 21 plus runtime 75 of 75, the full gate, committed-object review, and a + fresh attempt-1 provider run. + Bot PR 80 run `30250528617` used base + `3d86b6d0e4ec4a6c6a7f7a35ff2787011771aa64`, synthetic merge + `3367101eaf48fd664f1c1975181c15d047d7fac2`, and browser artifact + `8646807702` with GitHub digest + `sha256:57de6b30f9a3a82ca33b4ad18f9f36c2b47dbc55dac301c4f11669040b7a4ae1`. + Its green provider browser job ran only the older six tests per engine, so it + is supporting A/B evidence only and does not cover the current focus negative + control. Its red source and aggregate jobs prove that its unsynchronized + verifier pin is not independently mergeable. +- The first Playwright 1.62 post-epoch full gate exposed C-117. With a retained + `wheel`-group `TMPDIR` outside the caller's groups, Darwin returned success + from the setgid `chmod` but cleared the bit. The writer correctly observed + unchanged `0644`; the test incorrectly treated API success as proof that its + mutant existed. The subtest passed 100 of 100 under the default + `staff`-group temp root and failed 100 of 100 under the retained root. Repair + the test oracle only; production output semantics remain unchanged. +- Final exact-object review found that C-119's HTTP-response and exact-heading + guards had no guard-specific falsifiers: deleting or weakening them retained + an 81-test green matrix. C-120 adds distinct open and reload 503 cases and a + substring-preserving accessible-name drift before a new committed-object and + provider epoch. +- Provider attempt 1 for exact source + `dcc824b31f858ab8fea5be683e5d81f12f039279` falsified even the remaining + `commit` lifecycle wait: Firefox received status 200, loaded every local + resource, and rendered the initialized workspace while `page.goto` remained + pending. C-121 replaces lifecycle waiting with a pre-armed exact-URL, + main-frame navigation-response observer, exact trigger token, response + admission, and the existing exact heading. A pure classifier truth table, + live same-URL fetch decoy, static lifecycle-source oracle, and the C-120 + response and heading mutants close the new proof surface. +- Final Sol/max review of exact candidate + `c2315fdf28be95eab08089008773d7dd234d9c96` reproduced two false-green + documentation/proof candidates. C-122 makes P12.2's declarative and + executable post-`c2315fd` correction inventories the same three paths. + C-123 adds positive + and negative exact raw-base-URL cases so each local-origin clause is + executable rather than source-only. +- The same review reproduced C-124 by deleting token admission and waiter + abort while static 22/22 and runtime 93/93 stayed green. One injected + pending waiter in the existing navigation test now requires the exact token + failure, abort observation, and rejection consumption independently. + +## Purpose + +This plan converts the approved audit-remediation design into an ordered, +test-first implementation graph. It is complete only when every finding has a +durable owner update, an executable counterexample, a passing repair, a +business-logic compatibility disposition, and final full-gate evidence. + +## Authority and retirement + +The design's authority order, scope, non-claims, and retirement rules apply +unchanged. This plan is temporary execution authority. It retires with the +design only after the durable implementation, exact-object gates and reviews, +validated branch and pull request, the design's closed source-owned required +provider-check inventory is present and successful, every other triggered +check has a terminal disposition, retrospective routing is complete, and the +final pull-request closeout projection satisfies the design's retirement +predicate. + +## Execution policy + +Implementation uses one writer in the shared worktree. Review agents remain +read-only until code-review rounds. This prevents shared-state mutation from +invalidating independent review evidence. + +For every tranche: + +```text +owner delta + -> current-wrong counterexample + -> observe expected red + -> minimal production repair + -> narrow green + -> adjacent positive regression + -> contract/binding/non-claim parity +``` + +No tranche may: + +- weaken a durable non-claim to make a test pass; +- infer provider or registry evidence from local artifacts; +- add a shared abstraction with fewer than two genuine consumers; +- split or merge a large file without a separately proven cohesion defect; +- hide an unresolved selective edge behind the full gate; +- retain both old and new release-record owners; +- modify an already published release or historical tag. + +The `owner delta` is not deferred to P10. Before the first counterexample in +each P1-P9 tranche, the implementer must update the exact requirement, +non-claim, binding skeleton, and witness selector named by the design's durable +proof table. The binding may point to a test that is red during the tranche, +but the owner and intended proof route must already exist. P10 only validates +cross-owner parity, canonical order, and final freshness. + +## Dependency graph + +```text +P0 frozen baseline and owner inventory + | + +--> P1 release/version foundation + | | + | +--> P1A artifact-specific SBOM semantics + | +--> P4 machine CLI contracts + | +--> P8 package/onboarding projections + | + +--> P2 evidence/channel classification + | + +--> P3 confined filesystem boundaries + | + +--> P5 workflow and release oracles + | + +--> P6 proof-gap falsifiers + | + +--> P7 context wire migration + | | + | +--> P9 browser state and accessibility + | + +--> P8 package/onboarding projections + | + +--> P9 browser state and accessibility + | + v + P10 durable proof surfaces + | + v + P11 worktree review and candidate preparation + | + v + P12 committed-object review, push, pull request, provider status +``` + +Only P1 must precede public contract and package metadata projections. P7 must +precede browser tests that consume the new manifest vocabulary. Other +production tranches may be developed independently in concept, but are applied +serially in the shared worktree. + +## P0: Freeze and preflight + +### Objective + +Prove the implementation starts from the reviewed object and preserve user +changes. + +### Actions + +1. Before any branch mutation, require the only changed paths to be the two + reviewed, untracked implementation documents: + + ```bash + git branch --show-current + git rev-parse HEAD + test "$(git status --porcelain=v1 --untracked-files=all)" = \ + $'?? docs/implementation/audit-remediation-design.md\n?? docs/implementation/audit-remediation-plan.md' + ``` + +2. Before any switch, reject a target branch attached to another worktree: + + ```bash + current_worktree="$(git rev-parse --show-toplevel)" + target_worktree="$(git worktree list --porcelain | awk ' + /^worktree / { worktree = substr($0, 10) } + $0 == "branch refs/heads/fix/audit-remediation" { print worktree } + ')" + test -z "$target_worktree" || test "$target_worktree" = "$current_worktree" + ``` + +3. If the target ref already exists, validate its object and base relation + read-only before switching: + + ```bash + if git show-ref --verify --quiet refs/heads/fix/audit-remediation; then + test "$(git rev-parse refs/heads/fix/audit-remediation)" = \ + "3d86b6d0e4ec4a6c6a7f7a35ff2787011771aa64" + test "$(git merge-base refs/heads/fix/audit-remediation origin/main)" = \ + "3d86b6d0e4ec4a6c6a7f7a35ff2787011771aa64" + fi + ``` + +4. Create or switch to the branch without force or replacement: + + ```bash + if git show-ref --verify --quiet refs/heads/fix/audit-remediation; then + test "$(git branch --show-current)" = "fix/audit-remediation" || + git switch fix/audit-remediation + else + test "$(git rev-parse HEAD)" = \ + "3d86b6d0e4ec4a6c6a7f7a35ff2787011771aa64" + git switch -c fix/audit-remediation + fi + ``` + +5. Recheck the exact post-switch object and changed-path set: + + ```bash + test "$(git branch --show-current)" = "fix/audit-remediation" + test "$(git rev-parse HEAD)" = \ + "3d86b6d0e4ec4a6c6a7f7a35ff2787011771aa64" + test "$(git merge-base HEAD origin/main)" = \ + "3d86b6d0e4ec4a6c6a7f7a35ff2787011771aa64" + test "$(git status --porcelain=v1 --untracked-files=all)" = \ + $'?? docs/implementation/audit-remediation-design.md\n?? docs/implementation/audit-remediation-plan.md' + ``` + +6. Capture exact initial file inventory and package versions. +7. Run the cheapest baseline checks: + + ```bash + git diff --check + go test ./internal/command/adoptiondoctor \ + ./internal/command/publicapi \ + ./internal/command/readinesscloseout \ + ./internal/tools/releasesbom \ + ./internal/tools/releasechange \ + ./internal/tools/coveragemetrics \ + ./internal/tools/packageverify \ + ./scripts \ + ./internal/app + ``` + +8. Do not rerun the full artifact gate before the first production tranche + unless a narrow baseline unexpectedly fails. + +### Stop condition + +Any pre-existing test failure, unknown tracked change, or head mismatch stops +implementation until it is adjudicated. It is not relabelled as an audit fix. + +## P1: Release and version-policy foundation + +### Findings + +R-10 and the versioned parts of R-02, R-09, R-24, R-26, R-28. + +### Owner changes + +- update `REQ-PROOFKIT-QUALITY-024` to own change-record schema v2, + `previousVersion`, `changeClass`, and compatible bump classification; +- update its binding and witness skeleton before adding the first red case; +- preserve registry/publication non-claims; +- add migration text that the already published `0.1.160` is immutable. + +### Test-first edits + +1. In `internal/tools/releasechange/record_test.go`, add: + + - `0.1.159 -> 0.1.160` plus breaking changes: rejected; + - `0.1.160 -> 0.2.0` plus breaking changes: admitted; + - `1.2.3 -> 1.2.4` plus breaking changes: rejected; + - `1.2.3 -> 1.3.0` plus compatible changes: admitted; + - `previousVersion >= version`: rejected; + - declared `compatible` plus required migration: rejected; + - invalid or non-canonical SemVer: rejected; + - duplicate/unknown v2 keys: rejected. + - generated npm install and rollback commands both include `--save-exact`, + and rollback names the literal `previousVersion`. + +2. Update release closeout tests to require the v2 path and version relation. +3. Confirm the new cases fail against the current v1 implementation. + +### Production edits + +1. Replace `release/change-record.v1.json` with + `release/change-record.v2.json`. +2. In `internal/tools/releasechange/record.go`: + + - set `RecordPath` to v2; + - admit `previousVersion` and closed `changeClass`; + - implement local SemVer parsing/comparison; + - derive the required change class from breaking changes and migration; + - reject patch-range-compatible breaking versions; + - render exact npm install and rollback commands from the admitted current + and previous versions. + +3. Update every exact consumer: + + - `internal/tools/releasemanifest`; + - `internal/tools/releasecloseoutinput`; + - `internal/tools/releasepreflight`; + - workflow source oracles; + - package checks; + - release documentation; + - binding and witness records. + +4. Advance synchronized version surfaces to `0.2.0`. The only authored version + owners are `package.json` and `package-lock.json`; Python metadata, artifact + names, release fixtures, and manifests are regenerated or validated + projections: + + - `package.json` and lockfile; + - Python project/package metadata; + - change record current version and `previousVersion=0.1.160`; + - release fixtures, manifests, expected artifact names, and notes. + +5. Record all intentional breaking changes already approved by the design, + including both `adoption-doctor` blocked-prerequisite state/exit and + non-enforced advisory rule-status changes, the absolute-symlink rejection, + and versioned context/browser envelopes. +6. Bind a direct current-record falsifier proving that the adoption-doctor + change and migration step reach rendered release notes. + +### Narrow gate + +```bash +go test ./internal/tools/releasechange \ + ./internal/tools/releasemanifest \ + ./internal/tools/releasecloseoutinput \ + ./internal/tools/releasepreflight +``` + +### Rollback condition + +If a live owner proves `0.2.0` conflicts with an unpublished version allocation, +stop. Do not create a special-case validator for `0.1.160`. + +## P1A: Artifact-specific SBOM dependency semantics + +### Finding + +R-02. + +### Owner-first changes + +Before the red case, update `REQ-PROOFKIT-QUALITY-002`, its binding, and its +witness skeleton to distinguish runtime dependencies of the shipped npm +artifact from source/tool module inventory. Preserve the non-claim that a +source module edge implies an installed-artifact runtime edge. + +### Test-first edits + +1. Add `TestArtifactSpecificRuntimeEdgesAndExcludedInventory` in + `internal/tools/releasesbom/main_test.go`. +2. Use two independent fixtures: + + - source/build inventory from a module graph containing source-only, + test-only, and tool modules; + - content-bound build-info inventories for individual release binaries. + +3. Require: + + - one canonical component per admitted package identity; + - every source-graph-only component has `scope=excluded`, + `proofkit:evidence-class=source_build_inventory`, and no runtime edge; + - a runtime edge exists only from the exact binary BOM reference whose + build information names the module; + - stripped binaries with no admitted build information have an empty + runtime edge set; + - distribution representations do not invent runtime edges. + +4. Mutate one source-only module into a runtime edge and prove failure. +5. Bind each binary inventory to its file content/digest so evidence from one + binary cannot authorize another binary's edge. + +### Production edits + +1. Retain `go list -m all` only as excluded source/build inventory. +2. Read each release binary with `debug/buildinfo.ReadFile` or an equivalent + content-bound parser and create `binary BOM-ref -> module` edges only from + that binary's admitted build information. +3. Deduplicate components by canonical package identity before emitting + relationships. +4. Keep package and wheel components edge-free without their own + artifact-derived evidence. +5. Preserve deterministic order and existing package/provenance non-claims. + +### Gate + +```bash +go test ./internal/tools/releasesbom +``` + +## P2: Evidence-state and CLI channel classification + +### Findings + +R-01 and R-07. + +### P2.1 Adoption blocked state + +Files: + +- `internal/command/adoptiondoctor/adoptiondoctor.go`; +- `internal/command/adoptiondoctor/adoptiondoctor_test.go`; +- `internal/app/cli_abi_test.go`; +- retirement requirement and binding records. + +Steps: + +1. Update `REQ-PROOFKIT-RETIRE-008`, its binding, non-claim, and witness + skeleton for unconditional external-precondition blocking. +2. Add `TestBuildBlocksEveryModeForExternalPreconditions`. +3. Cover `observe`, `warn`, `enforce-touched`, and `enforce-all`. +4. Add the adjacent positive case: observe-mode candidate/advisory gap remains + `passed/0` with a `skipped` rule. +5. Confirm the blocked observe/warn cases fail on current code. +6. Split unconditional blocked gaps from policy-enforced advisory gaps. +7. Compute top-level state, rule state, exit code, and promotion readiness from + the correct sets. + +Gate: + +```bash +go test ./internal/command/adoptiondoctor ./internal/kernel/adoptionmode +``` + +### P2.2 Structural input versus semantic report + +Files: + +- the nine command packages named in the design; +- `internal/app/app.go`; +- planning and agent-envelope command adapters; +- `internal/app/cli_abi_test.go`; +- CLI contract process metadata. + +Steps: + +1. Update `REQ-PROOFKIT-QUALITY-004`, the CLI process contract, and the nine + affected binding/witness + skeletons for structural-channel versus semantic-report classification. +2. Add `TestRequiredInputCommandsRouteStructuralErrorsByMode` with exact + command inventory: + + - `branch-authority`; + - `changed-path-set`; + - `deployment-evidence-admission`; + - `external-consumer`; + - `package-runtime-dependency-admission`; + - `readiness-closeout`; + - `registry-consumer`; + - `registry-consumer-proof-input-compose`; + - `repo-profile-admission`. + +3. For ordinary `{}` input assert exit 1, empty stdout, non-empty sanitized + stderr. +4. For supported explicit agent-envelope forms assert exit 1, one invalid-input + JSON envelope, empty stderr. +5. For one structurally admitted semantic failure assert nonzero exit, one + report JSON value, empty stderr. +6. Confirm all nine ordinary cases fail on current code. +7. Change builders to return admission errors rather than synthetic reports. +8. Keep envelope conversion only in the app adapter. +9. Remove any app-layer inspection of synthetic report IDs. + +Gate: + +```bash +go test ./internal/app -run RequiredInputCommandsRoute +go test ./internal/command/branchauthority \ + ./internal/command/changedpathset \ + ./internal/command/deploymentevidenceadmission \ + ./internal/command/externalconsumer \ + ./internal/command/packageruntimedependency \ + ./internal/command/readinesscloseout \ + ./internal/command/registryconsumer \ + ./internal/command/registryconsumerinputcompose \ + ./internal/command/repoprofileadmission +``` + +### Compatibility assertion + +No valid admitted semantic decision changes. Only malformed-input channel +routing and impossible `blocked -> passed` outcomes change. + +## P3: Handle-confined filesystem boundaries + +### Findings + +R-03 and R-04. + +### P3.1 TypeScript scanner + +Files: + +- `internal/command/publicapi/public_api.go`; +- `internal/command/publicapi/public_api_test.go`; +- package boundary requirement, CLI contract, and binding. + +Test-first steps: + +1. Update the package-boundary requirement, binding, non-claim, and witness + skeleton for handle-confined admission and the absolute-symlink migration. +2. Add a private staged operation seam with channels controlled by same-package + tests. +3. Add `TestVerifyRejectsDeterministicSymlinkSwap` with two mandatory table + rows: + + - leaf/source symlink swap; + - ancestor-directory symlink swap. + + For both rows: + + - pause at the exact legacy validation/use boundary; + - replace the selected component with an external sentinel route; + - release execution; + - assert error/nonzero, no successful report, and no sentinel bytes in + output. + +4. Add stable relative in-root symlink success. +5. Add absolute in-root symlink rejection with exact migration diagnostic. +6. Add canonical `.tsx` target rejection. +7. Add pinned-file size/identity change rejection. +8. Add `TestCanonicalSourceSnapshotRejectsChangedCrossAliasAdmission`: prove + that stable aliases of one canonical source reuse the first parsed snapshot, + then replace that canonical source and require a fresh alias to fail on + identity or digest drift. +9. Add `TestVerifyPinsPackageRootAcrossInRootSiblingSwap`: replace the + manifest-owning package path with an in-root sibling after source + resolution, and prove all source bytes still come from the package sub-root + pinned before manifest admission. +10. Confirm the staged redirects reproduce current vulnerability without a + polling loop. + +Production steps: + +1. Open the caller-selected repo with `os.OpenRoot`. +2. Open each referenced package as a confined pinned sub-root before reading + its manifest. +3. Admit package manifests and all named sources through that same package + sub-root. +4. Bind their open handles with `os.SameFile`. +5. Read from the pinned lexical handle under existing file and aggregate + budgets. +6. Verify pre/post identity and size. +7. Cache immutable bytes by admitted relative identity and bind every lexical + alias of one canonical source to the first admitted identity, digest, and + parsed exports. +8. Reject a later alias when canonical identity or digest differs. +9. Reject absolute symlink targets as the recorded v0.2 breaking hardening. + +Gate: + +```bash +go test ./internal/command/publicapi +go test ./internal/app -run TypeScriptPublicAPI +``` + +### P3.2 Repository-relative output writer + +Files: + +- `internal/app/requirement_commands.go`; +- `internal/app/cli_abi_test.go`; +- spec-tree output requirement and binding. + +Test-first steps: + +1. Update the spec-tree output requirement, binding, non-claim, and witness + skeleton for root-confined publication. +2. Add private root-operation injection plus exact pre-temporary-file, + pre-object-admission, and irreversible-rename parent-swap barriers. +3. Add `TestOutputWriterRejectsDeterministicParentSwap`. +4. Assert external and in-root replacement sentinels remain unchanged and no + output or temporary residue reaches the displaced parent. +5. Replace the temporary route, change its permission bits, independently add + setuid, setgid, and sticky bits, rewrite its content, and substitute a + symlink before final object admission; require exact rejection, destination + non-mutation, and cleanup in every case. +6. Preserve stable output bytes, mode `0644`, empty stdout, and cleanup. +7. Confirm the current writer is caught by the staged counterexamples. + +Production steps: + +1. Open the repository root once. +2. Create/check parents through `os.Root` and pin the admitted destination + parent. +3. Create an unpredictable temporary file through the pinned parent with + `O_CREATE|O_EXCL`. +4. Write and chmod through the file handle and retain its admitted identity, + exact mode, and expected content digest. +5. Close, cross the exact object-admission barrier, re-admit the non-symlink + temporary entry's identity, exact mode, and content digest, cross the exact + rename barrier, then re-admit the current parent route at the irreversible + rename boundary. +6. Rename to the destination and clean failures through the same pinned parent, + preserving writable-child and nested-filesystem behavior. +7. Keep static symlink/directory diagnostics without relying on them for + confinement. + +Gate: + +```bash +go test ./internal/app -run OutputWriter +``` + +### Non-claims + +No checkout freshness, compiler provenance, protection from adversarial +concurrent content or namespace mutation by the same operating-system user +during the operation, fsync durability, or repository-wide transaction is +claimed. + +## P4: Complete machine CLI contracts + +### Finding + +R-09, plus the machine projection needed by R-15 and R-24. + +### Files + +- `proofkit/cli-contract.v2.json`; +- new `internal/tools/commandcontractgen`; +- new generated `internal/app/command_contract_generated.go`; +- new generated + `internal/command/stackpreset/preset_ids_generated.go`; +- `internal/app/command_descriptors.go`; +- `internal/app/command_help.go`; +- `internal/app/cli_contract_test.go`; +- `internal/app/cli_abi_test.go`; +- `package.json`. + +Before the first red case, update `REQ-PROOFKIT-QUALITY-004` and +`REQ-PROOFKIT-PACKAGE-002`, their bindings/non-claims/witness skeletons, to +name the authored contract, both generated projections, selector-resolution +rule, and freshness gate. + +### P4.1 Define the authored contract model + +1. Add bounded `root_shape_only` input/output definitions with stable IDs and + canonical digests. +2. For all 74 required-input commands, author: + + - contract ID and schema version; + - one or more condition-complete root variants; + - root kind (`object`, `array`, or explicitly unconstrained `json_value`); + - exact closed allowed/required top-level fields for object roots; + - exact owner requirement and native admission witness selector. + +3. For every JSON-producing command, author: + + - every supported JSON-producing flag/mode condition; + - exact root kind and top-level fields for each object variant; + - first-class array variants where a command can return an array; + - exact `rootType=union` when bounded variants have more than one root kind; + - explicit unconstrained `json_value` only where no narrower root contract + is honestly owned, never as the aggregate for a bounded union. + +4. Add exact flag-value choices for stack presets from the same machine owner. +5. State explicitly that nested fields, scalar types, collection cardinality, + nullability, and cross-field semantics are outside this bounded contract. + +### P4.2 Generator + +1. Add `internal/tools/commandcontractgen`. +2. Parse the authored contract with strict duplicate-key rejection. +3. Reject the superseded inferred nested/delegated graph grammar. +4. Reject digest mismatches, missing required-input contracts, missing + JSON-output contracts, duplicate IDs, missing native witness selectors, + duplicate or empty variant conditions, missing default conditions for + optional mode flags, invalid root kinds, scalar-capable `json_value` used + for bounded unions, and allowed/required fields on non-object roots. +5. Resolve each native selector through a deterministic tracked-test inventory: + + - selector identifies an exact `_test.go` path and test function; + - Go AST confirms the function exists with a valid test signature; + - the witness command selects that test; + - stale path, nonexistent test, or unselectable command is rejected. + +6. Generate one deterministic Go map used only for private help/descriptor + metadata and one lower-package preset-ID table. Both outputs come from the + same authored input and one generator execution. +7. Support `--check` without rewriting. +8. Add: + + ```json + "command-contract:check": "go run ./internal/tools/commandcontractgen --check" + ``` + +9. Place `npm run command-contract:check` in `npm run check` before command + family and Go gates. + +### P4.3 Runtime and compatibility projection + +1. Remove manually authored schema summaries where generated metadata owns the + same fact. +2. Merge generated metadata with private runner/owner/test registration. +3. Render help from generated summaries and flag choices. +4. Include fully resolved input/output contract content and canonical digests + in the ABI projection. +5. Add mutation tests for: + + - root kind; + - allowed and required root fields; + - variant condition; + - schema version; + - deleted contract; + - digest mismatch. + - stale/nonexistent native witness selector. + - either generated output stale while the other is current. + +6. Classify `nativeAdmissionWitnessSelector` as source-checkout evidence, not a + package-consumer route. The shipped contract may name that evidence class, + but does not promise that `_test.go` files are installed. + +7. Add direct `app.Run` root-shape assertions for the high-risk multi-mode + commands, including every object/array and agent-envelope route identified + by the independent audit ledger. +8. Treat native source digests only as conservative change-review sentinels, + never as proof that the authored root shape equals native behavior. +9. Update the ABI golden only after every mutation is killed. + +### Gate + +```bash +npm run command-contract:check +go test ./internal/tools/commandcontractgen ./internal/app -run CLIContract +go test ./internal/app -run CLIABI +``` + +### Complexity guard + +Do not create a second JSON Schema runtime or make the generator validate +command input. It projects and closes the public compatibility declaration; +native admission remains command-owned. Do not infer nested ownership from AST +field-name similarity; leave nested/type/cardinality claims explicit +non-claims until an owner-generated typed schema can prove them. + +## P5: Workflow and release source oracles + +### Findings + +R-05, R-06, R-13, and R-28. + +### Files + +- `scripts/workflow_package_gate_oracle_test.go`; +- `scripts/workflow_oracle_support_test.go`; +- `scripts/workflow_browser_runtime_oracle_test.go`; +- `scripts/workflow_runtime_preconditions_test.go`; +- `scripts/workflow_source_oracles_test.go`; +- `scripts/workflow_security_scanner_oracles_test.go`; +- sorted exact inventory of every `.github/workflows/*.yml`; +- supply-chain quality requirements and bindings. + +Before the first red case, update the affected quality requirements, their +bindings/non-claims/witness skeletons, and add `REQ-PROOFKIT-QUALITY-025`. + +### P5.1 Exact expression guards + +1. Add negative fixtures for: + + - expected predicate plus `|| true`; + - `false && expected`; + - expected text only inside a quoted literal; + - whitespace inside a quoted literal; + - empty guard where not explicitly admitted. + +2. Replace substring predicates with exact per-job/per-step allowlists. +3. Normalize layout only outside quoted literals. +4. Preserve current owner-reviewed expressions byte-semantically. + +### P5.2 Exact CI aggregate + +1. Add `TestCIWorkflowDeclaresFailClosedRequiredAggregate`, + `TestCIRequiredAggregateRejectsNeutralizedScript`, + `TestCIRequiredAggregateRejectsExecutionOverrides`, and + `TestCIRequiredAggregateRejectsPlatformSmokeSubstitution` to the exact + requirement bindings. +2. Mutate fixtures with dead branch, `|| true`, early `exit 0`, and background + execution. +3. Mutate inherited workflow defaults and environment, job defaults and + environment, step environment, shell, and working-directory, and literal, + expression, explicitly false, and explicit YAML `null` forbidden fields. +4. Require exact job identifiers, provider-check names, hosted runners, needs, + `always()`, and safe workflow run defaults; require no workflow, job, or + step environment entries, reusable jobs, job defaults, or job-level + `continue-on-error` on any required leaf or aggregate job, one aggregate run + step, and no step-level execution override anywhere in those jobs. +5. Compare the whole aggregate shell block, the exact ordered macOS step + inventory with presence-aware `run`/`uses`/`with` keys, the exact + repository-local setup-action digest, and the whole fail-closed + platform-smoke package-script owner command. + +### P5.3 External action pins + +1. Complete `REQ-PROOFKIT-QUALITY-025` with: + + - external `uses` must be a 40-lowercase-hex commit; + - local `./` actions remain confined; + - no claim of action safety, tag equivalence, or provider execution. + +2. Add binding/witness IDs from the design. +3. Add `TestWorkflowExternalActionsUseFullCommitSHAs`. +4. Discover and sort the exact tracked workflow inventory, reject any + unadmitted extension/path, and inspect every YAML `uses`. +5. Replace one fixture ref with a tag and short SHA to prove failure through + the same common oracle. + +### P5.4 Existing release immutability + +1. Add `TestExistingReleasePathIsReadOnlyAndFailsOnDrift`. +2. Freeze the complete existing-release shell block as one canonical + owner-reviewed form. +3. Permit only exact release view/download provider calls inside it. +4. Remove missing-asset upload/backfill logic. +5. Make missing, extra, or different assets terminate nonzero before mutation. +6. Add negative fixtures for upload, edit, delete, `gh api`, `curl`, alternate + clients, shell indirection, and extra commands. +7. Keep new-release behavior unchanged. + +### Gate + +```bash +go test ./scripts -run 'WorkflowGuard|RequiredAggregate|ExternalActions|ExistingRelease' +npm run go:actionlint +``` + +### Non-claims + +No local oracle proves provider execution, branch protection, release asset +state, or external action safety. + +## P6: Independent blocking falsifiers + +### Findings + +R-11a through R-11d and R-12. + +### P6.1 Self-hosting verdict + +1. Apply split owner-first deltas before red tests: + + - R-11a and R-11d: + `REQ-PROOFKIT-PACKAGE-004`; + - R-11b: + `REQ-PROOFKIT-PACKAGE-006` and `REQ-PROOFKIT-QUALITY-023`; + - R-11c: + `REQ-PROOFKIT-PACKAGE-001`; + - R-12: + `REQ-PROOFKIT-QUALITY-010`. + + Update each exact binding/non-claim/witness skeleton without assigning a + neighboring invariant as owner. + +2. Extract a command-local pure verdict helper over `(process error, output + bytes)` in `scripts/validate-self-hosting-receipts.go`. +3. Keep the fixed executable invocation unchanged. +4. Add `TestRunProofkitVerdictCases` for nonzero process exit, invalid JSON, + wrong state, and + exact passed output. +5. Keep production output unchanged. + +### P6.2 Package and wheel integrity + +1. Add one-negative-at-a-time tests for: + + - npm name/version, duplicate, missing artifact; + - wheel version, duplicate, missing file, and SHA mismatch; + - every root tarball forbidden exact name and suffix; + - local versus CI receipt identity. + +2. Exercise `verifyRootPackage`, not only the leaf deny helper. +3. Use a complete minimal tarball plus exactly one forbidden entry. + +### P6.3 Coverage closure + +1. Add one case for each of the five command-route arrays. +2. Add one case for each of the four linkage dead-zone arrays. +3. Add all-empty success. +4. Require each mutation to fail at least its named negative test while the + positive fixture remains green. +5. Preserve `COVERAGE-01` as unresolved; static route metadata still does not + become semantic execution evidence. + +`npm run self:receipt` is intentionally not part of this narrow gate. It is +artifact-dependent and runs only after P10 rebuilds the current package +evidence. The executable P6 gate is: + +```bash +go test ./scripts -run 'RunProofkitVerdict|PackageArtifactRefs|PythonArtifactRefs|ReceiptID' +go test ./internal/tools/packageverify -run 'ForbiddenRootEntry|VerifyRootPackage' +go test ./internal/tools/coveragemetrics -run 'EachCommandRoute|EachLinkage' +npm run self:coverage +``` + +## P7: Honest context/diff/browser wire vocabulary + +### Finding + +R-24. + +### Files + +- `internal/command/requirementcontext`; +- `internal/command/requirementdiff`; +- `internal/command/requirementgraph`; +- `internal/command/requirementbrowser`; +- affected app ABI tests and CLI contracts; +- spec requirements and bindings. + +Before the first red case, update the context/diff/browser/graph requirements, +bindings, non-claims, and migration witness skeletons for the closed v1 adapter +and v2-only producer boundary. + +### P7.1 Versioned model + +1. Define context snapshot v2: + + ```text + expectedDigestCoverage = none | partial | all + ``` + +2. Fully admit v1 before adapting: + + ```text + unverified -> none + partially_verified -> partial + verified -> all + ``` + +3. Reject mixed v1/v2 keys. +4. Make all producers emit v2 only. + +### P7.2 Downstream envelopes + +Version and migrate separately: + +- semantic-diff input/output v2; +- workspace manifest v2; +- affected workspace API projections v2; +- CLI output contracts and golden corpus. + +Legacy terms may appear only in: + +- strict v1 adapters; +- migration fixtures; +- compatibility diagnostics. + +They must not appear in v2 output, current help, current contract descriptions, +or UI. + +### Tests + +1. `TestV1DigestCoverageAdapters`. +2. `TestV2DigestCoverageProjections`. +3. Mismatch expected/current remains rejected. +4. V1 normalized output equals the equivalent v2 semantic record. +5. Mixed fields and malformed legacy records fail. +6. Add `TestLegacyDigestVocabularyConfinedToV1AdaptersAndFixtures`. It scans + Go sources, CLI contract/help, browser assets, and test producers; legacy + vocabulary is allowed only in exact v1 adapters, migration fixtures, and + compatibility diagnostics. + +### Gate + +```bash +go test ./internal/command/requirementcontext \ + ./internal/command/requirementdiff \ + ./internal/command/requirementgraph \ + ./internal/command/requirementbrowser \ + ./internal/app +``` + +### Non-claims + +Coverage does not authenticate a baseline, producer, checkout, or freshness. + +## P8: Installed-artifact onboarding and documentation closure + +### Findings + +R-14 through R-17, R-22, R-25 through R-27, and the documentation portion of +R-10. + +Before the first red case, update the exact package/spec requirements, +bindings, non-claims, and witness skeletons for R-14 through R-17, R-22, and +R-25 through R-27. + +### P8.1 Canonical install route + +1. Update npm install to `--save-exact`. +2. Use `npm exec --offline -- agentic-proofkit` for canonical local commands. +3. Remove unverified Bun execution examples; retain a non-claim until an + equivalent exact-tarball smoke exists. +4. Explain when a bare command is valid without making it canonical. + +### P8.2 Stack preset and family discovery + +1. Add defensive-copy `stackpreset.IDs()`. +2. Project IDs from the machine CLI contract/generated metadata. +3. Direct help and invalid-ID diagnostics list all and only valid IDs. +4. Root help adds only the copyable + `npm exec --offline -- agentic-proofkit help families` discovery route. +5. Preserve token-efficient opt-in family expansion. +6. Preserve one authored vocabulary: + CLI contract -> generated lower-package table -> defensive-copy + `stackpreset.IDs()`. A sibling generated app table serves descriptors/help. + Remove any manual `presetIDs` list and prove exact bidirectional parity with + the profile map. Both generated files share one freshness gate. + +### P8.3 First valid input + +1. Add one marker-bounded minimal requirement-source JSON block to README. +2. Add the exact offline invocation. +3. State that example IDs and meaning are caller-replaceable and + non-authoritative. +4. Do not make `KnownKeys` globally verbose. + +### P8.4 Installed-artifact end-to-end witness + +Extend `internal/tools/packageverify/main_test.go` with +`TestExactTarballOnboardingTrace`: + +1. build/install the exact local tarball in a temporary consumer; +2. run root help through `npm exec --offline` and prove it exposes exactly one + copyable canonical family-discovery route; +3. parse and execute that exact displayed route, reject a bare executable + mutant, and prove family output exposes + `stack-preset`; +4. execute installed `help stack-preset` and extract every preset ID from that + human/agent help transition; +5. compare help-derived IDs bidirectionally with the installed machine + contract; the contract is a parity oracle, not a substitute UX route; +6. run every discovered stack preset ID; +7. read the marker-bounded command and JSON together from the installed + README; +8. execute exactly the admitted README argv through the same local + `npm exec --offline` and feed it the extracted JSON; +9. require exit 0, passed JSON, and empty stderr. + +### P8.5 Markdown and package reference closure + +1. Replace raw preset pipes in the contract-map table with separate code spans. +2. Add `TestContractMapDecisionTreeHasThreeCells`. +3. Remove `AGENTS.md` and `CONTRIBUTING.md` from the npm package. +4. Remove the active-backlog route from package-public README. +5. Keep `BACKLOG.md` source-checkout-only. +6. Update or exclude package projections that cite contributor-only files. +7. Treat self-hosting selectors as source-checkout evidence, not + package-consumer navigation. +8. Add field-aware `TestPackagePublicReferenceClosure`. +9. Bind mutable-release-fact policy separately to + `TestVerifyNoStalePackageDocsRejectsMutableReleaseFactsInMarkdown`; retain + reference closure only in its own PACKAGE-001 scenario. + +The closure inventory includes README Markdown destinations under the bounded +destination grammar, relative paths in the exact command-navigation statement +and owner-table cells, and every classified reference-bearing string field in +package-public machine projections. It does not claim a complete Markdown +parser or discovery of unclassified code-span paths. A relative reference is +admitted only if the normalized target is a shipped tarball entry; +source-checkout owners are denied explicitly. +`nativeAdmissionWitnessSelector` is an explicit source-checkout evidence class +and is not treated as package-consumer navigation. At minimum, kill: + +- the original `Active work ledger | BACKLOG.md` table-cell form; +- one ordinary dangling README link; +- `docs/MISSING.md` substituted into the exact README command-navigation code + span; +- one dangling package-public machine-field route; +- one false classification of a source-only witness as a shipped route. + +### P8.6 Python/platform projection + +Add a marker-bounded README block and compare it to owners: + +- macOS 12+, arm64/x64; +- Linux manylinux 2.17, arm64/x64; +- Python `>=3.9`; +- Windows unsupported; +- wrapper over the same Go CLI, not an SDK; +- conditional exact version install; +- no current PyPI availability claim. + +Document complete conditional chains: + +```text +python -m pip install agentic-proofkit== +python -m agentic_proofkit help + +uv add --dev agentic-proofkit== +uv run agentic-proofkit help +``` + +Add `TestREADMEPlatformAndPythonProjection`. +Add `TestReleaseTargetsProjectExactPythonWheelMetadata` and bind it with the +README projection plus `TestVerifyWheelContentsRequiresExactWheelMetadata` to +the exact release-platform Python-wheel scenario. + +### P8.7 Browser route diagnostic and launcher contract + +1. Include `workspace` in the command-local invalid-view diagnostic. +2. Add runtime/app parity test. +3. Refine `SPEC-021` to prohibit caller-supplied and native-witness execution + while permitting the fixed OS launcher. +4. Inject launcher operation in tests. +5. Require fixed executable/argv forms and server-generated loopback URL. +6. Add `TestOpenBrowserUsesFixedLauncherAndLoopbackURL`. +7. Keep that selector in the fixed-launcher scenario; bind the distinct + one-shot cleanup scenario to the three exact `TestServeOneShot*` cleanup and + concurrency tests. + +### Gate + +```bash +go test ./internal/command/stackpreset ./internal/app +go test ./internal/tools/packageverify -run 'OnboardingTrace|PackagePublicReferenceClosure' +go test ./internal/tools/pythonpackage ./internal/kernel/releaseplatform +go test ./internal/command/requirementbrowser -run 'InvalidView|OpenBrowser|ServeOneShot' +``` + +## P9: Browser state, accessibility, reflow, and contrast + +### Findings + +R-18 through R-23. + +Before the first red case, update the browser requirements, bindings, +non-claims, and witness skeletons for the state matrix, semantics, reflow, +target-size, and contrast contracts. + +### P9.1 Production state model + +1. Server HTML includes a visible bootstrap-loading state. +2. Initialization removes the capability token before API use. +3. Manifest fetch is inside a bounded initializer with sanitized terminal + failure. +4. View failures and handoff failures use distinct stable state IDs. +5. Active view controls expose `aria-current`. + +### P9.2 Native semantics + +1. Replace synthetic tree/treeitem roles with `ul`, `li`, and `article`. +2. Remove roving tab index and ArrowUp/ArrowDown handler. +3. Add a visible `Handoff packet` heading. +4. Make the packet a semantic region with `aria-labelledby` pointing to that + visible heading. +5. Preserve Tab/Shift+Tab, Enter/Space, selection, Unicode coordinates, and + handoff semantics. +6. Assert `getByRole("region", { name: "Handoff packet" })` in empty, + successful, and failed handoff states. + +### P9.3 Layout and colors + +1. Add `box-sizing`, `min-width:0`, and `max-width:100%` to grid regions. +2. Allow long human text and IDs to wrap. +3. Wrap navigation. +4. Add labelled internal graph and table scroll viewports. +5. Never hide document overflow globally. +6. Define explicit light/dark foreground, background, border, and focus tokens. +7. Preserve forced-colors adaptation. + +### P9.4 Deterministic state matrix + +Refactor `tests/browser/workspace.spec.mjs` around a table: + +```text +state +setup/barrier +expected data-state and heading +default axe +explicit target-size +320px reflow +contrast when applicable +``` + +Required rows: + +- bootstrap loading with deferred manifest; +- bootstrap failed; +- specifications loading with a deferred requirements response; +- specifications; +- specifications no-match with an admitted empty requirement projection; +- diff loading with a deferred diff response; +- diff; +- graph loading with a deferred graph response; +- graph; +- unavailable diff/graph; +- view request failed; +- handoff result; +- handoff failed. + +For every row: + +1. Assert body and stable content-substate identity before any oracle. +2. Run default axe and require no violations. +3. Explicitly enable `target-size`, require applicability, and require no + violations. +4. At 320 by 800, assert no document-level horizontal overflow. +5. Permit only labelled graph/table internal overflow. +6. Compute effective composited colors from actual rendered and focused + controls and their adjacent backgrounds in light and dark schemes for + Chromium, Firefox, and WebKit. +7. Require text contrast at least `4.5:1` and boundary/focus contrast at least + `3:1`. + +In a separate negative test, render an intentionally undersized control, +explicitly enable `target-size`, require applicability, and require at least +one violation. The production state matrix contains only zero-violation rows. + +### Gate + +```bash +npm run browser:static-check +npm run browser:test +``` + +### Non-claims + +No full WCAG 2.2 conformance, branded Safari, complete screen-reader +interoperability, every OS theme, or 400-percent zoom claim is added. + +## P10: Durable proof parity and current artifacts + +### Objective + +Validate that implementation evidence, not the temporary design/plan, owns +every repair. Exact owners and binding skeletons were already created before +their P1-P9 counterexamples. + +### Requirement-source parity + +Confirm only these exact affected invariants changed: + +- `REQ-PROOFKIT-RETIRE-008`; +- `REQ-PROOFKIT-PACKAGE-001`, `002`, `003`, `004`, `005`, `006`, `007`; +- `REQ-PROOFKIT-SPEC-001`, `009`, `011`, `018`, `019`, `021`; +- `REQ-PROOFKIT-SPEC-022`, `023`; +- `REQ-PROOFKIT-QUALITY-002`, `004`, `005`, `006`, `007`, `010`, `011`, + `013`, `016`, `019`, `022`, `023`, `024`; +- add `REQ-PROOFKIT-QUALITY-025`. + +Do not broaden unrelated requirements. + +### Implementation correction epoch + +Apply the design's C-01 through C-124 corrections before final parity: + +1. prove one immutable release byte snapshot owns hash and build information + before a same-handle content/identity recheck, and bind TypeScript canonical + first-admission bytes/digest/identity/parsed exports with deterministic + same-lexical and cross-alias swap falsifiers; +2. reject any CLI command whose honest root-shape input/output projection is + absent, open, structurally invalid, condition-ambiguous, or missing a + supported variant; cover explicit and omitted defaults with direct public + CLI oracles, while retaining nested fields, leaf types, collection + cardinalities, and nullability as explicit non-claims; +3. reject witness selectors that are named declarations but not valid + functions in active `_test.go` files discovered by the current Go build; +4. admit breaking major and pre-1.0 minor version increases without requiring + a `.0` target, while preserving breaking-patch rejection; +5. treat `SPEC-022` and `SPEC-023` as direct schema-v2 consumer owners; +6. keep source-hygiene identifier-sensitive without treating coincidental + content-digest substrings as organization-policy leakage; +7. derive receipt-kind mismatch fixtures from the current binding complement + so a legitimate proof-route expansion cannot silently invert the test; +8. pin package and output parent sub-roots, bind each admitted route to its + handle, and reject both outside-root and in-root sibling substitutions; +9. render blocked and enforceable adoption gaps as disjoint classes and keep + unresolved external prerequisites blocking in every mode; +10. admit plural native source ownership only as a sorted, non-empty, unique + alternative to singular ownership, with freshness checks for every source; +11. route nested deployment scanner admission failures through structural + `stderr`, not synthetic semantic JSON; +12. declare and execute both submitted and terminal one-shot browser output + variants; +13. admit the pilot `all` mode through one strict two-input envelope and prove + the package and public CLI routes; +14. include unstaged tracked deletions in package-artifact snapshot identity; +15. preserve the valid root package in forbidden-root-entry mutation fixtures; +16. remove only report helpers proven unreachable after structural-channel + migration, then refresh their native-source review sentinels; +17. require the CI aggregate to omit job-level `continue-on-error` rather than + attempting partial GitHub-expression truth evaluation; +18. hold specifications, diff, and graph requests independently and run the + complete browser state oracle against each visible loading state. +19. model workflow, job, and step execution controls in the typed workflow + oracle; reject unsafe inherited defaults, unexpected environment entries, + job defaults, shell or working-directory overrides, and any + `continue-on-error` presence on all required leaf and aggregate jobs or any + of their steps. +20. declare the intentional `adoption-doctor` blocked-state and nonzero-exit + breaking change plus its consumer migration step in the versioned change + record, and prove that both reach rendered notes. +21. declare the intentional non-enforced advisory rule transition from + `passed` to `skipped`, prove its migration reaches rendered notes, require + exact `observe=skipped` and `warn=warning` rule statuses, prove skipped + rules outside an `enforce-touched` selection preserve top-level `passed/0`, + and bind both adjacent semantic oracles to `REQ-PROOFKIT-RETIRE-008`. +22. retain an attempt-scoped browser report and test-results directory after a + failed proof command, upload only those diagnostic paths under exact + `failure()` semantics, and keep the passed proof artifact on its existing + success-only path; bind both lifecycle and workflow selectors only to the + `REQ-PROOFKIT-QUALITY-022` browser artifact-confinement and + failure-diagnostics-retention witnesses. +23. initialize the exact pinned axe distribution through the browser-context + script channel behind a removable anti-corruption module and require its + reachable builder entrypoint to evaluate only a deterministically tested + constant loader; configure retained traces to preserve actions, DOM + snapshots, network, and sources while disabling continuous screenshots; + request only one bounded best-effort screenshot after a failure, preserve + the default and target-size axe oracles plus the undersized-control negative + fixture, and reject either isolated control, retries, or a timeout increase + as unsupported alternatives; run repeated clean first-attempt browser + proofs before final exact-object closeout. +24. keep output temporary-file creation and cleanup on the pinned admitted + parent, but publish only through the repository root with full source and + destination routes; interleave an exact `before_publish` parent move and + prove that outside-root and in-root replacement sentinels, destination + bytes, and displaced temporary-file residue all remain unchanged or absent. +25. supersede C-28's substituted-source route by staging the temporary object + at the repository root, retaining its identity, and re-admitting that + identity plus the destination-parent route after the exact barrier; keep + publication and cleanup repository-root-confined, refresh dependent native + source projections, reject a deterministic temporary-route substitution + before final admission, and explicitly exclude adversarial same-user + namespace mutation after final admission because the cross-platform + standard-library surface cannot atomically prove both current-root ancestry + and pinned-parent identity. +26. supersede root-level C-29 staging by returning temporary creation, cleanup, + and publication to the pinned destination parent; after the exact barrier + re-admit the non-symlink temporary entry identity, exact mode, and content + digest, then re-admit the current parent route at the irreversible rename + boundary; falsify object replacement, permission drift, each special mode + bit, in-place rewrite, symlink aliasing, and parent replacement + independently; and retain same-user + concurrent content/namespace mutation as an explicit non-claim. +27. add the compatible repository-confined same-parent atomic output guarantee + to the machine release record and prove its exact summary reaches rendered + release notes. +28. construct a canonical closeout record from the exact final tree, diff + counts, admitted local gate facts, residual non-claims, and retrospective; + bind it by digest and unique sentinels in the reviewed body snapshot and + independently in the final server body. +29. copy every local evidence artifact used by the closeout projection once + into a private snapshot, validate and project only those exact bytes, and + recheck final `HEAD` plus tracked-tree cleanliness after record + construction; compare complete output-file mode to exactly `0644` and + independently falsify setuid, setgid, and sticky mutations. +30. remove every local artifact field not admitted by an exact closeout + predicate; retain only final-SHA provenance, enumerated state/count facts, + and explicitly checked command, tree-state, status, and exit values. +31. slurp every local snapshot during validation, require exactly one JSON + document, apply its full predicate to that object, and admit the exact + Chromium, Firefox, and WebKit project inventory before projection. +32. replace the workflow literal-disabled deny-list with closed required-CI and + release job inventories plus presence-aware exact absent-or-owner + conditions for every job and step; preserve only named conditional + exceptions and falsify dynamic false and explicit-null conditions on + required CI test steps and release candidate routes. +33. bind every required CI job to its exact provider-check name and runner, + require the macOS job's exact fail-closed platform-smoke command, reject + no-op and reusable-job substitutions, require positive owner admission + before each bound mutation table, and route the positive CI inventory + selector through both owning quality requirements. +34. replace normalized runner lists with exact scalar comparisons, close the + complete ordered macOS step inventory and exact package-script owner, + bind one positive selector to both real CI and release package-gate + workflows, falsify semantic shadowing and mixed-type runners, and execute + the restored singleton coverage filter before closeout. +35. admit the exact repository-local setup-action bytes, falsify a nested + action semantic shadow, make `run`, `uses`, and `with` key presence part of + exact step comparison, and reject whitespace or explicit-empty dual + execution syntax. +36. require the exact complete selector sets for the QUALITY-011 aggregate and + QUALITY-013 package-gate anti-vacuity scenarios, falsify missing and surplus + selectors, parse the marker-bounded README argv with a boundary-local + expansion-free literal shell-word grammar, preserve safe single quotes, + double quotes, escapes, and adjacent literal segments, and reject + operators, expansion, globbing, multiline input, and malformed quoting. +37. key every protected selector inventory by its exact requirement/scenario + pair and falsify an owner-only transfer of each newly protected critical + scenario. +38. compare required selector sets before the generic empty-selector path, + falsify complete deletion for both critical scenarios, reject NUL in every + lexer state, and require no unresolved confirmed finding from every final + reviewer. +39. trim only Bash space/tab command delimiters, preserve vertical-tab and + non-breaking-space argv bytes plus exact JSON fence bytes, reject escaped + NUL and unescaped double-quoted history expansion, and preserve the literal + Bash semantics of double-quoted `\\!`. +40. trim only leading Bash space/tab delimiters, leave trailing delimiters to + the lexer, and preserve trailing escaped space and tab in the final argv. +41. route the generic missing-selector-function mutant through an unprotected + binding so it remains independent from exact-set admission. +42. consume complete backslash runs before history markers and preserve exact + Bash-equivalent argv for quoted and unquoted run lengths one through four. +43. remove every severity cutoff from candidate-preparation evidence + disposition. +44. complete the pure exact-inventory phase before generic AST and active-file + validation so negative inventory mutants fail without repeated I/O. +45. consume every backslash run once for linear command parsing and retain a + 128-KiB non-history regression. +46. compose pure inventory and generic executability validation in production + while routing partial fixtures only through their owning phase. +47. bind generic missing-function and invalid-signature falsifiers to + QUALITY-010 and protect its complete four-selector inventory against empty, + missing, surplus, and owner-transfer mutants. +48. classify the shared workflow-oracle tests by owner, bind all seventeen + QUALITY-013 typed package-gate selectors, and protect the complete set + without absorbing tests owned by other requirements. +49. declare the existing readiness-closeout one-pass strict + character-reference policy as a breaking release change with exact + migration and rendered-note projection, admit zero or one Go-test + parameter name, bind the unnamed-parameter regression, expand the exact + QUALITY-010 executability inventory to five selectors, and protect the + three-selector QUALITY-024 release-record inventory. +50. split original Markdown structural segments before one-pass + character-reference decoding and falsify an encoded pipe; add the stable + specifications no-match substate to every browser oracle; disclose the + removed synthetic Arrow-key contract with migration guidance and the + compatible pilot-all envelope plus optional witness-selector I/O in the + machine release record and exact rendered-note witness; refresh the + readiness native-source, generated CLI-contract, and public ABI golden + projections. +51. project the copyable offline npm exec family-discovery route in root help, + parse and execute that displayed command in the exact-tarball consumer, + reject a bare-route mutant, and project the compatible correction through + release notes plus the app native-source, generated CLI-contract, and + public ABI golden projections. +52. remove only authored leading ASCII space/tab indentation before parsing + the displayed npm route, preserve its trailing bytes, and reject both + leading and trailing NBSP mutants. +53. display, parse, and execute every installed onboarding transition from + family discovery through leaf help, every contract-owned preset, the + displayed installed README path, and the README first-valid-input command; + reject bare or missing intermediate routes. +54. protect the exact onboarding release addition, classify removal of + installed governance files as breaking with migration guidance, and cover + TypeScript manifest ancestors plus sources in the absolute-symlink + migration. +55. admit exact ordered step inventories for CI source quality, CI browser + runtime, and the release candidate, including execution-key presence and + values; reject inserted npm-script shadow steps. +56. record every tracked path crossing the deterministic size threshold, + separate browser/runtime/source/scanner/support workflow responsibilities, + retain the logically inseparable QUALITY-011/013 cluster, update moved + binding paths, and protect PACKAGE-005, QUALITY-022, and QUALITY-025 + selector inventories against empty, missing, surplus, owner-transfer, and + stale-path mutants. +57. add `id` and `timeout-minutes` values and presence to each exact step + inventory, and reject both fields in source-quality, browser-runtime, and + release-candidate jobs. +58. require every protected selector inventory to retain its exact + `witnessPath`, and reject relocation independently. +59. freeze the exact five-file untracked inventory before staging, stage the + reviewed tracked-plus-untracked path union, and require no unstaged or + untracked remainder. +60. recompute the final threshold ledger and correct every stale LOC or byte + measurement, including `internal/app/app_test.go`. +61. bind the extracted CodeQL, OSV, and Scorecard scanner selectors to + QUALITY-005, QUALITY-006, and QUALITY-007 with exact selector and path + inventories. +62. traverse every family and leaf route from the exact installed tarball, + execute each displayed route, and require every installed invocation to be + the exact npm prefix plus its bare usage before following preset and README + continuations. +63. retain candidate path inventories in memory and feed the closed untracked + inventory to `git add` through stdin, preserving exact staged equality + without temporary-file cleanup. +64. require exact workflow, advisory, and provider permission maps for CodeQL, + OSV, advisory Scorecard, and public Scorecard, including explicit + inheritance; reject a missing floor, advisory write, and surplus provider + write independently. +65. collect unique Usage and Installed invocation lines before comparison, + require their authored order and exact command-token boundary, and reject + installed-before-Usage plus prefix-collision mutants. +66. bind the C-69 falsifier to QUALITY-019, protect the exact six-selector set + and witness path, and align the owner requirement with every displayed + family and leaf transition. +67. require the final browser closeout snapshot to contain exactly 31 executed + and passed tests for each of Chromium, Firefox, and WebKit, matching the + current 93-test committed gate. +68. declare `cli_flag_conjunction_v1` only for a root-shape definition whose + conditions close one identical sorted allowed-flag dimension set; reject + malformed, non-canonical, missing, surplus, type-mixed, or overlapping + assignments; enumerate the native-owner mode and pilot domains over all 80 + combinations and require exactly twelve declared valid states; derive each + ABI condition from parsed argv; reject repeated `--mode` and `--pilot`; + update the public ABI digest, requirements, bindings, release record, + migration, and rendered notes. +69. extend the decomposition-owner inventory from the five extracted workflow + owners to those files plus + `internal/tools/commandcontractgen/condition_model.go`; retain exact staged + path equality and empty unstaged and untracked remainders. +70. reject an explicitly empty `--pilot` value before option normalization, + require its exact CLI diagnostic without any root-condition projection, + and include the breaking rejection in the owning requirements, public ABI, + release record, migration, and rendered notes. +71. distinguish the current correction worktree from the baseline-relative + candidate: admit exactly one current untracked condition-model owner, stage + the exact correction path union, and independently prove that the resulting + baseline-relative additions contain all six decomposition-owner files. +72. derive the test's mode and pilot domains from immutable copies of the same + internal native lists that construct option-admission maps, while retaining + exact 80-combination and twelve-valid-state assertions. +73. restrict the condition-model opt-in to the current adoption output + definition, reject an unowned second definition, and require any future + extension to arrive with its own native-domain and argv-closure proof. +74. close the complete eighteen-file baseline-relative addition inventory after + staging and prove that the six decomposition owners are a subset rather + than misreporting that subset as the whole candidate delta. +75. bind the reachable guidance mode/scope JSON failure argv to its canonical + guidance condition and `06-guidance-report` root variant before inspecting + the body. +76. require the condition model to be referenced only by the exact adoption + command, output direction, and adoption output definition; reject aliases + of each component independently. +77. remove volatile exact line counts from decomposition rationale and retain + snapshot measurements only in the deterministic threshold ledger. +78. require every JSON assertion case to carry both exact route coordinates, + reject coordinates on non-JSON cases, and run condition and variant + assertions unconditionally for every JSON-emitting argv. +79. require actual non-empty JSON stdout exactly when a case declares a JSON + assertion, parse every observed stdout, and preserve the exact sorted + fourteen-case JSON inventory against whole-case deletion. +80. replace the condition-model file's duplicate generic sorted-key helper with + the existing same-package `sortedKeys` owner and remove the unused import. +81. replace the Mach-O compatibility scenario's README-only selector with the + exact negative, boundary-positive, truncated-parser, and legacy-parser + tests; add the requirement/scenario pair and witness path to the coverage + owner's exact critical inventory, and include it in the existing empty, + missing, surplus, owner-transfer, and relocation mutation table. +82. require exact selector/path inventories for release-platform Python-wheel + parity and browser one-shot cleanup; add one all-target wheel + metadata/filename projection test, retain README and verifier witnesses, + replace the cleanup route's fixed-launcher selector with the exact three + cleanup/concurrency tests, and keep the launcher in its separate existing + package-boundary scenario. +83. replace the mutable-release-fact scenario's reference-closure selector with + the existing ten-class stale-package-doc falsifier, retain reference + closure in PACKAGE-001, and add the PACKAGE-007 requirement/scenario/path + tuple to the five-class exact inventory mutation oracle. +84. require each CodeQL, OSV, and Scorecard workflow job inventory to equal its + expected advisory/provider union before permission validation, and reject + an otherwise valid unclassified job carrying `contents: write`. +85. add one immutable `cliexec` renderer with exact `npm_offline`, + `python_module`, and `path` profiles; make npm and Python wrappers overwrite + and export `AGENTIC_PROOFKIT_LAUNCHER_PROFILE` and + `AGENTIC_PROOFKIT_PYTHON_EXECUTABLE`; admit exactly absent-or-`path` with no + executable, `npm_offline` with no executable, or `python_module` with one + absolute executable containing no report-visible secret-like or Unicode + control content and reject every other combination without disclosing the + value; admit once at the Go process boundary without ambient autodetection; + thread it explicitly through help, stack-preset, + gradual-adoption-bootstrap including the adoption aggregate route, + project-structure, agent-route, adoption-workflow, and + requirement-coverage producers; test the exact display and structured-argv + paths listed by D-09, including caller-owned array prefixes and + native-witness argv, renderer-owned suffixes, envelope command refs, + decoded materialization payloads, and project workflow source-report + identity; in + the exact tarball consumer require every emitted preset command to carry + the npm-offline prefix and execute one exact self-continuation; in + `internal/tools/pythonpackage/continuation_test.go` build and install the + current native wheel in a temporary venv, require the absolute + venv-interpreter module prefix, execute one exact self-continuation, + traverse root/family/leaf help, and directly execute exact emitted + agent-route argv with npm absent from `PATH`; reject malformed profiles, + secret/control paths, wrong-profile, bare, missing, surplus, + field-relocation, caller-rewrite, Unicode-whitespace, and shell-expansion + mutants; bind the + exact D-09 requirement/scenario/path/selector/command rows and protect them + in the coverage critical inventory; update + `REQ-PROOFKIT-PACKAGE-002`, `REQ-PROOFKIT-PACKAGE-003`, + `REQ-PROOFKIT-PACKAGE-006`, `REQ-PROOFKIT-QUALITY-019`, their exact + bindings, and the breaking change plus migration witnessed by + `REQ-PROOFKIT-QUALITY-024` / + `TestCurrentChangeRecordNamesReviewedSemanticChanges`. +86. close the root-distinct native-output inventory over exactly + `adoption-contract-envelope`, `pilot-admission`, and `self-check`; set their + selectors to `internal/app/cli_abi_test.go` / + `TestAdoptionContractEnvelopeCLIABI`, + `TestStandaloneMultiVariantCommandsUseExactRootShapes`, and the new focused + `TestSelfCheckOutputUsesExactRootShape` with each exact anchored + `go test ./internal/app -run` command; add `internal/app` to pilot output + `nativeSources`, retain exact adoption + `{internal/command/adoptioncontract}` and self-check `{internal/app}` source + sets, and recompute all affected canonical digests and projections; add an + exact `command + direction + native-source-path-set + path + test + + executable command + requirement + scenario` inventory tying CLI contract + selectors and native owners to requirement bindings; map all three + selectors to `proofkit.package-boundary.cli-output-root-witnesses` and + `proofkit.supply-chain-quality.cli-abi-golden`, plus adoption to + `proofkit.spec-proof-core.adoption-contract-envelope-cli-abi`; reject empty, + missing, surplus, selector-substitution, source-set-substitution, + nativeSource/nativeSources downgrade, path-relocation, direction-transfer, + scenario-transfer, command-drift, and owner-transfer mutants; update + `REQ-PROOFKIT-PACKAGE-002`, `REQ-PROOFKIT-QUALITY-004`, + `REQ-PROOFKIT-SPEC-011`, their exact binding scenarios/selectors, and both + generated CLI-contract projections; own the tuple-closure oracle through + `proofkit.supply-chain-quality.cli-output-witness-contract` at + `internal/app/cli_output_witness_contract_test.go`, selector + `TestRootDistinctOutputWitnessBindingsAreExact`, rather than enlarging the + general CLI topology corpus. C-90 does not change public output roots or + require a consumer migration. +87. harden `python_module` launcher admission so the executable is a non-empty + absolute path without report-visible secret-like, Unicode control, or + Unicode format content; reject with field-only errors that do not disclose + the value and exercise the shared redaction fixture corpus plus complete + bidi-control mutants. Update + `REQ-PROOFKIT-PACKAGE-002`, its overview projection, and D-09's exact + admission matrix. C-91 adds no credential model or new secret scanner. +88. extend the generated-route closure across the root family route, every + family and leaf help route, every descriptor installed invocation, help + forms, stack-preset copyable routes, agent-route next-command and envelope + argv, direct and aggregate adoption-workflow phase/envelope argv, + requirement-coverage rerun argv, and project workflow plan argv; require + display fields to equal `cliexec.DisplayArgv(argv)` where present. Build the + project workflow plan and its source-report stable hash with the same + renderer, while separately proving that caller-owned native-witness argv is + byte-preserved. Bind + `TestGeneratedCommandInvocationProfileRouteClosure` beside the string-field + inventory selector and update the coverage exact set. Expand the installed + Python wheel trace through root help, every family and leaf help route, + agent-route emission, and direct execution of exact emitted argv with npm + absent from `PATH`; admit exact four-space route indentation and canonical + command operands, reject Unicode-whitespace and shell-expansion mutants, + and never execute generated stdout through a shell. Update + `REQ-PROOFKIT-PACKAGE-002`, `REQ-PROOFKIT-PACKAGE-006`, their overview and + bindings, D-09, and the breaking summary plus migration under + `REQ-PROOFKIT-QUALITY-024`. C-92 adds no public JSON field, ambient + resolver, or general shell parser. +89. make the output-writer identity mutant cross-platform deterministic: + create one ordinary replacement file with the exact expected bytes and mode + before writer entry, prove it coexists with and has an identity distinct + from the writer-created temporary object, then at `before_publish` remove + the temporary entry and rename the live replacement into that pathname. + Require the exact identity diagnostic, unchanged destination bytes, and no + `.proofkit-output-*` residue; repeat the complete parent-swap test enough + times to falsify accidental inode-allocation dependence. Neutralizing both + identity checks must make this same-content, same-mode mutant fail red. + C-93 changes no production behavior or public contract. +90. replace only the graph table's two page-realm `evaluateAll` calls with one + shared web-first assertion helper. For each node and edge sequence require + exact row count and `data-identity` equality at every index. Preserve the + complete interaction flow, all later graph assertions, one worker, zero + retries, and the 30-second timeout; falsify missing, surplus, reordered, + duplicated, substituted, and absent identities. C-94 changes no browser + product behavior or evidence count. +91. before typed workflow decoding, traverse raw YAML nodes for the exact seven + tracked workflow owners. Require closed workflow, job, and step key sets; + reject duplicate and merge keys; admit job `environment` only for release + `publish` as exact `npm-production` and release `publish-pypi` as the exact + `{name,url}` owner mapping. Add owner-positive coverage plus root, producer, + aggregate, step, reusable-job, local-action-escape, environment, and unknown + mutants; bind the selector under `REQ-PROOFKIT-QUALITY-025`. C-96 changes no + workflow bytes or provider claim. +92. add `.css` to the existing source-hygiene text inventory. Derive one + synthetic filename for every unique extension returned by `git ls-files` + for the tracked requirement-browser asset owner, and run the existing + staged-blob and current-worktree mutants over that closed set plus Markdown + and Python. Keep the digest-substring test and token matcher unchanged. + C-97 adds no MIME, encoding, or binary-detection abstraction. +93. replace the two selection scenarios' repeated `getAttribute`, + `waitForFunction`, `page.evaluate`, query scan, clamping, and manual event + dispatch. Use Playwright `selectText` and click for ordinary selection and + collapse. For the exact emoji selection, read the locator text in the test + runner, compute strict UTF-16 and code-point bounds independently, then use + one locator-scoped operation and require the exact quote, nonzero start, a + two-code-unit DOM span, and one-code-point output span. Do not add retries, + browser branches, production hooks, timeout, workers, or new test + identities. C-98 changes no product behavior. +94. distinguish `audit_baseline_sha=3d86b6d0e4ec4a6c6a7f7a35ff2787011771aa64` + from + `integration_base_sha=0df4c28bac9737f476f7dc66030363b8b40d5417` + throughout P12.4-P12.6. Bind final parent, remote `main`, PR base, diff, + provider projection, and closeout integration identity to the latter; + retain the former only as named historical evidence. Admit the exact + observed first-publication feature lease + `1a681c47911680d101d36b48ce818ea1905a7148`, then bind every later + correction publication to its exact current remote head and fail before + mutation when any identity differs. C-99 changes no product behavior or + provider state. +95. require the final coverage snapshot to contain exactly 69 requirements, + 69 bound requirements, 173 scenarios, and 78 commands. Exercise stale 167 + and adjacent 172/174 counts against the singleton snapshot predicate. + C-100 changes only closeout evidence admission. +96. reject Unicode categories `Cc` and `Cf` at Python launcher admission, + retain the existing non-disclosing field-only diagnostic, and exhaustively + equate the helper classification plus admission/non-disclosure result with + every Unicode scalar in `Cc or Cf`, including non-bidi `Cf`. Update + `REQ-PROOFKIT-PACKAGE-002`, its overview projection, the breaking change + record, its migration, and the exact release-projection oracle. C-101 adds + no Unicode normalization or general path-policy layer. +97. update `REQ-PROOFKIT-PACKAGE-002`, `REQ-PROOFKIT-QUALITY-004`, their + overview projections, and the existing PACKAGE-002/QUALITY-004 + `TestStandaloneMultiVariantCommandsUseExactRootShapes` binding. Preserve + `--stack-diverse` as a compatible `pilot-admission` alias and add + `--contract-envelope --stack-diverse` to the exact input/output condition + inventories and public root-shape witness. Reject repeated `--pilot`, + repeated `--stack-diverse`, and both mixed orders with one stable + selector-ambiguity diagnostic before input admission. Record the formerly + accepted repeated-selector routes as breaking and add an exact migration + step. Regenerate both CLI contract projections and update exact condition + inventories plus the release-projection oracle. C-102 changes no valid + single-selector route result or output shape. +98. replace the manually selected current change-record assertions with exact + ordered equality for all breaking change IDs/summaries, addition + IDs/summaries, and migration steps. Require byte-for-byte equality with one + independently authored complete current release-note projection, including + every section and boundary. Delete each current entry; substitute every + change ID, summary, and migration step; swap every adjacent pair; add one + valid surplus to each machine inventory; reorder or relocate a note bullet; + and add an in-block, appended, duplicate, or second-section note. Require the + existing + `TestCurrentChangeRecordNamesReviewedSemanticChanges` selector to reject + every mutant. Update `REQ-PROOFKIT-QUALITY-024` and its overview while + retaining the existing binding and selector. C-103 adds no source-diff + inference, public record field, or runtime behavior. +99. replace the publication refspec's unbraced variable-colon form with + `"${final_sha}:refs/heads/fix/audit-remediation"`. Require a complete scan + of this tracked plan to contain no unbraced shell variable immediately + followed by a colon before publication, then execute the corrected refspec + under the existing exact remote lease. C-104 adds no product behavior or + permanent shell framework. +100. after the first successful correction publication, preserve + `1a681c47911680d101d36b48ce818ea1905a7148` only as its historical lease + and require the next publication lease to equal current remote head + `90090a5c712efa70b900fed0e115274cfa4773f0`. Abort before mutation on any + mismatch and require the remote to equal the new literal final SHA after + publication. C-105 adds no product or provider-state claim. +101. after every later owner-file addition and document correction, recompute + the complete baseline-relative added-path inventory. Require the final set + to contain exactly 21 paths, including + `internal/app/cli_output_witness_contract_test.go`, + `internal/app/invocation_profile_test.go`, and + `internal/tools/pythonpackage/continuation_test.go`. At the C-106 freeze, + require exactly the two reviewed design/plan paths and an empty untracked + set before staging; retain the six-file decomposition-owner subset. + C-106 adds no source or runtime behavior. +102. replace the Scorecard public-publish input predicate with exact equality + over the three required output inputs and their values. Add a `repo_token` + surplus mutant that must fail through + `TestScorecardPublicPublishDeclaresRequiredOutputInputs`. C-107 changes no + workflow byte or public behavior. +103. recompute the base-to-candidate `requirementId -> invariant` delta and + require the P10 list to contain all 30 changed IDs with no surplus, + including `SPEC-011` and `QUALITY-005`, `QUALITY-006`, and `QUALITY-007`. + C-108 changes only temporary parity evidence. +104. remove every numeric line-count qualifier from the four + reverse-decomposition bullets instead of refreshing the stale values. + C-109 preserves every owner and split/merge disposition. +105. require `publish_results` to decode as the exact YAML boolean `true`, + remove the generic truth-expression helper from this predicate, and add a + `true }}` string-substitution mutant beside the C-107 surplus mutant. + C-110 changes no workflow byte. +106. time-index every historical 21-path and two-file staging statement to the + C-106 freeze and the later three-file correction statement to the C-114 + freeze; retain P12.2 as the sole current correction-path owner. C-111 + changes no staged path by itself. +107. require the selected named public-publish step to be the sole + `ossf/scorecard-action` step. Add a differently named second action with a + surplus `repo_token` input and require the same boundary predicate to + reject it. C-112 adds no full step inventory or workflow change. +108. split each action reference at one `@`, compare only owner/repository to + `ossf/scorecard-action` with ASCII case-insensitive equality, preserve the + ref bytes, and reject mixed-case second-action, distinct-repository, + subpath, empty-ref, and repeated-`@` mutants. C-113 adds no general action + parser or network dependency. +109. reject every non-ASCII repository byte before case-insensitive + comparison, retain ASCII mixed-case admission, and reject a Unicode + simple-fold long-s mutant. C-114 adds no normalization or provider lookup. +110. replace the `@axe-core/playwright` two-audit topology with one direct + pinned `axe-core` audit per state. Test first: + - freeze exact run options to the sole + `rules.target-size.enabled = true` override with no `runOnly`; + - register exact `{content: axeDistributionSource}` exactly once per test + context and reject zero, wrong, or surplus registrations; + - use a dedicated Playwright fixture whose fresh `Page`/`BrowserContext` + owns one test state, initializes before the body, and admits teardown + only after exactly one audit; reject sequential or concurrent duplicate + registration/audit attempts before their second page-realm operation, + prove failed-registration rollback, and make multi-state reuse of one + page lifetime a non-claim; + - require the exact pre-run version before any `configure` or `run`, exact + wrapper-equivalent same-origin and `playwright` branding configuration, + and returned `testEngine` pair `("axe-core", "4.12.1")`; + - require `page.frames()` to equal exactly `[page.mainFrame()]`; + - reject zero or child frames before evaluation and require zero evaluation + calls for both frame mutants; reject absent/wrong versions or callables, + missing/wrong/surplus configure and run options, a two-rule `runOnly`, + a default-rule override, wrong/missing result identity, more than one + evaluation, and any temporary-page access; + - in one runtime negative fixture use exactly one undersized named control + attributed to `target-size` and one independently normal-sized unnamed + control attributed to default `button-name`; require both violations, + then preserve the production zero-violation, target applicability, and + target incomplete predicates; + - remove `@axe-core/playwright` from package and lock inventories and from + the package verifier's exact source-only toolchain owner and fixture; + add a dedicated exact dependency-inventory test whose missing, wrong, + and surplus subcases otherwise satisfy every earlier manifest predicate + and therefore reach the `devDependencies` map-equality rejection; use + retained `@axe-core/playwright` as the surplus mutant; + - update only QUALITY-022's exact requirement sentence and overview + projection; retain existing binding and witness identities; + - preserve one worker, zero retries, 30 seconds, and the diagnostic policy. + Regenerate the exact dependency lock and run the narrow owners first: + + ```bash + npm uninstall --save-dev @axe-core/playwright + node --test \ + --test-name-pattern='^browser accessibility harness closes direct audit topology$' \ + scripts/browser-proof-inputs.test.mjs + go test ./internal/tools/packageverify \ + -run '^TestVerifyRootManifestBoundaryRejectsDevDependencyDrift$' + npm run browser:static-check + ``` + + The C-115 epoch was executed and falsified at iteration 15 as recorded in + review history. Do not rerun or relabel its first 14 passes as success. + Apply step 111 before running the replacement C-116 epoch below. +111. replace the three avoidable graph pass-path operations with one bounded + response-derived, web-first witness in the existing + `workspace renders admitted views and creates a keyboard-authorized + handoff` test. Test first and keep the total test identity count at 25: + - retain the exact failed C-115 trace/report identities above and make no + product or exact Firefox/Juggler cause claim; + - before Traceability activation, install one exact same-origin POST route + and request/response observers for `/api/v1/graph`; fetch the real + upstream response, require a non-empty node projection and a deterministic + sentinel absent from the upstream bytes, replace only the first node + label, fulfill the app-issued request, and require exactly one request, + interception, linked successful response, and rendered sentinel; + - implement that route and both observers as one attempt-scoped state + machine `armed -> intercepted -> admitted -> detached`. + `route.fetch({timeout: 5000, maxRedirects: 0, maxRetries: 0})` must retain + the original same-origin URL and return success. Fetch, parse, or fulfill + failure must terminally abort an unfinished route. A `finally` path must + await every started callback, remove the exact route and both listeners + on success, assertion failure, and timeout, and fail the witness if + detachment is incomplete. Close the proof window only after the sentinel, + exact counters, linked response, and successful detachment are admitted; + - deep-copy and recursively freeze the admitted response projection. + Derive expected node/edge identities, exact node/edge table rows, full + and Unicode-code-point-truncated labels, viewBox, node positions, and + non-degenerate edge endpoints only from that frozen response and the + independently restated layout formula; + - add one immutable exact assertion plan and local closed-record/list + helpers in `tests/browser/workspace.spec.mjs`. Execute every + `(surface, element, assertion, expected)` entry through one branch-free + injectable executor. The production sink performs and awaits the + Playwright matcher; a recording sink must receive and complete the exact + ordered call inventory. Missing, surplus, skipped, duplicated, or + unresolved-call mutants must fail. The same assertion plan must drive + synchronous one-dimension mutation tables; do not add a second helper + file or duplicate the contract in a source-string scanner; + - equate the response with exact graph datasets, root child order, line and + group identities, `title, rect, text` order, childlessness, attributes, + direct text, computed rectangle geometry, tables, captions, headers, + rows, and cells. Require zero document elements from the exact + declarative SVG animation set `animate`, `animateColor`, `animateMotion`, + `animateTransform`, `discard`, and `set`; + - use retryable locator assertions for the exact owned ancestor chain, + viewport, SVG, nodes, edges, and both table projections. Close the + design's exact display, visibility, opacity, transform family, overflow, + zoom, content visibility, animation, transition, filter, clip, mask, + paint, geometry, font, text-layout, text-security, row/cell visibility, + count, and ordering inventories. Do not use `boundingBox()`, raw + pass-path computed-style evaluation, element screenshots, or + `toBeVisible()` on SVG lines; + - make every named C-116 falsifier executable through the shared closed + inventories. Pure topology/style mutation tables must change one owned + dimension while retaining all unrelated dimensions; cross-engine facts + already established during design adjudication need not add repeated + page-realm mutation operations. The runtime sentinel remains the causal + cache falsifier; + - require independent mutants for response omission/cache, graph/table + order/count/text, hidden or alpha-zero surfaces, every retained style + key, geometry/transform/text layout, inert leaf/root topology, positive + transition duration, zero-duration positive delay, phase-split local and + ancestor CSS animation, external graph and ancestor SMIL source + admission, zero-dash and degenerate edges, static caption/header + substitution, and visible trust-cell glyph loss; + - preserve exact console-error absence, all existing trust-state content, + handoff behavior, one worker, zero retries, 30-second tests, + failure-only screenshot policy, and screenshot-free traces. Do not + change production CSS, workspace rendering, requirement text, binding, + route, public API, packet, or business logic for C-116; + - run the narrow static owner first: + + ```bash + npm run browser:static-check + ``` + + Then freeze every browser-runtime input and let iteration 1 of the + complete executable C-116 epoch below be the first replacement-path + Firefox runtime. Do not add a public single-project selector merely for + this correction. Any timeout or test mismatch blocks closeout and + requires trace adjudication. Playwright 1.61.1-versus-1.62 or + browser-lifetime A/B remains nonblocking after a 30-of-30 pass and + becomes blocking only after another replacement-path timeout or an exact + engine/lifetime cause claim. +111a. because the first provider execution after the 1.61.1 epoch stalled in a + different raw Playwright page operation, execute the isolated 1.62 A/B: + - change only `package.json`, `package-lock.json`, + `internal/tools/packageverify/main.go`, and + `internal/tools/packageverify/main_test.go`; + - require all four owners to admit exact version `1.62.0`; + - preserve every browser test, one worker, zero retries, the 30-second + timeout, shipped CLI JSON/exit-code behavior, product runtime, and + business logic; admit the nonbreaking source-only public package-metadata + pin and the lock-resolved Node `>=20` development-toolchain engine floor + without projecting that floor onto consuming runtime requirements; + - reject rerun-only evidence, retry or timeout inflation, test deletion, + and simultaneous browser-lifetime or raw-operation changes because each + either weakens the falsifier or mixes causal variables; + - run the package-verifier suite, then treat the version change as a new + resolved input digest and execute the complete 30-process epoch below + from iteration 1, followed in the same frozen-input sequence by both full + browser proofs, the composite gate, and the full gate; + - after committed-object review, require fresh provider attempt 1. A green + attempt admits the candidate without proving version-only causation. Any + local verdict, identity, package-admission, security, or platform + regression rolls back all four owner changes together. Another isolated + provider stall rejects a version-only conclusion and routes to a + separately reviewed browser-lifetime or remaining raw-operation + experiment with a new digest and complete epoch; + - create no new file, adapter, retry layer, configuration surface, or + version registry: the four existing owners are the minimum synchronized + boundary and there is no second consumer for another abstraction. +111b. correct C-117 in `internal/app/cli_abi_test.go` only: + - before creating the setgid temp object, normalize the test root group to + the caller's effective group so Darwin can materialize the special bit; + - after every mode `Chmod`, require immediate exact `Lstat` mode equality; + - retain separate permission, setuid, setgid, and sticky-bit mutants and + leave the production writer unchanged; + - prove the setgid case 100 of 100 times with the default temp root and 100 + of 100 times with the retained foreign-group `TMPDIR`, then run the full + gate; + - verify that the resolved browser input digest remains the Playwright 1.62 + epoch digest. Any digest change restarts that epoch from iteration 1; + - reject skip, deletion, rerun-only acceptance, a weaker expected error, or + a production change because none proves that the intended mutant exists; + - add no platform adapter, test file, or production hook without a second + consumer. If a supported Darwin or Linux filesystem cannot materialize + the postcondition after group normalization, stop for a separately + reviewed platform-specific fixture. +111c. correct C-118 in the existing workspace asset and browser witness: + - first add a delayed-handoff counterexample that submits from + Specifications, opens Diff, then releases the handoff response; + - require the packet region to publish the valid result while the global + state and active-view control remain Diff; + - capture the current view request identity at submission and condition + only the handoff success/failure global state write on identity equality; + - do not abort, retry, discard, or resubmit the handoff and do not change + server one-shot semantics. +111d. correct C-119 in the existing browser witness: + - retain both provider-attempt report and trace identity sets and classify + the four Firefox failures only from their observed evidence; + - route all then-current workspace opens and reloads + through owner-local helpers using `waitUntil: "commit"`; + - require a non-null successful main-resource response and the visible + server-owned workspace heading through Playwright's exact accessible-name + matcher for every helper call, and retain every downstream semantic + assertion; + - leave the `about:blank` axe negative-control navigation unchanged; + - add no lifecycle-event wait, retry, timeout increase, production change, + `networkidle` heuristic, or swallowed navigation failure. +111e. correct C-120 in the same browser witness: + - intercept the main document and preserve its original body while + returning status 503 for open and reload in independent cases; + - require each helper to reject with its owner-specific terminal error; + - preserve successful navigation while changing only the heading to + `browser.fixture.workspace drift`, so a substring matcher survives but + the exact matcher rejects; + - admit the original status 200, byte-changing substitution, and completed + route fulfillment independently so a fixture or route-handler failure + cannot satisfy the expected rejection; + - execute all three falsifiers in Chromium, Firefox, and WebKit and add no + production hook, retry, timeout increase, or new file. +111f. correct C-121 in the same browser witness and its existing static owner: + - retain exact provider run `30337477288`, browser job `90205431977`, + artifact `8679825289`, report and trace digests, and synthetic merge + identity as the counterexample to `waitUntil: "commit"`; + - admit the raw configured base URL as exact local HTTP root authority, + capture the current main frame, and arm an exact-URL main-frame + navigation-response waiter before scheduling either location assignment + or reload; + - require one exact trigger token, a successful response, and the exact + visible server-owned heading; abort and consume the pending waiter after + any trigger or admission failure; + - add a pure classifier truth table for main-frame navigation, same-URL + main-frame fetch, same-URL child-frame navigation, and foreign URL; + - rehearse independent deletion of the exact-URL, navigation, main-frame, + response-status, and exact-heading clauses and require the corresponding + Chromium falsifier to fail; + - deliver a same-URL 503 fetch before a classifier-only reload and prove + that only the navigation response is admitted; + - close the source corpus to 27 open-helper calls, two reload-helper calls, + one classifier-only reload trigger, and no direct workspace lifecycle + wait; preserve only the isolated `about:blank` axe control; + - execute all 31 tests in Chromium, Firefox, and WebKit; retain zero + retries and the existing timeout; add no production change, readiness + hook, dependency churn, or new file. +111g. correct C-122 in P12.2: + - make its declarative current correction inventory and executable expected + inventory name the same three exact post-`c2315fd` paths; + - retain the independent empty-untracked, six-owner-subset, 21-addition, + staged-equality, and empty-remainder predicates; + - reject a historical qualifier because both inventories own the same + current staging operation. +111h. correct C-123 in the existing navigation-classifier test: + - admit the configured base URL positively before navigation; + - reject non-string input, HTTPS, hostname drift, missing port, username, + password, non-root path, query, and fragment as independent rows; + - keep username and password counterexamples distinct; + - rehearse deletion of at least one high-boundary local-origin clause and + require its row to fail; + - retain 31 tests per engine by extending the cohesive C-121 test rather + than adding a second browser lifecycle. +111i. correct C-124 in the same test: + - replace assertion-library token matching with one exact terminal owner + error so its failure identity is stable; + - inject a pending response waiter whose abort signal rejects it and whose + fallback resolves to a distinct unsuccessful response; + - return a wrong trigger token and require the token error, observed abort, + and observed waiter-rejection consumption; + - require exact `waiter-armed`, `trigger-called`, `waiter-aborted`, + `waiter-consumed` order so pre-arm sequencing is executable; + - independently delete token admission, signal abort, and explicit + consumption in Chromium and require each owning assertion to fail; + - retain the real-page decoy path, one runtime test identity, zero retries, + and no production or general mocking surface. +112. after C-93, C-94, C-96 through C-124, and all document edits freeze, + recompute the + complete threshold ledger and correct every stale path, LOC, or byte count + before committed-object review. C-95 adds no decomposition requirement. + +### C-116 executable falsifier epoch + +The Firefox stress evidence is diagnostic bounded-reliability evidence, not a +new merge-proof artifact. It uses one immutable materialized browser-input +snapshot, one bounded server, 30 separate Playwright processes, and 30 unique +attempt directories. Every iteration must prove exactly the same 25 test +identities with 25 expected passes, zero skipped, unexpected, or flaky tests, +one worker, zero retries, and 30-second tests. The first failure stops the epoch +and retains its report, trace, screenshots, source snapshot, and server logs. +The historical identity authority is the exact sorted title projection from +the hash-verified C-115 iteration-15 report: +`sha256:f7b80cd6ea950cad6693a7b11020f746581d6eba4f2b7314700e4161448a554c`. +It is the SHA-256 of the no-newline compact JSON bytes produced by the same +`file::suite > title` jq projection below. Iteration 1 and every stress and +full Firefox projection must equal this digest; iteration 1 is not allowed to +redefine identity authority. +An outer watchdog gives every process a finite wall-clock deadline, owns its +detached process group, forwards wrapper interruption, and rejects a +successful leader that leaves descendants. It returns an ordinary child +result only after proving process-group absence. If bounded +initial forwarded or cleanup signal followed by `KILL` cannot prove absence, +it returns the recorded non-zero `kill-deadline-exceeded` blocker; deadline +and descendant cleanup use `TERM`, while wrapper interruption first forwards +its actual signal. Any signal or group-probe error is retained in the final +record and conservatively treated as continuing residual-process risk. A +blocking result admits no stress or full-gate evidence. + +Run from the repository root: + +```bash +set -euo pipefail +stress_root="$(mktemp -d "${TMPDIR:-/tmp}/proofkit-firefox-stress.XXXXXX")" +export PROOFKIT_FIREFOX_STRESS_ROOT="$stress_root" +PROOFKIT_BROWSER_INPUT_RESOLUTION="$( + go run ./internal/tools/browserproofverify --resolve-inputs +)" +export PROOFKIT_BROWSER_INPUT_RESOLUTION +frozen_input_digest="$( + node --input-type=module <<'NODE' +import {createHash} from "node:crypto"; +import {symlinkSync} from "node:fs"; +import {join, resolve} from "node:path"; +import { + loadBrowserProofInputResolution, + materializeInputSnapshot, +} from "./scripts/browser-proof-inputs.mjs"; + +const root = process.env.PROOFKIT_FIREFOX_STRESS_ROOT; +const resolution = loadBrowserProofInputResolution(); +const source = join(root, "source"); +const assets = materializeInputSnapshot(resolution.inputPaths, ".", source); +symlinkSync(resolve("node_modules"), join(source, "node_modules"), "dir"); +const inputResolution = { + serverTarget: resolution.serverTarget, + writerPath: resolution.writerPath, +}; +const value = createHash("sha256") + .update(JSON.stringify({assets, inputResolution})) + .digest("hex"); +process.stdout.write(`sha256:${value}`); +NODE +)" +test -n "$frozen_input_digest" +stress_source="$stress_root/source" +materialized_input_digest() { + node --input-type=module <<'NODE' +import {createHash} from "node:crypto"; +import {join} from "node:path"; +import { + loadBrowserProofInputResolution, + snapshotInputAssets, +} from "./scripts/browser-proof-inputs.mjs"; + +const root = process.env.PROOFKIT_FIREFOX_STRESS_ROOT; +const resolution = loadBrowserProofInputResolution(); +const assets = snapshotInputAssets( + resolution.inputPaths, + join(root, "source"), +); +const inputResolution = { + serverTarget: resolution.serverTarget, + writerPath: resolution.writerPath, +}; +const value = createHash("sha256") + .update(JSON.stringify({assets, inputResolution})) + .digest("hex"); +process.stdout.write(`sha256:${value}`); +NODE +} +test "$(materialized_input_digest)" = "$frozen_input_digest" +root_input_digest() { + node --input-type=module <<'NODE' +import {createHash} from "node:crypto"; +import { + loadBrowserProofInputResolution, + snapshotInputAssets, +} from "./scripts/browser-proof-inputs.mjs"; + +const resolution = loadBrowserProofInputResolution(); +const assets = snapshotInputAssets(resolution.inputPaths); +const inputResolution = { + serverTarget: resolution.serverTarget, + writerPath: resolution.writerPath, +}; +const value = createHash("sha256") + .update(JSON.stringify({assets, inputResolution})) + .digest("hex"); +process.stdout.write(`sha256:${value}`); +NODE +} +test "$(root_input_digest)" = "$frozen_input_digest" +run_with_deadline() { + deadline_seconds="$1" + watchdog_record="$2" + shift 2 + node --input-type=module - \ + "$deadline_seconds" "$watchdog_record" "$@" <<'NODE' +import {spawn} from "node:child_process"; +import {writeFileSync} from "node:fs"; + +const [deadlineText, recordPath, command, ...args] = process.argv.slice(2); +const deadlineSeconds = Number(deadlineText); +if ( + !Number.isSafeInteger(deadlineSeconds) || + deadlineSeconds <= 0 || + typeof command !== "string" || + command.length === 0 +) { + process.exit(125); +} +const startedAt = new Date().toISOString(); +let child; +let cleanupStatus; +let cleanupWrapperExitCode; +let deadlineTimer; +let groupPoll; +let finished = false; +let leaderExitCode = null; +let leaderSignal = null; +let pendingWrapperSignal; +const groupProbeErrors = []; +const signalErrors = []; +let termTimer; +let killTimer; +let dispatchWrapperSignal = (value) => { + pendingWrapperSignal ??= value; +}; +process.on("SIGINT", () => { + dispatchWrapperSignal({ + signal: "SIGINT", + status: "wrapper-sigint", + wrapperExitCode: 130, + }); +}); +process.on("SIGTERM", () => { + dispatchWrapperSignal({ + signal: "SIGTERM", + status: "wrapper-sigterm", + wrapperExitCode: 143, + }); +}); +child = spawn(command, args, {detached: true, stdio: "inherit"}); +const record = (status) => { + writeFileSync(recordPath, `${JSON.stringify({ + args, + command, + deadlineSeconds, + leaderExitCode, + leaderSignal, + groupProbeErrors, + signalErrors, + startedAt, + status, + })}\n`, {encoding: "utf8", mode: 0o600}); +}; +const groupExists = () => { + try { + process.kill(-child.pid, 0); + return true; + } catch (error) { + if (error?.code === "ESRCH") return false; + if (error?.code === "EPERM") return true; + groupProbeErrors.push({code: error?.code ?? "unknown"}); + return true; + } +}; +const signalGroup = (signal) => { + try { + process.kill(-child.pid, signal); + } catch (error) { + if (error?.code !== "ESRCH") { + signalErrors.push({code: error?.code ?? "unknown", signal}); + } + } +}; +const finish = (status, wrapperExitCode) => { + if (finished) return; + finished = true; + clearTimeout(deadlineTimer); + clearTimeout(termTimer); + clearTimeout(killTimer); + clearInterval(groupPoll); + record(status); + process.exit(wrapperExitCode); +}; +const beginCleanup = (status, wrapperExitCode, signal) => { + if (cleanupStatus !== undefined) return; + cleanupStatus = status; + cleanupWrapperExitCode = wrapperExitCode; + record(`${status}:${signal.toLowerCase()}-requested`); + signalGroup(signal); + groupPoll = setInterval(() => { + if (!groupExists()) finish(cleanupStatus, cleanupWrapperExitCode); + }, 100); + termTimer = setTimeout(() => { + if (!groupExists()) { + finish(cleanupStatus, cleanupWrapperExitCode); + return; + } + record(`${status}:kill-requested`); + signalGroup("SIGKILL"); + killTimer = setTimeout(() => { + if (groupExists()) { + finish("kill-deadline-exceeded", wrapperExitCode); + return; + } + finish(cleanupStatus, cleanupWrapperExitCode); + }, 5_000); + }, 5_000); +}; +child.once("error", (error) => { + record(`spawn-error:${error.code ?? "unknown"}`); + process.exit(125); +}); +child.once("exit", (code, signal) => { + leaderExitCode = code; + leaderSignal = signal; + if (cleanupStatus !== undefined) { + return; + } + if (groupExists()) { + beginCleanup("descendants-remain", 125, "SIGTERM"); + return; + } + finish("exited", Number.isInteger(code) ? code : 125); +}); +dispatchWrapperSignal = ({signal, status, wrapperExitCode}) => { + beginCleanup(status, wrapperExitCode, signal); +}; +if (pendingWrapperSignal !== undefined) { + dispatchWrapperSignal(pendingWrapperSignal); +} +deadlineTimer = setTimeout(() => { + beginCleanup("deadline-exceeded", 124, "SIGTERM"); +}, deadlineSeconds * 1_000); +NODE +} +stress_server="$stress_root/server" +( + cd "$stress_source" + run_with_deadline 300 "$stress_root/server-build-watchdog.json" \ + go build -o "$stress_server" ./internal/tools/browsertestserver +) +server_stdout="$stress_root/server.stdout" +server_stderr="$stress_root/server.stderr" +"$stress_server" >"$server_stdout" 2>"$server_stderr" & +server_pid=$! +stop_stress_server() { + stop_signal="" + if kill -0 "$server_pid" 2>/dev/null; then + kill -TERM "$server_pid" + stop_signal="TERM" + for _ in $(seq 1 100); do + if ! kill -0 "$server_pid" 2>/dev/null; then + break + fi + sleep 0.1 + done + if kill -0 "$server_pid" 2>/dev/null; then + kill -KILL "$server_pid" 2>/dev/null || true + stop_signal="KILL" + for _ in $(seq 1 50); do + if ! kill -0 "$server_pid" 2>/dev/null; then + break + fi + sleep 0.1 + done + fi + fi + if kill -0 "$server_pid" 2>/dev/null; then + printf 'stress server did not terminate after %s\\n' "$stop_signal" >&2 + return 1 + fi + set +e + wait "$server_pid" + server_status=$? + set -e + case "$server_status" in + 0|137|143) ;; + *) + printf 'stress server terminal status %s is not admitted\\n' \ + "$server_status" >&2 + return 1 + ;; + esac +} +trap stop_stress_server EXIT +for _ in $(seq 1 300); do + if grep -q '^Proofkit requirement browser: http://127\.0\.0\.1:[0-9][0-9]*/$' \ + "$server_stdout"; then + break + fi + if ! kill -0 "$server_pid" 2>/dev/null; then + sed -n '1,120p' "$server_stderr" >&2 + exit 1 + fi + sleep 0.1 +done +stress_url="$( + sed -n \ + 's/^Proofkit requirement browser: \(http:\/\/127\.0\.0\.1:[0-9][0-9]*\/\)$/\1/p' \ + "$server_stdout" | + head -n 1 +)" +test -n "$stress_url" +stress_records="$stress_root/records.jsonl" +historical_test_ids_digest="sha256:f7b80cd6ea950cad6693a7b11020f746581d6eba4f2b7314700e4161448a554c" +expected_test_ids="" +for iteration in $(seq 1 30); do + test "$(materialized_input_digest)" = "$frozen_input_digest" + iteration_id="$(printf '%02d' "$iteration")" + iteration_root="$stress_root/iteration-$iteration_id" + mkdir -p "$iteration_root" + report_path="$iteration_root/playwright-report.json" + output_path="$iteration_root/test-results" + set +e + ( + cd "$stress_source" + PROOFKIT_BROWSER_TEST_URL="$stress_url" \ + PROOFKIT_BROWSER_TEST_REPORT_PATH="$report_path" \ + PROOFKIT_BROWSER_TEST_OUTPUT_DIR="$output_path" \ + run_with_deadline 900 "$iteration_root/watchdog.json" \ + node node_modules/@playwright/test/cli.js test --project=firefox + ) + exit_status=$? + set -e + current_input_digest="$(materialized_input_digest)" + test "$current_input_digest" = "$frozen_input_digest" + report_status=1 + watchdog_status="$( + jq -r .status "$iteration_root/watchdog.json" 2>/dev/null || + printf 'invalid' + )" + test_ids="" + executed_count=0 + passed_count=0 + if test -f "$report_path"; then + executed_count="$( + jq '[.suites[].specs[].tests[]] | length' "$report_path" 2>/dev/null || + printf '0' + )" + passed_count="$( + jq '[ + .suites[].specs[].tests[].results[] | + select(.status == "passed") + ] | length' "$report_path" 2>/dev/null || + printf '0' + )" + if jq -e ' + .errors == [] and + .config.workers == 1 and + ([.config.projects[] | + select( + .name == "firefox" and + .retries == 0 and + .repeatEach == 1 and + .timeout == 30000 + )] | length) == 1 and + .stats.expected == 25 and + .stats.skipped == 0 and + .stats.unexpected == 0 and + .stats.flaky == 0 and + ([.suites[].specs[].tests[]] | length) == 25 and + all( + .suites[].specs[].tests[]; + .projectName == "firefox" and + .status == "expected" and + (.results | length) == 1 and + .results[0].status == "passed" and + .results[0].retry == 0 + ) + ' "$report_path" >/dev/null; then + report_status=0 + test_ids="$( + jq -c \ + '[.suites[] | + .file as $file | + .title as $suite | + .specs[] | + "tests/browser/\($file)::\($suite) > \(.title)" + ] | sort' \ + "$report_path" + )" + fi + fi + test_ids_digest="$( + printf '%s' "$test_ids" | + shasum -a 256 | + awk '{print "sha256:" $1}' + )" + jq -cn \ + --argjson iteration "$iteration" \ + --arg inputDigest "$current_input_digest" \ + --arg project firefox \ + --arg testIdsDigest "$test_ids_digest" \ + --arg watchdogStatus "$watchdog_status" \ + --argjson executed "$executed_count" \ + --argjson passed "$passed_count" \ + --argjson exitStatus "$exit_status" \ + --argjson reportStatus "$report_status" \ + '{ + iteration: $iteration, + project: $project, + executed: $executed, + passed: $passed, + inputDigest: $inputDigest, + testIdsDigest: $testIdsDigest, + watchdogStatus: $watchdogStatus, + exitStatus: $exitStatus, + reportStatus: $reportStatus + }' >>"$stress_records" + test "$exit_status" -eq 0 + test "$watchdog_status" = "exited" + test "$report_status" -eq 0 + test "$test_ids_digest" = "$historical_test_ids_digest" + if test "$iteration" -eq 1; then + expected_test_ids="$test_ids" + else + test "$test_ids" = "$expected_test_ids" + fi +done +test "$(materialized_input_digest)" = "$frozen_input_digest" +test "$( + jq -s ' + length == 30 and + ([.[].iteration] == [range(1; 31)]) and + ([.[].inputDigest] | unique | length) == 1 and + ([.[].testIdsDigest] | unique) == + ["sha256:f7b80cd6ea950cad6693a7b11020f746581d6eba4f2b7314700e4161448a554c"] and + all(.[]; + .project == "firefox" and + .executed == 25 and + .passed == 25 and + .exitStatus == 0 and + .reportStatus == 0 and + .watchdogStatus == "exited" + ) + ' "$stress_records" +)" = "true" +test "$(jq -r .inputDigest "$stress_records" | sort -u)" = \ + "$frozen_input_digest" +kill -0 "$server_pid" +stop_stress_server +trap - EXIT +# Do not edit a browser-proof input after stress materialization. Run two +# owner-valid complete proofs, then the composite and full gates in this shell. +test "$(root_input_digest)" = "$frozen_input_digest" +browser_proof_firefox_test_ids() { + jq -c '[ + .projects[] | + select(.name == "firefox") | + .testIds[] + ] | sort' artifacts/proofkit/browser-runtime-proof.json +} +assert_historical_firefox_test_ids() { + current_firefox_test_ids="$(browser_proof_firefox_test_ids)" + current_firefox_test_ids_digest="$( + printf '%s' "$current_firefox_test_ids" | + shasum -a 256 | + awk '{print "sha256:" $1}' + )" + test "$current_firefox_test_ids" = "$expected_test_ids" + test "$current_firefox_test_ids_digest" = "$historical_test_ids_digest" +} +run_with_deadline 1800 "$stress_root/first-full-watchdog.json" \ + npm run browser:test +first_full_digest="$(jq -r .inputDigest artifacts/proofkit/browser-runtime-proof.json)" +test "$first_full_digest" = "$frozen_input_digest" +assert_historical_firefox_test_ids +test "$(root_input_digest)" = "$frozen_input_digest" +run_with_deadline 1800 "$stress_root/second-full-watchdog.json" \ + npm run browser:test +second_full_digest="$(jq -r .inputDigest artifacts/proofkit/browser-runtime-proof.json)" +test "$second_full_digest" = "$frozen_input_digest" +test "$first_full_digest" = "$second_full_digest" +assert_historical_firefox_test_ids +test "$(root_input_digest)" = "$frozen_input_digest" +run_with_deadline 1800 "$stress_root/composite-watchdog.json" \ + npm run browser:check +composite_digest="$(jq -r .inputDigest artifacts/proofkit/browser-runtime-proof.json)" +test "$composite_digest" = "$frozen_input_digest" +assert_historical_firefox_test_ids +test "$(root_input_digest)" = "$frozen_input_digest" +run_with_deadline 3600 "$stress_root/full-check-watchdog.json" \ + npm run check +assert_historical_firefox_test_ids +test "$(root_input_digest)" = "$frozen_input_digest" +``` + +No product or proof correction is complete from an implementation change +alone. Its applicable negative test, durable requirement, binding selector, +and generated projection must agree before P11. Temporary closeout corrections +C-99, C-100, C-104 through C-106, C-108, C-109, and C-111 instead require +their exact executable identity/count predicates, named counterexamples, and +independent plan review; they do not invent durable product requirements, +bindings, or generated projections. + +### Overview projections + +Update each touched spec overview only where the requirement's summarized +claim changes. Overview prose must not become an alternate policy owner. + +### Bindings and witness plan + +1. Confirm every exact scenario/witness route from the design's durable proof + table was applied owner-first. +2. Point falsifier witnesses to test files, not production-only files, where a + test now owns the negative case. +3. Add new command-contract generator check and action-pin witness commands. +4. Preserve environment classes and network non-claims. +5. Recompute binding and witness canonical order. + +### Backlog + +- do not claim `COVERAGE-01` closed; +- keep signed-tag and live-release rows blocked; +- update only rows whose source-local obligation actually changed; +- do not add provider success. + +### Release records + +Record: + +- public process-channel correction; +- context/diff/browser schema v2 migration; +- absolute symlink hardening; +- package-doc contraction; +- SBOM semantic correction; +- help/onboarding additions; +- accessibility behavior change; +- exact previous version and breaking change class. + +### Gate + +Run the exact owner and projection tests, rebuild current artifact evidence, +and only then run receipt/coverage validation: + +```bash +npm run go:test +npm run command-contract:check +npm run command-family:check +npm run package:artifact +npm run self:receipt +npm run self:coverage +``` + +## P11: Review and full closeout + +### P11.1 Local structural review + +1. Run `gofmt` on changed Go files. +2. Run `git diff --check`. +3. Confirm no Cyrillic entered tracked files. +4. Confirm no generated artifacts, package tarballs, caches, or proof residue + are tracked except exactly these two owner-admitted projections generated + together and freshness-checked by the same + `npm run command-contract:check` invocation: + + - `internal/app/command_contract_generated.go`; + - `internal/command/stackpreset/preset_ids_generated.go`. +5. Inspect `git diff --stat` and exact changed paths. +6. Use semantic diff to identify changed functions/types and unexpected blast + radius. + +### P11.2 Preliminary multi-agent worktree review + +Use three read-only reviewers against the final worktree: + +- proof/security/confinement and false-green oracles; +- contract/release/package and proof-routing closure; +- UX/accessibility/onboarding and business-logic preservation. + +Each finding remains a hypothesis until root reproduction. Fix confirmed +findings and rerun their narrow gates. These worktree reviews reduce risk but +do not replace the committed-object review in P12. + +### P11.3 Full gate + +Run: + +```bash +git diff --check +npm run check +``` + +`npm run check` must include `command-contract:check`. + +Any skipped or unavailable gate is reported with its exact blocker and is not +success. + +### P11.4 Candidate preparation + +After full worktree green: + +1. require exact evidence for all remaining confirmed objections and findings; +2. rerun any narrow gate affected by closeout edits; +3. verify the temporary docs contain no unresolved `pending` review state; +4. set design and plan status to `implementation candidate`, not + `implemented/validated`; +5. freeze the candidate path inventory and expected diff before the identity + check and commit. + +### Residual non-claims + +Final closeout must state: + +- no npm/PyPI/GitHub live publication proof; +- no branch-protection or tag-ruleset proof; +- no retroactive correction of `0.1.160`; +- no vulnerability absence or action safety; +- no full `COVERAGE-01` closure; +- no complete fuzz-space or performance-regression proof; +- no full WCAG 2.2 conformance; +- no consumer adoption or production readiness. + +## P12: Commit, publish branch, and open pull request + +### P12.1 Identity and authority check + +Immediately before external mutation: + +```bash +gh api user --jq .login +gh api repos/research-engineering/agentic-proofkit --jq .permissions +git config user.name +git config user.email +``` + +Require: + +```text +GitHub login = iperev +permissions.push = true +git user.name = iperev +``` + +An identity mismatch stops publication. Do not switch to an account with only +pull permission. + +### P12.2 Candidate commit + +1. Freeze the reviewed correction-path inventory before staging. In the + current correction epoch, require it to be exactly: + + ```text + docs/implementation/audit-remediation-design.md + docs/implementation/audit-remediation-plan.md + tests/browser/workspace.spec.mjs + ``` + + Require the complete untracked inventory to be empty. + + Independently require this six-file decomposition-owner subset: + + ```text + internal/tools/commandcontractgen/condition_model.go + scripts/workflow_browser_runtime_oracle_test.go + scripts/workflow_oracle_support_test.go + scripts/workflow_runtime_preconditions_test.go + scripts/workflow_security_scanner_oracles_test.go + scripts/workflow_source_oracles_test.go + ``` + + Require the complete candidate index to add exactly these twenty-one files + relative to the reviewed baseline: + + ```text + docs/implementation/audit-remediation-design.md + docs/implementation/audit-remediation-plan.md + internal/app/cli_output_witness_contract_test.go + internal/app/command_contract_generated.go + internal/app/invocation_profile_test.go + internal/command/requirementbrowser/requirementbrowser_test.go + internal/command/requirementbrowser/v1_adapter.go + internal/command/requirementcontext/v1_adapter.go + internal/command/requirementdiff/v1_adapter.go + internal/command/stackpreset/preset_ids_generated.go + internal/tools/commandcontractgen/condition_model.go + internal/tools/commandcontractgen/main.go + internal/tools/commandcontractgen/main_test.go + internal/tools/pythonpackage/continuation_test.go + release/change-record.v2.json + scripts/workflow_browser_runtime_oracle_test.go + scripts/workflow_oracle_support_test.go + scripts/workflow_runtime_preconditions_test.go + scripts/workflow_security_scanner_oracles_test.go + scripts/workflow_source_oracles_test.go + tests/browser/axe-harness.mjs + ``` + + Then stage exactly the current three-file correction inventory, prove + staged-path equality, prove the six-file owner subset, and prove the + complete twenty-one-file baseline-relative addition set: + + ```bash + set -euo pipefail + baseline_sha="3d86b6d0e4ec4a6c6a7f7a35ff2787011771aa64" + correction_path_inventory="$( + { + git diff --name-only HEAD + git ls-files --others --exclude-standard + } | LC_ALL=C sort -u + )" + expected_correction_path_inventory="$( + printf '%s\n' \ + docs/implementation/audit-remediation-design.md \ + docs/implementation/audit-remediation-plan.md \ + tests/browser/workspace.spec.mjs | + LC_ALL=C sort + )" + expected_untracked_inventory="" + expected_decomposition_owner_inventory="$( + printf '%s\n' \ + internal/tools/commandcontractgen/condition_model.go \ + scripts/workflow_browser_runtime_oracle_test.go \ + scripts/workflow_oracle_support_test.go \ + scripts/workflow_runtime_preconditions_test.go \ + scripts/workflow_security_scanner_oracles_test.go \ + scripts/workflow_source_oracles_test.go | + LC_ALL=C sort + )" + expected_baseline_added_inventory="$( + printf '%s\n' \ + docs/implementation/audit-remediation-design.md \ + docs/implementation/audit-remediation-plan.md \ + internal/app/cli_output_witness_contract_test.go \ + internal/app/command_contract_generated.go \ + internal/app/invocation_profile_test.go \ + internal/command/requirementbrowser/requirementbrowser_test.go \ + internal/command/requirementbrowser/v1_adapter.go \ + internal/command/requirementcontext/v1_adapter.go \ + internal/command/requirementdiff/v1_adapter.go \ + internal/command/stackpreset/preset_ids_generated.go \ + internal/tools/commandcontractgen/condition_model.go \ + internal/tools/commandcontractgen/main.go \ + internal/tools/commandcontractgen/main_test.go \ + internal/tools/pythonpackage/continuation_test.go \ + release/change-record.v2.json \ + scripts/workflow_browser_runtime_oracle_test.go \ + scripts/workflow_oracle_support_test.go \ + scripts/workflow_runtime_preconditions_test.go \ + scripts/workflow_security_scanner_oracles_test.go \ + scripts/workflow_source_oracles_test.go \ + tests/browser/axe-harness.mjs | + LC_ALL=C sort + )" + actual_untracked_inventory="$( + git ls-files --others --exclude-standard | LC_ALL=C sort + )" + test "$expected_correction_path_inventory" = \ + "$correction_path_inventory" + test "$expected_untracked_inventory" = "$actual_untracked_inventory" + printf '%s\n' "$expected_correction_path_inventory" | + git add --pathspec-from-file=- + staged_path_inventory="$( + git diff --cached --name-only | LC_ALL=C sort + )" + actual_baseline_added_inventory="$( + git diff --cached --diff-filter=A --name-only "$baseline_sha" | + LC_ALL=C sort + )" + missing_decomposition_owner_inventory="$( + comm -23 \ + <(printf '%s\n' "$expected_decomposition_owner_inventory") \ + <(printf '%s\n' "$actual_baseline_added_inventory") + )" + test "$expected_correction_path_inventory" = "$staged_path_inventory" + test -z "$missing_decomposition_owner_inventory" + test "$expected_baseline_added_inventory" = \ + "$actual_baseline_added_inventory" + test -z "$(git diff --name-only)" + test -z "$(git ls-files --others --exclude-standard)" + ``` + + Any extra or missing path stops candidate creation. +2. Review the staged diff and generated-file freshness. +3. Create the first candidate with a conventional, human-oriented message, or + amend that commit without changing its reviewed message after a correction + epoch: + + ```text + fix: close audit remediation gaps + ``` + + ```bash + git commit --amend --no-edit + ``` + +4. Record the candidate commit SHA. +5. Require a clean worktree apart from ignored local artifacts. + +### P12.3 Committed-object validation epoch + +Against the candidate SHA: + +1. rerun `git diff --check` and `npm run check`; +2. ask three fresh read-only agents to validate the committed diff against the + design and plan; +3. require all three to return `APPROVE` with no unresolved confirmed finding; +4. after every confirmed finding from those reviews is corrected, run one + additional independent `gpt-5.6-sol` reviewer with maximum reasoning + effort against the exact candidate SHA and require no unresolved confirmed + finding; +5. set design and plan status to `implemented/validated` only within the same + candidate correction epoch; +6. if any correction or status edit changes bytes, amend the candidate commit, + record the new SHA, and repeat the entire full gate and three-agent review; + the independent maximum-reasoning audit must also be repeated when a + confirmed correction changes bytes; +7. accept exactly one final committed SHA as publication input and substitute + its literal 40-hex value for `` in every P12.4-P12.6 + command. Re-deriving it from a later mutable `HEAD` is forbidden. + +### P12.4 Publish or refresh the branch + +Verify the already published remote before replacing its reviewed candidate: + +```bash +set -euo pipefail +audit_baseline_sha="3d86b6d0e4ec4a6c6a7f7a35ff2787011771aa64" +integration_base_sha="0df4c28bac9737f476f7dc66030363b8b40d5417" +final_sha="" +previous_remote_sha="7e29a8e2a85d36a0c439753dfb5b951ed598078a" +test "$(git branch --show-current)" = "fix/audit-remediation" +test "$(git rev-parse HEAD)" = "$final_sha" +test "$(git rev-list --parents -n 1 "$final_sha")" = \ + "$final_sha $integration_base_sha" +test "$(git merge-base "$audit_baseline_sha" "$integration_base_sha")" = \ + "$audit_baseline_sha" +test "$(git remote get-url --push origin)" = \ + "https://github.com/research-engineering/agentic-proofkit" +test "$integration_base_sha" = \ + "$(git ls-remote origin refs/heads/main | awk '{print $1}')" +test "$previous_remote_sha" = \ + "$(git ls-remote origin refs/heads/fix/audit-remediation | awk '{print $1}')" +git push \ + --force-with-lease=refs/heads/fix/audit-remediation:"$previous_remote_sha" \ + origin "${final_sha}:refs/heads/fix/audit-remediation" +test "$final_sha" = \ + "$(git ls-remote origin refs/heads/fix/audit-remediation | awk '{print $1}')" +test "$integration_base_sha" = \ + "$(git ls-remote origin refs/heads/main | awk '{print $1}')" +git branch --set-upstream-to=origin/fix/audit-remediation \ + fix/audit-remediation +``` + +Any remote mismatch stops publication. + +### P12.5 Create or refresh the pull request + +Create a non-draft pull request into `main` if none exists. If pull request +`#78` already exists for the branch, update that same pull request rather than +creating a duplicate. + +Title: + +```text +Close proof, contract, release, and onboarding audit gaps +``` + +The body includes: + +- historical audit baseline, integration base, and current PR scope; +- adjudicated fixes and rejected hypotheses; +- intentional compatibility changes and `0.2.0` migration; +- design and plan review history; +- exact narrow and full gates; +- provider failure diagnosis, corrective cycles, and current check status; +- residual non-claims; +- the closed large-file ledger, the remediated workflow god-file + concentration, and the rejected premature merge/split candidates. + +Write the reviewed body to +`/tmp/agentic-proofkit-audit-remediation-pr-body.md`. For the existing pull +request, run: + +```bash +set -euo pipefail +integration_base_sha="0df4c28bac9737f476f7dc66030363b8b40d5417" +final_sha="" +repo="research-engineering/agentic-proofkit" +test "$integration_base_sha" = \ + "$(git ls-remote origin refs/heads/main | awk '{print $1}')" +pr_before_edit="$(mktemp)" +gh pr view 78 \ + --repo "$repo" \ + --json author,headRefName,headRefOid,baseRefName,baseRefOid,state,mergedAt \ + > "$pr_before_edit" +jq -e \ + --arg sha "$final_sha" \ + --arg base "$integration_base_sha" \ + '.author.login == "iperev" + and .headRefName == "fix/audit-remediation" + and .headRefOid == $sha + and .baseRefName == "main" + and .baseRefOid == $base + and .state == "OPEN" + and .mergedAt == null' \ + "$pr_before_edit" >/dev/null + +gh pr edit 78 \ + --repo "$repo" \ + --title "Close proof, contract, release, and onboarding audit gaps" \ + --body-file /tmp/agentic-proofkit-audit-remediation-pr-body.md +pr_after_edit="$(mktemp)" +gh pr view 78 \ + --repo "$repo" \ + --json url,title,author,headRefName,headRefOid,baseRefName,baseRefOid,isDraft,state,mergedAt,body \ + > "$pr_after_edit" +jq -e \ + --arg sha "$final_sha" \ + --arg base "$integration_base_sha" \ + '.author.login == "iperev" + and .headRefName == "fix/audit-remediation" + and .headRefOid == $sha + and .baseRefName == "main" + and .baseRefOid == $base + and .isDraft == false + and .state == "OPEN" + and .mergedAt == null + and .title == "Close proof, contract, release, and onboarding audit gaps"' \ + "$pr_after_edit" >/dev/null +jq -j '.body' "$pr_after_edit" > /tmp/proofkit-pr-body-readback.md +cmp -s /tmp/agentic-proofkit-audit-remediation-pr-body.md \ + /tmp/proofkit-pr-body-readback.md +``` + +Require `author.login=iperev`, `headRefName=fix/audit-remediation`, +`headRefOid=`, `baseRefName=main`, +`baseRefOid=0df4c28bac9737f476f7dc66030363b8b40d5417`, `isDraft=false`, +`state=OPEN`, `mergedAt=null`, and the exact reviewed title above. The refreshed +body must name the final SHA, tree, file and line counts, gate counts, and +provider disposition; old candidate identities or counts are forbidden. + +### P12.6 Provider status + +1. Read back the PR author, exact `headRefOid` and `baseRefOid`, head/base refs, + `state`, and `mergedAt`. +2. Confirm author is `iperev`. +3. Inspect provider checks and require exactly these source-owned closeout + tuples to be present for the final SHA in provider workflow attempt `1`: + - `ci` / `quality / source`; + - `ci` / `quality / platform smoke / macos-15`; + - `ci` / `quality / browser runtime`; + - `ci` / `quality / required aggregate`. + This is the closed inventory owned by `.github/workflows/ci.yml` and its + typed workflow oracle, not evidence of provider branch-protection settings. +4. Wait for terminal checks when available without converting pending or + skipped states into success. +5. If a browser failure moves between tests at the same timeout cap, retain and + inspect the first-failure trace before changing application behavior, + timeout policy, or retry policy; diagnostic reruns are not merge proof. +6. Do not merge unless the user separately authorizes merge. +7. Treat a required provider failure or an absent required check as a blocker, + including when the failure is terminal and fully diagnosed; do not retire + the design or plan while any such blocker exists. +8. After all checks triggered for the final SHA reach terminal states and every + required check passes, complete retrospective routing. +9. Execute the canonical provider-projection function from step 10 once, + construct the canonical closeout record from the exact local sources below, + compute both SHA-256 values, and refresh the reviewed pull-request body with + exact `provider-projection-sha256: sha256:` and + `closeout-record-sha256: sha256:` markers plus canonical JSON bytes + between their respective exact `*-json-begin` and `*-json-end` sentinel + lines. Human-readable summary text remains explicitly non-authoritative. +10. Perform the bounded authoritative readback below. The two identity + snapshots reject a persistent head mismatch at either boundary, while the + provider endpoints bind every conclusion read to the literal final SHA: + +```bash +set -euo pipefail +audit_baseline_sha="3d86b6d0e4ec4a6c6a7f7a35ff2787011771aa64" +integration_base_sha="0df4c28bac9737f476f7dc66030363b8b40d5417" +final_sha="" +repo="research-engineering/agentic-proofkit" +branch_ref="refs/heads/fix/audit-remediation" +base_ref="refs/heads/main" +expected_title="Close proof, contract, release, and onboarding audit gaps" +pr_snapshot="$(mktemp)" +ci_runs="$(mktemp)" +ci_jobs="$(mktemp)" +check_runs="$(mktemp)" +commit_status="$(mktemp)" +provider_projection_one="$(mktemp)" +provider_projection_two="$(mktemp)" +provider_projection_embedded_one="$(mktemp)" +provider_projection_embedded_server="$(mktemp)" +closeout_record_expected="$(mktemp)" +closeout_record_embedded_one="$(mktemp)" +closeout_record_embedded_server="$(mktemp)" +coverage_snapshot="$(mktemp)" +browser_snapshot="$(mktemp)" +package_execution_snapshot="$(mktemp)" +local_closeout_report="$(mktemp)" +reviewed_body_snapshot="$(mktemp)" +pr_body_readback="$(mktemp)" +test "$final_sha" = "$(git rev-parse HEAD)" +test -z "$(git status --porcelain)" +final_tree="$(git rev-parse "$final_sha^{tree}")" +diff_stats="$( + git diff --numstat "$integration_base_sha" "$final_sha" | + awk ' + { + if ($1 !~ /^[0-9]+$/ || $2 !~ /^[0-9]+$/) { + invalid = 1 + } + files++ + added += $1 + deleted += $2 + } + END { + if (invalid) { + exit 1 + } + print files + 0, added + 0, deleted + 0 + } + ' +)" +read -r diff_file_count diff_added_lines diff_deleted_lines \ + <<< "$diff_stats" +cp artifacts/proofkit/coverage-metrics.json "$coverage_snapshot" +cp artifacts/proofkit/browser-runtime-proof.json "$browser_snapshot" +cp artifacts/proofkit/package-artifact-execution.json \ + "$package_execution_snapshot" +assert_coverage_snapshot() { + jq -e -s \ + --arg sha "$final_sha" ' + length == 1 + and (.[0] | + .provenance.sourceRevision == $sha + and .requirements.totalRecords == 69 + and .proofBindings.boundRequirementCount == 69 + and .proofBindings.scenarioCount == 173 + and .commandRoutes.commandCount == 78 + ) + ' >/dev/null +} +assert_coverage_snapshot < "$coverage_snapshot" +for rejected_scenario_count in 167 172 174; do + if jq -c \ + --argjson count "$rejected_scenario_count" \ + '.proofBindings.scenarioCount = $count' \ + "$coverage_snapshot" | + assert_coverage_snapshot; then + exit 1 + fi +done +jq -e -s \ + --arg sha "$final_sha" ' + length == 1 + and (.[0] | + .sourceRevision == $sha + and .sourceTreeState == "clean" + and .state == "passed" + and (.projects | length) == 3 + and (.projects | map(.name) | sort) + == ["chromium", "firefox", "webkit"] + and (.projects | all( + .executedTestCount == 31 + and .passedTestCount == 31 + )) + ) +' "$browser_snapshot" >/dev/null +jq -e -s \ + --arg sha "$final_sha" ' + length == 1 + and (.[0] | + .commandId == "proofkit.package-artifact" + and .sourceRevision == $sha + and .status == "passed" + and .exitCode == 0 + ) +' "$package_execution_snapshot" >/dev/null +go run ./internal/tools/releasecloseoutinput | + go run ./cmd/agentic-proofkit completion-criteria --input - \ + > "$local_closeout_report" +jq -e -s ' + length == 1 + and (.[0] | + .state == "passed" + and .summary.blockingCriterionCount == 5 + and .summary.blockingUnsatisfiedCount == 0 + and .summary.advisoryCriterionCount == 1 + and .summary.statusCounts.satisfied == 5 + and .summary.statusCounts.advisory_skipped == 1 + ) +' "$local_closeout_report" >/dev/null +jq -S -n \ + --arg finalSha "$final_sha" \ + --arg finalTree "$final_tree" \ + --arg auditBaselineSha "$audit_baseline_sha" \ + --arg integrationBaseSha "$integration_base_sha" \ + --argjson diffFiles "$diff_file_count" \ + --argjson diffAdded "$diff_added_lines" \ + --argjson diffDeleted "$diff_deleted_lines" \ + --slurpfile coverage "$coverage_snapshot" \ + --slurpfile browser "$browser_snapshot" \ + --slurpfile packageExecution "$package_execution_snapshot" \ + --slurpfile closeout "$local_closeout_report" \ + '{ + schemaVersion: 1, + finalSha: $finalSha, + finalTree: $finalTree, + auditBaselineSha: $auditBaselineSha, + integrationBaseSha: $integrationBaseSha, + diff: { + files: $diffFiles, + addedLines: $diffAdded, + deletedLines: $diffDeleted + }, + localGates: { + packageArtifact: { + commandId: $packageExecution[0].commandId, + sourceRevision: $packageExecution[0].sourceRevision, + status: $packageExecution[0].status, + exitCode: $packageExecution[0].exitCode + }, + browserRuntime: { + sourceRevision: $browser[0].sourceRevision, + sourceTreeState: $browser[0].sourceTreeState, + state: $browser[0].state, + projectCount: ($browser[0].projects | length), + executedTestCount: ( + $browser[0].projects | map(.executedTestCount) | add + ), + passedTestCount: ( + $browser[0].projects | map(.passedTestCount) | add + ) + }, + coverage: { + sourceRevision: $coverage[0].provenance.sourceRevision, + requirementCount: $coverage[0].requirements.totalRecords, + boundRequirementCount: + $coverage[0].proofBindings.boundRequirementCount, + scenarioCount: $coverage[0].proofBindings.scenarioCount, + commandCount: $coverage[0].commandRoutes.commandCount + }, + localCloseout: { + state: $closeout[0].state, + blockingCriterionCount: + $closeout[0].summary.blockingCriterionCount, + blockingUnsatisfiedCount: + $closeout[0].summary.blockingUnsatisfiedCount, + satisfiedCount: $closeout[0].summary.statusCounts.satisfied, + advisorySkippedCount: + $closeout[0].summary.statusCounts.advisory_skipped + } + }, + residualNonClaims: [ + "No registry publication, Trusted Publisher or OIDC provider identity, branch-protection setting, rollout, deployment, or production readiness is proven.", + "Pinned browser engines do not imply complete WCAG conformance or branded Safari parity.", + "The output writer does not claim protection from same-user content or namespace mutation during the operation, fsync durability, or a repository-wide transaction.", + "Local closeout evidence objects are individually admitted snapshots, not an atomic filesystem transaction across all artifacts.", + "Provider observations are bounded but not atomic across endpoints or immutable after the final response." + ], + retrospective: [ + "Single-factor Firefox hypotheses were falsified before the combined axe-initialization and trace-screenshot correction was admitted.", + "Repeated closeout misses require literal-SHA provider APIs, mutation oracles at irreversible boundaries, admitted byte snapshots, and machine-readable closeout projections." + ] + }' > "$closeout_record_expected" +test "$final_sha" = "$(git rev-parse HEAD)" +test -z "$(git status --porcelain)" +cp /tmp/agentic-proofkit-audit-remediation-pr-body.md \ + "$reviewed_body_snapshot" +reviewed_body_digest="sha256:$( + shasum -a 256 "$reviewed_body_snapshot" | awk '{print $1}' +)" + +assert_remote_and_pr_identity() { + test "$final_sha" = \ + "$(git ls-remote origin "$branch_ref" | awk '{print $1}')" + test "$integration_base_sha" = \ + "$(git ls-remote origin "$base_ref" | awk '{print $1}')" + gh pr view 78 --repo "$repo" \ + --json title,author,headRefName,headRefOid,baseRefName,baseRefOid,isDraft,state,mergedAt,body \ + > "$pr_snapshot" + jq -e \ + --arg sha "$final_sha" \ + --arg base "$integration_base_sha" \ + --arg title "$expected_title" \ + '.author.login == "iperev" + and .headRefName == "fix/audit-remediation" + and .headRefOid == $sha + and .baseRefName == "main" + and .baseRefOid == $base + and .isDraft == false + and .state == "OPEN" + and .mergedAt == null + and .title == $title' \ + "$pr_snapshot" >/dev/null +} + +assert_provider_disposition() { +local projection_path="$1" +gh api \ + "repos/$repo/actions/workflows/ci.yml/runs?event=pull_request&head_sha=$final_sha&per_page=100" \ + > "$ci_runs" +ci_run_id="$( + jq -er \ + --arg sha "$final_sha" \ + --arg base "$integration_base_sha" \ + '. as $response + | select( + $response.total_count == ($response.workflow_runs | length) + and $response.total_count > 0 + ) + | [ + $response.workflow_runs[] + | select( + .head_sha == $sha + and .head_branch == "fix/audit-remediation" + and .event == "pull_request" + and (.pull_requests | length) == 1 + and .pull_requests[0].number == 78 + and .pull_requests[0].head.ref == "fix/audit-remediation" + and .pull_requests[0].head.sha == $sha + and .pull_requests[0].head.repo.url + == "https://api.github.com/repos/research-engineering/agentic-proofkit" + and .pull_requests[0].base.ref == "main" + and .pull_requests[0].base.sha == $base + and .pull_requests[0].base.repo.url + == "https://api.github.com/repos/research-engineering/agentic-proofkit" + ) + ] + | if length != 1 then error("expected one exact-PR exact-SHA ci run") + else .[0] + end + | select( + .run_attempt == 1 + and .status == "completed" + and .conclusion == "success" + ) + | .id' \ + "$ci_runs" +)" +ci_run_attempt="$( + jq -er \ + --argjson id "$ci_run_id" \ + '.workflow_runs[] + | select(.id == $id) + | .run_attempt' \ + "$ci_runs" +)" +gh api \ + "repos/$repo/actions/runs/$ci_run_id/attempts/$ci_run_attempt/jobs?per_page=100" \ + > "$ci_jobs" +jq -e ' + [ + "quality / source", + "quality / platform smoke / macos-15", + "quality / browser runtime", + "quality / required aggregate" + ] as $required + | .jobs as $jobs + | ($jobs | length) == ($required | length) + and ([$jobs[].name] | sort) == ($required | sort) + and ($jobs | all( + .head_sha == $sha + and .run_attempt == $attempt + and .status == "completed" + and .conclusion == "success" + )) +' \ + --arg sha "$final_sha" \ + --argjson attempt "$ci_run_attempt" \ + "$ci_jobs" >/dev/null +gh api \ + "repos/$repo/commits/$final_sha/check-runs?filter=all&per_page=100" \ + > "$check_runs" +jq -e ' + .total_count == (.check_runs | length) + and (.check_runs | all(.status == "completed")) +' "$check_runs" >/dev/null +gh api \ + "repos/$repo/commits/$final_sha/status?per_page=100" \ + > "$commit_status" +jq -e \ + --arg sha "$final_sha" \ + '.sha == $sha + and (.statuses | length) < 100 + and (.statuses | all( + .state | IN("success", "failure", "error") + ))' \ + "$commit_status" >/dev/null +jq -S -n \ + --arg finalSha "$final_sha" \ + --arg integrationBaseSha "$integration_base_sha" \ + --argjson ciRunId "$ci_run_id" \ + --slurpfile runs "$ci_runs" \ + --slurpfile jobs "$ci_jobs" \ + --slurpfile checks "$check_runs" \ + --slurpfile statuses "$commit_status" \ + '{ + schemaVersion: 1, + finalSha: $finalSha, + integrationBaseSha: $integrationBaseSha, + ciRun: ( + $runs[0].workflow_runs[] + | select(.id == $ciRunId) + | { + id, + runAttempt: .run_attempt, + event, + status, + conclusion, + headSha: .head_sha, + headBranch: .head_branch, + pullRequests: [ + .pull_requests[] + | { + number, + headRef: .head.ref, + headSha: .head.sha, + headRepo: .head.repo.url, + baseRef: .base.ref, + baseSha: .base.sha, + baseRepo: .base.repo.url + } + ] + } + ), + jobs: ( + $jobs[0].jobs + | map({ + id, + name, + status, + conclusion, + headSha: .head_sha, + runAttempt: .run_attempt + }) + | sort_by(.name, .id) + ), + checkRuns: ( + $checks[0].check_runs + | map({ + id, + name, + status, + conclusion, + appSlug: .app.slug, + externalId: .external_id + }) + | sort_by(.name, .id) + ), + legacyStatuses: ( + $statuses[0].statuses + | map({ + id, + context, + state, + description, + targetUrl: .target_url, + creatorLogin: .creator.login + }) + | sort_by(.context, .id) + ) + }' > "$projection_path" +} + +assert_embedded_projection() { + local body_path="$1" + local projection_path="$2" + local embedded_path="$3" + local marker_name="$4" + local begin_line="$5" + local end_line="$6" + local projection_digest + projection_digest="sha256:$( + shasum -a 256 "$projection_path" | awk '{print $1}' + )" + test "$( + grep -Ec "^${marker_name}:" "$body_path" + )" -eq 1 + test "$( + grep -Ec \ + "^${marker_name}: sha256:[0-9a-f]{64}$" \ + "$body_path" + )" -eq 1 + grep -Fqx \ + "${marker_name}: $projection_digest" \ + "$body_path" + awk \ + -v begin_line="$begin_line" \ + -v end_line="$end_line" ' + BEGIN { + begins = 0 + ends = 0 + inside = 0 + invalid = 0 + } + $0 == begin_line { + begins++ + if (inside || ends > 0) { + invalid = 1 + } + inside = 1 + next + } + $0 == end_line { + ends++ + if (!inside) { + invalid = 1 + } + inside = 0 + next + } + inside { + print + } + END { + if (invalid || begins != 1 || ends != 1 || inside) { + exit 1 + } + } + ' "$body_path" > "$embedded_path" + cmp -s "$projection_path" "$embedded_path" +} + +assert_remote_and_pr_identity +assert_provider_disposition "$provider_projection_one" +assert_embedded_projection \ + "$reviewed_body_snapshot" \ + "$provider_projection_one" \ + "$provider_projection_embedded_one" \ + "provider-projection-sha256" \ + "provider-projection-json-begin" \ + "provider-projection-json-end" +assert_embedded_projection \ + "$reviewed_body_snapshot" \ + "$closeout_record_expected" \ + "$closeout_record_embedded_one" \ + "closeout-record-sha256" \ + "closeout-record-json-begin" \ + "closeout-record-json-end" +assert_remote_and_pr_identity +assert_provider_disposition "$provider_projection_two" +cmp -s "$provider_projection_one" "$provider_projection_two" +assert_remote_and_pr_identity +jq -j '.body' "$pr_snapshot" > "$pr_body_readback" +test "$reviewed_body_digest" = "sha256:$( + shasum -a 256 "$reviewed_body_snapshot" | awk '{print $1}' +)" +cmp -s "$reviewed_body_snapshot" "$pr_body_readback" +assert_embedded_projection \ + "$pr_body_readback" \ + "$provider_projection_two" \ + "$provider_projection_embedded_server" \ + "provider-projection-sha256" \ + "provider-projection-json-begin" \ + "provider-projection-json-end" +assert_embedded_projection \ + "$pr_body_readback" \ + "$closeout_record_expected" \ + "$closeout_record_embedded_server" \ + "closeout-record-sha256" \ + "closeout-record-json-begin" \ + "closeout-record-json-end" +``` + +Each provider pass applies the following rules. The workflow-run query is +filtered by the literal final SHA and `ci.yml`; the +filtered set must contain exactly one run bound uniquely to pull request `#78`, +the exact `fix/audit-remediation` head in this repository, and the `main` base +in this repository. Multiple runs for the same PR and SHA fail closed. The sole +run must itself pass, and its exact current attempt must equal `1` and contain +only the four closed-inventory jobs, each bound to that SHA, attempt `1`, and +concluded `success`; a provider rerun or repeated same-object run is diagnostic +evidence only. The commit check-run endpoint independently +requires every triggered check run for the literal SHA to be terminal and +fails closed if pagination would hide a row. The literal-SHA combined-status +endpoint rejects pending legacy contexts, admits terminal `success`, `failure`, +and `error` only for accurate reporting, and fails closed at the page limit. +The first pass emits a canonical projection of every provider +decision-relevant field. Each local artifact used by the closeout record is +copied once to a private file; validation and projection consume only those +same snapshot bytes, after which final `HEAD` and tracked-tree cleanliness are +rechecked. Only fields named by the exact validation predicates are projected; +opaque artifact digests are omitted. The snapshots are individually admitted +and do not claim an atomic filesystem transaction across all artifacts. Before +this final block, use an +earlier execution of the same function and the exact local snapshots admitted +above to put both the provider projection and canonical closeout record into +the reviewed local +pull-request body with their exact digest markers and sentinel pairs. The +closeout record machine-binds the final tree, diff counts, local gate counts, +residual non-claims, and retrospective instead of relying on narrative +presence. At block entry, copy those reviewed bytes once into a private +snapshot and bind that snapshot to its digest; the mutable fixed pathname is +never read again. The block rejects duplicate or malformed markers and +sentinels in that snapshot, extracts both embedded records, and requires byte +equality before requiring the second provider projection to be byte-identical. +The final server body is then compared with the unchanged reviewed snapshot +and independently subjected to the same marker, sentinel, digest, and +embedded-byte validation for both records. Human-readable summary prose is +explicitly non-authoritative. Thus both admitted local bytes and observed +server bytes are bound directly to the stable provider disposition and exact +closeout facts rather than merely asserting that some terminal predicate +passed. +The final identity snapshot is the one used for byte-transparent body +comparison and direct server-body validation. The complete provider pass is +executed twice, with identity checks before, between, and after the passes; +persistent provider reruns or new nonterminal rows therefore invalidate the +second pass. The observations are not an atomic snapshot across endpoints. +Concurrent `A -> B -> A` PR identity history between observations and provider +mutation after the final response remain explicit non-claims, but neither can +substitute provider conclusions for `A` because every provider query is bound +directly to `final_sha`. This final readback must require the exact final SHA, +tree, gate counts, bounded provider observations, residual non-claims, and +retrospective result. It is the final closeout projection and the last +retirement precondition. + +## Completion criteria + +The implementation is complete only if: + +1. all design findings R-01 through R-29 and R-11a through R-11d have their + exact durable witness routes; +2. every current-wrong counterexample is observed red before repair or is + otherwise preserved as an isolated mutation proof; +3. every narrow gate and `npm run check` pass on the final committed object; +4. all three final reviewers approve and the independent maximum-reasoning + audit reports no unresolved confirmed finding; +5. business-logic changes are limited to the design's intentional compatibility + list; +6. GitHub identity and push permission are reverified; +7. the branch is pushed and the PR authored by `iperev` is open, non-draft, + unmerged, and bound to the literal final SHA and reviewed integration-base + SHA; +8. in two consecutive final literal-SHA provider passes, every tuple in the + closed source-owned required-check inventory is present with a `success` + conclusion, every other observed check is terminal, and all states are + reported without claiming an atomic cross-endpoint snapshot; +9. retrospective routing is completed; +10. the final pull-request closeout projection is read back with the exact + provider disposition, residual non-claims, and retrospective result, while + bracketing identity snapshots both bind the remote head/base branches and + pull request to the literal final and integration-base SHAs and the + server-side + body equals the reviewed body and uniquely contains both the SHA-256 and + exact sentinel-delimited bytes of the byte-identical canonical provider + projections; +11. temporary design/plan retirement conditions are explicit and remain false + until criteria 1 through 10 all hold. + +## Review acceptance criteria + +Reviewers approve this plan only if: + +- every step is executable and ordered after its dependencies; +- every gate selector matches the test it claims to run; +- no production change precedes its owner and counterexample; +- no plan step invents provider, registry, or selective proof; +- the shared-worktree execution model preserves reviewer independence; +- the final publication account and permissions are explicit; +- the plan can be overturned safely at each named rollback condition. diff --git a/docs/proofkit-contract-map.md b/docs/proofkit-contract-map.md index 8a5db92..2200cb2 100644 --- a/docs/proofkit-contract-map.md +++ b/docs/proofkit-contract-map.md @@ -93,7 +93,8 @@ opt-in derived projection over the same report. This map explains the route families without becoming an execution, freshness, or merge decision. The exact route input vocabulary is machine-readable in `proofkit/cli-contract.v2.json` under `agent-route.inputContract`; the Go -admission implementation and shipped CLI contract are parity-tested. +admission implementation owns nested and semantic behavior, while the shipped +CLI contract owns the bounded root-shape variants and their ABI digest. Formal rule: @@ -113,7 +114,7 @@ Semantic context routes are `requirement-context-compose`, | State or goal | Next Proofkit route | Stop or escalation condition | |---|---|---| -| The agent does not know where to start. | `init` or `init --preset fresh|code-baseline|code-audit|legacy|change-set` | Treat output as dry-run route guidance only. Stop before scanning, writing files, or making requirements authoritative. | +| The agent does not know where to start. | `init`, or `init --preset` with `fresh`, `code-baseline`, `code-audit`, `legacy`, or `change-set` | Treat output as dry-run route guidance only. Stop before scanning, writing files, or making requirements authoritative. | | No admitted spec/profile exists and the caller has explicit capability observations. | `capability-map-admission`; use `trustMode: "code_baseline"` only when maintainers intentionally freeze current code, otherwise use `trustMode: "audit_from_code"`. | Stop before treating seeds as stable requirements. The consumer owns observation extraction, materialization, requirement meaning, and proof adequacy. | | No admitted spec/profile exists and no capability observations exist. | `scaffold-project-structure`, `adoption-workflow-plan`, or `stack-preset` | Stop before writing files; the consumer owns materialization, overwrite policy, and final requirement text. | | Candidate boundary is uncertain. | `adoption-doctor` or `gradual-adoption-guidance --agent-envelope` | Escalate to owner review when the boundary is advisory, ambiguous, or missing native witnesses. | diff --git a/docs/release-process.md b/docs/release-process.md index 8445868..0520115 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -42,7 +42,7 @@ Go source -> GitHub Release assets with checksums and SBOM for provenance lookup ``` -The committed `release/change-record.v1.json` owns the reviewed, version-bound +The committed `release/change-record.v2.json` owns the reviewed, version-bound declaration of the public-contract delta, migration decision, platform requirements, known limitations, and rollback strategy. It does not infer change completeness from the source diff. The repository-owned @@ -81,7 +81,8 @@ Before publishing a version: 1. The source tree is clean. 2. `package.json` contains the exact new version. -3. `release/change-record.v1.json` contains the same version and explicitly +3. `release/change-record.v2.json` contains the same version, names the exact + previous version and compatible or breaking change class, and explicitly classifies breaking changes, additions, migration, platform requirements, known limitations, and rollback. 4. `package.json` repository, license, bin, exports, files, and publishConfig @@ -211,9 +212,9 @@ The evidence must distinguish: These evidence classes are not interchangeable. Historical GitHub Releases are immutable provider state. If an older release -metadata record names an asset that is absent from the provider release and the -provider rejects exact backfill, the repository records that as a historical -archive-evidence exception instead of mutating the release model around it. +metadata record names an asset that is absent from the provider release, the +repository records that as a historical archive-evidence exception. It does +not attempt backfill regardless of whether the provider would permit mutation. Future releases must prevent recurrence by verifying the expected public asset set, byte-for-byte Release asset content, `release-notes.md` presence, release manifest and metadata checksum closure, and retained workflow evidence checksum diff --git a/docs/specs/proofkit-consumer-infra-retirement/overview.md b/docs/specs/proofkit-consumer-infra-retirement/overview.md index dea2e7c..5aafb2a 100644 --- a/docs/specs/proofkit-consumer-infra-retirement/overview.md +++ b/docs/specs/proofkit-consumer-infra-retirement/overview.md @@ -41,7 +41,8 @@ approval, rollout approval, and production decisions. - `REQ-PROOFKIT-RETIRE-008`: adoption doctor reports classify caller-provided imperfect-repository migration gaps and non-passing child reports into advisory, failed, or blocked states and emit bounded owner-specific guidance - without scanning repositories or owning semantic boundary decisions. + without scanning repositories or owning semantic boundary decisions; + unresolved external prerequisites remain blocked in every adoption mode. - `REQ-PROOFKIT-RETIRE-009`: workspace manifest fact projection turns explicit caller-owned manifest records into registry-compatible workspace facts and planning inputs without reading manifests from disk or owning package-manager diff --git a/docs/specs/proofkit-consumer-infra-retirement/requirements.v1.json b/docs/specs/proofkit-consumer-infra-retirement/requirements.v1.json index f59378d..7f77b78 100644 --- a/docs/specs/proofkit-consumer-infra-retirement/requirements.v1.json +++ b/docs/specs/proofkit-consumer-infra-retirement/requirements.v1.json @@ -197,7 +197,7 @@ { "requirementId": "REQ-PROOFKIT-RETIRE-008", "ownerId": "proofkit.consumer-infra-retirement", - "invariant": "Adoption doctor reports classify caller-provided imperfect-repository migration gaps, non-passing child reports, and stale current authority vocabulary facts into advisory, failed, or blocked states, and emit bounded owner-specific agent guidance without scanning repositories or owning semantic boundary decisions.", + "invariant": "Adoption doctor reports classify caller-provided imperfect-repository migration gaps, non-passing child reports, and stale current authority vocabulary facts into advisory, failed, or blocked states, and emit bounded owner-specific agent guidance without scanning repositories or owning semantic boundary decisions; every unresolved external prerequisite remains blocked in every adoption mode, while mode policy can relax only admitted advisory gaps.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": [ diff --git a/docs/specs/proofkit-package-boundary/overview.md b/docs/specs/proofkit-package-boundary/overview.md index c911208..cf1efbf 100644 --- a/docs/specs/proofkit-package-boundary/overview.md +++ b/docs/specs/proofkit-package-boundary/overview.md @@ -9,34 +9,63 @@ denial, and package artifact behavior only. - `REQ-PROOFKIT-PACKAGE-001`: the package artifact set exposes the supported CLI through one root package with embedded platform binaries while denying root imports, source imports, generated JavaScript imports, and deep internal - package paths as public contract. + package paths as public contract. Its closed machine-field inventory, bounded + Markdown destination grammar, README owner table, and exact README + command-navigation statement keep admitted routes closed over shipped entries + or explicit source-checkout evidence classes without claiming a complete + Markdown parser. - `REQ-PROOFKIT-PACKAGE-002`: the CLI builds deterministic reports, plans, generated source artifacts, and policy-admission results from explicit caller-owned JSON, declared no-input command parameters, or declared explicit - scanner scope classes. Explicit scanners consume only caller-named bounded - canonical files under documented fail-closed grammars rather than guessing - layouts or silently accepting unsupported syntax; no command executes native - witnesses, scans implicit repository state, decides proof freshness, or accepts - broad caller-supplied phrase suppressors that can hide readiness overclaims. + scanner scope classes. One immutable explicit launcher profile renders the + closed display-command and structured-argv inventory across help, preset, + bootstrap, project, route, workflow, and coverage surfaces without rewriting + caller commands or autodetecting a runner. Python executable admission + rejects secret-like, Unicode control, and Unicode format content without + disclosing it; the authored + CLI contract closes required input and JSON output root-shape variants, + exact successful-output selectors, native source sets, and requirement + scenarios, rejects repeated or mixed pilot selectors rather than applying + last-write-wins routing, then generates private help and preset projections + without claiming nested shape or type parity. + Explicit scanners consume only caller-named bounded canonical files under + documented fail-closed grammars, pin each referenced package sub-root before + reading its manifest and sources, bind every alias of one canonical source + to its first immutable identity, digest, and parsed result, and reject later + alias drift rather than guessing layouts or silently accepting unsupported + syntax. Explicit loopback browser opening accepts only the exact root URL and + one fixed platform launcher argv; no command executes native witnesses, + scans implicit repository state, decides proof freshness, or accepts literal or + strict-character-reference-encoded broad caller-supplied phrase suppressors + that can hide readiness overclaims. - `REQ-PROOFKIT-PACKAGE-003`: the root package remains installable and - executable by an outside consumer on the current native platform without - claiming registry publication. + executable by an outside consumer through the exact offline onboarding trace + on the current native platform; every generated preset command retains the + offline npm resolver and one exact emitted continuation is re-executed + without claiming registry publication. - `REQ-PROOFKIT-PACKAGE-004`: CI package-gate receipts used as merge evidence are admitted through a declared producer policy and proof-receipt shape - validator instead of current-build output alone. + validator instead of current-build output alone, and each fixed subprocess + verdict rejects process, JSON, or non-passed-state failure independently. - `REQ-PROOFKIT-PACKAGE-005`: tracked source hygiene covers each admitted text - language in staged blobs and the current worktree; Go formatting, static - analysis, test, package, and vulnerability gates remain the native - merge-critical quality floor and explicitly provision every runtime required - by a merge-critical oracle. + language in staged blobs and the current worktree, including every extension + currently present in the tracked authored browser assets, matches prohibited + organization identifiers only at identifier boundaries so content digests + cannot create false positives, and retains Go formatting, static analysis, + test, package, and vulnerability gates as the native merge-critical quality + floor with every required oracle runtime explicitly provisioned. - `REQ-PROOFKIT-PACKAGE-006`: Python/uv distribution is a platform wheel wrapper over the same Go CLI, with wheel-safe package metadata, wheel tags, embedded binary identity, local install smoke proof, POSIX exec - process-identity and signal preservation, and explicit non-claims until PyPI - publication. + process-identity and signal preservation, exact generated display and argv + routes through the active absolute interpreter, direct execution of the + root/family/leaf help chain and emitted agent-route argv with npm absent from + `PATH`, and an owner-checked README platform projection with explicit + non-claims until PyPI publication. - `REQ-PROOFKIT-PACKAGE-007`: package-public Markdown records release-channel - state only and must not embed exact per-version provider facts that are owned - by immutable registry, release, and manifest artifacts. + state only, excludes source-checkout contributor routes, closes admitted + package references, and must not embed exact per-version provider facts that + are owned by immutable registry, release, and manifest artifacts. ## Non-Claims diff --git a/docs/specs/proofkit-package-boundary/requirements.v1.json b/docs/specs/proofkit-package-boundary/requirements.v1.json index 1079c7d..5acbb25 100644 --- a/docs/specs/proofkit-package-boundary/requirements.v1.json +++ b/docs/specs/proofkit-package-boundary/requirements.v1.json @@ -8,7 +8,7 @@ { "requirementId": "REQ-PROOFKIT-PACKAGE-001", "ownerId": "proofkit.package-boundary", - "invariant": "The package artifact set exposes the supported CLI through one root package with embedded platform binaries while denying root imports, source imports, generated JavaScript imports, and deep internal package paths as public contract.", + "invariant": "The package artifact set exposes the supported CLI through one root package with embedded platform binaries while denying root imports, source imports, generated JavaScript imports, and deep internal package paths as public contract; every exact forbidden root name and suffix is exercised through complete root-package verification, the contract-map decision table retains exactly three cells per row, and each reference admitted through the closed machine-field inventory, bounded Markdown destination grammar, README owner table, or exact README command-navigation statement resolves to a shipped entry unless its exact field is explicitly classified as source-checkout evidence.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": [ @@ -18,7 +18,7 @@ "NC-PROOFKIT-PACKAGE-001" ], "nonClaims": [ - "This requirement does not claim consumer adoption, registry publication, rollout approval, or production readiness." + "This requirement does not claim a complete Markdown parser, discovery of unclassified code-span paths, consumer adoption, registry publication, rollout approval, or production readiness." ], "lifecycle": { "state": "active", @@ -35,7 +35,7 @@ { "requirementId": "REQ-PROOFKIT-PACKAGE-002", "ownerId": "proofkit.package-boundary", - "invariant": "The CLI builds deterministic reports, plans, generated source artifacts, and policy-admission results from explicit caller-owned JSON, declared no-input command parameters, or declared explicit scanner scope classes; explicit scanners consume only caller-named bounded canonical files under documented fail-closed grammars rather than guessing layouts or silently accepting unsupported syntax, and no command executes native witnesses, scans implicit repository state, decides proof freshness, or accepts broad caller-supplied phrase suppressors that can hide readiness overclaims.", + "invariant": "The CLI builds deterministic reports, plans, generated source artifacts, and policy-admission results from explicit caller-owned JSON, declared no-input command parameters, or declared explicit scanner scope classes; one immutable launcher-profile value renders every Proofkit-owned display command and structured argv field in the closed generated-command inventory across root, family, and leaf help, stack preset, bootstrap, project structure, agent route, adoption workflow, and requirement coverage surfaces while preserving caller-owned command bytes; launcher admission accepts only path, offline npm, or absolute-interpreter Python-module routes, rejects Python executable values containing report-visible secret-like, Unicode control, or Unicode format content without disclosing rejected values, and performs no ambient runner autodetection; the authored CLI contract closes every required-input and JSON-output root-shape variant declaration, rejects repeated or mixed pilot selectors instead of applying last-write-wins routing, binds each root-distinct output selector to its exact successful-output test, native source-path set, and requirement scenario, and generates private help and stack-preset projections without replacing native admission; explicit scanners consume only caller-named bounded canonical files under documented fail-closed grammars, pin each referenced package sub-root before reading its manifest and sources, bind every lexical alias of one canonical source route to the immutable identity, digest, and parsed result of its first admission, and reject later alias drift rather than guessing layouts or silently accepting unsupported syntax; explicit loopback browser opening accepts only the exact root URL and dispatches one fixed platform launcher argv; no command executes native witnesses, scans implicit repository state, decides proof freshness, or accepts literal or strict-character-reference-encoded broad caller-supplied phrase suppressors that can hide readiness overclaims.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": [ @@ -45,7 +45,7 @@ "NC-PROOFKIT-PACKAGE-002" ], "nonClaims": [ - "This requirement does not claim native witness execution, producer authentication, proof freshness, or merge approval." + "This requirement does not claim nested CLI field shape, scalar type, collection cardinality, nullability, a complete Markdown parser, native witness execution, installation of source-checkout witness files, runtime JSON Schema validation, producer authentication, proof freshness, or merge approval." ], "lifecycle": { "state": "active", @@ -62,7 +62,7 @@ { "requirementId": "REQ-PROOFKIT-PACKAGE-003", "ownerId": "proofkit.package-boundary", - "invariant": "The root package remains installable and executable by an outside consumer on the current native platform without claiming registry publication.", + "invariant": "The exact local root-package tarball remains installable and executable by an outside consumer on the current native platform through npm exec --offline, with a continuous root-help to family to stack-preset to every exact generated preset continuation and first-valid-input onboarding trace, including successful re-execution of one emitted self-continuation without network fallback, without claiming registry publication.", "claimLevel": "blocking", "riskClass": "medium", "proofBindingRefs": [ @@ -89,7 +89,7 @@ { "requirementId": "REQ-PROOFKIT-PACKAGE-004", "ownerId": "proofkit.package-boundary", - "invariant": "CI package-gate receipts used as merge evidence are admitted through a declared producer policy and proof-receipt shape validator instead of current-build output alone.", + "invariant": "CI package-gate receipts used as merge evidence are admitted through a declared producer policy and proof-receipt shape validator instead of current-build output alone, and every fixed Proofkit subprocess verdict independently rejects process failure, invalid JSON, and any non-passed state.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": [ @@ -116,7 +116,7 @@ { "requirementId": "REQ-PROOFKIT-PACKAGE-005", "ownerId": "proofkit.package-boundary", - "invariant": "Tracked source hygiene covers every admitted project text language in both staged blobs and the current worktree, while Go formatting, static analysis, vet, test, package artifact, and vulnerability gates remain the native merge-critical quality floor and explicitly provision every runtime required by a merge-critical oracle.", + "invariant": "Tracked source hygiene covers every admitted project text language in both staged blobs and the current worktree, matches prohibited organization identifiers only at identifier boundaries so content digests cannot create false positives, while Go formatting, static analysis, vet, test, package artifact, and vulnerability gates remain the native merge-critical quality floor and explicitly provision every runtime required by a merge-critical oracle.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": [ @@ -143,7 +143,7 @@ { "requirementId": "REQ-PROOFKIT-PACKAGE-006", "ownerId": "proofkit.package-boundary", - "invariant": "Python/uv distribution is a platform wheel wrapper over the same Go CLI, with wheel-safe package metadata, wheel tags, embedded binary identity, local install smoke proof, POSIX exec process-identity and signal preservation, and explicit non-claims until PyPI publication.", + "invariant": "Python/uv distribution is a platform wheel wrapper over the same Go CLI, with wheel-safe package metadata, wheel tags, embedded binary identity, exact wheel-set version, uniqueness, presence, and SHA closure, local install smoke proof, POSIX exec process-identity and signal preservation, generated display and argv routes bound to the absolute active interpreter plus -m agentic_proofkit, successful direct-argv re-execution of an exact emitted self-continuation, the complete root-help to family-help to leaf-help chain, and an exact emitted agent-route argv with npm absent from PATH, and a marker-bounded README projection of the owned Python and platform matrix with explicit non-claims until PyPI publication.", "claimLevel": "blocking", "riskClass": "medium", "proofBindingRefs": [ @@ -170,7 +170,7 @@ { "requirementId": "REQ-PROOFKIT-PACKAGE-007", "ownerId": "proofkit.package-boundary", - "invariant": "Package-public Markdown records release-channel completion state only and must not embed exact package versions, source refs, provider run URLs, registry tarball URLs, integrity strings, shasums, or other per-version release facts that are owned by immutable registry records, GitHub Release artifacts, and generated release manifests.", + "invariant": "Package-public Markdown records release-channel completion state only, excludes contributor-only governance and backlog routes from the npm artifact, closes Markdown and admitted machine references over shipped entries with explicit source-checkout evidence exceptions, and must not embed exact package versions, source refs, provider run URLs, registry tarball URLs, integrity strings, shasums, or other per-version release facts that are owned by immutable registry records, GitHub Release artifacts, and generated release manifests.", "claimLevel": "blocking", "riskClass": "medium", "proofBindingRefs": [ diff --git a/docs/specs/proofkit-spec-proof-core/overview.md b/docs/specs/proofkit-spec-proof-core/overview.md index e9cf8ce..31b70c5 100644 --- a/docs/specs/proofkit-spec-proof-core/overview.md +++ b/docs/specs/proofkit-spec-proof-core/overview.md @@ -13,7 +13,9 @@ execution receipts, and merge policy. - `REQ-PROOFKIT-SPEC-001`: requirement source admission validates structured `REQ-*` records and source-package shape without owning requirement meaning - or scanning overview prose as authority. + or scanning overview prose as authority; one shipped, marker-bounded example + is parsed as bounded expansion-free literal shell words and executed through + the installed current product as a first valid input. - `REQ-PROOFKIT-SPEC-002`: requirement proof binding reports validate caller-owned requirement-to-witness mappings, require compact scenarios to be admitted `surface_id::stable_anchor` identities, require compact witness @@ -58,9 +60,11 @@ execution receipts, and merge policy. reading sources, rendering views, or owning requirement/proof semantics. - `REQ-PROOFKIT-SPEC-009`: requirement spec tree views render admitted caller-owned hierarchy through shared safe browser document fragments, - deterministic CLI JSON, Markdown, HTML, explicit output paths, and loopback - browser serving without accepting caller-owned raw HTML or making rendered - output authoritative. + deterministic CLI JSON, Markdown, HTML, repository-confined same-parent + atomic output replacement after final destination-parent plus + temporary-object identity, mode, and content admission, and loopback browser + serving with one exact supported-view vocabulary without accepting + caller-owned raw HTML or making rendered output authoritative. - `REQ-PROOFKIT-SPEC-010`: requirement impact input composition converts caller-owned base/current requirement sources, single-current-binding compact proof contracts, changed-path facts, generated-artifact policy, local @@ -69,8 +73,11 @@ execution receipts, and merge policy. impact evaluator. - `REQ-PROOFKIT-SPEC-011`: adoption contract envelope admission validates a complete caller-owned aggregate adoption envelope, selects one child route - through orthogonal CLI flags, and delegates to existing child command - contracts without becoming a second adoption readiness policy. + through orthogonal CLI flags, rejects repeated single-value mode or pilot + selectors and an explicitly empty pilot value, binds its union output to the + exact successful public-CLI root witness and native owner, and delegates to + existing child command contracts without becoming a second adoption readiness + policy. - `REQ-PROOFKIT-SPEC-012`: requirement authoring plans package caller-provided design, implementation, PR, code, test, and clarification facts into candidate-only requirement updates, delegate structural checks to requirement @@ -94,27 +101,32 @@ execution receipts, and merge policy. and emits only bounded candidate requirements, bindings, or owner guidance. - `REQ-PROOFKIT-SPEC-018`: an authored command-family catalog covers every public CLI command exactly once, deterministically generates the private - runtime navigation projection, and adds opt-in family help while preserving - existing help invocation forms, process channels, no-input behavior, and leaf - dispatch; descriptor and help truth remains owned by `REQ-PROOFKIT-QUALITY-004`. + runtime navigation projection, adds token-bounded root discovery, and projects + exact stack-preset IDs into help and diagnostics while preserving existing + help invocation forms, process channels, no-input behavior, and leaf dispatch; + descriptor and help truth remains owned by `REQ-PROOFKIT-QUALITY-004`. - `REQ-PROOFKIT-SPEC-019`: explicit catalogs compose content-bound semantic - context snapshots through existing source, tree, proof, and coverage owners - without ambient repository discovery. + schema-v2 context snapshots with exact expected-digest coverage through + existing source, tree, proof, and coverage owners, with strict v1 adaptation + and without ambient repository discovery. - `REQ-PROOFKIT-SPEC-020`: bounded context queries select parent-before-child, role-aware, reference-closed semantic subsets by stable identity and report each active bound without treating bounded absence as source absence. - `REQ-PROOFKIT-SPEC-021`: the loopback workspace progressively presents immutable context, semantic diff, traceability trust states, authority - boundaries, and non-claims, then emits a bounded source-bound question packet - only after explicit user submission. + boundaries, one exact loopback root URL through one fixed platform launcher + argv, visible loading and sanitized terminal failure states through native + document semantics, and non-claims, then emits a bounded source-bound question + packet from a visibly labelled region only after explicit user submission. - `REQ-PROOFKIT-SPEC-022`: semantic diff compares admitted requirement fields - by owner-declared scalar, set, map, and lifecycle semantics, covers entity - additions and removals, and remains closed under output re-admission. + by owner-declared scalar, set, map, and lifecycle semantics through schema-v2 + records and strict v1 adapters, covers entity additions and removals, and + remains closed under output re-admission. - `REQ-PROOFKIT-SPEC-023`: traceability graph input schema v2 preserves specification, proof, code traceability, and native execution as distinct - evidence planes and accepts code topology only as explicit caller-owned input - with source-digest, parent-edge, abstraction-order, and pre-materialization - budget closure. + evidence planes, consumes the normalized v1/v2 context boundary, and accepts + code topology only as explicit caller-owned input with source-digest, + parent-edge, abstraction-order, and pre-materialization budget closure. ## Non-Claims diff --git a/docs/specs/proofkit-spec-proof-core/requirements.v1.json b/docs/specs/proofkit-spec-proof-core/requirements.v1.json index 2f96344..3da0d43 100644 --- a/docs/specs/proofkit-spec-proof-core/requirements.v1.json +++ b/docs/specs/proofkit-spec-proof-core/requirements.v1.json @@ -8,7 +8,7 @@ { "requirementId": "REQ-PROOFKIT-SPEC-001", "ownerId": "proofkit.spec-proof-core", - "invariant": "Requirement source admission validates structured REQ-* records and source-package shape without owning requirement meaning or scanning overview prose as authority.", + "invariant": "Requirement source admission validates structured REQ-* records and source-package shape without owning requirement meaning or scanning overview prose as authority; the shipped README routes one marker-bounded exact offline command and exactly one JSON example through the installed current product as a first valid input, using bounded expansion-free literal shell-word semantics so safe Bash quoting and directly executed argv remain equivalent.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": [ @@ -224,7 +224,7 @@ { "requirementId": "REQ-PROOFKIT-SPEC-009", "ownerId": "proofkit.spec-proof-core", - "invariant": "Requirement spec tree views render admitted caller-owned spec-tree hierarchy through shared safe browser document fragments, deterministic CLI JSON, Markdown, HTML, explicit output paths, and loopback browser serving without accepting caller-owned raw HTML or making rendered output authoritative.", + "invariant": "Requirement spec tree views render admitted caller-owned spec-tree hierarchy through shared safe browser document fragments, deterministic CLI JSON, Markdown, HTML, explicit output paths through repository-confined same-parent atomic replacement after final destination-parent plus temporary-object identity, mode, and content admission, and loopback browser serving without accepting caller-owned raw HTML or making rendered output authoritative; the browser command and runtime share one exact supported-view vocabulary, and invalid-view diagnostics list all and only those views.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": [ @@ -234,7 +234,7 @@ "NC-PROOFKIT-SPEC-009" ], "nonClaims": [ - "This requirement does not claim repository scanning, source freshness computation from files, requirement meaning, native witness execution, committed generated HTML, merge approval, release approval, rollout approval, or production readiness." + "This requirement does not claim repository scanning, source freshness computation from files, requirement meaning, native witness execution, protection from adversarial concurrent content or namespace mutation by the same operating-system user during the operation, filesystem crash or fsync durability, committed generated HTML, merge approval, release approval, rollout approval, or production readiness." ], "lifecycle": { "state": "active", @@ -278,7 +278,7 @@ { "requirementId": "REQ-PROOFKIT-SPEC-011", "ownerId": "proofkit.spec-proof-core", - "invariant": "Adoption contract envelope admission validates a complete caller-owned aggregate adoption envelope, selects one child adoption, bootstrap, guidance, workflow, or pilot route through orthogonal CLI flags, and delegates to existing child command contracts without scanning repositories, writing files, executing witnesses, or creating a second adoption readiness policy.", + "invariant": "Adoption contract envelope admission validates a complete caller-owned aggregate adoption envelope, selects one child adoption, bootstrap, guidance, workflow, or pilot route through orthogonal CLI flags, rejects repeated single-value mode or pilot selectors and an explicitly empty pilot value rather than changing or misreporting the selected root variant, binds its union output contract to the exact successful public-CLI root-shape witness and native source owner, and delegates to existing child command contracts without scanning repositories, writing files, executing witnesses, or creating a second adoption readiness policy.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": [ @@ -467,7 +467,7 @@ { "requirementId": "REQ-PROOFKIT-SPEC-018", "ownerId": "proofkit.spec-proof-core", - "invariant": "Command family navigation projects the complete public CLI command inventory from one authored family catalog into a deterministic private runtime projection, preserves root and per-command help invocation forms, stdout and stderr channel discipline, no-input behavior, and leaf dispatch, admits only opt-in family help forms, rejects missing, duplicated, oversized, unsorted, or reserved-operand-colliding families, and does not infer command selection, execute commands through family membership, or create a second command inventory owner.", + "invariant": "Command family navigation projects the complete public CLI command inventory from one authored family catalog into a deterministic private runtime projection, preserves root and per-command help invocation forms, stdout and stderr channel discipline, no-input behavior, and leaf dispatch, exposes one token-bounded root discovery route, and projects the exact stack-preset vocabulary from the authored CLI contract into generated runtime IDs, direct help, and invalid-ID diagnostics; it admits only opt-in family help forms, rejects missing, duplicated, oversized, unsorted, or reserved-operand-colliding families, and does not infer command selection, execute commands through family membership, or create a second command inventory owner.", "claimLevel": "blocking", "riskClass": "medium", "proofBindingRefs": [ @@ -494,12 +494,12 @@ { "requirementId": "REQ-PROOFKIT-SPEC-019", "ownerId": "proofkit.spec-proof-core", - "invariant": "Requirement context composition reads only caller-selected repository-relative catalog paths through a confined filesystem root, admits each source through its semantic owner, records current and optional expected digests, and emits one bounded content-bound derived snapshot without ambient scanning or new requirement, proof, coverage, freshness, or merge authority.", + "invariant": "Requirement context composition reads only caller-selected repository-relative catalog paths through a confined filesystem root, admits each source through its semantic owner, records current and optional expected digests, and emits one bounded content-bound schema-v2 derived snapshot whose expectedDigestCoverage is exactly none, partial, or all; the strict schema-v1 adapter fully admits the legacy record before mapping it to the same semantic snapshot, mixed v1/v2 vocabulary is rejected, and no producer emits v1.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], "nonClaimRefs": ["NC-PROOFKIT-SPEC-019"], - "nonClaims": ["This requirement does not claim ambient repository discovery, later-checkout freshness, requirement meaning, proof adequacy, native witness execution, merge approval, release approval, rollout approval, or production readiness."], + "nonClaims": ["Expected-digest coverage records only equality between caller-supplied expected digests and admitted current bytes; it does not authenticate a producer, baseline, checkout, or freshness and does not claim ambient repository discovery, requirement meaning, proof adequacy, native witness execution, merge approval, release approval, rollout approval, or production readiness."], "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, "deferral": null, "updatePolicy": {"reviewOwnerId": "proofkit.spec-proof-core", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} @@ -520,12 +520,12 @@ { "requirementId": "REQ-PROOFKIT-SPEC-021", "ownerId": "proofkit.spec-proof-core", - "invariant": "The loopback requirement workspace visibly preserves presentation-only authority boundaries, non-claims, snapshot and baseline identity, and trust-significant graph states while rendering immutable admitted context, optional semantic diff, and optional traceability graph through exact allowlisted routes, a per-process capability, same-origin handoff admission, source-digest and JSON-pointer anchors, Unicode code-point quote coordinates, bounded browser-session question packets after explicit UI submission across the admitted tree domain subject to explicit packet byte bounds, and terminal one-shot concurrent session semantics without persistence, command execution, provider access, or authority promotion.", + "invariant": "The loopback requirement workspace visibly preserves presentation-only authority boundaries, non-claims, snapshot identity, schema-v2 expected-digest coverage, and trust-significant graph states while rendering immutable admitted context, optional schema-v2 semantic diff, and optional traceability graph through exact allowlisted routes and schema-v2 manifest and affected API projections, a per-process capability removed from the document before API use, one exact loopback root URL passed to one fixed platform launcher argv, visible bootstrap loading and sanitized terminal failure states with stable identities, native list and article semantics, active-view aria-current, a visibly headed handoff region, same-origin handoff admission, source-digest and JSON-pointer anchors, Unicode code-point quote coordinates, bounded browser-session question packets after explicit UI submission across the admitted list domain subject to explicit packet byte bounds, and terminal one-shot concurrent session semantics without persistence, caller-supplied command execution, native-witness execution, provider access, or authority promotion; the only local-process side effect is the fixed launcher above.", "claimLevel": "blocking", "riskClass": "critical", "proofBindingRefs": ["proofkit/requirement-bindings.json"], "nonClaimRefs": ["NC-PROOFKIT-SPEC-021"], - "nonClaims": ["This requirement does not authenticate the surrounding browser profile or operating-system user and does not claim remote access, annotation persistence, agent execution, provider delivery, proof freshness, merge approval, release approval, rollout approval, branded Safari behavior, or production readiness."], + "nonClaims": ["Expected-digest coverage does not authenticate a producer, baseline, checkout, or freshness; successful launcher invocation does not prove browser rendering; this requirement also does not authenticate the surrounding browser profile or operating-system user and does not claim remote access, annotation persistence, agent execution, provider delivery, complete screen-reader interoperability, full WCAG 2.2 conformance, every operating-system theme, 400-percent zoom behavior, merge approval, release approval, rollout approval, branded Safari behavior, or production readiness."], "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, "deferral": null, "updatePolicy": {"reviewOwnerId": "proofkit.spec-proof-core", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} @@ -533,12 +533,12 @@ { "requirementId": "REQ-PROOFKIT-SPEC-022", "ownerId": "proofkit.spec-proof-core", - "invariant": "Requirement semantic diff compares two admitted context snapshots by stable requirement identity and owner-declared scalar, set, map, and lifecycle field classes, producing canonically ordered added, removed, and modified facts that remain closed under output re-admission without textual diff, inferred move semantics, or authority over requirement meaning.", + "invariant": "Requirement semantic diff schema-v2 input compares two fully admitted normalized context snapshots by stable requirement identity and owner-declared scalar, set, map, and lifecycle field classes, producing a schema-v2 canonically ordered output with base and current expected-digest coverage that remains closed under output re-admission; strict schema-v1 input and output adapters fully admit legacy records before normalization and reject mixed-version vocabulary.", "claimLevel": "blocking", "riskClass": "medium", "proofBindingRefs": ["proofkit/requirement-bindings.json"], "nonClaimRefs": ["NC-PROOFKIT-SPEC-022"], - "nonClaims": ["This requirement does not claim textual equivalence, inferred moves, product meaning, proof freshness, merge approval, release approval, rollout approval, or production readiness."], + "nonClaims": ["Expected-digest coverage does not authenticate a producer, baseline, checkout, or freshness; this requirement also does not claim textual equivalence, inferred moves, product meaning, merge approval, release approval, rollout approval, or production readiness."], "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, "deferral": null, "updatePolicy": {"reviewOwnerId": "proofkit.spec-proof-core", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} @@ -546,12 +546,12 @@ { "requirementId": "REQ-PROOFKIT-SPEC-023", "ownerId": "proofkit.spec-proof-core", - "invariant": "Requirement traceability graph input schema v2 deterministically projects admitted specification and proof topology plus explicit caller-owned code topology while preserving specification, proof, code traceability, and native execution as distinct evidence planes; one typed topology validator rejects dangling endpoints, enforces the bijection between non-root parentNodeId facts and contains edges with broader parent abstraction, verifies every supplied code-source digest, and applies node, edge, and source-byte budgets before per-item semantic materialization.", + "invariant": "Requirement traceability graph input schema v2 deterministically consumes a fully admitted normalized context snapshot, including strict schema-v1 adaptation or native schema-v2 expected-digest coverage, and projects admitted specification and proof topology plus explicit caller-owned code topology while preserving specification, proof, code traceability, and native execution as distinct evidence planes; one typed topology validator rejects dangling endpoints, enforces the bijection between non-root parentNodeId facts and contains edges with broader parent abstraction, verifies every supplied code-source digest, and applies node, edge, and source-byte budgets before per-item semantic materialization.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], "nonClaimRefs": ["NC-PROOFKIT-SPEC-023"], - "nonClaims": ["This requirement does not infer code topology, line or branch coverage, native execution, proof freshness, merge approval, release approval, rollout approval, or production readiness."], + "nonClaims": ["Expected-digest coverage does not authenticate a producer, baseline, checkout, or freshness; this requirement also does not infer code topology, line or branch coverage, native execution, merge approval, release approval, rollout approval, or production readiness."], "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, "deferral": null, "updatePolicy": {"reviewOwnerId": "proofkit.spec-proof-core", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} diff --git a/docs/specs/proofkit-supply-chain-quality/overview.md b/docs/specs/proofkit-supply-chain-quality/overview.md index 18cb715..ebcadaa 100644 --- a/docs/specs/proofkit-supply-chain-quality/overview.md +++ b/docs/specs/proofkit-supply-chain-quality/overview.md @@ -15,25 +15,44 @@ vulnerability absence, or consumer rollout safety by itself. inventory and optional GitHub artifact attestations without claiming public-source provenance before a public-source tag release exists. - `REQ-PROOFKIT-QUALITY-002`: release evidence includes a deterministic SBOM - for package, wheel, binary, and Go module inventory without claiming - vulnerability absence or license approval. + whose runtime edges and digest come from one immutable byte snapshot of the + exact owning binary through an identity-checked pinned handle with a final + same-handle content check; source/tool module inventory is excluded and + package or wheel representations invent no runtime edges. - `REQ-PROOFKIT-QUALITY-003`: pure parser and canonicalization boundaries have fuzz/property tests that prove no-panic and round-trip stability without fuzzing side-effecting CLI or filesystem flows. - `REQ-PROOFKIT-QUALITY-004`: public CLI ABI has a small golden corpus and canonical ABI hash covering command topology, exit code, stdout/stderr - channel discipline, JSON parseability, stable public diagnostics, explicit - output schema evolution for breaking JSON field changes, and - descriptor/contract/help parity without making private descriptors public API. + channel discipline, JSON parseability, stable public diagnostics, complete + versioned root-shape input/output variants, exact source-checkout selectors, + exact native source sets and requirement scenarios for root-distinct output + witnesses, generated help and preset projections, explicit root schema + evolution, and descriptor/contract/help parity without making private + descriptors public API. The admitted adoption-envelope machine condition + model uses complete + canonical flag conjunctions, proves finite valid-option closure against + native admission, binds exercised argv to one exact condition and variant, + and rejects repeated mode or pilot selectors plus an empty pilot value that + would normalize to absence. Any later condition-model owner requires its own + admitted native-closure witness. The separate pilot-admission route declares + its contract-envelope stack-diverse alias and rejects repeated or mixed + `--pilot` and `--stack-diverse` selectors instead of applying last-write-wins + routing. Nested fields, leaf types, cardinality, + nullability, and cross-field semantics remain native-command concerns rather + than machine-contract claims. - `REQ-PROOFKIT-QUALITY-005`: CodeQL workflow source is admitted as an - independent semantic security analysis signal for Go source without replacing - native Go static gates. + independent semantic security analysis signal for Go source; advisory + analysis remains read-only, provider publication is isolated and disabled on + pull requests, and native Go static gates remain required. - `REQ-PROOFKIT-QUALITY-006`: OSV workflow source is admitted as an advisory - multi-ecosystem dependency signal without replacing `govulncheck` - reachable-code evidence. + multi-ecosystem dependency signal; advisory analysis remains read-only, + provider publication is isolated and disabled on pull requests, and + `govulncheck` reachable-code evidence remains required. - `REQ-PROOFKIT-QUALITY-007`: Scorecard workflow source is admitted as an - advisory repository hygiene signal without claiming branch protection or - provider settings that the repository cannot prove from source. + advisory repository hygiene signal; advisory analysis remains read-only, + public publication authority is isolated with exact output inputs, and + source does not claim branch protection or unobserved provider settings. - `REQ-PROOFKIT-QUALITY-008`: GitHub Actions workflow syntax and expression semantics are checked by actionlint in the local package gate. - `REQ-PROOFKIT-QUALITY-009`: performance-sensitive parser and serializer @@ -41,23 +60,42 @@ vulnerability absence, or consumer rollout safety by itself. required PR gate before stable baselines exist. - `REQ-PROOFKIT-QUALITY-010`: coverage metrics report requirement, binding, witness, CLI inventory linkage, and descriptor-owned command proof-route - candidates from admitted test-evidence-inventory rows. The gate fails closed - on linkage dead zones, failed inventory admission, missing candidates, - unknown command refs, contract-only commands, or route-only commands. Static - route metadata and source syntax never become semantic falsifier evidence. + candidates from admitted test-evidence-inventory rows, while critical + anti-vacuity scenarios retain exact closed selector inventories. Each linkage and route + conjunct has an independent fail-closed falsifier, and each source-checkout + selector resolves to a valid function in an active Go test file with its + exact executable command; static route metadata, prose, source markers, test + existence, and failure-capable syntax never become semantic falsifier + evidence. - `REQ-PROOFKIT-QUALITY-011`: CI separates the OS-independent full source/package gate from macOS platform smoke, executes the complete Go package set through its owner command, uses explicit hosted runner labels instead of floating latest labels, and exposes one fail-closed aggregate gate - that requires every required leaf check to finish with success. + whose exact expression and script reject neutralization and require every + required leaf check to finish with success, while the aggregate job omits + job defaults and `continue-on-error` entirely. Exact workflow-level bash + defaults plus a closed inventory require exact provider-check names and + scalar runners, bind the macOS job's presence-aware exact ordered step + inventory and exact local setup-action bytes to the fail-closed + package-script owner command, require every required leaf job and step to + omit conditions except for exact named owner conditions, and reject + dynamically false or explicit-null conditions, dual `run`/`uses` keys, + non-string runner labels, nested or top-level semantic shadow steps, + reusable-job substitution, inherited or local environment entries, shell, + working-directory, and `continue-on-error` overrides. - `REQ-PROOFKIT-QUALITY-012`: release and adoption channel identifiers use one canonical authority vocabulary that separates durable authority channels from display labels, publisher environments, statuses, and candidate evidence. - `REQ-PROOFKIT-QUALITY-013`: self-hosting workflow package-gate evidence is checked by a typed workflow oracle instead of text search, proving the - package gate is reachable, exact, advisory, success-gated for always-running - downstream jobs and required evidence-publication steps, and ordered before - evidence publication. + package gate is reachable, exact, advisory, and guarded only by absent or + exact owner-reviewed conditions for every trust-significant job and step + across the closed required CI and release inventories; dynamically false and + neutralized expressions are rejected alongside unsafe workflow run defaults, + unexpected workflow or step environment entries, gate-job defaults or + environment entries, and shell, working-directory, or + `continue-on-error` override on any step in the gate job; the gate remains + ordered before evidence publication. - `REQ-PROOFKIT-QUALITY-014`: release authority consumers compare downstream policy against the admitted typed release-authority projection and admitted report digest instead of rereading caller-owned raw `releaseAuthorityInput` @@ -68,8 +106,8 @@ vulnerability absence, or consumer rollout safety by itself. - `REQ-PROOFKIT-QUALITY-016`: release platform targets use one private owner that projects platform suffixes, Go build targets, npm OS/CPU metadata, package tar entries, Python wheel tags, PyPI candidate completeness, - self-hosting native binary selection, and SBOM binary subjects without - becoming public API. + self-hosting native binary selection, SBOM binary subjects, and the exact + marker-bounded README platform matrix without becoming public API. - `REQ-PROOFKIT-QUALITY-017`: report-visible secret-shaped JSON traversal uses one private kernel owner for deterministic paths and finding kinds while command packages only map findings to their local report policy. @@ -78,7 +116,13 @@ vulnerability absence, or consumer rollout safety by itself. npm and PyPI channels and release closeout rejects publication claims without that tuple. - `REQ-PROOFKIT-QUALITY-019`: installed package smoke proof verifies one - successful JSON report command, one failed-report command, and the current + continuous offline route through every displayed family and leaf-help + transition, binds each installed invocation to its ordered exact bare Usage + command token, requires every exact generated preset command to retain the + offline npm prefix, re-executes one emitted continuation, reaches the first + valid README input, executes one successful JSON report command and one + failed-report command, applies bounded expansion-free literal parsing to + emitted and README argv, and verifies the current `json-report-cli-adapter-source` generated source artifact from the package-managed binary, including report identity, state, exit code, stdout, stderr discipline, generated-source hash, owner-source parity, and exact @@ -96,16 +140,36 @@ vulnerability absence, or consumer rollout safety by itself. - `REQ-PROOFKIT-QUALITY-022`: the requirement workspace uses an explicit embedded asset set, strict authored-JavaScript type checking, exact secured routes, bounded server cleanup, repository-confined non-symlink proof - artifacts, and machine-admitted per-project rendered engine evidence without - runtime dependencies or a production bundler. + artifacts, and machine-admitted per-project rendered engine evidence for its + stable-state accessibility matrix, including independently held bootstrap, + specifications, diff, and graph loading states, target size, 320-pixel + reflow, labelled internal overflow, and light/dark rendered contrast without + runtime dependencies or a production bundler; one direct audit initializes + pinned axe code through the browser-context script channel, admits only the + main frame and exact engine identity, preserves same-origin and Playwright + branding, and runs default rules plus explicitly enabled target size, while + failed attempts retain attempt-scoped action, DOM, network, and source traces + without continuous screenshot capture plus a bounded best-effort failure + screenshot, without creating passed proof. - `REQ-PROOFKIT-QUALITY-023`: Python wheels independently bind advertised - platform compatibility to decoded executable bytes and carry Core Metadata - 2.4 plus an exact, RECORD-closed repository license payload. -- `REQ-PROOFKIT-QUALITY-024`: a version-bound machine record declares the - reviewed public-contract change set and owns release-note content, while one - retained-evidence owner builds and verifies checksums against exact + platform compatibility to decoded executable bytes, and the wheel set rejects + version, identity, presence, and digest drift; wheels carry Core Metadata 2.4 + plus an exact, RECORD-closed repository license payload. +- `REQ-PROOFKIT-QUALITY-024`: a closed version-bound machine record binds the + exact previous/current SemVer pair and compatible/breaking class, declares the + exact complete current breaking, addition, and migration inventories + including channel-specific generated continuation bytes, rejects missing, + substituted, reordered, or surplus entries, and owns one independently + authored byte-exact complete current release-note projection, while one + retained- + evidence owner builds and verifies checksums against exact downloadable artifact-relative paths without inferring change completeness from the source diff. +- `REQ-PROOFKIT-QUALITY-025`: exact workflow inventory, closed raw-YAML keys + before typed decoding, exact release deployment environments, pinned action + commit identities, admitted status expressions, the required aggregate shell + program, and read-only treatment of an existing release are source-oracle + invariants. ## Non-Claims diff --git a/docs/specs/proofkit-supply-chain-quality/requirements.v1.json b/docs/specs/proofkit-supply-chain-quality/requirements.v1.json index c821471..df5c6d2 100644 --- a/docs/specs/proofkit-supply-chain-quality/requirements.v1.json +++ b/docs/specs/proofkit-supply-chain-quality/requirements.v1.json @@ -25,7 +25,7 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-002", "ownerId": "proofkit.supply-chain-quality", - "invariant": "Release evidence includes a deterministic SBOM for package, wheel, binary, and Go module inventory without claiming vulnerability absence or license approval.", + "invariant": "Release evidence includes a deterministic SBOM in which source and tool Go module inventory is explicitly excluded, package and wheel representations invent no runtime edges, and each required runtime dependency edge and subject digest are derived from one immutable byte snapshot read through an identity-checked pinned handle to the exact release binary that owns the edge, with a final same-handle content check, without claiming reachability, vulnerability absence, or license approval.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -51,12 +51,12 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-004", "ownerId": "proofkit.supply-chain-quality", - "invariant": "Public CLI ABI has a small golden corpus and canonical ABI hash covering command topology, exit code, stdout and stderr channel discipline, JSON parseability, stable public diagnostics, explicit output schema evolution for breaking JSON field changes, and descriptor/contract/help parity without making private descriptors a public API.", + "invariant": "Public CLI ABI has a small golden corpus and canonical ABI hash covering command topology, exit code, stdout and stderr channel discipline, JSON parseability, stable public diagnostics, complete versioned root-shape input and JSON-output variant declarations, exact source-checkout witness selectors, exact native source-path sets and requirement scenarios for every root-distinct output witness, deterministic generated help and stack-preset projections, explicit output schema evolution for breaking JSON root changes, and descriptor/contract/help parity without making private descriptors a public API; the admitted adoption-contract-envelope machine condition model uses canonical complete flag conjunctions, rejects ambiguous or incomplete assignments, closes the finite valid option space against native option admission, binds each exercised argv to its exact condition and variant, and rejects repeated mode or pilot flags or an empty pilot value rather than silently changing or misreporting the selected variant, while any later condition-model owner requires its own admitted native-closure witness; the separate pilot-admission route declares its contract-envelope stack-diverse alias and rejects repeated or mixed --pilot and --stack-diverse selectors instead of applying last-write-wins routing.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], "nonClaimRefs": ["NC-PROOFKIT-QUALITY-004"], - "nonClaims": ["This requirement does not claim exhaustive command semantic completeness beyond the declared route inventory or byte-for-byte stability for diagnostics outside the public CLI ABI."], + "nonClaims": ["This requirement does not claim nested field shape, scalar type, collection cardinality, nullability, exhaustive command semantic completeness beyond the declared route inventory, installation of source-checkout witness files, runtime JSON Schema validation, or byte-for-byte stability for diagnostics outside the public CLI ABI."], "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, "deferral": null, "updatePolicy": {"reviewOwnerId": "proofkit.supply-chain-quality", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} @@ -64,7 +64,7 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-005", "ownerId": "proofkit.supply-chain-quality", - "invariant": "CodeQL workflow source is admitted as an independent semantic security analysis signal for Go source without replacing native Go static gates.", + "invariant": "CodeQL workflow source is admitted as an independent semantic security analysis signal for Go source; advisory analysis remains read-only, provider publication authority is isolated and disabled on pull requests, and native Go static gates remain required.", "claimLevel": "blocking", "riskClass": "medium", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -77,7 +77,7 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-006", "ownerId": "proofkit.supply-chain-quality", - "invariant": "OSV workflow source is admitted as an advisory multi-ecosystem dependency signal without replacing govulncheck reachable-code evidence.", + "invariant": "OSV workflow source is admitted as an advisory multi-ecosystem dependency signal; advisory analysis remains read-only, provider publication authority is isolated and disabled on pull requests, and govulncheck reachable-code evidence remains required.", "claimLevel": "blocking", "riskClass": "medium", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -90,7 +90,7 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-007", "ownerId": "proofkit.supply-chain-quality", - "invariant": "Scorecard workflow source is admitted as an advisory repository hygiene signal without claiming branch protection or provider settings that the repository cannot prove from source.", + "invariant": "Scorecard workflow source is admitted as an advisory repository hygiene signal; advisory analysis remains read-only, public publication authority is isolated with exact output inputs, and source does not claim branch protection or unobserved provider settings.", "claimLevel": "blocking", "riskClass": "medium", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -129,7 +129,7 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-010", "ownerId": "proofkit.supply-chain-quality", - "invariant": "Coverage metrics report requirement, binding, witness, CLI inventory linkage, and descriptor-owned command proof-route candidates from admitted test-evidence-inventory rows, failing closed on explicit requirement/proof linkage dead zones, failed command-route inventory admission, missing candidate routes, unknown command refs, contract-only commands, or route-only commands; static route metadata, prose, source markers, test existence, and failure-capable syntax remain candidate evidence and cannot satisfy semantic_falsifier coverage.", + "invariant": "Coverage metrics report requirement, binding, witness, CLI inventory linkage, and descriptor-owned command proof-route candidates from admitted test-evidence-inventory rows, failing closed independently for each requirement/proof linkage dead-zone class and each missing-candidate, unknown-candidate-ref, unknown-semantic-ref, contract-only, or route-only command-route class; source-checkout witness selectors must resolve to valid functions in active Go test files with exact executable commands, critical anti-vacuity scenarios must retain their exact closed selector inventories, and failed command-route inventory admission also fails closed, while static route metadata, prose, source markers, test existence, and failure-capable syntax remain candidate evidence and cannot satisfy semantic_falsifier coverage.", "claimLevel": "blocking", "riskClass": "medium", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -142,7 +142,7 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-011", "ownerId": "proofkit.supply-chain-quality", - "invariant": "CI separates the OS-independent full source/package gate from the platform-specific macOS execution smoke, executes the complete Go package set through the owner command rather than a partial package list, uses explicit hosted runner labels instead of floating latest labels, and exposes one fail-closed aggregate quality gate that requires every required leaf check to finish with success so skipped, failed, or cancelled leaf jobs cannot satisfy branch-protection evidence.", + "invariant": "CI separates the OS-independent full source/package gate from the platform-specific macOS execution smoke, executes the complete Go package set through the owner command rather than a partial package list, uses exact scalar hosted runners and provider-check names, requires the macOS job's presence-aware exact ordered step inventory and exact repository-local setup-action bytes to execute the exact fail-closed platform-smoke package-script owner command, and exposes one exact fail-closed aggregate quality gate under exact bash workflow run defaults with no inherited working-directory or environment entries; a closed inventory requires every required leaf job and step to omit conditions except for exact owner-reviewed named conditions, every required leaf job and the aggregate job omit job defaults, reusable-workflow substitution, and continue-on-error and have no job environment entries, all of their steps omit shell, working-directory, and continue-on-error and have no step environment entries, and the aggregate's reviewed expression and script require every exact required leaf check to finish with success; neutralizing wrappers, dynamically false or explicit-null conditions, execution overrides, dual run/uses keys, extra disjunctions, non-string runner labels, nested or top-level semantic shadow steps, no-op or reusable-job substitution, skipped, failed, or cancelled leaf jobs cannot satisfy branch-protection evidence.", "claimLevel": "blocking", "riskClass": "medium", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -168,7 +168,7 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-013", "ownerId": "proofkit.supply-chain-quality", - "invariant": "Self-hosting workflow package-gate evidence is checked by a typed workflow oracle that proves the package gate is reachable, exact, advisory, success-gated for always-running downstream jobs and required evidence-publication steps, and ordered before evidence publication instead of relying on workflow text search.", + "invariant": "Self-hosting workflow package-gate evidence is checked by a typed workflow oracle that proves the package gate is reachable, exact, advisory, and guarded only by an absent condition or the exact owner-reviewed condition for every trust-significant job and step; the oracle checks the complete required CI and release job inventories, rejects neutralizing or dynamically false expression variants, requires any workflow run defaults to retain exact bash without a working-directory override, admits only the exact owner-reviewed workflow and step environment entries, requires the gate job to omit job defaults and continue-on-error and have no job environment entries, requires every step in that job to omit shell, working-directory, and continue-on-error, and orders the gate before evidence publication instead of relying on workflow text search.", "claimLevel": "blocking", "riskClass": "medium", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -207,7 +207,7 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-016", "ownerId": "proofkit.supply-chain-quality", - "invariant": "Release platform targets have one private owner that projects exact platform suffixes, Go build targets, npm OS/CPU metadata, package tar binary entries, Python wheel tags, PyPI candidate completeness, self-hosting native binary selection, and SBOM binary subjects without turning the platform matrix into public API.", + "invariant": "Release platform targets have one private owner that projects exact platform suffixes, Go build targets, npm OS/CPU metadata, package tar binary entries, Python wheel tags, PyPI candidate completeness, self-hosting native binary selection, SBOM binary subjects, and the marker-bounded README platform and architecture matrix in both directions without turning the platform matrix into public API.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -246,7 +246,7 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-019", "ownerId": "proofkit.supply-chain-quality", - "invariant": "Installed package smoke proof verifies the package-managed binary can process explicit JSON input for one successful report command and one failed-report command, and can emit the current json-report-cli-adapter-source artifact from the installed package binary with source bytes equal to the generator owner source, preserving report kind, report id, state, explicit input counts, exit code, stdout JSON, generated-source hash, and stderr channel discipline without admitting unlisted consumer working-directory files in addition to help discoverability.", + "invariant": "Installed package smoke proof builds the current product, installs the exact local tarball, and verifies a continuous npm exec --offline route from root help through every displayed family and leaf-help transition, with each installed invocation exactly bound to its ordered bare Usage command token; it then verifies exact stack-preset vocabulary, requires every exact generated preset command string to retain the offline npm prefix, re-executes one emitted self-continuation, and continues to exactly one marker-bounded README command and JSON first input, with every emitted or README argv admitted through bounded expansion-free literal shell-word semantics before direct execution. It also verifies that the package-managed binary can process explicit JSON input for one successful report command and one failed-report command and emit the current json-report-cli-adapter-source artifact with source bytes equal to the generator owner source, preserving report kind, report id, state, explicit input counts, exit code, stdout JSON, generated-source hash, and stderr channel discipline without admitting unlisted consumer working-directory files.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -285,12 +285,12 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-022", "ownerId": "proofkit.supply-chain-quality", - "invariant": "Authored requirement workspace JavaScript and CSS are embedded through an explicit compile-time asset set, served only by exact routes with restrictive response headers, strictly type-checked without a production bundler, and covered by Go lifecycle, concurrent terminal-state, HTTP security, and repository-confined non-symlink artifact falsifiers plus a machine-admitted per-project execution report proving the same non-empty passed test identities in pinned Chromium, Firefox, and Playwright WebKit, with bounded browser-server cleanup and no added runtime package dependencies.", + "invariant": "Authored requirement workspace JavaScript and CSS are embedded through an explicit compile-time asset set, served only by exact routes with restrictive response headers, and strictly type-checked without a production bundler; Go lifecycle, concurrent terminal-state, HTTP security, and repository-confined non-symlink artifact falsifiers plus one machine-admitted state-matrix execution report prove the same non-empty passed test identities in pinned Chromium, Firefox, and Playwright WebKit across independently held bootstrap, specifications, diff, and graph loading states, bootstrap failure, terminal specifications, diff, graph, unavailable views, view failure, handoff result, and handoff failure, with one direct audit initializing the pinned axe distribution through the browser-context script channel, admitting only the main frame and exact engine identity, preserving same-origin and Playwright branding, and running default rules plus explicitly enabled target-size after state identity, followed by 320 CSS-pixel document reflow, labelled internal graph and table overflow, and rendered light/dark text, boundary, and focus contrast oracles, a combined negative fixture for undersized named and normal-sized unnamed controls, bounded browser-server cleanup, attempt-scoped failed-run action, DOM, network, and source traces without continuous screenshot capture plus one bounded best-effort failure screenshot retained separately from authoritative passed proof, and no added runtime package dependencies.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], "nonClaimRefs": ["NC-PROOFKIT-QUALITY-022"], - "nonClaims": ["This requirement does not claim full WCAG conformance, every branded browser, provider CI ingestion, registry publication, release approval, rollout approval, or production readiness."], + "nonClaims": ["This requirement does not claim full WCAG 2.2 conformance, branded Safari behavior, complete screen-reader interoperability, every operating-system theme, 400-percent zoom behavior, continuous screenshot evidence, guaranteed capture of the best-effort failure screenshot, provider CI ingestion, registry publication, release approval, rollout approval, or production readiness."], "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, "deferral": null, "updatePolicy": {"reviewOwnerId": "proofkit.supply-chain-quality", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} @@ -298,7 +298,7 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-023", "ownerId": "proofkit.supply-chain-quality", - "invariant": "Each Python wheel binds its embedded executable to the release binary digest, independently decodes Darwin Mach-O bytes so an advertised macOS platform minimum is never lower than the executable minimum, uses Core Metadata 2.4 License-Expression and License-File fields, carries an exact repository LICENSE copy under the wheel licenses directory, and closes every payload entry through RECORD.", + "invariant": "Each Python wheel binds its embedded executable to the release binary digest, and the wheel-set verifier independently rejects version drift, duplicate identities, missing files, and SHA drift; each wheel independently decodes Darwin Mach-O bytes so an advertised macOS platform minimum is never lower than the executable minimum, uses Core Metadata 2.4 License-Expression and License-File fields, carries an exact repository LICENSE copy under the wheel licenses directory, and closes every payload entry through RECORD.", "claimLevel": "blocking", "riskClass": "critical", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -311,7 +311,7 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-024", "ownerId": "proofkit.supply-chain-quality", - "invariant": "Release metadata generation admits one version-bound machine-readable declaration of the reviewed public-contract change set and renders release notes as its exact projection, while one repository-owned retained-evidence builder and verifier checksum the exact final downloadable artifact topology with artifact-relative paths, reject unbound evidence files and symlink substitution, and fail release closeout on record, note, path, or digest drift.", + "invariant": "Release metadata generation admits one closed schema-versioned machine-readable declaration of the reviewed public-contract change set, requires exact ordered equality for the complete current breaking-change, addition, and migration inventories, including channel-specific changes to public generated continuation bytes, binds its exact previous and current canonical SemVer values to a compatible or breaking change class, rejects missing, substituted, reordered, or surplus current entries plus non-monotonic or patch-range breaking releases, and renders one byte-exact independently authored complete current release-note projection with no relocated, duplicate, appended, surplus, or second owned section, while one repository-owned retained-evidence builder and verifier checksum the exact final downloadable artifact topology with artifact-relative paths, reject unbound evidence files and symlink substitution, and fail release closeout on record, note, path, or digest drift.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -320,6 +320,19 @@ "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, "deferral": null, "updatePolicy": {"reviewOwnerId": "proofkit.supply-chain-quality", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-025", + "ownerId": "proofkit.supply-chain-quality", + "invariant": "Every tracked GitHub Actions workflow is admitted by one exact sorted inventory and a closed raw-YAML key grammar at workflow, job, and step boundaries before typed decoding, with only exact owner-reviewed release deployment environments; every external action reference is pinned to a full lowercase 40-hex commit identity; local action references remain repository-confined; status expressions and the required CI aggregate match exact owner-reviewed forms; and an already-existing GitHub Release is a read-only object whose metadata, exact asset set, and asset bytes must match the candidate before the workflow exits successfully.", + "claimLevel": "blocking", + "riskClass": "critical", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-QUALITY-025"], + "nonClaims": ["This requirement does not claim external action safety, tag equivalence, provider execution, branch-protection configuration, release publication, or provider-side asset immutability."], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.supply-chain-quality", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} } ] } diff --git a/internal/app/adoption_commands.go b/internal/app/adoption_commands.go index adc2e62..2e95332 100644 --- a/internal/app/adoption_commands.go +++ b/internal/app/adoption_commands.go @@ -10,11 +10,12 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/command/projectstructure" "github.com/research-engineering/agentic-proofkit/internal/kernel/adoptionmode" "github.com/research-engineering/agentic-proofkit/internal/kernel/agentenvelope" + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" "github.com/research-engineering/agentic-proofkit/internal/kernel/jsonpointer" "github.com/research-engineering/agentic-proofkit/internal/kernel/report" ) -func runAdoptionContractEnvelope(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int { +func runAdoptionContractEnvelope(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer, renderer cliexec.Renderer) int { options, err := parseAdoptionContractArgs(args) if err != nil { writeDiagnostic(stderr, err) @@ -28,7 +29,7 @@ func runAdoptionContractEnvelope(args []string, stdin io.Reader, stdout io.Write writeDiagnostic(stderr, err) return 1 } - output, exitCode, err := adoptioncontract.Build(input, adoptioncontract.Options{ + output, exitCode, err := adoptioncontract.BuildWithRenderer(input, adoptioncontract.Options{ AgentEnvelope: options.agentEnvelope, MaterializationManifest: options.materializationManifest, Mode: options.mode, @@ -38,7 +39,7 @@ func runAdoptionContractEnvelope(args []string, stdin io.Reader, stdout io.Write GuidanceMode: options.guidanceMode, TouchedRuleIDs: options.touchedRuleIDs, }, - }) + }, renderer) if err != nil && options.agentEnvelope { return writeJSON(agentenvelope.InvalidInput(diagnosticMessage(err)), 1, nil, stdout, stderr) } @@ -63,6 +64,9 @@ func parseAdoptionContractArgs(args []string) (adoptionContractArgs, error) { options.inputPath = args[index+1] index++ case "--mode": + if options.mode != "" { + return adoptionContractArgs{}, fmt.Errorf("--mode may be specified only once") + } if index+1 >= len(args) || args[index+1] == "" { return adoptionContractArgs{}, fmt.Errorf("--mode requires adoption, bootstrap, guidance, pilot, or workflow") } @@ -73,7 +77,10 @@ func parseAdoptionContractArgs(args []string) (adoptionContractArgs, error) { case "--materialization-manifest": options.materializationManifest = true case "--pilot": - if index+1 >= len(args) { + if options.pilotSet { + return adoptionContractArgs{}, fmt.Errorf("--pilot may be specified only once") + } + if index+1 >= len(args) || args[index+1] == "" { return adoptionContractArgs{}, fmt.Errorf("--pilot requires first, stack-diverse, or all") } options.pilot = args[index+1] @@ -150,14 +157,7 @@ func runPilotAdmission(args []string, stdin io.Reader, stdout io.Writer, stderr } } if options.pilot == "all" { - first, firstExitCode, err := pilotadmission.BuildFromContractEnvelope(input, "input", pilotadmission.Options{}) - if err != nil { - writeDiagnostic(stderr, err) - return 1 - } - stackDiverse, stackDiverseExitCode, err := pilotadmission.BuildFromContractEnvelope(input, "stackDiverseInput", pilotadmission.Options{ - RequireStackDiverseReleaseCandidate: true, - }) + first, firstExitCode, stackDiverse, stackDiverseExitCode, err := pilotadmission.BuildAllFromContractEnvelope(input) if err != nil { writeDiagnostic(stderr, err) return 1 @@ -191,6 +191,7 @@ func runPilotAdmission(args []string, stdin io.Reader, stdout io.Writer, stderr func parsePilotAdmissionArgs(args []string) (pilotAdmissionArgs, error) { options := pilotAdmissionArgs{pilot: "first"} inputPointerSeen := false + pilotSelectorSeen := false for index := 0; index < len(args); index++ { switch args[index] { case "--input": @@ -209,8 +210,15 @@ func parsePilotAdmissionArgs(args []string) (pilotAdmissionArgs, error) { case "--contract-envelope": options.contractEnvelope = true case "--stack-diverse": + if pilotSelectorSeen { + return pilotAdmissionArgs{}, fmt.Errorf("pilot selector may be specified only once") + } + pilotSelectorSeen = true options.pilot = "stack-diverse" case "--pilot": + if pilotSelectorSeen { + return pilotAdmissionArgs{}, fmt.Errorf("pilot selector may be specified only once") + } if index+1 >= len(args) { return pilotAdmissionArgs{}, fmt.Errorf("--pilot requires first, stack-diverse, or all") } @@ -218,6 +226,7 @@ func parsePilotAdmissionArgs(args []string) (pilotAdmissionArgs, error) { if pilot != "first" && pilot != "stack-diverse" && pilot != "all" { return pilotAdmissionArgs{}, fmt.Errorf("--pilot requires first, stack-diverse, or all") } + pilotSelectorSeen = true options.pilot = pilot index++ default: @@ -236,7 +245,7 @@ func parsePilotAdmissionArgs(args []string) (pilotAdmissionArgs, error) { return options, nil } -func runProjectStructure(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int { +func runProjectStructure(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer, renderer cliexec.Renderer) int { options, err := parsePlanningArgs("scaffold-project-structure", args) if err != nil { writeDiagnostic(stderr, err) @@ -255,12 +264,12 @@ func runProjectStructure(args []string, stdin io.Reader, stdout io.Writer, stder } } if options.agentEnvelope { - output, exitCode, err := projectstructure.BuildEnvelope(input) + output, exitCode, err := projectstructure.BuildEnvelopeWithRenderer(input, renderer) if err != nil { return writeJSON(agentenvelope.InvalidInput(diagnosticMessage(err)), 1, nil, stdout, stderr) } return writeJSON(output, exitCode, nil, stdout, stderr) } - output, exitCode, err := projectstructure.Build(input) + output, exitCode, err := projectstructure.BuildWithRenderer(input, renderer) return writeJSON(output, exitCode, err, stdout, stderr) } diff --git a/internal/app/agent_route_command.go b/internal/app/agent_route_command.go index ab71328..802461d 100644 --- a/internal/app/agent_route_command.go +++ b/internal/app/agent_route_command.go @@ -5,10 +5,11 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/command/agentroute" "github.com/research-engineering/agentic-proofkit/internal/kernel/agentenvelope" + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" "github.com/research-engineering/agentic-proofkit/internal/kernel/jsonpointer" ) -func runAgentRoute(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int { +func runAgentRoute(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer, renderer cliexec.Renderer) int { options, err := parsePlanningArgs("agent-route", args) if err != nil { writeDiagnostic(stderr, err) @@ -33,13 +34,13 @@ func runAgentRoute(args []string, stdin io.Reader, stdout io.Writer, stderr io.W } } if options.agentEnvelope { - output, exitCode, err := agentroute.BuildEnvelope(input) + output, exitCode, err := agentroute.BuildEnvelopeWithRenderer(input, renderer) if err != nil { return writeJSON(agentenvelope.InvalidInput(diagnosticMessage(err)), 1, nil, stdout, stderr) } return writeJSON(output, exitCode, nil, stdout, stderr) } - output, exitCode, err := agentroute.Build(input) + output, exitCode, err := agentroute.BuildWithRenderer(input, renderer) if err != nil { writeDiagnostic(stderr, err) return 1 diff --git a/internal/app/app.go b/internal/app/app.go index 87f839d..226dee4 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -10,6 +10,7 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/command/gradualadoption" "github.com/research-engineering/agentic-proofkit/internal/command/jsonreportcliadaptersource" "github.com/research-engineering/agentic-proofkit/internal/command/stackpreset" + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" "github.com/research-engineering/agentic-proofkit/internal/kernel/jsonpointer" ) @@ -19,6 +20,10 @@ import ( const maxInputBytes = 32 << 20 func Run(ctx context.Context, args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int { + return RunWithRenderer(ctx, args, stdin, stdout, stderr, cliexec.PathRenderer()) +} + +func RunWithRenderer(ctx context.Context, args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer, renderer cliexec.Renderer) int { args, layout, layoutExplicit, err := parseProcessOptions(args) if err != nil { writeDiagnostic(stderr, err) @@ -29,7 +34,7 @@ func Run(ctx context.Context, args []string, stdin io.Reader, stdout io.Writer, writeDiagnosticf(stderr, "--json-layout is valid only for JSON command output") return 1 } - return writeText(usage(), 0, nil, stdout, stderr) + return writeText(usageWithRenderer(renderer), 0, nil, stdout, stderr) } descriptor, ok := commandDescriptorFor(args[0]) if !ok { @@ -41,7 +46,7 @@ func Run(ctx context.Context, args []string, stdin io.Reader, stdout io.Writer, writeDiagnosticf(stderr, "--json-layout is valid only for JSON command output") return 1 } - return writeText(commandUsage(descriptor), 0, nil, stdout, stderr) + return writeText(commandUsageWithRenderer(descriptor, renderer), 0, nil, stdout, stderr) } parsedArguments := classifyDescriptorArguments(descriptor, args[1:]) if err := validateJSONLayoutUse(descriptor, parsedArguments, layoutExplicit); err != nil { @@ -60,7 +65,7 @@ func Run(ctx context.Context, args []string, stdin io.Reader, stdout io.Writer, writeDiagnosticf(stderr, "help families accepts no additional operands") return 1 } - return writeText(commandFamiliesUsage(), 0, nil, stdout, stderr) + return writeText(commandFamiliesUsageWithRenderer(renderer), 0, nil, stdout, stderr) } if len(args) >= 2 && args[1] == "family" { if len(args) < 3 { @@ -71,7 +76,7 @@ func Run(ctx context.Context, args []string, stdin io.Reader, stdout io.Writer, writeDiagnosticf(stderr, "help family accepts exactly one family id") return 1 } - output, err := commandFamilyUsage(args[2]) + output, err := commandFamilyUsageWithRenderer(args[2], renderer) if err != nil { writeDiagnostic(stderr, err) return 1 @@ -84,13 +89,13 @@ func Run(ctx context.Context, args []string, stdin io.Reader, stdout io.Writer, writeDiagnosticf(stderr, "unsupported help target: %s", args[1]) return 1 } - return writeText(commandUsage(target), 0, nil, stdout, stderr) + return writeText(commandUsageWithRenderer(target, renderer), 0, nil, stdout, stderr) } if len(args) != 1 && !(len(args) == 2 && (args[1] == "--help" || args[1] == "-h")) { _, _ = fmt.Fprintln(stderr, "help supports only --help or -h") return 1 } - return writeText(usage(), 0, nil, stdout, stderr) + return writeText(usageWithRenderer(renderer), 0, nil, stdout, stderr) case commandRunnerInit: preset, err := parseInitArgs(args[1:]) if err != nil { @@ -105,22 +110,30 @@ func Run(ctx context.Context, args []string, stdin io.Reader, stdout io.Writer, buildEnvelope: adoptiondoctor.BuildEnvelope, }) case commandRunnerAdoptionContractEnvelope: - return runAdoptionContractEnvelope(args[1:], stdin, stdout, stderr) + return runAdoptionContractEnvelope(args[1:], stdin, stdout, stderr, renderer) case commandRunnerAdoptionWorkflow: return runAgentEnvelopeCommand(args[0], args[1:], stdin, stdout, stderr, agentEnvelopeBuilders{ - build: adoptionworkflow.Build, - buildEnvelope: adoptionworkflow.BuildEnvelope, - buildFromContract: adoptionworkflow.BuildFromContractEnvelope, - buildEnvelopeFromContract: adoptionworkflow.BuildEnvelopeFromContractEnvelope, + build: func(raw any) (map[string]any, int, error) { + return adoptionworkflow.BuildWithRenderer(raw, renderer) + }, + buildEnvelope: func(raw any) (map[string]any, int, error) { + return adoptionworkflow.BuildEnvelopeWithRenderer(raw, renderer) + }, + buildFromContract: func(raw any) (map[string]any, int, error) { + return adoptionworkflow.BuildFromContractEnvelopeWithRenderer(raw, renderer) + }, + buildEnvelopeFromContract: func(raw any) (map[string]any, int, error) { + return adoptionworkflow.BuildEnvelopeFromContractEnvelopeWithRenderer(raw, renderer) + }, supportsContractEnvelope: true, supportsMaterializationFile: false, }) case commandRunnerAgentRoute: - return runAgentRoute(args[1:], stdin, stdout, stderr) + return runAgentRoute(args[1:], stdin, stdout, stderr, renderer) case commandRunnerContractEnvelope: return runContractEnvelopeCommand(args[0], args[1:], stdin, stdout, stderr, gradualadoption.Build, gradualadoption.BuildFromContractEnvelope) case commandRunnerGradualAdoptionBootstrap: - return runGradualAdoptionBootstrap(args[1:], stdin, stdout, stderr) + return runGradualAdoptionBootstrap(args[1:], stdin, stdout, stderr, renderer) case commandRunnerGradualAdoptionGuidance: return runGradualAdoptionGuidance(args[1:], stdin, stdout, stderr) case commandRunnerJSONReportCLIAdapterSource: @@ -134,7 +147,7 @@ func Run(ctx context.Context, args []string, stdin io.Reader, stdout io.Writer, case commandRunnerPilotAdmission: return runPilotAdmission(args[1:], stdin, stdout, stderr) case commandRunnerProjectStructure: - return runProjectStructure(args[1:], stdin, stdout, stderr) + return runProjectStructure(args[1:], stdin, stdout, stderr, renderer) case commandRunnerTypeScriptPublicAPISurfaces: return runTypeScriptPublicAPI(args[1:], stdin, stdout, stderr) case commandRunnerStackPreset: @@ -143,7 +156,7 @@ func Run(ctx context.Context, args []string, stdin io.Reader, stdout io.Writer, writeDiagnostic(stderr, err) return 1 } - record, err := stackpreset.Build(presetID) + record, err := stackpreset.BuildWithRenderer(presetID, renderer) if err != nil { writeDiagnostic(stderr, err) return 1 @@ -158,7 +171,7 @@ func Run(ctx context.Context, args []string, stdin io.Reader, stdout io.Writer, case commandRunnerRequirementContextCompose: return runRequirementContextCompose(args[1:], stdin, stdout, stderr) case commandRunnerRequirementView: - return runRequirementView(args[0], args[1:], stdin, stdout, stderr) + return runRequirementView(args[0], args[1:], stdin, stdout, stderr, renderer) case commandRunnerTestEvidenceInventory: return runTestEvidenceInventory(args[1:], stdin, stdout, stderr) case commandRunnerPlanning: diff --git a/internal/app/app_test.go b/internal/app/app_test.go index f83885e..1d460b8 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -18,9 +18,37 @@ import ( "time" "github.com/research-engineering/agentic-proofkit/internal/command/agentroute" + "github.com/research-engineering/agentic-proofkit/internal/command/requirementbrowser" "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" ) +func TestRequirementBrowserInvalidViewDiagnosticMatchesRuntime(t *testing.T) { + _, _, runtimeErr := requirementbrowser.BuildPlan(map[string]any{}, requirementbrowser.Options{ + Host: "127.0.0.1", + PortSet: true, + View: "invalid", + }) + if runtimeErr == nil { + t.Fatal("runtime accepted invalid browser view") + } + var stdout bytes.Buffer + var stderr bytes.Buffer + status := Run( + t.Context(), + []string{"requirement-browser-server", "--input", "-", "--view", "invalid"}, + strings.NewReader("{}"), + &stdout, + &stderr, + ) + const vocabulary = "source, proof, coverage, spec-tree, or workspace" + if status != 1 || + stdout.Len() != 0 || + strings.Count(stderr.String(), vocabulary) != 1 || + strings.Count(runtimeErr.Error(), vocabulary) != 1 { + t.Fatalf("app/runtime diagnostic drift status=%d stdout=%q stderr=%q runtime=%q", status, stdout.String(), stderr.String(), runtimeErr) + } +} + var processTestBinary = struct { once sync.Once path string diff --git a/internal/app/cli_abi_test.go b/internal/app/cli_abi_test.go index 6850565..9803f48 100644 --- a/internal/app/cli_abi_test.go +++ b/internal/app/cli_abi_test.go @@ -2,13 +2,22 @@ package app import ( "bytes" + "context" "encoding/base64" "encoding/json" - "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" + "io" + "net/http" "os" "path/filepath" + "regexp" + "runtime" + "slices" "strings" "testing" + "time" + + "github.com/research-engineering/agentic-proofkit/internal/testsupport/browserfixture" + "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" ) func TestCLIABIGoldenCorpus(t *testing.T) { @@ -19,16 +28,18 @@ func TestCLIABIGoldenCorpus(t *testing.T) { t.Fatalf("read spec fixture: %v", err) } cases := []struct { - name string - args []string - stdin string - wantStatus int - wantStdoutJSON bool - wantStdout string - wantStdoutHas []string - wantStdoutNotHas []string - wantStderr string - wantStderrHas []string + name string + args []string + stdin string + wantStatus int + wantStdoutJSON bool + wantStdout string + wantStdoutHas []string + wantStdoutNotHas []string + wantStderr string + wantStderrHas []string + wantInputVariant string + wantOutputVariant string }{ { name: "unsupported command", @@ -265,22 +276,26 @@ func TestCLIABIGoldenCorpus(t *testing.T) { wantStderrHas: []string{"requires --language"}, }, { - name: "test evidence inventory emits passed report JSON", - args: []string{"test-evidence-inventory", "--input", "-"}, - stdin: cliTestEvidenceInventory(), - wantStatus: 0, - wantStdoutJSON: true, + name: "test evidence inventory emits passed report JSON", + args: []string{"test-evidence-inventory", "--input", "-"}, + stdin: cliTestEvidenceInventory(), + wantStatus: 0, + wantStdoutJSON: true, + wantInputVariant: "01-direct-inventory", + wantOutputVariant: "03-report", wantStdoutHas: []string{ `"reportKind": "proofkit.test-evidence-inventory"`, `"state": "passed"`, }, }, { - name: "test evidence inventory emits normalized inventory JSON", - args: []string{"test-evidence-inventory", "--input", "-", "--normalized-inventory"}, - stdin: cliTestEvidenceInventory(), - wantStatus: 0, - wantStdoutJSON: true, + name: "test evidence inventory emits normalized inventory JSON", + args: []string{"test-evidence-inventory", "--input", "-", "--normalized-inventory"}, + stdin: cliTestEvidenceInventory(), + wantStatus: 0, + wantStdoutJSON: true, + wantInputVariant: "01-direct-inventory", + wantOutputVariant: "01-normalized-direct", wantStdoutHas: []string{ `"normalizedKind": "proofkit.test-evidence-inventory.normalized"`, `"sourceAuthority": "caller_owned_inventory"`, @@ -290,11 +305,13 @@ func TestCLIABIGoldenCorpus(t *testing.T) { }, }, { - name: "test evidence inventory emits proof-binding-derived normalized inventory JSON", - args: []string{"test-evidence-inventory", "--input", "-", "--projection", "proof-binding-derived", "--normalized-inventory"}, - stdin: cliProofBindingDerivedInventoryInput(), - wantStatus: 0, - wantStdoutJSON: true, + name: "test evidence inventory emits proof-binding-derived normalized inventory JSON", + args: []string{"test-evidence-inventory", "--input", "-", "--projection", "proof-binding-derived", "--normalized-inventory"}, + stdin: cliProofBindingDerivedInventoryInput(), + wantStatus: 0, + wantStdoutJSON: true, + wantInputVariant: "03-proof-binding-derived", + wantOutputVariant: "02-normalized-proof-binding", wantStdoutHas: []string{ `"normalizedKind": "proofkit.test-evidence-inventory.normalized"`, `"projectionKind": "proofkit.proof-binding-test-inventory"`, @@ -303,11 +320,13 @@ func TestCLIABIGoldenCorpus(t *testing.T) { }, }, { - name: "test evidence inventory emits discovery draft candidate JSON", - args: []string{"test-evidence-inventory", "--input", "-", "--projection", "discovery-draft"}, - stdin: cliTestDiscoveryDraftInput(), - wantStatus: 0, - wantStdoutJSON: true, + name: "test evidence inventory emits discovery draft candidate JSON", + args: []string{"test-evidence-inventory", "--input", "-", "--projection", "discovery-draft"}, + stdin: cliTestDiscoveryDraftInput(), + wantStatus: 0, + wantStdoutJSON: true, + wantInputVariant: "02-discovery-draft", + wantOutputVariant: "03-report", wantStdoutHas: []string{ `"reportKind": "proofkit.test-inventory-discovery-draft"`, `"authority": "caller_owned_test_discovery_candidate_inventory"`, @@ -321,33 +340,39 @@ func TestCLIABIGoldenCorpus(t *testing.T) { }, }, { - name: "test evidence inventory failed report keeps stdout JSON", - args: []string{"test-evidence-inventory", "--input", "-"}, - stdin: cliTestEvidenceInventoryMissingAnchor(), - wantStatus: 1, - wantStdoutJSON: true, + name: "test evidence inventory failed report keeps stdout JSON", + args: []string{"test-evidence-inventory", "--input", "-"}, + stdin: cliTestEvidenceInventoryMissingAnchor(), + wantStatus: 1, + wantStdoutJSON: true, + wantInputVariant: "01-direct-inventory", + wantOutputVariant: "03-report", wantStdoutHas: []string{ `"missing_semantic_anchor:test.cli.semantic"`, `"state": "failed"`, }, }, { - name: "test evidence inventory normalized mode fails closed", - args: []string{"test-evidence-inventory", "--input", "-", "--normalized-inventory"}, - stdin: cliTestEvidenceInventoryMissingAnchor(), - wantStatus: 1, - wantStdoutJSON: true, + name: "test evidence inventory normalized mode fails closed", + args: []string{"test-evidence-inventory", "--input", "-", "--normalized-inventory"}, + stdin: cliTestEvidenceInventoryMissingAnchor(), + wantStatus: 1, + wantStdoutJSON: true, + wantInputVariant: "01-direct-inventory", + wantOutputVariant: "03-report", wantStdoutHas: []string{ `"reportKind": "proofkit.test-evidence-inventory"`, `"state": "failed"`, }, }, { - name: "requirement coverage view emits passed report JSON", - args: []string{"requirement-coverage-view", "--input", "-"}, - stdin: cliCoverageInput(cliCoverageInventory()), - wantStatus: 0, - wantStdoutJSON: true, + name: "requirement coverage view emits passed report JSON", + args: []string{"requirement-coverage-view", "--input", "-"}, + stdin: cliCoverageInput(cliCoverageInventory()), + wantStatus: 0, + wantStdoutJSON: true, + wantInputVariant: "02-coverage-structured", + wantOutputVariant: "02-report", wantStdoutHas: []string{ `"viewKind": "proofkit.requirement-coverage-view"`, `"covered_by_semantic_falsifier"`, @@ -439,6 +464,13 @@ func TestCLIABIGoldenCorpus(t *testing.T) { t.Run(item.name, func(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer + if item.wantInputVariant != "" { + var input any + if err := json.Unmarshal([]byte(item.stdin), &input); err != nil { + t.Fatalf("root-oracle input must be JSON: %v", err) + } + assertPublicCLIRootVariant(t, item.args[0], "input", item.wantInputVariant, input) + } status := Run(t.Context(), item.args, strings.NewReader(item.stdin), &stdout, &stderr) if status != item.wantStatus { t.Fatalf("status=%d want %d stdout=%s stderr=%s", status, item.wantStatus, stdout.String(), stderr.String()) @@ -465,10 +497,13 @@ func TestCLIABIGoldenCorpus(t *testing.T) { } } if item.wantStdoutJSON { - var parsed map[string]any + var parsed any if err := json.Unmarshal(stdout.Bytes(), &parsed); err != nil { t.Fatalf("stdout must be JSON: %v\n%s", err, stdout.String()) } + if item.wantOutputVariant != "" { + assertPublicCLIRootVariant(t, item.args[0], "output", item.wantOutputVariant, parsed) + } if stderr.Len() != 0 { t.Fatalf("admitted JSON ABI must keep stderr empty: %s", stderr.String()) } @@ -480,6 +515,101 @@ func TestCLIABIGoldenCorpus(t *testing.T) { } } +func TestSelfCheckOutputUsesExactRootShape(t *testing.T) { + var stdout bytes.Buffer + var stderr bytes.Buffer + status := Run(t.Context(), []string{"self-check", "--input", "-"}, strings.NewReader(`{"ok":true}`), &stdout, &stderr) + if status != 0 || stderr.Len() != 0 { + t.Fatalf("status=%d stdout=%s stderr=%s", status, stdout.String(), stderr.String()) + } + var output any + if err := json.Unmarshal(stdout.Bytes(), &output); err != nil { + t.Fatalf("stdout must be JSON: %v\n%s", err, stdout.String()) + } + assertPublicCLIRootVariant(t, "self-check", "output", "01-root", output) +} + +func TestRequiredInputCommandsRouteStructuralErrorsByMode(t *testing.T) { + commands := []string{ + "branch-authority", + "changed-path-set", + "deployment-evidence-admission", + "external-consumer", + "package-runtime-dependency-admission", + "readiness-closeout", + "registry-consumer", + "registry-consumer-proof-input-compose", + "repo-profile-admission", + } + for _, command := range commands { + t.Run(command+"/ordinary", func(t *testing.T) { + var stdout bytes.Buffer + var stderr bytes.Buffer + status := Run(t.Context(), []string{command, "--input", "-"}, strings.NewReader(`{}`), &stdout, &stderr) + if status != 1 || stdout.Len() != 0 || stderr.Len() == 0 { + t.Fatalf("status=%d stdout=%q stderr=%q, want 1/empty/diagnostic", status, stdout.String(), stderr.String()) + } + }) + } + + t.Run("deployment-evidence-admission/nested-structural-error", func(t *testing.T) { + input := `{ + "schemaVersion":1, + "admissionId":"proofkit.test.deployment", + "evidence":{ + "schema":"deployment.evidence.v1", + "proofScope":"local", + "deploymentClaim":"candidate", + "evidenceId":"proofkit.test.evidence", + "facts":[{ + "factId":"proofkit.test.fact", + "kind":"release_artifact", + "sourceCommits":["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"], + "imageRefs":["registry.example.test/proofkit@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"] + }], + "nonClaims":["Deployment evidence test fixture does not prove live deployment."] + }, + "policy":{ + "expectedDeploymentClaim":"candidate", + "expectedEvidenceSchema":"deployment.evidence.v1", + "expectedProofScope":"local", + "forbiddenValueIndicators":[], + "localRefIndicators":["127.0.0.1","localhost"], + "requiredFactIds":["proofkit.test.fact"], + "requiredFactKinds":["release_artifact"], + "requiredNonClaims":["Deployment evidence test fixture does not prove live deployment."], + "requireDigestPinnedImageRefs":true, + "requireLowercaseSourceCommits":true, + "temporaryEndpointHostSuffixes":["trycloudflare.com"] + }, + "rawOperatorEvidence":[{"evidenceRef":"operator.note","payload":{"":"value"}}], + "nonClaims":["Deployment evidence admission test fixture is not release proof."] + }` + var stdout bytes.Buffer + var stderr bytes.Buffer + status := Run(t.Context(), []string{"deployment-evidence-admission", "--input", "-"}, strings.NewReader(input), &stdout, &stderr) + if status != 1 || stdout.Len() != 0 || !strings.Contains(stderr.String(), "object key must be non-empty text") { + t.Fatalf("status=%d stdout=%q stderr=%q, want 1/empty/structural diagnostic", status, stdout.String(), stderr.String()) + } + }) + + t.Run("changed-path-set/agent-envelope", func(t *testing.T) { + var stdout bytes.Buffer + var stderr bytes.Buffer + status := Run(t.Context(), []string{"changed-path-set", "--input", "-", "--agent-envelope"}, strings.NewReader(`{}`), &stdout, &stderr) + if status != 1 || stderr.Len() != 0 { + t.Fatalf("status=%d stdout=%q stderr=%q, want 1/envelope/empty", status, stdout.String(), stderr.String()) + } + var envelope map[string]any + if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil { + t.Fatalf("decode envelope: %v", err) + } + if envelope["envelopeId"] != "proofkit.agent-envelope.invalid-input" { + t.Fatalf("envelopeId=%v, want invalid-input", envelope["envelopeId"]) + } + }) +} + func TestSpecializedParsersRejectDuplicateInputPointer(t *testing.T) { duplicate := []string{"--input", "-", "--input-pointer", "", "--input-pointer", "/nested"} cases := []struct { @@ -602,8 +732,299 @@ func TestRequirementSpecTreeViewOutputPathAdmission(t *testing.T) { } } +func TestOutputWriterRejectsDeterministicParentSwap(t *testing.T) { + for _, targetKind := range []string{"outside-root", "in-root-sibling"} { + t.Run("before-temp/"+targetKind, func(t *testing.T) { + root := t.TempDir() + t.Chdir(root) + if err := os.Mkdir("out", 0o755); err != nil { + t.Fatal(err) + } + target := t.TempDir() + if targetKind == "in-root-sibling" { + target = filepath.Join(root, "victim") + if err := os.Mkdir(target, 0o755); err != nil { + t.Fatal(err) + } + } + sentinel := filepath.Join(target, "sentinel.txt") + if err := os.WriteFile(sentinel, []byte("unchanged"), 0o600); err != nil { + t.Fatal(err) + } + swapped := false + outputWriterBarrier = func(stage, outputPath string) { + if swapped || stage != "parent_admitted" || outputPath != "out/result.txt" { + return + } + swapped = true + if err := os.Rename("out", "out-original"); err != nil { + t.Fatal(err) + } + linkTarget := target + if targetKind == "in-root-sibling" { + linkTarget = "victim" + } + if err := os.Symlink(linkTarget, "out"); err != nil { + t.Fatal(err) + } + } + t.Cleanup(func() { outputWriterBarrier = nil }) + + err := writeRepoRelativeOutputFile(filepath.FromSlash("out/result.txt"), []byte("must-not-escape")) + if !swapped { + t.Fatal("output writer barrier was not reached") + } + if err == nil { + t.Fatal("writeRepoRelativeOutputFile() accepted swapped parent") + } + content, readErr := os.ReadFile(sentinel) + if readErr != nil || string(content) != "unchanged" { + t.Fatalf("target sentinel changed: content=%q error=%v", content, readErr) + } + if _, statErr := os.Stat(filepath.Join(target, "result.txt")); !os.IsNotExist(statErr) { + t.Fatalf("output reached swapped target: stat error=%v", statErr) + } + }) + } + + for _, barrierStage := range []string{"before_publish", "before_rename"} { + for _, targetKind := range []string{"outside-root", "in-root-sibling"} { + t.Run(strings.ReplaceAll(barrierStage, "_", "-")+"/"+targetKind, func(t *testing.T) { + root := t.TempDir() + t.Chdir(root) + if err := os.Mkdir("out", 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join("out", "result.txt"), []byte("unchanged"), 0o600); err != nil { + t.Fatal(err) + } + + displaced := filepath.Join(root, "out-original") + replacement := filepath.Join(root, "out") + if targetKind == "outside-root" { + displaced = filepath.Join(t.TempDir(), "moved-out") + } else { + if err := os.Mkdir("victim", 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join("victim", "sentinel.txt"), []byte("unchanged"), 0o600); err != nil { + t.Fatal(err) + } + } + + swapped := false + outputWriterBarrier = func(stage, outputPath string) { + if swapped || stage != barrierStage || outputPath != "out/result.txt" { + return + } + swapped = true + if err := os.Rename("out", displaced); err != nil { + t.Fatal(err) + } + if targetKind == "outside-root" { + if err := os.Mkdir("out", 0o755); err != nil { + t.Fatal(err) + } + return + } + if err := os.Rename("victim", "out"); err != nil { + t.Fatal(err) + } + } + t.Cleanup(func() { outputWriterBarrier = nil }) + + err := writeRepoRelativeOutputFile(filepath.FromSlash("out/result.txt"), []byte("must-not-escape")) + if !swapped { + t.Fatalf("output writer %s barrier was not reached", barrierStage) + } + if err == nil { + t.Fatal("writeRepoRelativeOutputFile() accepted a final parent swap") + } + content, readErr := os.ReadFile(filepath.Join(displaced, "result.txt")) + if readErr != nil || string(content) != "unchanged" { + t.Fatalf("displaced target changed: content=%q error=%v", content, readErr) + } + entries, readDirErr := os.ReadDir(displaced) + if readDirErr != nil { + t.Fatal(readDirErr) + } + for _, entry := range entries { + if strings.HasPrefix(entry.Name(), ".proofkit-output-") { + t.Fatalf("temporary output escaped cleanup: %s", filepath.Join(displaced, entry.Name())) + } + } + if _, statErr := os.Stat(filepath.Join(replacement, "result.txt")); !os.IsNotExist(statErr) { + t.Fatalf("output reached replacement parent: stat error=%v", statErr) + } + if targetKind == "in-root-sibling" { + sentinel, sentinelErr := os.ReadFile(filepath.Join(replacement, "sentinel.txt")) + if sentinelErr != nil || string(sentinel) != "unchanged" { + t.Fatalf("replacement sentinel changed: content=%q error=%v", sentinel, sentinelErr) + } + } + }) + } + } + + for _, mutation := range []string{ + "identity", + "content", + "mode-permissions", + "mode-setuid", + "mode-setgid", + "mode-sticky", + "symlink", + } { + t.Run("before-publish/temp-"+mutation, func(t *testing.T) { + root := t.TempDir() + if mutation == "mode-setgid" { + effectiveGroupID := os.Getegid() + if effectiveGroupID < 0 { + t.Fatal("effective group ID is required for the setgid mutant") + } + if err := os.Chown(root, -1, effectiveGroupID); err != nil { + t.Fatalf("normalize setgid mutant root group: %v", err) + } + } + t.Chdir(root) + if err := os.Mkdir("out", 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join("out", "result.txt"), []byte("unchanged"), 0o600); err != nil { + t.Fatal(err) + } + identityReplacement := filepath.Join("out", "temp-identity-replacement") + if mutation == "identity" { + if err := os.WriteFile(identityReplacement, []byte("must-not-substitute"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.Chmod(identityReplacement, 0o644); err != nil { + t.Fatal(err) + } + } + + mutated := false + outputWriterBarrier = func(stage, outputPath string) { + if mutated || stage != "before_publish" || outputPath != "out/result.txt" { + return + } + entries, err := os.ReadDir("out") + if err != nil { + t.Fatal(err) + } + tempName := "" + for _, entry := range entries { + if strings.HasPrefix(entry.Name(), ".proofkit-output-") { + if tempName != "" { + t.Fatal("multiple temporary outputs found before publication") + } + tempName = entry.Name() + } + } + if tempName == "" { + t.Fatal("temporary output was not found before publication") + } + mutated = true + tempPath := filepath.Join("out", tempName) + if mutation == "identity" { + tempInfo, err := os.Lstat(tempPath) + if err != nil { + t.Fatal(err) + } + replacementInfo, err := os.Lstat(identityReplacement) + if err != nil { + t.Fatal(err) + } + if os.SameFile(tempInfo, replacementInfo) { + t.Fatal("identity replacement did not coexist with a distinct temporary object") + } + if err := os.Remove(tempPath); err != nil { + t.Fatal(err) + } + if err := os.Rename(identityReplacement, tempPath); err != nil { + t.Fatal(err) + } + return + } + if mutation == "symlink" { + backupPath := filepath.Join("out", "temp-original") + if err := os.Rename(tempPath, backupPath); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.Remove(backupPath) }) + if err := os.Symlink(filepath.Base(backupPath), tempPath); err != nil { + t.Fatal(err) + } + return + } + mutatedMode := os.FileMode(0) + switch mutation { + case "mode-permissions": + mutatedMode = 0o600 + case "mode-setuid": + mutatedMode = 0o644 | os.ModeSetuid + case "mode-setgid": + mutatedMode = 0o644 | os.ModeSetgid + case "mode-sticky": + mutatedMode = 0o644 | os.ModeSticky + } + if mutatedMode != 0 { + if err := os.Chmod(tempPath, mutatedMode); err != nil { + t.Fatal(err) + } + mutatedInfo, err := os.Lstat(tempPath) + if err != nil { + t.Fatal(err) + } + if mutatedInfo.Mode() != mutatedMode { + t.Fatalf("temporary output mode mutant did not materialize: got %v want %v", mutatedInfo.Mode(), mutatedMode) + } + return + } + if err := os.WriteFile(tempPath, []byte("substituted"), 0o600); err != nil { + t.Fatal(err) + } + } + t.Cleanup(func() { outputWriterBarrier = nil }) + + err := writeRepoRelativeOutputFile(filepath.FromSlash("out/result.txt"), []byte("must-not-substitute")) + if !mutated { + t.Fatal("output writer temporary-object barrier was not reached") + } + expectedError := "temporary output " + mutation + " changed" + if mutation == "symlink" { + expectedError = "temporary output identity changed" + } + if strings.HasPrefix(mutation, "mode-") { + expectedError = "temporary output mode changed" + } + if err == nil || !strings.Contains(err.Error(), expectedError) { + t.Fatalf("writeRepoRelativeOutputFile() temp mutation error=%v", err) + } + content, readErr := os.ReadFile(filepath.Join("out", "result.txt")) + if readErr != nil || string(content) != "unchanged" { + t.Fatalf("destination changed: content=%q error=%v", content, readErr) + } + entries, readDirErr := os.ReadDir("out") + if readDirErr != nil { + t.Fatal(readDirErr) + } + for _, entry := range entries { + if strings.HasPrefix(entry.Name(), ".proofkit-output-") { + t.Fatalf("substituted temporary output escaped cleanup: %s", entry.Name()) + } + } + }) + } +} + func TestRequirementBrowserServerSpecTreeCLIABI(t *testing.T) { commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.006501090000492450297502395866607916441745680520470690190439229497945905166304") + var input any + if err := json.Unmarshal([]byte(cliRequirementSpecTreeInput()), &input); err != nil { + t.Fatalf("root-oracle input must be JSON: %v", err) + } + assertPublicCLIRootVariant(t, "requirement-browser-server", "input", "06-spec-tree", input) var stdout bytes.Buffer var stderr bytes.Buffer status := Run(t.Context(), []string{"requirement-browser-server", "--input", "-", "--view", "spec-tree"}, strings.NewReader(cliRequirementSpecTreeInput()), &stdout, &stderr) @@ -614,6 +1035,7 @@ func TestRequirementBrowserServerSpecTreeCLIABI(t *testing.T) { if err := json.Unmarshal(stdout.Bytes(), &parsed); err != nil { t.Fatalf("stdout must be JSON: %v\n%s", err, stdout.String()) } + assertPublicCLIRootVariant(t, "requirement-browser-server", "output", "01-plan", parsed) if parsed["planKind"] != "proofkit.requirement-browser-server-plan" || parsed["renderedViewKind"] != "proofkit.requirement-spec-tree-view" || parsed["view"] != "spec-tree" { t.Fatalf("unexpected spec-tree browser plan: %#v", parsed) } @@ -622,6 +1044,123 @@ func TestRequirementBrowserServerSpecTreeCLIABI(t *testing.T) { } } +func TestRequirementBrowserOneShotCLIOutputVariants(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("test launcher fixture is POSIX-only") + } + fixture, err := browserfixture.Workspace() + if err != nil { + t.Fatal(err) + } + input, err := json.Marshal(fixture) + if err != nil { + t.Fatal(err) + } + launcherDir := t.TempDir() + launcherName := "xdg-open" + if runtime.GOOS == "darwin" { + launcherName = "open" + } + launcherPath := filepath.Join(launcherDir, launcherName) + launcherScript := "#!/bin/sh\nprintf '%s\\n' \"$1\" > \"$PROOFKIT_TEST_BROWSER_URL_FILE\"\n" + if err := os.WriteFile(launcherPath, []byte(launcherScript), 0o755); err != nil { + t.Fatal(err) + } + urlFile := filepath.Join(t.TempDir(), "browser-url") + t.Setenv("PROOFKIT_TEST_BROWSER_URL_FILE", urlFile) + t.Setenv("PATH", launcherDir+string(os.PathListSeparator)+os.Getenv("PATH")) + args := []string{ + "requirement-browser-server", + "--input", "-", + "--view", "workspace", + "--serve", + "--open", + "--session-mode", "one-shot-question", + "--session-timeout-seconds", "5", + } + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + var submittedStdout bytes.Buffer + var submittedStderr bytes.Buffer + result := make(chan int, 1) + go func() { + result <- Run(ctx, args, bytes.NewReader(input), &submittedStdout, &submittedStderr) + }() + var browserURL string + deadline := time.Now().Add(3 * time.Second) + for browserURL == "" && time.Now().Before(deadline) { + content, readErr := os.ReadFile(urlFile) + if readErr == nil { + browserURL = strings.TrimSpace(string(content)) + break + } + time.Sleep(10 * time.Millisecond) + } + if browserURL == "" { + t.Fatal("browser launcher did not receive one-shot URL") + } + response, err := http.Get(browserURL) + if err != nil { + t.Fatal(err) + } + body, _ := io.ReadAll(response.Body) + _ = response.Body.Close() + capabilityMatch := regexp.MustCompile(`name="proofkit-browser-capability" content="([A-Za-z0-9_-]{43})"`).FindSubmatch(body) + if len(capabilityMatch) != 2 { + t.Fatalf("workspace capability missing from browser shell: %s", body) + } + handoff := `{"annotations":[{"anchorId":"requirement:REQ-CONSUMER-001:invariant","exactQuote":"preserves","startCodePoint":11,"endCodePoint":20,"question":"Does this remain true?"}]}` + request, err := http.NewRequest(http.MethodPost, browserURL+"api/v1/handoff", strings.NewReader(handoff)) + if err != nil { + t.Fatal(err) + } + request.Header.Set("Origin", strings.TrimSuffix(browserURL, "/")) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("X-Proofkit-Browser-Capability", string(capabilityMatch[1])) + handoffResponse, err := http.DefaultClient.Do(request) + if err != nil { + t.Fatal(err) + } + handoffBody, _ := io.ReadAll(handoffResponse.Body) + _ = handoffResponse.Body.Close() + if handoffResponse.StatusCode != http.StatusOK { + t.Fatalf("handoff status=%d body=%s", handoffResponse.StatusCode, handoffBody) + } + select { + case status := <-result: + if status != 0 || submittedStderr.Len() != 0 { + t.Fatalf("submitted status=%d stdout=%s stderr=%s", status, submittedStdout.String(), submittedStderr.String()) + } + case <-time.After(5 * time.Second): + t.Fatal("submitted one-shot CLI did not return") + } + var submitted any + if err := json.Unmarshal(submittedStdout.Bytes(), &submitted); err != nil { + t.Fatalf("submitted stdout must be JSON: %v\n%s", err, submittedStdout.String()) + } + assertPublicCLIRootVariant(t, "requirement-browser-server", "output", "03-one-shot-submitted", submitted) + + var terminalStdout bytes.Buffer + var terminalStderr bytes.Buffer + terminalStatus := Run(t.Context(), []string{ + "requirement-browser-server", + "--input", "-", + "--view", "workspace", + "--serve", + "--open", + "--session-mode", "one-shot-question", + "--session-timeout-seconds", "1", + }, bytes.NewReader(input), &terminalStdout, &terminalStderr) + if terminalStatus != 1 || terminalStderr.Len() != 0 { + t.Fatalf("terminal status=%d stdout=%s stderr=%s", terminalStatus, terminalStdout.String(), terminalStderr.String()) + } + var terminal any + if err := json.Unmarshal(terminalStdout.Bytes(), &terminal); err != nil { + t.Fatalf("terminal stdout must be JSON: %v\n%s", err, terminalStdout.String()) + } + assertPublicCLIRootVariant(t, "requirement-browser-server", "output", "02-one-shot-terminal", terminal) +} + func TestAdoptionDoctorCLIABI(t *testing.T) { commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.018061704360080936767110902235369680299779598229946624164723476412925521966788") cases := []struct { @@ -693,18 +1232,24 @@ func TestAdoptionDoctorCLIABI(t *testing.T) { } func TestAdoptionContractEnvelopeCLIABI(t *testing.T) { + exercisedRootConditions := map[string]struct{}{} + exercisedJSONCases := []string{} cases := []struct { - name string - args []string - stdin string - wantStatus int - wantStderr string - assertJSON func(t *testing.T, value any) + name string + args []string + stdin string + wantStatus int + wantStderr string + rootCondition string + rootVariant string + assertJSON func(t *testing.T, value any) }{ { - name: "workflow mode emits child workflow plan", - args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "workflow"}, - wantStatus: 0, + name: "workflow mode emits child workflow plan", + args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "workflow"}, + wantStatus: 0, + rootCondition: "--agent-envelope=absent --materialization-manifest=absent --mode=workflow --pilot=absent", + rootVariant: "11-workflow-plan", assertJSON: func(t *testing.T, value any) { object := jsonObject(t, value) assertStringField(t, object, "planKind", "proofkit.adoption-workflow-plan") @@ -712,9 +1257,11 @@ func TestAdoptionContractEnvelopeCLIABI(t *testing.T) { }, }, { - name: "workflow agent envelope emits child envelope", - args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "workflow", "--agent-envelope"}, - wantStatus: 0, + name: "workflow agent envelope emits child envelope", + args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "workflow", "--agent-envelope"}, + wantStatus: 0, + rootCondition: "--agent-envelope=present --materialization-manifest=absent --mode=workflow --pilot=absent", + rootVariant: "10-workflow-agent", assertJSON: func(t *testing.T, value any) { object := jsonObject(t, value) assertStringField(t, object, "envelopeId", "proofkit-adoption-workflow.agent-envelope") @@ -722,9 +1269,11 @@ func TestAdoptionContractEnvelopeCLIABI(t *testing.T) { }, }, { - name: "adoption mode emits gradual adoption report", - args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "adoption"}, - wantStatus: 0, + name: "adoption mode emits gradual adoption report", + args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "adoption"}, + wantStatus: 0, + rootCondition: "--agent-envelope=absent --materialization-manifest=absent --mode=adoption --pilot=absent", + rootVariant: "01-adoption-report", assertJSON: func(t *testing.T, value any) { object := jsonObject(t, value) assertStringField(t, object, "reportKind", "proofkit.gradual-adoption") @@ -732,9 +1281,37 @@ func TestAdoptionContractEnvelopeCLIABI(t *testing.T) { }, }, { - name: "bootstrap materialization emits manifest", - args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "bootstrap", "--materialization-manifest"}, - wantStatus: 0, + name: "bootstrap mode emits bootstrap projection", + args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "bootstrap"}, + wantStatus: 0, + rootCondition: "--agent-envelope=absent --materialization-manifest=absent --mode=bootstrap --pilot=absent", + rootVariant: "02-bootstrap", + assertJSON: func(t *testing.T, value any) { + object := jsonObject(t, value) + if _, ok := object["plannedFiles"].([]any); !ok { + t.Fatalf("bootstrap projection must expose plannedFiles: %#v", object) + } + assertStringField(t, jsonObjectField(t, object, "report"), "reportKind", "proofkit.gradual-adoption-bootstrap") + }, + }, + { + name: "bootstrap agent mode emits envelope", + args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "bootstrap", "--agent-envelope"}, + wantStatus: 0, + rootCondition: "--agent-envelope=present --materialization-manifest=absent --mode=bootstrap --pilot=absent", + rootVariant: "03-bootstrap-agent", + assertJSON: func(t *testing.T, value any) { + object := jsonObject(t, value) + assertStringField(t, object, "envelopeId", "proofkit.cli.bootstrap.agent-envelope") + assertStringField(t, jsonObjectField(t, object, "sourceReport"), "reportKind", "proofkit.gradual-adoption-bootstrap") + }, + }, + { + name: "bootstrap materialization emits manifest", + args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "bootstrap", "--materialization-manifest"}, + wantStatus: 0, + rootCondition: "--agent-envelope=absent --materialization-manifest=present --mode=bootstrap --pilot=absent", + rootVariant: "04-bootstrap-materialization", assertJSON: func(t *testing.T, value any) { object := jsonObject(t, value) assertStringField(t, object, "manifestKind", "proofkit.gradual-adoption-bootstrap-materialization-manifest") @@ -745,9 +1322,11 @@ func TestAdoptionContractEnvelopeCLIABI(t *testing.T) { }, }, { - name: "guidance override emits guidance report", - args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "guidance", "--guidance-mode", "warn", "--checked-scope", "all"}, - wantStatus: 0, + name: "guidance override emits guidance report", + args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "guidance", "--guidance-mode", "warn", "--checked-scope", "all"}, + wantStatus: 0, + rootCondition: "--agent-envelope=absent --materialization-manifest=absent --mode=guidance --pilot=absent", + rootVariant: "06-guidance-report", assertJSON: func(t *testing.T, value any) { object := jsonObject(t, value) assertStringField(t, object, "reportKind", "proofkit.gradual-adoption-guidance") @@ -757,9 +1336,11 @@ func TestAdoptionContractEnvelopeCLIABI(t *testing.T) { }, }, { - name: "guidance agent envelope emits child envelope", - args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "guidance", "--agent-envelope", "--guidance-mode", "warn", "--checked-scope", "none"}, - wantStatus: 0, + name: "guidance agent envelope emits child envelope", + args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "guidance", "--agent-envelope", "--guidance-mode", "warn", "--checked-scope", "none"}, + wantStatus: 0, + rootCondition: "--agent-envelope=present --materialization-manifest=absent --mode=guidance --pilot=absent", + rootVariant: "05-guidance-agent", assertJSON: func(t *testing.T, value any) { object := jsonObject(t, value) assertStringField(t, object, "envelopeId", "proofkit.cli.guidance.agent-envelope") @@ -767,9 +1348,11 @@ func TestAdoptionContractEnvelopeCLIABI(t *testing.T) { }, }, { - name: "pilot first emits pilot report", - args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "pilot", "--pilot", "first"}, - wantStatus: 0, + name: "pilot first emits pilot report", + args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "pilot", "--pilot", "first"}, + wantStatus: 0, + rootCondition: "--agent-envelope=absent --materialization-manifest=absent --mode=pilot --pilot=first", + rootVariant: "08-pilot-first", assertJSON: func(t *testing.T, value any) { object := jsonObject(t, value) assertStringField(t, object, "reportId", "proofkit.cli.pilot.first") @@ -777,9 +1360,23 @@ func TestAdoptionContractEnvelopeCLIABI(t *testing.T) { }, }, { - name: "pilot stack diverse emits stack-diverse report", - args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "pilot", "--pilot", "stack-diverse"}, - wantStatus: 0, + name: "pilot omitted defaults to first report", + args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "pilot"}, + wantStatus: 0, + rootCondition: "--agent-envelope=absent --materialization-manifest=absent --mode=pilot --pilot=absent", + rootVariant: "08-pilot-first", + assertJSON: func(t *testing.T, value any) { + object := jsonObject(t, value) + assertStringField(t, object, "reportId", "proofkit.cli.pilot.first") + assertStringField(t, object, "reportKind", "proofkit.pilot-admission") + }, + }, + { + name: "pilot stack diverse emits stack-diverse report", + args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "pilot", "--pilot", "stack-diverse"}, + wantStatus: 0, + rootCondition: "--agent-envelope=absent --materialization-manifest=absent --mode=pilot --pilot=stack-diverse", + rootVariant: "09-pilot-stack-diverse", assertJSON: func(t *testing.T, value any) { object := jsonObject(t, value) assertStringField(t, object, "reportId", "proofkit.cli.pilot.stack-diverse") @@ -787,9 +1384,11 @@ func TestAdoptionContractEnvelopeCLIABI(t *testing.T) { }, }, { - name: "pilot all emits both pilot reports", - args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "pilot", "--pilot", "all"}, - wantStatus: 0, + name: "pilot all emits both pilot reports", + args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "pilot", "--pilot", "all"}, + wantStatus: 0, + rootCondition: "--agent-envelope=absent --materialization-manifest=absent --mode=pilot --pilot=all", + rootVariant: "07-pilot-all", assertJSON: func(t *testing.T, value any) { items := jsonArray(t, value) if len(items) != 2 { @@ -800,10 +1399,12 @@ func TestAdoptionContractEnvelopeCLIABI(t *testing.T) { }, }, { - name: "agent envelope invalid aggregate emits repair packet", - args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "workflow", "--agent-envelope"}, - stdin: cliInvalidAdoptionContractEnvelopeInput(), - wantStatus: 1, + name: "agent envelope invalid aggregate emits repair packet", + args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "workflow", "--agent-envelope"}, + stdin: cliInvalidAdoptionContractEnvelopeInput(), + wantStatus: 1, + rootCondition: "--agent-envelope=present --materialization-manifest=absent --mode=workflow --pilot=absent", + rootVariant: "10-workflow-agent", assertJSON: func(t *testing.T, value any) { object := jsonObject(t, value) assertStringField(t, object, "envelopeId", "proofkit.agent-envelope.invalid-input") @@ -812,6 +1413,24 @@ func TestAdoptionContractEnvelopeCLIABI(t *testing.T) { } }, }, + { + name: "duplicate mode is rejected instead of changing the selected root variant", + args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "adoption", "--mode", "bootstrap"}, + wantStatus: 1, + wantStderr: "--mode may be specified only once\n", + }, + { + name: "duplicate pilot is rejected instead of changing the selected root variant", + args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "pilot", "--pilot", "all", "--pilot", "first"}, + wantStatus: 1, + wantStderr: "--pilot may be specified only once\n", + }, + { + name: "empty pilot is rejected instead of projecting a present flag as absent", + args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "pilot", "--pilot", ""}, + wantStatus: 1, + wantStderr: "--pilot requires first, stack-diverse, or all\n", + }, { name: "input pointer is rejected", args: []string{"adoption-contract-envelope", "--input", "-", "--input-pointer", "/workflow", "--mode", "workflow"}, @@ -849,9 +1468,11 @@ func TestAdoptionContractEnvelopeCLIABI(t *testing.T) { wantStderr: "--checked-scope requires none, touched, or all\n", }, { - name: "invalid guidance mode scope pair rejected", - args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "guidance", "--guidance-mode", "enforce-all", "--checked-scope", "touched"}, - wantStatus: 1, + name: "invalid guidance mode scope pair rejected", + args: []string{"adoption-contract-envelope", "--input", "-", "--mode", "guidance", "--guidance-mode", "enforce-all", "--checked-scope", "touched"}, + wantStatus: 1, + rootCondition: "--agent-envelope=absent --materialization-manifest=absent --mode=guidance --pilot=absent", + rootVariant: "06-guidance-report", assertJSON: func(t *testing.T, value any) { object := jsonObject(t, value) assertStringField(t, object, "state", "failed") @@ -874,6 +1495,27 @@ func TestAdoptionContractEnvelopeCLIABI(t *testing.T) { if stdin == "" { stdin = cliAdoptionContractEnvelopeInput() } + var input any + if err := json.Unmarshal([]byte(stdin), &input); err != nil { + t.Fatalf("root-oracle input must be JSON: %v", err) + } + assertPublicCLIRootVariant(t, "adoption-contract-envelope", "input", "01-root", input) + expectsJSON := item.assertJSON != nil + if expectsJSON { + if item.rootCondition == "" || item.rootVariant == "" { + t.Fatal("every JSON-emitting argv must declare an exact output condition and root variant") + } + parsedOptions, err := parseAdoptionContractArgs(item.args[1:]) + if err != nil { + t.Fatalf("parse root-condition argv: %v", err) + } + if actual := adoptionOutputConditionFromParsed(parsedOptions); actual != item.rootCondition { + t.Fatalf("argv root condition=%q want %q", actual, item.rootCondition) + } + exercisedRootConditions[item.rootCondition] = struct{}{} + } else if item.rootCondition != "" || item.rootVariant != "" { + t.Fatal("non-JSON argv must not declare an output condition or root variant") + } status := Run(t.Context(), item.args, strings.NewReader(stdin), &stdout, &stderr) if status != item.wantStatus { t.Fatalf("status=%d want %d stdout=%s stderr=%s", status, item.wantStatus, stdout.String(), stderr.String()) @@ -887,15 +1529,194 @@ func TestAdoptionContractEnvelopeCLIABI(t *testing.T) { if item.wantStderr != "" && stdout.Len() != 0 { t.Fatalf("stdout must be empty for argument failure: %s", stdout.String()) } - if item.assertJSON != nil { + emittedJSON := stdout.Len() != 0 + if emittedJSON != expectsJSON { + t.Fatalf("actual JSON emission=%t want fixture assertion=%t stdout=%q", emittedJSON, expectsJSON, stdout.String()) + } + if emittedJSON { + exercisedJSONCases = append(exercisedJSONCases, item.name) var parsed any if err := json.Unmarshal(stdout.Bytes(), &parsed); err != nil { t.Fatalf("stdout must be JSON: %v\n%s", err, stdout.String()) } + assertPublicCLIRootVariant(t, "adoption-contract-envelope", "output", item.rootVariant, parsed) + assertPublicCLIRootVariantCondition(t, "adoption-contract-envelope", "output", item.rootVariant, item.rootCondition) item.assertJSON(t, parsed) } }) } + slices.Sort(exercisedJSONCases) + expectedJSONCases := []string{ + "adoption mode emits gradual adoption report", + "agent envelope invalid aggregate emits repair packet", + "bootstrap agent mode emits envelope", + "bootstrap materialization emits manifest", + "bootstrap mode emits bootstrap projection", + "guidance agent envelope emits child envelope", + "guidance override emits guidance report", + "invalid guidance mode scope pair rejected", + "pilot all emits both pilot reports", + "pilot first emits pilot report", + "pilot omitted defaults to first report", + "pilot stack diverse emits stack-diverse report", + "workflow agent envelope emits child envelope", + "workflow mode emits child workflow plan", + } + if !slices.Equal(exercisedJSONCases, expectedJSONCases) { + t.Fatalf("exercised adoption JSON cases=%v want exact %v", exercisedJSONCases, expectedJSONCases) + } + if len(exercisedRootConditions) != 12 { + t.Fatalf("exercised adoption root conditions=%d want 12: %v", len(exercisedRootConditions), exercisedRootConditions) + } +} + +func TestStandaloneMultiVariantCommandsUseExactRootShapes(t *testing.T) { + pilotCases := []struct { + name string + args []string + stdin string + inputVariant string + outputVariant string + }{ + { + name: "direct input", + args: []string{"pilot-admission", "--input", "-"}, + stdin: cliJSON(cliPilotInput("proofkit.cli.pilot.first", false)), + inputVariant: "04-direct-first", + outputVariant: "02-report", + }, + { + name: "direct explicit first", + args: []string{"pilot-admission", "--input", "-", "--pilot", "first"}, + stdin: cliJSON(cliPilotInput("proofkit.cli.pilot.first", false)), + inputVariant: "04-direct-first", + outputVariant: "02-report", + }, + { + name: "direct stack diverse", + args: []string{"pilot-admission", "--input", "-", "--pilot", "stack-diverse"}, + stdin: cliJSON(cliPilotInput("proofkit.cli.pilot.stack-diverse", true)), + inputVariant: "05-direct-stack-diverse", + outputVariant: "02-report", + }, + { + name: "contract default first", + args: []string{"pilot-admission", "--input", "-", "--contract-envelope"}, + stdin: cliPilotContractEnvelopeInput("first"), + inputVariant: "02-contract-first", + outputVariant: "02-report", + }, + { + name: "contract explicit first", + args: []string{"pilot-admission", "--input", "-", "--contract-envelope", "--pilot", "first"}, + stdin: cliPilotContractEnvelopeInput("first"), + inputVariant: "02-contract-first", + outputVariant: "02-report", + }, + { + name: "contract stack diverse", + args: []string{"pilot-admission", "--input", "-", "--contract-envelope", "--pilot", "stack-diverse"}, + stdin: cliPilotContractEnvelopeInput("stack-diverse"), + inputVariant: "03-contract-stack-diverse", + outputVariant: "02-report", + }, + { + name: "contract stack diverse alias", + args: []string{"pilot-admission", "--input", "-", "--contract-envelope", "--stack-diverse"}, + stdin: cliPilotContractEnvelopeInput("stack-diverse"), + inputVariant: "03-contract-stack-diverse", + outputVariant: "02-report", + }, + { + name: "contract all", + args: []string{"pilot-admission", "--input", "-", "--contract-envelope", "--pilot", "all"}, + stdin: cliPilotContractEnvelopeInput("all"), + inputVariant: "01-contract-all", + outputVariant: "01-all", + }, + } + for _, item := range pilotCases { + t.Run("pilot/"+item.name, func(t *testing.T) { + var input any + if err := json.Unmarshal([]byte(item.stdin), &input); err != nil { + t.Fatal(err) + } + assertPublicCLIRootVariant(t, "pilot-admission", "input", item.inputVariant, input) + var stdout bytes.Buffer + var stderr bytes.Buffer + status := Run(t.Context(), item.args, strings.NewReader(item.stdin), &stdout, &stderr) + if status != 0 || stderr.Len() != 0 { + t.Fatalf("status=%d stdout=%s stderr=%s", status, stdout.String(), stderr.String()) + } + var output any + if err := json.Unmarshal(stdout.Bytes(), &output); err != nil { + t.Fatalf("stdout must be JSON: %v\n%s", err, stdout.String()) + } + assertPublicCLIRootVariant(t, "pilot-admission", "output", item.outputVariant, output) + }) + } + + for _, item := range []struct { + name string + args []string + }{ + { + name: "repeated pilot", + args: []string{"--pilot", "all", "--pilot", "first"}, + }, + { + name: "stack alias then pilot", + args: []string{"--stack-diverse", "--pilot", "first"}, + }, + { + name: "pilot then stack alias", + args: []string{"--pilot", "first", "--stack-diverse"}, + }, + { + name: "repeated stack alias", + args: []string{"--stack-diverse", "--stack-diverse"}, + }, + } { + t.Run("pilot rejects "+item.name, func(t *testing.T) { + args := append([]string{"pilot-admission", "--input", "-"}, item.args...) + var stdout bytes.Buffer + var stderr bytes.Buffer + status := Run(t.Context(), args, strings.NewReader("{}"), &stdout, &stderr) + if status != 1 || stdout.Len() != 0 || stderr.String() != "pilot selector may be specified only once\n" { + t.Fatalf("status=%d stdout=%q stderr=%q", status, stdout.String(), stderr.String()) + } + }) + } + + conformanceInput := cliConformanceProfileInput() + for _, item := range []struct { + name string + args []string + outputVariant string + }{ + {name: "list", args: []string{"conformance-profile", "--input", "-", "--list"}, outputVariant: "01-list"}, + {name: "profile default json", args: []string{"conformance-profile", "--input", "-", "--profile", "local"}, outputVariant: "02-profile"}, + {name: "verify", args: []string{"conformance-profile", "--input", "-", "--verify"}, outputVariant: "03-verify"}, + } { + t.Run("conformance/"+item.name, func(t *testing.T) { + var input any + if err := json.Unmarshal([]byte(conformanceInput), &input); err != nil { + t.Fatal(err) + } + assertPublicCLIRootVariant(t, "conformance-profile", "input", "01-root", input) + var stdout bytes.Buffer + var stderr bytes.Buffer + status := Run(t.Context(), item.args, strings.NewReader(conformanceInput), &stdout, &stderr) + if status != 0 || stderr.Len() != 0 { + t.Fatalf("status=%d stdout=%s stderr=%s", status, stdout.String(), stderr.String()) + } + var output any + if err := json.Unmarshal(stdout.Bytes(), &output); err != nil { + t.Fatalf("stdout must be JSON: %v\n%s", err, stdout.String()) + } + assertPublicCLIRootVariant(t, "conformance-profile", "output", item.outputVariant, output) + }) + } } func assertManifestGuidanceCommands(t *testing.T, manifest map[string]any) { @@ -1010,6 +1831,86 @@ func cliAdoptionContractEnvelopeInput() string { return string(content) } +func cliPilotContractEnvelopeInput(mode string) string { + envelope := map[string]any{"schema": "proofkit.pilot-admission.v1"} + if mode == "first" || mode == "all" { + envelope["input"] = cliPilotInput("proofkit.cli.pilot.first", false) + } + if mode == "stack-diverse" || mode == "all" { + envelope["stackDiverseInput"] = cliPilotInput("proofkit.cli.pilot.stack-diverse", true) + } + return cliJSON(envelope) +} + +func cliConformanceProfileInput() string { + manifestNonClaim := "Conformance profile CLI fixture is not live proof." + return cliJSON(map[string]any{ + "schemaVersion": json.Number("1"), + "profileId": "local", + "policy": map[string]any{ + "knownEnvironmentClasses": []any{"local-go"}, + "localEnvironmentClasses": []any{"local-go"}, + "allowedProofContractStates": []any{"witness_backed"}, + "blockingStatuses": []any{"blocking"}, + "failOnUnusedAllowedEnvironmentClass": true, + "expectedManifest": map[string]any{ + "contractId": "proofkit.cli.conformance", + "contractKind": "proofkit.conformance-manifest", + "authorityState": "canonical", + "normalizationProfile": "proofkit.cli.v1", + "sourceContract": "docs/contracts/requirement-proof-bindings.v1.json", + "nonClaims": []any{manifestNonClaim}, + }, + }, + "manifest": map[string]any{ + "schemaVersion": json.Number("1"), + "contractId": "proofkit.cli.conformance", + "contractKind": "proofkit.conformance-manifest", + "authorityState": "canonical", + "normalizationProfile": "proofkit.cli.v1", + "sourceContract": "docs/contracts/requirement-proof-bindings.v1.json", + "nonClaims": []any{manifestNonClaim}, + "profiles": []any{map[string]any{ + "profileId": "local", + "purpose": "Local proof profile.", + "preconditionPolicy": "local_only", + "requiredSurfaceIds": []any{"surface.local"}, + "optionalSurfaceIds": []any{}, + "allowedEnvironmentClasses": []any{"local-go"}, + "nonClaims": []any{"Local profile CLI fixture does not execute commands."}, + }}, + }, + "proofContract": map[string]any{ + "contractId": "proofkit.cli.proof-contract", + "surfaces": []any{map[string]any{ + "surfaceId": "surface.local", + "requiredEnvironmentClasses": []any{"local-go"}, + "preconditionedEnvironmentClasses": []any{}, + }}, + "bindings": []any{map[string]any{ + "requirementId": "REQ-PROOFKIT-001", + "surfaceId": "surface.local", + "scenarioId": "proofkit.scenario", + "blockingStatus": "blocking", + "proofContractState": "witness_backed", + "requiredEnvironmentClasses": []any{"local-go"}, + "verifyCommands": []any{"go test ./..."}, + "witnessRefs": []any{ + map[string]any{"role": "unit", "selector": "internal/test.go::TestOK"}, + }, + }}, + }, + }) +} + +func cliJSON(value any) string { + content, err := json.Marshal(value) + if err != nil { + panic(err) + } + return string(content) +} + func cliInvalidAdoptionContractEnvelopeInput() string { payload := map[string]any{ "schema": "proofkit.adoption-contract-envelope.v1", diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index e48477a..e0c61a6 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/json" "fmt" + "maps" "os" "path/filepath" "reflect" @@ -13,13 +14,15 @@ import ( "strings" "testing" + "github.com/research-engineering/agentic-proofkit/internal/command/adoptioncontract" "github.com/research-engineering/agentic-proofkit/internal/command/agentroute" + "github.com/research-engineering/agentic-proofkit/internal/command/stackpreset" "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" ) const ( - cliContractPublicABISHA256 = "10d5eb817410b82f4bae384806f1e87449860b4fcf5c8e5f875ab5a1a204627d" + cliContractPublicABISHA256 = "1b615f82dd66b83cf3d1a72154d7f4b7d1946eb3a6727cf4d0c54f065462c63f" maxAggregateFileReadBytesForContractTest = 64 << 20 maxPackageManifestBytesForContractTest = 256 << 10 maxSourceFileBytesForContractTest = 8 << 20 @@ -126,6 +129,559 @@ func TestCLIContractMatchesDispatcherAndHelp(t *testing.T) { } } +func TestCLIContractsAreCompleteGeneratedAndWitnessBound(t *testing.T) { + contract := readCLIContract(t) + if len(contract.ContractDefinitions) == 0 { + t.Fatal("CLI contract must expose reusable closed definitions") + } + definitions := cliContractDefinitionMap(t, contract.ContractDefinitions) + for _, command := range contract.Commands { + metadata, ok := generatedCommandContractMetadataByName[command.Command] + if !ok { + t.Fatalf("%s missing generated command-contract metadata", command.Command) + } + if command.Input == "required" { + assertBoundCommandContract(t, command.Command, "input", command.InputContract, definitions, metadata.InputContractSHA256, "nativeAdmissionWitnessSelector") + if len(metadata.InputSchemaSummary) == 0 { + t.Fatalf("%s input help summary is empty", command.Command) + } + } + if slices.Contains(command.OutputModes, "json") { + assertBoundCommandContract(t, command.Command, "output", command.OutputContract, definitions, metadata.OutputContractSHA256, "nativeOutputWitnessSelector") + } + } + presetChoices := generatedCommandContractMetadataByName["stack-preset"].FlagChoices["--preset"] + if !slices.Equal(presetChoices, stackpreset.IDs()) { + t.Fatalf("generated app preset choices=%v package projection=%v", presetChoices, stackpreset.IDs()) + } +} + +func TestCLIContractRootShapeVariantInventoryIsClosedAndModeComplete(t *testing.T) { + contract := readCLIContract(t) + definitions := cliContractDefinitionMap(t, contract.ContractDefinitions) + usedDefinitions := map[string]string{} + for _, command := range contract.Commands { + for _, direction := range []struct { + name string + raw any + }{ + {name: "input", raw: command.InputContract}, + {name: "output", raw: command.OutputContract}, + } { + if direction.raw == nil { + continue + } + binding := canonicalJSONValue(t, direction.raw).(map[string]any) + definitionID := binding["rootDefinitionRef"].(string) + if prior, exists := usedDefinitions[definitionID]; exists { + t.Fatalf("root-shape definition %s is shared by %s and %s %s", definitionID, prior, command.Command, direction.name) + } + usedDefinitions[definitionID] = command.Command + " " + direction.name + definition := definitions[definitionID] + if definition == nil { + t.Fatalf("%s %s root-shape definition %s is missing", command.Command, direction.name, definitionID) + } + assertRootShapeDefinition(t, definitionID, definition) + } + } + if len(usedDefinitions) != len(definitions) { + t.Fatalf("used root-shape definitions=%d want all %d definitions", len(usedDefinitions), len(definitions)) + } + + expectedConditions := map[string][]string{ + "adoption-contract-envelope output": {"--agent-envelope=absent", "--agent-envelope=present", "--materialization-manifest=absent", "--materialization-manifest=present", "--mode=adoption", "--mode=bootstrap", "--mode=guidance", "--mode=pilot", "--mode=workflow", "--pilot=absent"}, + "conformance-profile output": {"--list", "--profile", "--verify", "without --format"}, + "pilot-admission output": {"--pilot all", "--pilot first", "--pilot stack-diverse", "without --pilot"}, + "requirement-browser-server input": {"--view coverage", "--view proof", "--view source", "--view spec-tree", "--view workspace"}, + "requirement-browser-server output": {"--serve", "--session-mode one-shot-question", "state=cancelled|expired", "state=submitted", "without --serve"}, + "requirement-proof-source-set output": {"canonical_contract", "resolver_input"}, + "requirement-proof-view input": {"compact", "structured"}, + "requirement-proof-view output": {"compact", "structured"}, + "test-evidence-inventory input": {"direct inventory", "discovery-draft", "proof-binding-derived", "source-set", "wrapped inventory"}, + "test-evidence-inventory output": {"normalized-inventory", "proof-binding-derived", "failure report", "without --normalized-inventory"}, + "witness-plan input": {"without projection", "projection=requirement-bindings"}, + } + exactHighRiskConditions := map[string][]string{ + "adoption-contract-envelope output": { + "--agent-envelope=absent --materialization-manifest=absent --mode=adoption --pilot=absent", + "--agent-envelope=absent --materialization-manifest=absent --mode=bootstrap --pilot=absent", + "--agent-envelope=present --materialization-manifest=absent --mode=bootstrap --pilot=absent", + "--agent-envelope=absent --materialization-manifest=present --mode=bootstrap --pilot=absent", + "--agent-envelope=present --materialization-manifest=absent --mode=guidance --pilot=absent", + "--agent-envelope=absent --materialization-manifest=absent --mode=guidance --pilot=absent", + "--agent-envelope=absent --materialization-manifest=absent --mode=pilot --pilot=all", + "--agent-envelope=absent --materialization-manifest=absent --mode=pilot --pilot=absent", + "--agent-envelope=absent --materialization-manifest=absent --mode=pilot --pilot=first", + "--agent-envelope=absent --materialization-manifest=absent --mode=pilot --pilot=stack-diverse", + "--agent-envelope=present --materialization-manifest=absent --mode=workflow --pilot=absent", + "--agent-envelope=absent --materialization-manifest=absent --mode=workflow --pilot=absent", + }, + "conformance-profile output": { + "--list", + "--profile --format json", + "--profile without --format (defaults json)", + "--verify", + }, + "pilot-admission input": { + "--contract-envelope --pilot all", + "--contract-envelope --pilot first", + "--contract-envelope without --pilot (defaults first)", + "--contract-envelope --pilot stack-diverse", + "--contract-envelope --stack-diverse", + "without --contract-envelope; --pilot first", + "without --contract-envelope; without --pilot (defaults first)", + "without --contract-envelope; --pilot stack-diverse", + "without --contract-envelope; --stack-diverse", + }, + "pilot-admission output": { + "--contract-envelope --pilot all", + "--contract-envelope --pilot first", + "--contract-envelope --pilot stack-diverse", + "--contract-envelope --stack-diverse", + "--contract-envelope without --pilot (defaults first)", + "direct input", + }, + "requirement-browser-server output": { + "without --serve; --view coverage", + "without --serve; --view proof", + "without --serve; --view source", + "without --serve; --view spec-tree", + "without --serve; --view workspace", + "--open; --serve; --session-mode one-shot-question; --view workspace; state=cancelled|expired", + "--open; --serve; --session-mode one-shot-question; --view workspace; state=submitted", + }, + "requirement-proof-source-set output": { + "projection.kind=canonical_contract", + "projection.kind=resolver_input", + }, + "requirement-proof-view input": { + "compact proof contract root", + "structured requirement proof binding root", + }, + "requirement-proof-view output": { + "compact proof contract root", + "structured requirement proof binding root", + }, + "witness-plan input": { + "without projection", + "projection=requirement-bindings", + }, + } + for _, command := range contract.Commands { + for _, direction := range []struct { + name string + raw any + }{ + {name: "input", raw: command.InputContract}, + {name: "output", raw: command.OutputContract}, + } { + if direction.raw == nil { + continue + } + binding := canonicalJSONValue(t, direction.raw).(map[string]any) + definition := definitions[binding["rootDefinitionRef"].(string)] + conditionText := rootShapeConditionText(t, definition) + key := command.Command + " " + direction.name + if expected, ok := exactHighRiskConditions[key]; ok { + actual := rootShapeConditions(t, definition) + if !slices.Equal(actual, expected) { + t.Fatalf("%s exact root-shape conditions=%v want %v", key, actual, expected) + } + } + for _, expected := range expectedConditions[key] { + if !strings.Contains(conditionText, expected) { + t.Fatalf("%s root-shape variants omit condition %q: %s", key, expected, conditionText) + } + } + if direction.name == "input" && command.ContractEnvelope != nil && *command.ContractEnvelope { + for _, expected := range []string{"--contract-envelope", "without --contract-envelope"} { + if !strings.Contains(conditionText, expected) { + t.Fatalf("%s input root-shape variants omit %q: %s", command.Command, expected, conditionText) + } + } + } + if direction.name == "output" && command.AgentEnvelope != nil && *command.AgentEnvelope { + if !strings.Contains(conditionText, "--agent-envelope") { + t.Fatalf("%s output root-shape variants omit --agent-envelope: %s", command.Command, conditionText) + } + if len(definition["fieldTree"].(map[string]any)["variants"].([]any)) < 2 { + t.Fatalf("%s output must distinguish agent and non-agent root shapes", command.Command) + } + } + } + } +} + +func TestCLIConditionModelClosesAdoptionOutputRoutes(t *testing.T) { + contract := readCLIContract(t) + definitions := cliContractDefinitionMap(t, contract.ContractDefinitions) + definition := definitions["proofkit.adoption-contract-envelope.output.v1.root-shape"] + if definition == nil { + t.Fatal("adoption output root-shape definition is missing") + } + fieldTree := definition["fieldTree"].(map[string]any) + if fieldTree["conditionModel"] != "cli_flag_conjunction_v1" { + t.Fatalf("conditionModel=%v want cli_flag_conjunction_v1", fieldTree["conditionModel"]) + } + conditionOwners := map[string]string{} + for _, raw := range fieldTree["variants"].([]any) { + variant := raw.(map[string]any) + for _, condition := range stringsFromAny(variant["when"].([]any)) { + if previous, duplicate := conditionOwners[condition]; duplicate { + t.Fatalf("condition %q is owned by %s and %v", condition, previous, variant["variantId"]) + } + conditionOwners[condition] = variant["variantId"].(string) + } + } + if len(conditionOwners) != 12 { + t.Fatalf("adoption output condition cases=%d want 12", len(conditionOwners)) + } + + validConditionOwners := map[string]string{} + validOptionCount := 0 + modeDomain := adoptioncontract.SupportedModes() + pilotDomain := append([]string{""}, adoptioncontract.SupportedPilotVariants()...) + mutatedModes := adoptioncontract.SupportedModes() + if len(mutatedModes) == 0 { + t.Fatal("native mode domain is empty") + } + mutatedModes[0] = "caller-mutant" + if slices.Contains(adoptioncontract.SupportedModes(), "caller-mutant") { + t.Fatal("native mode domain aliases caller-owned memory") + } + mutatedPilots := adoptioncontract.SupportedPilotVariants() + if len(mutatedPilots) == 0 { + t.Fatal("native pilot domain is empty") + } + mutatedPilots[0] = "caller-mutant" + if slices.Contains(adoptioncontract.SupportedPilotVariants(), "caller-mutant") { + t.Fatal("native pilot domain aliases caller-owned memory") + } + for _, mode := range modeDomain { + for _, agentEnvelope := range []bool{false, true} { + for _, materializationManifest := range []bool{false, true} { + for _, pilot := range pilotDomain { + options := adoptioncontract.Options{ + AgentEnvelope: agentEnvelope, + MaterializationManifest: materializationManifest, + Mode: mode, + Pilot: pilot, + } + condition := adoptionOutputCondition(options) + _, matched := conditionOwners[condition] + err := adoptioncontract.ValidateOptions(options) + if err == nil && !matched { + t.Fatalf("valid options %#v have no condition %q", options, condition) + } + if err != nil && matched { + t.Fatalf("invalid options %#v match condition %q owned by %s: %v", options, condition, conditionOwners[condition], err) + } + if err == nil { + validOptionCount++ + validConditionOwners[condition] = conditionOwners[condition] + } + } + } + } + } + if total := len(modeDomain) * 2 * 2 * len(pilotDomain); total != 80 { + t.Fatalf("native normalized adoption option domain=%d want 80", total) + } + if validOptionCount != 12 { + t.Fatalf("valid normalized adoption output options=%d want 12", validOptionCount) + } + if !maps.Equal(validConditionOwners, conditionOwners) { + t.Fatalf("reachable condition owners=%v want exact declared set %v", validConditionOwners, conditionOwners) + } +} + +func adoptionOutputCondition(options adoptioncontract.Options) string { + flagState := func(present bool) string { + if present { + return "present" + } + return "absent" + } + pilot := options.Pilot + if pilot == "" { + pilot = "absent" + } + return fmt.Sprintf( + "--agent-envelope=%s --materialization-manifest=%s --mode=%s --pilot=%s", + flagState(options.AgentEnvelope), + flagState(options.MaterializationManifest), + options.Mode, + pilot, + ) +} + +func adoptionOutputConditionFromParsed(options adoptionContractArgs) string { + return adoptionOutputCondition(adoptioncontract.Options{ + AgentEnvelope: options.agentEnvelope, + MaterializationManifest: options.materializationManifest, + Mode: options.mode, + Pilot: options.explicitPilot(), + }) +} + +func assertRootShapeDefinition(t *testing.T, id string, definition map[string]any) { + t.Helper() + if definition["closed"] != true { + t.Fatalf("%s root-shape definition is not closed", id) + } + if refs := definition["definitionRefs"].([]any); len(refs) != 0 { + t.Fatalf("%s root-shape definition has nested refs: %v", id, refs) + } + fieldTree := definition["fieldTree"].(map[string]any) + expectedFieldTreeKeys := []string{"kind", "nonClaims", "variants"} + if _, present := fieldTree["conditionModel"]; present { + expectedFieldTreeKeys = []string{"conditionModel", "kind", "nonClaims", "variants"} + } + assertStringSet(t, sortedMapKeys(fieldTree), expectedFieldTreeKeys, id+" fieldTree keys") + if fieldTree["kind"] != "root_shape_only" { + t.Fatalf("%s fieldTree kind=%v want root_shape_only", id, fieldTree["kind"]) + } + assertStringSet(t, stringsFromAny(fieldTree["nonClaims"].([]any)), []string{ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection.", + }, id+" non-claims") + variants := fieldTree["variants"].([]any) + if len(variants) == 0 { + t.Fatalf("%s has no root-shape variants", id) + } + rootKinds := map[string]struct{}{} + for _, raw := range variants { + variant := raw.(map[string]any) + assertStringSet(t, sortedMapKeys(variant), []string{"allowedFields", "requiredFields", "rootKind", "variantId", "when"}, id+" variant keys") + allowed := stringsFromAny(variant["allowedFields"].([]any)) + required := stringsFromAny(variant["requiredFields"].([]any)) + assertSortedUnique(t, allowed, id+" allowed root fields") + assertSortedUnique(t, required, id+" required root fields") + for _, field := range required { + if !slices.Contains(allowed, field) { + t.Fatalf("%s variant %v requires root field %s outside allowed fields", id, variant["variantId"], field) + } + } + rootKind := variant["rootKind"].(string) + rootKinds[rootKind] = struct{}{} + if rootKind != "object" && (len(allowed) != 0 || len(required) != 0) { + t.Fatalf("%s variant %v non-object root declares fields", id, variant["variantId"]) + } + if when := stringsFromAny(variant["when"].([]any)); len(when) == 0 { + t.Fatalf("%s variant %v has no CLI condition", id, variant["variantId"]) + } + } + switch definition["rootType"] { + case "object": + if len(rootKinds) != 1 { + t.Fatalf("%s object root has kinds %v", id, rootKinds) + } + if _, ok := rootKinds["object"]; !ok { + t.Fatalf("%s object root omits object variant", id) + } + case "union": + if len(rootKinds) < 2 { + t.Fatalf("%s union does not enumerate distinct root kinds: %v", id, rootKinds) + } + if _, unbounded := rootKinds["json_value"]; unbounded { + t.Fatalf("%s union includes unbounded json_value", id) + } + case "json_value": + if len(variants) != 1 { + t.Fatalf("%s json_value root has %d variants, want one", id, len(variants)) + } + if _, ok := rootKinds["json_value"]; !ok { + t.Fatalf("%s json_value root is not explicitly unconstrained", id) + } + default: + t.Fatalf("%s has unsupported rootType %v", id, definition["rootType"]) + } +} + +func rootShapeConditionText(t *testing.T, definition map[string]any) string { + t.Helper() + return strings.Join(rootShapeConditions(t, definition), "\n") +} + +func rootShapeConditions(t *testing.T, definition map[string]any) []string { + t.Helper() + fieldTree := definition["fieldTree"].(map[string]any) + conditions := []string{} + for _, raw := range fieldTree["variants"].([]any) { + variant := raw.(map[string]any) + conditions = append(conditions, stringsFromAny(variant["when"].([]any))...) + } + return conditions +} + +func assertPublicCLIRootVariant(t *testing.T, commandName string, direction string, variantID string, value any) { + t.Helper() + contract := readCLIContract(t) + definitions := cliContractDefinitionMap(t, contract.ContractDefinitions) + var command *cliContractCommand + for index := range contract.Commands { + if contract.Commands[index].Command == commandName { + command = &contract.Commands[index] + break + } + } + if command == nil { + t.Fatalf("root oracle command %s is missing", commandName) + } + rawContract := command.OutputContract + if direction == "input" { + rawContract = command.InputContract + } + binding := canonicalJSONValue(t, rawContract).(map[string]any) + definition := definitions[binding["rootDefinitionRef"].(string)] + fieldTree := definition["fieldTree"].(map[string]any) + var selected map[string]any + for _, raw := range fieldTree["variants"].([]any) { + variant := raw.(map[string]any) + if variant["variantId"] == variantID { + selected = variant + break + } + } + if selected == nil { + t.Fatalf("%s %s root variant %s is missing", commandName, direction, variantID) + } + switch selected["rootKind"] { + case "array": + if _, ok := value.([]any); !ok { + t.Fatalf("%s %s variant %s requires array root, got %T", commandName, direction, variantID, value) + } + case "json_value": + return + case "object": + record, ok := value.(map[string]any) + if !ok { + t.Fatalf("%s %s variant %s requires object root, got %T", commandName, direction, variantID, value) + } + allowed := stringsFromAny(selected["allowedFields"].([]any)) + for key := range record { + if !slices.Contains(allowed, key) { + t.Fatalf("%s %s variant %s emitted uncontracted root field %s; allowed=%v", commandName, direction, variantID, key, allowed) + } + } + for _, field := range stringsFromAny(selected["requiredFields"].([]any)) { + if _, exists := record[field]; !exists { + t.Fatalf("%s %s variant %s omitted required root field %s; value=%v", commandName, direction, variantID, field, record) + } + } + default: + t.Fatalf("%s %s variant %s has unsupported root kind %v", commandName, direction, variantID, selected["rootKind"]) + } +} + +func assertPublicCLIRootVariantCondition(t *testing.T, commandName string, direction string, variantID string, condition string) { + t.Helper() + contract := readCLIContract(t) + definitions := cliContractDefinitionMap(t, contract.ContractDefinitions) + var command *cliContractCommand + for index := range contract.Commands { + if contract.Commands[index].Command == commandName { + command = &contract.Commands[index] + break + } + } + if command == nil { + t.Fatalf("condition oracle command %s is missing", commandName) + } + rawContract := command.OutputContract + if direction == "input" { + rawContract = command.InputContract + } + binding := canonicalJSONValue(t, rawContract).(map[string]any) + definition := definitions[binding["rootDefinitionRef"].(string)] + for _, raw := range definition["fieldTree"].(map[string]any)["variants"].([]any) { + variant := raw.(map[string]any) + if variant["variantId"] != variantID { + continue + } + if !slices.Contains(stringsFromAny(variant["when"].([]any)), condition) { + t.Fatalf("%s %s variant %s omits exact condition %q", commandName, direction, variantID, condition) + } + return + } + t.Fatalf("%s %s root variant %s is missing", commandName, direction, variantID) +} + +func TestContractMapDecisionTreeHasThreeCells(t *testing.T) { + content, err := os.ReadFile(filepath.Join("..", "..", "docs", "proofkit-contract-map.md")) + if err != nil { + t.Fatal(err) + } + inDecisionTree := false + rowCount := 0 + for _, line := range strings.Split(string(content), "\n") { + if line == "Decision tree:" { + inDecisionTree = true + continue + } + if !inDecisionTree { + continue + } + if line == "## Routing Rules" { + break + } + if !strings.HasPrefix(line, "|") { + continue + } + rowCount++ + if separators := strings.Count(line, "|"); separators != 4 { + t.Fatalf("decision-tree row has %d separators, want 4 for three cells: %s", separators, line) + } + } + if rowCount < 3 { + t.Fatalf("decision-tree row count=%d, want a non-empty three-column table", rowCount) + } +} + +func assertBoundCommandContract(t *testing.T, command string, direction string, raw any, definitions map[string]map[string]any, wantDigest string, selectorKey string) { + t.Helper() + if raw == nil { + t.Fatalf("%s missing %s contract", command, direction) + } + value := canonicalJSONValue(t, raw).(map[string]any) + for _, field := range []string{"contractId", "schemaVersion", "rootType", "closed", "rootDefinitionRef", "rootDefinitionDigest", "ownerRequirementRefs", selectorKey, "compatibilitySummary"} { + if _, ok := value[field]; !ok { + t.Fatalf("%s %s contract missing %s", command, direction, field) + } + } + if (value["rootType"] != "object" && value["rootType"] != "json_value" && value["rootType"] != "union") || value["closed"] != true { + t.Fatalf("%s %s contract root is not closed: %#v", command, direction, value) + } + source, hasSource := value["nativeSource"].(map[string]any) + rawSources, hasSources := value["nativeSources"].([]any) + if hasSource == hasSources { + t.Fatalf("%s %s contract must declare exactly one native source form", command, direction) + } + if hasSource && source["evidenceClass"] != "source_checkout" { + t.Fatalf("%s %s native source is not source-checkout evidence", command, direction) + } + for index, rawSource := range rawSources { + source, ok := rawSource.(map[string]any) + if !ok || source["evidenceClass"] != "source_checkout" { + t.Fatalf("%s %s nativeSources[%d] is not source-checkout evidence", command, direction, index) + } + } + selector := value[selectorKey].(map[string]any) + if selector["evidenceClass"] != "source_checkout" { + t.Fatalf("%s %s selector is not source-checkout evidence", command, direction) + } + resolved := resolvedCommandContract(t, value, definitions) + encoded, err := json.Marshal(resolved) + if err != nil { + t.Fatal(err) + } + sum := sha256.Sum256(encoded) + gotDigest := "sha256:" + fmt.Sprintf("%x", sum[:]) + if gotDigest != wantDigest { + t.Fatalf("%s %s generated digest=%s want %s", command, direction, wantDigest, gotDigest) + } +} + func TestRequirementBrowserHelpMatchesWorkspaceInputContract(t *testing.T) { contract := readCLIContract(t) var browser cliContractCommand @@ -172,6 +728,7 @@ func TestProofkitContractMapRoutesRequiredInputCommands(t *testing.T) { func TestCLIContractPublicABIGoldenStable(t *testing.T) { contract := readCLIContract(t) + definitions := cliContractDefinitionMap(t, contract.ContractDefinitions) commands := []any{} for _, command := range contract.Commands { record := map[string]any{ @@ -192,14 +749,24 @@ func TestCLIContractPublicABIGoldenStable(t *testing.T) { if command.ContractEnvelope != nil { record["contractEnvelope"] = *command.ContractEnvelope } + metadata := generatedCommandContractMetadataByName[command.Command] + if command.InputContract != nil { + record["inputContract"] = resolvedCommandContract(t, canonicalJSONValue(t, command.InputContract).(map[string]any), definitions) + record["inputContractSHA256"] = metadata.InputContractSHA256 + } + if command.OutputContract != nil { + record["outputContract"] = resolvedCommandContract(t, canonicalJSONValue(t, command.OutputContract).(map[string]any), definitions) + record["outputContractSHA256"] = metadata.OutputContractSHA256 + } commands = append(commands, record) } abi := map[string]any{ - "commands": commands, - "contractId": contract.ContractID, - "packageName": contract.PackageName, - "processContract": contract.ProcessContract, - "schemaVersion": contract.SchemaVersion, + "commands": commands, + "contractDefinitions": contract.ContractDefinitions, + "contractId": contract.ContractID, + "packageName": contract.PackageName, + "processContract": contract.ProcessContract, + "schemaVersion": contract.SchemaVersion, } encoded, err := json.Marshal(abi) if err != nil { @@ -212,6 +779,50 @@ func TestCLIContractPublicABIGoldenStable(t *testing.T) { } } +func cliContractDefinitionMap(t *testing.T, raw []any) map[string]map[string]any { + t.Helper() + definitions := make(map[string]map[string]any, len(raw)) + for _, value := range raw { + record := canonicalJSONValue(t, value).(map[string]any) + id, _ := record["definitionId"].(string) + if id == "" { + t.Fatal("CLI contract definition has no definitionId") + } + definitions[id] = record + } + return definitions +} + +func resolvedCommandContract(t *testing.T, contract map[string]any, definitions map[string]map[string]any) map[string]any { + t.Helper() + rootID, _ := contract["rootDefinitionRef"].(string) + return map[string]any{ + "contract": contract, + "resolvedRootDefinition": resolveCLIContractDefinition(t, rootID, definitions, map[string]bool{}), + } +} + +func resolveCLIContractDefinition(t *testing.T, id string, definitions map[string]map[string]any, visiting map[string]bool) map[string]any { + t.Helper() + if visiting[id] { + t.Fatalf("CLI contract definition cycle includes %s", id) + } + definition, ok := definitions[id] + if !ok { + t.Fatalf("CLI contract references missing definition %s", id) + } + visiting[id] = true + resolved := maps.Clone(definition) + references := stringsFromAny(resolved["definitionRefs"].([]any)) + children := make([]any, 0, len(references)) + for _, reference := range references { + children = append(children, resolveCLIContractDefinition(t, reference, definitions, visiting)) + } + delete(visiting, id) + resolved["resolvedDefinitionRefs"] = children + return resolved +} + func TestCommandDescriptorContractParityRejectsMutations(t *testing.T) { contract := readCLIContract(t) cases := []struct { @@ -722,7 +1333,7 @@ func TestDescriptorFlagConstraintsAreRenderedTruthfully(t *testing.T) { "requirement-browser-server": "agentic-proofkit requirement-browser-server --input [--empty-local-environment-policy] [--host 127.0.0.1|::1] [--input-pointer ] [--local-environment-class ] [--open] [--port ] [--scope ] [--serve] [--session-mode browse|one-shot-question] [--session-timeout-seconds <1..7200>] --view ", "requirement-context-compose": "agentic-proofkit requirement-context-compose --input [--input-pointer ] --repo-root ", "requirement-proof-resolver": "agentic-proofkit requirement-proof-resolver --input [--input-pointer ] (--empty-local-environment-policy | --local-environment-class )", - "stack-preset": "agentic-proofkit stack-preset --preset ", + "stack-preset": "agentic-proofkit stack-preset --preset ", "typescript-public-api-surfaces": "agentic-proofkit typescript-public-api-surfaces --input [--input-pointer ] --repo-root ", } constrainedCount := 0 @@ -775,11 +1386,12 @@ func TestDescriptorFlagConstraintsExecuteBeforeCommandDispatch(t *testing.T) { } type cliContract struct { - Commands []cliContractCommand `json:"commands"` - ContractID string `json:"contractId"` - PackageName string `json:"packageName"` - ProcessContract any `json:"processContract"` - SchemaVersion int `json:"schemaVersion"` + Commands []cliContractCommand `json:"commands"` + ContractDefinitions []any `json:"contractDefinitions"` + ContractID string `json:"contractId"` + PackageName string `json:"packageName"` + ProcessContract any `json:"processContract"` + SchemaVersion int `json:"schemaVersion"` } type cliContractCommand struct { @@ -824,7 +1436,7 @@ func assertCLIContractSchema(t *testing.T) { if err != nil { t.Fatalf("decode raw CLI contract: %v", err) } - assertKeys(t, "CLI contract", keys(record), []string{"commands", "contractId", "packageName", "processContract", "schemaVersion"}) + assertKeys(t, "CLI contract", keys(record), []string{"commands", "contractDefinitions", "contractId", "packageName", "processContract", "schemaVersion"}) var processContract map[string]json.RawMessage if err := json.Unmarshal(record["processContract"], &processContract); err != nil { t.Fatalf("decode process contract: %v", err) @@ -1340,18 +1952,38 @@ func TestAgentRouteInputContractMatchesAdmission(t *testing.T) { if route.OutputContract == nil { t.Fatal("agent-route must expose its versioned output contract") } - got := canonicalJSONValue(t, route.InputContract) + got := commandOwnedContractProjection(t, route.InputContract) want := canonicalJSONValue(t, agentroute.InputContract()) if !reflect.DeepEqual(got, want) { t.Fatalf("agent-route input contract drift\ngot: %#v\nwant: %#v", got, want) } - gotOutput := canonicalJSONValue(t, route.OutputContract) + gotOutput := commandOwnedContractProjection(t, route.OutputContract) wantOutput := canonicalJSONValue(t, agentroute.OutputContract()) if !reflect.DeepEqual(gotOutput, wantOutput) { t.Fatalf("agent-route output contract drift\ngot: %#v\nwant: %#v", gotOutput, wantOutput) } } +func commandOwnedContractProjection(t *testing.T, value any) any { + t.Helper() + record := canonicalJSONValue(t, value).(map[string]any) + for _, key := range []string{ + "closed", + "compatibilitySummary", + "nativeAdmissionWitnessSelector", + "nativeOutputWitnessSelector", + "nativeSource", + "nativeSources", + "ownerRequirementRefs", + "rootDefinitionDigest", + "rootDefinitionRef", + "rootType", + } { + delete(record, key) + } + return record +} + func canonicalJSONValue(t *testing.T, value any) any { t.Helper() content, err := json.Marshal(value) diff --git a/internal/app/cli_output_witness_contract_test.go b/internal/app/cli_output_witness_contract_test.go new file mode 100644 index 0000000..d4d3afb --- /dev/null +++ b/internal/app/cli_output_witness_contract_test.go @@ -0,0 +1,510 @@ +package app + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "slices" + "sort" + "strings" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" +) + +func TestRootDistinctOutputWitnessBindingsAreExact(t *testing.T) { + contract := readCLIContract(t) + bindings := readRootDistinctOutputBindings(t) + if err := validateRootDistinctOutputWitnessBindings(contract, bindings); err != nil { + t.Fatalf("current root-distinct output witness inventory is invalid: %v", err) + } + + mutants := []struct { + name string + mutate func(t *testing.T, contract *cliContract, bindings *rootDistinctOutputBindingFile) + }{ + { + name: "empty", + mutate: func(t *testing.T, contract *cliContract, bindings *rootDistinctOutputBindingFile) { + for index := range bindings.Bindings { + selectors := make([]rootDistinctOutputWitnessSelector, 0, len(bindings.Bindings[index].WitnessSelectors)) + for _, selector := range bindings.Bindings[index].WitnessSelectors { + if !isRootDistinctOutputSelector(selector.Selector) { + selectors = append(selectors, selector) + } + } + bindings.Bindings[index].WitnessSelectors = selectors + } + }, + }, + { + name: "missing", + mutate: func(t *testing.T, contract *cliContract, bindings *rootDistinctOutputBindingFile) { + binding := rootDistinctBindingForMutation(t, bindings, "proofkit.package-boundary.cli-output-root-witnesses") + binding.WitnessSelectors = binding.WitnessSelectors[:len(binding.WitnessSelectors)-1] + }, + }, + { + name: "surplus", + mutate: func(t *testing.T, contract *cliContract, bindings *rootDistinctOutputBindingFile) { + binding := rootDistinctBindingForMutation(t, bindings, "proofkit.package-boundary.cli-output-root-witnesses") + binding.WitnessSelectors = append(binding.WitnessSelectors, binding.WitnessSelectors[0]) + }, + }, + { + name: "selector-substitution", + mutate: func(t *testing.T, contract *cliContract, bindings *rootDistinctOutputBindingFile) { + output := rootDistinctOutputContractForMutation(t, contract, "adoption-contract-envelope") + output["nativeOutputWitnessSelector"].(map[string]any)["test"] = "TestSelfCheckOutputUsesExactRootShape" + }, + }, + { + name: "source-missing", + mutate: func(t *testing.T, contract *cliContract, bindings *rootDistinctOutputBindingFile) { + output := rootDistinctOutputContractForMutation(t, contract, "pilot-admission") + sources := output["nativeSources"].([]any) + output["nativeSources"] = sources[1:] + }, + }, + { + name: "source-surplus", + mutate: func(t *testing.T, contract *cliContract, bindings *rootDistinctOutputBindingFile) { + output := rootDistinctOutputContractForMutation(t, contract, "pilot-admission") + output["nativeSources"] = append(output["nativeSources"].([]any), map[string]any{ + "path": "internal/command/stackpreset", + "canonicalDigest": "sha256:" + strings.Repeat("a", 64), + "evidenceClass": "source_checkout", + }) + }, + }, + { + name: "source-substitution", + mutate: func(t *testing.T, contract *cliContract, bindings *rootDistinctOutputBindingFile) { + output := rootDistinctOutputContractForMutation(t, contract, "pilot-admission") + output["nativeSources"].([]any)[0].(map[string]any)["path"] = "internal/kernel/admission" + }, + }, + { + name: "native-source-form-downgrade", + mutate: func(t *testing.T, contract *cliContract, bindings *rootDistinctOutputBindingFile) { + output := rootDistinctOutputContractForMutation(t, contract, "pilot-admission") + sources := output["nativeSources"].([]any) + output["nativeSource"] = sources[len(sources)-1] + delete(output, "nativeSources") + }, + }, + { + name: "path-relocation", + mutate: func(t *testing.T, contract *cliContract, bindings *rootDistinctOutputBindingFile) { + output := rootDistinctOutputContractForMutation(t, contract, "self-check") + output["nativeOutputWitnessSelector"].(map[string]any)["path"] = "internal/app/app_test.go" + }, + }, + { + name: "direction-transfer", + mutate: func(t *testing.T, contract *cliContract, bindings *rootDistinctOutputBindingFile) { + command := rootDistinctCommandForMutation(t, contract, "self-check") + input := cloneRootDistinctJSONFixture(t, command.OutputContract).(map[string]any) + input["nativeAdmissionWitnessSelector"] = input["nativeOutputWitnessSelector"] + delete(input, "nativeOutputWitnessSelector") + command.InputContract = input + command.OutputContract = nil + }, + }, + { + name: "scenario-transfer", + mutate: func(t *testing.T, contract *cliContract, bindings *rootDistinctOutputBindingFile) { + binding := rootDistinctBindingForMutation(t, bindings, "proofkit.supply-chain-quality.cli-abi-golden") + binding.ScenarioID = "proofkit.supply-chain-quality.cli-abi-mutant" + }, + }, + { + name: "contract-command-drift", + mutate: func(t *testing.T, contract *cliContract, bindings *rootDistinctOutputBindingFile) { + output := rootDistinctOutputContractForMutation(t, contract, "self-check") + output["nativeOutputWitnessSelector"].(map[string]any)["command"] = "go test ./internal/app -run '^TestCLIABIGoldenCorpus$'" + }, + }, + { + name: "binding-command-drift", + mutate: func(t *testing.T, contract *cliContract, bindings *rootDistinctOutputBindingFile) { + binding := rootDistinctBindingForMutation(t, bindings, "proofkit.package-boundary.cli-output-root-witnesses") + binding.WitnessSelectors[0].Command = "go test ./internal/app -run '^TestCLIABIGoldenCorpus$'" + }, + }, + { + name: "owner-transfer", + mutate: func(t *testing.T, contract *cliContract, bindings *rootDistinctOutputBindingFile) { + binding := rootDistinctBindingForMutation(t, bindings, "proofkit.package-boundary.cli-output-root-witnesses") + binding.RequirementID = "REQ-PROOFKIT-QUALITY-009" + }, + }, + { + name: "binding-path-relocation", + mutate: func(t *testing.T, contract *cliContract, bindings *rootDistinctOutputBindingFile) { + binding := rootDistinctBindingForMutation(t, bindings, "proofkit.spec-proof-core.adoption-contract-envelope-cli-abi") + binding.WitnessPath = "internal/command/adoptioncontract/adoptioncontract_test.go" + }, + }, + { + name: "command-transfer", + mutate: func(t *testing.T, contract *cliContract, bindings *rootDistinctOutputBindingFile) { + rootDistinctCommandForMutation(t, contract, "pilot-admission").Command = "pilot-admission-mutant" + }, + }, + } + for _, mutant := range mutants { + t.Run(mutant.name, func(t *testing.T) { + mutantContract := cloneRootDistinctJSONFixture(t, contract) + mutantBindings := cloneRootDistinctJSONFixture(t, bindings) + mutant.mutate(t, &mutantContract, &mutantBindings) + if err := validateRootDistinctOutputWitnessBindings(mutantContract, mutantBindings); err == nil { + t.Fatal("mutant preserved the exact root-distinct output witness inventory") + } + }) + } +} + +type rootDistinctOutputBindingFile struct { + Bindings []rootDistinctOutputBinding `json:"bindings"` +} + +type rootDistinctOutputBinding struct { + RequirementID string `json:"requirementId"` + ScenarioID string `json:"scenarioId"` + WitnessPath string `json:"witnessPath"` + WitnessSelectors []rootDistinctOutputWitnessSelector `json:"witnessSelectors"` +} + +type rootDistinctOutputWitnessSelector struct { + Command string `json:"command"` + Selector string `json:"selector"` +} + +type rootDistinctOutputContractExpectation struct { + Command string + NativeSourceForm string + NativeSourcePaths []string + SelectorPath string + SelectorTest string + ExecutableCommand string +} + +type rootDistinctOutputBindingMapping struct { + RequirementID string + ScenarioID string + SelectorTest string +} + +type rootDistinctOutputContractBoundary struct { + Command string + Direction string + NativeSourceForm string + NativeSourcePaths []string + ContractSelectorPath string + ContractSelectorTest string + ContractExecutableCommand string +} + +type rootDistinctOutputWitnessTuple struct { + Command string `json:"command"` + Direction string `json:"direction"` + NativeSourceForm string `json:"nativeSourceForm"` + NativeSourcePaths []string `json:"nativeSourcePaths"` + ContractSelectorPath string `json:"contractSelectorPath"` + ContractSelectorTest string `json:"contractSelectorTest"` + ContractExecutableCommand string `json:"contractExecutableCommand"` + BindingWitnessPath string `json:"bindingWitnessPath"` + BindingExecutableCommand string `json:"bindingExecutableCommand"` + RequirementID string `json:"requirementId"` + ScenarioID string `json:"scenarioId"` +} + +func validateRootDistinctOutputWitnessBindings(contract cliContract, bindings rootDistinctOutputBindingFile) error { + got := rootDistinctOutputWitnessTuples(contract, bindings) + want := expectedRootDistinctOutputWitnessTuples() + gotKeys := rootDistinctOutputWitnessTupleKeys(got) + wantKeys := rootDistinctOutputWitnessTupleKeys(want) + if !slices.Equal(gotKeys, wantKeys) { + return fmt.Errorf("root-distinct output witness tuples=%v, want exact %v", gotKeys, wantKeys) + } + return nil +} + +func rootDistinctOutputWitnessTuples(contract cliContract, bindings rootDistinctOutputBindingFile) []rootDistinctOutputWitnessTuple { + contractBoundaries := rootDistinctOutputContractBoundaries(contract) + tuples := []rootDistinctOutputWitnessTuple{} + for _, binding := range bindings.Bindings { + for _, selector := range binding.WitnessSelectors { + if !isRootDistinctOutputSelector(selector.Selector) { + continue + } + for _, boundary := range contractBoundaries { + if boundary.ContractSelectorTest != selector.Selector { + continue + } + tuples = append(tuples, rootDistinctOutputWitnessTuple{ + Command: boundary.Command, + Direction: boundary.Direction, + NativeSourceForm: boundary.NativeSourceForm, + NativeSourcePaths: cloneStrings(boundary.NativeSourcePaths), + ContractSelectorPath: boundary.ContractSelectorPath, + ContractSelectorTest: boundary.ContractSelectorTest, + ContractExecutableCommand: boundary.ContractExecutableCommand, + BindingWitnessPath: binding.WitnessPath, + BindingExecutableCommand: selector.Command, + RequirementID: binding.RequirementID, + ScenarioID: binding.ScenarioID, + }) + } + } + } + return tuples +} + +func rootDistinctOutputContractBoundaries(contract cliContract) []rootDistinctOutputContractBoundary { + expectedByCommand := map[string]rootDistinctOutputContractExpectation{} + for _, expected := range rootDistinctOutputContractExpectations() { + expectedByCommand[expected.Command] = expected + } + boundaries := []rootDistinctOutputContractBoundary{} + for _, command := range contract.Commands { + for _, direction := range []struct { + name string + raw any + selectorKey string + }{ + {name: "input", raw: command.InputContract, selectorKey: "nativeAdmissionWitnessSelector"}, + {name: "output", raw: command.OutputContract, selectorKey: "nativeOutputWitnessSelector"}, + } { + value, ok := direction.raw.(map[string]any) + if !ok { + continue + } + selector, _ := value[direction.selectorKey].(map[string]any) + selectorTest, _ := selector["test"].(string) + _, targetCommand := expectedByCommand[command.Command] + if !isRootDistinctOutputSelector(selectorTest) && !(targetCommand && direction.name == "output") { + continue + } + sourceForm, sourcePaths := rootDistinctNativeSourcePaths(value) + selectorPath, _ := selector["path"].(string) + executableCommand, _ := selector["command"].(string) + boundaries = append(boundaries, rootDistinctOutputContractBoundary{ + Command: command.Command, + Direction: direction.name, + NativeSourceForm: sourceForm, + NativeSourcePaths: sourcePaths, + ContractSelectorPath: selectorPath, + ContractSelectorTest: selectorTest, + ContractExecutableCommand: executableCommand, + }) + } + } + return boundaries +} + +func rootDistinctNativeSourcePaths(contract map[string]any) (string, []string) { + rawSource, hasSource := contract["nativeSource"] + rawSources, hasSources := contract["nativeSources"] + paths := []string{} + if source, ok := rawSource.(map[string]any); ok { + if path, ok := source["path"].(string); ok { + paths = append(paths, path) + } + } + if sources, ok := rawSources.([]any); ok { + for _, raw := range sources { + source, _ := raw.(map[string]any) + if path, ok := source["path"].(string); ok { + paths = append(paths, path) + } + } + } + sort.Strings(paths) + switch { + case hasSource && !hasSources: + return "nativeSource", paths + case !hasSource && hasSources: + return "nativeSources", paths + case hasSource && hasSources: + return "nativeSource+nativeSources", paths + default: + return "missing", paths + } +} + +func expectedRootDistinctOutputWitnessTuples() []rootDistinctOutputWitnessTuple { + expectedBySelector := map[string]rootDistinctOutputContractExpectation{} + for _, expected := range rootDistinctOutputContractExpectations() { + expectedBySelector[expected.SelectorTest] = expected + } + tuples := make([]rootDistinctOutputWitnessTuple, 0, len(rootDistinctOutputBindingMappings())) + for _, mapping := range rootDistinctOutputBindingMappings() { + expected := expectedBySelector[mapping.SelectorTest] + tuples = append(tuples, rootDistinctOutputWitnessTuple{ + Command: expected.Command, + Direction: "output", + NativeSourceForm: expected.NativeSourceForm, + NativeSourcePaths: cloneStrings(expected.NativeSourcePaths), + ContractSelectorPath: expected.SelectorPath, + ContractSelectorTest: expected.SelectorTest, + ContractExecutableCommand: expected.ExecutableCommand, + BindingWitnessPath: expected.SelectorPath, + BindingExecutableCommand: expected.ExecutableCommand, + RequirementID: mapping.RequirementID, + ScenarioID: mapping.ScenarioID, + }) + } + return tuples +} + +func rootDistinctOutputContractExpectations() []rootDistinctOutputContractExpectation { + return []rootDistinctOutputContractExpectation{ + { + Command: "adoption-contract-envelope", + NativeSourceForm: "nativeSource", + NativeSourcePaths: []string{"internal/command/adoptioncontract"}, + SelectorPath: "internal/app/cli_abi_test.go", + SelectorTest: "TestAdoptionContractEnvelopeCLIABI", + ExecutableCommand: "go test ./internal/app -run '^TestAdoptionContractEnvelopeCLIABI$'", + }, + { + Command: "pilot-admission", + NativeSourceForm: "nativeSources", + NativeSourcePaths: []string{"internal/app", "internal/command/pilotadmission"}, + SelectorPath: "internal/app/cli_abi_test.go", + SelectorTest: "TestStandaloneMultiVariantCommandsUseExactRootShapes", + ExecutableCommand: "go test ./internal/app -run '^TestStandaloneMultiVariantCommandsUseExactRootShapes$'", + }, + { + Command: "self-check", + NativeSourceForm: "nativeSource", + NativeSourcePaths: []string{"internal/app"}, + SelectorPath: "internal/app/cli_abi_test.go", + SelectorTest: "TestSelfCheckOutputUsesExactRootShape", + ExecutableCommand: "go test ./internal/app -run '^TestSelfCheckOutputUsesExactRootShape$'", + }, + } +} + +func rootDistinctOutputBindingMappings() []rootDistinctOutputBindingMapping { + return []rootDistinctOutputBindingMapping{ + { + RequirementID: "REQ-PROOFKIT-PACKAGE-002", + ScenarioID: "proofkit.package-boundary.cli-output-root-witnesses", + SelectorTest: "TestAdoptionContractEnvelopeCLIABI", + }, + { + RequirementID: "REQ-PROOFKIT-PACKAGE-002", + ScenarioID: "proofkit.package-boundary.cli-output-root-witnesses", + SelectorTest: "TestSelfCheckOutputUsesExactRootShape", + }, + { + RequirementID: "REQ-PROOFKIT-PACKAGE-002", + ScenarioID: "proofkit.package-boundary.cli-output-root-witnesses", + SelectorTest: "TestStandaloneMultiVariantCommandsUseExactRootShapes", + }, + { + RequirementID: "REQ-PROOFKIT-QUALITY-004", + ScenarioID: "proofkit.supply-chain-quality.cli-abi-golden", + SelectorTest: "TestAdoptionContractEnvelopeCLIABI", + }, + { + RequirementID: "REQ-PROOFKIT-QUALITY-004", + ScenarioID: "proofkit.supply-chain-quality.cli-abi-golden", + SelectorTest: "TestSelfCheckOutputUsesExactRootShape", + }, + { + RequirementID: "REQ-PROOFKIT-QUALITY-004", + ScenarioID: "proofkit.supply-chain-quality.cli-abi-golden", + SelectorTest: "TestStandaloneMultiVariantCommandsUseExactRootShapes", + }, + { + RequirementID: "REQ-PROOFKIT-SPEC-011", + ScenarioID: "proofkit.spec-proof-core.adoption-contract-envelope-cli-abi", + SelectorTest: "TestAdoptionContractEnvelopeCLIABI", + }, + } +} + +func isRootDistinctOutputSelector(selector string) bool { + for _, expected := range rootDistinctOutputContractExpectations() { + if expected.SelectorTest == selector { + return true + } + } + return false +} + +func rootDistinctOutputWitnessTupleKeys(tuples []rootDistinctOutputWitnessTuple) []string { + keys := make([]string, 0, len(tuples)) + for _, tuple := range tuples { + encoded, err := json.Marshal(tuple) + if err != nil { + panic(err) + } + keys = append(keys, string(encoded)) + } + sort.Strings(keys) + return keys +} + +func readRootDistinctOutputBindings(t *testing.T) rootDistinctOutputBindingFile { + t.Helper() + file, err := os.Open(filepath.Join(repoRoot(t), "proofkit", "requirement-bindings.json")) + if err != nil { + t.Fatalf("open requirement bindings: %v", err) + } + defer file.Close() + bindings, err := admission.DecodeTypedJSON[rootDistinctOutputBindingFile](file, 16<<20) + if err != nil { + t.Fatalf("decode requirement bindings: %v", err) + } + return bindings +} + +func rootDistinctCommandForMutation(t *testing.T, contract *cliContract, command string) *cliContractCommand { + t.Helper() + for index := range contract.Commands { + if contract.Commands[index].Command == command { + return &contract.Commands[index] + } + } + t.Fatalf("root-distinct command %s is missing", command) + return nil +} + +func rootDistinctOutputContractForMutation(t *testing.T, contract *cliContract, command string) map[string]any { + t.Helper() + output, ok := rootDistinctCommandForMutation(t, contract, command).OutputContract.(map[string]any) + if !ok { + t.Fatalf("%s output contract is missing", command) + } + return output +} + +func rootDistinctBindingForMutation(t *testing.T, bindings *rootDistinctOutputBindingFile, scenarioID string) *rootDistinctOutputBinding { + t.Helper() + for index := range bindings.Bindings { + if bindings.Bindings[index].ScenarioID == scenarioID { + return &bindings.Bindings[index] + } + } + t.Fatalf("root-distinct binding %s is missing", scenarioID) + return nil +} + +func cloneRootDistinctJSONFixture[T any](t *testing.T, input T) T { + t.Helper() + content, err := json.Marshal(input) + if err != nil { + t.Fatalf("marshal JSON fixture: %v", err) + } + var clone T + if err := json.Unmarshal(content, &clone); err != nil { + t.Fatalf("unmarshal JSON fixture: %v", err) + } + return clone +} diff --git a/internal/app/command_contract_generated.go b/internal/app/command_contract_generated.go new file mode 100644 index 0000000..1ca60c6 --- /dev/null +++ b/internal/app/command_contract_generated.go @@ -0,0 +1,92 @@ +// Code generated by internal/tools/commandcontractgen; DO NOT EDIT. +package app + +const commandContractSourceSHA256 = "90025ee656c5d90b1909423776121f12ce28b74b52d0eae2c34dd1bc55d2c6e9" + +type generatedCommandContractMetadata struct { + InputContractSHA256 string + InputSchemaSummary []string + OutputContractSHA256 string + FlagChoices map[string][]string +} + +var generatedCommandContractMetadataByName = map[string]generatedCommandContractMetadata{ + "adoption-checklist": {InputContractSHA256: "sha256:4e6c4c9b369279837a5894c0b3f842a411dce529b91c91cb2d4ec63eb5ee4c2c", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.adoption-checklist.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:9d0d0e60f0935407fd31007d8502459663eb4c7228dc5e3c7727ae2c9907bdc9", FlagChoices: map[string][]string{}}, + "adoption-contract-envelope": {InputContractSHA256: "sha256:33204c558b8d2d41ede42cbbebaa079ea73cdbd9568b45c0cfd5c61f7a45ccd5", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.adoption-contract-envelope.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:e66dcc00c0a1aee44c4a221f96e2f009918b67cfd9da4b352e087bcf780e4c7c", FlagChoices: map[string][]string{}}, + "adoption-doctor": {InputContractSHA256: "sha256:622f97a9bb82bb5cf772f43fe3ba79db79dc8d82f8f23f1dfb2b82e2d7c8f7ca", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.adoption-doctor.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:901ec6f06d36928738c7603d126858f9bf5f32df8bcd521b5bfa9f053c722bcd", FlagChoices: map[string][]string{}}, + "adoption-workflow-plan": {InputContractSHA256: "sha256:b32ae67179d7b6dcf1ea66cb6b2b2691c8367ce2e2be367619b65973166da55c", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.adoption-workflow-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:8d64cb53ebd0307e3cebc3435286a3d2a1ee8a0ad6f7514fc0fb3285db0f565b", FlagChoices: map[string][]string{}}, + "agent-route": {InputContractSHA256: "sha256:ad0a30069885ee74212d7cd0bed6461ff6d835127b7a7d10f24cb9f941ad2d54", InputSchemaSummary: []string{"availableInputs", "browserMode", "goal", "knownChangedPaths", "mode", "nonClaims", "observedReports", "openBrowser", "routeId", "schemaVersion", "root-shape-only definition proofkit.agent-route.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:805ffb86061551b826671fdab70f3c7396d6ba26d07c44eadb9853d63a35083b", FlagChoices: map[string][]string{}}, + "binding-partition": {InputContractSHA256: "sha256:366ad082045af52b2ac6604f18626d0f285b2db73b45d9a82687b8d3b0d2b3fd", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.binding-partition.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:52840879e13a00ef9a4abaad6cdb33000511674d5f9003fb56f387fdf58fadc8", FlagChoices: map[string][]string{}}, + "branch-authority": {InputContractSHA256: "sha256:8a3ed74978898593fbdbf1f7fa684dae450fbd9019edcd60d07f818d63363ed4", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.branch-authority.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3c7dc74842299b92cd5baf57cc8666e9415963091359e5faf654e28da89561f1", FlagChoices: map[string][]string{}}, + "capability-map-admission": {InputContractSHA256: "sha256:efe87497cf6a32cfd1f9c9c191393cac8781d1e9211914a8a7b6aaa44e7f371d", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.capability-map-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:bfb32535fd51bed168111912acb1651527897fe59a592119dc0032ce9286c9db", FlagChoices: map[string][]string{}}, + "changed-path-set": {InputContractSHA256: "sha256:8fe97426a58969e3e8dcbd52ed44540666b4de6be0487e8a3bc5088ae9c0f933", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.changed-path-set.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:abccdbf78e67f633ce49c34e8849c03f08ce42fa934a4c68969720c5045bf593", FlagChoices: map[string][]string{}}, + "completion-criteria": {InputContractSHA256: "sha256:99c49c44b001e40383787e4c55f66621b8a8315f09635f1baf2326dc09bec4e6", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.completion-criteria.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:c90bb9605c7a22914104701a068dded510534bdeb60f4d601555d46c2d3d8a6d", FlagChoices: map[string][]string{}}, + "conformance-profile": {InputContractSHA256: "sha256:d32e3341a705fcc3da7e4ba356107cb014bb563e6c0c159ce3be34e34ed1c74f", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.conformance-profile.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:f625d4991272f92b536c1b64d0bbdd37c8329198e604011a475831ed3d256ecb", FlagChoices: map[string][]string{}}, + "custom-rule-boundary": {InputContractSHA256: "sha256:18119bc01230b9cb01cbb56bab531c1d2153c689aea3863b24de4f345eec5ede", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.custom-rule-boundary.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:8a80f33974c60f960daa4f9803f2b5dfb34ff323418f5f0ab75d7b348653a34a", FlagChoices: map[string][]string{}}, + "deployment-evidence-admission": {InputContractSHA256: "sha256:c3472b87497be14c107dcf95f2c98594d21ec88e514919fe8fb7da858206421d", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.deployment-evidence-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:818fb2b7dbf67be1da27cc62e78c7bd7fdaddaa38fa5ec80f0cad64fbcb60d90", FlagChoices: map[string][]string{}}, + "document-lifecycle-boundary": {InputContractSHA256: "sha256:393836f0ce8e23df351e01d7dec221908ae76fa9b1a045b96fce229eecc844ed", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.document-lifecycle-boundary.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:65c7c0338df60d83d71df9fce3a7e60ce0b0d86819a38e2a6ffeac58b4abc934", FlagChoices: map[string][]string{}}, + "evidence-graph": {InputContractSHA256: "sha256:82c4f14a91ae1b77d1e338e4a1390eec45093cb184830c5dab11e0d2d192c55e", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.evidence-graph.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:254ee56ab897df021856db23b62e9366ed2dfe26b821efa0da9eff4ef83a2592", FlagChoices: map[string][]string{}}, + "external-consumer": {InputContractSHA256: "sha256:0e820ed1e4ecfbfb2a9b66e11f883511be5d18e501f641c9d3f3366303508d00", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.external-consumer.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:2615a60f4cd21a865ed604aca94bf5066adb4bc8f7a3a2345bd53d181429822f", FlagChoices: map[string][]string{}}, + "gradual-adoption": {InputContractSHA256: "sha256:b82598422d2e18c7160d827836e964db6e9b0c2b14a119c1d4f5c4d18779c17f", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.gradual-adoption.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:75310c4f53b1fd3838d03e8988e7eb5faef61e757149a6f556033e23ddc488ff", FlagChoices: map[string][]string{}}, + "gradual-adoption-bootstrap": {InputContractSHA256: "sha256:453bab6dc3f3c84ab9cd7d375966b1cd881b53f8e8414da4aa837f8215db6f19", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.gradual-adoption-bootstrap.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:c00c79676575f2c5903321e87b4b494a3b6d77219b19552701cecde469db5427", FlagChoices: map[string][]string{}}, + "gradual-adoption-guidance": {InputContractSHA256: "sha256:4752cbac81c864cb3e18a39facfd666a9707314233d54798c7f71e67d7f2800c", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.gradual-adoption-guidance.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:171fed4bb8d32a47fc5ec49796f5b0b55ed666feaccc2fbbfeb12da31d80ecc9", FlagChoices: map[string][]string{}}, + "help": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "", FlagChoices: map[string][]string{}}, + "impact": {InputContractSHA256: "sha256:c67d90fde422b44e765e46712e99a2e32af64caf3eaf98abc3992ec805204153", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.impact.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:f3a528a18045c6c47b9b4eb199272c7b9e322657c8307e051eec49533140d4c6", FlagChoices: map[string][]string{}}, + "init": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:3e59a3002327c759e5e747f8baacaa63a4d6784e1a1c520f0a54e01af3f2faa0", FlagChoices: map[string][]string{}}, + "json-report-cli-adapter-source": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:5fcab8ffc599c54045ee04e5eb5abbe0175791e74d5f7edb2940e89424c5b4aa", FlagChoices: map[string][]string{}}, + "migration-parity-admission": {InputContractSHA256: "sha256:cefa31d13404e638e321c8d8d73b96082dd86fbaa1af13f0d12c16ab3e95d3e6", InputSchemaSummary: []string{"schemaVersion=1", "paritySetId", "sourceProofOwners[]", "targetProofkitRefs[]", "parityRecords[]", "nonClaims[]", "root-shape-only definition proofkit.migration-parity-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:52ecee9e28f374142c981eba96cccdb8eb017202556bcf11123ffe71160a7db4", FlagChoices: map[string][]string{}}, + "migration-plan": {InputContractSHA256: "sha256:58a62759a634101ce2ca9218184175134bbe5633328e1b23797b94c19fc9b11a", InputSchemaSummary: []string{"schemaVersion=1", "migrationId", "sourceProofOwners[]", "targetProofkitRefs[]", "parityEvidenceRefs[]", "retainedOwners[]", "retirementCandidates[]", "followUpCommands[]", "nonClaims[]", "root-shape-only definition proofkit.migration-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:f14f0381e9dc241357c346315b95b03ef5b23f1d1bbc3b00f111fbe1515ed3ff", FlagChoices: map[string][]string{}}, + "obligation-decision": {InputContractSHA256: "sha256:48d5110ff6f50ddda69bd92fbb47014c835dba6cd36b0ea1751b178619390e18", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.obligation-decision.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:dde199c92f94ea76491a84900ed7803dd534db4df6002792a174dafbddc00889", FlagChoices: map[string][]string{}}, + "package-runtime-dependency-admission": {InputContractSHA256: "sha256:fc85887af9b8fcd899d245f0db30b2f2f68609822fc268126bf999082bb4115f", InputSchemaSummary: []string{"schemaVersion=1", "reportId", "expectedDependencySpec", "expectedLockfileIntegrity", "expectedPackageName", "expectedPackageVersion", "admissibleLocations{}", "packageResolution{}", "nonClaims[]", "root-shape-only definition proofkit.package-runtime-dependency-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:c012032e8c8212fd50bc2e85669cc610609ca2124ebc992c9e88f44a1ad2d5fc", FlagChoices: map[string][]string{}}, + "pilot-admission": {InputContractSHA256: "sha256:6c4d6fb7ba99cb8da806826584807a723891955c83c04d0054a848e303e5ca9a", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.pilot-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:34d2ef2405b2a96937ec9527fed0abbee05d6aa8e41c27bf57512ac3ae44850d", FlagChoices: map[string][]string{}}, + "producer-policy-self-proof": {InputContractSHA256: "sha256:d48e18826000c8d415f3c44b6c686e1da6ed962ef7ca36c9f705de8c68d034f9", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.producer-policy-self-proof.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:e82a3989a743f8babc6069f7af82b1dd1ea62bad8dbb18d95e105b36f74e4276", FlagChoices: map[string][]string{}}, + "proof-obligation-algebra": {InputContractSHA256: "sha256:e8b03035a81579d03e1c7084999cdf2104fa4befa3ab7eecf1ae20db995f5f80", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.proof-obligation-algebra.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:2925965910426487ff135f16d20e654e2be5a880960ffeb62a83773ae0297c15", FlagChoices: map[string][]string{}}, + "proof-receipt-admission": {InputContractSHA256: "sha256:7cb4c4fb60c8b5a37109bbd8c00d567749f7d181bbc905d8bc58155f139c44cb", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.proof-receipt-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3f802ac3fac6762ede51f0e0a151f16dc10b4a20344a3887b3ee8bae43ce94f2", FlagChoices: map[string][]string{}}, + "proof-slice": {InputContractSHA256: "sha256:c6abf98e38371a2afdd005b7d17317aa97fbae7a5aa9672367ee8ea4a53b9258", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.proof-slice.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:73e5f9db8bf966c9818b6581b10c6b20167af4ab1c4b33fa619c040a165125c7", FlagChoices: map[string][]string{}}, + "readiness-closeout": {InputContractSHA256: "sha256:d3a0a2b44372b5970afac5f2ed273dd5e315e9afb58930ecaffee4c3e92e67f8", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.readiness-closeout.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:f51a259cb1e4c1bcaa1de7eb7beec3b0fbd1bd7d4b62d33a74623542f8e4fc37", FlagChoices: map[string][]string{}}, + "receipt-currentness-scope": {InputContractSHA256: "sha256:b581dbabdb74baf30093544409a8d9b467aed4b69695bfe0ac3b9ab005913a2d", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.receipt-currentness-scope.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3b51b4b58ca5080e2dd6e3674f24710b7d0b8e6463b592f4521b160a42709cc4", FlagChoices: map[string][]string{}}, + "receipt-producer-admission": {InputContractSHA256: "sha256:676aa03b2331a094e287dd3f2dfad3a74403b6f8dadb17e420b446aac0b3592c", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.receipt-producer-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:626b910bb8110901d6769c0ec2a14d216d9ca422aaf2f73ef5fc851c60b0847a", FlagChoices: map[string][]string{}}, + "receipt-trust-class": {InputContractSHA256: "sha256:abc502cf3ac2553d6c2f3b57011af8cfb137b4071fb46f973ab72de917aabf50", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.receipt-trust-class.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:31f36036e665b53b5bbb10eb2843e7a0937fd5c2926860d8337c2f880a5dc7a3", FlagChoices: map[string][]string{}}, + "registry-consumer": {InputContractSHA256: "sha256:8b19d55503aeed6359fe6def27cdff1bcd097820a1a4c6ea137e5d151b49b7be", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.registry-consumer.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:0b557f1db529d4527807513a97ebaedc4ae0b3d61b4445647ccd77667674db4b", FlagChoices: map[string][]string{}}, + "registry-consumer-proof-input-compose": {InputContractSHA256: "sha256:80bcaf6de948af9087e886dbf352e071fc49b98c2d4409250bfdf51ed19e84b3", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.registry-consumer-proof-input-compose.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:7dc6c11bde951ae90d54779759b9625751983a7a3aaae02006c9e7747ff149db", FlagChoices: map[string][]string{}}, + "release-authority": {InputContractSHA256: "sha256:807f53ceab20f949fda99c10448f8490e16b22ae5b8915c9f471337321f5ffef", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.release-authority.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:7b01b165b79c37bbee95f272b2ab6e7707ec9c3fd542a2a5a820b676810adb86", FlagChoices: map[string][]string{}}, + "rendered-artifact-freshness": {InputContractSHA256: "sha256:90961665c3b781202babf99b06743cb04e84a09a27bb4a967ce2cde965312f32", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.rendered-artifact-freshness.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:e5227c6c50f175c0c0b98395a8d53e8c582a4ffa51d5744e1c175e56147a15d7", FlagChoices: map[string][]string{}}, + "repo-profile-admission": {InputContractSHA256: "sha256:203a0be8c1fde0d39809b687e012b4aff3b656be0ddc5b93ccd75de7777448a6", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.repo-profile-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:c761a9a6b5bb0aa1344eabb281985823daa7ef19784cf59634dc17cdea06d4a6", FlagChoices: map[string][]string{}}, + "requirement-authoring-plan": {InputContractSHA256: "sha256:9167f6ea1e888c196c14799f30a7657cf7e74a4af53f15ef61b44b9ae82cc5ba", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.requirement-authoring-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:6cca126c3295537f0c7a73892e8c8778bbd10e442290e75dd45b833373312a46", FlagChoices: map[string][]string{}}, + "requirement-bindings": {InputContractSHA256: "sha256:c9fc55b5b8d67849adb8a10b441b73f33a9eb1b6b6afeee7ec698874e1f9cd50", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.requirement-bindings.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:f61039fafcdb2ced7655c1be0bd9b46d689878f27aebdf12975c8e9f32cbd14e", FlagChoices: map[string][]string{}}, + "requirement-browser-server": {InputContractSHA256: "sha256:07bd092a6776545126490f4b35184f2b052cc12af28beda5fc7658cd0b7c7acb", InputSchemaSummary: []string{"workspace mode: schemaVersion=2", "workspace mode: workspaceId", "workspace mode: context=proofkit.requirement-context schemaVersion=2 with strict v1 adapter", "workspace mode: diffInput=proofkit.requirement-semantic-diff-input schemaVersion=2 (optional)", "workspace mode: graphInput=proofkit.requirement-traceability-graph-input schemaVersion=2 (optional)", "--session-mode values: browse|one-shot-question", "one-shot-question requires --view workspace --serve --open", "--session-timeout-seconds is 1..7200 and requires one-shot-question", "source|proof|coverage|spec-tree modes retain their owner input contracts", "root-shape-only definition proofkit.requirement-browser-server.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:51543cb699ab4b64f536307976c49ce21944ee31befe3ab2994906d5b51aaf91", FlagChoices: map[string][]string{}}, + "requirement-context-compose": {InputContractSHA256: "sha256:2eb447e8fb4163c9c9a5bad9fe62daa0c00184e37b97ce960b0bd0c9e9c91a9a", InputSchemaSummary: []string{"schemaVersion=1", "catalogId", "specTree.path", "requirementSources[] (non-empty)", "requirementSources[].nodeId", "requirementSources[].path", "expectedSourceDigest (optional sha256 ref)", "proofBinding.path (optional)", "coverage.path (optional)", "exact catalog paths only; no discovery", "root-shape-only definition proofkit.requirement-context-compose.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:1458969fbb3e6fec27f89a7d46192fbcec3fae371ba66fcf735fccd247c4d289", FlagChoices: map[string][]string{}}, + "requirement-context-slice": {InputContractSHA256: "sha256:8894f3dcca52c1b2eb75cee1f5d005a00bd0d41cd62c5759ec0ac69f431d2fdc", InputSchemaSummary: []string{"schemaVersion=1", "sliceId", "context=proofkit.requirement-context schemaVersion=2 with strict v1 adapter", "query.profile=routing|specification|proof|coverage|review", "query.nodeIds[]|requirementIds[]|ownerIds[]|lifecycleStates[]", "query.maxDepth=0..512", "query.maxNodes=1..4096", "query.maxRequirements=1..16384", "root-shape-only definition proofkit.requirement-context-slice.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:ce06b9b2ca9af894f6f452da5bc872c821721e723c627b26264a6dcecee89819", FlagChoices: map[string][]string{}}, + "requirement-coverage-input-compose": {InputContractSHA256: "sha256:26e46a1438f5633a835f4afaef293bed5417b13f4cd4c0a752d02659fcd094c6", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.requirement-coverage-input-compose.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:4e246b49fd99038d4b1df9ce484ef2214c3129769df2aaf67b398367a451dd86", FlagChoices: map[string][]string{}}, + "requirement-coverage-view": {InputContractSHA256: "sha256:afb8646a0beb81a27815c436423e0b273855217f5d78af28b334682bd8100211", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.requirement-coverage-view.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:0a353e48748887d7c2b4ab0dc68b48c0d4630ff42c954a490f94cfde05b38412", FlagChoices: map[string][]string{}}, + "requirement-impact-input-compose": {InputContractSHA256: "sha256:dd90693ee688680315d371160d5b4de936ad7a1fb9c4b2725c66cead311f983e", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.requirement-impact-input-compose.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3a31f2500447918a4f77ec0c9c385944db603ac602bb7122957ce72cc8fe2f8b", FlagChoices: map[string][]string{}}, + "requirement-proof-resolver": {InputContractSHA256: "sha256:6b032c893c770f260d72976b1ead28455e2775988771eee85cb8065bcdb7a91d", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.requirement-proof-resolver.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:99cf48212bfbfc4667a4951c0b4f4480701bfe5715028b6b05db28ecc0b0ae50", FlagChoices: map[string][]string{}}, + "requirement-proof-source-set": {InputContractSHA256: "sha256:68df60452e7403b2051214bffe045372a90063ba9ef7cf8714cfc1ea5e16fa5b", InputSchemaSummary: []string{"canonicalEnvelope", "sourceSet", "sources", "root-shape-only definition proofkit.requirement-proof-source-set.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:c74a57fd4ada0a06c0fafd31ea1aedb20035c67e8e7ef0fc413ae8bf086143b3", FlagChoices: map[string][]string{}}, + "requirement-proof-view": {InputContractSHA256: "sha256:236b5c87d83c64f13143cab74f2e3e50898f09ff1ec07590d6b6cce51a90ba7d", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.requirement-proof-view.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:c31930829de0cf5fec96b92bb155f21e2836ff8e5ac7b29b6c1b514c9a33390e", FlagChoices: map[string][]string{}}, + "requirement-semantic-diff": {InputContractSHA256: "sha256:c44263f7ddce2949506ccaeb73e32e78c16bad3250cc6575d355f5874d9fdaa4", InputSchemaSummary: []string{"schemaVersion=2", "diffId", "baseContext=proofkit.requirement-context schemaVersion=2", "currentContext=proofkit.requirement-context schemaVersion=2", "strict schemaVersion=1 adapter requires two v1 contexts", "query.requirementIds[] (optional)", "query.ownerIds[] (optional)", "query.maxChanges=1..8192 (optional)", "root-shape-only definition proofkit.requirement-semantic-diff.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:f88bcb2815f7ee1d66ca5ec06d090dd7f2de4ce8430529621742474c0edf992b", FlagChoices: map[string][]string{}}, + "requirement-source-admission": {InputContractSHA256: "sha256:748da7b4f6bba55877cfe51efcbd85fbdab2f2329a9d9668ea2905713c80c417", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.requirement-source-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:2f6a2db4163b1829bd4cdf27890477abd77697d70dfd591943d47b7be798a90e", FlagChoices: map[string][]string{}}, + "requirement-source-transition": {InputContractSHA256: "sha256:fad33de2e47d31a81b2dd1a84214d061dd2b7ec88a5ba0153287aebb29b19bd8", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.requirement-source-transition.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:aedf88574794d4916fd1d8b234538dbfbf358461b4d117ea4d0380e7890182b7", FlagChoices: map[string][]string{}}, + "requirement-source-view": {InputContractSHA256: "sha256:0819889f9bfaddefe0555250612ef5f4d9172899b04d427d48b0420d765c00ad", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.requirement-source-view.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:2f29bd1edea7c3e930a143c2f96c229a0a199e52c89367302c414759bf660c44", FlagChoices: map[string][]string{}}, + "requirement-spec-tree": {InputContractSHA256: "sha256:96876589778a1cf1bc3f41fa33ad86de502db05886ba620ecbee59225060e315", InputSchemaSummary: []string{"schemaVersion", "treeId", "rootNodeId", "callerAnnotations", "nodes", "edges", "overlays", "root-shape-only definition proofkit.requirement-spec-tree.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:a027461411b8614288186df2c014a9e5303f905f0fb4cfc9b3ed6439790ae547", FlagChoices: map[string][]string{}}, + "requirement-spec-tree-view": {InputContractSHA256: "sha256:9e725fc145c437e0f9cdea0deed86189da855cc7d0350d33e59c80d34d91c03a", InputSchemaSummary: []string{"schemaVersion", "treeId", "rootNodeId", "callerAnnotations", "nodes", "edges", "overlays", "root-shape-only definition proofkit.requirement-spec-tree-view.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:ba9b258e45d485936ddbfd76f7a7c43a5ae5760a4fe6da56bce4a45afd4221e6", FlagChoices: map[string][]string{}}, + "requirement-traceability-graph": {InputContractSHA256: "sha256:c3dfdd8e25325f4e1170559a4d6ab905a82cca92e5040ea22232596b93d389f1", InputSchemaSummary: []string{"schemaVersion=2", "graphId", "context=proofkit.requirement-context schemaVersion=2 with strict v1 adapter", "codeSources[].path+content (optional, bounded UTF-8)", "codeTopology.nodes[].abstractionLevel=repository|package|module|file|symbol|source_range", "codeTopology.nodes[].sourceDigest+currentnessState", "codeTopology.edges[].evidenceRefs+authorityClass+currentnessState", "codeTopology.nativeCoverage[].producerId+evidenceRef+authorityClass+currentnessState+state", "root-shape-only definition proofkit.requirement-traceability-graph.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:5f1c027b553fb9dd558cd296b22d26371a69cbf4c0aeac38ffd51eb1b8505122", FlagChoices: map[string][]string{}}, + "scaffold-profile-plan": {InputContractSHA256: "sha256:bc2a9dc33664fc0555bb5c4b67c6c2caa451995f7bcb1b8add8ea8a8aabd88a6", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.scaffold-profile-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3d5d6584ef88c14534333e62b677ecac73d5bae659edf71893faa7ab1068659c", FlagChoices: map[string][]string{}}, + "scaffold-project-structure": {InputContractSHA256: "sha256:0db5eca08d353a8d314908a34d8293c947a9d208e280353d9784b489576ec55a", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.scaffold-project-structure.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:79950f8b779b00616be24b2d7e28021a83e9414881676ae86ab467940d36e6cb", FlagChoices: map[string][]string{}}, + "secret-scan": {InputContractSHA256: "sha256:cf88b1608360f7f0efe6a65e547f327c721d3befef2136471a14b16d015625ab", InputSchemaSummary: []string{"files", "nonClaims", "reportId", "schemaVersion", "root-shape-only definition proofkit.secret-scan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:b26eb2f693f2bc69f30d19f8e17ed2d3f35a5c3201bbff7e261b3eb671d48118", FlagChoices: map[string][]string{}}, + "selective-gate-evidence": {InputContractSHA256: "sha256:345d1f2bf38ba2b1b18b64c2ed1298d3d25c8153a7b0504a86adfcbfd2e93077", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.selective-gate-evidence.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:2e51ee45b106004daea83fe39a2d812b72ca77cdd35a893ad81150d6d7952068", FlagChoices: map[string][]string{}}, + "selective-gate-obligation-decision-input": {InputContractSHA256: "sha256:b404d9aab9362490006bd21b1919b9664af37a8e7c96ce8833f7e6541ded71fc", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.selective-gate-obligation-decision-input.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:f6a493fe11ea2d2ccfa9f9bea186ab1ccee02d0f15306cb297b567a638f385d2", FlagChoices: map[string][]string{}}, + "selective-gate-plan": {InputContractSHA256: "sha256:5293a5a4c7d8426cf637e6f8d252095ca0eb1714365bb89bec83307b778c678a", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.selective-gate-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:d7bffed853af5595af08b03859be01c283a3bdff1b3502d94ddc190889977647", FlagChoices: map[string][]string{}}, + "self-check": {InputContractSHA256: "sha256:9a959ace6f23c7ad731276ce5d0408b7e449de5ac03d8e73734ef803f726f718", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.self-check.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3efb6efe482e19d3930fc2662060390565e61bb8b4259c87708915763d13818d", FlagChoices: map[string][]string{}}, + "spec-overview-claims": {InputContractSHA256: "sha256:2490dcd34ba7485e13f8f33e8a288a0463c4c52cc6b0d82c57777466927e49a4", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.spec-overview-claims.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:554f3a7020e9820ccb90672629fd769c52b2f298f356040aa3b0a817666cbfbf", FlagChoices: map[string][]string{}}, + "spec-proof-bundle-admission": {InputContractSHA256: "sha256:6b6c2875b6476e63a1911e7d6112d9999df2babbee969f84abc4c9e4b470c933", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.spec-proof-bundle-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:e9e0eb66cebca3b99fe5036fb2e7327a9284934ed76f58818d18094d0546fc52", FlagChoices: map[string][]string{}}, + "stack-preset": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:f495e9ade4e1e7af7a8f8b2059f7611cc016e6080363afccf76d8dfc2dbc6d2d", FlagChoices: map[string][]string{"--preset": []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"}}}, + "test-evidence-inventory": {InputContractSHA256: "sha256:9f2217986ec85f017b16075749c53ae1b01b348bc9a6b66db423a6c3564e4528", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.test-evidence-inventory.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:4db9063936db590c97f33fd47b6fbafcf974ec3dab1f21ffaad37f7b9549e68c", FlagChoices: map[string][]string{}}, + "text-policy": {InputContractSHA256: "sha256:21ec8496730700d7baaac18652dee438a41011e093f7aa0d1c323b27c2de6e9c", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.text-policy.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:2b11ed3777cfbccc612a3a64c4b4dfe68f1d77143026102eead42a3331ac9489", FlagChoices: map[string][]string{}}, + "typescript-public-api-surfaces": {InputContractSHA256: "sha256:e8e55a53390c50fe0d8e669907318474c237327f2f094b3c78908b4fd6941035", InputSchemaSummary: []string{"schemaVersion=1", "machineContract=public_api_surfaces", "entries[].packageManifestPath", "entries[].packageName", "entries[].exportKey", "entries[].exportConditions[] (non-empty, sorted unique by condition)", "entries[].exportConditions[].condition", "entries[].exportConditions[].path", "entries[].exportConditions[].sourcePath (declared and canonical target .ts/.mts/.cts)", "entries[].runtimeExports[]", "entries[].typeExports[]", "entries[].deniedExportKeys[] (optional)", "sourceGrammar=fail_closed_restricted_typescript_exports_v1", "maxSourceFileBytes=8388608", "maxPackageManifestBytes=262144", "maxAggregateFileReadBytes=67108864", "root-shape-only definition proofkit.typescript-public-api-surfaces.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:2f05ea8073e7a2d4a08de962461f5fb31a78f14e77f9ce2406e3ce40ec7beb68", FlagChoices: map[string][]string{}}, + "witness-plan": {InputContractSHA256: "sha256:7814c5d27487a361bac77045afe32c6449c24881f04d5496da7182b3f2c0c1ee", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.witness-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:722c577bf5cf1dab8c9da8d18fc0634e1b9a6471aa5ca0b05f55b730cdd0d303", FlagChoices: map[string][]string{}}, + "witness-scheduler-plan": {InputContractSHA256: "sha256:972c782dc8c5f012380acba2f7e80030adccef3a93c63255e60b2ac75af9cc4c", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.witness-scheduler-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:1c87ee7d359b28e66e5236ff8f8e779d9d8b452f8337b589f8b7bcfd17db1dc4", FlagChoices: map[string][]string{}}, + "workspace-changed-package-plan": {InputContractSHA256: "sha256:77528c486d3b95c85be7c653d544ae9190662e5a3293cc70475efb28e8fe7485", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.workspace-changed-package-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:02db1649b4d5aaa4b5abff0579b6edbbd89af43a58fcafe0b1258fa396a78570", FlagChoices: map[string][]string{}}, + "workspace-manifest-facts": {InputContractSHA256: "sha256:f47345a49a8efe208fec8ccf336fcf18d433601b583eb9147596e6c2e4d4faa0", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.workspace-manifest-facts.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:d04416ec9e2cfded2be80ce8558da228cdb23f2c4c3bf6794309b7c74b7cfe1d", FlagChoices: map[string][]string{}}, + "workspace-registry": {InputContractSHA256: "sha256:7da33057c0ebc034aa256ff86809bb9a84fad5ad26a412d5982682719560058b", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.workspace-registry.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:80ef9a2a27256527211a00dca3bf08936121173044b95d95f26e55e7f63f1f90", FlagChoices: map[string][]string{}}, + "workspace-shard-partition": {InputContractSHA256: "sha256:caf4c37f9f264a1886b251eaa3695972fa6ef547e0968397f4e9cfa18b7e5f6f", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.workspace-shard-partition.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:7fedc9ebd169865719ff8ab079a3da67b832144776e2497694a2674d46c6e2f5", FlagChoices: map[string][]string{}}, +} diff --git a/internal/app/command_coverage_test.go b/internal/app/command_coverage_test.go index c53376e..8beb3e3 100644 --- a/internal/app/command_coverage_test.go +++ b/internal/app/command_coverage_test.go @@ -438,7 +438,7 @@ func TestNoInputCommandsHaveCommandSpecificBehavior(t *testing.T) { stdout.Reset() stderr.Reset() status = Run(t.Context(), []string{"stack-preset", "--preset", "unknown"}, strings.NewReader(""), &stdout, &stderr) - if status == 0 || stdout.Len() != 0 || !strings.Contains(stderr.String(), "known stack preset") { + if status == 0 || stdout.Len() != 0 || !strings.Contains(stderr.String(), "--preset requires one of:") { t.Fatalf("stack-preset accepted unknown preset status=%d stdout=%s stderr=%s", status, stdout.String(), stderr.String()) } }) diff --git a/internal/app/command_descriptors.go b/internal/app/command_descriptors.go index 415e01b..d042fde 100644 --- a/internal/app/command_descriptors.go +++ b/internal/app/command_descriptors.go @@ -56,6 +56,7 @@ type commandDescriptor struct { requiredFlags []string exactlyOneOfFlagGroups [][]string flagValueRequirements []flagValueRequirement + flagValueChoices map[string][]string inputSchemaSummary []string outputModes []string agentEnvelope bool @@ -94,10 +95,10 @@ var commandDescriptors = []commandDescriptor{ command("impact", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("impact")), command("init", commandInputNone, flags("--preset"), modes("json"), ownerDirs("initplan"), withRunner(commandRunnerInit), withSemanticAppTests("TestCLIABIGoldenCorpus")), command("json-report-cli-adapter-source", commandInputNone, flags("--format", "--language"), modes("json"), ownerDirs("jsonreportcliadaptersource"), withRunner(commandRunnerJSONReportCLIAdapterSource), withRequiredFlags("--language")), - command("migration-parity-admission", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("migrationparityadmission"), withInputSchemaSummary("schemaVersion=1", "paritySetId", "sourceProofOwners[]", "targetProofkitRefs[]", "parityRecords[]", "nonClaims[]")), - command("migration-plan", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("migrationplan"), withInputSchemaSummary("schemaVersion=1", "migrationId", "sourceProofOwners[]", "targetProofkitRefs[]", "parityEvidenceRefs[]", "retainedOwners[]", "retirementCandidates[]", "followUpCommands[]", "nonClaims[]")), + command("migration-parity-admission", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("migrationparityadmission")), + command("migration-plan", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("migrationplan")), command("obligation-decision", commandInputRequired, flags("--agent-envelope", "--input", "--input-pointer"), modes("json"), ownerDirs("obligationdecision"), withRunner(commandRunnerPlanning), withAgentEnvelope()), - command("package-runtime-dependency-admission", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("packageruntimedependency"), withInputSchemaSummary("schemaVersion=1", "reportId", "expectedDependencySpec", "expectedLockfileIntegrity", "expectedPackageName", "expectedPackageVersion", "admissibleLocations{}", "packageResolution{}", "nonClaims[]")), + command("package-runtime-dependency-admission", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("packageruntimedependency")), command("pilot-admission", commandInputRequired, flags("--contract-envelope", "--input", "--input-pointer", "--pilot", "--stack-diverse"), modes("json"), ownerDirs("pilotadmission"), withRunner(commandRunnerPilotAdmission), withContractEnvelope()), command("producer-policy-self-proof", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("producerpolicyselfproof")), command("proof-obligation-algebra", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("proofobligationalgebra")), @@ -114,22 +115,22 @@ var commandDescriptors = []commandDescriptor{ command("repo-profile-admission", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("repoprofileadmission")), command("requirement-authoring-plan", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("requirementauthoringplan")), command("requirement-bindings", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("requirementbinding")), - command("requirement-browser-server", commandInputRequired, flags("--empty-local-environment-policy", "--host", "--input", "--input-pointer", "--local-environment-class", "--open", "--port", "--scope", "--serve", "--session-mode", "--session-timeout-seconds", "--view"), modes("json", "server"), ownerDirs("requirementbrowser"), withRunner(commandRunnerRequirementBrowserServer), withSemanticAppTests("TestRequirementBrowserServerSpecTreeCLIABI"), withRequiredFlags("--view"), withFlagValueRequirement("--session-mode", "one-shot-question", "--open", "--serve", "--view"), withInputSchemaSummary("workspace mode: schemaVersion=1", "workspace mode: workspaceId", "workspace mode: context=proofkit.requirement-context", "workspace mode: diffInput=proofkit.requirement-semantic-diff-input (optional)", "workspace mode: graphInput=proofkit.requirement-traceability-graph-input schemaVersion=2 (optional)", "--session-mode values: browse|one-shot-question", "one-shot-question requires --view workspace --serve --open", "--session-timeout-seconds is 1..7200 and requires one-shot-question", "source|proof|coverage|spec-tree modes retain their owner input contracts")), - command("requirement-context-compose", commandInputRequired, flags("--input", "--input-pointer", "--repo-root"), modes("json"), ownerDirs("requirementcontext"), withRunner(commandRunnerRequirementContextCompose), withSemanticAppTests("TestRequirementContextCommandsComposeThroughWholeCLI"), withScopeClass(commandScopeExplicitFileSystemScan), withRequiredFlags("--repo-root"), withInputSchemaSummary("schemaVersion=1", "catalogId", "specTree.path", "requirementSources[] (non-empty)", "requirementSources[].nodeId", "requirementSources[].path", "expectedSourceDigest (optional sha256 ref)", "proofBinding.path (optional)", "coverage.path (optional)", "exact catalog paths only; no discovery")), - command("requirement-context-slice", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("requirementcontext"), withSemanticAppTests("TestRequirementContextCommandsComposeThroughWholeCLI"), withInputSchemaSummary("schemaVersion=1", "sliceId", "context=proofkit.requirement-context", "query.profile=routing|specification|proof|coverage|review", "query.nodeIds[]|requirementIds[]|ownerIds[]|lifecycleStates[]", "query.maxDepth=0..512", "query.maxNodes=1..4096", "query.maxRequirements=1..16384")), + command("requirement-browser-server", commandInputRequired, flags("--empty-local-environment-policy", "--host", "--input", "--input-pointer", "--local-environment-class", "--open", "--port", "--scope", "--serve", "--session-mode", "--session-timeout-seconds", "--view"), modes("json", "server"), ownerDirs("requirementbrowser"), withRunner(commandRunnerRequirementBrowserServer), withSemanticAppTests("TestRequirementBrowserServerSpecTreeCLIABI"), withRequiredFlags("--view"), withFlagValueRequirement("--session-mode", "one-shot-question", "--open", "--serve", "--view")), + command("requirement-context-compose", commandInputRequired, flags("--input", "--input-pointer", "--repo-root"), modes("json"), ownerDirs("requirementcontext"), withRunner(commandRunnerRequirementContextCompose), withSemanticAppTests("TestRequirementContextCommandsComposeThroughWholeCLI"), withScopeClass(commandScopeExplicitFileSystemScan), withRequiredFlags("--repo-root")), + command("requirement-context-slice", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("requirementcontext"), withSemanticAppTests("TestRequirementContextCommandsComposeThroughWholeCLI")), command("requirement-coverage-input-compose", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("requirementcoverageinput")), command("requirement-coverage-view", commandInputRequired, flags("--agent-envelope", "--format", "--input", "--input-pointer"), modes("html", "json", "markdown"), ownerDirs("requirementcoverageview"), withRunner(commandRunnerRequirementView), withAgentEnvelope()), command("requirement-impact-input-compose", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("requirementimpactinput")), command("requirement-proof-resolver", commandInputRequired, flags("--empty-local-environment-policy", "--input", "--input-pointer", "--local-environment-class"), modes("json"), ownerDirs("requirementbinding"), withRunner(commandRunnerRequirementProofResolver), withExactlyOneOfFlags("--empty-local-environment-policy", "--local-environment-class")), command("requirement-proof-source-set", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("requirementproofsourceset")), command("requirement-proof-view", commandInputRequired, flags("--empty-local-environment-policy", "--format", "--input", "--input-pointer", "--local-environment-class", "--scope"), modes("html", "json", "markdown"), ownerDirs("requirementproofview"), withRunner(commandRunnerRequirementView)), - command("requirement-semantic-diff", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("requirementdiff"), withSemanticAppTests("TestRequirementContextCommandsComposeThroughWholeCLI"), withInputSchemaSummary("schemaVersion=1", "diffId", "baseContext=proofkit.requirement-context", "currentContext=proofkit.requirement-context", "query.requirementIds[] (optional)", "query.ownerIds[] (optional)", "query.maxChanges=1..8192 (optional)")), + command("requirement-semantic-diff", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("requirementdiff"), withSemanticAppTests("TestRequirementContextCommandsComposeThroughWholeCLI")), command("requirement-source-admission", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("requirementsourceadmission")), command("requirement-source-transition", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("requirementsourcetransition")), command("requirement-source-view", commandInputRequired, flags("--format", "--input", "--input-pointer"), modes("html", "json", "markdown"), ownerDirs("requirementsourceview"), withRunner(commandRunnerRequirementView)), command("requirement-spec-tree", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("requirementspectree")), command("requirement-spec-tree-view", commandInputRequired, flags("--format", "--input", "--input-pointer", "--output"), modes("html", "json", "markdown"), ownerDirs("requirementspectree"), withRunner(commandRunnerRequirementView)), - command("requirement-traceability-graph", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("requirementgraph"), withSemanticAppTests("TestRequirementContextCommandsComposeThroughWholeCLI"), withInputSchemaSummary("schemaVersion=2", "graphId", "context=proofkit.requirement-context", "codeSources[].path+content (optional, bounded UTF-8)", "codeTopology.nodes[].abstractionLevel=repository|package|module|file|symbol|source_range", "codeTopology.nodes[].sourceDigest+currentnessState", "codeTopology.edges[].evidenceRefs+authorityClass+currentnessState", "codeTopology.nativeCoverage[].producerId+evidenceRef+authorityClass+currentnessState+state")), + command("requirement-traceability-graph", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("requirementgraph"), withSemanticAppTests("TestRequirementContextCommandsComposeThroughWholeCLI")), command("scaffold-profile-plan", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("scaffoldprofileplan")), command("scaffold-project-structure", commandInputRequired, flags("--agent-envelope", "--input", "--input-pointer"), modes("json"), ownerDirs("projectstructure"), withRunner(commandRunnerProjectStructure), withAgentEnvelope()), command("selective-gate-evidence", commandInputRequired, flags("--agent-envelope", "--input", "--input-pointer"), modes("json"), ownerDirs("selectivegateevidence"), withRunner(commandRunnerPlanning), withAgentEnvelope()), @@ -142,7 +143,7 @@ var commandDescriptors = []commandDescriptor{ command("stack-preset", commandInputNone, flags("--preset"), modes("json"), ownerDirs("stackpreset"), withRunner(commandRunnerStackPreset), withSemanticAppTests("TestNoInputCommandsHaveCommandSpecificBehavior"), withRequiredFlags("--preset")), command("test-evidence-inventory", commandInputRequired, flags("--input", "--input-pointer", "--normalized-inventory", "--projection"), modes("json", "normalized-inventory"), ownerDirs("proofbindingtestinventory", "testevidenceinventory"), withRunner(commandRunnerTestEvidenceInventory)), command("text-policy", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("textpolicy")), - command("typescript-public-api-surfaces", commandInputRequired, flags("--input", "--input-pointer", "--repo-root"), modes("json"), ownerDirs("publicapi"), withRunner(commandRunnerTypeScriptPublicAPISurfaces), withScopeClass(commandScopeExplicitFileSystemScan), withRequiredFlags("--repo-root"), withInputSchemaSummary("schemaVersion=1", "machineContract=public_api_surfaces", "entries[].packageManifestPath", "entries[].packageName", "entries[].exportKey", "entries[].exportConditions[] (non-empty, sorted unique by condition)", "entries[].exportConditions[].condition", "entries[].exportConditions[].path", "entries[].exportConditions[].sourcePath (declared and canonical target .ts/.mts/.cts)", "entries[].runtimeExports[]", "entries[].typeExports[]", "entries[].deniedExportKeys[] (optional)", "sourceGrammar=fail_closed_restricted_typescript_exports_v1", "maxSourceFileBytes=8388608", "maxPackageManifestBytes=262144", "maxAggregateFileReadBytes=67108864")), + command("typescript-public-api-surfaces", commandInputRequired, flags("--input", "--input-pointer", "--repo-root"), modes("json"), ownerDirs("publicapi"), withRunner(commandRunnerTypeScriptPublicAPISurfaces), withScopeClass(commandScopeExplicitFileSystemScan), withRequiredFlags("--repo-root")), command("witness-plan", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("witnessplan")), command("witness-scheduler-plan", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("witnessschedulerplan")), command("workspace-changed-package-plan", commandInputRequired, flags("--agent-envelope", "--input", "--input-pointer"), modes("json"), ownerDirs("workspaceplanning"), withRunner(commandRunnerPlanning), withAgentEnvelope()), @@ -200,6 +201,10 @@ func command(name string, input commandInputMode, allowedFlags []string, outputM for _, option := range options { option(&descriptor) } + if metadata, ok := generatedCommandContractMetadataByName[name]; ok { + descriptor.inputSchemaSummary = cloneStrings(metadata.InputSchemaSummary) + descriptor.flagValueChoices = cloneStringMap(metadata.FlagChoices) + } return descriptor } @@ -240,12 +245,6 @@ func withSemanticAppTests(testNames ...string) commandDescriptorOption { } } -func withInputSchemaSummary(fields ...string) commandDescriptorOption { - return func(descriptor *commandDescriptor) { - descriptor.inputSchemaSummary = cloneStrings(fields) - } -} - func withRequiredFlags(flags ...string) commandDescriptorOption { return func(descriptor *commandDescriptor) { descriptor.requiredFlags = cloneStrings(flags) @@ -324,6 +323,11 @@ func buildCommandDescriptorIndex(descriptors []commandDescriptor) map[string]com } } } + for flag, choices := range descriptor.flagValueChoices { + if !slices.Contains(descriptor.allowedFlags, flag) || !isSortedUnique(choices) { + panic("invalid generated flag choices: " + descriptor.name + " " + flag) + } + } index[descriptor.name] = descriptor.clone() } return index @@ -399,6 +403,7 @@ func (descriptor commandDescriptor) clone() commandDescriptor { descriptor.requiredFlags = cloneStrings(descriptor.requiredFlags) descriptor.exactlyOneOfFlagGroups = cloneStringMatrix(descriptor.exactlyOneOfFlagGroups) descriptor.flagValueRequirements = cloneFlagValueRequirements(descriptor.flagValueRequirements) + descriptor.flagValueChoices = cloneStringMap(descriptor.flagValueChoices) descriptor.inputSchemaSummary = cloneStrings(descriptor.inputSchemaSummary) descriptor.outputModes = cloneStrings(descriptor.outputModes) descriptor.semanticAppTests = cloneStrings(descriptor.semanticAppTests) @@ -426,3 +431,14 @@ func cloneFlagValueRequirements(values []flagValueRequirement) []flagValueRequir func cloneStrings(values []string) []string { return append([]string(nil), values...) } + +func cloneStringMap(values map[string][]string) map[string][]string { + if values == nil { + return nil + } + out := make(map[string][]string, len(values)) + for key, value := range values { + out[key] = cloneStrings(value) + } + return out +} diff --git a/internal/app/command_family_catalog.go b/internal/app/command_family_catalog.go index e6b413d..73031a6 100644 --- a/internal/app/command_family_catalog.go +++ b/internal/app/command_family_catalog.go @@ -3,6 +3,8 @@ package app import ( "fmt" "strings" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" ) type commandFamily struct { @@ -21,17 +23,17 @@ type commandFamilyCatalog struct { SchemaVersion int } -func commandFamiliesUsage() string { +func commandFamiliesUsageWithRenderer(renderer cliexec.Renderer) string { lines := []string{"Command families:"} for _, family := range generatedCommandFamilyCatalog().Families { lines = append(lines, fmt.Sprintf(" %s\t%s", family.ID, family.Label)) lines = append(lines, " "+family.Purpose) + lines = append(lines, " "+renderer.DisplayCommand("help", "family", family.ID)) } - lines = append(lines, "", "Use `agentic-proofkit help family ` for leaf commands.") return strings.Join(lines, "\n") + "\n" } -func commandFamilyUsage(familyID string) (string, error) { +func commandFamilyUsageWithRenderer(familyID string, renderer cliexec.Renderer) (string, error) { for _, family := range generatedCommandFamilyCatalog().Families { if family.ID != familyID { continue @@ -46,6 +48,7 @@ func commandFamilyUsage(familyID string) (string, error) { } for _, command := range family.Commands { lines = append(lines, " "+command) + lines = append(lines, " "+renderer.DisplayCommand("help", command)) } return strings.Join(lines, "\n") + "\n", nil } diff --git a/internal/app/command_family_catalog_test.go b/internal/app/command_family_catalog_test.go index 04bd9c8..5596146 100644 --- a/internal/app/command_family_catalog_test.go +++ b/internal/app/command_family_catalog_test.go @@ -2,8 +2,11 @@ package app import ( "bytes" + "slices" "strings" "testing" + + "github.com/research-engineering/agentic-proofkit/internal/command/stackpreset" ) func TestCommandFamilyCatalogMatchesDescriptors(t *testing.T) { @@ -44,6 +47,9 @@ func TestCommandFamilyHelpFormsAreOptInAndLeafDispatchIsUnchanged(t *testing.T) if strings.Contains(rootHelp, "Command families:") { t.Fatal("default help unexpectedly includes the opt-in family catalog") } + if !strings.Contains(rootHelp, "agentic-proofkit help families") { + t.Fatal("root help does not expose the opt-in command-family discovery route") + } familiesHelp := runTextCommand(t, []string{"help", "families"}) for _, family := range generatedCommandFamilyCatalog().Families { if !strings.Contains(familiesHelp, " "+family.ID+"\t"+family.Label) { @@ -54,12 +60,79 @@ func TestCommandFamilyHelpFormsAreOptInAndLeafDispatchIsUnchanged(t *testing.T) if !strings.Contains(familyHelp, " "+command+"\n") { t.Fatalf("family %s help does not route command %s", family.ID, command) } + copyableRoute := "agentic-proofkit help " + command + copyableLine := " " + copyableRoute + "\n" + if strings.Count(familyHelp, copyableLine) != 1 { + t.Fatalf("family %s copyable route %q count=%d, want 1", family.ID, copyableRoute, strings.Count(familyHelp, copyableLine)) + } direct := runTextCommand(t, []string{"help", command}) descriptor, _ := commandDescriptorFor(command) if direct != commandUsage(descriptor) { t.Fatalf("direct help for %s changed through family navigation", command) } } + copyableFamilyRoute := "agentic-proofkit help family " + family.ID + if strings.Count(familiesHelp, copyableFamilyRoute) != 1 { + t.Fatalf("family list copyable route %q count=%d, want 1", copyableFamilyRoute, strings.Count(familiesHelp, copyableFamilyRoute)) + } + } +} + +func TestRootHelpDiscoversFamiliesWithoutExpandingThem(t *testing.T) { + rootHelp := runTextCommand(t, []string{"help"}) + const route = "agentic-proofkit help families" + if strings.Count(rootHelp, route) != 1 { + t.Fatalf("root help family route count=%d, want 1", strings.Count(rootHelp, route)) + } + for _, family := range generatedCommandFamilyCatalog().Families { + if strings.Contains(rootHelp, family.Label) || strings.Contains(rootHelp, family.Purpose) { + t.Fatalf("root help eagerly expanded family %s", family.ID) + } + } +} + +func TestStackPresetVocabularyProjectsFromOneOwner(t *testing.T) { + choices := generatedCommandContractMetadataByName["stack-preset"].FlagChoices["--preset"] + if !slices.Equal(choices, stackpreset.IDs()) { + t.Fatalf("CLI contract preset choices=%v runtime preset ids=%v", choices, stackpreset.IDs()) + } + descriptor, ok := commandDescriptorFor("stack-preset") + if !ok { + t.Fatal("stack-preset descriptor missing") + } + if !slices.Equal(descriptor.flagValueChoices["--preset"], choices) { + t.Fatalf("descriptor preset choices=%v contract choices=%v", descriptor.flagValueChoices["--preset"], choices) + } + help := commandUsage(descriptor) + const prefix = "agentic-proofkit stack-preset --preset <" + start := strings.Index(help, prefix) + if start < 0 { + t.Fatalf("stack-preset help omits generated usage: %s", help) + } + start += len(prefix) + end := strings.Index(help[start:], ">") + if end < 0 { + t.Fatalf("stack-preset help has no closed preset vocabulary: %s", help) + } + helpIDs := strings.Split(help[start:start+end], "|") + if !slices.Equal(helpIDs, choices) { + t.Fatalf("help preset ids=%v contract choices=%v", helpIDs, choices) + } + for _, presetID := range choices { + route := "agentic-proofkit stack-preset --preset " + presetID + if strings.Count(help, route) != 1 { + t.Fatalf("stack-preset copyable route %q count=%d, want 1", route, strings.Count(help, route)) + } + } + if strings.Count(help, "Path: node_modules/@research-engineering/agentic-proofkit/README.md") != 1 { + t.Fatal("stack-preset help does not expose exactly one installed README continuation") + } + var stdout bytes.Buffer + var stderr bytes.Buffer + status := Run(t.Context(), []string{"stack-preset", "--preset", "unknown"}, panicReader{}, &stdout, &stderr) + wantDiagnostic := "--preset requires one of: " + strings.Join(choices, ", ") + "\n" + if status != 1 || stdout.Len() != 0 || stderr.String() != wantDiagnostic { + t.Fatalf("invalid preset status=%d stdout=%q stderr=%q, want %q", status, stdout.String(), stderr.String(), wantDiagnostic) } } @@ -76,6 +149,10 @@ func TestExistingHelpEntrypointsRemainCompatible(t *testing.T) { } for _, descriptor := range commandDescriptors { expected := commandUsage(descriptor) + installedLine := "Installed invocation:\n " + installedCommandUsageLine(descriptor) + "\n" + if strings.Count(expected, installedLine) != 1 { + t.Fatalf("command %s installed invocation %q count=%d, want 1", descriptor.name, installedCommandUsageLine(descriptor), strings.Count(expected, installedLine)) + } aliases := [][]string{{"help", descriptor.name}} if descriptor.name != "help" { aliases = append(aliases, []string{descriptor.name, "--help"}, []string{descriptor.name, "-h"}) @@ -86,6 +163,13 @@ func TestExistingHelpEntrypointsRemainCompatible(t *testing.T) { } } } + requirementSource, ok := commandDescriptorFor("requirement-source-admission") + if !ok { + t.Fatal("requirement-source-admission descriptor missing") + } + if strings.Count(commandUsage(requirementSource), "Path: node_modules/@research-engineering/agentic-proofkit/README.md") != 1 { + t.Fatal("requirement-source-admission help does not expose exactly one installed README continuation") + } } func TestHelpCommandUsageIncludesGeneratedFamilyGrammar(t *testing.T) { diff --git a/internal/app/command_help.go b/internal/app/command_help.go index 2a2fe31..c885743 100644 --- a/internal/app/command_help.go +++ b/internal/app/command_help.go @@ -4,6 +4,8 @@ import ( "fmt" "slices" "strings" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" ) func isCommandHelpRequest(args []string) bool { @@ -11,11 +13,21 @@ func isCommandHelpRequest(args []string) bool { } func commandUsage(descriptor commandDescriptor) string { - lines := []string{"Usage:"} - lines = append(lines, " "+commandUsageLine(descriptor)) + return commandUsageWithRenderer(descriptor, cliexec.PathRenderer()) +} + +func commandUsageWithRenderer(descriptor commandDescriptor, renderer cliexec.Renderer) string { + lines := []string{ + "Usage:", + " " + commandUsageLine(descriptor), + "", + "Installed invocation:", + " " + installedCommandUsageLineWithRenderer(descriptor, renderer), + } if descriptor.name == "help" { + lines = append(lines, "", "Installed help routes:") for _, form := range generatedCommandFamilyCatalog().HelpForms { - lines = append(lines, " agentic-proofkit "+form) + lines = append(lines, " "+renderer.DisplayCommand()+" "+form) } } lines = append(lines, @@ -37,6 +49,19 @@ func commandUsage(descriptor commandDescriptor) string { for _, flag := range descriptor.allowedFlags { lines = append(lines, " "+flag) } + if descriptor.name == "stack-preset" { + lines = append(lines, "", "Copyable preset commands:") + for _, presetID := range descriptor.flagValueChoices["--preset"] { + lines = append(lines, " "+renderer.DisplayCommand("stack-preset", "--preset", presetID)) + } + } + if descriptor.name == "stack-preset" || descriptor.name == "requirement-source-admission" { + lines = append(lines, + "", + "Continue with the installed README first-valid-input example:", + " Path: node_modules/@research-engineering/agentic-proofkit/README.md", + ) + } if len(descriptor.exactlyOneOfFlagGroups) > 0 || len(descriptor.flagValueRequirements) > 0 { lines = append(lines, "", "Flag constraints:") for _, group := range descriptor.exactlyOneOfFlagGroups { @@ -62,10 +87,19 @@ func commandUsage(descriptor commandDescriptor) string { } } lines = append(lines, "", "Public contract:") - lines = append(lines, " CLI/JSON input, output modes, exit codes, and flags are owned by proofkit/cli-contract.v2.json.") + lines = append(lines, " CLI command routing, root JSON shapes, output modes, exit codes, and flags are owned by proofkit/cli-contract.v2.json.") + lines = append(lines, " Nested field semantics remain owned by native command admission.") return strings.Join(lines, "\n") + "\n" } +func installedCommandUsageLine(descriptor commandDescriptor) string { + return installedCommandUsageLineWithRenderer(descriptor, cliexec.PathRenderer()) +} + +func installedCommandUsageLineWithRenderer(descriptor commandDescriptor, renderer cliexec.Renderer) string { + return renderer.DisplayCommand() + strings.TrimPrefix(commandUsageLine(descriptor), cliexec.BinaryName) +} + func commandUsageLine(descriptor commandDescriptor) string { if descriptor.name == "help" { return "agentic-proofkit help [|-h|--help]" @@ -79,6 +113,10 @@ func commandUsageLine(descriptor commandDescriptor) string { continue } required := slices.Contains(descriptor.requiredFlags, flag) + if choices := descriptor.flagValueChoices[flag]; len(choices) > 0 { + segments = append(segments, optionalUsageSegment(flag+" <"+strings.Join(choices, "|")+">", required)) + continue + } switch flag { case "--input": continue diff --git a/internal/app/command_registry.go b/internal/app/command_registry.go index af6dd77..5c1c681 100644 --- a/internal/app/command_registry.go +++ b/internal/app/command_registry.go @@ -57,7 +57,7 @@ type genericCommandBuilder func(any) (any, int, error) var genericCommandBuilders = mustGenericCommandBuilders(map[string]genericCommandBuilder{ "adoption-checklist": reportOutput(adoptionchecklist.Build), "binding-partition": reportOutput(bindingpartition.Build), - "branch-authority": reportOutputWithoutError(branchauthority.Build), + "branch-authority": reportOutput(branchauthority.Build), "capability-map-admission": reportOutput(capabilitymapadmission.Build), "completion-criteria": reportOutput(completioncriteria.Build), "custom-rule-boundary": reportOutput(customruleboundary.Build), @@ -68,7 +68,7 @@ var genericCommandBuilders = mustGenericCommandBuilders(map[string]genericComman "impact": outputWithExit(impact.Build), "migration-parity-admission": reportOutput(migrationparityadmission.Build), "migration-plan": outputWithExit(migrationplan.Build), - "package-runtime-dependency-admission": reportOutputWithoutError(packageruntimedependency.Build), + "package-runtime-dependency-admission": reportOutput(packageruntimedependency.Build), "producer-policy-self-proof": reportOutput(producerpolicyselfproof.Build), "proof-obligation-algebra": reportOutput(proofobligationalgebra.Build), "proof-receipt-admission": reportOutput(proofreceiptadmission.Build), @@ -138,13 +138,6 @@ func reportOutput(builder func(any) (report.Record, int, error)) genericCommandB } } -func reportOutputWithoutError(builder func(any) (report.Record, int)) genericCommandBuilder { - return func(input any) (any, int, error) { - record, exitCode := builder(input) - return record.JSONValue(), exitCode, nil - } -} - func selfCheckOutput(input any) (any, int, error) { return report.BuildSelfCheckReport(input).JSONValue(), 0, nil } diff --git a/internal/app/envelope_commands.go b/internal/app/envelope_commands.go index e042fb5..188d945 100644 --- a/internal/app/envelope_commands.go +++ b/internal/app/envelope_commands.go @@ -6,6 +6,7 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/command/gradualadoption" "github.com/research-engineering/agentic-proofkit/internal/kernel/adoptionmode" + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" "github.com/research-engineering/agentic-proofkit/internal/kernel/jsonpointer" ) @@ -84,7 +85,7 @@ func runGradualAdoptionGuidance(args []string, stdin io.Reader, stdout io.Writer return writeJSON(output, exitCode, err, stdout, stderr) } -func runGradualAdoptionBootstrap(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int { +func runGradualAdoptionBootstrap(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer, renderer cliexec.Renderer) int { options, err := parseEnvelopeCommandArgs("gradual-adoption-bootstrap", args, agentEnvelopeBuilders{ supportsContractEnvelope: true, supportsMaterializationFile: true, @@ -106,26 +107,26 @@ func runGradualAdoptionBootstrap(args []string, stdin io.Reader, stdout io.Write } } if options.agentEnvelope && options.contractEnvelope { - output, exitCode, err := gradualadoption.BuildBootstrapEnvelopeFromContractEnvelope(input) + output, exitCode, err := gradualadoption.BuildBootstrapEnvelopeFromContractEnvelopeWithRenderer(input, renderer) return writeJSON(output, exitCode, err, stdout, stderr) } if options.agentEnvelope { - output, exitCode, err := gradualadoption.BuildBootstrapEnvelope(input) + output, exitCode, err := gradualadoption.BuildBootstrapEnvelopeWithRenderer(input, renderer) return writeJSON(output, exitCode, err, stdout, stderr) } if options.materialization && options.contractEnvelope { - output, exitCode, err := gradualadoption.BuildBootstrapMaterializationManifestFromContractEnvelope(input) + output, exitCode, err := gradualadoption.BuildBootstrapMaterializationManifestFromContractEnvelopeWithRenderer(input, renderer) return writeJSON(output, exitCode, err, stdout, stderr) } if options.materialization { - output, exitCode, err := gradualadoption.BuildBootstrapMaterializationManifest(input) + output, exitCode, err := gradualadoption.BuildBootstrapMaterializationManifestWithRenderer(input, renderer) return writeJSON(output, exitCode, err, stdout, stderr) } if options.contractEnvelope { - output, exitCode, err := gradualadoption.BuildBootstrapFromContractEnvelope(input) + output, exitCode, err := gradualadoption.BuildBootstrapFromContractEnvelopeWithRenderer(input, renderer) return writeJSON(output, exitCode, err, stdout, stderr) } - output, exitCode, err := gradualadoption.BuildBootstrap(input) + output, exitCode, err := gradualadoption.BuildBootstrapWithRenderer(input, renderer) return writeJSON(output, exitCode, err, stdout, stderr) } diff --git a/internal/app/invocation_profile_test.go b/internal/app/invocation_profile_test.go new file mode 100644 index 0000000..5986ac8 --- /dev/null +++ b/internal/app/invocation_profile_test.go @@ -0,0 +1,889 @@ +package app + +import ( + "bytes" + "encoding/json" + "reflect" + "sort" + "strings" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/command/gradualadoption" + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" +) + +func TestGeneratedCommandInvocationProfileFieldInventory(t *testing.T) { + aggregate := invocationProfileObject(t, invocationProfileJSON(t, cliAdoptionContractEnvelopeInput())) + gradual := invocationProfileObjectValue(t, aggregate["gradual"], "aggregate gradual") + bootstrapInput, err := gradualadoption.BootstrapInputFromContractEnvelope(gradual) + if err != nil { + t.Fatalf("derive bootstrap input: %v", err) + } + callerCommands := invocationProfileStrings(t, bootstrapInput["commands"], "bootstrap caller commands") + projectInput := invocationProfileProjectInput(bootstrapInput) + + profiles := []struct { + name string + profile string + pythonExecutable string + }{ + {name: "npm offline", profile: cliexec.ProfileNPMOffline}, + {name: "python module", profile: cliexec.ProfilePythonModule, pythonExecutable: "/tmp/proofkit venv/bin/python"}, + } + for _, profile := range profiles { + t.Run(profile.name, func(t *testing.T) { + renderer, err := cliexec.AdmitLauncherProfile(profile.profile, profile.pythonExecutable) + if err != nil { + t.Fatalf("admit renderer: %v", err) + } + + stack := invocationProfileRun(t, renderer, []string{"stack-preset", "--preset", "typescript_workspace"}, "") + invocationProfileAssertInventory(t, renderer, stack, map[string]int{ + "$.diagnostics[?key=preset].value.suggestedCommands[*]": 2, + }, len(callerCommands)) + preset := invocationProfileDiagnostic(t, stack, "preset") + invocationProfileAssertGenerated(t, renderer, invocationProfileStrings(t, invocationProfileObjectValue(t, preset, "preset diagnostic")["suggestedCommands"], "preset suggested commands")) + + directBootstrap := invocationProfileRun(t, renderer, []string{"gradual-adoption-bootstrap", "--input", "-", "--contract-envelope"}, invocationProfileEncode(t, gradual)) + bootstrapInventory := invocationProfileBootstrapInventory("$") + invocationProfileAssertInventory(t, renderer, directBootstrap, bootstrapInventory, len(callerCommands)) + invocationProfileAssertBootstrap(t, renderer, directBootstrap, callerCommands) + invocationProfileAssertInventoryMutants(t, renderer, directBootstrap, bootstrapInventory, callerCommands) + + directEnvelope := invocationProfileRun(t, renderer, []string{"gradual-adoption-bootstrap", "--input", "-", "--contract-envelope", "--agent-envelope"}, invocationProfileEncode(t, gradual)) + invocationProfileAssertInventory(t, renderer, directEnvelope, map[string]int{ + "$.commands[*].command": 3, + }, len(callerCommands)) + invocationProfileAssertEnvelope(t, renderer, directEnvelope) + + directManifest := invocationProfileRun(t, renderer, []string{"gradual-adoption-bootstrap", "--input", "-", "--contract-envelope", "--materialization-manifest"}, invocationProfileEncode(t, gradual)) + invocationProfileAssertInventory(t, renderer, directManifest, map[string]int{ + "$.files[?payloadKey=adoptionGuidance].content::$.agentGuidance.commands[callerCommandCount:]": 3, + "$.nextCommands[*]": 3, + }, len(callerCommands)) + invocationProfileAssertManifest(t, renderer, directManifest, callerCommands) + + aggregateBootstrap := invocationProfileRun(t, renderer, []string{"adoption-contract-envelope", "--input", "-", "--mode", "bootstrap"}, invocationProfileEncode(t, aggregate)) + if !reflect.DeepEqual(aggregateBootstrap, directBootstrap) { + t.Fatal("adoption aggregate bootstrap route did not preserve the admitted renderer") + } + aggregateEnvelope := invocationProfileRun(t, renderer, []string{"adoption-contract-envelope", "--input", "-", "--mode", "bootstrap", "--agent-envelope"}, invocationProfileEncode(t, aggregate)) + if !reflect.DeepEqual(aggregateEnvelope, directEnvelope) { + t.Fatal("adoption aggregate envelope route did not preserve the admitted renderer") + } + aggregateManifest := invocationProfileRun(t, renderer, []string{"adoption-contract-envelope", "--input", "-", "--mode", "bootstrap", "--materialization-manifest"}, invocationProfileEncode(t, aggregate)) + if !reflect.DeepEqual(aggregateManifest, directManifest) { + t.Fatal("adoption aggregate materialization route did not preserve the admitted renderer") + } + + project := invocationProfileRun(t, renderer, []string{"scaffold-project-structure", "--input", "-"}, invocationProfileEncode(t, projectInput)) + projectInventory := invocationProfileBootstrapInventory("$.bootstrapReport") + projectInventory["$.materializationManifest.files[?purpose=caller-owned gradual adoption guidance input].content::$.agentGuidance.commands[callerCommandCount:]"] = 3 + projectInventory["$.materializationManifest.nextCommands[*]"] = 6 + invocationProfileAssertInventory(t, renderer, project, projectInventory, len(callerCommands)) + invocationProfileAssertBootstrap(t, renderer, invocationProfileObjectValue(t, project["bootstrapReport"], "project bootstrap report"), callerCommands) + invocationProfileAssertManifest(t, renderer, invocationProfileObjectValue(t, project["materializationManifest"], "project materialization manifest"), callerCommands) + + projectEnvelope := invocationProfileRun(t, renderer, []string{"scaffold-project-structure", "--input", "-", "--agent-envelope"}, invocationProfileEncode(t, projectInput)) + invocationProfileAssertInventory(t, renderer, projectEnvelope, map[string]int{ + "$.commands[*].command": 6, + }, len(callerCommands)) + invocationProfileAssertEnvelope(t, renderer, projectEnvelope) + }) + } +} + +func TestGeneratedCommandInvocationProfileRouteClosure(t *testing.T) { + aggregate := invocationProfileObject(t, invocationProfileJSON(t, cliAdoptionContractEnvelopeInput())) + gradual := invocationProfileObjectValue(t, aggregate["gradual"], "aggregate gradual") + bootstrapInput, err := gradualadoption.BootstrapInputFromContractEnvelope(gradual) + if err != nil { + t.Fatalf("derive bootstrap input: %v", err) + } + projectInput := invocationProfileProjectInput(bootstrapInput) + + profiles := []struct { + name string + profile string + pythonExecutable string + }{ + {name: "path", profile: cliexec.ProfilePath}, + {name: "npm offline", profile: cliexec.ProfileNPMOffline}, + {name: "python module", profile: cliexec.ProfilePythonModule, pythonExecutable: "/tmp/proofkit venv/bin/python"}, + } + for _, profile := range profiles { + t.Run(profile.name, func(t *testing.T) { + renderer, err := cliexec.AdmitLauncherProfile(profile.profile, profile.pythonExecutable) + if err != nil { + t.Fatalf("admit renderer: %v", err) + } + invocationProfileAssertHelpRouteClosure(t, renderer) + + agentRouteInput := map[string]any{ + "schemaVersion": json.Number("1"), + "routeId": "proofkit.cli.invocation-profile-route", + "goal": "validate_requirement_source", + "mode": "observe", + "availableInputs": []any{ + map[string]any{"kind": "requirement_source", "ref": "docs/specs/module/requirements.v1.json"}, + }, + } + agentRoute := invocationProfileRun(t, renderer, []string{"agent-route", "--input", "-"}, invocationProfileEncode(t, agentRouteInput)) + agentRouteInventory := map[string]int{"$.nextCommands[*].argv": 1} + invocationProfileAssertArgvInventory(t, renderer, agentRoute, agentRouteInventory) + invocationProfileAssertArgvInventoryMutants(t, renderer, agentRoute, agentRouteInventory) + + agentRouteEnvelope := invocationProfileRun(t, renderer, []string{"agent-route", "--input", "-", "--agent-envelope"}, invocationProfileEncode(t, agentRouteInput)) + agentRouteEnvelopeInventory := map[string]int{"$.commands[*].argv": 1} + invocationProfileAssertArgvInventory(t, renderer, agentRouteEnvelope, agentRouteEnvelopeInventory) + invocationProfileAssertArgvInventoryMutantsAt(t, renderer, agentRouteEnvelope, agentRouteEnvelopeInventory, "commands") + invocationProfileAssertDisplayMatchesArgvField(t, agentRouteEnvelope, "display") + agentRouteDisplayMutant := invocationProfileObject(t, invocationProfileJSON(t, invocationProfileEncode(t, agentRouteEnvelope))) + agentRouteDisplayCommand := invocationProfileArgvCommand(t, agentRouteDisplayMutant, "commands") + agentRouteDisplayCommand["display"] = agentRouteDisplayCommand["proofkitRoute"] + if invocationProfileDisplayMatchesArgv(agentRouteDisplayMutant, "display") { + t.Fatal("agent-route bare-display mutant survived exact display/argv closure") + } + + workflowInput := cliAdoptionWorkflowInput() + directWorkflow := invocationProfileRun(t, renderer, []string{"adoption-workflow-plan", "--input", "-"}, invocationProfileEncode(t, workflowInput)) + workflowInventory := map[string]int{"$.phases[?phase=release].commands[*].argv": 2} + invocationProfileAssertArgvInventory(t, renderer, directWorkflow, workflowInventory) + + aggregateInput := invocationProfileObject(t, invocationProfileJSON(t, cliAdoptionContractEnvelopeInput())) + aggregateWorkflow := invocationProfileRun(t, renderer, []string{"adoption-contract-envelope", "--input", "-", "--mode", "workflow"}, invocationProfileEncode(t, aggregateInput)) + if !reflect.DeepEqual(aggregateWorkflow, directWorkflow) { + t.Fatal("adoption aggregate workflow route did not preserve the admitted renderer") + } + + directWorkflowEnvelope := invocationProfileRun(t, renderer, []string{"adoption-workflow-plan", "--input", "-", "--agent-envelope"}, invocationProfileEncode(t, workflowInput)) + workflowEnvelopeInventory := map[string]int{"$.commands[*].argv": 2} + invocationProfileAssertArgvInventory(t, renderer, directWorkflowEnvelope, workflowEnvelopeInventory) + invocationProfileAssertDisplayMatchesArgv(t, directWorkflowEnvelope) + aggregateWorkflowEnvelope := invocationProfileRun(t, renderer, []string{"adoption-contract-envelope", "--input", "-", "--mode", "workflow", "--agent-envelope"}, invocationProfileEncode(t, aggregateInput)) + if !reflect.DeepEqual(aggregateWorkflowEnvelope, directWorkflowEnvelope) { + t.Fatal("adoption aggregate workflow envelope did not preserve the admitted renderer") + } + + coverageEnvelope := invocationProfileRunStatus(t, renderer, []string{"requirement-coverage-view", "--input", "-", "--agent-envelope"}, cliCoverageInput("null"), 1) + invocationProfileAssertArgvInventory(t, renderer, coverageEnvelope, map[string]int{"$.commands[*].argv": 1}) + + project := invocationProfileRun(t, renderer, []string{"scaffold-project-structure", "--input", "-"}, invocationProfileEncode(t, projectInput)) + invocationProfileAssertProjectCallerArgvPreserved(t, bootstrapInput, project) + projectWorkflow := invocationProfileObjectValue(t, project["adoptionWorkflowPlan"], "project adoption workflow plan") + invocationProfileAssertArgvInventory(t, renderer, projectWorkflow, map[string]int{ + "$.phases[?phase=bind].commands[*].argv": 3, + "$.phases[?phase=bootstrap].commands[*].argv": 2, + "$.phases[?phase=profile].commands[*].argv": 2, + }) + }) + } +} + +func invocationProfileAssertProjectCallerArgvPreserved(t *testing.T, bootstrapInput map[string]any, project map[string]any) { + t.Helper() + inputWitnesses := invocationProfileObjectValue(t, bootstrapInput["nativeWitnesses"], "bootstrap input native witnesses") + inputCommands := invocationProfileArray(t, inputWitnesses["commands"], "bootstrap input native witness commands") + bootstrap := invocationProfileObjectValue(t, project["bootstrapReport"], "project bootstrap report") + payloads := invocationProfileObjectValue(t, bootstrap["payloads"], "project bootstrap payloads") + adoptionProfile := invocationProfileObjectValue(t, payloads["adoptionProfile"], "project adoption profile") + witnessOwners := []struct { + name string + raw any + }{ + {name: "adoption profile", raw: invocationProfileObjectValue(t, adoptionProfile["nativeWitnesses"], "project adoption profile native witnesses")["commands"]}, + {name: "witness plan input", raw: invocationProfileObjectValue(t, payloads["witnessPlanInput"], "project witness plan input")["commands"]}, + {name: "witness plan", raw: invocationProfileObjectValue(t, bootstrap["witnessPlan"], "project witness plan")["commands"]}, + } + for _, owner := range witnessOwners { + commands := invocationProfileArray(t, owner.raw, owner.name+" commands") + if !reflect.DeepEqual(commands, inputCommands) { + t.Fatalf("%s caller-owned argv changed: got %#v want %#v", owner.name, commands, inputCommands) + } + } +} + +func invocationProfileAssertHelpRouteClosure(t *testing.T, renderer cliexec.Renderer) { + t.Helper() + root := invocationProfileRunText(t, renderer, []string{"help"}) + rootRoute := renderer.DisplayCommand("help", "families") + if !invocationProfileRootHelpRouteMatches(root, rootRoute) { + t.Fatalf("root help did not expose exactly the admitted family route %q", rootRoute) + } + for _, mutant := range []string{ + strings.Replace(root, rootRoute, "npx agentic-proofkit help families", 1), + root + " npx agentic-proofkit help families\n", + strings.Replace(root, rootRoute, "", 1), + } { + if invocationProfileRootHelpRouteMatches(mutant, rootRoute) { + t.Fatal("root help route mutant survived exact launcher-profile closure") + } + } + + families := invocationProfileRunText(t, renderer, []string{"help", "families"}) + catalog := generatedCommandFamilyCatalog() + if strings.Count(families, " help family ") != len(catalog.Families) { + t.Fatalf("family help route count=%d, want %d", strings.Count(families, " help family "), len(catalog.Families)) + } + for _, family := range catalog.Families { + route := renderer.DisplayCommand("help", "family", family.ID) + routeLine := " " + route + "\n" + if strings.Count(families, routeLine) != 1 { + t.Fatalf("family route %q count=%d, want 1", route, strings.Count(families, routeLine)) + } + familyHelp := invocationProfileRunText(t, renderer, []string{"help", "family", family.ID}) + if strings.Count(familyHelp, " help ") != len(family.Commands) { + t.Fatalf("family %s leaf route count=%d, want %d", family.ID, strings.Count(familyHelp, " help "), len(family.Commands)) + } + for _, command := range family.Commands { + leafRoute := renderer.DisplayCommand("help", command) + leafRouteLine := " " + leafRoute + "\n" + if strings.Count(familyHelp, leafRouteLine) != 1 { + t.Fatalf("family %s leaf route %q count=%d, want 1", family.ID, leafRoute, strings.Count(familyHelp, leafRouteLine)) + } + descriptor, ok := commandDescriptorFor(command) + if !ok { + t.Fatalf("family %s command %s has no descriptor", family.ID, command) + } + leafHelp := invocationProfileRunText(t, renderer, []string{"help", command}) + installedUsage := "Installed invocation:\n " + installedCommandUsageLineWithRenderer(descriptor, renderer) + "\n" + if strings.Count(leafHelp, installedUsage) != 1 { + t.Fatalf("command %s installed usage %q count=%d, want 1", command, installedUsage, strings.Count(leafHelp, installedUsage)) + } + if command == "help" { + for _, form := range catalog.HelpForms { + route := " " + renderer.DisplayCommand() + " " + form + "\n" + if strings.Count(leafHelp, route) != 1 { + t.Fatalf("help form %q count=%d, want 1", route, strings.Count(leafHelp, route)) + } + } + } + } + } + + stackDescriptor, ok := commandDescriptorFor("stack-preset") + if !ok { + t.Fatal("stack-preset descriptor missing") + } + stackHelp := invocationProfileRunText(t, renderer, []string{"help", "stack-preset"}) + installedUsage := installedCommandUsageLineWithRenderer(stackDescriptor, renderer) + if !strings.Contains(stackHelp, "Installed invocation:\n "+installedUsage+"\n") { + t.Fatalf("stack-preset help missing admitted installed usage %q", installedUsage) + } + for _, presetID := range stackDescriptor.flagValueChoices["--preset"] { + route := renderer.DisplayCommand("stack-preset", "--preset", presetID) + routeLine := " " + route + "\n" + if strings.Count(stackHelp, routeLine) != 1 { + t.Fatalf("stack-preset route %q count=%d, want 1", route, strings.Count(stackHelp, routeLine)) + } + } +} + +func invocationProfileRootHelpRouteMatches(output string, route string) bool { + return strings.Count(output, " help families") == 1 && + strings.Contains(output, "Discover command families:\n "+route+"\n") +} + +func invocationProfileAssertArgvInventory(t *testing.T, renderer cliexec.Renderer, output any, expected map[string]int) { + t.Helper() + actual, invalid := invocationProfileArgvInventory(t, output, renderer.Argv()) + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("generated argv inventory=%v, want exact %v", actual, expected) + } + if len(invalid) != 0 { + t.Fatalf("generated argv fields contain non-active invocation profiles: %v", invalid) + } +} + +func invocationProfileAssertArgvInventoryMutants(t *testing.T, renderer cliexec.Renderer, output map[string]any, expected map[string]int) { + invocationProfileAssertArgvInventoryMutantsAt(t, renderer, output, expected, "nextCommands") +} + +func invocationProfileAssertArgvInventoryMutantsAt(t *testing.T, renderer cliexec.Renderer, output map[string]any, expected map[string]int, collectionKey string) { + t.Helper() + mutants := []struct { + name string + mutate func(map[string]any) + }{ + { + name: "missing argv", + mutate: func(mutant map[string]any) { + command := invocationProfileArgvCommand(t, mutant, collectionKey) + delete(command, "argv") + }, + }, + { + name: "unknown argv prefix", + mutate: func(mutant map[string]any) { + command := invocationProfileArgvCommand(t, mutant, collectionKey) + argv := invocationProfileArray(t, command["argv"], "mutant argv") + command["argv"] = append([]any{"npx", cliexec.BinaryName}, argv[len(renderer.Argv()):]...) + }, + }, + { + name: "bare or wrong-profile argv", + mutate: func(mutant map[string]any) { + command := invocationProfileArgvCommand(t, mutant, collectionKey) + argv := invocationProfileArray(t, command["argv"], "mutant argv") + if renderer.Profile() == cliexec.ProfilePath { + command["argv"] = append([]any{"npm", "exec", "--offline", "--", cliexec.BinaryName}, argv[len(renderer.Argv()):]...) + return + } + command["argv"] = append([]any{cliexec.BinaryName}, argv[len(renderer.Argv()):]...) + }, + }, + { + name: "surplus argv", + mutate: func(mutant map[string]any) { + command := invocationProfileArgvCommand(t, mutant, collectionKey) + mutant["surplus"] = map[string]any{"argv": command["argv"]} + }, + }, + { + name: "relocated argv", + mutate: func(mutant map[string]any) { + command := invocationProfileArgvCommand(t, mutant, collectionKey) + mutant["relocated"] = map[string]any{"argv": command["argv"]} + delete(command, "argv") + }, + }, + } + for _, item := range mutants { + t.Run("argv mutant/"+item.name, func(t *testing.T) { + mutant := invocationProfileObject(t, invocationProfileJSON(t, invocationProfileEncode(t, output))) + item.mutate(mutant) + actual, invalid := invocationProfileArgvInventory(t, mutant, renderer.Argv()) + if reflect.DeepEqual(actual, expected) && len(invalid) == 0 { + t.Fatalf("%s mutant survived exact generated argv inventory", item.name) + } + }) + } +} + +func invocationProfileArgvCommand(t *testing.T, output map[string]any, collectionKey string) map[string]any { + t.Helper() + commands := invocationProfileArray(t, output[collectionKey], "mutant "+collectionKey) + if len(commands) == 0 { + t.Fatalf("mutant %s must contain at least one command", collectionKey) + } + return invocationProfileObjectValue(t, commands[0], "mutant command") +} + +func invocationProfileArgvInventory(t *testing.T, output any, expectedPrefix []string) (map[string]int, map[string]int) { + t.Helper() + inventory := map[string]int{} + invalid := map[string]int{} + var walk func(any, string) + walk = func(raw any, path string) { + switch value := raw.(type) { + case map[string]any: + keys := make([]string, 0, len(value)) + for key := range value { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + if key == "argv" { + argv := invocationProfileStrings(t, value[key], path+".argv") + inventory[path+".argv"]++ + if len(argv) < len(expectedPrefix) || !reflect.DeepEqual(argv[:len(expectedPrefix)], expectedPrefix) { + invalid[path+".argv"]++ + } + continue + } + walk(value[key], path+"."+key) + } + case []any: + for _, item := range value { + selector := "[*]" + if object, ok := item.(map[string]any); ok { + if phase, ok := object["phase"].(string); ok { + selector = "[?phase=" + phase + "]" + } + } + walk(item, path+selector) + } + } + } + walk(output, "$") + return inventory, invalid +} + +func invocationProfileAssertDisplayMatchesArgv(t *testing.T, envelope map[string]any) { + t.Helper() + invocationProfileAssertDisplayMatchesArgvField(t, envelope, "command") +} + +func invocationProfileAssertDisplayMatchesArgvField(t *testing.T, envelope map[string]any, displayField string) { + t.Helper() + if !invocationProfileDisplayMatchesArgv(envelope, displayField) { + t.Fatalf("envelope %s fields do not exactly render argv", displayField) + } +} + +func invocationProfileDisplayMatchesArgv(envelope map[string]any, displayField string) bool { + rawCommands, ok := envelope["commands"].([]any) + if !ok || len(rawCommands) == 0 { + return false + } + for _, raw := range rawCommands { + command, ok := raw.(map[string]any) + if !ok { + return false + } + rawArgv, ok := command["argv"].([]any) + if !ok || len(rawArgv) == 0 { + return false + } + argv := make([]string, 0, len(rawArgv)) + for _, rawValue := range rawArgv { + value, ok := rawValue.(string) + if !ok || value == "" { + return false + } + argv = append(argv, value) + } + display, ok := command[displayField].(string) + if !ok || display != cliexec.DisplayArgv(argv) { + return false + } + } + return true +} + +func invocationProfileBootstrapInventory(root string) map[string]int { + return map[string]int{ + root + ".agentActionPlan[?phase=verify].commands[*]": 3, + root + ".nextCommands[*]": 3, + root + ".payloads.adoptionGuidance.agentGuidance.commands[callerCommandCount:]": 3, + root + ".report.diagnostics[?key=agentActionPlan].value[?phase=verify].commands[*]": 3, + root + ".report.diagnostics[?key=nextCommands].value[*]": 3, + } +} + +func invocationProfileAssertInventory(t *testing.T, renderer cliexec.Renderer, output any, expected map[string]int, callerCommandCount int) { + t.Helper() + actual, wrongProfiles := invocationProfileCommandInventory(t, output, renderer.DisplayCommand(), callerCommandCount) + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("generated command inventory=%v, want exact %v", actual, expected) + } + if len(wrongProfiles) != 0 { + t.Fatalf("generated command fields contain non-active invocation profiles: %v", wrongProfiles) + } +} + +func invocationProfileAssertInventoryMutants(t *testing.T, renderer cliexec.Renderer, output map[string]any, expected map[string]int, callerCommands []string) { + t.Helper() + nextCommands := invocationProfileArray(t, output["nextCommands"], "bootstrap nextCommands") + generatedCommand := nextCommands[0].(string) + bareCommand := cliexec.PathRenderer().DisplayCommand("self-check", "--input", "proofkit/input.json") + wrongProfile, err := cliexec.AdmitLauncherProfile(cliexec.ProfileNPMOffline, "") + if renderer.Profile() == cliexec.ProfileNPMOffline { + wrongProfile, err = cliexec.AdmitLauncherProfile(cliexec.ProfilePythonModule, "/tmp/wrong-profile/bin/python") + } + if err != nil { + t.Fatalf("construct wrong-profile mutant: %v", err) + } + wrongProfileCommand := wrongProfile.DisplayCommand("self-check", "--input", "proofkit/input.json") + + mutants := []struct { + name string + mutate func(map[string]any) + }{ + { + name: "surplus field", + mutate: func(mutant map[string]any) { + mutant["surplusGeneratedCommand"] = generatedCommand + }, + }, + { + name: "bare surplus field", + mutate: func(mutant map[string]any) { + mutant["surplusBareCommand"] = bareCommand + }, + }, + { + name: "wrong profile surplus field", + mutate: func(mutant map[string]any) { + mutant["surplusWrongProfileCommand"] = wrongProfileCommand + }, + }, + { + name: "field relocation", + mutate: func(mutant map[string]any) { + mutant["relocatedNextCommands"] = mutant["nextCommands"] + delete(mutant, "nextCommands") + }, + }, + { + name: "missing command", + mutate: func(mutant map[string]any) { + commands := invocationProfileArray(t, mutant["nextCommands"], "mutant nextCommands") + mutant["nextCommands"] = commands[1:] + }, + }, + { + name: "wrong profile in expected field", + mutate: func(mutant map[string]any) { + commands := invocationProfileArray(t, mutant["nextCommands"], "mutant nextCommands") + commands[0] = wrongProfileCommand + }, + }, + } + for _, item := range mutants { + t.Run("inventory mutant/"+item.name, func(t *testing.T) { + mutant := invocationProfileObject(t, invocationProfileJSON(t, invocationProfileEncode(t, output))) + item.mutate(mutant) + if invocationProfileInventoryMatches(t, mutant, renderer.DisplayCommand(), len(callerCommands), expected) { + t.Fatalf("%s mutant survived exact generated-command inventory", item.name) + } + }) + } + + callerMutant := invocationProfileObject(t, invocationProfileJSON(t, invocationProfileEncode(t, output))) + payloads := invocationProfileObjectValue(t, callerMutant["payloads"], "caller mutant payloads") + guidance := invocationProfileObjectValue(t, payloads["adoptionGuidance"], "caller mutant adoption guidance") + agentGuidance := invocationProfileObjectValue(t, guidance["agentGuidance"], "caller mutant agent guidance") + commands := invocationProfileArray(t, agentGuidance["commands"], "caller mutant guidance commands") + commands[0] = generatedCommand + mutatedCaller := invocationProfileStrings(t, agentGuidance["commands"], "caller mutant guidance commands") + if reflect.DeepEqual(mutatedCaller[:len(callerCommands)], callerCommands) { + t.Fatal("caller rewrite mutant survived caller/generated partition oracle") + } +} + +func invocationProfileInventoryMatches(t *testing.T, output any, prefix string, callerCommandCount int, expected map[string]int) bool { + t.Helper() + actual, wrongProfiles := invocationProfileCommandInventory(t, output, prefix, callerCommandCount) + return reflect.DeepEqual(actual, expected) && len(wrongProfiles) == 0 +} + +func invocationProfileCommandInventory(t *testing.T, output any, prefix string, callerCommandCount int) (map[string]int, map[string]int) { + t.Helper() + inventory := map[string]int{} + wrongProfiles := map[string]int{} + var walk func(any, string) + walk = func(raw any, path string) { + switch value := raw.(type) { + case string: + if invocationProfileIsKnownGeneratedCommand(value) { + inventory[path]++ + if !strings.HasPrefix(value, prefix+" ") { + wrongProfiles[path]++ + } + } + case map[string]any: + keys := make([]string, 0, len(value)) + for key := range value { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + if key == "content" && invocationProfileIsAdoptionGuidanceFile(value) { + content, ok := value[key].(string) + if !ok { + t.Fatalf("%s.content=%#v, want JSON string", path, value[key]) + } + walk(invocationProfileJSON(t, content), path+".content::$") + continue + } + walk(value[key], path+"."+key) + } + case []any: + for index, item := range value { + if strings.HasSuffix(path, ".agentGuidance.commands") && index < callerCommandCount { + continue + } + selector := "[*]" + if object, ok := item.(map[string]any); ok { + switch { + case strings.HasSuffix(path, ".diagnostics"): + if key, ok := object["key"].(string); ok { + selector = "[?key=" + key + "]" + } + case strings.HasSuffix(path, ".agentActionPlan") || strings.Contains(path, "[?key=agentActionPlan].value"): + if phase, ok := object["phase"].(string); ok { + selector = "[?phase=" + phase + "]" + } + case strings.HasSuffix(path, ".files"): + if payloadKey, ok := object["payloadKey"].(string); ok { + selector = "[?payloadKey=" + payloadKey + "]" + } else if purpose, ok := object["purpose"].(string); ok { + selector = "[?purpose=" + purpose + "]" + } + } + } + if strings.HasSuffix(path, ".agentGuidance.commands") { + selector = "[callerCommandCount:]" + } + walk(item, path+selector) + } + } + } + walk(output, "$") + return inventory, wrongProfiles +} + +func invocationProfileIsKnownGeneratedCommand(value string) bool { + switch { + case strings.HasPrefix(value, cliexec.BinaryName+" "): + return true + case strings.HasPrefix(value, "npm exec --offline -- "+cliexec.BinaryName+" "): + return true + } + moduleMarker := " -m agentic_proofkit " + moduleIndex := strings.Index(value, moduleMarker) + if moduleIndex <= 0 { + return false + } + executable := value[:moduleIndex] + return strings.HasPrefix(executable, "/") || strings.HasPrefix(executable, "'/") +} + +func invocationProfileRun(t *testing.T, renderer cliexec.Renderer, args []string, stdin string) map[string]any { + return invocationProfileRunStatus(t, renderer, args, stdin, 0) +} + +func invocationProfileRunStatus(t *testing.T, renderer cliexec.Renderer, args []string, stdin string, expectedStatus int) map[string]any { + t.Helper() + var stdout bytes.Buffer + var stderr bytes.Buffer + status := RunWithRenderer(t.Context(), args, strings.NewReader(stdin), &stdout, &stderr, renderer) + if status != expectedStatus || stderr.Len() != 0 { + t.Fatalf("RunWithRenderer(%v) status=%d stderr=%s stdout=%s", args, status, stderr.String(), stdout.String()) + } + return invocationProfileObject(t, invocationProfileJSON(t, stdout.String())) +} + +func invocationProfileRunText(t *testing.T, renderer cliexec.Renderer, args []string) string { + t.Helper() + var stdout bytes.Buffer + var stderr bytes.Buffer + status := RunWithRenderer(t.Context(), args, panicReader{}, &stdout, &stderr, renderer) + if status != 0 || stderr.Len() != 0 { + t.Fatalf("RunWithRenderer(%v) status=%d stderr=%s stdout=%s", args, status, stderr.String(), stdout.String()) + } + return stdout.String() +} + +func invocationProfileAssertBootstrap(t *testing.T, renderer cliexec.Renderer, output map[string]any, callerCommands []string) { + t.Helper() + invocationProfileAssertGenerated(t, renderer, invocationProfileStrings(t, output["nextCommands"], "bootstrap nextCommands")) + + for _, rawAction := range invocationProfileArray(t, output["agentActionPlan"], "bootstrap agentActionPlan") { + action := invocationProfileObjectValue(t, rawAction, "bootstrap action") + if action["phase"] == "verify" { + invocationProfileAssertGenerated(t, renderer, invocationProfileStrings(t, action["commands"], "bootstrap verify commands")) + } + } + + payloads := invocationProfileObjectValue(t, output["payloads"], "bootstrap payloads") + guidance := invocationProfileObjectValue(t, payloads["adoptionGuidance"], "bootstrap adoption guidance") + agentGuidance := invocationProfileObjectValue(t, guidance["agentGuidance"], "bootstrap agent guidance") + invocationProfileAssertCallerAndGenerated(t, renderer, invocationProfileStrings(t, agentGuidance["commands"], "bootstrap guidance commands"), callerCommands) + + report := invocationProfileObjectValue(t, output["report"], "bootstrap report") + invocationProfileAssertGenerated(t, renderer, invocationProfileStrings(t, invocationProfileDiagnostic(t, report, "nextCommands"), "report nextCommands")) + for _, rawAction := range invocationProfileArray(t, invocationProfileDiagnostic(t, report, "agentActionPlan"), "report agentActionPlan") { + action := invocationProfileObjectValue(t, rawAction, "report action") + if action["phase"] == "verify" { + invocationProfileAssertGenerated(t, renderer, invocationProfileStrings(t, action["commands"], "report verify commands")) + } + } +} + +func invocationProfileAssertManifest(t *testing.T, renderer cliexec.Renderer, manifest map[string]any, callerCommands []string) { + t.Helper() + invocationProfileAssertGenerated(t, renderer, invocationProfileStrings(t, manifest["nextCommands"], "manifest nextCommands")) + for _, rawFile := range invocationProfileArray(t, manifest["files"], "manifest files") { + file := invocationProfileObjectValue(t, rawFile, "manifest file") + if !invocationProfileIsAdoptionGuidanceFile(file) { + continue + } + content, ok := file["content"].(string) + if !ok { + t.Fatalf("adoptionGuidance content=%#v, want JSON string", file["content"]) + } + guidance := invocationProfileObject(t, invocationProfileJSON(t, content)) + agentGuidance := invocationProfileObjectValue(t, guidance["agentGuidance"], "materialized agent guidance") + invocationProfileAssertCallerAndGenerated(t, renderer, invocationProfileStrings(t, agentGuidance["commands"], "materialized guidance commands"), callerCommands) + return + } + t.Fatal("manifest missing adoptionGuidance payload identity") +} + +func invocationProfileIsAdoptionGuidanceFile(file map[string]any) bool { + return file["payloadKey"] == "adoptionGuidance" || + file["purpose"] == "caller-owned gradual adoption guidance input" +} + +func invocationProfileAssertEnvelope(t *testing.T, renderer cliexec.Renderer, envelope map[string]any) { + t.Helper() + commands := []string{} + for _, rawCommand := range invocationProfileArray(t, envelope["commands"], "envelope commands") { + command := invocationProfileObjectValue(t, rawCommand, "envelope command") + value, ok := command["command"].(string) + if !ok { + t.Fatalf("envelope command=%#v, want string", command["command"]) + } + commands = append(commands, value) + } + invocationProfileAssertGenerated(t, renderer, commands) +} + +func invocationProfileAssertCallerAndGenerated(t *testing.T, renderer cliexec.Renderer, commands []string, callerCommands []string) { + t.Helper() + if len(commands) <= len(callerCommands) { + t.Fatalf("commands=%#v, want caller prefix and generated suffix", commands) + } + if !reflect.DeepEqual(commands[:len(callerCommands)], callerCommands) { + t.Fatalf("caller command prefix=%#v, want unchanged %#v", commands[:len(callerCommands)], callerCommands) + } + invocationProfileAssertGenerated(t, renderer, commands[len(callerCommands):]) +} + +func invocationProfileAssertGenerated(t *testing.T, renderer cliexec.Renderer, commands []string) { + t.Helper() + if len(commands) == 0 { + t.Fatal("generated command field must not be empty") + } + prefix := renderer.DisplayCommand() + for _, command := range commands { + if !strings.HasPrefix(command, prefix+" ") { + t.Fatalf("generated command=%q, want exact admitted prefix %q", command, prefix) + } + } +} + +func invocationProfileDiagnostic(t *testing.T, report map[string]any, key string) any { + t.Helper() + for _, rawDiagnostic := range invocationProfileArray(t, report["diagnostics"], "diagnostics") { + diagnostic := invocationProfileObjectValue(t, rawDiagnostic, "diagnostic") + if diagnostic["key"] == key { + return diagnostic["value"] + } + } + t.Fatalf("diagnostic %q missing", key) + return nil +} + +func invocationProfileProjectInput(bootstrap map[string]any) map[string]any { + bootstrap["paths"].(map[string]any)["adoptionProfilePath"] = "proofkit/adoption-profile.json" + return map[string]any{ + "schemaVersion": json.Number("1"), + "scaffoldId": "proofkit.cli.invocation-profile-project", + "nonClaims": []any{"Invocation profile fixture does not write files."}, + "paths": map[string]any{ + "bootstrapInputPath": "proofkit/bootstrap.v1.json", + "repoProfileScaffoldInputPath": "proofkit/repo-profile-scaffold.v1.json", + "workflowInputPath": "proofkit/adoption-workflow.v1.json", + }, + "workflow": map[string]any{ + "nonClaims": []any{"Invocation profile workflow fixture does not execute commands."}, + "scenario": "new_repository", + "workflowId": "proofkit.cli.invocation-profile-workflow", + }, + "repoProfileScaffold": map[string]any{ + "schemaVersion": json.Number("1"), + "planId": "proofkit.cli.invocation-profile-repo-plan", + "presetId": "typescript_workspace", + "repository": map[string]any{ + "name": "consumer-repo", + "primaryLanguages": []any{"go"}, + "profilePath": "proofkit/profile.json", + "rootPackageName": "consumer-repo", + }, + "paths": map[string]any{ + "bindingPath": "proofkit/proof-bindings.json", + "generatedArtifacts": []any{}, + "policyPath": "proofkit/profile.json", + "proofLikePaths": []any{"docs/specs/cli-adoption/requirements.v1.json"}, + "retiredProofLikePaths": []any{}, + "routerPath": "AGENTS.md", + "specGlobs": []any{"docs/specs/**/*.json"}, + }, + "requirements": map[string]any{"idPattern": "REQ-CONSUMER-[0-9]+"}, + "environmentClasses": []any{"local-go"}, + "commandMatcherHints": []any{ + map[string]any{ + "allowedScripts": []any{"check"}, + "credentialClass": "none", + "id": "consumer.check", + "kind": "bun_repo_script", + "networkPolicy": "none", + "parallelGroup": "local", + }, + }, + "nonClaims": []any{"Invocation profile scaffold fixture does not prove repository facts."}, + }, + "bootstrap": bootstrap, + } +} + +func invocationProfileJSON(t *testing.T, raw any) any { + t.Helper() + var encoded []byte + switch value := raw.(type) { + case string: + encoded = []byte(value) + default: + var err error + encoded, err = json.Marshal(value) + if err != nil { + t.Fatalf("marshal JSON: %v", err) + } + } + var decoded any + decoder := json.NewDecoder(bytes.NewReader(encoded)) + decoder.UseNumber() + if err := decoder.Decode(&decoded); err != nil { + t.Fatalf("decode JSON: %v\n%s", err, encoded) + } + return decoded +} + +func invocationProfileEncode(t *testing.T, raw any) string { + t.Helper() + encoded, err := json.Marshal(raw) + if err != nil { + t.Fatalf("marshal JSON: %v", err) + } + return string(encoded) +} + +func invocationProfileObject(t *testing.T, raw any) map[string]any { + t.Helper() + return invocationProfileObjectValue(t, raw, "JSON object") +} + +func invocationProfileObjectValue(t *testing.T, raw any, context string) map[string]any { + t.Helper() + value, ok := raw.(map[string]any) + if !ok { + t.Fatalf("%s=%#v, want object", context, raw) + } + return value +} + +func invocationProfileArray(t *testing.T, raw any, context string) []any { + t.Helper() + value, ok := raw.([]any) + if !ok { + t.Fatalf("%s=%#v, want array", context, raw) + } + return value +} + +func invocationProfileStrings(t *testing.T, raw any, context string) []string { + t.Helper() + values := invocationProfileArray(t, raw, context) + result := make([]string, 0, len(values)) + for _, rawValue := range values { + value, ok := rawValue.(string) + if !ok { + t.Fatalf("%s item=%#v, want string", context, rawValue) + } + result = append(result, value) + } + return result +} diff --git a/internal/app/planning_changed_command.go b/internal/app/planning_changed_command.go index 1e64aa5..cd975ff 100644 --- a/internal/app/planning_changed_command.go +++ b/internal/app/planning_changed_command.go @@ -4,11 +4,15 @@ import ( "io" "github.com/research-engineering/agentic-proofkit/internal/command/changedpathset" + "github.com/research-engineering/agentic-proofkit/internal/kernel/agentenvelope" ) func runChangedPathSetPlanning(input any, options planningArgs, stdout io.Writer, stderr io.Writer) int { result, err := changedpathset.Build(input) if err != nil { + if options.agentEnvelope { + return writeJSON(agentenvelope.InvalidInput(err.Error()), 1, nil, stdout, stderr) + } writeDiagnostic(stderr, err) return 1 } diff --git a/internal/app/requirement_commands.go b/internal/app/requirement_commands.go index 48ca1ef..99b550a 100644 --- a/internal/app/requirement_commands.go +++ b/internal/app/requirement_commands.go @@ -1,6 +1,11 @@ package app import ( + "bytes" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "errors" "fmt" "io" "os" @@ -12,12 +17,15 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/command/requirementsourceview" "github.com/research-engineering/agentic-proofkit/internal/command/requirementspectree" "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" "github.com/research-engineering/agentic-proofkit/internal/kernel/compactproofcontract" "github.com/research-engineering/agentic-proofkit/internal/kernel/jsonpointer" "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" ) -func runRequirementView(command string, args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int { +var outputWriterBarrier func(stage string, outputPath string) + +func runRequirementView(command string, args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer, renderer cliexec.Renderer) int { options, err := parseRequirementViewArgs(command, args) if err != nil { writeDiagnostic(stderr, err) @@ -39,7 +47,7 @@ func runRequirementView(command string, args []string, stdin io.Reader, stdout i return writeSourceView(input, options, stdout, stderr) } if command == "requirement-coverage-view" { - return writeCoverageView(input, options, stdout, stderr) + return writeCoverageView(input, options, stdout, stderr, renderer) } if command == "requirement-spec-tree-view" { return writeSpecTreeView(input, options, stdout, stderr) @@ -177,7 +185,7 @@ func writeProofView(input any, options requirementViewArgs, stdout io.Writer, st return writeJSON(output, exitCode, err, stdout, stderr) } -func writeCoverageView(input any, options requirementViewArgs, stdout io.Writer, stderr io.Writer) int { +func writeCoverageView(input any, options requirementViewArgs, stdout io.Writer, stderr io.Writer, renderer cliexec.Renderer) int { if format := options.format; format == "markdown" { output, exitCode, err := requirementcoverageview.BuildMarkdown(input) return writeText(output, exitCode, err, stdout, stderr) @@ -185,7 +193,7 @@ func writeCoverageView(input any, options requirementViewArgs, stdout io.Writer, output, exitCode, err := requirementcoverageview.BuildHTML(input) return writeText(output, exitCode, err, stdout, stderr) } - output, exitCode, err := requirementcoverageview.BuildJSON(input, requirementcoverageview.Options{AgentEnvelope: options.agentEnvelope}) + output, exitCode, err := requirementcoverageview.BuildJSON(input, requirementcoverageview.Options{AgentEnvelope: options.agentEnvelope, Renderer: renderer}) return writeJSON(output, exitCode, err, stdout, stderr) } @@ -239,29 +247,49 @@ func writeViewText(output string, exitCode int, err error, outputPath string, st } func writeRepoRelativeOutputFile(nativePath string, content []byte) error { - parent := filepath.Dir(nativePath) - if err := ensureOutputParent(parent); err != nil { + relative, err := admit.SafeRepoRelativePath(filepath.ToSlash(nativePath), "output path") + if err != nil { + return err + } + root, err := os.OpenRoot(".") + if err != nil { + return err + } + defer root.Close() + parent := pathDir(relative) + if err := ensureOutputParent(root, parent); err != nil { + return err + } + parentRoot, parentInfo, err := openStableOutputParent(root, parent) + if err != nil { return err } - if info, err := os.Lstat(nativePath); err == nil { + defer parentRoot.Close() + leaf := pathBase(relative) + if info, err := parentRoot.Lstat(filepath.FromSlash(leaf)); err == nil { if info.Mode()&os.ModeSymlink != 0 { - return fmt.Errorf("output path must not be a symlink: %s", filepath.ToSlash(nativePath)) + return fmt.Errorf("output path must not be a symlink: %s", relative) } if info.IsDir() { - return fmt.Errorf("output path must not be a directory: %s", filepath.ToSlash(nativePath)) + return fmt.Errorf("output path must not be a directory: %s", relative) } } else if !os.IsNotExist(err) { return err } - temp, err := os.CreateTemp(parent, ".proofkit-output-*") + if outputWriterBarrier != nil { + outputWriterBarrier("parent_admitted", relative) + } + if err := requireStableOutputParent(root, parent, parentInfo); err != nil { + return err + } + tempRelative, temp, err := createRootTemp(parentRoot, ".") if err != nil { return err } - tempPath := temp.Name() cleanup := true defer func() { if cleanup { - _ = os.Remove(tempPath) + _ = parentRoot.Remove(filepath.FromSlash(tempRelative)) } }() if _, err := temp.Write(content); err != nil { @@ -272,32 +300,92 @@ func writeRepoRelativeOutputFile(nativePath string, content []byte) error { _ = temp.Close() return err } + tempInfo, err := temp.Stat() + if err != nil { + _ = temp.Close() + return err + } if err := temp.Close(); err != nil { return err } - if err := os.Rename(tempPath, nativePath); err != nil { + if outputWriterBarrier != nil { + outputWriterBarrier("before_publish", relative) + } + if err := requireStableOutputParent(root, parent, parentInfo); err != nil { + return err + } + currentTempRouteInfo, err := parentRoot.Lstat(filepath.FromSlash(tempRelative)) + if err != nil { + return err + } + if currentTempRouteInfo.Mode()&os.ModeSymlink != 0 || !os.SameFile(tempInfo, currentTempRouteInfo) { + return errors.New("temporary output identity changed before publication") + } + if currentTempRouteInfo.Mode() != 0o644 { + return errors.New("temporary output mode changed before publication") + } + currentTemp, err := parentRoot.Open(filepath.FromSlash(tempRelative)) + if err != nil { + return err + } + currentTempInfo, err := currentTemp.Stat() + if err != nil { + _ = currentTemp.Close() + return err + } + if !os.SameFile(tempInfo, currentTempInfo) { + _ = currentTemp.Close() + return errors.New("temporary output identity changed before publication") + } + if currentTempInfo.Mode() != 0o644 { + _ = currentTemp.Close() + return errors.New("temporary output mode changed before publication") + } + currentDigest := sha256.New() + if _, err := io.Copy(currentDigest, currentTemp); err != nil { + _ = currentTemp.Close() + return err + } + if err := currentTemp.Close(); err != nil { + return err + } + expectedDigest := sha256.Sum256(content) + if !bytes.Equal(currentDigest.Sum(nil), expectedDigest[:]) { + return errors.New("temporary output content changed before publication") + } + if outputWriterBarrier != nil { + outputWriterBarrier("before_rename", relative) + } + if err := requireStableOutputParent(root, parent, parentInfo); err != nil { + return err + } + if err := parentRoot.Rename(filepath.FromSlash(tempRelative), filepath.FromSlash(leaf)); err != nil { return err } cleanup = false return nil } -func ensureOutputParent(parent string) error { +func ensureOutputParent(root *os.Root, parent string) error { if parent == "." || parent == "" { return nil } - current := "." - for _, part := range strings.Split(filepath.Clean(parent), string(filepath.Separator)) { + current := "" + for _, part := range strings.Split(parent, "/") { if part == "." || part == "" { continue } - current = filepath.Join(current, part) - info, err := os.Lstat(current) + if current == "" { + current = part + } else { + current += "/" + part + } + info, err := root.Lstat(filepath.FromSlash(current)) if os.IsNotExist(err) { - if err := os.Mkdir(current, 0o755); err != nil && !os.IsExist(err) { + if err := root.Mkdir(filepath.FromSlash(current), 0o755); err != nil && !os.IsExist(err) { return err } - info, err = os.Lstat(current) + info, err = root.Lstat(filepath.FromSlash(current)) } if err != nil { return err @@ -311,3 +399,80 @@ func ensureOutputParent(parent string) error { } return nil } + +func openStableOutputParent(root *os.Root, parent string) (*os.Root, os.FileInfo, error) { + nativeParent := filepath.FromSlash(parent) + routeInfo, err := root.Lstat(nativeParent) + if err != nil { + return nil, nil, err + } + if routeInfo.Mode()&os.ModeSymlink != 0 { + return nil, nil, fmt.Errorf("output parent must not be a symlink: %s", parent) + } + if !routeInfo.IsDir() { + return nil, nil, fmt.Errorf("output parent must be a directory: %s", parent) + } + parentRoot, err := root.OpenRoot(nativeParent) + if err != nil { + return nil, nil, err + } + handleInfo, err := parentRoot.Stat(".") + if err != nil { + _ = parentRoot.Close() + return nil, nil, err + } + if !os.SameFile(routeInfo, handleInfo) { + _ = parentRoot.Close() + return nil, nil, fmt.Errorf("output parent changed while it was admitted: %s", parent) + } + return parentRoot, handleInfo, nil +} + +func requireStableOutputParent(root *os.Root, parent string, admitted os.FileInfo) error { + info, err := root.Lstat(filepath.FromSlash(parent)) + if err != nil { + return fmt.Errorf("output parent changed after admission: %s: %w", parent, err) + } + if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() || !os.SameFile(info, admitted) { + return fmt.Errorf("output parent changed after admission: %s", parent) + } + return nil +} + +func createRootTemp(root *os.Root, parent string) (string, *os.File, error) { + for attempt := 0; attempt < 32; attempt++ { + random := make([]byte, 16) + if _, err := rand.Read(random); err != nil { + return "", nil, err + } + name := ".proofkit-output-" + hex.EncodeToString(random) + relative := name + if parent != "." && parent != "" { + relative = parent + "/" + name + } + file, err := root.OpenFile(filepath.FromSlash(relative), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) + if err == nil { + return relative, file, nil + } + if !os.IsExist(err) { + return "", nil, err + } + } + return "", nil, fmt.Errorf("unable to allocate a unique output temporary file") +} + +func pathDir(value string) string { + index := strings.LastIndexByte(value, '/') + if index < 0 { + return "." + } + return value[:index] +} + +func pathBase(value string) string { + index := strings.LastIndexByte(value, '/') + if index < 0 { + return value + } + return value[index+1:] +} diff --git a/internal/app/requirement_context_cli_test.go b/internal/app/requirement_context_cli_test.go index 5cd6049..54a3e71 100644 --- a/internal/app/requirement_context_cli_test.go +++ b/internal/app/requirement_context_cli_test.go @@ -3,9 +3,13 @@ package app import ( "bytes" "context" + "crypto/sha256" + "encoding/hex" "encoding/json" + "io/fs" "os" "path/filepath" + "strings" "testing" "github.com/research-engineering/agentic-proofkit/internal/command/requirementcontext" @@ -49,6 +53,9 @@ func TestRequirementContextCommandsComposeThroughWholeCLI(t *testing.T) { if _, err := requirementcontext.AdmitSnapshot(base); err != nil { t.Fatalf("whole-CLI context output failed owner admission: %v", err) } + if base["schemaVersion"] != json.Number("2") || base["expectedDigestCoverage"] != "none" || base["baselineVerification"] != nil { + t.Fatalf("whole-CLI context output did not use the v2 digest-coverage contract: %#v", base) + } slice := runAppJSON(t, []string{"requirement-context-slice", "--input", "-"}, map[string]any{ "schemaVersion": json.Number("1"), "sliceId": "consumer.context.slice", "context": base, "query": map[string]any{"profile": "specification", "requirementIds": []any{"REQ-CONSUMER-001"}}, @@ -61,12 +68,15 @@ func TestRequirementContextCommandsComposeThroughWholeCLI(t *testing.T) { writeCLIJSONFixture(t, root, "docs/specs/consumer/requirements.v1.json", requirementSource) current := runAppJSON(t, []string{"requirement-context-compose", "--input", "-", "--repo-root", root}, catalog) diff := runAppJSON(t, []string{"requirement-semantic-diff", "--input", "-"}, map[string]any{ - "schemaVersion": json.Number("1"), "diffId": "consumer.requirement.diff", + "schemaVersion": json.Number("2"), "diffId": "consumer.requirement.diff", "baseContext": base, "currentContext": current, }) if diff["changeCount"] != json.Number("1") { t.Fatalf("whole-CLI semantic diff changeCount=%v, want 1", diff["changeCount"]) } + if diff["schemaVersion"] != json.Number("2") || diff["baseExpectedDigestCoverage"] != "none" || diff["currentExpectedDigestCoverage"] != "none" || diff["baseBaselineVerification"] != nil || diff["currentBaselineVerification"] != nil { + t.Fatalf("whole-CLI semantic diff did not use the v2 digest-coverage contract: %#v", diff) + } if _, err := requirementdiff.AdmitOutput(diff, current["snapshotId"].(string)); err != nil { t.Fatalf("whole-CLI semantic diff failed owner admission: %v", err) } @@ -79,6 +89,314 @@ func TestRequirementContextCommandsComposeThroughWholeCLI(t *testing.T) { } } +func TestLegacyDigestVocabularyConfinedToV1AdaptersAndFixtures(t *testing.T) { + repoRoot := filepath.Clean(filepath.Join("..", "..")) + allowed := map[string]struct{}{ + "internal/app/requirement_context_cli_test.go": {}, + "internal/command/requirementbrowser/v1_adapter.go": {}, + "internal/command/requirementbrowser/workspace_test.go": {}, + "internal/command/requirementcontext/requirementcontext_test.go": {}, + "internal/command/requirementcontext/v1_adapter.go": {}, + "internal/command/requirementdiff/requirementdiff_test.go": {}, + "internal/command/requirementdiff/v1_adapter.go": {}, + "internal/command/requirementgraph/requirementgraph_test.go": {}, + } + legacy := []string{ + "BaselineVerification", + "baselineVerification", + "baseBaselineVerification", + "currentBaselineVerification", + "partially_verified", + "Baseline:", + } + roots := []string{ + "internal/app", + "internal/command/requirementbrowser", + "internal/command/requirementcontext", + "internal/command/requirementdiff", + "internal/command/requirementgraph", + "internal/testsupport/browserfixture", + "tests/browser", + } + for _, root := range roots { + err := filepath.WalkDir(filepath.Join(repoRoot, root), func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() { + return nil + } + relative, err := filepath.Rel(repoRoot, path) + if err != nil { + return err + } + relative = filepath.ToSlash(relative) + if _, ok := allowed[relative]; ok { + return nil + } + extension := filepath.Ext(relative) + if extension != ".go" && extension != ".js" && extension != ".json" && extension != ".mjs" { + return nil + } + content, err := os.ReadFile(path) + if err != nil { + return err + } + for _, token := range legacy { + if strings.Contains(string(content), token) { + t.Errorf("legacy digest vocabulary %q escaped into %s", token, relative) + } + } + return nil + }) + if err != nil { + t.Fatal(err) + } + } + contract, err := os.ReadFile(filepath.Join(repoRoot, "proofkit/cli-contract.v2.json")) + if err != nil { + t.Fatal(err) + } + for _, token := range legacy { + if strings.Contains(string(contract), token) { + t.Errorf("legacy digest vocabulary %q escaped into proofkit/cli-contract.v2.json", token) + } + } +} + +func TestConditionSensitiveProofCommandsUseExactRootVariants(t *testing.T) { + for _, projection := range []struct { + kind string + variant string + }{ + {kind: "canonical_contract", variant: "01-canonical-contract"}, + {kind: "resolver_input", variant: "02-resolver-input"}, + } { + t.Run("source-set/"+projection.kind, func(t *testing.T) { + input := cliProofSourceSetInput(t, projection.kind) + assertPublicCLIRootVariant(t, "requirement-proof-source-set", "input", "01-root", input) + output := runAppJSON(t, []string{"requirement-proof-source-set", "--input", "-"}, input) + assertPublicCLIRootVariant(t, "requirement-proof-source-set", "output", projection.variant, output) + }) + } + + structured := readCLIJSONObject(t, "proofkit/requirement-bindings.json") + assertPublicCLIRootVariant(t, "requirement-proof-view", "input", "02-structured", structured) + structuredOutput := runAppJSON(t, []string{"requirement-proof-view", "--input", "-", "--scope", "graph"}, structured) + assertPublicCLIRootVariant(t, "requirement-proof-view", "output", "02-structured", structuredOutput) + + compact := cliCompactProofContract() + assertPublicCLIRootVariant(t, "requirement-proof-view", "input", "01-compact", compact) + compactOutput := runAppJSON(t, []string{"requirement-proof-view", "--input", "-", "--empty-local-environment-policy"}, compact) + assertPublicCLIRootVariant(t, "requirement-proof-view", "output", "01-compact", compactOutput) + + directWitnessPlan := cliWitnessPlanDirectInput() + assertPublicCLIRootVariant(t, "witness-plan", "input", "01-direct", directWitnessPlan) + _ = runAppJSON(t, []string{"witness-plan", "--input", "-"}, directWitnessPlan) + + projectedWitnessPlan := cliWitnessPlanProjectionInput() + assertPublicCLIRootVariant(t, "witness-plan", "input", "02-requirement-bindings-projection", projectedWitnessPlan) + _ = runAppJSON(t, []string{"witness-plan", "--input", "-"}, projectedWitnessPlan) +} + +func readCLIJSONObject(t *testing.T, path string) map[string]any { + t.Helper() + content, err := os.ReadFile(filepath.Join(repoRoot(t), filepath.FromSlash(path))) + if err != nil { + t.Fatal(err) + } + value, err := admission.DecodeJSON(bytes.NewReader(content), int64(len(content))) + if err != nil { + t.Fatal(err) + } + record, ok := value.(map[string]any) + if !ok { + t.Fatalf("%s must be an object", path) + } + return record +} + +func cliProofSourceSetInput(t *testing.T, projection string) map[string]any { + t.Helper() + fragment := map[string]any{ + "schema_version": json.Number("1"), + "contract_kind": "requirement_proof_binding_fragment", + "contract_id": "requirement-proof-bindings/fragment/v1", + "authority_state": "canonical_requirement_to_proof_binding_fragment", + "normalization_profile": "json/v1:utf8+lf+compact-owner-row-arrays", + "source_id": "source.local", + "surfaces": []any{ + []any{"source.local", []any{"unit"}, false, "not_allowed", "none", []any{"local-go"}, []any{}, "checked"}, + }, + "bindings": []any{ + []any{ + "REQ-PROOFKIT-SOURCE-001", + "source.local", + "source.local::owned_invariant", + "contract", + "owned_invariant", + "witness_backed", + "blocking", + []any{"local-go"}, + []any{"internal/source_test.go::TestPositive", []any{"local-go"}, []any{"go test ./..."}, json.Number("0")}, + []any{"internal/source_test.go::TestNegative", []any{"local-go"}, []any{"go test ./..."}, json.Number("1")}, + []any{"go test ./..."}, + "checked", + }, + }, + } + fragmentBytes, err := stablejson.Marshal(fragment) + if err != nil { + t.Fatal(err) + } + sum := sha256.Sum256(fragmentBytes) + path := "docs/contracts/requirement-proof-bindings/local.v1.json" + return map[string]any{ + "canonicalEnvelope": map[string]any{ + "schemaVersion": json.Number("1"), + "contractKind": "requirement_proof_binding", + "contractId": "requirement-proof-bindings/v1", + "authorityState": "canonical_requirement_to_proof_binding", + "normalizationProfile": "json/v1:utf8+lf+compact-row-arrays", + "nonClaims": []any{"CLI source-set fixture does not prove repository coverage."}, + "surfaceColumns": []any{"surface_id", "proof_families", "rollout_claim_allowed", "rollout_claim_state", "rollout_claim_scope", "required_environment_classes", "preconditioned_environment_classes", "mutation_resistance_state"}, + "bindingColumns": []any{"requirement_id", "surface_id", "scenario_id", "invariant_role", "owned_invariant", "proof_contract_state", "blocking_status", "required_environment_classes", "positive_witness", "falsification_witness", "verify_commands", "mutation_resistance_state"}, + "witnessColumns": []any{"selector", "environment_classes", "verify_commands", "resolution_order_index"}, + }, + "sourceSet": map[string]any{ + "schema_version": json.Number("1"), + "contract_kind": "requirement_proof_binding_source_set", + "contract_id": "requirement-proof-bindings/source-set/v1", + "authority_state": "requirement_proof_binding_source_index", + "normalization_profile": "json/v1:utf8+lf+ordered-source-refs", + "source_columns": []any{"source_id", "path", "sha256", "role", "non_claims"}, + "sources": []any{ + []any{"source.local", path, hex.EncodeToString(sum[:]), "requirement_proof_binding_fragment", []any{"CLI source owns its fixture rows."}}, + }, + "non_claims": []any{"CLI source-set fixture does not prove repository coverage."}, + }, + "sources": []any{map[string]any{"path": path, "text": string(fragmentBytes)}}, + "projection": map[string]any{"kind": projection}, + } +} + +func cliCompactProofContract() map[string]any { + return map[string]any{ + "schema_version": json.Number("1"), + "authority_state": "canonical", + "contract_id": "proofkit.cli.compact", + "contract_kind": "requirement_proof_binding", + "normalization_profile": "proofkit.compact.v1", + "non_claims": []any{"Compact CLI fixture does not execute witnesses."}, + "surface_columns": []any{"surface_id", "required_environment_classes", "preconditioned_environment_classes"}, + "surfaces": []any{[]any{"proofkit.surface", []any{"local-go"}, []any{}}}, + "witness_columns": []any{"selector", "environment_classes", "verify_commands", "resolution_order_index"}, + "binding_columns": []any{"requirement_id", "surface_id", "scenario_id", "invariant_role", "owned_invariant", "proof_contract_state", "blocking_status", "required_environment_classes", "positive_witness", "falsification_witness", "verify_commands", "mutation_resistance_state"}, + "bindings": []any{[]any{ + "REQ-PROOFKIT-COMPACT-001", + "proofkit.surface", + "proofkit.surface::scenario.compact", + "contract", + "proofkit.compact", + "witness_backed", + "blocking", + []any{"local-go"}, + []any{"tests/positive_test.go::TestPositive", []any{"local-go"}, []any{"go test ./... -run TestPositive"}, json.Number("0")}, + []any{"tests/negative_test.go::TestNegative", []any{"local-go"}, []any{"go test ./... -run TestNegative"}, json.Number("1")}, + []any{"go test ./... -run TestPositive", "go test ./... -run TestNegative"}, + "no_known_advisory_gap", + }}, + } +} + +func cliWitnessPlanProjectionInput() map[string]any { + vocabulary := cliWitnessPlanVocabulary() + return map[string]any{ + "schemaVersion": json.Number("1"), + "projection": "requirement-bindings", + "vocabulary": vocabulary, + "requirementProofBinding": map[string]any{ + "schemaVersion": json.Number("1"), + "bindingId": "proofkit.cli.witnessplan.binding", + "requirements": []any{map[string]any{ + "claimLevel": "blocking", + "nonClaims": []any{"Witness-plan CLI fixture does not execute commands."}, + "ownerId": "proofkit.witnessplan", + "proofState": "witness_backed", + "requirementId": "REQ-PROOFKIT-WITNESSPLAN-001", + "specPath": "docs/specs/proofkit-witnessplan/requirements.v1.json", + }}, + "bindings": []any{map[string]any{ + "commandIds": []any{"proofkit.test-command"}, + "environmentClasses": []any{"local-go"}, + "requirementId": "REQ-PROOFKIT-WITNESSPLAN-001", + "scenarioId": "proofkit.witnessplan.scenario", + "witnessId": "proofkit.witnessplan.witness", + "witnessKind": "contract", + "witnessPath": "internal/command/witnessplan/witnessplan_test.go", + }}, + "witnessCommands": []any{map[string]any{ + "command": "go test ./internal/command/witnessplan", + "commandId": "proofkit.test-command", + "environmentClass": "local-go", + }}, + "selection": map[string]any{ + "changedPaths": []any{}, + "ownerIds": []any{}, + "requirementIds": []any{}, + }, + "nonClaims": []any{"Witness-plan CLI fixture does not prove command pass evidence."}, + }, + } +} + +func cliWitnessPlanDirectInput() map[string]any { + return map[string]any{ + "schemaVersion": json.Number("1"), + "vocabulary": cliWitnessPlanVocabulary(), + "commands": []any{map[string]any{ + "schemaVersion": json.Number("1"), + "id": "proofkit.test-command", + "cwd": ".", + "argv": []any{"go", "test", "./..."}, + "timeoutMs": json.Number("1000"), + "networkPolicy": "none", + "credentialClass": "none", + "cachePolicy": "disabled", + "parallelGroup": "local", + "environment": map[string]any{ + "inherit": "none", + "allowlist": []any{}, + "classes": []any{"local-go"}, + }, + "expectedArtifacts": []any{ + map[string]any{"kind": "report", "path": "artifacts/proofkit/report.json", "required": true}, + }, + "exitCodePolicy": map[string]any{ + "kind": "zero", + "successCodes": []any{json.Number("0")}, + }, + }}, + } +} + +func cliWitnessPlanVocabulary() map[string]any { + return map[string]any{ + "artifactKinds": []any{"report"}, + "credentialClasses": []any{"none"}, + "environmentClasses": []any{"local-go"}, + "nonCacheableCredentialClasses": []any{}, + "parallelGroups": []any{"local"}, + "maxTimeoutMs": json.Number("10000"), + "environmentClassPolicies": []any{map[string]any{ + "environmentClass": "local-go", + "networkPolicies": []any{"none"}, + "credentialClasses": []any{"none"}, + "cachePolicies": []any{"disabled"}, + }}, + } +} + func cliRequirementSource(invariant string) map[string]any { return map[string]any{ "schemaVersion": json.Number("1"), "sourceId": "consumer.requirements", diff --git a/internal/app/stack_command.go b/internal/app/stack_command.go index 9583c7e..a50c099 100644 --- a/internal/app/stack_command.go +++ b/internal/app/stack_command.go @@ -2,6 +2,7 @@ package app import ( "fmt" + "strings" "github.com/research-engineering/agentic-proofkit/internal/command/stackpreset" ) @@ -21,7 +22,7 @@ func parseStackPresetArgs(args []string) (string, error) { index++ } if presetID == "" || !stackpreset.IsPresetID(presetID) { - return "", fmt.Errorf("--preset requires a known stack preset id") + return "", fmt.Errorf("--preset requires one of: %s", strings.Join(stackpreset.IDs(), ", ")) } return presetID, nil } diff --git a/internal/app/usage.go b/internal/app/usage.go index c71b4d6..76f94e2 100644 --- a/internal/app/usage.go +++ b/internal/app/usage.go @@ -1,13 +1,24 @@ package app -import "strings" +import ( + "strings" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" +) func usage() string { + return usageWithRenderer(cliexec.PathRenderer()) +} + +func usageWithRenderer(renderer cliexec.Renderer) string { lines := []string{"Usage:", " agentic-proofkit [--json-layout pretty|compact] [arguments]", "", "Commands:"} for _, descriptor := range commandDescriptors { lines = append(lines, " "+commandUsageLine(descriptor)) } lines = append(lines, + "", + "Discover command families:", + " "+renderer.DisplayCommand("help", "families"), "", "The Go runtime is the primary CLI implementation. CLI/JSON is the public cross-language contract.", ) diff --git a/internal/command/adoptioncontract/adoptioncontract.go b/internal/command/adoptioncontract/adoptioncontract.go index 2d1ad1b..fc10e5a 100644 --- a/internal/command/adoptioncontract/adoptioncontract.go +++ b/internal/command/adoptioncontract/adoptioncontract.go @@ -2,27 +2,20 @@ package adoptioncontract import ( "fmt" + "slices" "github.com/research-engineering/agentic-proofkit/internal/command/adoptionworkflow" "github.com/research-engineering/agentic-proofkit/internal/command/gradualadoption" "github.com/research-engineering/agentic-proofkit/internal/command/pilotadmission" "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" "github.com/research-engineering/agentic-proofkit/internal/kernel/contractenv" ) -var modes = map[string]struct{}{ - "adoption": {}, - "bootstrap": {}, - "guidance": {}, - "pilot": {}, - "workflow": {}, -} - -var pilotVariants = map[string]struct{}{ - "all": {}, - "first": {}, - "stack-diverse": {}, -} +var supportedModes = []string{"adoption", "bootstrap", "guidance", "pilot", "workflow"} +var supportedPilotVariants = []string{"all", "first", "stack-diverse"} +var modes = stringSet(supportedModes) +var pilotVariants = stringSet(supportedPilotVariants) type Options struct { AgentEnvelope bool @@ -32,6 +25,14 @@ type Options struct { Guidance gradualadoption.GuidanceOptions } +func SupportedModes() []string { + return slices.Clone(supportedModes) +} + +func SupportedPilotVariants() []string { + return slices.Clone(supportedPilotVariants) +} + type aggregateEnvelope struct { EnvelopeID string Gradual map[string]any @@ -41,6 +42,10 @@ type aggregateEnvelope struct { } func Build(raw any, options Options) (any, int, error) { + return BuildWithRenderer(raw, options, cliexec.PathRenderer()) +} + +func BuildWithRenderer(raw any, options Options, renderer cliexec.Renderer) (any, int, error) { if err := ValidateOptions(options); err != nil { return nil, 1, err } @@ -54,19 +59,19 @@ func Build(raw any, options Options) (any, int, error) { switch options.Mode { case "workflow": if options.AgentEnvelope { - return adoptionworkflow.BuildEnvelopeFromContractEnvelope(workflowEnvelope(envelope)) + return adoptionworkflow.BuildEnvelopeFromContractEnvelopeWithRenderer(workflowEnvelope(envelope), renderer) } - return adoptionworkflow.BuildFromContractEnvelope(workflowEnvelope(envelope)) + return adoptionworkflow.BuildFromContractEnvelopeWithRenderer(workflowEnvelope(envelope), renderer) case "adoption": return gradualadoption.BuildFromContractEnvelope(adoptionEnvelope(envelope)) case "bootstrap": if options.AgentEnvelope { - return gradualadoption.BuildBootstrapEnvelopeFromContractEnvelope(bootstrapEnvelope(envelope)) + return gradualadoption.BuildBootstrapEnvelopeFromContractEnvelopeWithRenderer(bootstrapEnvelope(envelope), renderer) } if options.MaterializationManifest { - return gradualadoption.BuildBootstrapMaterializationManifestFromContractEnvelope(bootstrapEnvelope(envelope)) + return gradualadoption.BuildBootstrapMaterializationManifestFromContractEnvelopeWithRenderer(bootstrapEnvelope(envelope), renderer) } - return gradualadoption.BuildBootstrapFromContractEnvelope(bootstrapEnvelope(envelope)) + return gradualadoption.BuildBootstrapFromContractEnvelopeWithRenderer(bootstrapEnvelope(envelope), renderer) case "guidance": if options.AgentEnvelope { return gradualadoption.BuildGuidanceEnvelopeFromContractEnvelope(guidanceEnvelope(envelope), options.Guidance) @@ -169,6 +174,14 @@ func ValidateOptions(options Options) error { return nil } +func stringSet(values []string) map[string]struct{} { + result := make(map[string]struct{}, len(values)) + for _, value := range values { + result[value] = struct{}{} + } + return result +} + func workflowEnvelope(envelope aggregateEnvelope) map[string]any { return map[string]any{ "schema": "proofkit.adoption-workflow.v1", diff --git a/internal/command/adoptiondoctor/adoptiondoctor.go b/internal/command/adoptiondoctor/adoptiondoctor.go index e4b735d..0c31f0e 100644 --- a/internal/command/adoptiondoctor/adoptiondoctor.go +++ b/internal/command/adoptiondoctor/adoptiondoctor.go @@ -202,10 +202,12 @@ func admitInput(raw any) (Input, error) { func build(input Input) Result { gaps := adoptionGaps(input) + blocked := blockedGaps(gaps) enforced := enforcedGaps(input, gaps) + blocking := append(append([]gap{}, blocked...), enforced...) state := "passed" exitCode := 0 - if hasBlockedGap(enforced) { + if len(blocked) > 0 { state = "blocked" exitCode = 1 } else if len(enforced) > 0 { @@ -219,6 +221,7 @@ func build(input Input) Result { State: state, Summary: map[string]any{ "blockedPreconditionCount": len(input.BlockedPreconditions), + "blockedGapCount": len(blocked), "candidateBoundaryCount": len(input.CandidateBoundaries), "checkedScope": input.CheckedScope, "childReportCount": len(input.ChildReports), @@ -236,7 +239,7 @@ func build(input Input) Result { {Key: "routeCommands", Value: routeCommandDiagnostics(input.OwnerRoutes)}, {Key: "candidateBoundaries", Value: candidatesJSON(input.CandidateBoundaries)}, {Key: "staleAuthority", Value: staleAuthorityJSON(input.StaleAuthority)}, - {Key: "promotionReadiness", Value: promotionReadiness(input, enforced)}, + {Key: "promotionReadiness", Value: promotionReadiness(input, blocking)}, }, RuleResults: ruleResults(input, gaps, enforced), NonClaims: admit.StringSliceToAny(mergedNonClaims(input.NonClaims)), @@ -368,25 +371,29 @@ func missingOwnerRouteGaps(input Input, touched map[string]struct{}) []gap { } func enforcedGaps(input Input, gaps []gap) []gap { - if !adoptionmode.IsEnforcing(input.Mode) { - return []gap{} - } enforced := []gap{} for _, item := range gaps { - if input.Mode == adoptionmode.EnforceAll || item.Touched { + if !isUnconditionalBlockedGap(item) && + adoptionmode.IsEnforcing(input.Mode) && + (input.Mode == adoptionmode.EnforceAll || item.Touched) { enforced = append(enforced, item) } } return enforced } -func hasBlockedGap(gaps []gap) bool { +func blockedGaps(gaps []gap) []gap { + blocked := []gap{} for _, item := range gaps { - if item.Kind == "blocked_precondition" || item.Kind == "child_report_blocked" { - return true + if isUnconditionalBlockedGap(item) { + blocked = append(blocked, item) } } - return false + return blocked +} + +func isUnconditionalBlockedGap(item gap) bool { + return item.Kind == "blocked_precondition" || item.Kind == "child_report_blocked" } func ruleResults(input Input, gaps []gap, enforced []gap) []report.RuleResult { @@ -403,15 +410,25 @@ func ruleResults(input Input, gaps []gap, enforced []gap) []report.RuleResult { } results := make([]report.RuleResult, 0, len(gaps)) for _, item := range gaps { - status := "passed" + if isUnconditionalBlockedGap(item) { + results = append(results, report.RuleResult{ + RuleID: "proofkit.adoption-doctor." + item.Kind, + Status: "blocked", + Message: item.Message, + Diagnostics: []report.Diagnostic{ + {Key: "gapId", Value: item.GapID}, + {Key: "phase", Value: item.Phase}, + {Key: "touched", Value: item.Touched}, + }, + }) + continue + } + status := "skipped" if input.Mode == adoptionmode.Warn { status = "warning" } if _, ok := enforcedSet[item.GapID]; ok { status = "failed" - if item.Kind == "blocked_precondition" || item.Kind == "child_report_blocked" { - status = "blocked" - } } results = append(results, report.RuleResult{ RuleID: "proofkit.adoption-doctor." + item.Kind, diff --git a/internal/command/adoptiondoctor/adoptiondoctor_test.go b/internal/command/adoptiondoctor/adoptiondoctor_test.go index cbfa780..834cd5a 100644 --- a/internal/command/adoptiondoctor/adoptiondoctor_test.go +++ b/internal/command/adoptiondoctor/adoptiondoctor_test.go @@ -10,6 +10,10 @@ import ( func TestBuildReportsObserveAndWarnWithoutBlockingAdvisoryGaps(t *testing.T) { for _, mode := range []string{"observe", "warn"} { t.Run(mode, func(t *testing.T) { + wantRuleStatus := "skipped" + if mode == "warn" { + wantRuleStatus = "warning" + } input := baseInput() input["mode"] = mode input["checkedScope"] = "touched" @@ -24,10 +28,54 @@ func TestBuildReportsObserveAndWarnWithoutBlockingAdvisoryGaps(t *testing.T) { if summary["gapCount"].(int) == 0 { t.Fatalf("summary=%#v, want advisory gap count", summary) } + rules := report["ruleResults"].([]any) + if len(rules) == 0 { + t.Fatal("ruleResults is empty, want advisory rule results") + } + for _, rawRule := range rules { + rule := rawRule.(map[string]any) + if rule["status"] != wantRuleStatus { + t.Fatalf("rule=%#v, want status %q", rule, wantRuleStatus) + } + } }) } } +func TestBuildEnforceTouchedSkipsGapsOutsideTouchedSelection(t *testing.T) { + input := completeInput() + input["mode"] = "enforce-touched" + input["checkedScope"] = "touched" + input["ownerRoutes"] = append(input["ownerRoutes"].([]any), map[string]any{ + "commands": []any{}, + "nativeWitnessRefs": []any{}, + "nonClaims": []any{"Owner route evidence is caller-provided."}, + "owner": "consumer.repository", + "proofBindingPaths": []any{}, + "routeId": "consumer.untouched-route", + "specPaths": []any{"docs/specs/untouched/requirements.v1.json"}, + "touchedRuleIds": []any{"REQ-CONSUMER-OTHER"}, + }) + + report, exitCode, err := Build(decodeInput(t, input)) + if err != nil { + t.Fatalf("Build() error=%v", err) + } + if exitCode != 0 || report["state"] != "passed" { + t.Fatalf("state=%v exit=%d, want passed/0", report["state"], exitCode) + } + rules := report["ruleResults"].([]any) + if len(rules) == 0 { + t.Fatal("ruleResults is empty, want non-enforced advisory rule results") + } + for _, rawRule := range rules { + rule := rawRule.(map[string]any) + if rule["status"] != "skipped" { + t.Fatalf("rule=%#v, want skipped outside touched selection", rule) + } + } +} + func TestBuildFailsEnforcementForCandidateBoundaryAndMissingRoutes(t *testing.T) { commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.007421917124002404912211613315239198743059540465995158650367800592497462368119") input := baseInput() @@ -168,6 +216,49 @@ func TestBuildBlocksEnforcementForExternalPreconditions(t *testing.T) { } } +func TestBuildBlocksEveryModeForExternalPreconditions(t *testing.T) { + for _, mode := range []string{"observe", "warn", "enforce-touched", "enforce-all"} { + t.Run(mode, func(t *testing.T) { + input := completeInput() + input["mode"] = mode + if mode == "observe" || mode == "warn" { + input["checkedScope"] = "none" + } else if mode == "enforce-touched" { + input["checkedScope"] = "touched" + } else { + input["checkedScope"] = "all" + } + input["blockedPreconditions"] = []any{ + map[string]any{ + "evidenceRefs": []any{"docs/external-evidence.md"}, + "nonClaim": "Unavailable external evidence is not passed.", + "owner": "consumer.repository", + "preconditionId": "consumer.external-evidence-unavailable", + "reason": "Required external evidence is unavailable.", + "touchedRuleIds": []any{}, + }, + } + report, exitCode, err := Build(decodeInput(t, input)) + if err != nil { + t.Fatalf("Build() error=%v", err) + } + if exitCode != 1 || report["state"] != "blocked" { + t.Fatalf("state=%v exit=%d, want blocked/1", report["state"], exitCode) + } + if !hasRule(report, "proofkit.adoption-doctor.blocked_precondition", "blocked") { + t.Fatalf("blocked precondition rule = %#v", report["ruleResults"]) + } + summary, ok := report["summary"].(map[string]any) + if !ok { + t.Fatalf("summary=%#v, want object", report["summary"]) + } + if summary["blockedGapCount"] != 1 || summary["enforcedGapCount"] != 0 { + t.Fatalf("summary counts=%#v, want blockedGapCount=1 enforcedGapCount=0", summary) + } + }) + } +} + func TestBuildFailsEnforcementForNonPassingChildReports(t *testing.T) { for _, mode := range []string{"enforce-all", "enforce-touched"} { for _, childState := range []string{"skipped", "warning"} { diff --git a/internal/command/adoptionworkflow/adoptionworkflow.go b/internal/command/adoptionworkflow/adoptionworkflow.go index 809af3a..a3e44e6 100644 --- a/internal/command/adoptionworkflow/adoptionworkflow.go +++ b/internal/command/adoptionworkflow/adoptionworkflow.go @@ -9,6 +9,7 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/command/stackpreset" "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" "github.com/research-engineering/agentic-proofkit/internal/kernel/agentenvelope" + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" "github.com/research-engineering/agentic-proofkit/internal/kernel/contractenv" "github.com/research-engineering/agentic-proofkit/internal/kernel/report" ) @@ -74,7 +75,11 @@ type Result struct { } func Build(raw any) (map[string]any, int, error) { - result, err := BuildResult(raw) + return BuildWithRenderer(raw, cliexec.PathRenderer()) +} + +func BuildWithRenderer(raw any, renderer cliexec.Renderer) (map[string]any, int, error) { + result, err := BuildResultWithRenderer(raw, renderer) if err != nil { return nil, 1, err } @@ -82,7 +87,11 @@ func Build(raw any) (map[string]any, int, error) { } func BuildResult(raw any) (Result, error) { - result, err := build(raw) + return BuildResultWithRenderer(raw, cliexec.PathRenderer()) +} + +func BuildResultWithRenderer(raw any, renderer cliexec.Renderer) (Result, error) { + result, err := build(raw, renderer) if err != nil { return Result{}, err } @@ -90,7 +99,11 @@ func BuildResult(raw any) (Result, error) { } func BuildEnvelope(raw any) (map[string]any, int, error) { - result, err := build(raw) + return BuildEnvelopeWithRenderer(raw, cliexec.PathRenderer()) +} + +func BuildEnvelopeWithRenderer(raw any, renderer cliexec.Renderer) (map[string]any, int, error) { + result, err := build(raw, renderer) if err != nil { return agentenvelope.InvalidInput(err.Error()), 1, nil } @@ -98,19 +111,27 @@ func BuildEnvelope(raw any) (map[string]any, int, error) { } func BuildFromContractEnvelope(raw any) (map[string]any, int, error) { + return BuildFromContractEnvelopeWithRenderer(raw, cliexec.PathRenderer()) +} + +func BuildFromContractEnvelopeWithRenderer(raw any, renderer cliexec.Renderer) (map[string]any, int, error) { workflowInput, err := inputFromContractEnvelope(raw) if err != nil { return nil, 1, err } - return Build(workflowInput) + return BuildWithRenderer(workflowInput, renderer) } func BuildEnvelopeFromContractEnvelope(raw any) (map[string]any, int, error) { + return BuildEnvelopeFromContractEnvelopeWithRenderer(raw, cliexec.PathRenderer()) +} + +func BuildEnvelopeFromContractEnvelopeWithRenderer(raw any, renderer cliexec.Renderer) (map[string]any, int, error) { workflowInput, err := inputFromContractEnvelope(raw) if err != nil { return agentenvelope.InvalidInput(err.Error()), 1, nil } - return BuildEnvelope(workflowInput) + return BuildEnvelopeWithRenderer(workflowInput, renderer) } func inputFromContractEnvelope(raw any) (map[string]any, error) { @@ -121,7 +142,7 @@ func inputFromContractEnvelope(raw any) (map[string]any, error) { return contractenv.ObjectField(envelope, "workflow", "adoption workflow contract envelope") } -func build(raw any) (result, error) { +func build(raw any, renderer cliexec.Renderer) (result, error) { record, ok := raw.(map[string]any) if !ok { return result{}, fmt.Errorf("adoption workflow plan input must be an object") @@ -157,7 +178,7 @@ func build(raw any) (result, error) { return result{}, err } blockers := workflowBlockers(scenario, presetID, inputRefs) - phases := workflowPhases(scenario, presetID, inputRefs) + phases := workflowPhases(scenario, presetID, inputRefs, renderer) planState := "blocked" exitCode := 1 if len(blockers) == 0 { @@ -255,28 +276,28 @@ func presetID(raw any) (*string, error) { return &value, nil } -func workflowPhases(scenario string, presetID *string, refs []inputRef) []any { +func workflowPhases(scenario string, presetID *string, refs []inputRef, renderer cliexec.Renderer) []any { byKind := refsByKind(refs) phases := make([]any, 0, len(phaseOrder)) for _, phase := range phaseOrder { phases = append(phases, map[string]any{ - "commands": workflowCommandsForPhase(scenario, presetID, byKind, phase), + "commands": workflowCommandsForPhase(scenario, presetID, byKind, phase, renderer), "phase": phase, }) } return phases } -func workflowCommandsForPhase(scenario string, presetID *string, refs map[string]inputRef, phase string) []any { +func workflowCommandsForPhase(scenario string, presetID *string, refs map[string]inputRef, phase string, renderer cliexec.Renderer) []any { if phase == "profile" { commands := []map[string]any{} if presetID != nil { - commands = append(commands, command("stack-preset", []string{"stack-preset", "--preset", *presetID}, []string{})) + commands = append(commands, command("stack-preset", []string{"stack-preset", "--preset", *presetID}, []string{}, renderer)) } if scenario == "new_repository" || scenario == "existing_gradual_adoption" { - commands = appendInputCommand(commands, refs, "repo_profile_scaffold", "scaffold-profile-plan") + commands = appendInputCommand(commands, refs, "repo_profile_scaffold", "scaffold-profile-plan", renderer) } - commands = appendInputCommand(commands, refs, "branch_authority", "branch-authority") + commands = appendInputCommand(commands, refs, "branch_authority", "branch-authority", renderer) return mapsToAny(commands) } if phase == "bootstrap" { @@ -284,8 +305,8 @@ func workflowCommandsForPhase(scenario string, presetID *string, refs map[string return []any{} } commands := []map[string]any{} - commands = appendInputCommand(commands, refs, "gradual_adoption_bootstrap", "gradual-adoption-bootstrap") - commands = appendInputCommand(commands, refs, "gradual_adoption_guidance", "gradual-adoption-guidance") + commands = appendInputCommand(commands, refs, "gradual_adoption_bootstrap", "gradual-adoption-bootstrap", renderer) + commands = appendInputCommand(commands, refs, "gradual_adoption_guidance", "gradual-adoption-guidance", renderer) return mapsToAny(commands) } if phase == "bind" { @@ -293,39 +314,39 @@ func workflowCommandsForPhase(scenario string, presetID *string, refs map[string return []any{} } commands := []map[string]any{} - commands = appendInputCommand(commands, refs, "requirement_bindings", "requirement-bindings") - commands = appendInputCommand(commands, refs, "requirement_bindings", "proof-slice") - commands = appendInputCommand(commands, refs, "witness_plan", "witness-scheduler-plan") + commands = appendInputCommand(commands, refs, "requirement_bindings", "requirement-bindings", renderer) + commands = appendInputCommand(commands, refs, "requirement_bindings", "proof-slice", renderer) + commands = appendInputCommand(commands, refs, "witness_plan", "witness-scheduler-plan", renderer) return mapsToAny(commands) } if phase == "migrate" { if scenario != "legacy_proof_migration" { return []any{} } - return mapsToAny(appendInputCommand([]map[string]any{}, refs, "migration_plan", "migration-plan")) + return mapsToAny(appendInputCommand([]map[string]any{}, refs, "migration_plan", "migration-plan", renderer)) } if phase == "plan-gates" { if scenario == "new_repository" { return []any{} } - return mapsToAny(appendInputCommand([]map[string]any{}, refs, "selective_gate_plan", "selective-gate-plan")) + return mapsToAny(appendInputCommand([]map[string]any{}, refs, "selective_gate_plan", "selective-gate-plan", renderer)) } if phase == "collect-evidence" { commands := []map[string]any{} if scenario != "new_repository" { - commands = appendInputCommand(commands, refs, "receipt_producer_admission", "receipt-producer-admission") - commands = appendInputCommand(commands, refs, "selective_gate_evidence", "selective-gate-evidence") + commands = appendInputCommand(commands, refs, "receipt_producer_admission", "receipt-producer-admission", renderer) + commands = appendInputCommand(commands, refs, "selective_gate_evidence", "selective-gate-evidence", renderer) } - commands = appendInputCommand(commands, refs, "adoption_checklist", "adoption-checklist") + commands = appendInputCommand(commands, refs, "adoption_checklist", "adoption-checklist", renderer) return mapsToAny(commands) } if scenario != "release_channel" { return []any{} } commands := []map[string]any{} - commands = appendInputCommand(commands, refs, "release_authority", "release-authority") - commands = appendInputCommand(commands, refs, "external_consumer", "external-consumer") - commands = appendInputCommand(commands, refs, "registry_consumer", "registry-consumer") + commands = appendInputCommand(commands, refs, "release_authority", "release-authority", renderer) + commands = appendInputCommand(commands, refs, "external_consumer", "external-consumer", renderer) + commands = appendInputCommand(commands, refs, "registry_consumer", "registry-consumer", renderer) return mapsToAny(commands) } @@ -366,9 +387,9 @@ func requiredKinds(scenario string) []string { } } -func command(commandID string, argv []string, inputRefIDs []string) map[string]any { +func command(commandID string, argv []string, inputRefIDs []string, renderer cliexec.Renderer) map[string]any { return map[string]any{ - "argv": admit.StringSliceToAny(append([]string{"agentic-proofkit"}, argv...)), + "argv": admit.StringSliceToAny(renderer.Argv(argv...)), "commandId": "proofkit.adoption-workflow.command." + commandID, "inputRefIds": admit.StringSliceToAny(inputRefIDs), "nonClaim": "Workflow command refs are caller-owned routes only and do not execute commands or prove pass evidence.", @@ -376,12 +397,12 @@ func command(commandID string, argv []string, inputRefIDs []string) map[string]a } } -func appendInputCommand(commands []map[string]any, refs map[string]inputRef, inputKind string, cliCommand string) []map[string]any { +func appendInputCommand(commands []map[string]any, refs map[string]inputRef, inputKind string, cliCommand string, renderer cliexec.Renderer) []map[string]any { ref, ok := refs[inputKind] if !ok { return commands } - return append(commands, command(cliCommand, []string{cliCommand, "--input", ref.Path}, []string{ref.RefID})) + return append(commands, command(cliCommand, []string{cliCommand, "--input", ref.Path}, []string{ref.RefID}, renderer)) } func newBlocker(scenario string, reason string, requiredKinds []string) blocker { @@ -446,7 +467,7 @@ func envelope(result result) map[string]any { argv := anyToStrings(commandRef["argv"].([]any)) commands = append(commands, map[string]any{ "argv": admit.StringSliceToAny(argv), - "command": strings.Join(argv, " "), + "command": cliexec.DisplayArgv(argv), "commandId": commandID, "nonClaim": "Agent command refs preserve argv boundaries but do not execute commands or prove pass evidence.", "owner": "consumer_repository", diff --git a/internal/command/agentroute/agentroute.go b/internal/command/agentroute/agentroute.go index ef95c2d..97440bf 100644 --- a/internal/command/agentroute/agentroute.go +++ b/internal/command/agentroute/agentroute.go @@ -7,6 +7,7 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" "github.com/research-engineering/agentic-proofkit/internal/kernel/adoptionmode" + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" ) var ( @@ -399,6 +400,10 @@ var nonClaims = []any{ } func Build(raw any) (map[string]any, int, error) { + return BuildWithRenderer(raw, cliexec.PathRenderer()) +} + +func BuildWithRenderer(raw any, renderer cliexec.Renderer) (map[string]any, int, error) { input, err := admitInput(raw) if err != nil { return nil, 1, err @@ -422,7 +427,7 @@ func Build(raw any) (map[string]any, int, error) { state = "blocked_ambiguous_state" exitCode = 1 } - return buildReport(input, spec, state, missing), exitCode, nil + return buildReport(input, spec, state, missing, renderer), exitCode, nil } func InputContract() map[string]any { @@ -853,8 +858,8 @@ func buildUnknownGoal(input routeInput) map[string]any { } } -func buildReport(input routeInput, spec routeSpec, state string, missing []map[string]any) map[string]any { - nextCommands := commandReports(spec.NextCommands, input) +func buildReport(input routeInput, spec routeSpec, state string, missing []map[string]any, renderer cliexec.Renderer) map[string]any { + nextCommands := commandReports(spec.NextCommands, input, renderer) if state != "routed" { nextCommands = []any{} } @@ -887,16 +892,14 @@ func buildReport(input routeInput, spec routeSpec, state string, missing []map[s } } -func commandReports(commands []commandSpec, input routeInput) []any { +func commandReports(commands []commandSpec, input routeInput, renderer cliexec.Renderer) []any { result := []any{} for _, command := range commands { if !commandInputsAvailable(command, input.AvailableInputs) { continue } - argv := []any{"agentic-proofkit", command.Command} - for _, arg := range command.ExtraArgs { - argv = append(argv, arg) - } + argv := renderer.Argv(command.Command) + argv = append(argv, command.ExtraArgs...) for _, arg := range command.ArgInputs { ref, ok := input.AvailableInputs[arg.InputKind] if !ok { @@ -918,7 +921,7 @@ func commandReports(commands []commandSpec, input routeInput) []any { argv = append(argv, "--input", ref) } report := map[string]any{ - "argv": argv, + "argv": admit.StringSliceToAny(argv), "command": command.Command, "why": command.Why, } diff --git a/internal/command/agentroute/envelope.go b/internal/command/agentroute/envelope.go index bf01a66..a2a0ad1 100644 --- a/internal/command/agentroute/envelope.go +++ b/internal/command/agentroute/envelope.go @@ -6,11 +6,16 @@ import ( "strings" "github.com/research-engineering/agentic-proofkit/internal/kernel/agentenvelope" + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" ) func BuildEnvelope(raw any) (map[string]any, int, error) { - report, exitCode, err := Build(raw) + return BuildEnvelopeWithRenderer(raw, cliexec.PathRenderer()) +} + +func BuildEnvelopeWithRenderer(raw any, renderer cliexec.Renderer) (map[string]any, int, error) { + report, exitCode, err := BuildWithRenderer(raw, renderer) if err != nil { return nil, 1, err } @@ -122,10 +127,11 @@ func commandContextRefs(commands []map[string]any, reportID string) []map[string if commandName == "" { continue } + argv := stringsFromAny(command["argv"]) refs = append(refs, map[string]any{ "argv": command["argv"], "commandId": commandID(reportID, command), - "display": commandName, + "display": cliexec.DisplayArgv(argv), "nonClaim": "Agent-route command refs do not execute commands or admit command results.", "owner": "consumer_repository", "proofkitRoute": commandName, diff --git a/internal/command/branchauthority/branchauthority.go b/internal/command/branchauthority/branchauthority.go index b482e24..a0fcae5 100644 --- a/internal/command/branchauthority/branchauthority.go +++ b/internal/command/branchauthority/branchauthority.go @@ -51,10 +51,10 @@ type admittedInput struct { ReportID string } -func Build(raw any) (report.Record, int) { +func Build(raw any) (report.Record, int, error) { input, err := admitInput(raw) if err != nil { - return invalidInputReport(err.Error()), 1 + return report.Record{}, 1, err } branchRefs := make([]any, 0, len(input.BranchRefs)) requiredDrift := []string{} @@ -112,28 +112,9 @@ func Build(raw any) (report.Record, int) { NonClaims: input.NonClaims, } if state == "passed" { - return record, 0 - } - return record, 1 -} - -func invalidInputReport(failure string) report.Record { - return report.Record{ - SchemaVersion: 1, - ReportKind: reportKind, - ReportID: "invalid-input", - State: "failed", - Summary: map[string]any{ - "advisoryDriftCount": 0, - "branchRefCount": 0, - "requiredDriftCount": 0, - }, - Diagnostics: []report.Diagnostic{}, - RuleResults: []report.RuleResult{ - rule("branch_authority.input", "failed", failure), - }, - NonClaims: branchAuthorityNonClaims, + return record, 0, nil } + return record, 1, nil } func admitInput(raw any) (admittedInput, error) { diff --git a/internal/command/branchauthority/branchauthority_test.go b/internal/command/branchauthority/branchauthority_test.go index 1684e97..5978804 100644 --- a/internal/command/branchauthority/branchauthority_test.go +++ b/internal/command/branchauthority/branchauthority_test.go @@ -9,12 +9,18 @@ import ( func TestBuildAdmitsAlignedRequiredBranchAndRejectsRequiredDrift(t *testing.T) { commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.059237477459693884008799222027390418858342772482064960101818015827014505791733") - record, exitCode := Build(validBranchAuthorityInput("main")) + record, exitCode, err := Build(validBranchAuthorityInput("main")) + if err != nil { + t.Fatalf("Build() error=%v", err) + } if exitCode != 0 || record.State != "passed" { t.Fatalf("Build() exitCode=%d state=%s, want passed", exitCode, record.State) } - record, exitCode = Build(validBranchAuthorityInput("feature/test")) + record, exitCode, err = Build(validBranchAuthorityInput("feature/test")) + if err != nil { + t.Fatalf("Build() error=%v", err) + } encoded, _ := json.Marshal(record) if exitCode == 0 || record.State != "failed" || !strings.Contains(string(encoded), "proofkit.test.default") || !strings.Contains(string(encoded), "drifted") { t.Fatalf("Build() accepted required branch drift: exitCode=%d record=%s", exitCode, string(encoded)) diff --git a/internal/command/changedpathset/changedpathset.go b/internal/command/changedpathset/changedpathset.go index f581909..3b8b8ab 100644 --- a/internal/command/changedpathset/changedpathset.go +++ b/internal/command/changedpathset/changedpathset.go @@ -71,33 +71,7 @@ var changedPathSetNonClaims = []string{ func Build(raw any) (Result, error) { input, err := admitInput(raw) if err != nil { - failure := errorText(err) - emptyHash := changedPathsHash([]string{}) - record := report.Record{ - SchemaVersion: 1, - ReportKind: "proofkit.changed-path-set", - ReportID: "invalid-input", - State: "failed", - Summary: map[string]any{ - "changedPathCount": 0, - "duplicatePathCount": 0, - "invalidPathCount": 1, - "sourceCount": 0, - }, - Diagnostics: []report.Diagnostic{{Key: "changedPathSetHash", Value: emptyHash}}, - RuleResults: []report.RuleResult{rule("changed_path_set.input", "failed", failure)}, - NonClaims: []any{"invalid input does not prove changed path completeness"}, - } - return Result{ - ChangedPaths: []string{}, - ChangedPathSetHash: emptyHash, - DuplicatePaths: []Diagnostic{}, - ExitCode: 1, - Failures: []string{failure}, - InvalidPaths: []Diagnostic{{SourceID: "input", Path: "", Reason: failure}}, - Report: record, - SourceSummaries: []SourceSummary{}, - }, nil + return Result{}, fmt.Errorf("%s", errorText(err)) } return build(input), nil } diff --git a/internal/command/changedpathset/changedpathset_test.go b/internal/command/changedpathset/changedpathset_test.go index fd20a0a..a6a0f85 100644 --- a/internal/command/changedpathset/changedpathset_test.go +++ b/internal/command/changedpathset/changedpathset_test.go @@ -116,40 +116,32 @@ func TestBuildDeduplicatesAndFailsClosedOnInvalidPaths(t *testing.T) { func TestBuildRejectsSecretLikeReportVisibleText(t *testing.T) { secret := "sk-proj-abcdefghijklmnop" - result, err := Build(map[string]any{ + _, err := Build(map[string]any{ "schemaVersion": json.Number("1"), "reportId": "proofkit.test.changed-path-set", "preexistingFailures": []any{}, "nonClaims": []any{secret}, "sources": []any{map[string]any{"sourceId": "git", "paths": []any{"a.ts"}}}, }) - if err != nil { - t.Fatalf("Build() error = %v", err) - } - encoded, _ := json.Marshal(result.Report) - if result.ExitCode == 0 || result.Report.State != "failed" { - t.Fatalf("Build() exitCode=%d state=%s, want failed", result.ExitCode, result.Report.State) + if err == nil || !strings.Contains(err.Error(), "secret-like values") { + t.Fatalf("Build() error = %v, want structural rejection", err) } - if strings.Contains(string(encoded), "abcdefghijklmnop") { - t.Fatalf("Build() leaked secret text in report: %s", string(encoded)) + if strings.Contains(err.Error(), "abcdefghijklmnop") { + t.Fatalf("Build() leaked secret text in error: %s", err) } - result, err = Build(map[string]any{ + _, err = Build(map[string]any{ "schemaVersion": json.Number("1"), "reportId": "proofkit.test.changed-path-set", "preexistingFailures": []any{"https://user:password@example.invalid"}, "nonClaims": []any{"Changed-path test input does not prove git diff freshness."}, "sources": []any{map[string]any{"sourceId": "git", "paths": []any{"a.ts"}}}, }) - if err != nil { - t.Fatalf("Build() second error = %v", err) - } - encoded, _ = json.Marshal(result.Report) - if result.ExitCode == 0 || result.Report.State != "failed" { - t.Fatalf("Build() second exitCode=%d state=%s, want failed", result.ExitCode, result.Report.State) + if err == nil || !strings.Contains(err.Error(), "secret-like values") { + t.Fatalf("Build() second error = %v, want structural rejection", err) } - if strings.Contains(string(encoded), "password") || strings.Contains(string(encoded), "example.invalid") { - t.Fatalf("Build() leaked URL credential text in report: %s", string(encoded)) + if strings.Contains(err.Error(), "password") || strings.Contains(err.Error(), "example.invalid") { + t.Fatalf("Build() leaked URL credential text in error: %s", err) } } diff --git a/internal/command/deploymentevidenceadmission/deployment_evidence_admission.go b/internal/command/deploymentevidenceadmission/deployment_evidence_admission.go index 862a4fb..2258e5b 100644 --- a/internal/command/deploymentevidenceadmission/deployment_evidence_admission.go +++ b/internal/command/deploymentevidenceadmission/deployment_evidence_admission.go @@ -61,7 +61,7 @@ type input struct { func Build(raw any) (report.Record, int, error) { input, err := admitInput(raw) if err != nil { - return invalidInputReport(err.Error()), 1, nil + return report.Record{}, 1, err } blockedReasons := []string{} @@ -74,7 +74,7 @@ func Build(raw any) (report.Record, int, error) { blockedReasons = append(blockedReasons, "deployment evidence is required") } else { if findings, err := scanSecretShapedJSON(input.Evidence, "evidence"); err != nil { - return invalidInputReport(err.Error()), 1, nil + return report.Record{}, 1, err } else { failures = append(failures, findings...) } @@ -90,7 +90,7 @@ func Build(raw any) (report.Record, int, error) { } if findings, err := scanRawOperatorEvidence(input.RawOperatorEvidence); err != nil { - return invalidInputReport(err.Error()), 1, nil + return report.Record{}, 1, err } else { failures = append(failures, findings...) } @@ -493,34 +493,6 @@ func deploymentEvidenceRuleResults(state string, blockedReasons []string, failur } } -func invalidInputReport(message string) report.Record { - return report.Record{ - SchemaVersion: 1, - ReportKind: reportKind, - ReportID: "invalid_input", - State: "failed", - Summary: map[string]any{ - "blockedReasonCount": 0, - "endpointCount": 0, - "evidenceId": nil, - "factCount": 0, - "failureCount": 1, - "requiredFactIdCount": 0, - "requiredFactKindCount": 0, - }, - Diagnostics: []report.Diagnostic{{Key: "failures", Value: []any{message}}}, - RuleResults: []report.RuleResult{ - { - Diagnostics: []report.Diagnostic{{Key: "failure", Value: message}}, - Message: message, - RuleID: "proofkit.deployment-evidence-admission.input", - Status: "failed", - }, - }, - NonClaims: []any{"invalid deployment evidence admission input does not prove deployment evidence state"}, - } -} - func objectValue(value any, context string) (map[string]any, error) { record, ok := value.(map[string]any) if !ok { diff --git a/internal/command/externalconsumer/externalconsumer.go b/internal/command/externalconsumer/externalconsumer.go index 75733ad..eaa6475 100644 --- a/internal/command/externalconsumer/externalconsumer.go +++ b/internal/command/externalconsumer/externalconsumer.go @@ -142,7 +142,7 @@ type reportInput struct { func Build(raw any) (report.Record, int, error) { admitted, err := admitReportInput(raw) if err != nil { - return failedAdmissionReport(err), 1, nil + return report.Record{}, 1, err } failures := []string{} failures = append(failures, inputFailures(admitted.Input)...) @@ -865,26 +865,6 @@ func stableSHA256(value any) string { return hex.EncodeToString(sum[:]) } -func failedAdmissionReport(err error) report.Record { - return report.Record{ - SchemaVersion: 1, - ReportKind: reportKind, - ReportID: "proofkit.external-consumer.invalid-input", - State: "failed", - Summary: map[string]any{"admission": "failed"}, - Diagnostics: []report.Diagnostic{}, - RuleResults: []report.RuleResult{ - { - RuleID: "proofkit.external-consumer.failure.001", - Status: "failed", - Message: err.Error(), - Diagnostics: []report.Diagnostic{}, - }, - }, - NonClaims: []any{"Invalid external-consumer input is not proofkit consumption evidence."}, - } -} - func ruleResults(failures []string) []report.RuleResult { if len(failures) == 0 { return []report.RuleResult{ diff --git a/internal/command/externalconsumer/externalconsumer_test.go b/internal/command/externalconsumer/externalconsumer_test.go index f4eef36..c8cb3a6 100644 --- a/internal/command/externalconsumer/externalconsumer_test.go +++ b/internal/command/externalconsumer/externalconsumer_test.go @@ -58,16 +58,12 @@ func TestBuildRejectsSecretLikeCallerOwnedText(t *testing.T) { input := validExternalConsumerInput(t) input["input"].(map[string]any)["sourceRepository"] = secretLike - record, exitCode, err := Build(input) - if err != nil { - t.Fatalf("Build() error=%v", err) - } - encoded, _ := json.Marshal(record) - if exitCode == 0 || record.State != "failed" || !strings.Contains(string(encoded), "secret-like values") { - t.Fatalf("Build() accepted secret-like caller text: exit=%d record=%s", exitCode, string(encoded)) + _, exitCode, err := Build(input) + if exitCode != 1 || err == nil || !strings.Contains(err.Error(), "secret-like values") { + t.Fatalf("Build() accepted secret-like caller text: exit=%d error=%v", exitCode, err) } - if strings.Contains(string(encoded), secretLike) { - t.Fatalf("Build() leaked secret-like caller text: %s", string(encoded)) + if strings.Contains(err.Error(), secretLike) { + t.Fatalf("Build() leaked secret-like caller text: %s", err) } } diff --git a/internal/command/gradualadoption/bootstrap.go b/internal/command/gradualadoption/bootstrap.go index 107d46a..b4863cb 100644 --- a/internal/command/gradualadoption/bootstrap.go +++ b/internal/command/gradualadoption/bootstrap.go @@ -31,7 +31,11 @@ type BootstrapResult struct { } func BuildBootstrap(raw any) (map[string]any, int, error) { - result, err := BuildBootstrapResult(raw) + return BuildBootstrapWithRenderer(raw, cliexec.PathRenderer()) +} + +func BuildBootstrapWithRenderer(raw any, renderer cliexec.Renderer) (map[string]any, int, error) { + result, err := BuildBootstrapResultWithRenderer(raw, renderer) if err != nil { return nil, 1, err } @@ -39,15 +43,23 @@ func BuildBootstrap(raw any) (map[string]any, int, error) { } func BuildBootstrapResult(raw any) (BootstrapResult, error) { - return buildBootstrap(raw) + return BuildBootstrapResultWithRenderer(raw, cliexec.PathRenderer()) +} + +func BuildBootstrapResultWithRenderer(raw any, renderer cliexec.Renderer) (BootstrapResult, error) { + return buildBootstrap(raw, renderer) } func BuildBootstrapFromContractEnvelope(raw any) (map[string]any, int, error) { + return BuildBootstrapFromContractEnvelopeWithRenderer(raw, cliexec.PathRenderer()) +} + +func BuildBootstrapFromContractEnvelopeWithRenderer(raw any, renderer cliexec.Renderer) (map[string]any, int, error) { input, err := BootstrapInputFromContractEnvelope(raw) if err != nil { return nil, 1, err } - return BuildBootstrap(input) + return BuildBootstrapWithRenderer(input, renderer) } func BootstrapInputFromContractEnvelope(raw any) (map[string]any, error) { @@ -147,7 +159,7 @@ func (result BootstrapResult) JSONValue() map[string]any { } } -func buildBootstrap(raw any) (BootstrapResult, error) { +func buildBootstrap(raw any, renderer cliexec.Renderer) (BootstrapResult, error) { record, ok := raw.(map[string]any) if !ok { return BootstrapResult{}, fmt.Errorf("proofkit gradual adoption bootstrap input must be an object") @@ -196,9 +208,9 @@ func buildBootstrap(raw any) (BootstrapResult, error) { nonClaims, err := admit.SortedText(append(append([]string{}, bootstrapNonClaims...), callerNonClaims...), "bootstrap merged nonClaims", false) addErr(&failures, err) nextCommands := []string{ - cliexec.DisplayCommand("gradual-adoption", "--input", stringFromMap(paths, "adoptionProfilePath")), - cliexec.DisplayCommand("gradual-adoption-guidance", "--input", stringFromMap(paths, "adoptionGuidancePath")), - cliexec.DisplayCommand("witness-scheduler-plan", "--input", stringFromMap(paths, "witnessPlanInputPath")), + renderer.DisplayCommand("gradual-adoption", "--input", stringFromMap(paths, "adoptionProfilePath")), + renderer.DisplayCommand("gradual-adoption-guidance", "--input", stringFromMap(paths, "adoptionGuidancePath")), + renderer.DisplayCommand("witness-scheduler-plan", "--input", stringFromMap(paths, "witnessPlanInputPath")), } adoptionProfile := map[string]any{ "adoptionId": bootstrapID, diff --git a/internal/command/gradualadoption/bootstrap_envelope.go b/internal/command/gradualadoption/bootstrap_envelope.go index dad0378..0209b8b 100644 --- a/internal/command/gradualadoption/bootstrap_envelope.go +++ b/internal/command/gradualadoption/bootstrap_envelope.go @@ -6,10 +6,15 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" "github.com/research-engineering/agentic-proofkit/internal/kernel/agentenvelope" + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" ) func BuildBootstrapEnvelope(raw any) (map[string]any, int, error) { - result, err := buildBootstrap(raw) + return BuildBootstrapEnvelopeWithRenderer(raw, cliexec.PathRenderer()) +} + +func BuildBootstrapEnvelopeWithRenderer(raw any, renderer cliexec.Renderer) (map[string]any, int, error) { + result, err := buildBootstrap(raw, renderer) if err != nil { return agentenvelope.InvalidInput(err.Error()), 1, nil } @@ -17,11 +22,15 @@ func BuildBootstrapEnvelope(raw any) (map[string]any, int, error) { } func BuildBootstrapEnvelopeFromContractEnvelope(raw any) (map[string]any, int, error) { + return BuildBootstrapEnvelopeFromContractEnvelopeWithRenderer(raw, cliexec.PathRenderer()) +} + +func BuildBootstrapEnvelopeFromContractEnvelopeWithRenderer(raw any, renderer cliexec.Renderer) (map[string]any, int, error) { input, err := BootstrapInputFromContractEnvelope(raw) if err != nil { return agentenvelope.InvalidInput(err.Error()), 1, nil } - return BuildBootstrapEnvelope(input) + return BuildBootstrapEnvelopeWithRenderer(input, renderer) } func BootstrapEnvelope(result BootstrapResult) map[string]any { diff --git a/internal/command/gradualadoption/bootstrap_materialization.go b/internal/command/gradualadoption/bootstrap_materialization.go index 2e798aa..85c7d8b 100644 --- a/internal/command/gradualadoption/bootstrap_materialization.go +++ b/internal/command/gradualadoption/bootstrap_materialization.go @@ -3,12 +3,17 @@ package gradualadoption import ( "fmt" + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" ) func BuildBootstrapMaterializationManifest(raw any) (map[string]any, int, error) { - result, err := buildBootstrap(raw) + return BuildBootstrapMaterializationManifestWithRenderer(raw, cliexec.PathRenderer()) +} + +func BuildBootstrapMaterializationManifestWithRenderer(raw any, renderer cliexec.Renderer) (map[string]any, int, error) { + result, err := buildBootstrap(raw, renderer) if err != nil { return nil, 1, err } @@ -20,11 +25,15 @@ func BuildBootstrapMaterializationManifest(raw any) (map[string]any, int, error) } func BuildBootstrapMaterializationManifestFromContractEnvelope(raw any) (map[string]any, int, error) { + return BuildBootstrapMaterializationManifestFromContractEnvelopeWithRenderer(raw, cliexec.PathRenderer()) +} + +func BuildBootstrapMaterializationManifestFromContractEnvelopeWithRenderer(raw any, renderer cliexec.Renderer) (map[string]any, int, error) { input, err := BootstrapInputFromContractEnvelope(raw) if err != nil { return nil, 1, err } - return BuildBootstrapMaterializationManifest(input) + return BuildBootstrapMaterializationManifestWithRenderer(input, renderer) } func BootstrapMaterializationManifest(result BootstrapResult) (map[string]any, error) { diff --git a/internal/command/gradualadoption/gradualadoption_test.go b/internal/command/gradualadoption/gradualadoption_test.go index 26a1d2e..789a68f 100644 --- a/internal/command/gradualadoption/gradualadoption_test.go +++ b/internal/command/gradualadoption/gradualadoption_test.go @@ -2,6 +2,7 @@ package gradualadoption import ( "encoding/json" + "reflect" "strings" "testing" @@ -85,31 +86,86 @@ func TestBootstrapRejectsShellControlGuidanceCommand(t *testing.T) { } func TestBootstrapPreservesCallerDisplayCommandInGuidancePayload(t *testing.T) { - result, err := BuildBootstrapResult(validBootstrapInput()) - if err != nil { - t.Fatalf("BuildBootstrapResult() error=%v", err) - } - if result.ExitCode != 0 { - t.Fatalf("BuildBootstrapResult() exit=%d report=%#v, want passed", result.ExitCode, result.Record.JSONValue()) - } - guidance := result.Payloads["adoptionGuidance"].(map[string]any) - agentGuidance := guidance["agentGuidance"].(map[string]any) - commands := anyStrings(agentGuidance["commands"]) - if !containsString(commands, "go test ./internal/command/gradualadoption") { - t.Fatalf("caller-provided bootstrap command was not preserved: %#v", commands) - } - if !containsString(commands, "agentic-proofkit gradual-adoption --input proofkit/profile.json") { - t.Fatalf("generated bootstrap command missing: %#v", commands) - } - if !containsString(commands, "agentic-proofkit witness-scheduler-plan --input proofkit/witness-plan.json") { - t.Fatalf("generated witness scheduler command missing: %#v", commands) + input := validBootstrapInput() + callerCommands := anyStrings(input["commands"]) + profiles := []struct { + name string + profile string + pythonExecutable string + }{ + {name: "path", profile: cliexec.ProfilePath}, + {name: "npm offline", profile: cliexec.ProfileNPMOffline}, + {name: "python module", profile: cliexec.ProfilePythonModule, pythonExecutable: "/tmp/proofkit venv/bin/python"}, } - if containsString(commands, "agentic-proofkit witness-plan --input proofkit/witness-plan.json") { - t.Fatalf("generated bootstrap command uses catalog command for scheduler-plan fixture: %#v", commands) + for _, profile := range profiles { + t.Run(profile.name, func(t *testing.T) { + renderer, err := cliexec.AdmitLauncherProfile(profile.profile, profile.pythonExecutable) + if err != nil { + t.Fatalf("AdmitLauncherProfile() error=%v", err) + } + result, err := BuildBootstrapResultWithRenderer(input, renderer) + if err != nil { + t.Fatalf("BuildBootstrapResultWithRenderer() error=%v", err) + } + if result.ExitCode != 0 { + t.Fatalf("BuildBootstrapResultWithRenderer() exit=%d report=%#v, want passed", result.ExitCode, result.Record.JSONValue()) + } + guidance := result.Payloads["adoptionGuidance"].(map[string]any) + agentGuidance := guidance["agentGuidance"].(map[string]any) + commands := anyStrings(agentGuidance["commands"]) + if len(commands) != len(callerCommands)+len(result.NextCommands) { + t.Fatalf("guidance commands=%#v, want exact caller prefix and generated suffix", commands) + } + if !reflect.DeepEqual(commands[:len(callerCommands)], callerCommands) { + t.Fatalf("caller-provided bootstrap prefix=%#v, want unchanged %#v", commands[:len(callerCommands)], callerCommands) + } + if !reflect.DeepEqual(commands[len(callerCommands):], result.NextCommands) { + t.Fatalf("generated guidance suffix=%#v, want %#v", commands[len(callerCommands):], result.NextCommands) + } + if !containsString(commands, renderer.DisplayCommand("gradual-adoption", "--input", "proofkit/profile.json")) { + t.Fatalf("generated bootstrap command missing: %#v", commands) + } + if !containsString(commands, renderer.DisplayCommand("witness-scheduler-plan", "--input", "proofkit/witness-plan.json")) { + t.Fatalf("generated witness scheduler command missing: %#v", commands) + } + if containsString(commands, renderer.DisplayCommand("witness-plan", "--input", "proofkit/witness-plan.json")) { + t.Fatalf("generated bootstrap command uses catalog command for scheduler-plan fixture: %#v", commands) + } + for _, command := range result.NextCommands { + if !strings.HasPrefix(command, renderer.DisplayCommand()+" ") { + t.Fatalf("generated command=%q, want exact renderer prefix %q", command, renderer.DisplayCommand()) + } + } + + manifest, err := BootstrapMaterializationManifest(result) + if err != nil { + t.Fatalf("BootstrapMaterializationManifest() error=%v", err) + } + assertMaterializedCallerCommandPrefix(t, manifest, callerCommands, result.NextCommands) + }) } - for _, command := range result.NextCommands { - assertPackageExecutableCommand(t, command) +} + +func assertMaterializedCallerCommandPrefix(t *testing.T, manifest map[string]any, callerCommands []string, generatedCommands []string) { + t.Helper() + for _, rawFile := range manifest["files"].([]any) { + file := rawFile.(map[string]any) + if file["payloadKey"] != "adoptionGuidance" { + continue + } + var guidance map[string]any + if err := json.Unmarshal([]byte(file["content"].(string)), &guidance); err != nil { + t.Fatalf("decode materialized adoption guidance: %v", err) + } + commands := anyStrings(guidance["agentGuidance"].(map[string]any)["commands"]) + if len(commands) != len(callerCommands)+len(generatedCommands) || + !reflect.DeepEqual(commands[:len(callerCommands)], callerCommands) || + !reflect.DeepEqual(commands[len(callerCommands):], generatedCommands) { + t.Fatalf("materialized guidance commands=%#v, want caller prefix %#v and generated suffix %#v", commands, callerCommands, generatedCommands) + } + return } + t.Fatal("materialization manifest missing adoptionGuidance payload") } func TestBootstrapRejectsUnknownRootAndNestedFields(t *testing.T) { @@ -373,14 +429,3 @@ func containsString(values []string, needle string) bool { } return false } - -func assertPackageExecutableCommand(t *testing.T, command string) { - t.Helper() - fields := strings.Fields(command) - if len(fields) < 2 { - t.Fatalf("generated command %q has no command name", command) - } - if fields[0] != cliexec.BinaryName { - t.Fatalf("generated command %q uses binary %q, want %q", command, fields[0], cliexec.BinaryName) - } -} diff --git a/internal/command/packageruntimedependency/package_runtime_dependency_test.go b/internal/command/packageruntimedependency/package_runtime_dependency_test.go index 95564fe..2cb41c1 100644 --- a/internal/command/packageruntimedependency/package_runtime_dependency_test.go +++ b/internal/command/packageruntimedependency/package_runtime_dependency_test.go @@ -10,7 +10,10 @@ import ( func TestBuildAdmitsExternalRuntimeDependencyAndRejectsWorkspaceResolution(t *testing.T) { commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.027617011738725950397530096345580960352335933116627715389463795934691621119686") input := validPackageRuntimeDependencyInput() - record, exitCode := Build(input) + record, exitCode, err := Build(input) + if err != nil { + t.Fatalf("Build() error=%v", err) + } if exitCode != 0 || record.State != "passed" { t.Fatalf("Build() exit=%d state=%s, want passed", exitCode, record.State) } @@ -19,7 +22,10 @@ func TestBuildAdmitsExternalRuntimeDependencyAndRejectsWorkspaceResolution(t *te input["packageResolution"].(map[string]any)["packageRoot"] = "/repo/packages/proofkit" input["packageResolution"].(map[string]any)["realPackageRoot"] = "/repo/packages/proofkit" input["packageResolution"].(map[string]any)["resolvedEntryPoint"] = "/repo/packages/proofkit/dist/index.js" - record, exitCode = Build(input) + record, exitCode, err = Build(input) + if err != nil { + t.Fatalf("Build() error=%v", err) + } if exitCode == 0 || record.State != "failed" { t.Fatalf("Build(workspace) exit=%d state=%s, want failed", exitCode, record.State) } @@ -29,7 +35,10 @@ func TestBuildRejectsLockfileIntegrityDrift(t *testing.T) { input := validPackageRuntimeDependencyInput() input["packageResolution"].(map[string]any)["lockfileIntegrity"] = "sha512-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - record, exitCode := Build(input) + record, exitCode, err := Build(input) + if err != nil { + t.Fatalf("Build() error=%v", err) + } if exitCode == 0 || record.State != "failed" { t.Fatalf("Build() exit=%d state=%s, want failed", exitCode, record.State) } @@ -43,19 +52,12 @@ func TestBuildRejectsSecretLikeReportVisibleText(t *testing.T) { input := validPackageRuntimeDependencyInput() input["nonClaims"] = []any{secret} - record, exitCode := Build(input) - if exitCode == 0 || record.State != "failed" { - t.Fatalf("Build() exit=%d state=%s, want failed", exitCode, record.State) - } - encoded, err := json.Marshal(record) - if err != nil { - t.Fatalf("marshal report: %v", err) - } - if strings.Contains(string(encoded), secret) { - t.Fatalf("report leaked secret-shaped caller text: %s", string(encoded)) + _, exitCode, err := Build(input) + if exitCode != 1 || err == nil || !strings.Contains(err.Error(), "secret-like values") { + t.Fatalf("Build() exit=%d error=%v, want structural rejection", exitCode, err) } - if !strings.Contains(string(encoded), "secret-like values") { - t.Fatalf("report=%s, want secret-like rejection", string(encoded)) + if strings.Contains(err.Error(), secret) { + t.Fatalf("error leaked secret-shaped caller text: %s", err) } } diff --git a/internal/command/packageruntimedependency/packageruntimedependency.go b/internal/command/packageruntimedependency/packageruntimedependency.go index 1ffdfac..4d3b4b1 100644 --- a/internal/command/packageruntimedependency/packageruntimedependency.go +++ b/internal/command/packageruntimedependency/packageruntimedependency.go @@ -66,10 +66,10 @@ type locationFacts struct { ResolvedEntryPointInsideNodeModules bool } -func Build(raw any) (report.Record, int) { +func Build(raw any) (report.Record, int, error) { input, err := admitInput(raw) if err != nil { - return invalidInputReport(sanitizedError(err)), 1 + return report.Record{}, 1, fmt.Errorf("%s", sanitizedError(err)) } facts := makeLocationFacts(input) mode := classifyMode(facts) @@ -142,9 +142,9 @@ func Build(raw any) (report.Record, int) { NonClaims: admit.StringSliceToAny(nonClaims), } if state == "passed" { - return record, 0 + return record, 0, nil } - return record, 1 + return record, 1, nil } func admitInput(raw any) (input, error) { @@ -357,34 +357,6 @@ func status(passed bool) string { return "failed" } -func invalidInputReport(message string) report.Record { - return report.Record{ - SchemaVersion: 1, - ReportKind: reportKind, - ReportID: "invalid-input", - State: "failed", - Summary: map[string]any{ - "accepted": false, - "failureCount": 1, - "mode": "unadmitted_package", - }, - Diagnostics: []report.Diagnostic{ - {Key: "error", Value: message}, - }, - RuleResults: []report.RuleResult{ - { - Diagnostics: []report.Diagnostic{ - {Key: "phase", Value: "input_admission"}, - }, - Message: message, - RuleID: "proofkit.package-runtime-dependency-admission.input", - Status: "failed", - }, - }, - NonClaims: standardNonClaims, - } -} - func packageName(raw any, context string) (string, error) { text, err := nonEmptyText(raw, context) if err != nil { diff --git a/internal/command/pilotadmission/pilotadmission.go b/internal/command/pilotadmission/pilotadmission.go index 8143502..7d75ef3 100644 --- a/internal/command/pilotadmission/pilotadmission.go +++ b/internal/command/pilotadmission/pilotadmission.go @@ -326,6 +326,32 @@ func BuildFromContractEnvelope(raw any, field string, options Options) (report.R return Build(input, options) } +func BuildAllFromContractEnvelope(raw any) (report.Record, int, report.Record, int, error) { + envelope, err := contractenv.Object(raw, "proofkit.pilot-admission.v1", "pilot admission", "input", "stackDiverseInput") + if err != nil { + return report.Record{}, 1, report.Record{}, 1, err + } + firstInput, err := contractenv.ObjectField(envelope, "input", "pilot admission contract envelope") + if err != nil { + return report.Record{}, 1, report.Record{}, 1, err + } + stackDiverseInput, err := contractenv.ObjectField(envelope, "stackDiverseInput", "pilot admission contract envelope") + if err != nil { + return report.Record{}, 1, report.Record{}, 1, err + } + first, firstExitCode, err := Build(firstInput, Options{}) + if err != nil { + return report.Record{}, 1, report.Record{}, 1, err + } + stackDiverse, stackDiverseExitCode, err := Build(stackDiverseInput, Options{ + RequireStackDiverseReleaseCandidate: true, + }) + if err != nil { + return report.Record{}, 1, report.Record{}, 1, err + } + return first, firstExitCode, stackDiverse, stackDiverseExitCode, nil +} + func admitBlockingDisposition(record map[string]any, failures *[]string) admittedBlockingDisposition { addErr(failures, admit.KnownKeys(record, []string{"dispositionPolicy", "explicitlyDeferredRequirements", "requirements", "totalBlockingRequirements", "unmappedRequirements", "witnessBackedRequirements"}, "blocking disposition")) if record["dispositionPolicy"] != "all_blocking_requirements_must_be_witnessed_or_explicitly_deferred" { diff --git a/internal/command/pilotadmission/pilotadmission_test.go b/internal/command/pilotadmission/pilotadmission_test.go index dcf6c5c..cf78df4 100644 --- a/internal/command/pilotadmission/pilotadmission_test.go +++ b/internal/command/pilotadmission/pilotadmission_test.go @@ -38,6 +38,47 @@ func TestBuildRejectsPilotDisplayCommandShellControlTokens(t *testing.T) { } } +func TestBuildAllFromContractEnvelopeAdmitsBothOwnedInputs(t *testing.T) { + firstInput := validPilotInput() + stackDiverseInput := validPilotInput() + stackDiverseInput["pilotId"] = "proofkit.test.pilot.stack-diverse" + stackDiverseInput["stackDiversity"] = validStackDiversity() + stackDiverseInput["cacheNegativeChecks"] = validCacheNegativeChecks() + impactInput := stackDiverseInput["impactDemos"].([]any)[0].(map[string]any)["impactInput"].(map[string]any) + impactInput["changedBindingRecordIds"] = []any{"REQ-PROOFKIT-001"} + impactInput["changedPaths"] = []any{ + "docs/specs/proofkit/requirements.v1.json", + "internal/proofkit/witness_test.go", + } + impactInput["changedWitnessPathCoverage"] = []any{ + map[string]any{ + "path": "internal/proofkit/witness_test.go", + "recordIds": []any{"REQ-PROOFKIT-001"}, + }, + } + + envelope := map[string]any{ + "schema": "proofkit.pilot-admission.v1", + "input": firstInput, + "stackDiverseInput": stackDiverseInput, + } + first, firstExitCode, stackDiverse, stackDiverseExitCode, err := BuildAllFromContractEnvelope(envelope) + if err != nil { + t.Fatalf("BuildAllFromContractEnvelope() error = %v", err) + } + if firstExitCode != 0 || first.State != "passed" { + t.Fatalf("first exit=%d state=%s, want passed", firstExitCode, first.State) + } + if stackDiverseExitCode != 0 || stackDiverse.State != "passed" { + t.Fatalf("stack-diverse exit=%d state=%s rules=%#v, want passed", stackDiverseExitCode, stackDiverse.State, stackDiverse.RuleResults) + } + + delete(envelope, "stackDiverseInput") + if _, _, _, _, err := BuildAllFromContractEnvelope(envelope); err == nil || !strings.Contains(err.Error(), "must declare object stackDiverseInput") { + t.Fatalf("BuildAllFromContractEnvelope() error = %v, want missing field rejection", err) + } +} + func validPilotInput() map[string]any { return map[string]any{ "schemaVersion": json.Number("1"), @@ -147,3 +188,33 @@ func validPilotInput() map[string]any { "rolloutClaim": false, } } + +func validStackDiversity() map[string]any { + dimensions := make([]any, 0, len(stackDiversityDimensions)) + for _, dimension := range stackDiversityDimensions { + dimensions = append(dimensions, map[string]any{ + "baseline": "baseline-" + dimension, + "candidate": "candidate-" + dimension, + "dimension": dimension, + "evidence": "docs/evidence/" + dimension + ".md", + }) + } + return map[string]any{ + "baselinePilotId": "proofkit.test.pilot", + "dimensions": dimensions, + } +} + +func validCacheNegativeChecks() []any { + checks := make([]any, 0, len(cacheInvalidationClasses)) + for _, inputClass := range cacheInvalidationClasses { + checks = append(checks, map[string]any{ + "checkId": "proofkit.test.cache." + inputClass, + "evidence": "Cache invalidates on " + inputClass + " changes.", + "expectedOutcome": "invalidate_output", + "invalidatedInputClass": inputClass, + "liveOrCredentialedCacheable": false, + }) + } + return checks +} diff --git a/internal/command/projectstructure/materialization.go b/internal/command/projectstructure/materialization.go index 83645bb..49a68a7 100644 --- a/internal/command/projectstructure/materialization.go +++ b/internal/command/projectstructure/materialization.go @@ -53,7 +53,7 @@ func projectPaths(raw any) (projectPathSet, error) { }, nil } -func buildMaterializationManifest(input materializationInput) (map[string]any, error) { +func buildMaterializationManifest(input materializationInput, renderer cliexec.Renderer) (map[string]any, error) { files := []map[string]any{} repoProfileFile, err := payloadFile(input.paths.repoProfileScaffoldInputPath, "caller-owned repo-profile scaffold input", "project_scaffold", input.repoProfileInput, nil) if err != nil { @@ -116,9 +116,9 @@ func buildMaterializationManifest(input materializationInput) (map[string]any, e } } nextCommands := []any{ - cliexec.DisplayCommand("scaffold-profile-plan", "--input", input.paths.repoProfileScaffoldInputPath), - cliexec.DisplayCommand("gradual-adoption-bootstrap", "--input", input.paths.bootstrapInputPath), - cliexec.DisplayCommand("adoption-workflow-plan", "--input", input.paths.workflowInputPath), + renderer.DisplayCommand("scaffold-profile-plan", "--input", input.paths.repoProfileScaffoldInputPath), + renderer.DisplayCommand("gradual-adoption-bootstrap", "--input", input.paths.bootstrapInputPath), + renderer.DisplayCommand("adoption-workflow-plan", "--input", input.paths.workflowInputPath), } nextCommands = append(nextCommands, anyArray(input.bootstrapManifest["nextCommands"])...) return map[string]any{ diff --git a/internal/command/projectstructure/projectstructure_test.go b/internal/command/projectstructure/projectstructure_test.go index b54f28f..56e2ae4 100644 --- a/internal/command/projectstructure/projectstructure_test.go +++ b/internal/command/projectstructure/projectstructure_test.go @@ -2,10 +2,13 @@ package projectstructure import ( "encoding/json" + "reflect" "strings" "testing" + "github.com/research-engineering/agentic-proofkit/internal/command/adoptionworkflow" "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" + "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" "github.com/research-engineering/agentic-proofkit/internal/kernel/report" "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" ) @@ -48,6 +51,54 @@ func TestBuildAdmitsProjectStructureScaffoldAndEmitsBoundedEnvelope(t *testing.T } } +func TestBuildRendererOwnsWorkflowPlanAndSourceReportIdentity(t *testing.T) { + profiles := []struct { + name string + profile string + pythonExecutable string + }{ + {name: "npm offline", profile: cliexec.ProfileNPMOffline}, + {name: "python module", profile: cliexec.ProfilePythonModule, pythonExecutable: "/tmp/proofkit-python"}, + } + for _, profile := range profiles { + t.Run(profile.name, func(t *testing.T) { + renderer, err := cliexec.AdmitLauncherProfile(profile.profile, profile.pythonExecutable) + if err != nil { + t.Fatalf("AdmitLauncherProfile() error=%v", err) + } + result, err := BuildResultWithRenderer(validProjectStructureInput(), renderer) + if err != nil { + t.Fatalf("BuildResultWithRenderer() error=%v", err) + } + workflow, err := adoptionworkflow.BuildResultWithRenderer(result.AdoptionWorkflowInput, renderer) + if err != nil { + t.Fatalf("BuildResultWithRenderer(workflow) error=%v", err) + } + if !reflect.DeepEqual(result.AdoptionWorkflowPlan, workflow.Plan) { + t.Fatal("project structure workflow plan does not preserve the admitted renderer") + } + expectedHash, err := digest.StableJSONSHA256Ref(workflow.Record.JSONValue()) + if err != nil { + t.Fatalf("StableJSONSHA256Ref() error=%v", err) + } + matches := 0 + for _, raw := range anyArray(result.Manifest["sourceReports"]) { + source := mapValue(raw) + if source["reportKind"] != workflow.Record.ReportKind { + continue + } + matches++ + if source["stableHash"] != expectedHash { + t.Fatalf("workflow source report stableHash=%v, want %s", source["stableHash"], expectedHash) + } + } + if matches != 1 { + t.Fatalf("workflow source report count=%d, want 1", matches) + } + }) + } +} + func TestBuildRejectsSecretLikeProjectStructureNonClaims(t *testing.T) { input := validProjectStructureInput() input["nonClaims"] = []any{"Authorization: Bearer abcdefghijklmnop"} diff --git a/internal/command/projectstructure/scaffold.go b/internal/command/projectstructure/scaffold.go index a26cc6e..94eb1a1 100644 --- a/internal/command/projectstructure/scaffold.go +++ b/internal/command/projectstructure/scaffold.go @@ -8,6 +8,7 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/command/gradualadoption" "github.com/research-engineering/agentic-proofkit/internal/command/scaffoldprofileplan" "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" "github.com/research-engineering/agentic-proofkit/internal/kernel/report" ) @@ -30,7 +31,11 @@ type Result struct { } func Build(raw any) (map[string]any, int, error) { - result, err := BuildResult(raw) + return BuildWithRenderer(raw, cliexec.PathRenderer()) +} + +func BuildWithRenderer(raw any, renderer cliexec.Renderer) (map[string]any, int, error) { + result, err := BuildResultWithRenderer(raw, renderer) if err != nil { return nil, 1, err } @@ -38,7 +43,11 @@ func Build(raw any) (map[string]any, int, error) { } func BuildEnvelope(raw any) (map[string]any, int, error) { - result, err := BuildResult(raw) + return BuildEnvelopeWithRenderer(raw, cliexec.PathRenderer()) +} + +func BuildEnvelopeWithRenderer(raw any, renderer cliexec.Renderer) (map[string]any, int, error) { + result, err := BuildResultWithRenderer(raw, renderer) if err != nil { return nil, 1, err } @@ -46,6 +55,10 @@ func BuildEnvelope(raw any) (map[string]any, int, error) { } func BuildResult(raw any) (Result, error) { + return BuildResultWithRenderer(raw, cliexec.PathRenderer()) +} + +func BuildResultWithRenderer(raw any, renderer cliexec.Renderer) (Result, error) { input, ok := raw.(map[string]any) if !ok { return Result{}, fmt.Errorf("project structure scaffold input must be an object") @@ -83,7 +96,7 @@ func BuildResult(raw any) (Result, error) { if err != nil { return Result{}, err } - bootstrap, err := gradualadoption.BuildBootstrapResult(bootstrapInput) + bootstrap, err := gradualadoption.BuildBootstrapResultWithRenderer(bootstrapInput, renderer) if err != nil { return Result{}, err } @@ -95,7 +108,7 @@ func BuildResult(raw any) (Result, error) { if err != nil { return Result{}, err } - adoptionWorkflow, err := adoptionworkflow.BuildResult(workflowInput) + adoptionWorkflow, err := adoptionworkflow.BuildResultWithRenderer(workflowInput, renderer) if err != nil { return Result{}, err } @@ -123,7 +136,7 @@ func BuildResult(raw any) (Result, error) { bootstrap.Record, adoptionWorkflow.Record, }, - }) + }, renderer) if err != nil { return Result{}, err } diff --git a/internal/command/publicapi/public_api.go b/internal/command/publicapi/public_api.go index 6104098..ae2062e 100644 --- a/internal/command/publicapi/public_api.go +++ b/internal/command/publicapi/public_api.go @@ -1,9 +1,11 @@ package publicapi import ( + "crypto/sha256" "fmt" "io" "os" + pathpkg "path" "path/filepath" "sort" "strings" @@ -45,21 +47,35 @@ type packageSnapshot struct { dir string manifest map[string]any name string + root *os.Root } type sourceExportSnapshot struct { runtimeExports []string typeExports []string + digest [sha256.Size]byte + identity os.FileInfo +} + +type admittedFileSnapshot struct { + canonical string + content string + digest [sha256.Size]byte + identity os.FileInfo } type scanCache struct { + root *os.Root repoRoot string + initErr error maxBytes int64 bytesRead int64 - files map[string][]byte + files map[string]admittedFileSnapshot sourceExports map[string]sourceExportSnapshot } +var scanAdmissionBarrier func(stage string, lexicalPath string) + func Verify(raw any, options Options) (map[string]any, int, error) { return verifyWithScanBudget(raw, options, maxAggregateScanBytes) } @@ -68,11 +84,11 @@ func verifyWithScanBudget(raw any, options Options, scanBudget int64) (map[strin if options.MachineContract == "" { options.MachineContract = defaultMachineContract } - repoRoot, err := filepath.EvalSymlinks(options.RepoRoot) - if err != nil { - return nil, 1, err + scan := newScanCache(options.RepoRoot, scanBudget) + if scan.initErr != nil { + return nil, 1, scan.initErr } - scan := newScanCache(repoRoot, scanBudget) + defer scan.root.Close() manifest, err := admitManifest(raw, options.MachineContract) if err != nil { return nil, 1, err @@ -81,6 +97,7 @@ func verifyWithScanBudget(raw any, options Options, scanBudget int64) (map[strin if err != nil { return nil, 1, err } + defer closePackageRoots(packages) failures := []string{} verifyCoveredPackageExportKeys(packages, manifest, &failures) seenKeys := map[string]struct{}{} @@ -103,7 +120,7 @@ func verifyWithScanBudget(raw any, options Options, scanBudget int64) (map[strin actualRuntimeSet := map[string]struct{}{} actualTypeSet := map[string]struct{}{} for _, sourcePath := range entrySourcePaths(item) { - resolvedSource, err := resolvePackageSource(scan.repoRoot, pkg.dir, sourcePath, manifestKey+" source") + actualRuntime, actualTypes, err := scan.collectSourceExports(sourcePath, pkg, manifestKey+" source") if err != nil { if os.IsNotExist(err) { failures = append(failures, fmt.Sprintf("%s source does not exist: %s", manifestKey, sourcePath)) @@ -111,10 +128,6 @@ func verifyWithScanBudget(raw any, options Options, scanBudget int64) (map[strin } return nil, 1, err } - actualRuntime, actualTypes, err := scan.collectSourceExports(resolvedSource, manifestKey+" source") - if err != nil { - return nil, 1, err - } for _, value := range actualRuntime { actualRuntimeSet[value] = struct{}{} } @@ -259,76 +272,173 @@ func exportConditions(raw any, context string) ([]exportCondition, error) { return conditions, nil } -func referencedPackages(scan *scanCache, entries []entry) (map[string]packageSnapshot, error) { +func referencedPackages(scan *scanCache, entries []entry) (_ map[string]packageSnapshot, returnErr error) { byManifest := map[string]packageSnapshot{} + defer func() { + if returnErr != nil { + closePackageRoots(byManifest) + } + }() manifestByName := map[string]string{} for _, item := range entries { if _, exists := byManifest[item.PackageManifestPath]; exists { continue } - manifestPath := filepath.Join(scan.repoRoot, filepath.FromSlash(item.PackageManifestPath)) - resolvedManifest, err := resolvedPathUnderRoot(scan.repoRoot, manifestPath, "TypeScript public API package manifest") - if err != nil { - return nil, err - } - manifest, err := readPackageManifest(scan, resolvedManifest) + manifest, _, packageDir, packageRoot, err := readPackageManifest(scan, item.PackageManifestPath) if err != nil { return nil, err } name, err := nonEmptyString(manifest["name"], item.PackageManifestPath+" name") if err != nil { + _ = packageRoot.Close() return nil, err } if previous, exists := manifestByName[name]; exists && previous != item.PackageManifestPath { + packageRoot.Close() return nil, fmt.Errorf("duplicate referenced package name %s in %s and %s", name, previous, item.PackageManifestPath) } manifestByName[name] = item.PackageManifestPath - byManifest[item.PackageManifestPath] = packageSnapshot{dir: filepath.Dir(resolvedManifest), manifest: manifest, name: name} + byManifest[item.PackageManifestPath] = packageSnapshot{dir: packageDir, manifest: manifest, name: name, root: packageRoot} } return byManifest, nil } -func readPackageManifest(scan *scanCache, path string) (map[string]any, error) { - source, _, err := scan.readFile(path, "TypeScript public API package manifest", maxPackageManifestBytes) +func readPackageManifest(scan *scanCache, path string) (_ map[string]any, _ admittedFileSnapshot, _ string, _ *os.Root, returnErr error) { + const context = "TypeScript public API package manifest" + lexical, err := scan.relativePath(path, context) if err != nil { - return nil, err + return nil, admittedFileSnapshot{}, "", nil, err + } + packageDir := pathpkg.Dir(lexical) + root, err := scan.root.OpenRoot(filepath.FromSlash(packageDir)) + if err != nil { + return nil, admittedFileSnapshot{}, "", nil, fmt.Errorf("open referenced package root %s: %w", packageDir, err) + } + defer func() { + if returnErr != nil { + _ = root.Close() + } + }() + snapshot, err := scan.readRelativeFileSnapshot( + root, + lexical, + pathpkg.Base(lexical), + packageDir, + context, + maxPackageManifestBytes, + ) + if err != nil { + return nil, admittedFileSnapshot{}, "", nil, err } - parsed, err := admission.DecodeJSON(strings.NewReader(string(source)), int64(len(source))) + parsed, err := admission.DecodeJSON(strings.NewReader(snapshot.content), int64(len(snapshot.content))) if err != nil { - return nil, fmt.Errorf("%s: %w", path, err) + return nil, admittedFileSnapshot{}, "", nil, fmt.Errorf("%s: %w", path, err) } record, ok := parsed.(map[string]any) if !ok { - return nil, fmt.Errorf("%s must contain a JSON object", path) + return nil, admittedFileSnapshot{}, "", nil, fmt.Errorf("%s must contain a JSON object", path) + } + return record, snapshot, packageDir, root, nil +} + +func closePackageRoots(packages map[string]packageSnapshot) { + for _, pkg := range packages { + if pkg.root != nil { + _ = pkg.root.Close() + } } - return record, nil } func newScanCache(repoRoot string, maxBytes int64) *scanCache { + root, err := os.OpenRoot(repoRoot) return &scanCache{ + root: root, repoRoot: repoRoot, + initErr: err, maxBytes: maxBytes, - files: map[string][]byte{}, + files: map[string]admittedFileSnapshot{}, sourceExports: map[string]sourceExportSnapshot{}, } } -func (scan *scanCache) readFile(filePath string, context string, maxFileBytes int64) ([]byte, string, error) { - resolved, err := resolvedPathUnderRoot(scan.repoRoot, filePath, context) +func (scan *scanCache) readFile(filePath string, context string, maxFileBytes int64) (string, string, error) { + snapshot, err := scan.readFileSnapshot(filePath, context, maxFileBytes) if err != nil { - return nil, "", err + return "", "", err } - if content, ok := scan.files[resolved]; ok { - if int64(len(content)) > maxFileBytes { - return nil, "", fmt.Errorf("%s exceeds the %s file limit", context, byteLimitLabel(maxFileBytes)) + return snapshot.content, snapshot.canonical, nil +} + +func (scan *scanCache) readFileSnapshot(filePath string, context string, maxFileBytes int64) (admittedFileSnapshot, error) { + if scan.initErr != nil { + return admittedFileSnapshot{}, scan.initErr + } + lexical, err := scan.relativePath(filePath, context) + if err != nil { + return admittedFileSnapshot{}, err + } + return scan.readRelativeFileSnapshot(scan.root, lexical, lexical, "", context, maxFileBytes) +} + +func (scan *scanCache) readFileSnapshotUnder(filePath string, pkg packageSnapshot, context string, maxFileBytes int64) (admittedFileSnapshot, error) { + if scan.initErr != nil { + return admittedFileSnapshot{}, scan.initErr + } + lexical, err := scan.relativePath(filePath, context) + if err != nil { + return admittedFileSnapshot{}, err + } + if pkg.root == nil || !pathWithin(pkg.dir, lexical) { + return admittedFileSnapshot{}, fmt.Errorf("%s must identify a file under its referenced package manifest directory", context) + } + packageRelative := strings.TrimPrefix(lexical, strings.TrimSuffix(pkg.dir, "/")+"/") + return scan.readRelativeFileSnapshot(pkg.root, lexical, packageRelative, pkg.dir, context, maxFileBytes) +} + +func (scan *scanCache) readRelativeFileSnapshot(root *os.Root, lexical string, rootRelative string, canonicalPrefix string, context string, maxFileBytes int64) (admittedFileSnapshot, error) { + if snapshot, ok := scan.files[lexical]; ok { + if int64(len(snapshot.content)) > maxFileBytes { + return admittedFileSnapshot{}, fmt.Errorf("%s exceeds the %s file limit", context, byteLimitLabel(maxFileBytes)) } - return content, resolved, nil + return snapshot, nil + } + canonicalRelative, err := canonicalRelativePath(root, rootRelative, context) + if err != nil { + return admittedFileSnapshot{}, err } - file, err := os.Open(resolved) + canonical := canonicalRelative + if canonicalPrefix != "" { + canonical = pathpkg.Join(canonicalPrefix, canonicalRelative) + } + if scanAdmissionBarrier != nil { + scanAdmissionBarrier("canonical_resolved", lexical) + } + file, err := root.Open(filepath.FromSlash(rootRelative)) if err != nil { - return nil, "", err + return admittedFileSnapshot{}, err } defer file.Close() + canonicalFile, err := root.Open(filepath.FromSlash(canonicalRelative)) + if err != nil { + return admittedFileSnapshot{}, err + } + canonicalInfo, err := canonicalFile.Stat() + if closeErr := canonicalFile.Close(); err == nil { + err = closeErr + } + if err != nil { + return admittedFileSnapshot{}, err + } + before, err := file.Stat() + if err != nil { + return admittedFileSnapshot{}, err + } + if !before.Mode().IsRegular() { + return admittedFileSnapshot{}, fmt.Errorf("%s must identify a regular file", context) + } + if !os.SameFile(before, canonicalInfo) { + return admittedFileSnapshot{}, fmt.Errorf("%s changed identity during confined admission", context) + } remaining := scan.maxBytes - scan.bytesRead readLimit := maxFileBytes if remaining < readLimit { @@ -339,33 +449,60 @@ func (scan *scanCache) readFile(filePath string, context string, maxFileBytes in } content, err := io.ReadAll(io.LimitReader(file, readLimit+1)) if err != nil { - return nil, "", err + return admittedFileSnapshot{}, err } if int64(len(content)) > readLimit { if maxFileBytes <= remaining { - return nil, "", fmt.Errorf("%s exceeds the %s file limit", context, byteLimitLabel(maxFileBytes)) + return admittedFileSnapshot{}, fmt.Errorf("%s exceeds the %s file limit", context, byteLimitLabel(maxFileBytes)) } - return nil, "", fmt.Errorf("TypeScript public API scan exceeds the %s aggregate file-read limit", byteLimitLabel(scan.maxBytes)) + return admittedFileSnapshot{}, fmt.Errorf("TypeScript public API scan exceeds the %s aggregate file-read limit", byteLimitLabel(scan.maxBytes)) + } + after, err := file.Stat() + if err != nil { + return admittedFileSnapshot{}, err + } + if !os.SameFile(before, after) || before.Size() != after.Size() || after.Size() != int64(len(content)) { + return admittedFileSnapshot{}, fmt.Errorf("%s changed identity or size during confined read", context) } scan.bytesRead += int64(len(content)) - scan.files[resolved] = content - return content, resolved, nil + snapshot := admittedFileSnapshot{ + canonical: canonical, + content: string(content), + digest: sha256.Sum256(content), + identity: before, + } + scan.files[lexical] = snapshot + return snapshot, nil } -func (scan *scanCache) collectSourceExports(filePath string, context string) ([]string, []string, error) { - source, resolved, err := scan.readFile(filePath, context, maxSourceFileBytes) +func (scan *scanCache) collectSourceExports(filePath string, pkg packageSnapshot, context string) ([]string, []string, error) { + admitted, err := scan.readFileSnapshotUnder(filePath, pkg, context, maxSourceFileBytes) if err != nil { return nil, nil, err } - if snapshot, ok := scan.sourceExports[resolved]; ok { - return snapshot.runtimeExports, snapshot.typeExports, nil + if !pathWithin(pkg.dir, admitted.canonical) { + return nil, nil, fmt.Errorf("%s must resolve under its referenced package manifest directory", context) + } + if err := requireTypeScriptSourceExtension(admitted.canonical, context+" canonical target"); err != nil { + return nil, nil, err + } + if snapshot, ok := scan.sourceExports[admitted.canonical]; ok { + if !os.SameFile(snapshot.identity, admitted.identity) || snapshot.digest != admitted.digest { + return nil, nil, fmt.Errorf("%s canonical source changed identity or content during scan", context) + } + return append([]string(nil), snapshot.runtimeExports...), append([]string(nil), snapshot.typeExports...), nil } - runtimeExports, typeExports, err := CollectExports(string(source)) + runtimeExports, typeExports, err := CollectExports(admitted.content) if err != nil { return nil, nil, err } - scan.sourceExports[resolved] = sourceExportSnapshot{runtimeExports: runtimeExports, typeExports: typeExports} - return runtimeExports, typeExports, nil + scan.sourceExports[admitted.canonical] = sourceExportSnapshot{ + runtimeExports: append([]string(nil), runtimeExports...), + typeExports: append([]string(nil), typeExports...), + digest: admitted.digest, + identity: admitted.identity, + } + return append([]string(nil), runtimeExports...), append([]string(nil), typeExports...), nil } func byteLimitLabel(limit int64) string { @@ -378,16 +515,75 @@ func byteLimitLabel(limit int64) string { return fmt.Sprintf("%d-byte", limit) } -func resolvedPathUnderRoot(repoRoot string, filePath string, context string) (string, error) { - resolved, err := filepath.EvalSymlinks(filePath) - if err != nil { - return "", err +func (scan *scanCache) relativePath(filePath string, context string) (string, error) { + value := filePath + if filepath.IsAbs(value) { + relative, err := filepath.Rel(scan.repoRoot, value) + if err != nil { + return "", fmt.Errorf("%s must resolve inside repo root", context) + } + value = relative + } + value = filepath.ToSlash(filepath.Clean(value)) + if value == "." { + return "", fmt.Errorf("%s must identify a file under repo root", context) } - relative, err := filepath.Rel(repoRoot, resolved) - if err != nil || relative == "." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) || relative == ".." || filepath.IsAbs(relative) { + relative, err := admit.SafeRepoRelativePath(value, context) + if err != nil { return "", fmt.Errorf("%s must resolve inside repo root", context) } - return resolved, nil + return relative, nil +} + +func (scan *scanCache) canonicalRelativePath(lexical string, context string) (string, error) { + return canonicalRelativePath(scan.root, lexical, context) +} + +func canonicalRelativePath(root *os.Root, lexical string, context string) (string, error) { + pending := strings.Split(lexical, "/") + resolved := []string{} + for links := 0; len(pending) > 0; { + component := pending[0] + pending = pending[1:] + candidate := pathpkg.Join(append(resolved, component)...) + info, err := root.Lstat(filepath.FromSlash(candidate)) + if err != nil { + return "", err + } + if info.Mode()&os.ModeSymlink == 0 { + resolved = append(resolved, component) + continue + } + links++ + if links > 255 { + return "", fmt.Errorf("%s contains too many symlink traversals", context) + } + target, err := root.Readlink(filepath.FromSlash(candidate)) + if err != nil { + return "", err + } + if filepath.IsAbs(target) { + return "", fmt.Errorf("%s uses an absolute symlink target; use a relative in-root symlink", context) + } + combined := pathpkg.Join(append([]string{pathpkg.Dir(candidate), filepath.ToSlash(target)}, pending...)...) + safe, err := admit.SafeRepoRelativePath(combined, context) + if err != nil { + return "", fmt.Errorf("%s must resolve inside repo root", context) + } + pending = strings.Split(safe, "/") + resolved = nil + } + if len(resolved) == 0 { + return "", fmt.Errorf("%s must identify a file under repo root", context) + } + return pathpkg.Join(resolved...), nil +} + +func pathWithin(parent, child string) bool { + if parent == "." || parent == "" { + return child != "." && child != "" + } + return child != parent && strings.HasPrefix(child, strings.TrimSuffix(parent, "/")+"/") } func safeRepoPath(raw any, context string) (string, error) { @@ -418,21 +614,6 @@ func requireTypeScriptSourceExtension(path string, context string) error { } } -func resolvePackageSource(repoRoot string, packageDir string, sourcePath string, context string) (string, error) { - resolved, err := resolvedPathUnderRoot(repoRoot, filepath.Join(repoRoot, filepath.FromSlash(sourcePath)), context) - if err != nil { - return "", err - } - relative, err := filepath.Rel(packageDir, resolved) - if err != nil || relative == "." || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) || filepath.IsAbs(relative) { - return "", fmt.Errorf("%s must resolve under its referenced package manifest directory", context) - } - if err := requireTypeScriptSourceExtension(resolved, context+" canonical target"); err != nil { - return "", err - } - return resolved, nil -} - func verifyPackageExportMap(pkg packageSnapshot, item entry, failures *[]string) { exportsField, ok := pkg.manifest["exports"].(map[string]any) if !ok { diff --git a/internal/command/publicapi/public_api_test.go b/internal/command/publicapi/public_api_test.go index ebd5755..65ec67a 100644 --- a/internal/command/publicapi/public_api_test.go +++ b/internal/command/publicapi/public_api_test.go @@ -3,11 +3,12 @@ package publicapi import ( "encoding/json" "fmt" - "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" "os" "path/filepath" "strings" "testing" + + "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" ) func TestVerifyTypeScriptPackagePublicAPISurfaces(t *testing.T) { @@ -26,6 +27,41 @@ func TestVerifyTypeScriptPackagePublicAPISurfaces(t *testing.T) { } } +func TestVerifyTypeScriptRootPackagePublicAPISurfaces(t *testing.T) { + repoRoot := t.TempDir() + if err := os.MkdirAll(filepath.Join(repoRoot, "src"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile( + filepath.Join(repoRoot, "src", "index.ts"), + []byte("export const VALUE = 1;\nexport function makeThing() {}\nexport type Mode = string;\nexport interface Thing {}\n"), + 0o600, + ); err != nil { + t.Fatal(err) + } + writeJSON(t, filepath.Join(repoRoot, "package.json"), map[string]any{ + "name": "@example/alpha", + "exports": map[string]any{ + ".": map[string]any{ + "import": "./src/index.ts", + "types": "./src/index.ts", + }, + "./internal": nil, + }, + }) + input := publicAPIManifest() + item := input["entries"].([]any)[0].(map[string]any) + item["packageManifestPath"] = "package.json" + for _, raw := range item["exportConditions"].([]any) { + raw.(map[string]any)["sourcePath"] = "src/index.ts" + } + + output, exitCode, err := Verify(input, Options{RepoRoot: repoRoot}) + if err != nil || exitCode != 0 { + t.Fatalf("Verify() exit=%d error=%v output=%#v, want root-package success", exitCode, err, output) + } +} + func TestVerifyTypeScriptPackagePublicAPIAcceptsExplicitSourceMappingsForCompiledTargets(t *testing.T) { repoRoot := writeTypeScriptPackageFixture(t) packageRoot := filepath.Join(repoRoot, "packages", "alpha") @@ -153,7 +189,7 @@ func TestVerifyTypeScriptPackagePublicAPIRejectsSymlinkEscapedSource(t *testing. } _, exitCode, err := Verify(publicAPIManifest(), Options{RepoRoot: repoRoot}) - if exitCode != 1 || err == nil || !strings.Contains(err.Error(), "must resolve inside repo root") { + if exitCode != 1 || err == nil || !strings.Contains(err.Error(), "absolute symlink target") { t.Fatalf("Verify() exitCode=%d error=%v, want symlink escape rejection", exitCode, err) } } @@ -172,7 +208,7 @@ func TestVerifyTypeScriptPackagePublicAPIRejectsSymlinkToTSX(t *testing.T) { if err := os.Remove(sourcePath); err != nil { t.Fatal(err) } - if err := os.Symlink(tsxPath, sourcePath); err != nil { + if err := os.Symlink("index.tsx", sourcePath); err != nil { t.Skipf("symlink unavailable: %v", err) } @@ -182,6 +218,297 @@ func TestVerifyTypeScriptPackagePublicAPIRejectsSymlinkToTSX(t *testing.T) { } } +func TestVerifyAcceptsStableRelativeInRootSymlink(t *testing.T) { + repoRoot := writeTypeScriptPackageFixture(t) + sourcePath := filepath.Join(repoRoot, "packages", "alpha", "src", "index.ts") + targetPath := filepath.Join(repoRoot, "packages", "alpha", "src", "real.ts") + content, err := os.ReadFile(sourcePath) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(targetPath, content, 0o600); err != nil { + t.Fatal(err) + } + if err := os.Remove(sourcePath); err != nil { + t.Fatal(err) + } + if err := os.Symlink("real.ts", sourcePath); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + output, exitCode, err := Verify(publicAPIManifest(), Options{RepoRoot: repoRoot}) + if err != nil || exitCode != 0 { + t.Fatalf("Verify() exit=%d error=%v output=%#v, want confined relative symlink success", exitCode, err, output) + } +} + +func TestVerifyRejectsDeterministicSymlinkSwap(t *testing.T) { + for _, item := range []struct { + name string + swap func(t *testing.T, repoRoot, outsideRoot string) + }{ + { + name: "leaf source swap", + swap: func(t *testing.T, repoRoot, outsideRoot string) { + sourcePath := filepath.Join(repoRoot, "packages", "alpha", "src", "index.ts") + if err := os.Remove(sourcePath); err != nil { + t.Fatal(err) + } + if err := os.Symlink(filepath.Join(outsideRoot, "index.ts"), sourcePath); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "ancestor directory swap", + swap: func(t *testing.T, repoRoot, outsideRoot string) { + sourceDir := filepath.Join(repoRoot, "packages", "alpha", "src") + if err := os.Rename(sourceDir, sourceDir+"-original"); err != nil { + t.Fatal(err) + } + if err := os.Symlink(outsideRoot, sourceDir); err != nil { + t.Fatal(err) + } + }, + }, + } { + t.Run(item.name, func(t *testing.T) { + repoRoot := writeTypeScriptPackageFixture(t) + outsideRoot := t.TempDir() + if err := os.WriteFile(filepath.Join(outsideRoot, "index.ts"), []byte(`export const SENTINEL = "outside";`), 0o600); err != nil { + t.Fatal(err) + } + swapped := false + scanAdmissionBarrier = func(stage, lexical string) { + if swapped || stage != "canonical_resolved" || lexical != "packages/alpha/src/index.ts" { + return + } + swapped = true + item.swap(t, repoRoot, outsideRoot) + } + t.Cleanup(func() { scanAdmissionBarrier = nil }) + + output, exitCode, err := Verify(publicAPIManifest(), Options{RepoRoot: repoRoot}) + if !swapped { + t.Fatal("deterministic scan barrier was not reached") + } + if exitCode != 1 || err == nil { + t.Fatalf("Verify() exit=%d error=%v output=%#v, want fail-closed swap rejection", exitCode, err, output) + } + if strings.Contains(fmt.Sprint(output), "SENTINEL") || strings.Contains(err.Error(), "SENTINEL") { + t.Fatalf("outside sentinel leaked through confined scanner: output=%#v error=%v", output, err) + } + }) + } +} + +func TestVerifyPinsPackageRootAcrossInRootSiblingSwap(t *testing.T) { + repoRoot := writeTypeScriptPackageFixture(t) + alphaSource := filepath.Join(repoRoot, "packages", "alpha", "src", "index.ts") + if err := os.WriteFile(alphaSource, []byte("export const ORIGINAL = 1;\n"), 0o600); err != nil { + t.Fatal(err) + } + swapped := false + scanAdmissionBarrier = func(stage, lexical string) { + if swapped || stage != "canonical_resolved" || lexical != "packages/alpha/src/index.ts" { + return + } + swapped = true + packagesDir := filepath.Join(repoRoot, "packages") + betaSourceDir := filepath.Join(packagesDir, "beta", "src") + if err := os.MkdirAll(betaSourceDir, 0o755); err != nil { + t.Fatal(err) + } + betaSource := "export const VALUE = 1;\nexport function makeThing() {}\nexport type Mode = string;\nexport interface Thing {}\n" + if err := os.WriteFile(filepath.Join(betaSourceDir, "index.ts"), []byte(betaSource), 0o600); err != nil { + t.Fatal(err) + } + alphaDir := filepath.Join(packagesDir, "alpha") + if err := os.Rename(alphaDir, alphaDir+"-original"); err != nil { + t.Fatal(err) + } + if err := os.Symlink("beta", alphaDir); err != nil { + t.Fatal(err) + } + } + t.Cleanup(func() { scanAdmissionBarrier = nil }) + + output, exitCode, err := Verify(publicAPIManifest(), Options{RepoRoot: repoRoot}) + if !swapped { + t.Fatal("deterministic scan barrier was not reached") + } + if err != nil || exitCode != 1 { + t.Fatalf("Verify() exit=%d error=%v output=%#v, want pinned original package source mismatch", exitCode, err, output) + } + failures := fmt.Sprint(output["failures"]) + if !strings.Contains(failures, "ORIGINAL") || strings.Contains(failures, "SENTINEL") { + t.Fatalf("Verify() failures=%s, want proof that the pinned alpha source, not the sibling, was read", failures) + } +} + +func TestScanCacheBindsBytesToFirstCanonicalIdentityAcrossSymlinkRetarget(t *testing.T) { + repoRoot := t.TempDir() + if err := os.WriteFile(filepath.Join(repoRoot, "a.ts"), []byte("A"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(repoRoot, "b.ts"), []byte("B"), 0o600); err != nil { + t.Fatal(err) + } + linkPath := filepath.Join(repoRoot, "entry.ts") + if err := os.Symlink("a.ts", linkPath); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + scan := newScanCache(repoRoot, maxAggregateScanBytes) + if scan.initErr != nil { + t.Fatal(scan.initErr) + } + t.Cleanup(func() { + if err := scan.root.Close(); err != nil { + t.Errorf("close scan root: %v", err) + } + }) + + firstContent, firstCanonical, err := scan.readFile("entry.ts", "fixture source", maxSourceFileBytes) + if err != nil { + t.Fatal(err) + } + if firstContent != "A" || firstCanonical != "a.ts" { + t.Fatalf("first read=(%q, %q), want bytes and canonical identity from a.ts", firstContent, firstCanonical) + } + if err := os.Remove(linkPath); err != nil { + t.Fatal(err) + } + if err := os.Symlink("b.ts", linkPath); err != nil { + t.Fatal(err) + } + currentCanonical, err := scan.canonicalRelativePath("entry.ts", "fixture source") + if err != nil { + t.Fatal(err) + } + if currentCanonical != "b.ts" { + t.Fatalf("retargeted lexical path resolves to %q, want b.ts", currentCanonical) + } + + secondContent, secondCanonical, err := scan.readFile("entry.ts", "fixture source", maxSourceFileBytes) + if err != nil { + t.Fatal(err) + } + if secondContent != "A" || secondCanonical != "a.ts" { + t.Fatalf("cached read=(%q, %q), want indivisible first-admission snapshot (A, a.ts)", secondContent, secondCanonical) + } +} + +func TestCanonicalSourceSnapshotRejectsChangedCrossAliasAdmission(t *testing.T) { + originalContent := []byte("export const ALPHA = 1;") + for _, test := range []struct { + name string + mutate func(t *testing.T, targetPath string, before os.FileInfo) + }{ + { + name: "identity drift with stable digest", + mutate: func(t *testing.T, targetPath string, before os.FileInfo) { + replacementPath := targetPath + ".replacement" + if err := os.WriteFile(replacementPath, originalContent, 0o600); err != nil { + t.Fatal(err) + } + if err := os.Remove(targetPath); err != nil { + t.Fatal(err) + } + if err := os.Rename(replacementPath, targetPath); err != nil { + t.Fatal(err) + } + after, err := os.Stat(targetPath) + if err != nil { + t.Fatal(err) + } + if os.SameFile(before, after) { + t.Fatal("replacement retained file identity; test cannot isolate identity drift") + } + }, + }, + { + name: "digest drift with stable identity", + mutate: func(t *testing.T, targetPath string, before os.FileInfo) { + file, err := os.OpenFile(targetPath, os.O_WRONLY|os.O_TRUNC, 0) + if err != nil { + t.Fatal(err) + } + if _, err := file.Write([]byte("export const BETA_ = 1;")); err != nil { + file.Close() + t.Fatal(err) + } + if err := file.Close(); err != nil { + t.Fatal(err) + } + after, err := os.Stat(targetPath) + if err != nil { + t.Fatal(err) + } + if !os.SameFile(before, after) { + t.Fatal("in-place rewrite changed file identity; test cannot isolate digest drift") + } + }, + }, + } { + t.Run(test.name, func(t *testing.T) { + repoRoot := t.TempDir() + packageDir := "packages/alpha" + packageRoot := filepath.Join(repoRoot, filepath.FromSlash(packageDir)) + if err := os.MkdirAll(packageRoot, 0o755); err != nil { + t.Fatal(err) + } + targetPath := filepath.Join(packageRoot, "real.ts") + if err := os.WriteFile(targetPath, originalContent, 0o600); err != nil { + t.Fatal(err) + } + for _, alias := range []string{"one.ts", "two.ts", "three.ts"} { + if err := os.Symlink("real.ts", filepath.Join(packageRoot, alias)); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + } + scan := newScanCache(repoRoot, maxAggregateScanBytes) + if scan.initErr != nil { + t.Fatal(scan.initErr) + } + t.Cleanup(func() { + if err := scan.root.Close(); err != nil { + t.Errorf("close scan root: %v", err) + } + }) + pinnedRoot, err := os.OpenRoot(packageRoot) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := pinnedRoot.Close(); err != nil { + t.Errorf("close pinned package root: %v", err) + } + }) + pkg := packageSnapshot{dir: packageDir, root: pinnedRoot} + + runtimeExports, _, err := scan.collectSourceExports(packageDir+"/one.ts", pkg, "first alias") + if err != nil { + t.Fatal(err) + } + assertStringSlice(t, runtimeExports, []string{"ALPHA"}) + runtimeExports, _, err = scan.collectSourceExports(packageDir+"/two.ts", pkg, "stable second alias") + if err != nil { + t.Fatalf("stable alias reuse: %v", err) + } + assertStringSlice(t, runtimeExports, []string{"ALPHA"}) + + before, err := os.Stat(targetPath) + if err != nil { + t.Fatal(err) + } + test.mutate(t, targetPath, before) + _, _, err = scan.collectSourceExports(packageDir+"/three.ts", pkg, "changed third alias") + if err == nil || !strings.Contains(err.Error(), "canonical source changed identity or content during scan") { + t.Fatalf("collectSourceExports() error=%v, want cross-alias canonical drift rejection", err) + } + }) + } +} + func TestVerifyTypeScriptPackagePublicAPIRejectsAmbiguousPackageManifest(t *testing.T) { cases := []struct { name string @@ -411,7 +738,7 @@ func TestScanCacheRejectsOversizedSource(t *testing.T) { t.Fatalf("close oversized source: %v", err) } scan := newScanCache(resolvedRoot, maxAggregateScanBytes) - if _, _, err := scan.readFile(path, "fixture source", maxSourceFileBytes); err == nil || !strings.Contains(err.Error(), "8 MiB") { + if _, _, err := scan.readFile("oversized.ts", "fixture source", maxSourceFileBytes); err == nil || !strings.Contains(err.Error(), "8 MiB") { t.Fatalf("scan.readFile() error=%v, want bounded file rejection", err) } } @@ -478,10 +805,10 @@ func TestScanCacheRejectsAggregateBytesAcrossUniqueFiles(t *testing.T) { t.Fatal(err) } scan := newScanCache(resolvedRoot, 10) - if _, _, err := scan.readFile(firstPath, "first source", maxSourceFileBytes); err != nil { + if _, _, err := scan.readFile("first.ts", "first source", maxSourceFileBytes); err != nil { t.Fatalf("read first source: %v", err) } - if _, _, err := scan.readFile(secondPath, "second source", maxSourceFileBytes); err == nil || !strings.Contains(err.Error(), "aggregate file-read limit") { + if _, _, err := scan.readFile("second.ts", "second source", maxSourceFileBytes); err == nil || !strings.Contains(err.Error(), "aggregate file-read limit") { t.Fatalf("scan.readFile() error=%v, want aggregate budget rejection", err) } } @@ -504,7 +831,7 @@ func TestReadPackageManifestRejectsOversizedMetadata(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := readPackageManifest(newScanCache(resolvedRoot, maxAggregateScanBytes), manifestPath); err == nil || !strings.Contains(err.Error(), "256 KiB") { + if _, _, _, _, err := readPackageManifest(newScanCache(resolvedRoot, maxAggregateScanBytes), "package.json"); err == nil || !strings.Contains(err.Error(), "256 KiB") { t.Fatalf("readPackageManifest() error=%v, want package metadata bound rejection", err) } } diff --git a/internal/command/readinesscloseout/readinesscloseout.go b/internal/command/readinesscloseout/readinesscloseout.go index 23ed8b4..e7edf69 100644 --- a/internal/command/readinesscloseout/readinesscloseout.go +++ b/internal/command/readinesscloseout/readinesscloseout.go @@ -2,6 +2,7 @@ package readinesscloseout import ( "fmt" + "html" "regexp" "sort" "strings" @@ -23,7 +24,9 @@ var readinessCloseoutNonClaims = []string{ var statusPattern = regexp.MustCompile(`^[A-Z]+(?:-[A-Z]+)?$`) var rowIDPattern = regexp.MustCompile(`^[A-Z]+(?:-[A-Z]+)*-\d+[A-Z]?$`) -var splitSegmentPattern = regexp.MustCompile(`[\n.;|]+`) +var markdownStructuralSegmentPattern = regexp.MustCompile(`[\n|]+`) +var phraseSegmentPattern = regexp.MustCompile(`[.;]+`) +var strictCharacterReferencePattern = regexp.MustCompile(`&(?:#[xX][0-9a-fA-F]+|#[0-9]+|[A-Za-z][A-Za-z0-9]+);`) var nonAlphaNumericPattern = regexp.MustCompile(`[^a-z0-9]+`) var whitespacePattern = regexp.MustCompile(`\s+`) @@ -165,8 +168,7 @@ type classification struct { func Build(raw any) (report.Record, int, error) { input, err := admitInput(raw) if err != nil { - result := invalidInputReport(err.Error()) - return result, 1, nil + return report.Record{}, 1, err } return buildReport(input) } @@ -434,12 +436,14 @@ func matchesPhraseRule(segment string, rule phraseRule) bool { } func normalizedSegments(text string) []string { - parts := splitSegmentPattern.Split(text, -1) result := []string{} - for _, part := range parts { - normalized := normalizeForPhraseScan(part) - if len(normalized) > 0 { - result = append(result, normalized) + for _, structuralSegment := range markdownStructuralSegmentPattern.Split(text, -1) { + decoded := decodeOneStrictCharacterReferencePass(structuralSegment) + for _, phraseSegment := range phraseSegmentPattern.Split(decoded, -1) { + normalized := normalizeForPhraseScan(phraseSegment) + if len(normalized) > 0 { + result = append(result, normalized) + } } } return result @@ -456,6 +460,10 @@ func normalizeForPhraseScan(value string) string { return " " + normalized + " " } +func decodeOneStrictCharacterReferencePass(value string) string { + return strictCharacterReferencePattern.ReplaceAllStringFunc(value, html.UnescapeString) +} + func includesAny(segment string, phrases []string) bool { for _, phrase := range phrases { if strings.Contains(segment, normalizeForPhraseScan(phrase)) { @@ -795,34 +803,6 @@ func hasNonClaimActionToken(normalized string) bool { return false } -func invalidInputReport(failure string) report.Record { - return report.Record{ - SchemaVersion: 1, - ReportKind: reportKind, - ReportID: "invalid-input", - State: "failed", - Summary: map[string]any{ - "blocked": 0, - "failed": 1, - "outOfScope": 0, - "passed": 0, - "readinessClaim": "classification_honesty_only", - "rowCount": 0, - "runIdentity": "invalid-input", - }, - Diagnostics: []report.Diagnostic{}, - RuleResults: []report.RuleResult{ - { - Diagnostics: []report.Diagnostic{}, - Message: failure, - RuleID: "readiness_closeout.input", - Status: "failed", - }, - }, - NonClaims: []any{"invalid input does not prove readiness closeout state"}, - } -} - func classificationDiagnostics(input closeoutInput, definition inputDefinition) []report.Diagnostic { return []report.Diagnostic{ {Key: "classification", Value: definition.Classification}, diff --git a/internal/command/readinesscloseout/readinesscloseout_test.go b/internal/command/readinesscloseout/readinesscloseout_test.go index bd98920..368955a 100644 --- a/internal/command/readinesscloseout/readinesscloseout_test.go +++ b/internal/command/readinesscloseout/readinesscloseout_test.go @@ -76,19 +76,10 @@ func TestBuildRejectsShellControlExactCommand(t *testing.T) { input := minimalCloseoutInput("### Production Readiness Roadmap\n") input["exactCommand"] = "proofkit readiness closeout && curl example.test" - record, status, err := Build(input) - if err != nil { - t.Fatalf("Build() unexpected error=%v", err) - } - if status == 0 || record.State != "failed" { - t.Fatalf("Build() status=%d state=%s, want failed", status, record.State) + _, status, err := Build(input) + if status != 1 || err == nil || !strings.Contains(err.Error(), "display-only command text") { + t.Fatalf("Build() status=%d error=%v, want structural rejection", status, err) } - for _, rule := range record.RuleResults { - if strings.Contains(rule.Message, "display-only command text") { - return - } - } - t.Fatalf("missing display-only command failure: %#v", record.RuleResults) } func TestBuildRejectsSecretLikeReportTextThroughCentralAdmission(t *testing.T) { @@ -96,38 +87,23 @@ func TestBuildRejectsSecretLikeReportTextThroughCentralAdmission(t *testing.T) { input := minimalCloseoutInput(closedFrontierMarkdown()) input["nonClaims"] = []any{secret} - record, status, err := Build(input) - if err != nil { - t.Fatalf("Build() unexpected error=%v", err) + _, status, err := Build(input) + if status != 1 || err == nil || !strings.Contains(err.Error(), "secret-like values") { + t.Fatalf("Build() status=%d error=%v, want structural rejection", status, err) } - if status == 0 || record.State != "failed" { - t.Fatalf("Build() status=%d state=%s, want failed", status, record.State) - } - encoded, err := json.Marshal(record) - if err != nil { - t.Fatalf("marshal record: %v", err) - } - if strings.Contains(string(encoded), secret) { - t.Fatalf("readiness report leaked secret-shaped caller text: %s", string(encoded)) + if strings.Contains(err.Error(), secret) { + t.Fatalf("readiness error leaked secret-shaped caller text: %s", err) } - assertRuleMessageContains(t, record, "readiness_closeout.input", "secret-like values") } func TestBuildRejectsCallerControlledReportKind(t *testing.T) { input := minimalCloseoutInput(closedFrontierMarkdown()) input["reportKind"] = "caller.controlled.kind" - record, status, err := Build(input) - if err != nil { - t.Fatalf("Build() unexpected error=%v", err) - } - if status == 0 || record.State != "failed" { - t.Fatalf("Build() status=%d state=%s, want failed", status, record.State) - } - if record.ReportKind != "proofkit.readiness-closeout" { - t.Fatalf("ReportKind=%q, want command-owned identity", record.ReportKind) + _, status, err := Build(input) + if status != 1 || err == nil || !strings.Contains(err.Error(), "unsupported field") { + t.Fatalf("Build() status=%d error=%v, want structural rejection", status, err) } - assertRuleMessageContains(t, record, "readiness_closeout.input", "unsupported field") } func TestBuildRejectsPassedClassificationForBlockedOwnerRow(t *testing.T) { @@ -182,20 +158,16 @@ func TestBuildRejectsBroadNegationAndFrontierOverclaim(t *testing.T) { input["phraseRules"] = []any{frontierAuthorityPhraseRule()} input["negatedNonClaimPhrases"] = []any{"must not"} - record, status, err := Build(input) - if err != nil { - t.Fatalf("Build() broad negation error=%v", err) - } - if status == 0 || record.State != "failed" { - t.Fatalf("Build() accepted broad negation: status=%d record=%#v", status, record) + _, status, err := Build(input) + if status != 1 || err == nil || !strings.Contains(err.Error(), "scoped non-claim predicate") { + t.Fatalf("Build() broad negation status=%d error=%v", status, err) } - assertRuleMessageContains(t, record, "readiness_closeout.input", "scoped non-claim predicate") input = minimalCloseoutInput(markdown) input["phraseRules"] = []any{frontierAuthorityPhraseRule()} input["negatedNonClaimPhrases"] = []any{"must not claim"} - record, status, err = Build(input) + record, status, err := Build(input) if err != nil { t.Fatalf("Build() scoped negation error=%v", err) } @@ -219,20 +191,63 @@ func TestBuildRejectsBroadNegationAndFrontierOverclaim(t *testing.T) { } } +func TestPhraseScanDecodesOneStrictCharacterReference(t *testing.T) { + for _, item := range []struct { + name string + text string + wantFailed bool + }{ + { + name: "single semicolon entity", + text: "Future authoring rows require separate owner proof before merge authority is established.", + wantFailed: true, + }, + { + name: "encoded structural delimiter remains text", + text: "Future authoring rows require separate owner proof before merge| authority is established.", + wantFailed: true, + }, + { + name: "double encoded entity is decoded once", + text: "Future authoring rows require separate owner proof before m&#101;rge authority is established.", + }, + { + name: "missing semicolon is not admitted", + text: "Future authoring rows require separate owner proof before merge authority is established.", + }, + } { + t.Run(item.name, func(t *testing.T) { + input := minimalCloseoutInput(closedFrontierMarkdown(item.text)) + input["phraseRules"] = []any{frontierAuthorityPhraseRule()} + input["negatedNonClaimPhrases"] = []any{"must not claim"} + record, status, err := Build(input) + if err != nil { + t.Fatalf("Build() error=%v", err) + } + if item.wantFailed { + if status != 1 || record.State != "failed" { + t.Fatalf("Build() status=%d state=%s, want failed", status, record.State) + } + assertRuleMessageContains(t, record, "PROD-09.frontier.non_claims", "live authority claim") + return + } + if status != 0 || record.State != "passed" { + t.Fatalf("Build() status=%d state=%s, want passed", status, record.State) + } + }) + } +} + func TestBuildRejectsScopeFreeNegatedNonClaimPhrases(t *testing.T) { for _, phrase := range []string{"must not", "do not", "does not", "not", "no", "non claim", "later row", "is blocked", "blocked on later rows", "blocked until later row", "remains blocked on later rows"} { t.Run(phrase, func(t *testing.T) { input := minimalCloseoutInput(closedFrontierMarkdown()) input["negatedNonClaimPhrases"] = []any{phrase} - record, status, err := Build(input) - if err != nil { - t.Fatalf("Build() broad phrase error=%v", err) - } - if status == 0 || record.State != "failed" { - t.Fatalf("Build() accepted broad phrase %q: status=%d record=%#v", phrase, status, record) + _, status, err := Build(input) + if status != 1 || err == nil || !strings.Contains(err.Error(), "scoped non-claim predicate") { + t.Fatalf("Build() broad phrase %q status=%d error=%v", phrase, status, err) } - assertRuleMessageContains(t, record, "readiness_closeout.input", "scoped non-claim predicate") }) } } diff --git a/internal/command/registryconsumer/registryconsumer.go b/internal/command/registryconsumer/registryconsumer.go index 0d70b4e..78f0dc8 100644 --- a/internal/command/registryconsumer/registryconsumer.go +++ b/internal/command/registryconsumer/registryconsumer.go @@ -71,7 +71,7 @@ type reportInput struct { func Build(raw any) (report.Record, int, error) { admitted, err := admitReportInput(raw) if err != nil { - return failedAdmissionReport(err), 1, nil + return report.Record{}, 1, err } failures := []string{} failures = append(failures, inputFailures(admitted.Input)...) @@ -436,28 +436,6 @@ func consumerProofDiagnostic(proofValue *proof) any { } } -func failedAdmissionReport(err error) report.Record { - return report.Record{ - SchemaVersion: 1, - ReportKind: reportKind, - ReportID: "proofkit.registry-consumer.invalid-input", - State: "failed", - Summary: map[string]any{ - "admission": "failed", - }, - Diagnostics: []report.Diagnostic{}, - RuleResults: []report.RuleResult{ - { - RuleID: "proofkit.registry-consumer.failure.001", - Status: "failed", - Message: err.Error(), - Diagnostics: []report.Diagnostic{}, - }, - }, - NonClaims: []any{"Invalid registry-consumer input is not proofkit consumption evidence."}, - } -} - func ruleResults(failures []string) []report.RuleResult { if len(failures) == 0 { return []report.RuleResult{ diff --git a/internal/command/registryconsumer/registryconsumer_test.go b/internal/command/registryconsumer/registryconsumer_test.go index 3cc790c..25d28dd 100644 --- a/internal/command/registryconsumer/registryconsumer_test.go +++ b/internal/command/registryconsumer/registryconsumer_test.go @@ -47,16 +47,12 @@ func TestRegistryConsumerRejectsSecretLikeCallerOwnedText(t *testing.T) { input := validRegistryConsumerInput(t) input["input"].(map[string]any)["consumerId"] = secretLike - record, exitCode, err := Build(input) - if err != nil { - t.Fatalf("Build() error = %v", err) + _, exitCode, err := Build(input) + if exitCode != 1 || err == nil || !strings.Contains(err.Error(), "secret-like values") { + t.Fatalf("Build() accepted secret-like caller text: exit=%d error=%v", exitCode, err) } - encoded, _ := json.Marshal(record) - if exitCode == 0 || record.State != "failed" || !strings.Contains(string(encoded), "secret-like values") { - t.Fatalf("Build() accepted secret-like caller text: exit=%d record=%s", exitCode, string(encoded)) - } - if strings.Contains(string(encoded), secretLike) { - t.Fatalf("Build() leaked secret-like caller text: %s", string(encoded)) + if strings.Contains(err.Error(), secretLike) { + t.Fatalf("Build() leaked secret-like caller text: %s", err) } } @@ -187,17 +183,10 @@ func TestRegistryConsumerRejectsLegacyRootImportProof(t *testing.T) { proof := input["proof"].(map[string]any) proof["rootImportOutputSha256"] = sha256Hex() - record, exitCode, err := Build(input) - if err != nil { - t.Fatalf("Build() error = %v", err) - } - if exitCode != 1 { - t.Fatalf("Build() exit=%d, want admission failure", exitCode) - } - if record.ReportID != "proofkit.registry-consumer.invalid-input" { - t.Fatalf("Build() reportID=%s, want invalid input report", record.ReportID) + _, exitCode, err := Build(input) + if exitCode != 1 || err == nil || !strings.Contains(err.Error(), "rootImportOutputSha256") { + t.Fatalf("Build() exit=%d error=%v, want admission failure", exitCode, err) } - assertFailureMessage(t, record.RuleResults, "rootImportOutputSha256") } func anyStrings(values []any) []string { diff --git a/internal/command/registryconsumerinputcompose/registry_consumer_input_compose.go b/internal/command/registryconsumerinputcompose/registry_consumer_input_compose.go index 6f68a79..15ca9b1 100644 --- a/internal/command/registryconsumerinputcompose/registry_consumer_input_compose.go +++ b/internal/command/registryconsumerinputcompose/registry_consumer_input_compose.go @@ -108,7 +108,7 @@ type precondition struct { func Build(raw any) (map[string]any, int, error) { input, err := admitInput(raw) if err != nil { - return invalidInputOutput(err), 1, nil + return nil, 1, err } output, exitCode := buildOutput(input) return output, exitCode, nil @@ -659,28 +659,6 @@ func ruleResults(blockers []string, failures []string) []any { return results } -func invalidInputOutput(err error) map[string]any { - return map[string]any{ - "compositionId": "proofkit.registry-consumer-proof-input-compose.invalid-input", - "compositionKind": compositionKind, - "nonClaims": admit.StringSliceToAny(standardNonClaims), - "registryConsumerInput": nil, - "ruleResults": []any{ - map[string]any{ - "diagnostics": []any{}, - "message": admit.RedactDiagnosticValue(err.Error()), - "ruleId": "proofkit.registry-consumer-proof-input-compose.failure.001", - "status": "failed", - }, - }, - "schemaVersion": 1, - "state": "failed", - "summary": map[string]any{ - "admission": "failed", - }, - } -} - func matchingFailures(failures []string, marker string) []string { result := []string{} for _, failure := range failures { diff --git a/internal/command/registryconsumerinputcompose/registry_consumer_input_compose_test.go b/internal/command/registryconsumerinputcompose/registry_consumer_input_compose_test.go index cd26486..739055c 100644 --- a/internal/command/registryconsumerinputcompose/registry_consumer_input_compose_test.go +++ b/internal/command/registryconsumerinputcompose/registry_consumer_input_compose_test.go @@ -302,17 +302,10 @@ func TestBuildRejectsMalformedSmokeDigestAsInvalidInput(t *testing.T) { input := validComposeInput(t) input["smoke"].(map[string]any)["binarySmokeOutputSha256"] = strings.Repeat("A", 64) - output, exitCode, err := Build(input) - if err != nil { - t.Fatalf("Build() error=%v", err) - } - if exitCode == 0 || output["state"] != "failed" { - t.Fatalf("Build() exit=%d state=%v, want failed", exitCode, output["state"]) - } - if output["registryConsumerInput"] != nil { - t.Fatalf("invalid input output must not emit registryConsumerInput") + _, exitCode, err := Build(input) + if exitCode != 1 || err == nil || !strings.Contains(err.Error(), "must be lowercase sha256 hex") { + t.Fatalf("Build() exit=%d error=%v, want admission failure", exitCode, err) } - assertRuleMessage(t, output, "must be lowercase sha256 hex") } func TestBuildRejectsSecretLikeReleaseAuthorityFreeTextWithoutComposedInput(t *testing.T) { diff --git a/internal/command/repoprofileadmission/repo_profile_admission.go b/internal/command/repoprofileadmission/repo_profile_admission.go index 3601644..f44dfd7 100644 --- a/internal/command/repoprofileadmission/repo_profile_admission.go +++ b/internal/command/repoprofileadmission/repo_profile_admission.go @@ -147,13 +147,7 @@ type admissionResult struct { func Build(raw any) (report.Record, int, error) { result, err := verify(raw) if err != nil { - result = admissionResult{ - CommandMatcherCount: 0, - DistinctWitnessCommandCount: 0, - EnvironmentClassCount: 0, - GeneratedArtifactCount: 0, - Failures: []string{err.Error()}, - } + return report.Record{}, 1, err } record := buildReport(result) if record.State == "passed" { diff --git a/internal/command/repoprofileadmission/repo_profile_admission_test.go b/internal/command/repoprofileadmission/repo_profile_admission_test.go index 74be3ba..03678c5 100644 --- a/internal/command/repoprofileadmission/repo_profile_admission_test.go +++ b/internal/command/repoprofileadmission/repo_profile_admission_test.go @@ -44,14 +44,10 @@ func TestBuildAdmitsOptionalInputSchemaVersionOne(t *testing.T) { input = validRepoProfileInput() input["schemaVersion"] = json.Number("2") - record, exitCode, err = Build(input) - if err != nil { - t.Fatalf("Build() unexpected error = %v", err) - } - if exitCode == 0 || record.State != "failed" { - t.Fatalf("Build() admitted invalid schemaVersion: exit=%d state=%s", exitCode, record.State) + _, exitCode, err = Build(input) + if exitCode != 1 || err == nil || !strings.Contains(err.Error(), "schemaVersion must be 1") { + t.Fatalf("Build() admitted invalid schemaVersion: exit=%d error=%v", exitCode, err) } - assertRecordContains(t, record.JSONValue(), "schemaVersion must be 1") } func TestBuildRejectsUnknownRepoProfileAdmissionFields(t *testing.T) { @@ -89,15 +85,9 @@ func TestBuildRejectsUnknownRepoProfileAdmissionFields(t *testing.T) { t.Run(item.name, func(t *testing.T) { input := validRepoProfileInput() item.mutate(input) - record, exitCode, err := Build(input) - if err != nil { - t.Fatalf("Build() unexpected error = %v", err) - } - if exitCode == 0 || record.State != "failed" { - t.Fatalf("Build() exit=%d state=%s, want failed", exitCode, record.State) - } - if !strings.Contains(record.RuleResults[0].Message, "unsupported field") { - t.Fatalf("failure message=%q, want unsupported field", record.RuleResults[0].Message) + _, exitCode, err := Build(input) + if exitCode != 1 || err == nil || !strings.Contains(err.Error(), "unsupported field") { + t.Fatalf("Build() exit=%d error=%v, want unsupported field", exitCode, err) } }) } @@ -125,19 +115,12 @@ func TestBuildRejectsSchemeAndDriveLikeRepoProfilePaths(t *testing.T) { t.Run(item.name, func(t *testing.T) { input := validRepoProfileInput() item.mutate(input) - record, exitCode, err := Build(input) - if err != nil { - t.Fatalf("Build() unexpected error = %v", err) - } - if exitCode == 0 || record.State != "failed" { - t.Fatalf("Build() exit=%d state=%s, want failed", exitCode, record.State) - } - if !strings.Contains(record.RuleResults[0].Message, "repository-relative POSIX path") { - t.Fatalf("failure message=%q, want path rejection", record.RuleResults[0].Message) + _, exitCode, err := Build(input) + if exitCode != 1 || err == nil || !strings.Contains(err.Error(), "repository-relative POSIX path") { + t.Fatalf("Build() exit=%d error=%v, want path rejection", exitCode, err) } - encoded, _ := json.Marshal(record.JSONValue()) - if strings.Contains(string(encoded), "file:docs/INDEX.md") || strings.Contains(string(encoded), "C:/outside/INDEX.md") { - t.Fatalf("Build() leaked rejected caller path: %s", encoded) + if strings.Contains(err.Error(), "file:docs/INDEX.md") || strings.Contains(err.Error(), "C:/outside/INDEX.md") { + t.Fatalf("Build() leaked rejected caller path: %s", err) } }) } @@ -149,16 +132,12 @@ func TestBuildRedactsCallerPathDiagnostics(t *testing.T) { documents := profile["documents"].(map[string]any) documents["policyPath"] = "docs/sk-proj-abcdefghijklmnop.md" - record, exitCode, err := Build(input) - if err != nil { - t.Fatalf("Build() unexpected error = %v", err) + _, exitCode, err := Build(input) + if exitCode != 1 || err == nil || !strings.Contains(err.Error(), "secret-like values") { + t.Fatalf("Build() exit=%d error=%v, want secret-like rejection", exitCode, err) } - if exitCode == 0 || record.State != "failed" { - t.Fatalf("Build() exit=%d state=%s, want failed", exitCode, record.State) - } - encoded, _ := json.Marshal(record.JSONValue()) - if strings.Contains(string(encoded), "abcdefghijklmnop") { - t.Fatalf("Build() leaked caller path diagnostic: %s", encoded) + if strings.Contains(err.Error(), "abcdefghijklmnop") { + t.Fatalf("Build() leaked caller path diagnostic: %s", err) } } @@ -335,15 +314,9 @@ func TestBuildRejectsShellControlCommandEnvironmentPair(t *testing.T) { map[string]any{"command": "go test ./... && curl example.test", "environmentClasses": []any{"local-go"}}, } - record, exitCode, err := Build(input) - if err != nil { - t.Fatalf("Build() unexpected error = %v", err) - } - if exitCode == 0 || record.State != "failed" { - t.Fatalf("Build() exit=%d state=%s, want failed", exitCode, record.State) - } - if !strings.Contains(record.RuleResults[0].Message, "display-only command text") { - t.Fatalf("failure message=%q, want display-only command rejection", record.RuleResults[0].Message) + _, exitCode, err := Build(input) + if exitCode != 1 || err == nil || !strings.Contains(err.Error(), "display-only command text") { + t.Fatalf("Build() exit=%d error=%v, want display-only command rejection", exitCode, err) } } diff --git a/internal/command/requirementbinding/requirementbinding.go b/internal/command/requirementbinding/requirementbinding.go index c05f181..8b5c047 100644 --- a/internal/command/requirementbinding/requirementbinding.go +++ b/internal/command/requirementbinding/requirementbinding.go @@ -61,10 +61,16 @@ type Binding struct { WitnessID string WitnessKind string WitnessPath string + WitnessSelectors []WitnessSelector CommandIDs []string EnvironmentClasses []string } +type WitnessSelector struct { + Command string + Selector string +} + type WitnessCommand struct { CommandID string Command string @@ -245,7 +251,7 @@ func InputValue(input Input) map[string]any { } bindings := make([]any, 0, len(input.Bindings)) for _, binding := range input.Bindings { - bindings = append(bindings, map[string]any{ + value := map[string]any{ "commandIds": admit.StringSliceToAny(binding.CommandIDs), "environmentClasses": admit.StringSliceToAny(binding.EnvironmentClasses), "requirementId": binding.RequirementID, @@ -253,7 +259,11 @@ func InputValue(input Input) map[string]any { "witnessId": binding.WitnessID, "witnessKind": binding.WitnessKind, "witnessPath": binding.WitnessPath, - }) + } + if len(binding.WitnessSelectors) > 0 { + value["witnessSelectors"] = witnessSelectorValues(binding.WitnessSelectors) + } + bindings = append(bindings, value) } witnessCommands := make([]any, 0, len(input.WitnessCommands)) for _, command := range input.WitnessCommands { @@ -550,7 +560,7 @@ func admitBinding(raw any) (Binding, error) { if !ok { return Binding{}, fmt.Errorf("proof binding record must be an object") } - if err := admit.KnownKeys(record, []string{"commandIds", "environmentClasses", "requirementId", "scenarioId", "witnessId", "witnessKind", "witnessPath"}, "proof binding record"); err != nil { + if err := admit.KnownKeys(record, []string{"commandIds", "environmentClasses", "requirementId", "scenarioId", "witnessId", "witnessKind", "witnessPath", "witnessSelectors"}, "proof binding record"); err != nil { return Binding{}, err } requirementID, err := admit.RuleID(record["requirementId"], "binding requirementId") @@ -585,17 +595,74 @@ func admitBinding(raw any) (Binding, error) { if err != nil { return Binding{}, err } + witnessSelectors, err := admitWitnessSelectors(record["witnessSelectors"]) + if err != nil { + return Binding{}, err + } return Binding{ RequirementID: requirementID, ScenarioID: scenarioID, WitnessID: witnessID, WitnessKind: witnessKind, WitnessPath: witnessPath, + WitnessSelectors: witnessSelectors, CommandIDs: commandIDs, EnvironmentClasses: environmentClasses, }, nil } +func admitWitnessSelectors(raw any) ([]WitnessSelector, error) { + if raw == nil { + return []WitnessSelector{}, nil + } + values, ok := raw.([]any) + if !ok || len(values) == 0 { + return nil, fmt.Errorf("witnessSelectors must be a non-empty array when present") + } + selectors := make([]WitnessSelector, 0, len(values)) + seen := map[string]struct{}{} + for index, rawSelector := range values { + record, ok := rawSelector.(map[string]any) + if !ok { + return nil, fmt.Errorf("witnessSelectors[%d] must be an object", index) + } + if err := admit.KnownKeys(record, []string{"command", "selector"}, "witness selector"); err != nil { + return nil, err + } + selector, err := admit.NonEmptyText(record["selector"], "witness selector selector") + if err != nil { + return nil, err + } + if strings.ContainsAny(selector, " \t\r\n") { + return nil, fmt.Errorf("witness selector selector must not contain whitespace") + } + if _, duplicate := seen[selector]; duplicate { + return nil, fmt.Errorf("witness selector selectors must be unique") + } + seen[selector] = struct{}{} + command, err := admit.DisplayOnlyCommandText(record["command"], "witness selector command") + if err != nil { + return nil, err + } + if !strings.Contains(command, selector) { + return nil, fmt.Errorf("witness selector command must contain its selector") + } + selectors = append(selectors, WitnessSelector{Command: command, Selector: selector}) + } + sort.Slice(selectors, func(left, right int) bool { + return selectors[left].Selector < selectors[right].Selector + }) + return selectors, nil +} + +func witnessSelectorValues(selectors []WitnessSelector) []any { + values := make([]any, 0, len(selectors)) + for _, selector := range selectors { + values = append(values, map[string]any{"command": selector.Command, "selector": selector.Selector}) + } + return values +} + func admitWitnessCommands(raw any) ([]WitnessCommand, error) { values, err := array(raw, "witnessCommands") if err != nil { @@ -750,14 +817,18 @@ func buildGraph(input Input) map[string]any { for _, requirement := range input.Requirements { scenarios := make([]any, 0, len(bindingsByRequirement[requirement.RequirementID])) for _, binding := range bindingsByRequirement[requirement.RequirementID] { - scenarios = append(scenarios, map[string]any{ + scenario := map[string]any{ "commandIds": admit.StringSliceToAny(binding.CommandIDs), "environmentClasses": admit.StringSliceToAny(binding.EnvironmentClasses), "scenarioId": binding.ScenarioID, "witnessId": binding.WitnessID, "witnessKind": binding.WitnessKind, "witnessPath": binding.WitnessPath, - }) + } + if len(binding.WitnessSelectors) > 0 { + scenario["witnessSelectors"] = witnessSelectorValues(binding.WitnessSelectors) + } + scenarios = append(scenarios, scenario) } requirements = append(requirements, map[string]any{ "claimLevel": requirement.ClaimLevel, diff --git a/internal/command/requirementbrowser/assets/workspace.css b/internal/command/requirementbrowser/assets/workspace.css index c410b74..4737e66 100644 --- a/internal/command/requirementbrowser/assets/workspace.css +++ b/internal/command/requirementbrowser/assets/workspace.css @@ -1,23 +1,256 @@ -:root { color-scheme: light dark; font-family: ui-sans-serif, system-ui, sans-serif; } -body { margin: 0; display: grid; grid-template-columns: minmax(0, 1fr) 22rem; min-height: 100vh; } -header, main, aside { padding: 1rem 1.5rem; } -header { grid-column: 1 / -1; border-bottom: 1px solid CanvasText; } -header p { margin: 0; font-size: .8rem; text-transform: uppercase; } -h1 { margin: .25rem 0; } -nav { display: flex; gap: .5rem; margin-bottom: 1rem; } -button { min-height: 2.5rem; } -article { border-top: 1px solid color-mix(in srgb, CanvasText 25%, transparent); padding: .75rem 0; } -aside { border-left: 1px solid CanvasText; } -textarea { box-sizing: border-box; min-height: 8rem; width: 100%; } -#selected-context { padding-left: 1.25rem; } -#selected-context li { overflow-wrap: anywhere; } -pre { max-width: 100%; overflow: auto; white-space: pre-wrap; } -svg { display: block; min-width: 50rem; width: 100%; } -.graph-viewport { border: 1px solid color-mix(in srgb, CanvasText 35%, transparent); max-height: 42rem; overflow: auto; } -[data-select-anchor][aria-pressed="true"] { outline: 3px solid #1f6feb; outline-offset: 2px; } -svg rect { fill: Canvas; stroke: CanvasText; } -svg line { stroke: CanvasText; stroke-width: 1.5; } -svg text { fill: CanvasText; font-size: 13px; } -table { border-collapse: collapse; width: 100%; } -th, td { border: 1px solid color-mix(in srgb, CanvasText 35%, transparent); padding: .4rem; text-align: left; } -@media (max-width: 48rem) { body { display: block; } aside { border-left: 0; border-top: 1px solid CanvasText; } } +:root { + color-scheme: light dark; + font-family: ui-sans-serif, system-ui, sans-serif; + --canvas: #ffffff; + --surface: #f2f4f7; + --active-surface: #d9e8ff; + --text: #172033; + --muted-text: #344054; + --border: #667085; + --focus: #005fcc; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + background: var(--canvas); + color: var(--text); +} + +body { + background: var(--canvas); + color: var(--text); + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(16rem, 22rem); + margin: 0; + min-height: 100vh; +} + +header, +main, +aside { + max-width: 100%; + min-width: 0; + padding: 1rem 1.5rem; +} + +header { + border-bottom: 2px solid var(--border); + grid-column: 1 / -1; +} + +header p { + font-size: .8rem; + margin: 0; + text-transform: uppercase; +} + +h1 { + margin: .25rem 0; +} + +h1, +h2, +h3, +p, +li, +td, +th, +label, +button { + overflow-wrap: anywhere; +} + +nav { + display: flex; + flex-wrap: wrap; + gap: .5rem; + margin-bottom: 1rem; + max-width: 100%; + min-width: 0; +} + +button, +textarea { + background: var(--surface); + border: 2px solid var(--border); + color: var(--text); + font: inherit; +} + +button { + min-block-size: 2.75rem; + min-inline-size: 2.75rem; + padding: .5rem .75rem; +} + +button:disabled { + color: var(--muted-text); + opacity: 1; +} + +button[aria-current="page"] { + background: var(--active-surface); + font-weight: 700; +} + +button:focus-visible, +textarea:focus-visible, +[tabindex="0"]:focus-visible { + outline: 3px solid var(--focus); + outline-offset: 2px; +} + +article { + border-top: 2px solid var(--border); + min-width: 0; + padding: .75rem 0; +} + +aside { + border-left: 2px solid var(--border); +} + +textarea { + display: block; + min-height: 8rem; + width: 100%; +} + +#workspace-content, +#workspace-authority, +#handoff-output, +#selected-context, +[aria-label="Specification requirements"] { + max-width: 100%; + min-width: 0; +} + +#selected-context { + padding-left: 1.25rem; +} + +pre { + margin: 0; + max-width: 100%; + overflow-wrap: anywhere; + white-space: pre-wrap; +} + +svg { + display: block; + max-width: none; + min-width: 50rem; + width: 100%; +} + +.graph-viewport, +.table-viewport { + border: 2px solid var(--border); + max-width: 100%; + min-width: 0; + overflow: auto; +} + +.graph-viewport { + max-height: 42rem; +} + +.table-viewport { + margin-block: 1rem; +} + +[data-select-anchor][aria-pressed="true"] { + outline: 3px solid var(--focus); + outline-offset: 2px; +} + +svg rect { + fill: var(--surface); + stroke: var(--border); +} + +svg line { + stroke: var(--text); + stroke-width: 1.5; +} + +svg text { + fill: var(--text); + font-size: 13px; +} + +table { + border-collapse: collapse; + min-width: 100%; + width: max-content; +} + +th, +td { + border: 2px solid var(--border); + max-width: 24rem; + padding: .5rem; + text-align: left; +} + +@media (prefers-color-scheme: dark) { + :root { + --canvas: #101828; + --surface: #1d2939; + --active-surface: #344054; + --text: #f2f4f7; + --muted-text: #d0d5dd; + --border: #98a2b3; + --focus: #84adff; + } +} + +@media (max-width: 48rem) { + body { + display: block; + } + + aside { + border-left: 0; + border-top: 2px solid var(--border); + } +} + +@media (forced-colors: active) { + header, + aside, + article, + button, + textarea, + .graph-viewport, + .table-viewport, + th, + td { + border-color: CanvasText; + } + + button:focus-visible, + textarea:focus-visible, + [tabindex="0"]:focus-visible, + [data-select-anchor][aria-pressed="true"] { + outline-color: Highlight; + } + + svg rect { + fill: Canvas; + stroke: CanvasText; + } + + svg line { + stroke: CanvasText; + } + + svg text { + fill: CanvasText; + } +} diff --git a/internal/command/requirementbrowser/assets/workspace.js b/internal/command/requirementbrowser/assets/workspace.js index 5148840..b35dcde 100644 --- a/internal/command/requirementbrowser/assets/workspace.js +++ b/internal/command/requirementbrowser/assets/workspace.js @@ -16,14 +16,15 @@ const contentElement = document.querySelector("#workspace-content"); if (!(contentElement instanceof HTMLElement)) throw new Error("Missing workspace content region"); const content = /** @type {HTMLElement} */ (contentElement); -const manifest = await fetchJSON("/api/v1/manifest", {headers: {"X-Proofkit-Browser-Capability": capability}}); const authorityElement = document.querySelector("#workspace-authority"); if (!(authorityElement instanceof HTMLElement)) throw new Error("Missing workspace authority boundary"); const authorityText = authorityElement.querySelector("[data-authority]"); const authorityNonClaims = authorityElement.querySelector("[data-non-claims]"); if (!(authorityText instanceof HTMLElement) || !(authorityNonClaims instanceof HTMLUListElement)) throw new Error("Missing workspace authority fields"); -authorityText.textContent = `Authority: ${manifest.authority}. Snapshot: ${manifest.snapshotId}. Baseline: ${manifest.baselineVerification}.`; -appendTextItems(authorityNonClaims, manifest.nonClaims ?? []); +const authorityTextView = /** @type {HTMLElement} */ (authorityText); +const authorityNonClaimsView = /** @type {HTMLUListElement} */ (authorityNonClaims); +/** @type {any} */ +let manifest = null; let activeRequestId = ""; let requestSequence = 0; /** @type {AbortController | null} */ @@ -44,18 +45,65 @@ async function fetchJSON(path, init) { return response.json(); } +/** @param {string} state */ +function setWorkspaceState(state) { + document.body.dataset.state = state; +} + +/** @param {"specifications" | "diff" | "graph"} activeView */ +function setActiveView(activeView) { + for (const control of document.querySelectorAll("[data-view]")) { + if (!(control instanceof HTMLButtonElement)) continue; + if (control.dataset.view === activeView) { + control.setAttribute("aria-current", "page"); + } else { + control.removeAttribute("aria-current"); + } + } +} + +function enableViewControls() { + for (const control of document.querySelectorAll("[data-view]")) { + if (control instanceof HTMLButtonElement) control.disabled = false; + } +} + +async function initializeWorkspace() { + try { + manifest = await fetchJSON("/api/v1/manifest", {headers: {"X-Proofkit-Browser-Capability": capability}}); + authorityTextView.textContent = `Authority: ${manifest.authority}. Snapshot: ${manifest.snapshotId}. Expected-digest coverage: ${manifest.expectedDigestCoverage}.`; + appendTextItems(authorityNonClaimsView, manifest.nonClaims ?? []); + enableViewControls(); + await renderSpecifications(); + } catch { + activeViewController?.abort(); + content.replaceChildren(); + content.setAttribute("aria-busy", "false"); + const heading = document.createElement("h2"); + heading.textContent = "Workspace unavailable"; + const alert = document.createElement("p"); + alert.setAttribute("role", "alert"); + alert.textContent = "The admitted workspace could not be loaded."; + content.append(heading, alert); + setWorkspaceState("bootstrap-failed"); + } +} + /** @param {string} path @param {any} body @param {AbortSignal} [signal] @returns {Promise} */ async function post(path, body, signal) { return fetchJSON(path, {method: "POST", headers, body: JSON.stringify(body), signal}); } -/** @param {string} title @param {string} requestPrefix */ -function beginView(title, requestPrefix) { +/** @param {string} title @param {string} requestPrefix @param {"specifications" | "diff" | "graph"} view */ +function beginView(title, requestPrefix, view) { activeViewController?.abort(); activeViewController = new AbortController(); const requestId = nextRequestId(requestPrefix); clearSelection(); + setActiveView(view); + setWorkspaceState(`${view}-loading`); content.replaceChildren(); + content.setAttribute("aria-busy", "true"); const heading = document.createElement("h2"); heading.textContent = title; const status = document.createElement("p"); @@ -67,32 +115,43 @@ function beginView(title, requestPrefix) { return {requestId, signal: activeViewController.signal, status}; } -/** @param {HTMLElement} status @param {unknown} error */ -function failView(status, error) { +/** @param {HTMLElement} status */ +function failView(status) { + content.setAttribute("aria-busy", "false"); + setWorkspaceState("view-failed"); + status.setAttribute("role", "alert"); + status.setAttribute("aria-live", "assertive"); status.dataset.state = "failed"; - status.textContent = error instanceof Error ? error.message : "Workspace request failed."; + status.textContent = "The admitted workspace view could not be loaded."; +} + +/** @param {any} response @param {string} requestId @param {AbortSignal} signal */ +function admitCurrentViewResponse(response, requestId, signal) { + if (signal.aborted || requestId !== activeRequestId) return false; + if (response?.requestId !== requestId || response?.snapshotId !== manifest.snapshotId) { + throw new Error("Workspace view response identity mismatch"); + } + return true; } /** @param {number} [offset] */ async function renderSpecifications(offset = 0) { - const {requestId, signal, status} = beginView("Specifications", "browser.specifications"); + const {requestId, signal, status} = beginView("Specifications", "browser.specifications", "specifications"); try { const response = await post("/api/v1/requirements", { requestId, snapshotId: manifest.snapshotId, query: {maxRecords: 256, offset}, }, signal); - if (signal.aborted || requestId !== activeRequestId || response.requestId !== requestId || response.snapshotId !== manifest.snapshotId) return; + if (!admitCurrentViewResponse(response, requestId, signal)) return; status.remove(); - const tree = document.createElement("div"); - tree.setAttribute("role", "tree"); - tree.setAttribute("aria-label", "Specification requirements"); + const list = document.createElement("ul"); + list.setAttribute("aria-label", "Specification requirements"); const requirements = response.projection?.requirements ?? []; let itemIndex = 0; for (const requirement of requirements) { + const item = document.createElement("li"); const article = document.createElement("article"); - article.setAttribute("role", "treeitem"); - article.tabIndex = itemIndex === 0 ? 0 : -1; article.dataset.requirementId = requirement.requirementId; const title = document.createElement("h3"); title.textContent = requirement.requirementId; @@ -120,56 +179,45 @@ async function renderSpecifications(offset = 0) { announceSelection(); }); article.append(title, boundary, invariant, choose); - tree.append(article); + item.append(article); + list.append(item); itemIndex += 1; } if (itemIndex === 0) { status.dataset.state = "no-match"; status.textContent = "No requirements matched the admitted query."; content.append(status); + content.setAttribute("aria-busy", "false"); + setWorkspaceState("specifications"); return; } - tree.addEventListener("keydown", moveTreeFocus); - content.append(tree); + content.append(list); appendPagingControls("specifications", offset, response.projection.selectedRequirementCount ?? 0, response.projection.availableRequirementCount ?? 0); + content.setAttribute("aria-busy", "false"); + setWorkspaceState("specifications"); } catch (error) { if (signal.aborted) return; - failView(status, error); + failView(status); } } -/** @param {KeyboardEvent} event */ -function moveTreeFocus(event) { - if (!["ArrowDown", "ArrowUp"].includes(event.key)) return; - const items = /** @type {HTMLElement[]} */ ([...content.querySelectorAll('[role="treeitem"]')]); - const active = document.activeElement; - if (!(active instanceof HTMLElement)) return; - const current = items.indexOf(active); - if (current < 0) return; - event.preventDefault(); - const direction = event.key === "ArrowDown" ? 1 : -1; - const next = Math.max(0, Math.min(items.length - 1, current + direction)); - const nextItem = items[next]; - if (!nextItem) return; - for (const item of items) item.tabIndex = item === nextItem ? 0 : -1; - nextItem.focus(); -} - /** @param {number} [offset] */ async function renderDiff(offset = 0) { - const {requestId, signal, status} = beginView("Semantic diff", "browser.diff"); + const {requestId, signal, status} = beginView("Semantic diff", "browser.diff", "diff"); if (!manifest.diffAvailable) { + content.setAttribute("aria-busy", "false"); + setWorkspaceState("diff-unavailable"); status.dataset.state = "unavailable"; status.textContent = "No admitted semantic diff was supplied."; return; } try { const response = await post("/api/v1/diff", {requestId, snapshotId: manifest.snapshotId, query: {maxRecords: 512, offset}}, signal); - if (signal.aborted || requestId !== activeRequestId || response.requestId !== requestId || response.snapshotId !== manifest.snapshotId) return; + if (!admitCurrentViewResponse(response, requestId, signal)) return; status.remove(); appendProjectionBoundary(response.projection.authority, response.projection.nonClaims ?? [], [ - `Base snapshot: ${response.projection.baseSnapshotId} (${response.projection.baseBaselineVerification}).`, - `Current snapshot: ${response.projection.currentSnapshotId} (${response.projection.currentBaselineVerification}).`, + `Base snapshot: ${response.projection.baseSnapshotId} (expected-digest coverage: ${response.projection.baseExpectedDigestCoverage}).`, + `Current snapshot: ${response.projection.currentSnapshotId} (expected-digest coverage: ${response.projection.currentExpectedDigestCoverage}).`, ]); for (const change of response.projection.changes ?? []) { const article = document.createElement("article"); @@ -186,23 +234,27 @@ async function renderDiff(offset = 0) { content.append(article); } appendPagingControls("diff", offset, response.projection.selectedChangeCount ?? 0, response.projection.availableChangeCount ?? 0); + content.setAttribute("aria-busy", "false"); + setWorkspaceState("diff"); } catch (error) { if (signal.aborted) return; - failView(status, error); + failView(status); } } /** @param {number} [offset] */ async function renderGraph(offset = 0, edgeOffset = 0) { - const {requestId, signal, status} = beginView("Traceability graph", "browser.graph"); + const {requestId, signal, status} = beginView("Traceability graph", "browser.graph", "graph"); if (!manifest.graphAvailable) { + content.setAttribute("aria-busy", "false"); + setWorkspaceState("graph-unavailable"); status.dataset.state = "unavailable"; status.textContent = "No admitted traceability graph was supplied."; return; } try { const response = await post("/api/v1/graph", {requestId, snapshotId: manifest.snapshotId, query: {edgeOffset, maxEdges: 2048, maxRecords: 256, offset}}, signal); - if (signal.aborted || requestId !== activeRequestId || response.requestId !== requestId || response.snapshotId !== manifest.snapshotId) return; + if (!admitCurrentViewResponse(response, requestId, signal)) return; status.remove(); const graph = response.projection; appendProjectionBoundary(graph.authority, graph.nonClaims ?? [], [`Source snapshot: ${graph.sourceSnapshotId}.`]); @@ -250,6 +302,9 @@ async function renderGraph(offset = 0, edgeOffset = 0) { } const viewport = document.createElement("div"); viewport.className = "graph-viewport"; + viewport.setAttribute("role", "region"); + viewport.setAttribute("aria-label", "Traceability graph viewport"); + viewport.tabIndex = 0; viewport.append(svg); content.append(viewport, graphTable( "Admitted traceability nodes", @@ -265,9 +320,11 @@ async function renderGraph(offset = 0, edgeOffset = 0) { )); appendPagingControls("graph", offset, graph.primaryNodeCount ?? 0, graph.availableNodeCount ?? 0); appendGraphEdgeControls(offset, edgeOffset, graph.selectedEdgeCount ?? 0, graph.availableIncidentEdgeCount ?? 0); + content.setAttribute("aria-busy", "false"); + setWorkspaceState("graph"); } catch (error) { if (signal.aborted) return; - failView(status, error); + failView(status); } } @@ -326,6 +383,11 @@ function appendGraphEdgeControls(nodeOffset, edgeOffset, selectedCount, availabl /** @param {string} captionText @param {string[]} headings @param {string[][]} rows @param {string} identityKind */ function graphTable(captionText, headings, rows, identityKind) { + const viewport = document.createElement("div"); + viewport.className = "table-viewport"; + viewport.setAttribute("role", "region"); + viewport.setAttribute("aria-label", `${captionText} table viewport`); + viewport.tabIndex = 0; const table = document.createElement("table"); table.dataset.identityKind = identityKind; const caption = document.createElement("caption"); @@ -350,7 +412,8 @@ function graphTable(captionText, headings, rows, identityKind) { body.append(row); } table.append(caption, head, body); - return table; + viewport.append(table); + return viewport; } /** @param {unknown} value */ @@ -481,21 +544,31 @@ submit.addEventListener("click", async () => { if (submit.disabled) return; const question = questionInput.value.trim(); if (selectionState.targets.length === 0 || !question) { + status.setAttribute("role", "status"); + status.setAttribute("aria-live", "polite"); status.textContent = "Select invariant text and enter a question."; return; } + const submissionViewRequestId = activeRequestId; submit.disabled = true; + status.setAttribute("role", "status"); + status.setAttribute("aria-live", "polite"); status.textContent = "Creating handoff packet..."; try { const packet = await post("/api/v1/handoff", {annotations: selectionState.targets.map((target) => ({...target, question}))}); packetView.textContent = JSON.stringify(packet, null, 2); status.textContent = "Handoff packet created."; - } catch (error) { - status.textContent = error instanceof Error ? error.message : "Handoff was rejected."; + if (submissionViewRequestId === activeRequestId) setWorkspaceState("handoff-result"); + } catch { + packetView.replaceChildren(); + status.setAttribute("role", "alert"); + status.setAttribute("aria-live", "assertive"); + status.textContent = "The handoff packet could not be created."; + if (submissionViewRequestId === activeRequestId) setWorkspaceState("handoff-failed"); } finally { submit.disabled = false; } }); announceSelection(); -void renderSpecifications(); +void initializeWorkspace(); diff --git a/internal/command/requirementbrowser/handoff_bounds_test.go b/internal/command/requirementbrowser/handoff_bounds_test.go index 94634d3..15773d3 100644 --- a/internal/command/requirementbrowser/handoff_bounds_test.go +++ b/internal/command/requirementbrowser/handoff_bounds_test.go @@ -64,7 +64,7 @@ func TestHandoffDerivedContextByteBoundary(t *testing.T) { } func TestHandoffFinalPacketByteBoundaryIsReachable(t *testing.T) { - const invariantBytes = maxHandoffContextBytes - 2532 + const invariantBytes = maxHandoffContextBytes - 2800 const quoteBytes = 16204 session := workspaceSessionForInvariant(t, strings.Repeat("z", invariantBytes)) anchor := session.Anchors["requirement:REQ-CONSUMER-001:invariant"] diff --git a/internal/command/requirementbrowser/http_handler.go b/internal/command/requirementbrowser/http_handler.go index d58ca29..86d17ac 100644 --- a/internal/command/requirementbrowser/http_handler.go +++ b/internal/command/requirementbrowser/http_handler.go @@ -116,7 +116,7 @@ func serveWorkspaceRequirements(response http.ResponseWriter, request *http.Requ return } projection, state := requirementWindow(session.Requirements, query) - serveWorkspaceJSON(response, request.Method, map[string]any{"projection": projection, "requestId": requestID, "schemaVersion": json.Number("1"), "snapshotId": session.SnapshotID, "state": state}) + serveWorkspaceJSON(response, request.Method, map[string]any{"projection": projection, "requestId": requestID, "schemaVersion": json.Number("2"), "snapshotId": session.SnapshotID, "state": state}) } func requirementWindow(requirements []any, query projectionQuery) (map[string]any, string) { @@ -165,7 +165,7 @@ func serveWorkspaceQuery(response http.ResponseWriter, request *http.Request, ex serveWorkspaceJSON(response, request.Method, map[string]any{ "queryId": queryID, "requestId": requestID, - "schemaVersion": json.Number("1"), + "schemaVersion": json.Number("2"), "slice": slice, "snapshotId": session.SnapshotID, "state": slice["state"], @@ -204,7 +204,7 @@ func serveWorkspaceProjection(response http.ResponseWriter, request *http.Reques } else { projection, state = graphWindow(projection, query) } - serveWorkspaceJSON(response, request.Method, map[string]any{"projection": projection, "requestId": requestID, "schemaVersion": json.Number("1"), "snapshotId": session.SnapshotID, "state": state}) + serveWorkspaceJSON(response, request.Method, map[string]any{"projection": projection, "requestId": requestID, "schemaVersion": json.Number("2"), "snapshotId": session.SnapshotID, "state": state}) } type projectionQuery struct { @@ -264,18 +264,18 @@ func diffWindow(full map[string]any, query projectionQuery) (map[string]any, str state = "partial_with_omissions" } return map[string]any{ - "authority": "lookup_fragment_only", - "availableChangeCount": len(changes), - "baseBaselineVerification": full["baseBaselineVerification"], - "baseSnapshotId": full["baseSnapshotId"], - "changes": selected, - "currentBaselineVerification": full["currentBaselineVerification"], - "currentSnapshotId": full["currentSnapshotId"], - "nonClaims": full["nonClaims"], - "omittedChangeCount": len(changes) - len(selected), - "projectionKind": "proofkit.requirement-semantic-diff-fragment", - "selectedChangeCount": len(selected), - "sourceDiffId": full["diffId"], + "authority": "lookup_fragment_only", + "availableChangeCount": len(changes), + "baseExpectedDigestCoverage": full["baseExpectedDigestCoverage"], + "baseSnapshotId": full["baseSnapshotId"], + "changes": selected, + "currentExpectedDigestCoverage": full["currentExpectedDigestCoverage"], + "currentSnapshotId": full["currentSnapshotId"], + "nonClaims": full["nonClaims"], + "omittedChangeCount": len(changes) - len(selected), + "projectionKind": "proofkit.requirement-semantic-diff-fragment", + "selectedChangeCount": len(selected), + "sourceDiffId": full["diffId"], }, state } diff --git a/internal/command/requirementbrowser/requirementbrowser.go b/internal/command/requirementbrowser/requirementbrowser.go index 301c207..b382651 100644 --- a/internal/command/requirementbrowser/requirementbrowser.go +++ b/internal/command/requirementbrowser/requirementbrowser.go @@ -127,7 +127,7 @@ func render(raw any, options Options) (renderedView, error) { }, nil } if options.View != "proof" { - return renderedView{}, fmt.Errorf("requirement-browser-server requires --view source, proof, coverage, or spec-tree") + return renderedView{}, fmt.Errorf("requirement-browser-server requires --view source, proof, coverage, spec-tree, or workspace") } compact := requirementproofview.IsCompact(raw) if compact && len(options.LocalEnvironmentClasses) == 0 && !options.EmptyLocalEnvironmentPolicy { diff --git a/internal/command/requirementbrowser/requirementbrowser_test.go b/internal/command/requirementbrowser/requirementbrowser_test.go new file mode 100644 index 0000000..4e718e0 --- /dev/null +++ b/internal/command/requirementbrowser/requirementbrowser_test.go @@ -0,0 +1,19 @@ +package requirementbrowser + +import ( + "strings" + "testing" +) + +func TestInvalidViewListsEverySupportedView(t *testing.T) { + _, _, err := BuildPlan(map[string]any{}, Options{Host: "127.0.0.1", PortSet: true, View: "invalid"}) + if err == nil { + t.Fatal("BuildPlan accepted an invalid view") + } + message := err.Error() + for _, view := range []string{"source", "proof", "coverage", "spec-tree", "workspace"} { + if strings.Count(message, view) != 1 { + t.Fatalf("invalid-view diagnostic count for %q=%d, want 1: %s", view, strings.Count(message, view), message) + } + } +} diff --git a/internal/command/requirementbrowser/server.go b/internal/command/requirementbrowser/server.go index 14d3651..b0a70ad 100644 --- a/internal/command/requirementbrowser/server.go +++ b/internal/command/requirementbrowser/server.go @@ -8,6 +8,7 @@ import ( "io" "net" "net/http" + "net/url" "os/exec" "runtime" "strconv" @@ -291,19 +292,40 @@ func waitServerDone(ctx context.Context, handle ServerHandle) error { } func openBrowser(ctx context.Context, url string) error { + return openBrowserWithLauncher(ctx, runtime.GOOS, url, startBrowserProcess) +} + +type browserLaunchOperation func(context.Context, string, ...string) error + +func openBrowserWithLauncher(ctx context.Context, goos, rawURL string, launch browserLaunchOperation) error { + parsed, err := url.Parse(rawURL) + if err != nil || + parsed.Scheme != "http" || + (parsed.Hostname() != "127.0.0.1" && parsed.Hostname() != "::1") || + parsed.Port() == "" || + parsed.User != nil || + parsed.RawQuery != "" || + parsed.Fragment != "" || + parsed.Path != "/" { + return fmt.Errorf("open browser requires a server-generated loopback URL") + } var command string var args []string - switch runtime.GOOS { + switch goos { case "darwin": command = "open" - args = []string{url} + args = []string{rawURL} case "windows": command = "cmd" - args = []string{"/c", "start", "", url} + args = []string{"/c", "start", "", rawURL} default: command = "xdg-open" - args = []string{url} + args = []string{rawURL} } + return launch(ctx, command, args...) +} + +func startBrowserProcess(ctx context.Context, command string, args ...string) error { process := exec.CommandContext(ctx, command, args...) if err := process.Start(); err != nil { return fmt.Errorf("open browser: %w", err) diff --git a/internal/command/requirementbrowser/server_test.go b/internal/command/requirementbrowser/server_test.go index 2090d38..00ff606 100644 --- a/internal/command/requirementbrowser/server_test.go +++ b/internal/command/requirementbrowser/server_test.go @@ -9,6 +9,7 @@ import ( "net" "net/http" "path/filepath" + "slices" "strconv" "strings" "testing" @@ -130,6 +131,52 @@ func TestStartServerServesExplicitSourceViews(t *testing.T) { } } +func TestOpenBrowserUsesFixedLauncherAndLoopbackURL(t *testing.T) { + const loopbackURL = "http://127.0.0.1:43127/" + cases := []struct { + goos string + wantCommand string + wantArgs []string + }{ + {goos: "darwin", wantCommand: "open", wantArgs: []string{loopbackURL}}, + {goos: "linux", wantCommand: "xdg-open", wantArgs: []string{loopbackURL}}, + {goos: "windows", wantCommand: "cmd", wantArgs: []string{"/c", "start", "", loopbackURL}}, + } + for _, test := range cases { + t.Run(test.goos, func(t *testing.T) { + calls := 0 + err := openBrowserWithLauncher(t.Context(), test.goos, loopbackURL, func(_ context.Context, command string, args ...string) error { + calls++ + if command != test.wantCommand || !slices.Equal(args, test.wantArgs) { + t.Fatalf("launcher command=%q args=%v, want %q %v", command, args, test.wantCommand, test.wantArgs) + } + return nil + }) + if err != nil || calls != 1 { + t.Fatalf("openBrowserWithLauncher() error=%v calls=%d", err, calls) + } + }) + } + for _, unsafe := range []string{ + "https://127.0.0.1:43127/", + "http://attacker.invalid:43127/", + "http://127.0.0.1:43127/?target=attacker", + "http://127.0.0.1:43127/#fragment", + "http://127.0.0.1:43127/other", + } { + t.Run(unsafe, func(t *testing.T) { + called := false + err := openBrowserWithLauncher(t.Context(), "linux", unsafe, func(context.Context, string, ...string) error { + called = true + return nil + }) + if err == nil || called { + t.Fatalf("unsafe URL error=%v launcherCalled=%v", err, called) + } + }) + } +} + func TestStartServerServesExplicitProofViews(t *testing.T) { handle, err := StartServer(proofInput(t), Options{ Host: "127.0.0.1", diff --git a/internal/command/requirementbrowser/v1_adapter.go b/internal/command/requirementbrowser/v1_adapter.go new file mode 100644 index 0000000..c6e3534 --- /dev/null +++ b/internal/command/requirementbrowser/v1_adapter.go @@ -0,0 +1,5 @@ +package requirementbrowser + +func admitV1WorkspaceInput(record map[string]any) error { + return requireWorkspaceNestedVersions(record, 1) +} diff --git a/internal/command/requirementbrowser/workspace.go b/internal/command/requirementbrowser/workspace.go index a30ea77..b2bf915 100644 --- a/internal/command/requirementbrowser/workspace.go +++ b/internal/command/requirementbrowser/workspace.go @@ -41,8 +41,8 @@ func buildWorkspace(raw any) (workspaceSession, string, error) { if err := admit.KnownKeys(record, []string{"context", "diffInput", "graphInput", "schemaVersion", "workspaceId"}, "requirement browser workspace input"); err != nil { return workspaceSession{}, "", err } - if !admit.JSONNumberEquals(record["schemaVersion"], 1) { - return workspaceSession{}, "", fmt.Errorf("requirement browser workspace schemaVersion must be 1") + if err := admitWorkspaceInputVersion(record); err != nil { + return workspaceSession{}, "", err } workspaceID, err := admit.RuleID(record["workspaceId"], "requirement browser workspaceId") if err != nil { @@ -88,20 +88,55 @@ func buildWorkspace(raw any) (workspaceSession, string, error) { } } manifest := map[string]any{ - "authority": "presentation_adapter", - "availableViews": []any{"specifications", "diff", "graph"}, - "baselineVerification": snapshot.BaselineVerification, - "diffAvailable": diff != nil, - "graphAvailable": graph != nil, - "nonClaims": admit.StringSliceToAny(serverNonClaims), - "requirementCount": len(requirements), - "schemaVersion": json.Number("1"), - "snapshotId": snapshot.SnapshotID, - "workspaceId": workspaceID, + "authority": "presentation_adapter", + "availableViews": []any{"specifications", "diff", "graph"}, + "diffAvailable": diff != nil, + "expectedDigestCoverage": snapshot.ExpectedDigestCoverage, + "graphAvailable": graph != nil, + "nonClaims": admit.StringSliceToAny(serverNonClaims), + "requirementCount": len(requirements), + "schemaVersion": json.Number("2"), + "snapshotId": snapshot.SnapshotID, + "workspaceId": workspaceID, } return workspaceSession{Anchors: anchors, ContextValue: requirementcontext.SnapshotValue(snapshot), Diff: diff, Graph: graph, Manifest: manifest, Requirements: requirements, SnapshotID: snapshot.SnapshotID}, workspaceHTML(workspaceID), nil } +func admitWorkspaceInputVersion(record map[string]any) error { + switch { + case admit.JSONNumberEquals(record["schemaVersion"], 1): + return admitV1WorkspaceInput(record) + case admit.JSONNumberEquals(record["schemaVersion"], 2): + return requireWorkspaceNestedVersions(record, 2) + default: + return fmt.Errorf("requirement browser workspace schemaVersion must be 1 or 2") + } +} + +func requireWorkspaceNestedVersions(record map[string]any, expected int) error { + contextRecord, ok := record["context"].(map[string]any) + if !ok || !admit.JSONNumberEquals(contextRecord["schemaVersion"], int64(expected)) { + return fmt.Errorf("requirement browser workspace schemaVersion %d requires context schemaVersion %d", expected, expected) + } + if rawDiff := record["diffInput"]; rawDiff != nil { + diff, ok := rawDiff.(map[string]any) + if !ok || !admit.JSONNumberEquals(diff["schemaVersion"], int64(expected)) { + return fmt.Errorf("requirement browser workspace schemaVersion %d requires diffInput schemaVersion %d", expected, expected) + } + } + if rawGraph := record["graphInput"]; rawGraph != nil { + graph, ok := rawGraph.(map[string]any) + if !ok || !admit.JSONNumberEquals(graph["schemaVersion"], 2) { + return fmt.Errorf("requirement browser workspace graphInput schemaVersion must be 2") + } + graphContext, ok := graph["context"].(map[string]any) + if !ok || !admit.JSONNumberEquals(graphContext["schemaVersion"], int64(expected)) { + return fmt.Errorf("requirement browser workspace schemaVersion %d requires graphInput context schemaVersion %d", expected, expected) + } + } + return nil +} + func validateGraphSnapshotClosure(snapshot requirementcontext.Snapshot, graph map[string]any) error { expectedSpecs := map[string]struct{}{} for _, node := range snapshot.Tree.Nodes { @@ -219,10 +254,10 @@ func workspaceHTML(workspaceID string) string { "", "" + html.EscapeString(workspaceID) + " - Proofkit workspace", "", - "

Proofkit semantic workspace

" + html.EscapeString(workspaceID) + "

Authority boundary

    ", - "
    ", - "
    ", - "", + "

    Proofkit semantic workspace

    " + html.EscapeString(workspaceID) + "

    Authority boundary

    Loading admitted authority...

      ", + "
      ", + "

      Loading workspace

      Loading admitted manifest...

      ", + "", "\n", }, "") } diff --git a/internal/command/requirementbrowser/workspace_test.go b/internal/command/requirementbrowser/workspace_test.go index f6b88c7..ad07e56 100644 --- a/internal/command/requirementbrowser/workspace_test.go +++ b/internal/command/requirementbrowser/workspace_test.go @@ -68,8 +68,8 @@ func TestWorkspaceServerEnforcesCapabilityAndBuildsSourceBoundHandoff(t *testing t.Fatal(err) } manifest := manifestValue.(map[string]any) - if manifest["baselineVerification"] != "unverified" || manifest["snapshotId"] != handle.SnapshotID { - t.Fatalf("workspace manifest lost baseline identity: %#v", manifest) + if manifest["expectedDigestCoverage"] != "none" || manifest["snapshotId"] != handle.SnapshotID { + t.Fatalf("workspace manifest lost digest-coverage identity: %#v", manifest) } handoff := `{"annotations":[{"anchorId":"requirement:REQ-CONSUMER-001:invariant","exactQuote":"preserves","startCodePoint":11,"endCodePoint":20,"question":"Does this remain true?"}]}` handoffRequest, _ := http.NewRequest(http.MethodPost, handle.URL+"api/v1/handoff", strings.NewReader(handoff)) @@ -110,6 +110,130 @@ func TestWorkspaceServerEnforcesCapabilityAndBuildsSourceBoundHandoff(t *testing } } +func TestV2DigestCoverageProjections(t *testing.T) { + fixture := workspaceFixture(t) + current := fixture["context"].(map[string]any) + base := cloneWorkspaceRecord(t, current) + base["projections"].(map[string]any)["requirementSources"].([]any)[0].(map[string]any)["requirements"].([]any)[0].(map[string]any)["invariant"] = "The system preserved the previous semantic identity." + resignWorkspaceSnapshot(t, base) + fixture["diffInput"] = map[string]any{"baseContext": base, "currentContext": current, "diffId": "consumer.workspace.digest-coverage", "schemaVersion": json.Number("2")} + fixture["graphInput"] = map[string]any{"context": current, "graphId": "consumer.workspace.digest-coverage", "schemaVersion": json.Number("2")} + + handle, capability := startWorkspaceTestServer(t, fixture, false) + manifest := getWorkspaceJSON(t, handle.URL+"api/v1/manifest", capability) + if manifest["schemaVersion"] != json.Number("2") || manifest["expectedDigestCoverage"] != "none" || manifest["baselineVerification"] != nil { + t.Fatalf("workspace manifest is not a clean v2 projection: %#v", manifest) + } + for _, path := range []string{"requirements", "diff", "graph"} { + response := postWorkspaceJSON(t, handle.URL+"api/v1/"+path, capability, map[string]any{ + "query": map[string]any{}, "requestId": "consumer.workspace." + path, "snapshotId": handle.SnapshotID, + }) + if response["schemaVersion"] != json.Number("2") { + t.Fatalf("%s response schemaVersion = %v, want 2", path, response["schemaVersion"]) + } + if path == "diff" { + projection := response["projection"].(map[string]any) + if projection["baseExpectedDigestCoverage"] != "none" || projection["currentExpectedDigestCoverage"] != "none" || projection["baseBaselineVerification"] != nil || projection["currentBaselineVerification"] != nil { + t.Fatalf("diff API projection is not clean v2: %#v", projection) + } + } + } + + v2Minimal := workspaceFixture(t) + v1Minimal := cloneWorkspaceRecord(t, v2Minimal) + v1Minimal["schemaVersion"] = json.Number("1") + v1Context := v1Minimal["context"].(map[string]any) + delete(v1Context, "expectedDigestCoverage") + v1Context["baselineVerification"] = "unverified" + v1Context["nonClaims"] = []any{ + "Requirement context is a derived projection and is not requirement, proof, coverage, merge, release, rollout, or readiness authority.", + "Requirement context does not execute native witnesses or prove source freshness after composition.", + } + v1Context["schemaVersion"] = json.Number("1") + v1Session, _, err := buildWorkspace(v1Minimal) + if err != nil { + t.Fatalf("buildWorkspace(v1) error = %v", err) + } + v2Session, _, err := buildWorkspace(v2Minimal) + if err != nil { + t.Fatal(err) + } + if got, want := stableWorkspaceBytes(t, v1Session.Manifest), stableWorkspaceBytes(t, v2Session.Manifest); !bytes.Equal(got, want) { + t.Fatalf("v1 workspace normalized to a different v2 manifest\n got: %s\nwant: %s", got, want) + } + v1Minimal["context"] = current + if _, _, err := buildWorkspace(v1Minimal); err == nil { + t.Fatal("buildWorkspace accepted a v1 envelope with a v2 context") + } +} + +func getWorkspaceJSON(t *testing.T, url, capability string) map[string]any { + t.Helper() + request, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + t.Fatal(err) + } + request.Header.Set("X-Proofkit-Browser-Capability", capability) + response, err := http.DefaultClient.Do(request) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + return decodeWorkspaceResponse(t, response) +} + +func postWorkspaceJSON(t *testing.T, url, capability string, value any) map[string]any { + t.Helper() + body := stableWorkspaceBytes(t, value) + request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + t.Fatal(err) + } + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Origin", strings.Split(url, "/api/")[0]) + request.Header.Set("X-Proofkit-Browser-Capability", capability) + response, err := http.DefaultClient.Do(request) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + return decodeWorkspaceResponse(t, response) +} + +func decodeWorkspaceResponse(t *testing.T, response *http.Response) map[string]any { + t.Helper() + body, err := io.ReadAll(response.Body) + if err != nil { + t.Fatal(err) + } + if response.StatusCode != http.StatusOK { + t.Fatalf("workspace response status=%d body=%s", response.StatusCode, body) + } + decoded, err := admission.DecodeJSON(bytes.NewReader(body), int64(len(body))) + if err != nil { + t.Fatal(err) + } + return decoded.(map[string]any) +} + +func cloneWorkspaceRecord(t *testing.T, value map[string]any) map[string]any { + t.Helper() + decoded, err := admission.DecodeJSON(bytes.NewReader(stableWorkspaceBytes(t, value)), 24<<20) + if err != nil { + t.Fatal(err) + } + return decoded.(map[string]any) +} + +func stableWorkspaceBytes(t *testing.T, value any) []byte { + t.Helper() + encoded, err := stablejson.Marshal(value) + if err != nil { + t.Fatal(err) + } + return encoded +} + func TestWorkspaceAssetsHaveExactSecureHTTPContract(t *testing.T) { handle, err := StartServer(workspaceFixture(t), Options{Host: "127.0.0.1", Port: 0, PortSet: true, View: "workspace"}) if err != nil { @@ -236,7 +360,7 @@ func TestWorkspaceAdmitsMultiFieldSemanticDiffFromProducer(t *testing.T) { requirement["invariant"] = "The system preserved the previous semantic identity." requirement["riskClass"] = "medium" resignWorkspaceSnapshot(t, base) - fixture["diffInput"] = map[string]any{"baseContext": base, "currentContext": current, "diffId": "consumer.workspace.diff", "schemaVersion": json.Number("1")} + fixture["diffInput"] = map[string]any{"baseContext": base, "currentContext": current, "diffId": "consumer.workspace.diff", "schemaVersion": json.Number("2")} workspace, _, err := buildWorkspace(fixture) if err != nil { @@ -307,14 +431,65 @@ func TestWorkspaceCapabilityReplacementIsConfinedToBootstrapMeta(t *testing.T) { } } -func TestOneShotTimeoutEmitsOneCompactTerminalPacket(t *testing.T) { - var stdout bytes.Buffer - err := Serve(t.Context(), workspaceFixture(t), Options{Host: "127.0.0.1", Port: 0, PortSet: true, SessionMode: "one-shot-question", SessionTimeout: 10 * time.Millisecond, View: "workspace"}, &stdout) +func TestOneShotOutputVariantsUseExactRootShapes(t *testing.T) { + var terminalStdout bytes.Buffer + err := Serve(t.Context(), workspaceFixture(t), Options{Host: "127.0.0.1", Port: 0, PortSet: true, SessionMode: "one-shot-question", SessionTimeout: 10 * time.Millisecond, View: "workspace"}, &terminalStdout) if !errors.Is(err, ErrOneShotTerminal) { t.Fatalf("Serve() error = %v, want terminal state", err) } - if strings.Count(stdout.String(), "\n") != 1 || strings.Contains(stdout.String(), "\n ") || !strings.Contains(stdout.String(), `"state":"expired"`) { - t.Fatalf("unexpected one-shot packet: %q", stdout.String()) + if strings.Count(terminalStdout.String(), "\n") != 1 || strings.Contains(terminalStdout.String(), "\n ") { + t.Fatalf("unexpected one-shot terminal packet layout: %q", terminalStdout.String()) + } + terminalValue, err := admission.DecodeJSON(bytes.NewReader(terminalStdout.Bytes()), int64(terminalStdout.Len())) + if err != nil { + t.Fatal(err) + } + terminal := terminalValue.(map[string]any) + assertExactRootKeys(t, terminal, "handoffKind", "nonClaims", "schemaVersion", "snapshotRefs", "state") + if terminal["state"] != "expired" { + t.Fatalf("terminal state=%v, want expired", terminal["state"]) + } + + handle, capability := startWorkspaceTestServer(t, workspaceFixture(t), true) + var submittedStdout bytes.Buffer + result := make(chan error, 1) + go func() { + result <- serveHandle(t.Context(), handle, Options{SessionMode: "one-shot-question"}, &submittedStdout) + }() + response := postWorkspaceHandoff(t, handle.URL, capability) + body, _ := io.ReadAll(response.Body) + _ = response.Body.Close() + if response.StatusCode != http.StatusOK { + t.Fatalf("handoff status=%d body=%s", response.StatusCode, body) + } + select { + case err := <-result: + if err != nil { + t.Fatalf("serveHandle() submitted error = %v", err) + } + case <-time.After(time.Second): + t.Fatal("serveHandle did not emit submitted packet") + } + submittedValue, err := admission.DecodeJSON(bytes.NewReader(submittedStdout.Bytes()), int64(submittedStdout.Len())) + if err != nil { + t.Fatal(err) + } + submitted := submittedValue.(map[string]any) + assertExactRootKeys(t, submitted, "annotations", "context", "handoffKind", "instructionAuthority", "nonClaims", "schemaVersion", "snapshotRefs", "sourceTextAuthority", "state") + if submitted["state"] != "submitted" { + t.Fatalf("submitted state=%v, want submitted", submitted["state"]) + } +} + +func assertExactRootKeys(t *testing.T, record map[string]any, keys ...string) { + t.Helper() + if len(record) != len(keys) { + t.Fatalf("root keys=%v, want exactly %v", record, keys) + } + for _, key := range keys { + if _, ok := record[key]; !ok { + t.Fatalf("root is missing %s: %#v", key, record) + } } } @@ -388,8 +563,8 @@ func workspaceFixtureWithInvariant(t *testing.T, invariant string) map[string]an if err != nil { t.Fatal(err) } - contextValue := requirementcontext.SnapshotValue(requirementcontext.Snapshot{BaselineVerification: "unverified", CatalogID: "consumer.context", Projections: projections, SnapshotID: digest.SHA256TextRef(string(encoded)), Sources: sources}) - return map[string]any{"schemaVersion": json.Number("1"), "workspaceId": "consumer.workspace", "context": contextValue} + contextValue := requirementcontext.SnapshotValue(requirementcontext.Snapshot{CatalogID: "consumer.context", ExpectedDigestCoverage: "none", Projections: projections, SnapshotID: digest.SHA256TextRef(string(encoded)), Sources: sources}) + return map[string]any{"schemaVersion": json.Number("2"), "workspaceId": "consumer.workspace", "context": contextValue} } func resignWorkspaceSnapshot(t *testing.T, contextValue map[string]any) { diff --git a/internal/command/requirementcontext/compose.go b/internal/command/requirementcontext/compose.go index 064c326..a046d96 100644 --- a/internal/command/requirementcontext/compose.go +++ b/internal/command/requirementcontext/compose.go @@ -162,8 +162,8 @@ func Compose(repoRoot string, raw any) (map[string]any, error) { }) sort.Slice(sources, func(left, right int) bool { return sources[left].SourceRef < sources[right].SourceRef }) projections["requirementSources"] = requirementSources - verification := baselineVerification(sources) - snapshot := Snapshot{BaselineVerification: verification, CatalogID: catalogID, Projections: projections, Sources: sources} + coverage := expectedDigestCoverage(sources) + snapshot := Snapshot{CatalogID: catalogID, ExpectedDigestCoverage: coverage, Projections: projections, Sources: sources} identityValue := map[string]any{"catalogId": catalogID, "projections": projections, "sources": sourceIdentityValues(sources)} encoded, err := stablejson.Marshal(identityValue) if err != nil { @@ -314,20 +314,20 @@ func readCatalogSource(root *os.Root, path string) (any, []byte, string, error) return value, content, digest.SHA256TextRef(string(content)), nil } -func baselineVerification(sources []Source) string { - verified := 0 +func expectedDigestCoverage(sources []Source) string { + covered := 0 for _, source := range sources { if source.ExpectedDigest != "" { - verified++ + covered++ } } - if verified == 0 { - return "unverified" + if covered == 0 { + return "none" } - if verified == len(sources) { - return "verified" + if covered == len(sources) { + return "all" } - return "partially_verified" + return "partial" } func sourceIdentityValues(sources []Source) []any { diff --git a/internal/command/requirementcontext/model.go b/internal/command/requirementcontext/model.go index f613979..e11847c 100644 --- a/internal/command/requirementcontext/model.go +++ b/internal/command/requirementcontext/model.go @@ -23,6 +23,7 @@ const ( var boundaryNonClaims = []string{ "Requirement context is a derived projection and is not requirement, proof, coverage, merge, release, rollout, or readiness authority.", "Requirement context does not execute native witnesses or prove source freshness after composition.", + "Expected-digest coverage records caller-supplied expected/current equality only and does not authenticate a producer, baseline, checkout, or freshness.", } type Source struct { @@ -36,15 +37,15 @@ type Source struct { } type Snapshot struct { - BaselineVerification string - CatalogID string - Coverage map[string]any - ProofBinding *requirementbinding.Input - Projections map[string]any - RequirementSources []requirementsourceadmission.Source - SnapshotID string - Sources []Source - Tree requirementspectree.Tree + CatalogID string + Coverage map[string]any + ExpectedDigestCoverage string + ProofBinding *requirementbinding.Input + Projections map[string]any + RequirementSources []requirementsourceadmission.Source + SnapshotID string + Sources []Source + Tree requirementspectree.Tree } func SnapshotValue(snapshot Snapshot) map[string]any { @@ -68,14 +69,14 @@ func SnapshotValue(snapshot Snapshot) map[string]any { sources = append(sources, record) } return map[string]any{ - "baselineVerification": snapshot.BaselineVerification, - "catalogId": snapshot.CatalogID, - "contextKind": ContextKind, - "nonClaims": admit.StringSliceToAny(boundaryNonClaims), - "projections": snapshot.Projections, - "schemaVersion": json.Number("1"), - "snapshotId": snapshot.SnapshotID, - "sources": sources, + "catalogId": snapshot.CatalogID, + "contextKind": ContextKind, + "expectedDigestCoverage": snapshot.ExpectedDigestCoverage, + "nonClaims": admit.StringSliceToAny(boundaryNonClaims), + "projections": snapshot.Projections, + "schemaVersion": json.Number("2"), + "snapshotId": snapshot.SnapshotID, + "sources": sources, } } @@ -84,21 +85,44 @@ func AdmitSnapshot(raw any) (Snapshot, error) { if !ok { return Snapshot{}, fmt.Errorf("requirement context must be an object") } - if err := admit.KnownKeys(record, []string{"baselineVerification", "catalogId", "contextKind", "nonClaims", "projections", "schemaVersion", "snapshotId", "sources"}, "requirement context"); err != nil { + switch { + case admit.JSONNumberEquals(record["schemaVersion"], 1): + return admitV1Snapshot(record) + case admit.JSONNumberEquals(record["schemaVersion"], 2): + return admitV2Snapshot(record) + default: + return Snapshot{}, fmt.Errorf("requirement context schemaVersion must be 1 or 2") + } +} + +func admitV2Snapshot(record map[string]any) (Snapshot, error) { + if err := admit.KnownKeys(record, []string{"catalogId", "contextKind", "expectedDigestCoverage", "nonClaims", "projections", "schemaVersion", "snapshotId", "sources"}, "requirement context v2"); err != nil { return Snapshot{}, err } - if !admit.JSONNumberEquals(record["schemaVersion"], 1) || record["contextKind"] != ContextKind { - return Snapshot{}, fmt.Errorf("requirement context identity is invalid") + if !admit.JSONNumberEquals(record["schemaVersion"], 2) || record["contextKind"] != ContextKind { + return Snapshot{}, fmt.Errorf("requirement context v2 identity is invalid") } - catalogID, err := admit.RuleID(record["catalogId"], "requirement context catalogId") + coverage, err := admit.Enum(record["expectedDigestCoverage"], map[string]struct{}{"all": {}, "none": {}, "partial": {}}, "requirement context expectedDigestCoverage") if err != nil { return Snapshot{}, err } - snapshotID, err := admitDigestRef(record["snapshotId"], "requirement context snapshotId") + snapshot, err := admitSnapshotRecord(record, boundaryNonClaims) if err != nil { return Snapshot{}, err } - verification, err := admit.Enum(record["baselineVerification"], map[string]struct{}{"verified": {}, "partially_verified": {}, "unverified": {}}, "requirement context baselineVerification") + if coverage != expectedDigestCoverage(snapshot.Sources) { + return Snapshot{}, fmt.Errorf("requirement context expectedDigestCoverage does not match admitted sources") + } + snapshot.ExpectedDigestCoverage = coverage + return validateSnapshotSize(snapshot) +} + +func admitSnapshotRecord(record map[string]any, expectedNonClaims []string) (Snapshot, error) { + catalogID, err := admit.RuleID(record["catalogId"], "requirement context catalogId") + if err != nil { + return Snapshot{}, err + } + snapshotID, err := admitDigestRef(record["snapshotId"], "requirement context snapshotId") if err != nil { return Snapshot{}, err } @@ -120,11 +144,7 @@ func AdmitSnapshot(raw any) (Snapshot, error) { if err := validateProjectionSources(tree, requirementSources, proofBinding, coverage, sources); err != nil { return Snapshot{}, err } - derivedVerification := baselineVerification(sources) - if verification != derivedVerification { - return Snapshot{}, fmt.Errorf("requirement context baselineVerification does not match admitted sources") - } - if err := admitExactNonClaims(record["nonClaims"]); err != nil { + if err := admitExactNonClaims(record["nonClaims"], expectedNonClaims); err != nil { return Snapshot{}, err } identityValue := map[string]any{"catalogId": catalogID, "projections": canonicalProjections, "sources": sourceIdentityValues(sources)} @@ -136,16 +156,19 @@ func AdmitSnapshot(raw any) (Snapshot, error) { return Snapshot{}, fmt.Errorf("requirement context snapshotId does not match admitted content") } snapshot := Snapshot{ - BaselineVerification: verification, - CatalogID: catalogID, - Coverage: coverage, - ProofBinding: proofBinding, - Projections: canonicalProjections, - RequirementSources: requirementSources, - SnapshotID: snapshotID, - Sources: sources, - Tree: tree, + CatalogID: catalogID, + Coverage: coverage, + ProofBinding: proofBinding, + Projections: canonicalProjections, + RequirementSources: requirementSources, + SnapshotID: snapshotID, + Sources: sources, + Tree: tree, } + return snapshot, nil +} + +func validateSnapshotSize(snapshot Snapshot) (Snapshot, error) { fullValue, err := stablejson.Marshal(SnapshotValue(snapshot)) if err != nil { return Snapshot{}, err @@ -309,12 +332,12 @@ func requirementSourceValues(sources []requirementsourceadmission.Source) []any return values } -func admitExactNonClaims(raw any) error { +func admitExactNonClaims(raw any, expectedNonClaims []string) error { values, ok := raw.([]any) - if !ok || len(values) != len(boundaryNonClaims) { + if !ok || len(values) != len(expectedNonClaims) { return fmt.Errorf("requirement context nonClaims must equal the command-owned boundary") } - for index, expected := range boundaryNonClaims { + for index, expected := range expectedNonClaims { if values[index] != expected { return fmt.Errorf("requirement context nonClaims must equal the command-owned boundary") } diff --git a/internal/command/requirementcontext/requirementcontext_test.go b/internal/command/requirementcontext/requirementcontext_test.go index 2413a3c..625b1fd 100644 --- a/internal/command/requirementcontext/requirementcontext_test.go +++ b/internal/command/requirementcontext/requirementcontext_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" @@ -25,8 +26,8 @@ func TestComposeAndSliceRoundTrip(t *testing.T) { if err != nil { t.Fatalf("AdmitSnapshot() error = %v", err) } - if snapshot.BaselineVerification != "unverified" { - t.Fatalf("baseline verification = %q, want unverified", snapshot.BaselineVerification) + if snapshot.ExpectedDigestCoverage != "none" { + t.Fatalf("expected digest coverage = %q, want none", snapshot.ExpectedDigestCoverage) } output, err := Slice(map[string]any{ "schemaVersion": json.Number("1"), @@ -54,7 +55,95 @@ func TestComposeAndSliceRoundTrip(t *testing.T) { } } -func TestSnapshotAdmissionRejectsForgedBaselineAndProjectionLedger(t *testing.T) { +func TestV1DigestCoverageAdapters(t *testing.T) { + root := fixtureRepository(t) + for _, test := range []struct { + name string + covered []string + expected string + legacy string + }{ + {name: "none", expected: "none", legacy: "unverified"}, + {name: "partial", covered: []string{"specTree"}, expected: "partial", legacy: "partially_verified"}, + {name: "all", covered: []string{"specTree", "requirementSources"}, expected: "all", legacy: "verified"}, + } { + t.Run(test.name, func(t *testing.T) { + catalog := fixtureCatalog() + for _, owner := range test.covered { + switch owner { + case "specTree": + setExpectedFixtureDigest(t, root, catalog["specTree"].(map[string]any)) + case "requirementSources": + setExpectedFixtureDigest(t, root, catalog["requirementSources"].([]any)[0].(map[string]any)) + } + } + v2, err := Compose(root, catalog) + if err != nil { + t.Fatal(err) + } + if v2["schemaVersion"] != json.Number("2") || v2["expectedDigestCoverage"] != test.expected { + t.Fatalf("v2 projection = %#v, want coverage %q", v2, test.expected) + } + v1 := v1SnapshotFixture(t, v2, test.legacy) + admitted, err := AdmitSnapshot(v1) + if err != nil { + t.Fatalf("AdmitSnapshot(v1) error = %v", err) + } + if got, want := mustStableJSON(t, SnapshotValue(admitted)), mustStableJSON(t, v2); !bytes.Equal(got, want) { + t.Fatalf("v1 normalized output differs from v2\n got: %s\nwant: %s", got, want) + } + }) + } + + v2, err := Compose(root, fixtureCatalog()) + if err != nil { + t.Fatal(err) + } + mixedV2 := deepClone(t, v2) + mixedV2["baselineVerification"] = "unverified" + if _, err := AdmitSnapshot(mixedV2); err == nil { + t.Fatal("AdmitSnapshot accepted mixed v1/v2 fields under schema v2") + } + mixedV1 := v1SnapshotFixture(t, v2, "unverified") + mixedV1["expectedDigestCoverage"] = "none" + if _, err := AdmitSnapshot(mixedV1); err == nil { + t.Fatal("AdmitSnapshot accepted mixed v1/v2 fields under schema v1") + } + malformedV1 := v1SnapshotFixture(t, v2, "verified") + if _, err := AdmitSnapshot(malformedV1); err == nil { + t.Fatal("AdmitSnapshot accepted a malformed legacy coverage claim") + } +} + +func setExpectedFixtureDigest(t *testing.T, root string, entry map[string]any) { + t.Helper() + content, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(entry["path"].(string)))) + if err != nil { + t.Fatal(err) + } + entry["expectedSourceDigest"] = digest.SHA256TextRef(string(content)) +} + +func v1SnapshotFixture(t *testing.T, v2 map[string]any, legacy string) map[string]any { + t.Helper() + v1 := deepClone(t, v2) + delete(v1, "expectedDigestCoverage") + v1["baselineVerification"] = legacy + v1["nonClaims"] = admit.StringSliceToAny(v1BoundaryNonClaims) + v1["schemaVersion"] = json.Number("1") + return v1 +} + +func mustStableJSON(t *testing.T, value any) []byte { + t.Helper() + encoded, err := stablejson.Marshal(value) + if err != nil { + t.Fatal(err) + } + return encoded +} + +func TestSnapshotAdmissionRejectsForgedCoverageAndProjectionLedger(t *testing.T) { contextValue, err := Compose(fixtureRepository(t), fixtureCatalog()) if err != nil { t.Fatal(err) @@ -65,10 +154,10 @@ func TestSnapshotAdmissionRejectsForgedBaselineAndProjectionLedger(t *testing.T) source := raw.(map[string]any) source["expectedDigest"] = "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" } - forged["baselineVerification"] = "verified" + forged["expectedDigestCoverage"] = "all" resignSnapshot(t, forged) if _, err := AdmitSnapshot(forged); err == nil { - t.Fatal("AdmitSnapshot accepted mismatched expected digests as verified") + t.Fatal("AdmitSnapshot accepted mismatched expected digests as fully covered") } phantom := deepClone(t, contextValue) diff --git a/internal/command/requirementcontext/slice_closure_test.go b/internal/command/requirementcontext/slice_closure_test.go index 7027dbb..52e5af2 100644 --- a/internal/command/requirementcontext/slice_closure_test.go +++ b/internal/command/requirementcontext/slice_closure_test.go @@ -216,7 +216,7 @@ func sliceTopologyFixture(t *testing.T) map[string]any { if err != nil { t.Fatal(err) } - return SnapshotValue(Snapshot{BaselineVerification: "unverified", CatalogID: "consumer.slice-context", Projections: projections, SnapshotID: digest.SHA256TextRef(string(identity)), Sources: sources}) + return SnapshotValue(Snapshot{CatalogID: "consumer.slice-context", ExpectedDigestCoverage: "none", Projections: projections, SnapshotID: digest.SHA256TextRef(string(identity)), Sources: sources}) } func sliceRequirement(id, owner, claimLevel, lifecycleState string, replacementIDs []any) map[string]any { diff --git a/internal/command/requirementcontext/v1_adapter.go b/internal/command/requirementcontext/v1_adapter.go new file mode 100644 index 0000000..89ebfe4 --- /dev/null +++ b/internal/command/requirementcontext/v1_adapter.go @@ -0,0 +1,46 @@ +package requirementcontext + +import ( + "fmt" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" +) + +var v1BoundaryNonClaims = []string{ + "Requirement context is a derived projection and is not requirement, proof, coverage, merge, release, rollout, or readiness authority.", + "Requirement context does not execute native witnesses or prove source freshness after composition.", +} + +func admitV1Snapshot(record map[string]any) (Snapshot, error) { + if err := admit.KnownKeys(record, []string{"baselineVerification", "catalogId", "contextKind", "nonClaims", "projections", "schemaVersion", "snapshotId", "sources"}, "requirement context v1"); err != nil { + return Snapshot{}, err + } + if !admit.JSONNumberEquals(record["schemaVersion"], 1) || record["contextKind"] != ContextKind { + return Snapshot{}, fmt.Errorf("requirement context v1 identity is invalid") + } + verification, err := admit.Enum(record["baselineVerification"], map[string]struct{}{"partially_verified": {}, "unverified": {}, "verified": {}}, "requirement context v1 baselineVerification") + if err != nil { + return Snapshot{}, err + } + snapshot, err := admitSnapshotRecord(record, v1BoundaryNonClaims) + if err != nil { + return Snapshot{}, err + } + coverage := expectedDigestCoverage(snapshot.Sources) + if verification != v1VerificationValue(coverage) { + return Snapshot{}, fmt.Errorf("requirement context v1 baselineVerification does not match admitted sources") + } + snapshot.ExpectedDigestCoverage = coverage + return validateSnapshotSize(snapshot) +} + +func v1VerificationValue(coverage string) string { + switch coverage { + case "none": + return "unverified" + case "partial": + return "partially_verified" + default: + return "verified" + } +} diff --git a/internal/command/requirementcoverageview/envelope.go b/internal/command/requirementcoverageview/envelope.go index 52f58ea..c56aa5f 100644 --- a/internal/command/requirementcoverageview/envelope.go +++ b/internal/command/requirementcoverageview/envelope.go @@ -3,16 +3,18 @@ package requirementcoverageview import ( "fmt" + "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" "github.com/research-engineering/agentic-proofkit/internal/kernel/agentenvelope" + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" ) -func agentEnvelope(view map[string]any) map[string]any { +func agentEnvelope(view map[string]any, renderer cliexec.Renderer) map[string]any { state := stringValue(view["state"]) failures := stringArray(view["failures"]) commands := []map[string]any{} if state == "failed" { commands = append(commands, map[string]any{ - "argv": []any{"agentic-proofkit", "requirement-coverage-view", "--input", ""}, + "argv": admit.StringSliceToAny(renderer.Argv("requirement-coverage-view", "--input", "")), "commandId": "proofkit.requirement-coverage-view.rerun", "nonClaim": "Command refs are suggestions only; the consumer repository owns paths and execution.", "reason": "Rebuild the coverage view after repairing caller-owned coverage input.", diff --git a/internal/command/requirementcoverageview/requirementcoverageview.go b/internal/command/requirementcoverageview/requirementcoverageview.go index 781bd70..ad48240 100644 --- a/internal/command/requirementcoverageview/requirementcoverageview.go +++ b/internal/command/requirementcoverageview/requirementcoverageview.go @@ -6,7 +6,7 @@ func BuildJSON(raw any, options Options) (any, int, error) { return nil, 1, err } if options.AgentEnvelope { - return agentEnvelope(view), exitCode(view), nil + return agentEnvelope(view, options.Renderer), exitCode(view), nil } return view, exitCode(view), nil } diff --git a/internal/command/requirementcoverageview/types.go b/internal/command/requirementcoverageview/types.go index 873f787..d1ade21 100644 --- a/internal/command/requirementcoverageview/types.go +++ b/internal/command/requirementcoverageview/types.go @@ -3,6 +3,7 @@ package requirementcoverageview import ( "github.com/research-engineering/agentic-proofkit/internal/command/requirementsourceadmission" "github.com/research-engineering/agentic-proofkit/internal/command/testevidenceinventory" + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" ) var defaultNonClaims = []string{ @@ -14,6 +15,7 @@ var defaultNonClaims = []string{ type Options struct { AgentEnvelope bool + Renderer cliexec.Renderer } type compositeInput struct { CoverageUniverse coverageUniverse diff --git a/internal/command/requirementdiff/output_admission.go b/internal/command/requirementdiff/output_admission.go index e727d63..fe2fa2e 100644 --- a/internal/command/requirementdiff/output_admission.go +++ b/internal/command/requirementdiff/output_admission.go @@ -24,22 +24,37 @@ func AdmitOutput(raw any, currentSnapshotID string) (map[string]any, error) { if !ok { return nil, fmt.Errorf("requirement semantic diff output must be an object") } - if err := admit.KnownKeys(record, []string{"baseBaselineVerification", "baseSnapshotId", "changeCount", "changes", "currentBaselineVerification", "currentSnapshotId", "diffId", "diffKind", "nonClaims", "schemaVersion"}, "requirement semantic diff output"); err != nil { + switch { + case admit.JSONNumberEquals(record["schemaVersion"], 1): + return admitV1Output(record, currentSnapshotID) + case admit.JSONNumberEquals(record["schemaVersion"], 2): + return admitV2Output(record, currentSnapshotID) + default: + return nil, fmt.Errorf("requirement semantic diff output schemaVersion must be 1 or 2") + } +} + +func admitV2Output(record map[string]any, currentSnapshotID string) (map[string]any, error) { + if err := admit.KnownKeys(record, []string{"baseExpectedDigestCoverage", "baseSnapshotId", "changeCount", "changes", "currentExpectedDigestCoverage", "currentSnapshotId", "diffId", "diffKind", "nonClaims", "schemaVersion"}, "requirement semantic diff output v2"); err != nil { return nil, err } - if !admit.JSONNumberEquals(record["schemaVersion"], 1) && record["schemaVersion"] != 1 { - return nil, fmt.Errorf("requirement semantic diff output schemaVersion must be 1") + if !admit.JSONNumberEquals(record["schemaVersion"], 2) && record["schemaVersion"] != 2 { + return nil, fmt.Errorf("requirement semantic diff output schemaVersion must be 2") } if record["diffKind"] != "proofkit.requirement-semantic-diff" || record["currentSnapshotId"] != currentSnapshotID { return nil, fmt.Errorf("requirement semantic diff output identity is invalid") } - for _, key := range []string{"baseSnapshotId", "currentSnapshotId"} { - if _, err := digestRef(record[key], "requirement semantic diff output "+key); err != nil { + for _, key := range []string{"baseExpectedDigestCoverage", "currentExpectedDigestCoverage"} { + if _, err := admit.Enum(record[key], map[string]struct{}{"all": {}, "none": {}, "partial": {}}, "requirement semantic diff output "+key); err != nil { return nil, err } } - for _, key := range []string{"baseBaselineVerification", "currentBaselineVerification"} { - if _, err := admit.Enum(record[key], map[string]struct{}{"partially_verified": {}, "unverified": {}, "verified": {}}, "requirement semantic diff output "+key); err != nil { + return admitOutputRecord(record) +} + +func admitOutputRecord(record map[string]any) (map[string]any, error) { + for _, key := range []string{"baseSnapshotId", "currentSnapshotId"} { + if _, err := digestRef(record[key], "requirement semantic diff output "+key); err != nil { return nil, err } } diff --git a/internal/command/requirementdiff/requirementdiff.go b/internal/command/requirementdiff/requirementdiff.go index 37d8d89..276fc89 100644 --- a/internal/command/requirementdiff/requirementdiff.go +++ b/internal/command/requirementdiff/requirementdiff.go @@ -40,8 +40,8 @@ func Build(raw any) (map[string]any, error) { if err := admit.KnownKeys(record, []string{"baseContext", "currentContext", "diffId", "query", "schemaVersion"}, "requirement semantic diff input"); err != nil { return nil, err } - if !admit.JSONNumberEquals(record["schemaVersion"], 1) { - return nil, fmt.Errorf("requirement semantic diff schemaVersion must be 1") + if err := admitDiffInputVersion(record); err != nil { + return nil, err } diffID, err := admit.RuleID(record["diffId"], "requirement semantic diff diffId") if err != nil { @@ -66,19 +66,40 @@ func Build(raw any) (map[string]any, error) { return nil, err } return map[string]any{ - "baseBaselineVerification": base.BaselineVerification, - "baseSnapshotId": base.SnapshotID, - "changeCount": len(changes), - "changes": changes, - "currentBaselineVerification": current.BaselineVerification, - "currentSnapshotId": current.SnapshotID, - "diffId": diffID, - "diffKind": "proofkit.requirement-semantic-diff", - "nonClaims": admit.StringSliceToAny(nonClaims), - "schemaVersion": json.Number("1"), + "baseExpectedDigestCoverage": base.ExpectedDigestCoverage, + "baseSnapshotId": base.SnapshotID, + "changeCount": len(changes), + "changes": changes, + "currentExpectedDigestCoverage": current.ExpectedDigestCoverage, + "currentSnapshotId": current.SnapshotID, + "diffId": diffID, + "diffKind": "proofkit.requirement-semantic-diff", + "nonClaims": admit.StringSliceToAny(nonClaims), + "schemaVersion": json.Number("2"), }, nil } +func admitDiffInputVersion(record map[string]any) error { + switch { + case admit.JSONNumberEquals(record["schemaVersion"], 1): + return admitV1DiffInput(record) + case admit.JSONNumberEquals(record["schemaVersion"], 2): + return requireContextVersions(record, 2) + default: + return fmt.Errorf("requirement semantic diff schemaVersion must be 1 or 2") + } +} + +func requireContextVersions(record map[string]any, expected int) error { + for _, key := range []string{"baseContext", "currentContext"} { + contextRecord, ok := record[key].(map[string]any) + if !ok || !admit.JSONNumberEquals(contextRecord["schemaVersion"], int64(expected)) { + return fmt.Errorf("requirement semantic diff schemaVersion %d requires %s schemaVersion %d", expected, key, expected) + } + } + return nil +} + func admitQuery(raw any) (query, error) { if raw == nil { return query{MaxChanges: maxChanges, OwnerIDs: map[string]struct{}{}, RequirementIDs: map[string]struct{}{}}, nil diff --git a/internal/command/requirementdiff/requirementdiff_test.go b/internal/command/requirementdiff/requirementdiff_test.go index cb6b645..8d64dc5 100644 --- a/internal/command/requirementdiff/requirementdiff_test.go +++ b/internal/command/requirementdiff/requirementdiff_test.go @@ -16,7 +16,7 @@ func TestBuildClassifiesOwnerAwareRequirementChanges(t *testing.T) { commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.070793924321910266811017548213459952142613821158702213873670242368963866904489") baseline := contextFixture(t, "The system preserves the baseline.") current := contextFixture(t, "The system preserves the revised invariant.") - output, err := Build(map[string]any{"schemaVersion": json.Number("1"), "diffId": "consumer.requirement.diff", "baseContext": baseline, "currentContext": current, "query": map[string]any{"requirementIds": []any{"REQ-CONSUMER-001"}}}) + output, err := Build(map[string]any{"schemaVersion": json.Number("2"), "diffId": "consumer.requirement.diff", "baseContext": baseline, "currentContext": current, "query": map[string]any{"requirementIds": []any{"REQ-CONSUMER-001"}}}) if err != nil { t.Fatalf("Build() error = %v", err) } @@ -47,6 +47,69 @@ func TestBuildClassifiesOwnerAwareRequirementChanges(t *testing.T) { } } +func TestDigestCoverageAdaptersPreserveSemanticDiffV2(t *testing.T) { + baseV2 := contextFixture(t, "The system preserves the baseline.") + currentV2 := contextFixture(t, "The system preserves the revised invariant.") + v2Input := map[string]any{"baseContext": baseV2, "currentContext": currentV2, "diffId": "consumer.digest-coverage.diff", "schemaVersion": json.Number("2")} + v2Output, err := Build(v2Input) + if err != nil { + t.Fatal(err) + } + if v2Output["schemaVersion"] != json.Number("2") || v2Output["baseExpectedDigestCoverage"] != "none" || v2Output["currentExpectedDigestCoverage"] != "none" { + t.Fatalf("semantic diff did not emit v2 digest coverage: %#v", v2Output) + } + + baseV1 := v1DiffContextFixture(t, baseV2) + currentV1 := v1DiffContextFixture(t, currentV2) + v1Output, err := Build(map[string]any{"baseContext": baseV1, "currentContext": currentV1, "diffId": "consumer.digest-coverage.diff", "schemaVersion": json.Number("1")}) + if err != nil { + t.Fatalf("Build(v1) error = %v", err) + } + if got, want := stableBytes(t, v1Output), stableBytes(t, v2Output); !bytes.Equal(got, want) { + t.Fatalf("v1 input normalized to a different semantic diff\n got: %s\nwant: %s", got, want) + } + + var v1WireOutput map[string]any + for _, mapping := range []struct { + legacy string + v2 string + }{ + {legacy: "unverified", v2: "none"}, + {legacy: "partially_verified", v2: "partial"}, + {legacy: "verified", v2: "all"}, + } { + expectedV2 := cloneRequirementRecord(t, v2Output) + expectedV2["baseExpectedDigestCoverage"] = mapping.v2 + expectedV2["currentExpectedDigestCoverage"] = mapping.v2 + v1WireOutput = cloneRequirementRecord(t, expectedV2) + delete(v1WireOutput, "baseExpectedDigestCoverage") + delete(v1WireOutput, "currentExpectedDigestCoverage") + v1WireOutput["baseBaselineVerification"] = mapping.legacy + v1WireOutput["currentBaselineVerification"] = mapping.legacy + v1WireOutput["schemaVersion"] = json.Number("1") + normalized, err := AdmitOutput(v1WireOutput, currentV2["snapshotId"].(string)) + if err != nil { + t.Fatalf("AdmitOutput(v1 %s) error = %v", mapping.legacy, err) + } + if got, want := stableBytes(t, normalized), stableBytes(t, expectedV2); !bytes.Equal(got, want) { + t.Fatalf("v1 %s output normalized to a different semantic record\n got: %s\nwant: %s", mapping.legacy, got, want) + } + } + + if _, err := Build(map[string]any{"baseContext": baseV2, "currentContext": currentV2, "diffId": "consumer.mixed.diff", "schemaVersion": json.Number("1")}); err == nil { + t.Fatal("Build accepted a v1 envelope with v2 contexts") + } + mixedOutput := cloneRequirementRecord(t, v2Output) + mixedOutput["baseBaselineVerification"] = "unverified" + if _, err := AdmitOutput(mixedOutput, currentV2["snapshotId"].(string)); err == nil { + t.Fatal("AdmitOutput accepted mixed v1/v2 digest coverage fields") + } + v1WireOutput["baseExpectedDigestCoverage"] = "all" + if _, err := AdmitOutput(v1WireOutput, currentV2["snapshotId"].(string)); err == nil { + t.Fatal("AdmitOutput accepted mixed v1/v2 digest coverage fields under schema v1") + } +} + func TestBuildOutputIsClosedUnderAdmissionForMultipleChanges(t *testing.T) { baseline := contextFixture(t, "The system preserves the baseline.") current := contextFixture(t, "The system preserves the revised invariant.") @@ -58,7 +121,7 @@ func TestBuildOutputIsClosedUnderAdmissionForMultipleChanges(t *testing.T) { requirement["updatePolicy"].(map[string]any)["reviewOwnerId"] = "consumer.next-owner" resignContextFixture(t, current) - output, err := Build(map[string]any{"baseContext": baseline, "currentContext": current, "diffId": "consumer.multi-field.diff", "schemaVersion": json.Number("1")}) + output, err := Build(map[string]any{"baseContext": baseline, "currentContext": current, "diffId": "consumer.multi-field.diff", "schemaVersion": json.Number("2")}) if err != nil { t.Fatal(err) } @@ -100,7 +163,7 @@ func TestOwnerFilterPreservesStableIdentityAcrossOwnershipChange(t *testing.T) { } { t.Run(item.ownerID, func(t *testing.T) { output, err := Build(map[string]any{ - "baseContext": baseline, "currentContext": current, "diffId": "consumer.owner-transition.diff", "schemaVersion": json.Number("1"), + "baseContext": baseline, "currentContext": current, "diffId": "consumer.owner-transition.diff", "schemaVersion": json.Number("2"), "query": map[string]any{"ownerIds": []any{item.ownerID}}, }) if err != nil { @@ -154,7 +217,7 @@ func TestBuildCoversCompleteRequirementChangeAlgebra(t *testing.T) { output, err := Build(map[string]any{ "baseContext": baseline, "currentContext": current, - "diffId": "consumer.complete-algebra.diff", "schemaVersion": json.Number("1"), + "diffId": "consumer.complete-algebra.diff", "schemaVersion": json.Number("2"), }) if err != nil { t.Fatal(err) @@ -200,7 +263,7 @@ func TestBuildCoversCompleteRequirementChangeAlgebra(t *testing.T) { func TestBuildIncludesDeferralAndAdmissionBindsChangeIdentity(t *testing.T) { base := deferredContextFixture(t, "Review after the migration window.") current := deferredContextFixture(t, "Review after the compatibility window.") - output, err := Build(map[string]any{"baseContext": base, "currentContext": current, "diffId": "consumer.deferral.diff", "schemaVersion": json.Number("1")}) + output, err := Build(map[string]any{"baseContext": base, "currentContext": current, "diffId": "consumer.deferral.diff", "schemaVersion": json.Number("2")}) if err != nil { t.Fatal(err) } @@ -250,7 +313,29 @@ func contextFixture(t *testing.T, invariant string) map[string]any { if err != nil { t.Fatal(err) } - return requirementcontext.SnapshotValue(requirementcontext.Snapshot{BaselineVerification: "unverified", CatalogID: "consumer.context", Projections: projections, SnapshotID: digest.SHA256TextRef(string(encoded)), Sources: sources}) + return requirementcontext.SnapshotValue(requirementcontext.Snapshot{CatalogID: "consumer.context", ExpectedDigestCoverage: "none", Projections: projections, SnapshotID: digest.SHA256TextRef(string(encoded)), Sources: sources}) +} + +func v1DiffContextFixture(t *testing.T, value map[string]any) map[string]any { + t.Helper() + v1 := cloneRequirementRecord(t, value) + delete(v1, "expectedDigestCoverage") + v1["baselineVerification"] = "unverified" + v1["nonClaims"] = []any{ + "Requirement context is a derived projection and is not requirement, proof, coverage, merge, release, rollout, or readiness authority.", + "Requirement context does not execute native witnesses or prove source freshness after composition.", + } + v1["schemaVersion"] = json.Number("1") + return v1 +} + +func stableBytes(t *testing.T, value any) []byte { + t.Helper() + encoded, err := stablejson.Marshal(value) + if err != nil { + t.Fatal(err) + } + return encoded } func resignContextFixture(t *testing.T, value map[string]any) { diff --git a/internal/command/requirementdiff/v1_adapter.go b/internal/command/requirementdiff/v1_adapter.go new file mode 100644 index 0000000..79c6961 --- /dev/null +++ b/internal/command/requirementdiff/v1_adapter.go @@ -0,0 +1,53 @@ +package requirementdiff + +import ( + "encoding/json" + "fmt" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" +) + +func admitV1DiffInput(record map[string]any) error { + return requireContextVersions(record, 1) +} + +func admitV1Output(record map[string]any, currentSnapshotID string) (map[string]any, error) { + if err := admit.KnownKeys(record, []string{"baseBaselineVerification", "baseSnapshotId", "changeCount", "changes", "currentBaselineVerification", "currentSnapshotId", "diffId", "diffKind", "nonClaims", "schemaVersion"}, "requirement semantic diff output v1"); err != nil { + return nil, err + } + if !admit.JSONNumberEquals(record["schemaVersion"], 1) && record["schemaVersion"] != 1 { + return nil, fmt.Errorf("requirement semantic diff output schemaVersion must be 1") + } + if record["diffKind"] != "proofkit.requirement-semantic-diff" || record["currentSnapshotId"] != currentSnapshotID { + return nil, fmt.Errorf("requirement semantic diff output identity is invalid") + } + coverage := map[string]string{} + for _, key := range []string{"baseBaselineVerification", "currentBaselineVerification"} { + value, err := admit.Enum(record[key], map[string]struct{}{"partially_verified": {}, "unverified": {}, "verified": {}}, "requirement semantic diff output v1 "+key) + if err != nil { + return nil, err + } + coverage[key] = v1CoverageValue(value) + } + admitted, err := admitOutputRecord(record) + if err != nil { + return nil, err + } + delete(admitted, "baseBaselineVerification") + delete(admitted, "currentBaselineVerification") + admitted["baseExpectedDigestCoverage"] = coverage["baseBaselineVerification"] + admitted["currentExpectedDigestCoverage"] = coverage["currentBaselineVerification"] + admitted["schemaVersion"] = json.Number("2") + return canonicalCopy(admitted) +} + +func v1CoverageValue(value string) string { + switch value { + case "unverified": + return "none" + case "partially_verified": + return "partial" + default: + return "all" + } +} diff --git a/internal/command/requirementgraph/requirementgraph_test.go b/internal/command/requirementgraph/requirementgraph_test.go index bb5b9bb..b8e4e2a 100644 --- a/internal/command/requirementgraph/requirementgraph_test.go +++ b/internal/command/requirementgraph/requirementgraph_test.go @@ -285,6 +285,31 @@ func TestBuildRequiresInputSchemaVersion2(t *testing.T) { } } +func TestBuildConsumesNormalizedV1AndV2ContextSnapshots(t *testing.T) { + v2Context := graphContextFixture(t) + v2Output, err := Build(map[string]any{"context": v2Context, "graphId": "consumer.traceability.context-migration", "schemaVersion": json.Number("2")}) + if err != nil { + t.Fatal(err) + } + v1Context := decodedGraphOutput(t, v2Context) + delete(v1Context, "expectedDigestCoverage") + v1Context["baselineVerification"] = "unverified" + v1Context["nonClaims"] = []any{ + "Requirement context is a derived projection and is not requirement, proof, coverage, merge, release, rollout, or readiness authority.", + "Requirement context does not execute native witnesses or prove source freshness after composition.", + } + v1Context["schemaVersion"] = json.Number("1") + v1Output, err := Build(map[string]any{"context": v1Context, "graphId": "consumer.traceability.context-migration", "schemaVersion": json.Number("2")}) + if err != nil { + t.Fatalf("Build(v1 context) error = %v", err) + } + got, _ := stablejson.Marshal(v1Output) + want, _ := stablejson.Marshal(v2Output) + if !bytes.Equal(got, want) { + t.Fatalf("v1 and v2 contexts produced different graph records\n got: %s\nwant: %s", got, want) + } +} + func TestGraphIdentityIsPermutationStableAndRelationsAreTyped(t *testing.T) { input := graphPermutationInput(t) first, err := Build(input) @@ -452,5 +477,5 @@ func graphContextFixture(t *testing.T) map[string]any { if err != nil { t.Fatal(err) } - return requirementcontext.SnapshotValue(requirementcontext.Snapshot{BaselineVerification: "unverified", CatalogID: "consumer.context", Projections: projections, SnapshotID: digest.SHA256TextRef(string(encoded)), Sources: sources}) + return requirementcontext.SnapshotValue(requirementcontext.Snapshot{CatalogID: "consumer.context", ExpectedDigestCoverage: "none", Projections: projections, SnapshotID: digest.SHA256TextRef(string(encoded)), Sources: sources}) } diff --git a/internal/command/specproofbundleadmission/specproofbundleadmission_test.go b/internal/command/specproofbundleadmission/specproofbundleadmission_test.go index 385d8fb..a176662 100644 --- a/internal/command/specproofbundleadmission/specproofbundleadmission_test.go +++ b/internal/command/specproofbundleadmission/specproofbundleadmission_test.go @@ -129,7 +129,7 @@ func TestBuildRejectsUnknownReceiptSelector(t *testing.T) { func TestBuildRejectsReceiptKindThatDoesNotCoverWitnessSelector(t *testing.T) { input := validBundleInput(t) receipt := validProofReceipt() - receipt["receiptKind"] = "proofkit.go-test" + receipt["receiptKind"] = commandOutsideRequirement(t, input, "REQ-PROOFKIT-PACKAGE-004") receipt["witnessSelectors"] = []any{"REQ-PROOFKIT-PACKAGE-004"} input["receiptAdmission"] = proofReceiptChild(t, []any{receipt}) @@ -143,6 +143,34 @@ func TestBuildRejectsReceiptKindThatDoesNotCoverWitnessSelector(t *testing.T) { assertFailedRuleMessage(t, record.RuleResults, "proofkit.spec-proof-bundle-admission.failure.", "does not cover witness selector") } +func commandOutsideRequirement(t *testing.T, input map[string]any, requirementID string) string { + t.Helper() + bindingFile := input["requirementBindings"].(map[string]any) + covered := map[string]struct{}{} + all := []string{} + seen := map[string]struct{}{} + for _, raw := range bindingFile["bindings"].([]any) { + binding := raw.(map[string]any) + for _, commandRaw := range binding["commandIds"].([]any) { + commandID := commandRaw.(string) + if _, ok := seen[commandID]; !ok { + seen[commandID] = struct{}{} + all = append(all, commandID) + } + if binding["requirementId"] == requirementID { + covered[commandID] = struct{}{} + } + } + } + for _, commandID := range all { + if _, ok := covered[commandID]; !ok { + return commandID + } + } + t.Fatalf("fixture has no command outside requirement %s", requirementID) + return "" +} + func TestBuildRejectsBindingThatOmitsWitnessCommandEnvironment(t *testing.T) { input := validBundleInput(t) requirementBindings := input["requirementBindings"].(map[string]any) diff --git a/internal/command/stackpreset/preset_ids_generated.go b/internal/command/stackpreset/preset_ids_generated.go new file mode 100644 index 0000000..a9270de --- /dev/null +++ b/internal/command/stackpreset/preset_ids_generated.go @@ -0,0 +1,6 @@ +// Code generated by internal/tools/commandcontractgen; DO NOT EDIT. +package stackpreset + +const presetContractSourceSHA256 = "90025ee656c5d90b1909423776121f12ce28b74b52d0eae2c34dd1bc55d2c6e9" + +var presetIDs = []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"} diff --git a/internal/command/stackpreset/stackpreset.go b/internal/command/stackpreset/stackpreset.go index 0c1e7ac..a972a75 100644 --- a/internal/command/stackpreset/stackpreset.go +++ b/internal/command/stackpreset/stackpreset.go @@ -2,21 +2,13 @@ package stackpreset import ( "fmt" + "strings" "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" "github.com/research-engineering/agentic-proofkit/internal/kernel/report" ) -var presetIDs = []string{ - "agentic_runtime_repo", - "generated_docs_contract_repo", - "python_service", - "python_typescript_service", - "typescript_monorepo", - "typescript_workspace", -} - var presetNonClaims = []string{ "Stack presets do not read repository state.", "Stack presets do not execute native witnesses.", @@ -31,7 +23,7 @@ type preset struct { StarterEnvironmentClasses []string StarterProofLikePaths []string StarterWitnessKinds []string - SuggestedCommands []string + SuggestedCommandArgs [][]string } type Profile struct { @@ -52,7 +44,7 @@ var presets = map[string]preset{ StarterEnvironmentClasses: []string{"local-typecheck", "local-unit", "credentialed-live"}, StarterProofLikePaths: []string{"docs/specs/**/*.md", "packages/**/test/**/*.test.ts"}, StarterWitnessKinds: []string{"contract", "falsification", "local-unit", "live-preflight"}, - SuggestedCommands: []string{cliexec.DisplayCommand("stack-preset", "--preset", "agentic_runtime_repo"), cliexec.DisplayCommand("requirement-bindings", "--input", "docs/contracts/requirement-proof-bindings.v1.json"), cliexec.DisplayCommand("proof-slice", "--input", "docs/contracts/requirement-proof-bindings.v1.json")}, + SuggestedCommandArgs: [][]string{{"stack-preset", "--preset", "agentic_runtime_repo"}, {"requirement-bindings", "--input", "docs/contracts/requirement-proof-bindings.v1.json"}, {"proof-slice", "--input", "docs/contracts/requirement-proof-bindings.v1.json"}}, }, "generated_docs_contract_repo": { Purpose: "Starter profile for repositories with generated lookup docs and machine-readable proof contracts.", @@ -61,7 +53,7 @@ var presets = map[string]preset{ StarterEnvironmentClasses: []string{"local-docs", "local-generated-artifacts"}, StarterProofLikePaths: []string{"docs/**/*.md", "docs/contracts/**/*"}, StarterWitnessKinds: []string{"docs-surface", "generated-artifact", "schema"}, - SuggestedCommands: []string{cliexec.DisplayCommand("stack-preset", "--preset", "generated_docs_contract_repo"), cliexec.DisplayCommand("evidence-graph", "--input", "docs/contracts/requirement-proof-bindings.v1.json")}, + SuggestedCommandArgs: [][]string{{"stack-preset", "--preset", "generated_docs_contract_repo"}, {"evidence-graph", "--input", "docs/contracts/requirement-proof-bindings.v1.json"}}, }, "python_service": { Purpose: "Starter profile for Python services adopting proofkit through CLI reports.", @@ -70,7 +62,7 @@ var presets = map[string]preset{ StarterEnvironmentClasses: []string{"local-pytest", "local-ruff"}, StarterProofLikePaths: []string{"docs/specs/**/*.md", "tests/**/*.py"}, StarterWitnessKinds: []string{"contract", "falsification", "pytest"}, - SuggestedCommands: []string{cliexec.DisplayCommand("stack-preset", "--preset", "python_service"), cliexec.DisplayCommand("witness-scheduler-plan", "--input", "proofkit/witness-plan.json")}, + SuggestedCommandArgs: [][]string{{"stack-preset", "--preset", "python_service"}, {"witness-scheduler-plan", "--input", "proofkit/witness-plan.json"}}, }, "python_typescript_service": { Purpose: "Starter profile for services with Python runtime code and TypeScript tooling or UI packages.", @@ -79,7 +71,7 @@ var presets = map[string]preset{ StarterEnvironmentClasses: []string{"local-bun", "local-pytest", "local-typecheck"}, StarterProofLikePaths: []string{"docs/specs/**/*.md", "tests/**/*.py", "packages/**/test/**/*.test.ts"}, StarterWitnessKinds: []string{"contract", "falsification", "pytest", "typescript-unit"}, - SuggestedCommands: []string{cliexec.DisplayCommand("stack-preset", "--preset", "python_typescript_service"), cliexec.DisplayCommand("selective-gate-plan", "--input", "proofkit/selective-gate-plan.json")}, + SuggestedCommandArgs: [][]string{{"stack-preset", "--preset", "python_typescript_service"}, {"selective-gate-plan", "--input", "proofkit/selective-gate-plan.json"}}, }, "typescript_monorepo": { Purpose: "Starter profile for TypeScript monorepos with package graph and public API boundaries.", @@ -88,7 +80,7 @@ var presets = map[string]preset{ StarterEnvironmentClasses: []string{"local-bun", "local-typecheck"}, StarterProofLikePaths: []string{"docs/specs/**/*.md", "packages/**/test/**/*.test.ts"}, StarterWitnessKinds: []string{"contract", "package-test", "public-api", "typecheck"}, - SuggestedCommands: []string{cliexec.DisplayCommand("stack-preset", "--preset", "typescript_monorepo"), cliexec.DisplayCommand("selective-gate-plan", "--input", "proofkit/selective-gate-plan.json")}, + SuggestedCommandArgs: [][]string{{"stack-preset", "--preset", "typescript_monorepo"}, {"selective-gate-plan", "--input", "proofkit/selective-gate-plan.json"}}, }, "typescript_workspace": { Purpose: "Starter profile for a TypeScript workspace adopting proofkit one module at a time.", @@ -97,14 +89,18 @@ var presets = map[string]preset{ StarterEnvironmentClasses: []string{"local-bun", "local-typecheck"}, StarterProofLikePaths: []string{"docs/specs/**/*.md", "src/**/*.test.ts", "test/**/*.test.ts"}, StarterWitnessKinds: []string{"contract", "falsification", "unit", "typecheck"}, - SuggestedCommands: []string{cliexec.DisplayCommand("stack-preset", "--preset", "typescript_workspace"), cliexec.DisplayCommand("gradual-adoption-bootstrap", "--input", "proofkit/bootstrap.json")}, + SuggestedCommandArgs: [][]string{{"stack-preset", "--preset", "typescript_workspace"}, {"gradual-adoption-bootstrap", "--input", "proofkit/bootstrap.json"}}, }, } func Build(presetID string) (report.Record, error) { - preset, ok := ProfileFor(presetID) + return BuildWithRenderer(presetID, cliexec.PathRenderer()) +} + +func BuildWithRenderer(presetID string, renderer cliexec.Renderer) (report.Record, error) { + preset, ok := ProfileForWithRenderer(presetID, renderer) if !ok { - return report.Record{}, fmt.Errorf("--preset requires a known stack preset id") + return report.Record{}, fmt.Errorf("--preset requires one of: %s", strings.Join(IDs(), ", ")) } return report.Record{ SchemaVersion: 1, @@ -162,7 +158,15 @@ func IsPresetID(value string) bool { return false } +func IDs() []string { + return append([]string(nil), presetIDs...) +} + func ProfileFor(presetID string) (Profile, bool) { + return ProfileForWithRenderer(presetID, cliexec.PathRenderer()) +} + +func ProfileForWithRenderer(presetID string, renderer cliexec.Renderer) (Profile, bool) { preset, ok := presets[presetID] if !ok { return Profile{}, false @@ -174,10 +178,18 @@ func ProfileFor(presetID string) (Profile, bool) { StarterEnvironmentClasses: append([]string{}, preset.StarterEnvironmentClasses...), StarterProofLikePaths: append([]string{}, preset.StarterProofLikePaths...), StarterWitnessKinds: append([]string{}, preset.StarterWitnessKinds...), - SuggestedCommands: append([]string{}, preset.SuggestedCommands...), + SuggestedCommands: renderCommands(renderer, preset.SuggestedCommandArgs), }, true } +func renderCommands(renderer cliexec.Renderer, commands [][]string) []string { + rendered := make([]string, 0, len(commands)) + for _, command := range commands { + rendered = append(rendered, renderer.DisplayCommand(command...)) + } + return rendered +} + func stringsToAny(values []string) []any { result := make([]any, 0, len(values)) for _, value := range values { diff --git a/internal/command/stackpreset/stackpreset_test.go b/internal/command/stackpreset/stackpreset_test.go index e159354..df6a3cb 100644 --- a/internal/command/stackpreset/stackpreset_test.go +++ b/internal/command/stackpreset/stackpreset_test.go @@ -51,6 +51,11 @@ func TestPresetInventoryIsCompleteDeterministicAndDefensivelyCopied(t *testing.T t.Fatalf("preset %s missing from presetIDs", presetID) } } + publicIDs := IDs() + publicIDs[0] = "mutated" + if IDs()[0] == "mutated" { + t.Fatal("IDs leaked mutable generated preset inventory") + } original, ok := ProfileFor("typescript_workspace") if !ok { diff --git a/internal/kernel/cliexec/cliexec.go b/internal/kernel/cliexec/cliexec.go index aeabc61..07bd608 100644 --- a/internal/kernel/cliexec/cliexec.go +++ b/internal/kernel/cliexec/cliexec.go @@ -1,18 +1,101 @@ package cliexec -import "strings" +import ( + "fmt" + "path/filepath" + "strings" + "unicode" -const BinaryName = "agentic-proofkit" + "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" +) -func DisplayCommand(args ...string) string { - parts := make([]string, 0, len(args)+1) - parts = append(parts, BinaryName) - for _, arg := range args { - parts = append(parts, shellQuote(arg)) +const ( + BinaryName = "agentic-proofkit" + LauncherProfileEnvironment = "AGENTIC_PROOFKIT_LAUNCHER_PROFILE" + PythonExecutableEnvironment = "AGENTIC_PROOFKIT_PYTHON_EXECUTABLE" + + ProfileNPMOffline = "npm_offline" + ProfilePath = "path" + ProfilePythonModule = "python_module" +) + +type Renderer struct { + profile string + pythonExecutable string +} + +func PathRenderer() Renderer { + return Renderer{profile: ProfilePath} +} + +func AdmitLauncherProfile(profile string, pythonExecutable string) (Renderer, error) { + switch profile { + case "", ProfilePath: + if pythonExecutable != "" { + return Renderer{}, fmt.Errorf("%s must be empty unless %s is %s", PythonExecutableEnvironment, LauncherProfileEnvironment, ProfilePythonModule) + } + return PathRenderer(), nil + case ProfileNPMOffline: + if pythonExecutable != "" { + return Renderer{}, fmt.Errorf("%s must be empty unless %s is %s", PythonExecutableEnvironment, LauncherProfileEnvironment, ProfilePythonModule) + } + return Renderer{profile: ProfileNPMOffline}, nil + case ProfilePythonModule: + if pythonExecutable == "" || !filepath.IsAbs(pythonExecutable) { + return Renderer{}, fmt.Errorf("%s must be a non-empty absolute path when %s is %s", PythonExecutableEnvironment, LauncherProfileEnvironment, ProfilePythonModule) + } + if admit.ContainsSecretLikeValue(pythonExecutable) || strings.IndexFunc(pythonExecutable, isControlOrFormat) >= 0 { + return Renderer{}, fmt.Errorf("%s must not contain secret-like or control values", PythonExecutableEnvironment) + } + return Renderer{profile: ProfilePythonModule, pythonExecutable: pythonExecutable}, nil + default: + return Renderer{}, fmt.Errorf("%s must be empty, %s, %s, or %s", LauncherProfileEnvironment, ProfilePath, ProfileNPMOffline, ProfilePythonModule) + } +} + +func isControlOrFormat(character rune) bool { + return unicode.In(character, unicode.Cc, unicode.Cf) +} + +func (renderer Renderer) Profile() string { + if renderer.profile == "" { + return ProfilePath + } + return renderer.profile +} + +func (renderer Renderer) DisplayCommand(args ...string) string { + return DisplayArgv(renderer.Argv(args...)) +} + +func DisplayArgv(argv []string) string { + parts := make([]string, 0, len(argv)) + for _, value := range argv { + parts = append(parts, shellQuote(value)) } return strings.Join(parts, " ") } +func (renderer Renderer) Argv(args ...string) []string { + argv := renderer.invocationPrefix() + return append(argv, args...) +} + +func DisplayCommand(args ...string) string { + return PathRenderer().DisplayCommand(args...) +} + +func (renderer Renderer) invocationPrefix() []string { + switch renderer.Profile() { + case ProfileNPMOffline: + return []string{"npm", "exec", "--offline", "--", BinaryName} + case ProfilePythonModule: + return []string{renderer.pythonExecutable, "-m", "agentic_proofkit"} + default: + return []string{BinaryName} + } +} + func shellQuote(value string) string { if shellSafeToken(value) { return value diff --git a/internal/kernel/cliexec/cliexec_test.go b/internal/kernel/cliexec/cliexec_test.go index 0de0604..f59a5bb 100644 --- a/internal/kernel/cliexec/cliexec_test.go +++ b/internal/kernel/cliexec/cliexec_test.go @@ -1,6 +1,14 @@ package cliexec -import "testing" +import ( + "path/filepath" + "strings" + "testing" + "unicode" + "unicode/utf8" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" +) func TestDisplayCommandQuotesAmbiguousArgs(t *testing.T) { cases := []struct { @@ -30,3 +38,182 @@ func TestDisplayCommandQuotesAmbiguousArgs(t *testing.T) { } } } + +func TestLauncherProfileAdmissionMatrix(t *testing.T) { + python := "/tmp/proofkit venv/bin/python" + valid := []struct { + name string + profile string + pythonExecutable string + wantProfile string + wantCommand string + }{ + { + name: "absent fields use path", + wantProfile: ProfilePath, + wantCommand: "agentic-proofkit self-check --input proofkit/input.json", + }, + { + name: "explicit path", + profile: ProfilePath, + wantProfile: ProfilePath, + wantCommand: "agentic-proofkit self-check --input proofkit/input.json", + }, + { + name: "npm offline", + profile: ProfileNPMOffline, + wantProfile: ProfileNPMOffline, + wantCommand: "npm exec --offline -- agentic-proofkit self-check --input proofkit/input.json", + }, + { + name: "python module", + profile: ProfilePythonModule, + pythonExecutable: python, + wantProfile: ProfilePythonModule, + wantCommand: "'/tmp/proofkit venv/bin/python' -m agentic_proofkit self-check --input proofkit/input.json", + }, + } + for _, item := range valid { + t.Run(item.name, func(t *testing.T) { + renderer, err := AdmitLauncherProfile(item.profile, item.pythonExecutable) + if err != nil { + t.Fatalf("AdmitLauncherProfile(%q, %q) error=%v", item.profile, item.pythonExecutable, err) + } + if renderer.Profile() != item.wantProfile { + t.Fatalf("profile=%q want %q", renderer.Profile(), item.wantProfile) + } + if got := renderer.DisplayCommand("self-check", "--input", "proofkit/input.json"); got != item.wantCommand { + t.Fatalf("command=%q want %q", got, item.wantCommand) + } + }) + } + + invalid := []struct { + name string + profile string + pythonExecutable string + want string + mustNotContain string + }{ + { + name: "absent profile with executable", + pythonExecutable: python, + want: PythonExecutableEnvironment + " must be empty", + }, + { + name: "path with executable", + profile: ProfilePath, + pythonExecutable: python, + want: PythonExecutableEnvironment + " must be empty", + }, + { + name: "npm with executable", + profile: ProfileNPMOffline, + pythonExecutable: python, + want: PythonExecutableEnvironment + " must be empty", + }, + { + name: "python without executable", + profile: ProfilePythonModule, + want: PythonExecutableEnvironment + " must be a non-empty absolute path", + }, + { + name: "python with relative executable", + profile: ProfilePythonModule, + pythonExecutable: "venv/bin/python", + want: PythonExecutableEnvironment + " must be a non-empty absolute path", + }, + { + name: "unknown profile", + profile: "network_fallback", + want: LauncherProfileEnvironment + " must be empty", + }, + { + name: "unknown profile with executable", + profile: "network_fallback", + pythonExecutable: python, + want: LauncherProfileEnvironment + " must be empty", + }, + { + name: "python with secret-like path component", + profile: ProfilePythonModule, + pythonExecutable: filepath.Join(string(filepath.Separator), "tmp", strings.Join([]string{"gh", "p_", "12345678901234567890"}, ""), "bin", "python"), + want: PythonExecutableEnvironment + " must not contain secret-like or control values", + mustNotContain: "12345678901234567890", + }, + { + name: "python with control rune", + profile: ProfilePythonModule, + pythonExecutable: string(filepath.Separator) + "tmp/line\nbreak/bin/python", + want: PythonExecutableEnvironment + " must not contain secret-like or control values", + mustNotContain: "line\nbreak", + }, + } + for _, item := range invalid { + t.Run(item.name, func(t *testing.T) { + _, err := AdmitLauncherProfile(item.profile, item.pythonExecutable) + if err == nil || !strings.Contains(err.Error(), item.want) { + t.Fatalf("AdmitLauncherProfile(%q, %q) error=%v, want %q", item.profile, item.pythonExecutable, err, item.want) + } + if item.mustNotContain != "" && strings.Contains(err.Error(), item.mustNotContain) { + t.Fatalf("AdmitLauncherProfile error disclosed rejected value") + } + }) + } + + for character := rune(0); character <= unicode.MaxRune; character++ { + if !utf8.ValidRune(character) { + continue + } + wantRejected := unicode.In(character, unicode.Cc, unicode.Cf) + if got := isControlOrFormat(character); got != wantRejected { + t.Fatalf("isControlOrFormat(U+%04X)=%t, want %t", character, got, wantRejected) + } + if !wantRejected { + continue + } + rejected := string(filepath.Separator) + "tmp/" + string(character) + "spoof/bin/python" + _, err := AdmitLauncherProfile(ProfilePythonModule, rejected) + if err == nil || !strings.Contains(err.Error(), PythonExecutableEnvironment+" must not contain secret-like or control values") { + t.Fatalf("AdmitLauncherProfile accepted control or format U+%04X: %v", character, err) + } + if strings.Contains(err.Error(), rejected) || strings.ContainsRune(err.Error(), character) { + t.Fatalf("AdmitLauncherProfile error disclosed rejected control or format U+%04X", character) + } + } + + for _, fixture := range admit.ReportVisibleRedactionFixtures() { + t.Run("python rejects and does not disclose "+fixture.Name, func(t *testing.T) { + separator := string(filepath.Separator) + pythonExecutable := separator + "tmp" + separator + fixture.Input + separator + "bin" + separator + "python" + _, err := AdmitLauncherProfile(ProfilePythonModule, pythonExecutable) + if err == nil || !strings.Contains(err.Error(), PythonExecutableEnvironment+" must not contain secret-like or control values") { + t.Fatalf("AdmitLauncherProfile accepted a report-visible secret class: %v", err) + } + for _, needle := range fixture.SensitiveNeedles { + if strings.Contains(err.Error(), needle) { + t.Fatalf("AdmitLauncherProfile error disclosed rejected value") + } + } + }) + } +} + +func TestRendererArgvAndDisplayShareOneImmutableInvocation(t *testing.T) { + renderer, err := AdmitLauncherProfile(ProfilePythonModule, "/tmp/proofkit venv/bin/python") + if err != nil { + t.Fatal(err) + } + want := []string{"/tmp/proofkit venv/bin/python", "-m", "agentic_proofkit", "self-check", "--input", "proofkit/input.json"} + argv := renderer.Argv("self-check", "--input", "proofkit/input.json") + if strings.Join(argv, "\x00") != strings.Join(want, "\x00") { + t.Fatalf("argv=%q, want %q", argv, want) + } + if got := DisplayArgv(argv); got != renderer.DisplayCommand("self-check", "--input", "proofkit/input.json") { + t.Fatalf("display argv=%q, renderer display=%q", got, renderer.DisplayCommand("self-check", "--input", "proofkit/input.json")) + } + argv[0] = "mutated" + if got := renderer.Argv("self-check")[0]; got != "/tmp/proofkit venv/bin/python" { + t.Fatalf("renderer retained caller mutation: %q", got) + } +} diff --git a/internal/testsupport/browserfixture/fixture.go b/internal/testsupport/browserfixture/fixture.go index ceaa252..9520d50 100644 --- a/internal/testsupport/browserfixture/fixture.go +++ b/internal/testsupport/browserfixture/fixture.go @@ -23,7 +23,7 @@ func Workspace() (map[string]any, error) { } diffInput := map[string]any{ "baseContext": base, "currentContext": current, "diffId": "browser.fixture.diff", - "query": map[string]any{"requirementIds": []any{RequirementID}}, "schemaVersion": json.Number("1"), + "query": map[string]any{"requirementIds": []any{RequirementID}}, "schemaVersion": json.Number("2"), } code := "package retry\n\nfunc Retry() {}\n" start := strings.Index(code, "func Retry") @@ -42,7 +42,7 @@ func Workspace() (map[string]any, error) { }, "context": current, "graphId": "browser.fixture.graph", "schemaVersion": json.Number("2"), } - return map[string]any{"context": current, "diffInput": diffInput, "graphInput": graphInput, "schemaVersion": json.Number("1"), "workspaceId": "browser.fixture.workspace"}, nil + return map[string]any{"context": current, "diffInput": diffInput, "graphInput": graphInput, "schemaVersion": json.Number("2"), "workspaceId": "browser.fixture.workspace"}, nil } func snapshot(invariant string) (map[string]any, error) { @@ -66,5 +66,5 @@ func snapshot(invariant string) (map[string]any, error) { if err != nil { return nil, err } - return requirementcontext.SnapshotValue(requirementcontext.Snapshot{BaselineVerification: "unverified", CatalogID: "browser.fixture.context", Projections: projections, SnapshotID: digest.SHA256TextRef(string(encoded)), Sources: sources}), nil + return requirementcontext.SnapshotValue(requirementcontext.Snapshot{CatalogID: "browser.fixture.context", ExpectedDigestCoverage: "none", Projections: projections, SnapshotID: digest.SHA256TextRef(string(encoded)), Sources: sources}), nil } diff --git a/internal/tools/browserproofverify/artifact_paths.go b/internal/tools/browserproofverify/artifact_paths.go index 0f5a1c7..fdf243a 100644 --- a/internal/tools/browserproofverify/artifact_paths.go +++ b/internal/tools/browserproofverify/artifact_paths.go @@ -70,6 +70,16 @@ func cleanupBrowserProofRun(root, runDirectory string) error { return rootFS.RemoveAll(filepath.FromSlash(runDirectory)) } +func finalizeBrowserProofRun(root, runDirectory string, resultErr error) error { + if resultErr != nil { + return resultErr + } + if err := cleanupBrowserProofRun(root, runDirectory); err != nil { + return fmt.Errorf("clean browser proof run directory: %w", err) + } + return nil +} + func readRootedJSON(root, path string, maxBytes int64) (any, error) { rootFS, err := os.OpenRoot(root) if err != nil { diff --git a/internal/tools/browserproofverify/artifact_paths_test.go b/internal/tools/browserproofverify/artifact_paths_test.go index ed6a41a..12d9edf 100644 --- a/internal/tools/browserproofverify/artifact_paths_test.go +++ b/internal/tools/browserproofverify/artifact_paths_test.go @@ -1,6 +1,7 @@ package main import ( + "errors" "os" "path/filepath" "strings" @@ -28,6 +29,36 @@ func TestPrepareBrowserProofRunCreatesAConfinedDisposableDirectory(t *testing.T) } } +func TestFinalizeBrowserProofRunCleansSuccessAndRetainsFailureDiagnostics(t *testing.T) { + root := t.TempDir() + successPaths, err := prepareBrowserProofRun(root) + if err != nil { + t.Fatal(err) + } + if err := finalizeBrowserProofRun(root, successPaths.RunDirectory, nil); err != nil { + t.Fatal(err) + } + if _, err := os.Lstat(filepath.Join(root, filepath.FromSlash(successPaths.RunDirectory))); !os.IsNotExist(err) { + t.Fatalf("successful run directory survived cleanup: %v", err) + } + + failurePaths, err := prepareBrowserProofRun(root) + if err != nil { + t.Fatal(err) + } + diagnosticPath := filepath.Join(root, filepath.FromSlash(failurePaths.RunDirectory), "playwright-report.json") + if err := os.WriteFile(diagnosticPath, []byte("{}\n"), 0o600); err != nil { + t.Fatal(err) + } + runErr := errors.New("browser proof failed") + if err := finalizeBrowserProofRun(root, failurePaths.RunDirectory, runErr); !errors.Is(err, runErr) { + t.Fatalf("finalizeBrowserProofRun() error=%v, want original failure", err) + } + if content, err := os.ReadFile(diagnosticPath); err != nil || string(content) != "{}\n" { + t.Fatalf("failed run diagnostic was not retained: content=%q err=%v", content, err) + } +} + func TestPrepareBrowserProofRunRejectsArtifactRootSymlink(t *testing.T) { root := t.TempDir() external := t.TempDir() diff --git a/internal/tools/browserproofverify/main.go b/internal/tools/browserproofverify/main.go index 19e764a..0f03fc4 100644 --- a/internal/tools/browserproofverify/main.go +++ b/internal/tools/browserproofverify/main.go @@ -4,7 +4,6 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" - "errors" "fmt" "os" "os/exec" @@ -115,9 +114,7 @@ func runBrowserProof(root string) (resultErr error) { return err } defer func() { - if cleanupErr := cleanupBrowserProofRun(root, runPaths.RunDirectory); cleanupErr != nil { - resultErr = errors.Join(resultErr, fmt.Errorf("clean browser proof run directory: %w", cleanupErr)) - } + resultErr = finalizeBrowserProofRun(root, runPaths.RunDirectory, resultErr) }() command := exec.Command("node", manifest.WriterPath) command.Dir = root diff --git a/internal/tools/commandcontractgen/condition_model.go b/internal/tools/commandcontractgen/condition_model.go new file mode 100644 index 0000000..fa64079 --- /dev/null +++ b/internal/tools/commandcontractgen/condition_model.go @@ -0,0 +1,131 @@ +package main + +import ( + "errors" + "fmt" + "regexp" + "slices" + "strings" +) + +var cliFlagConditionAtomPattern = regexp.MustCompile(`^(--[a-z][a-z0-9-]*)=([a-z0-9][a-z0-9-]*)$`) + +const cliFlagConditionModelDefinitionID = "proofkit.adoption-contract-envelope.output.v1.root-shape" +const cliFlagConditionModelCommand = "adoption-contract-envelope" +const cliFlagConditionModelDirection = "output" + +type rootShapeConditionCase struct { + Dimensions map[string]string + Raw string + VariantID string +} + +func parseCLIFlagCondition(condition string) (map[string]string, error) { + atoms := strings.Fields(condition) + if len(atoms) == 0 { + return nil, errors.New("cli_flag_conjunction_v1 condition must contain at least one atom") + } + if condition != strings.Join(atoms, " ") { + return nil, errors.New("cli_flag_conjunction_v1 condition must use one ASCII space between atoms") + } + dimensions := make(map[string]string, len(atoms)) + previousFlag := "" + for _, atom := range atoms { + match := cliFlagConditionAtomPattern.FindStringSubmatch(atom) + if match == nil { + return nil, fmt.Errorf("invalid cli_flag_conjunction_v1 atom %q", atom) + } + flagName := match[1] + if previousFlag != "" && previousFlag >= flagName { + return nil, errors.New("cli_flag_conjunction_v1 atoms must be sorted by unique flag name") + } + previousFlag = flagName + dimensions[flagName] = match[2] + } + return dimensions, nil +} + +func admitCLIFlagConditionCases(id string, cases []rootShapeConditionCase) error { + if len(cases) == 0 { + return fmt.Errorf("contract definition %s cli_flag_conjunction_v1 requires condition cases", id) + } + expectedFlags := sortedKeys(cases[0].Dimensions) + statesByFlag := make(map[string]map[string]struct{}, len(expectedFlags)) + for _, conditionCase := range cases { + actualFlags := sortedKeys(conditionCase.Dimensions) + if !slices.Equal(actualFlags, expectedFlags) { + return fmt.Errorf( + "contract definition %s cli_flag_conjunction_v1 condition %q dimensions=%v want %v", + id, + conditionCase.Raw, + actualFlags, + expectedFlags, + ) + } + for flagName, state := range conditionCase.Dimensions { + if statesByFlag[flagName] == nil { + statesByFlag[flagName] = map[string]struct{}{} + } + statesByFlag[flagName][state] = struct{}{} + } + } + for _, flagName := range expectedFlags { + states := statesByFlag[flagName] + if _, hasPresent := states["present"]; hasPresent { + for state := range states { + if state != "absent" && state != "present" { + return fmt.Errorf("contract definition %s cli_flag_conjunction_v1 flag %s mixes present with literal values", id, flagName) + } + } + } + } + for leftIndex, left := range cases { + for _, right := range cases[leftIndex+1:] { + if left.VariantID == right.VariantID { + continue + } + disjoint := false + for _, flagName := range expectedFlags { + if left.Dimensions[flagName] != right.Dimensions[flagName] { + disjoint = true + break + } + } + if !disjoint { + return fmt.Errorf( + "contract definition %s cli_flag_conjunction_v1 conditions %q and %q overlap across variants %s and %s", + id, + left.Raw, + right.Raw, + left.VariantID, + right.VariantID, + ) + } + } + } + return nil +} + +func admitConditionModelFlags(command string, direction string, definitionID string, definition map[string]any, allowedFlags []string) error { + shape := definition["fieldTree"].(map[string]any) + if shape["conditionModel"] != "cli_flag_conjunction_v1" { + return nil + } + context := command + " " + direction + "Contract" + if command != cliFlagConditionModelCommand || direction != cliFlagConditionModelDirection || definitionID != cliFlagConditionModelDefinitionID { + return fmt.Errorf("%s condition model definition %s is not bound to its admitted command and direction", context, definitionID) + } + variants := shape["variants"].([]any) + firstVariant := variants[0].(map[string]any) + firstCondition := firstVariant["when"].([]any)[0].(string) + dimensions, err := parseCLIFlagCondition(firstCondition) + if err != nil { + return fmt.Errorf("%s condition model: %w", context, err) + } + for _, flagName := range sortedKeys(dimensions) { + if !slices.Contains(allowedFlags, flagName) { + return fmt.Errorf("%s condition model dimension %s is not an allowed flag", context, flagName) + } + } + return nil +} diff --git a/internal/tools/commandcontractgen/main.go b/internal/tools/commandcontractgen/main.go new file mode 100644 index 0000000..37f97c1 --- /dev/null +++ b/internal/tools/commandcontractgen/main.go @@ -0,0 +1,967 @@ +package main + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "flag" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "io/fs" + "os" + "os/exec" + "path/filepath" + "regexp" + "slices" + "sort" + "strconv" + "strings" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" +) + +const ( + cliContractPath = "proofkit/cli-contract.v2.json" + appGeneratedPath = "internal/app/command_contract_generated.go" + presetGeneratedPath = "internal/command/stackpreset/preset_ids_generated.go" + maxContractBytes = 16 << 20 +) + +var ( + digestPattern = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`) + testNamePattern = regexp.MustCompile(`^Test[A-Z0-9_][A-Za-z0-9_]*$`) +) + +type definitionRecord struct { + Content map[string]any + ID string + Digest string + References []string +} + +type generatedMetadata struct { + InputContractDigest string + InputSummary []string + OutputContractDigest string + FlagChoices map[string][]string +} + +func main() { + check := flag.Bool("check", false, "verify both generated command-contract projections") + flag.Parse() + if flag.NArg() != 0 { + fmt.Fprintln(os.Stderr, "commandcontractgen accepts only --check") + os.Exit(1) + } + root, err := os.Getwd() + if err == nil { + err = run(root, *check) + } + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func run(root string, check bool) error { + appContent, presetContent, err := render(root) + if err != nil { + return err + } + outputs := []struct { + label string + path string + content []byte + }{ + {label: "application", path: appGeneratedPath, content: appContent}, + {label: "preset", path: presetGeneratedPath, content: presetContent}, + } + for _, output := range outputs { + path := filepath.Join(root, filepath.FromSlash(output.path)) + if check { + current, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("read generated %s projection: %w", output.label, err) + } + if !bytes.Equal(current, output.content) { + return fmt.Errorf("%s projection is stale; run go run ./internal/tools/commandcontractgen", output.label) + } + continue + } + if err := writeAtomic(path, output.content); err != nil { + return fmt.Errorf("write generated %s projection: %w", output.label, err) + } + } + return nil +} + +func render(root string) ([]byte, []byte, error) { + source, contract, err := readContract(filepath.Join(root, filepath.FromSlash(cliContractPath))) + if err != nil { + return nil, nil, err + } + definitions, err := admitDefinitions(contract) + if err != nil { + return nil, nil, err + } + metadata, presets, err := admitCommands(root, contract, definitions) + if err != nil { + return nil, nil, err + } + sourceSum := sha256.Sum256(source) + appContent, err := renderApp(hex.EncodeToString(sourceSum[:]), metadata) + if err != nil { + return nil, nil, err + } + presetContent, err := renderPresets(hex.EncodeToString(sourceSum[:]), presets) + if err != nil { + return nil, nil, err + } + return appContent, presetContent, nil +} + +func readContract(path string) ([]byte, map[string]any, error) { + content, err := os.ReadFile(path) + if err != nil { + return nil, nil, fmt.Errorf("read CLI contract: %w", err) + } + if len(content) > maxContractBytes { + return nil, nil, errors.New("CLI contract exceeds size limit") + } + value, err := admission.DecodeJSON(bytes.NewReader(content), maxContractBytes) + if err != nil { + return nil, nil, fmt.Errorf("admit CLI contract: %w", err) + } + record, ok := value.(map[string]any) + if !ok { + return nil, nil, errors.New("CLI contract must be an object") + } + if number, ok := record["schemaVersion"].(json.Number); !ok || number.String() != "2" { + return nil, nil, errors.New("CLI contract schemaVersion must be 2") + } + if record["contractId"] != "proofkit.cli-contract.v2" { + return nil, nil, errors.New("CLI contract identity is invalid") + } + if err := rejectUnknownKeys(record, []string{"commands", "contractDefinitions", "contractId", "packageName", "processContract", "schemaVersion"}, "CLI contract"); err != nil { + return nil, nil, err + } + return content, record, nil +} + +func admitDefinitions(contract map[string]any) (map[string]definitionRecord, error) { + rawDefinitions, ok := contract["contractDefinitions"].([]any) + if !ok || len(rawDefinitions) == 0 { + return nil, errors.New("CLI contract contractDefinitions must be a non-empty array") + } + definitions := make(map[string]definitionRecord, len(rawDefinitions)) + previousID := "" + for index, raw := range rawDefinitions { + record, ok := raw.(map[string]any) + if !ok { + return nil, fmt.Errorf("contract definition %d must be an object", index) + } + id, ok := record["definitionId"].(string) + if !ok || id == "" { + return nil, fmt.Errorf("contract definition %d has invalid definitionId", index) + } + if previousID != "" && previousID >= id { + return nil, errors.New("CLI contract definitions must be sorted and unique") + } + previousID = id + if _, duplicate := definitions[id]; duplicate { + return nil, fmt.Errorf("duplicate contract definition id %s", id) + } + digest, ok := record["canonicalDigest"].(string) + if !ok || !digestPattern.MatchString(digest) { + return nil, fmt.Errorf("contract definition %s has invalid canonicalDigest", id) + } + canonicalRecord := cloneRecord(record) + delete(canonicalRecord, "canonicalDigest") + encoded, err := canonicalJSON(canonicalRecord) + if err != nil { + return nil, fmt.Errorf("canonicalize contract definition %s: %w", id, err) + } + if actual := sha256Digest(encoded); actual != digest { + return nil, fmt.Errorf("contract definition %s canonical digest mismatch: got %s want %s", id, actual, digest) + } + references, err := stringList(record["definitionRefs"], "contract definition "+id+" definitionRefs") + if err != nil { + return nil, err + } + if !sort.StringsAreSorted(references) || hasDuplicate(references) { + return nil, fmt.Errorf("contract definition %s definitionRefs must be sorted and unique", id) + } + if err := admitStructuralDefinition(id, record); err != nil { + return nil, err + } + definitions[id] = definitionRecord{Content: cloneRecord(record), ID: id, Digest: digest, References: references} + } + for _, definition := range definitions { + for _, reference := range definition.References { + if _, ok := definitions[reference]; !ok { + return nil, fmt.Errorf("contract definition %s references unknown definition %s", definition.ID, reference) + } + } + } + return definitions, nil +} + +func admitStructuralDefinition(id string, record map[string]any) error { + if err := rejectUnknownKeys(record, []string{"canonicalDigest", "closed", "definitionId", "definitionRefs", "fieldTree", "rootType", "schemaVersion"}, "contract definition "+id); err != nil { + return err + } + if version, ok := positiveJSONInteger(record["schemaVersion"]); !ok || version < 1 { + return fmt.Errorf("contract definition %s has invalid schemaVersion", id) + } + rootType, ok := record["rootType"].(string) + if !ok || (rootType != "json_value" && rootType != "object" && rootType != "union") { + return fmt.Errorf("contract definition %s rootType must be json_value, object, or union", id) + } + if record["closed"] != true { + return fmt.Errorf("contract definition %s must be closed", id) + } + references, err := stringList(record["definitionRefs"], "contract definition "+id+" definitionRefs") + if err != nil { + return err + } + if len(references) != 0 { + return fmt.Errorf("contract definition %s root-shape-only definitions must not reference nested definitions", id) + } + shape, ok := record["fieldTree"].(map[string]any) + if !ok { + return fmt.Errorf("contract definition %s is missing fieldTree", id) + } + if shape["kind"] != "root_shape_only" { + return fmt.Errorf("contract definition %s must use root_shape_only", id) + } + if err := rejectUnknownKeys(shape, []string{"conditionModel", "kind", "nonClaims", "variants"}, "contract definition "+id+" fieldTree"); err != nil { + return err + } + conditionModel := "" + if rawConditionModel, present := shape["conditionModel"]; present { + var ok bool + conditionModel, ok = rawConditionModel.(string) + if !ok || conditionModel != "cli_flag_conjunction_v1" { + return fmt.Errorf("contract definition %s has invalid conditionModel", id) + } + if id != cliFlagConditionModelDefinitionID { + return fmt.Errorf("contract definition %s conditionModel has no admitted native closure owner", id) + } + } + nonClaims, err := stringList(shape["nonClaims"], "contract definition "+id+" fieldTree.nonClaims") + if err != nil || !slices.Equal(nonClaims, []string{ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection.", + }) { + return fmt.Errorf("contract definition %s must declare the exact root-shape non-claims", id) + } + rawVariants, ok := shape["variants"].([]any) + if !ok || len(rawVariants) == 0 { + return fmt.Errorf("contract definition %s variants must be a non-empty array", id) + } + previousID := "" + rootKinds := map[string]struct{}{} + conditionOwners := map[string]string{} + var conditionCases []rootShapeConditionCase + for index, raw := range rawVariants { + variant, ok := raw.(map[string]any) + if !ok { + return fmt.Errorf("contract definition %s variant %d must be an object", id, index) + } + if err := rejectUnknownKeys(variant, []string{"allowedFields", "requiredFields", "rootKind", "variantId", "when"}, "contract definition "+id+" variant"); err != nil { + return err + } + variantID, ok := variant["variantId"].(string) + if !ok || variantID == "" || (previousID != "" && previousID >= variantID) { + return fmt.Errorf("contract definition %s variant ids must be non-empty, sorted, and unique", id) + } + previousID = variantID + conditions, err := rootShapeStringList(variant["when"], "contract definition "+id+" variant "+variantID+" when", false) + if err != nil { + return err + } + for _, condition := range conditions { + if previousVariant, duplicate := conditionOwners[condition]; duplicate { + return fmt.Errorf("contract definition %s condition %q is duplicated across variants %s and %s", id, condition, previousVariant, variantID) + } + conditionOwners[condition] = variantID + if conditionModel != "" { + dimensions, err := parseCLIFlagCondition(condition) + if err != nil { + return fmt.Errorf("contract definition %s variant %s condition %q: %w", id, variantID, condition, err) + } + conditionCases = append(conditionCases, rootShapeConditionCase{ + Dimensions: dimensions, + Raw: condition, + VariantID: variantID, + }) + } + } + rootKind, ok := variant["rootKind"].(string) + if !ok || (rootKind != "array" && rootKind != "json_value" && rootKind != "object") { + return fmt.Errorf("contract definition %s variant %s has invalid rootKind", id, variantID) + } + rootKinds[rootKind] = struct{}{} + if rootType == "object" && rootKind != "object" { + return fmt.Errorf("contract definition %s object root cannot declare %s variant %s", id, rootKind, variantID) + } + if rootType == "json_value" && rootKind != "json_value" { + return fmt.Errorf("contract definition %s json_value root cannot declare bounded %s variant %s", id, rootKind, variantID) + } + if rootType == "union" && rootKind == "json_value" { + return fmt.Errorf("contract definition %s union cannot include unconstrained json_value variant %s", id, variantID) + } + allowed, err := rootShapeStringList(variant["allowedFields"], "contract definition "+id+" variant "+variantID+" allowedFields", true) + if err != nil { + return err + } + required, err := rootShapeStringList(variant["requiredFields"], "contract definition "+id+" variant "+variantID+" requiredFields", true) + if err != nil { + return err + } + if rootKind != "object" && (len(allowed) != 0 || len(required) != 0) { + return fmt.Errorf("contract definition %s variant %s non-object root must not declare fields", id, variantID) + } + allowedSet := make(map[string]struct{}, len(allowed)) + for _, field := range allowed { + allowedSet[field] = struct{}{} + } + for _, field := range required { + if _, ok := allowedSet[field]; !ok { + return fmt.Errorf("contract definition %s variant %s required field %s is not allowed", id, variantID, field) + } + } + } + if conditionModel == "cli_flag_conjunction_v1" { + if err := admitCLIFlagConditionCases(id, conditionCases); err != nil { + return err + } + } + if rootType == "json_value" && (len(rawVariants) != 1 || len(rootKinds) != 1) { + return fmt.Errorf("contract definition %s json_value root must have exactly one unconstrained variant", id) + } + if rootType == "union" && len(rootKinds) < 2 { + return fmt.Errorf("contract definition %s union must enumerate at least two distinct root kinds", id) + } + return nil +} + +func rootShapeStringList(raw any, context string, allowEmpty bool) ([]string, error) { + values, err := stringList(raw, context) + if err != nil { + return nil, err + } + if !sort.StringsAreSorted(values) || hasDuplicate(values) { + return nil, fmt.Errorf("%s must be sorted and unique", context) + } + if !allowEmpty && len(values) == 0 { + return nil, fmt.Errorf("%s must be non-empty", context) + } + return values, nil +} + +func rejectUnknownKeys(record map[string]any, allowed []string, context string) error { + allowedSet := make(map[string]struct{}, len(allowed)) + for _, key := range allowed { + allowedSet[key] = struct{}{} + } + for key := range record { + if _, ok := allowedSet[key]; !ok { + return fmt.Errorf("%s has unknown field %s", context, key) + } + } + return nil +} + +func positiveJSONInteger(raw any) (int64, bool) { + number, ok := raw.(json.Number) + if !ok { + return 0, false + } + value, err := number.Int64() + return value, err == nil && value > 0 +} + +func resolveDefinition(id string, definitions map[string]definitionRecord) (map[string]any, error) { + definition, ok := definitions[id] + if !ok { + return nil, fmt.Errorf("unknown definition %s", id) + } + resolved := cloneRecord(definition.Content) + children := make([]any, 0, len(definition.References)) + for _, ref := range definition.References { + child, err := resolveDefinition(ref, definitions) + if err != nil { + return nil, err + } + children = append(children, child) + } + resolved["resolvedDefinitionRefs"] = children + return resolved, nil +} + +func admitCommands(root string, contract map[string]any, definitions map[string]definitionRecord) (map[string]generatedMetadata, []string, error) { + rawCommands, ok := contract["commands"].([]any) + if !ok || len(rawCommands) == 0 { + return nil, nil, errors.New("CLI contract commands must be a non-empty array") + } + metadata := make(map[string]generatedMetadata, len(rawCommands)) + contractIDs := map[string]string{} + activeTestFiles := map[string]map[string]struct{}{} + var presets []string + previous := "" + for index, raw := range rawCommands { + command, ok := raw.(map[string]any) + if !ok { + return nil, nil, fmt.Errorf("CLI command %d must be an object", index) + } + name, ok := command["command"].(string) + if !ok || name == "" { + return nil, nil, fmt.Errorf("CLI command %d has invalid command", index) + } + if previous != "" && previous >= name { + return nil, nil, errors.New("CLI commands must be sorted and unique") + } + previous = name + item := generatedMetadata{FlagChoices: map[string][]string{}} + allowedFlags, err := stringList(command["allowedFlags"], "command "+name+" allowedFlags") + if err != nil || !sort.StringsAreSorted(allowedFlags) || hasDuplicate(allowedFlags) { + return nil, nil, fmt.Errorf("command %s allowedFlags must be sorted and unique", name) + } + inputMode, _ := command["input"].(string) + inputContract, hasInputContract := command["inputContract"].(map[string]any) + if inputMode == "required" && !hasInputContract { + return nil, nil, fmt.Errorf("required-input command %s is missing inputContract", name) + } + if hasInputContract { + if err := admitUniqueContractID(name, "input", inputContract, contractIDs); err != nil { + return nil, nil, err + } + digest, summary, err := admitCommandContract(root, name, "input", inputContract, definitions, activeTestFiles, allowedFlags) + if err != nil { + return nil, nil, err + } + item.InputContractDigest = digest + item.InputSummary = summary + } + outputModes, err := stringList(command["outputModes"], "command "+name+" outputModes") + if err != nil { + return nil, nil, err + } + outputContract, hasOutputContract := command["outputContract"].(map[string]any) + if slices.Contains(outputModes, "json") && !hasOutputContract { + return nil, nil, fmt.Errorf("JSON-output command %s is missing outputContract", name) + } + if hasOutputContract { + if err := admitUniqueContractID(name, "output", outputContract, contractIDs); err != nil { + return nil, nil, err + } + digest, _, err := admitCommandContract(root, name, "output", outputContract, definitions, activeTestFiles, allowedFlags) + if err != nil { + return nil, nil, err + } + item.OutputContractDigest = digest + if rawChoices, ok := outputContract["flagChoices"].(map[string]any); ok { + flags := make([]string, 0, len(rawChoices)) + for flag := range rawChoices { + flags = append(flags, flag) + } + sort.Strings(flags) + for _, flag := range flags { + choices, err := stringList(rawChoices[flag], "command "+name+" flag choices "+flag) + if err != nil { + return nil, nil, err + } + if len(choices) == 0 || !sort.StringsAreSorted(choices) || hasDuplicate(choices) { + return nil, nil, fmt.Errorf("command %s flag choices %s must be non-empty, sorted, and unique", name, flag) + } + item.FlagChoices[flag] = choices + } + } + } + if name == "stack-preset" { + presets = append([]string(nil), item.FlagChoices["--preset"]...) + if len(presets) == 0 { + return nil, nil, errors.New("stack-preset outputContract must own non-empty --preset flag choices") + } + } + metadata[name] = item + } + return metadata, presets, nil +} + +func admitUniqueContractID(command string, direction string, contract map[string]any, seen map[string]string) error { + id, ok := contract["contractId"].(string) + if !ok || id == "" { + return fmt.Errorf("%s %sContract has invalid contractId", command, direction) + } + if previous, duplicate := seen[id]; duplicate { + return fmt.Errorf("duplicate command contract id %s used by %s and %s %sContract", id, previous, command, direction) + } + seen[id] = command + " " + direction + "Contract" + return nil +} + +func admitCommandContract(root string, command string, direction string, contract map[string]any, definitions map[string]definitionRecord, activeTestFiles map[string]map[string]struct{}, allowedFlags []string) (string, []string, error) { + context := command + " " + direction + "Contract" + contractID, ok := contract["contractId"].(string) + if !ok || contractID == "" { + return "", nil, fmt.Errorf("%s has invalid contractId", context) + } + if version, ok := positiveJSONInteger(contract["schemaVersion"]); !ok || version < 1 { + return "", nil, fmt.Errorf("%s has invalid schemaVersion", context) + } + if contract["closed"] != true { + return "", nil, fmt.Errorf("%s must declare a closed root", context) + } + ownerRequirements, err := stringList(contract["ownerRequirementRefs"], context+" ownerRequirementRefs") + if err != nil || len(ownerRequirements) == 0 || !sort.StringsAreSorted(ownerRequirements) || hasDuplicate(ownerRequirements) { + return "", nil, fmt.Errorf("%s ownerRequirementRefs must be non-empty, sorted, and unique", context) + } + definitionID, ok := contract["rootDefinitionRef"].(string) + if !ok || definitionID == "" { + return "", nil, fmt.Errorf("%s has invalid rootDefinitionRef", context) + } + definition, ok := definitions[definitionID] + if !ok { + return "", nil, fmt.Errorf("%s references unknown definition %s", context, definitionID) + } + if contract["rootDefinitionDigest"] != definition.Digest { + return "", nil, fmt.Errorf("%s definition digest mismatch for %s", context, definitionID) + } + if contract["rootType"] != definition.Content["rootType"] { + return "", nil, fmt.Errorf("%s rootType does not match definition %s", context, definitionID) + } + if err := admitConditionModelFlags(command, direction, definitionID, definition.Content, allowedFlags); err != nil { + return "", nil, err + } + if err := admitNativeSources(root, context, contract); err != nil { + return "", nil, err + } + selectorKey := "nativeAdmissionWitnessSelector" + if direction == "output" { + selectorKey = "nativeOutputWitnessSelector" + } + selector, ok := contract[selectorKey].(map[string]any) + if !ok { + return "", nil, fmt.Errorf("%s is missing %s", context, selectorKey) + } + if err := admitSelector(root, context, selector, activeTestFiles); err != nil { + return "", nil, err + } + summary, err := stringList(contract["compatibilitySummary"], context+" compatibilitySummary") + if err != nil || len(summary) == 0 { + return "", nil, fmt.Errorf("%s compatibilitySummary must be a non-empty string array", context) + } + resolvedDefinition, err := resolveDefinition(definitionID, definitions) + if err != nil { + return "", nil, fmt.Errorf("%s: %w", context, err) + } + resolvedRecord := map[string]any{ + "contract": contract, + "resolvedRootDefinition": resolvedDefinition, + } + encoded, err := canonicalJSON(resolvedRecord) + if err != nil { + return "", nil, err + } + return sha256Digest(encoded), summary, nil +} + +func admitNativeSources(root string, context string, contract map[string]any) error { + rawSource, hasSource := contract["nativeSource"] + rawSourcesValue, hasSources := contract["nativeSources"] + if hasSource == hasSources { + return fmt.Errorf("%s must declare exactly one of nativeSource or nativeSources", context) + } + if hasSource { + source, ok := rawSource.(map[string]any) + if !ok { + return fmt.Errorf("%s nativeSource must be an object", context) + } + return admitNativeSource(root, context+" nativeSource", source) + } + rawSources, ok := rawSourcesValue.([]any) + if !ok { + return fmt.Errorf("%s nativeSources must be an array", context) + } + if len(rawSources) == 0 { + return fmt.Errorf("%s nativeSources must be non-empty", context) + } + previousPath := "" + for index, rawSource := range rawSources { + itemContext := fmt.Sprintf("%s nativeSources[%d]", context, index) + source, ok := rawSource.(map[string]any) + if !ok { + return fmt.Errorf("%s must be an object", itemContext) + } + path, _ := source["path"].(string) + if previousPath != "" && previousPath >= path { + return fmt.Errorf("%s nativeSources must be sorted and unique by path", context) + } + if err := admitNativeSource(root, itemContext, source); err != nil { + return err + } + previousPath = path + } + return nil +} + +func admitNativeSource(root string, context string, source map[string]any) error { + if err := rejectUnknownKeys(source, []string{"canonicalDigest", "evidenceClass", "path"}, context); err != nil { + return err + } + if source["evidenceClass"] != "source_checkout" { + return fmt.Errorf("%s evidenceClass must be source_checkout", context) + } + path, ok := source["path"].(string) + if !ok || !safeRelativePath(path) { + return fmt.Errorf("%s path is invalid", context) + } + expected, ok := source["canonicalDigest"].(string) + if !ok || !digestPattern.MatchString(expected) { + return fmt.Errorf("%s canonicalDigest is invalid", context) + } + actual, err := digestSourcePath(root, path) + if err != nil { + return fmt.Errorf("%s: %w", context, err) + } + if actual != expected { + return fmt.Errorf("%s native source digest mismatch: got %s want %s", context, actual, expected) + } + return nil +} + +func admitSelector(root string, context string, selector map[string]any, activeTestFiles map[string]map[string]struct{}) error { + if err := rejectUnknownKeys(selector, []string{"command", "evidenceClass", "path", "test"}, context+" selector"); err != nil { + return err + } + if selector["evidenceClass"] != "source_checkout" { + return fmt.Errorf("%s selector evidenceClass must be source_checkout", context) + } + path, ok := selector["path"].(string) + if !ok || !safeRelativePath(path) || !strings.HasSuffix(path, "_test.go") { + return fmt.Errorf("%s selector path must identify a tracked _test.go file", context) + } + tracked, err := trackedPath(root, path) + if err != nil { + return fmt.Errorf("%s selector tracked inventory: %w", context, err) + } + if !tracked { + return fmt.Errorf("%s selector path must identify a Git-index-tracked _test.go file", context) + } + info, err := os.Lstat(filepath.Join(root, filepath.FromSlash(path))) + if err != nil { + return fmt.Errorf("%s selector path is unavailable: %w", context, err) + } + if !info.Mode().IsRegular() { + return fmt.Errorf("%s selector path must identify a regular non-symlink file", context) + } + testName, ok := selector["test"].(string) + if !ok || !testNamePattern.MatchString(testName) { + return fmt.Errorf("%s selector test is invalid", context) + } + packagePath := "./" + filepath.ToSlash(filepath.Dir(path)) + active, err := activePackageTestFiles(root, packagePath, activeTestFiles) + if err != nil { + return fmt.Errorf("%s selector active test inventory: %w", context, err) + } + if _, ok := active[path]; !ok { + return fmt.Errorf("%s selector path is not active in the current Go build", context) + } + expectedCommand := "go test " + packagePath + " -run '^" + testName + "$'" + if selector["command"] != expectedCommand { + return fmt.Errorf("%s selector command must select exactly %s", context, testName) + } + parsed, err := parser.ParseFile(token.NewFileSet(), filepath.Join(root, filepath.FromSlash(path)), nil, 0) + if err != nil { + return fmt.Errorf("%s parse selector source: %w", context, err) + } + testingAliases := map[string]struct{}{} + dotTesting := false + for _, imported := range parsed.Imports { + importPath, err := strconv.Unquote(imported.Path.Value) + if err != nil || importPath != "testing" { + continue + } + switch { + case imported.Name == nil: + testingAliases["testing"] = struct{}{} + case imported.Name.Name == ".": + dotTesting = true + case imported.Name.Name != "_": + testingAliases[imported.Name.Name] = struct{}{} + } + } + for _, declaration := range parsed.Decls { + function, ok := declaration.(*ast.FuncDecl) + if !ok || function.Recv != nil || function.Name.Name != testName { + continue + } + if validTestSignature(function, testingAliases, dotTesting) { + return nil + } + return fmt.Errorf("%s test function %s has invalid test signature", context, testName) + } + return fmt.Errorf("%s test function %s does not exist", context, testName) +} + +func activePackageTestFiles(root string, packagePath string, cache map[string]map[string]struct{}) (map[string]struct{}, error) { + if files, ok := cache[packagePath]; ok { + return files, nil + } + command := exec.Command("go", "list", "-json", packagePath) + command.Dir = root + output, err := command.Output() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + return nil, fmt.Errorf("go list %s failed: %w: %s", packagePath, err, strings.TrimSpace(string(exitErr.Stderr))) + } + return nil, fmt.Errorf("go list %s failed: %w", packagePath, err) + } + var inventory struct { + Dir string + TestGoFiles []string + XTestGoFiles []string + } + if err := json.Unmarshal(output, &inventory); err != nil { + return nil, fmt.Errorf("decode go list %s: %w", packagePath, err) + } + packageRelative, err := filepath.Rel(root, inventory.Dir) + packageRelative = filepath.ToSlash(packageRelative) + if err != nil || (packageRelative != "." && !safeRelativePath(packageRelative)) { + return nil, fmt.Errorf("go list %s returned an invalid package directory", packagePath) + } + if packageRelative == "." { + packageRelative = "" + } + files := map[string]struct{}{} + for _, name := range append(inventory.TestGoFiles, inventory.XTestGoFiles...) { + files[filepath.ToSlash(filepath.Join(packageRelative, name))] = struct{}{} + } + cache[packagePath] = files + return files, nil +} + +func trackedPath(root string, path string) (bool, error) { + command := exec.Command("git", "-C", root, "ls-files", "--error-unmatch", "--", path) + var stderr bytes.Buffer + command.Stderr = &stderr + if err := command.Run(); err != nil { + if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { + return false, nil + } + return false, fmt.Errorf("git ls-files failed: %w: %s", err, strings.TrimSpace(stderr.String())) + } + return true, nil +} + +func validTestSignature(function *ast.FuncDecl, testingAliases map[string]struct{}, dotTesting bool) bool { + if function.Body == nil || function.Type.TypeParams != nil { + return false + } + if function.Type.Results != nil && len(function.Type.Results.List) != 0 { + return false + } + if function.Type.Params == nil || len(function.Type.Params.List) != 1 { + return false + } + parameter := function.Type.Params.List[0] + if len(parameter.Names) > 1 { + return false + } + pointer, ok := parameter.Type.(*ast.StarExpr) + if !ok { + return false + } + if selector, ok := pointer.X.(*ast.SelectorExpr); ok { + if selector.Sel.Name != "T" { + return false + } + identifier, ok := selector.X.(*ast.Ident) + if !ok { + return false + } + _, ok = testingAliases[identifier.Name] + return ok + } + identifier, ok := pointer.X.(*ast.Ident) + return ok && dotTesting && identifier.Name == "T" +} + +func digestSourcePath(root string, relative string) (string, error) { + absolute := filepath.Join(root, filepath.FromSlash(relative)) + info, err := os.Stat(absolute) + if err != nil { + return "", err + } + paths := []string{} + if info.IsDir() { + err = filepath.WalkDir(absolute, func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() { + return nil + } + if strings.HasSuffix(entry.Name(), ".go") && !strings.HasSuffix(entry.Name(), "_test.go") && !strings.HasSuffix(entry.Name(), "_generated.go") { + relativePath, err := filepath.Rel(root, path) + if err != nil { + return err + } + paths = append(paths, filepath.ToSlash(relativePath)) + } + return nil + }) + if err != nil { + return "", err + } + } else { + paths = append(paths, relative) + } + sort.Strings(paths) + if len(paths) == 0 { + return "", errors.New("native source contains no admitted Go files") + } + hash := sha256.New() + for _, path := range paths { + content, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(path))) + if err != nil { + return "", err + } + hash.Write([]byte(path)) + hash.Write([]byte{0}) + hash.Write(content) + hash.Write([]byte{0}) + } + return "sha256:" + hex.EncodeToString(hash.Sum(nil)), nil +} + +func renderApp(sourceDigest string, metadata map[string]generatedMetadata) ([]byte, error) { + var output strings.Builder + output.WriteString("// Code generated by internal/tools/commandcontractgen; DO NOT EDIT.\n") + output.WriteString("package app\n\n") + fmt.Fprintf(&output, "const commandContractSourceSHA256 = %q\n\n", sourceDigest) + output.WriteString("type generatedCommandContractMetadata struct {\n") + output.WriteString("\tInputContractSHA256 string\n\tInputSchemaSummary []string\n\tOutputContractSHA256 string\n\tFlagChoices map[string][]string\n}\n\n") + output.WriteString("var generatedCommandContractMetadataByName = map[string]generatedCommandContractMetadata{\n") + for _, name := range sortedKeys(metadata) { + item := metadata[name] + fmt.Fprintf(&output, "\t%q: {InputContractSHA256: %q, InputSchemaSummary: %#v, OutputContractSHA256: %q, FlagChoices: %#v},\n", + name, item.InputContractDigest, item.InputSummary, item.OutputContractDigest, item.FlagChoices) + } + output.WriteString("}\n") + formatted, err := format.Source([]byte(output.String())) + if err != nil { + return nil, fmt.Errorf("format application command-contract projection: %w", err) + } + return formatted, nil +} + +func renderPresets(sourceDigest string, presets []string) ([]byte, error) { + var output strings.Builder + output.WriteString("// Code generated by internal/tools/commandcontractgen; DO NOT EDIT.\n") + output.WriteString("package stackpreset\n\n") + fmt.Fprintf(&output, "const presetContractSourceSHA256 = %q\n\n", sourceDigest) + fmt.Fprintf(&output, "var presetIDs = %#v\n", presets) + formatted, err := format.Source([]byte(output.String())) + if err != nil { + return nil, fmt.Errorf("format preset command-contract projection: %w", err) + } + return formatted, nil +} + +func canonicalJSON(value any) ([]byte, error) { + return json.Marshal(value) +} + +func sha256Digest(content []byte) string { + sum := sha256.Sum256(content) + return "sha256:" + hex.EncodeToString(sum[:]) +} + +func stringList(raw any, context string) ([]string, error) { + if raw == nil { + return []string{}, nil + } + values, ok := raw.([]any) + if !ok { + return nil, fmt.Errorf("%s must be an array", context) + } + result := make([]string, 0, len(values)) + for index, value := range values { + text, ok := value.(string) + if !ok || text == "" { + return nil, fmt.Errorf("%s item %d must be non-empty text", context, index) + } + result = append(result, text) + } + return result, nil +} + +func safeRelativePath(path string) bool { + if path == "" || filepath.IsAbs(path) || strings.Contains(path, `\`) { + return false + } + clean := filepath.ToSlash(filepath.Clean(filepath.FromSlash(path))) + return clean == path && clean != "." && clean != ".." && !strings.HasPrefix(clean, "../") +} + +func cloneRecord(record map[string]any) map[string]any { + out := make(map[string]any, len(record)) + for key, value := range record { + out[key] = value + } + return out +} + +func sortedKeys[T any](values map[string]T) []string { + keys := make([]string, 0, len(values)) + for key := range values { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} + +func hasDuplicate(values []string) bool { + for index := 1; index < len(values); index++ { + if values[index-1] == values[index] { + return true + } + } + return false +} + +func writeAtomic(path string, content []byte) error { + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + file, err := os.CreateTemp(filepath.Dir(path), ".command-contract-*") + if err != nil { + return err + } + tempPath := file.Name() + defer os.Remove(tempPath) + if _, err := file.Write(content); err != nil { + file.Close() + return err + } + if err := file.Sync(); err != nil { + file.Close() + return err + } + if err := file.Chmod(0o644); err != nil { + file.Close() + return err + } + if err := file.Close(); err != nil { + return err + } + return os.Rename(tempPath, path) +} diff --git a/internal/tools/commandcontractgen/main_test.go b/internal/tools/commandcontractgen/main_test.go new file mode 100644 index 0000000..c843344 --- /dev/null +++ b/internal/tools/commandcontractgen/main_test.go @@ -0,0 +1,786 @@ +package main + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "maps" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestCLIFlagConditionModelRejectsAmbiguity(t *testing.T) { + valid := []rootShapeConditionCase{ + { + Dimensions: mustParseCLIFlagCondition(t, "--agent-envelope=absent --mode=bootstrap"), + Raw: "--agent-envelope=absent --mode=bootstrap", + VariantID: "01-bootstrap", + }, + { + Dimensions: mustParseCLIFlagCondition(t, "--agent-envelope=present --mode=bootstrap"), + Raw: "--agent-envelope=present --mode=bootstrap", + VariantID: "02-bootstrap-agent", + }, + } + if err := admitCLIFlagConditionCases("proofkit.valid", valid); err != nil { + t.Fatalf("valid disjoint condition cases rejected: %v", err) + } + + for _, item := range []struct { + name string + condition string + }{ + {name: "natural language is not machine authority", condition: "--mode bootstrap"}, + {name: "whitespace is canonical", condition: "--agent-envelope=absent --mode=bootstrap"}, + {name: "atoms are sorted", condition: "--mode=bootstrap --agent-envelope=absent"}, + {name: "dimensions are unique", condition: "--mode=bootstrap --mode=guidance"}, + } { + t.Run(item.name, func(t *testing.T) { + if _, err := parseCLIFlagCondition(item.condition); err == nil { + t.Fatalf("parseCLIFlagCondition(%q) succeeded", item.condition) + } + }) + } + + t.Run("every case closes the same dimensions", func(t *testing.T) { + mutant := append([]rootShapeConditionCase(nil), valid...) + mutant[1] = rootShapeConditionCase{ + Dimensions: mustParseCLIFlagCondition(t, "--mode=bootstrap"), + Raw: "--mode=bootstrap", + VariantID: mutant[1].VariantID, + } + if err := admitCLIFlagConditionCases("proofkit.dimension-drift", mutant); err == nil || !strings.Contains(err.Error(), "dimensions=") { + t.Fatalf("dimension-drift error=%v", err) + } + }) + + t.Run("boolean state cannot overlap literal values", func(t *testing.T) { + mutant := []rootShapeConditionCase{ + { + Dimensions: mustParseCLIFlagCondition(t, "--mode=present"), + Raw: "--mode=present", + VariantID: "01-present", + }, + { + Dimensions: mustParseCLIFlagCondition(t, "--mode=bootstrap"), + Raw: "--mode=bootstrap", + VariantID: "02-literal", + }, + } + if err := admitCLIFlagConditionCases("proofkit.state-mix", mutant); err == nil || !strings.Contains(err.Error(), "mixes present with literal values") { + t.Fatalf("state-mix error=%v", err) + } + }) + + t.Run("different variants cannot own the same assignment", func(t *testing.T) { + mutant := append([]rootShapeConditionCase(nil), valid...) + mutant[1] = rootShapeConditionCase{ + Dimensions: maps.Clone(mutant[0].Dimensions), + Raw: "syntactically-distinct-test-fixture", + VariantID: mutant[1].VariantID, + } + if err := admitCLIFlagConditionCases("proofkit.overlap", mutant); err == nil || !strings.Contains(err.Error(), "overlap across variants") { + t.Fatalf("overlap error=%v", err) + } + }) + + t.Run("condition dimension must be an allowed command flag", func(t *testing.T) { + definition := map[string]any{ + "fieldTree": map[string]any{ + "conditionModel": "cli_flag_conjunction_v1", + "variants": []any{ + map[string]any{"when": []any{"--undeclared=present"}}, + }, + }, + } + err := admitConditionModelFlags( + cliFlagConditionModelCommand, + cliFlagConditionModelDirection, + cliFlagConditionModelDefinitionID, + definition, + []string{"--mode"}, + ) + if err == nil || !strings.Contains(err.Error(), "condition model dimension --undeclared is not an allowed flag") { + t.Fatalf("allowed-flag linkage error=%v", err) + } + }) + + t.Run("condition model is bound to its admitted command and direction", func(t *testing.T) { + definition := map[string]any{ + "fieldTree": map[string]any{ + "conditionModel": "cli_flag_conjunction_v1", + "variants": []any{ + map[string]any{"when": []any{"--mode=adoption"}}, + }, + }, + } + for _, item := range []struct { + name string + command string + direction string + definitionID string + }{ + {name: "command alias", command: "alias", direction: cliFlagConditionModelDirection, definitionID: cliFlagConditionModelDefinitionID}, + {name: "direction alias", command: cliFlagConditionModelCommand, direction: "input", definitionID: cliFlagConditionModelDefinitionID}, + {name: "definition alias", command: cliFlagConditionModelCommand, direction: cliFlagConditionModelDirection, definitionID: "proofkit.alias.output.v1.root-shape"}, + } { + t.Run(item.name, func(t *testing.T) { + err := admitConditionModelFlags(item.command, item.direction, item.definitionID, definition, []string{"--mode"}) + if err == nil || !strings.Contains(err.Error(), "is not bound to its admitted command and direction") { + t.Fatalf("owner-binding error=%v", err) + } + }) + } + }) +} + +func mustParseCLIFlagCondition(t *testing.T, condition string) map[string]string { + t.Helper() + dimensions, err := parseCLIFlagCondition(condition) + if err != nil { + t.Fatalf("parse condition %q: %v", condition, err) + } + return dimensions +} + +func TestRenderRejectsIncompleteAndStaleCommandContracts(t *testing.T) { + cases := []struct { + name string + mutate func(map[string]any) + want string + }{ + { + name: "required input contract missing", + mutate: func(contract map[string]any) { + commandAt(contract, "sample")["inputContract"] = nil + }, + want: "required-input command sample is missing inputContract", + }, + { + name: "JSON output contract missing", + mutate: func(contract map[string]any) { + commandAt(contract, "sample")["outputContract"] = nil + }, + want: "JSON-output command sample is missing outputContract", + }, + { + name: "duplicate command contract id", + mutate: func(contract map[string]any) { + command := commandAt(contract, "sample") + output := command["outputContract"].(map[string]any) + output["contractId"] = command["inputContract"].(map[string]any)["contractId"] + }, + want: "duplicate command contract id", + }, + { + name: "dangling definition", + mutate: func(contract map[string]any) { + commandAt(contract, "sample")["inputContract"].(map[string]any)["rootDefinitionRef"] = "proofkit.missing.v1" + }, + want: "references unknown definition", + }, + { + name: "nested definition reference", + mutate: func(contract map[string]any) { + definition := contract["contractDefinitions"].([]any)[0].(map[string]any) + definition["definitionRefs"] = []any{"proofkit.output.v1"} + refreshDefinitionDigests(contract) + }, + want: "must not reference nested definitions", + }, + { + name: "definition digest mismatch", + mutate: func(contract map[string]any) { + commandAt(contract, "sample")["inputContract"].(map[string]any)["rootDefinitionDigest"] = "sha256:" + strings.Repeat("0", 64) + }, + want: "definition digest mismatch", + }, + { + name: "misspelled structural field", + mutate: func(contract map[string]any) { + definition := contract["contractDefinitions"].([]any)[0].(map[string]any) + variant := definition["fieldTree"].(map[string]any)["variants"].([]any)[0].(map[string]any) + variant["requireFields"] = variant["requiredFields"] + refreshDefinitionDigests(contract) + }, + want: "unknown field requireFields", + }, + { + name: "open structural escape hatch", + mutate: func(contract map[string]any) { + definition := contract["contractDefinitions"].([]any)[0].(map[string]any) + variant := definition["fieldTree"].(map[string]any)["variants"].([]any)[0].(map[string]any) + variant["openFields"] = true + refreshDefinitionDigests(contract) + }, + want: "unknown field openFields", + }, + { + name: "required root field is not allowed", + mutate: func(contract map[string]any) { + definition := contract["contractDefinitions"].([]any)[0].(map[string]any) + variant := definition["fieldTree"].(map[string]any)["variants"].([]any)[0].(map[string]any) + variant["requiredFields"] = []any{"missing"} + refreshDefinitionDigests(contract) + }, + want: "required field missing is not allowed", + }, + { + name: "variant condition is empty", + mutate: func(contract map[string]any) { + definition := contract["contractDefinitions"].([]any)[0].(map[string]any) + variant := definition["fieldTree"].(map[string]any)["variants"].([]any)[0].(map[string]any) + variant["when"] = []any{} + refreshDefinitionDigests(contract) + }, + want: "when must be non-empty", + }, + { + name: "unknown condition model", + mutate: func(contract map[string]any) { + definition := contract["contractDefinitions"].([]any)[0].(map[string]any) + definition["fieldTree"].(map[string]any)["conditionModel"] = "free_text" + refreshDefinitionDigests(contract) + }, + want: "invalid conditionModel", + }, + { + name: "condition model without admitted native closure owner", + mutate: func(contract map[string]any) { + definition := contract["contractDefinitions"].([]any)[0].(map[string]any) + definition["fieldTree"].(map[string]any)["conditionModel"] = "cli_flag_conjunction_v1" + refreshDefinitionDigests(contract) + }, + want: "conditionModel has no admitted native closure owner", + }, + { + name: "variant condition is duplicated across variants", + mutate: func(contract map[string]any) { + definition := contract["contractDefinitions"].([]any)[0].(map[string]any) + variants := definition["fieldTree"].(map[string]any)["variants"].([]any) + variants = append(variants, map[string]any{ + "allowedFields": []any{"mode", "schemaVersion"}, + "requiredFields": []any{"mode", "schemaVersion"}, + "rootKind": "object", + "variantId": "z-duplicate-condition", + "when": []any{"default"}, + }) + definition["fieldTree"].(map[string]any)["variants"] = variants + refreshDefinitionDigests(contract) + }, + want: "is duplicated across variants", + }, + { + name: "root fields are not sorted", + mutate: func(contract map[string]any) { + definition := contract["contractDefinitions"].([]any)[0].(map[string]any) + variant := definition["fieldTree"].(map[string]any)["variants"].([]any)[0].(map[string]any) + variant["allowedFields"] = []any{"schemaVersion", "mode"} + refreshDefinitionDigests(contract) + }, + want: "allowedFields must be sorted and unique", + }, + { + name: "object definition declares array variant", + mutate: func(contract map[string]any) { + definition := contract["contractDefinitions"].([]any)[0].(map[string]any) + variant := definition["fieldTree"].(map[string]any)["variants"].([]any)[0].(map[string]any) + variant["allowedFields"] = []any{} + variant["requiredFields"] = []any{} + variant["rootKind"] = "array" + refreshDefinitionDigests(contract) + }, + want: "object root cannot declare array", + }, + { + name: "union has only one root kind", + mutate: func(contract map[string]any) { + definition := contract["contractDefinitions"].([]any)[0].(map[string]any) + definition["rootType"] = "union" + commandAt(contract, "sample")["inputContract"].(map[string]any)["rootType"] = "union" + refreshDefinitionDigests(contract) + refreshRootDefinitionDigests(contract) + }, + want: "union must enumerate at least two distinct root kinds", + }, + { + name: "json value disguises bounded object", + mutate: func(contract map[string]any) { + definition := contract["contractDefinitions"].([]any)[0].(map[string]any) + definition["rootType"] = "json_value" + commandAt(contract, "sample")["inputContract"].(map[string]any)["rootType"] = "json_value" + refreshDefinitionDigests(contract) + refreshRootDefinitionDigests(contract) + }, + want: "json_value root cannot declare bounded object", + }, + { + name: "root-shape non-claims drift", + mutate: func(contract map[string]any) { + definition := contract["contractDefinitions"].([]any)[0].(map[string]any) + definition["fieldTree"].(map[string]any)["nonClaims"] = []any{"Nested shape is not claimed."} + refreshDefinitionDigests(contract) + }, + want: "exact root-shape non-claims", + }, + { + name: "contract schema version is invalid", + mutate: func(contract map[string]any) { + commandAt(contract, "sample")["inputContract"].(map[string]any)["schemaVersion"] = 0 + }, + want: "invalid schemaVersion", + }, + { + name: "root type drifts from definition", + mutate: func(contract map[string]any) { + commandAt(contract, "sample")["inputContract"].(map[string]any)["rootType"] = "json_value" + }, + want: "rootType does not match definition", + }, + { + name: "deleted root definition", + mutate: func(contract map[string]any) { + contract["contractDefinitions"] = contract["contractDefinitions"].([]any)[1:] + }, + want: "references unknown definition", + }, + { + name: "native source digest mismatch", + mutate: func(contract map[string]any) { + source := commandAt(contract, "sample")["inputContract"].(map[string]any)["nativeSource"].(map[string]any) + source["canonicalDigest"] = "sha256:" + strings.Repeat("0", 64) + }, + want: "native source digest mismatch", + }, + { + name: "native source forms are both present", + mutate: func(contract map[string]any) { + input := commandAt(contract, "sample")["inputContract"].(map[string]any) + input["nativeSources"] = []any{input["nativeSource"]} + }, + want: "must declare exactly one of nativeSource or nativeSources", + }, + { + name: "native source forms are both absent", + mutate: func(contract map[string]any) { + delete(commandAt(contract, "sample")["inputContract"].(map[string]any), "nativeSource") + }, + want: "must declare exactly one of nativeSource or nativeSources", + }, + { + name: "native sources are empty", + mutate: func(contract map[string]any) { + input := commandAt(contract, "sample")["inputContract"].(map[string]any) + delete(input, "nativeSource") + input["nativeSources"] = []any{} + }, + want: "nativeSources must be non-empty", + }, + { + name: "native sources are duplicate", + mutate: func(contract map[string]any) { + input := commandAt(contract, "sample")["inputContract"].(map[string]any) + source := input["nativeSource"] + delete(input, "nativeSource") + input["nativeSources"] = []any{source, source} + }, + want: "nativeSources must be sorted and unique by path", + }, + { + name: "stale selector", + mutate: func(contract map[string]any) { + selector := commandAt(contract, "sample")["inputContract"].(map[string]any)["nativeAdmissionWitnessSelector"].(map[string]any) + selector["test"] = "TestMissing" + selector["command"] = "go test ./internal/command/sample -run '^TestMissing$'" + }, + want: "test function TestMissing does not exist", + }, + { + name: "selector name is not discoverable by go test", + mutate: func(contract map[string]any) { + selector := commandAt(contract, "sample")["inputContract"].(map[string]any)["nativeAdmissionWitnessSelector"].(map[string]any) + selector["test"] = "Testlowercase" + selector["command"] = "go test ./internal/command/sample -run '^Testlowercase$'" + }, + want: "selector test is invalid", + }, + { + name: "unselectable selector command", + mutate: func(contract map[string]any) { + selector := commandAt(contract, "sample")["inputContract"].(map[string]any)["nativeAdmissionWitnessSelector"].(map[string]any) + selector["command"] = "go test ./internal/command/sample" + }, + want: "selector command must select exactly", + }, + { + name: "tracked selector is inactive in current build", + mutate: func(contract map[string]any) { + for _, direction := range []string{"inputContract", "outputContract"} { + value, _ := commandAt(contract, "sample")[direction].(map[string]any) + selectorKey := "nativeAdmissionWitnessSelector" + if direction == "outputContract" { + selectorKey = "nativeOutputWitnessSelector" + } + selector := value[selectorKey].(map[string]any) + selector["path"] = "internal/command/sample/inactive_test.go" + selector["test"] = "TestInactive" + selector["command"] = "go test ./internal/command/sample -run '^TestInactive$'" + } + }, + want: "not active in the current Go build", + }, + { + name: "untracked selector", + mutate: func(contract map[string]any) { + source := []byte("package sample\n\nimport \"testing\"\n\nfunc TestUntracked(t *testing.T) {}\n") + _ = os.WriteFile(filepath.Join(fixtureRoot(contract), "internal/command/sample/untracked_test.go"), source, 0o644) + for _, direction := range []string{"inputContract", "outputContract"} { + value, _ := commandAt(contract, "sample")[direction].(map[string]any) + selectorKey := "nativeAdmissionWitnessSelector" + if direction == "outputContract" { + selectorKey = "nativeOutputWitnessSelector" + } + selector := value[selectorKey].(map[string]any) + selector["path"] = "internal/command/sample/untracked_test.go" + selector["test"] = "TestUntracked" + selector["command"] = "go test ./internal/command/sample -run '^TestUntracked$'" + } + }, + want: "Git-index-tracked", + }, + { + name: "deleted tracked selector", + mutate: func(contract map[string]any) { + _ = os.Remove(filepath.Join(fixtureRoot(contract), "internal/command/sample/sample_test.go")) + }, + want: "selector path is unavailable", + }, + { + name: "tracked selector replaced by symlink", + mutate: func(contract map[string]any) { + path := filepath.Join(fixtureRoot(contract), "internal/command/sample/sample_test.go") + _ = os.Remove(path) + _ = os.Symlink("sample.go", path) + }, + want: "regular non-symlink file", + }, + } + + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + root := writeFixture(t) + contract := readFixtureContract(t, root) + contract["__fixtureRoot"] = root + test.mutate(contract) + delete(contract, "__fixtureRoot") + writeFixtureContract(t, root, contract) + _, _, err := render(root) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("render() error=%v, want substring %q", err, test.want) + } + }) + } +} + +func TestRunGeneratesAndChecksBothProjections(t *testing.T) { + root := writeFixture(t) + if err := run(root, false); err != nil { + t.Fatalf("run(generate): %v", err) + } + for _, path := range []string{appGeneratedPath, presetGeneratedPath} { + content, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(path))) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + if !strings.Contains(string(content), "Code generated by internal/tools/commandcontractgen") { + t.Fatalf("%s lacks generated-file marker", path) + } + } + if err := run(root, true); err != nil { + t.Fatalf("run(check): %v", err) + } + appPath := filepath.Join(root, filepath.FromSlash(appGeneratedPath)) + if err := os.WriteFile(appPath, []byte("stale\n"), 0o644); err != nil { + t.Fatal(err) + } + if err := run(root, true); err == nil || !strings.Contains(err.Error(), "application projection is stale") { + t.Fatalf("run(check stale app) error=%v", err) + } + if err := run(root, false); err != nil { + t.Fatal(err) + } + presetPath := filepath.Join(root, filepath.FromSlash(presetGeneratedPath)) + if err := os.WriteFile(presetPath, []byte("stale\n"), 0o644); err != nil { + t.Fatal(err) + } + if err := run(root, true); err == nil || !strings.Contains(err.Error(), "preset projection is stale") { + t.Fatalf("run(check stale preset) error=%v", err) + } +} + +func TestRenderedMetadataDigestIncludesRootShapeVariants(t *testing.T) { + root := writeFixture(t) + contract := readFixtureContract(t, root) + definitions := contract["contractDefinitions"].([]any) + inputDefinition := definitions[0].(map[string]any) + refreshDefinitionDigests(contract) + refreshRootDefinitionDigests(contract) + writeFixtureContract(t, root, contract) + + before, _, err := render(root) + if err != nil { + t.Fatalf("render before root-shape mutation: %v", err) + } + inputVariant := inputDefinition["fieldTree"].(map[string]any)["variants"].([]any)[0].(map[string]any) + inputVariant["allowedFields"] = []any{"mode", "note", "schemaVersion"} + refreshDefinitionDigests(contract) + refreshRootDefinitionDigests(contract) + writeFixtureContract(t, root, contract) + + after, _, err := render(root) + if err != nil { + t.Fatalf("render after root-shape mutation: %v", err) + } + if bytes.Equal(before, after) { + t.Fatal("generated metadata did not change after a root-shape variant changed") + } +} + +func writeFixture(t *testing.T) string { + t.Helper() + root := t.TempDir() + for _, dir := range []string{ + "proofkit", + "internal/app", + "internal/command/sample", + "internal/command/stackpreset", + } { + if err := os.MkdirAll(filepath.Join(root, filepath.FromSlash(dir)), 0o755); err != nil { + t.Fatal(err) + } + } + source := []byte("package sample\n\nfunc Build() {}\n") + testSource := []byte("package sample\n\nimport testpkg \"testing\"\n\nfunc TestBuildRejectsInvalidInput(*testpkg.T) {}\n") + inactiveTestSource := []byte("//go:build never\n\npackage sample\n\nimport \"testing\"\n\nfunc TestInactive(t *testing.T) {}\n") + if err := os.WriteFile(filepath.Join(root, "internal/command/sample/sample.go"), source, 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(root, "internal/command/sample/sample_test.go"), testSource, 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(root, "internal/command/sample/inactive_test.go"), inactiveTestSource, 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module fixture.invalid/sample\n\ngo 1.25\n"), 0o644); err != nil { + t.Fatal(err) + } + sourceDigest := digestFiles(t, root, []string{"internal/command/sample/sample.go"}) + definitions := []any{ + structuralFixtureDefinition("proofkit.input.v1"), + structuralFixtureDefinition("proofkit.output.v1"), + } + contract := map[string]any{ + "schemaVersion": 2, + "contractId": "proofkit.cli-contract.v2", + "packageName": "@research-engineering/agentic-proofkit", + "processContract": map[string]any{}, + "contractDefinitions": definitions, + "commands": []any{ + map[string]any{ + "command": "sample", + "input": "required", + "stdin": true, + "inputPointer": true, + "scopeClass": "explicit_caller_input", + "outputModes": []any{"json"}, + "allowedFlags": []any{"--input", "--input-pointer"}, + "inputContract": contractFixture( + "proofkit.sample.input.v1", + "proofkit.input.v1", + sourceDigest, + "nativeAdmissionWitnessSelector", + ), + "outputContract": contractFixture( + "proofkit.sample.output.v1", + "proofkit.output.v1", + sourceDigest, + "nativeOutputWitnessSelector", + ), + }, + map[string]any{ + "command": "stack-preset", + "input": "none", + "stdin": false, + "inputPointer": false, + "scopeClass": "built_in_package_catalog", + "outputModes": []any{"json"}, + "allowedFlags": []any{"--preset"}, + "requiredFlags": []any{"--preset"}, + "outputContract": func() map[string]any { + value := contractFixture( + "proofkit.stack-preset.output.v1", + "proofkit.output.v1", + sourceDigest, + "nativeOutputWitnessSelector", + ) + value["flagChoices"] = map[string]any{"--preset": []any{"alpha", "beta"}} + return value + }(), + }, + }, + } + refreshDefinitionDigests(contract) + refreshRootDefinitionDigests(contract) + writeFixtureContract(t, root, contract) + runGit(t, root, "init") + runGit(t, root, "add", ".") + return root +} + +func refreshRootDefinitionDigests(contract map[string]any) { + definitions := contract["contractDefinitions"].([]any) + for _, command := range contract["commands"].([]any) { + record := command.(map[string]any) + for _, key := range []string{"inputContract", "outputContract"} { + value, ok := record[key].(map[string]any) + if !ok { + continue + } + ref := value["rootDefinitionRef"].(string) + for _, raw := range definitions { + definition := raw.(map[string]any) + if definition["definitionId"] == ref { + value["rootDefinitionDigest"] = definition["canonicalDigest"] + } + } + } + } +} + +func structuralFixtureDefinition(id string) map[string]any { + return map[string]any{ + "definitionId": id, + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": []any{}, + "fieldTree": map[string]any{ + "kind": "root_shape_only", + "nonClaims": []any{ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection.", + }, + "variants": []any{ + map[string]any{ + "allowedFields": []any{"mode", "schemaVersion"}, + "requiredFields": []any{"mode", "schemaVersion"}, + "rootKind": "object", + "variantId": "default", + "when": []any{"default"}, + }, + }, + }, + } +} + +func fixtureRoot(contract map[string]any) string { + root, _ := contract["__fixtureRoot"].(string) + return root +} + +func runGit(t *testing.T, root string, args ...string) { + t.Helper() + command := exec.Command("git", append([]string{"-C", root}, args...)...) + if output, err := command.CombinedOutput(); err != nil { + t.Fatalf("git %v: %v\n%s", args, err, output) + } +} + +func contractFixture(contractID string, definitionID string, sourceDigest string, selectorKey string) map[string]any { + return map[string]any{ + "contractId": contractID, + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": definitionID, + "rootDefinitionDigest": "", + "ownerRequirementRefs": []any{"REQ-PROOFKIT-PACKAGE-002"}, + "nativeSource": map[string]any{ + "path": "internal/command/sample", + "canonicalDigest": sourceDigest, + "evidenceClass": "source_checkout", + }, + selectorKey: map[string]any{ + "path": "internal/command/sample/sample_test.go", + "test": "TestBuildRejectsInvalidInput", + "command": "go test ./internal/command/sample -run '^TestBuildRejectsInvalidInput$'", + "evidenceClass": "source_checkout", + }, + "compatibilitySummary": []any{"schemaVersion=1", "closed native admission contract"}, + } +} + +func commandAt(contract map[string]any, name string) map[string]any { + for _, raw := range contract["commands"].([]any) { + command := raw.(map[string]any) + if command["command"] == name { + return command + } + } + panic("missing fixture command") +} + +func refreshDefinitionDigests(contract map[string]any) { + for _, raw := range contract["contractDefinitions"].([]any) { + definition := raw.(map[string]any) + delete(definition, "canonicalDigest") + encoded, _ := canonicalJSON(definition) + sum := sha256.Sum256(encoded) + definition["canonicalDigest"] = "sha256:" + hex.EncodeToString(sum[:]) + } +} + +func readFixtureContract(t *testing.T, root string) map[string]any { + t.Helper() + content, err := os.ReadFile(filepath.Join(root, cliContractPath)) + if err != nil { + t.Fatal(err) + } + var value map[string]any + if err := json.Unmarshal(content, &value); err != nil { + t.Fatal(err) + } + return value +} + +func writeFixtureContract(t *testing.T, root string, contract map[string]any) { + t.Helper() + content, err := json.MarshalIndent(contract, "", " ") + if err != nil { + t.Fatal(err) + } + content = append(content, '\n') + if err := os.WriteFile(filepath.Join(root, cliContractPath), content, 0o644); err != nil { + t.Fatal(err) + } +} + +func digestFiles(t *testing.T, root string, paths []string) string { + t.Helper() + hash := sha256.New() + for _, path := range paths { + content, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(path))) + if err != nil { + t.Fatal(err) + } + hash.Write([]byte(path)) + hash.Write([]byte{0}) + hash.Write(content) + hash.Write([]byte{0}) + } + return "sha256:" + hex.EncodeToString(hash.Sum(nil)) +} diff --git a/internal/tools/coveragemetrics/main.go b/internal/tools/coveragemetrics/main.go index a21e855..85bd099 100644 --- a/internal/tools/coveragemetrics/main.go +++ b/internal/tools/coveragemetrics/main.go @@ -4,10 +4,18 @@ import ( "encoding/json" "errors" "fmt" + "go/ast" + "go/parser" + "go/token" "os" + "os/exec" "path/filepath" "sort" + "strconv" + "strings" "time" + "unicode" + "unicode/utf8" "github.com/research-engineering/agentic-proofkit/internal/app" "github.com/research-engineering/agentic-proofkit/internal/command/requirementsourceadmission" @@ -48,10 +56,17 @@ type bindingRequirement struct { } type bindingScenario struct { - CommandIDs []string `json:"commandIds"` - RequirementID string `json:"requirementId"` - ScenarioID string `json:"scenarioId"` - WitnessID string `json:"witnessId"` + CommandIDs []string `json:"commandIds"` + RequirementID string `json:"requirementId"` + ScenarioID string `json:"scenarioId"` + WitnessID string `json:"witnessId"` + WitnessPath string `json:"witnessPath"` + WitnessSelectors []witnessSelector `json:"witnessSelectors"` +} + +type witnessSelector struct { + Command string `json:"command"` + Selector string `json:"selector"` } type witnessPlan struct { @@ -173,10 +188,364 @@ func run() error { closeoutErr := errors.Join( requireCommandRouteInventoryClosure(out.CommandRoutes), requireNoLinkageDeadZones(out.DeadZones), + validateBindingWitnessSelectorsAtRoot(".", bindings), ) return writeMetrics(out, closeoutErr) } +func validateBindingWitnessSelectorsAtRoot(root string, bindings bindingFile) error { + if err := validateRequiredBindingWitnessSelectors(bindings); err != nil { + return err + } + return validateBindingWitnessSelectorExecutabilityAtRoot(root, bindings) +} + +func validateRequiredBindingWitnessSelectors(bindings bindingFile) error { + type inventoryKey struct { + requirementID string + scenarioID string + } + required := map[inventoryKey][]string{ + {"REQ-PROOFKIT-PACKAGE-001", "proofkit.package-boundary.root-export-and-deep-import-denial"}: {"TestVerifyRootPackageRejectsEachForbiddenRootEntry"}, + {"REQ-PROOFKIT-PACKAGE-002", "proofkit.package-boundary.launcher-profile-admission"}: {"TestLauncherProfileAdmissionMatrix"}, + {"REQ-PROOFKIT-PACKAGE-002", "proofkit.package-boundary.generated-command-field-inventory"}: { + "TestGeneratedCommandInvocationProfileFieldInventory", + "TestGeneratedCommandInvocationProfileRouteClosure", + }, + {"REQ-PROOFKIT-PACKAGE-002", "proofkit.package-boundary.generated-command-caller-preservation"}: {"TestBootstrapPreservesCallerDisplayCommandInGuidancePayload"}, + {"REQ-PROOFKIT-PACKAGE-002", "proofkit.package-boundary.cli-output-root-witnesses"}: { + "TestAdoptionContractEnvelopeCLIABI", + "TestSelfCheckOutputUsesExactRootShape", + "TestStandaloneMultiVariantCommandsUseExactRootShapes", + }, + {"REQ-PROOFKIT-PACKAGE-003", "proofkit.package-boundary.outside-consumer-artifact"}: {"TestExactTarballOnboardingTrace"}, + {"REQ-PROOFKIT-PACKAGE-004", "proofkit.package-boundary.ci-receipt-anchor"}: {"TestReceiptIDKeepsLocalAndCIIdentitiesDistinct"}, + {"REQ-PROOFKIT-PACKAGE-004", "proofkit.package-boundary.self-hosting-report-verdict"}: {"TestRunProofkitVerdictCases"}, + {"REQ-PROOFKIT-PACKAGE-005", "proofkit.package-boundary.merge-critical-runtime-preconditions"}: {"TestCISourceQualityInstallsPythonBeforeLifecycleTests"}, + {"REQ-PROOFKIT-PACKAGE-006", "proofkit.package-boundary.python-wheel-candidate"}: {"TestPythonArtifactRefsRejectEachWheelIdentityDefect"}, + {"REQ-PROOFKIT-PACKAGE-006", "proofkit.package-boundary.python-wheel-generated-continuation"}: { + "TestExactDisplayedRouteOperandsRejectsWhitespaceAndExpansionMutants", + "TestInstalledWheelContinuationUsesExactPythonModuleProfileWithoutNPM", + }, + {"REQ-PROOFKIT-PACKAGE-007", "proofkit.package-boundary.package-public-docs-no-mutable-release-facts"}: { + "TestVerifyNoStalePackageDocsRejectsMutableReleaseFactsInMarkdown", + }, + {"REQ-PROOFKIT-QUALITY-004", "proofkit.supply-chain-quality.cli-abi-golden"}: { + "TestAdoptionContractEnvelopeCLIABI", + "TestRequiredInputCommandsRouteStructuralErrorsByMode", + "TestRequirementBrowserOneShotCLIOutputVariants", + "TestSelfCheckOutputUsesExactRootShape", + "TestStandaloneMultiVariantCommandsUseExactRootShapes", + }, + {"REQ-PROOFKIT-QUALITY-004", "proofkit.supply-chain-quality.cli-contract-topology"}: { + "TestCLIConditionModelClosesAdoptionOutputRoutes", + "TestCommandDescriptorContractParityRejectsMutations", + }, + {"REQ-PROOFKIT-QUALITY-004", "proofkit.supply-chain-quality.cli-output-witness-contract"}: { + "TestRootDistinctOutputWitnessBindingsAreExact", + }, + {"REQ-PROOFKIT-QUALITY-005", "proofkit.supply-chain-quality.codeql-permission-separation"}: { + "TestSecurityScannerWorkflowsSeparateProviderPublicationPermissions", + }, + {"REQ-PROOFKIT-QUALITY-006", "proofkit.supply-chain-quality.osv-permission-separation"}: { + "TestSecurityScannerWorkflowsSeparateProviderPublicationPermissions", + }, + {"REQ-PROOFKIT-QUALITY-007", "proofkit.supply-chain-quality.scorecard-permission-and-publication-inputs"}: { + "TestScorecardPublicPublishDeclaresRequiredOutputInputs", + "TestSecurityScannerWorkflowsSeparateProviderPublicationPermissions", + }, + {"REQ-PROOFKIT-QUALITY-010", "proofkit.supply-chain-quality.coverage-metrics"}: {"TestEachCommandRouteClosureConjunctHasIndependentFalsifier", "TestEachLinkageDeadZoneConjunctHasIndependentFalsifier"}, + {"REQ-PROOFKIT-QUALITY-010", "proofkit.supply-chain-quality.binding-selector-executability"}: { + "TestBindingWitnessSelectorsAcceptUnnamedGoTestParameter", + "TestBindingWitnessSelectorsRejectInvalidGoTestSignature", + "TestBindingWitnessSelectorsRejectMissingSemanticOwner", + "TestBindingWitnessSelectorsRejectNonTestAndBuildExcludedFiles", + "TestBindingWitnessSelectorsRequireExactCriticalInventories", + }, + {"REQ-PROOFKIT-QUALITY-011", "proofkit.supply-chain-quality.ci-required-aggregate-exactness"}: { + "TestCIRequiredAggregateRejectsExecutionOverrides", + "TestCIRequiredAggregateRejectsNeutralizedScript", + "TestCIRequiredAggregateRejectsPlatformSmokeSubstitution", + "TestCIWorkflowDeclaresFailClosedRequiredAggregate", + }, + {"REQ-PROOFKIT-QUALITY-013", "proofkit.supply-chain-quality.workflow-package-gate-oracle"}: { + "TestCIWorkflowDeclaresFailClosedRequiredAggregate", + "TestNeedsListNormalizesStringAndList", + "TestPackageGateWorkflowOracleAcceptsOwnerCIAndReleaseWorkflows", + "TestPackageGateWorkflowOracleAdmitsAlwaysWithNeedSuccess", + "TestPackageGateWorkflowOracleAdmitsLaterAlwaysWithSuccess", + "TestPackageGateWorkflowOracleAdmitsPrivateAttestationBypass", + "TestPackageGateWorkflowOracleRejectsAlwaysWithoutNeedSuccess", + "TestPackageGateWorkflowOracleRejectsDisabledAndShadowedEvidence", + "TestPackageGateWorkflowOracleRejectsDuplicatePriorStepName", + "TestPackageGateWorkflowOracleRejectsExecutionOverrides", + "TestPackageGateWorkflowOracleRejectsLateRequiredPriorStep", + "TestPackageGateWorkflowOracleRejectsMissingWorkflowPermissionFloor", + "TestPackageGateWorkflowOracleRejectsNeedSuccessBypass", + "TestPackageGateWorkflowOracleRejectsRequiredPriorExecutionOverride", + "TestPackageGateWorkflowOracleRejectsUnusedAllowedStepEnvironment", + "TestPackageGateWorkflowOracleRejectsWrongPriorStepCommand", + "TestWorkflowGuardExpressionsRejectNeutralization", + }, + {"REQ-PROOFKIT-QUALITY-016", "proofkit.supply-chain-quality.release-platform-python-wheels"}: { + "TestREADMEPlatformAndPythonProjection", + "TestReleaseTargetsProjectExactPythonWheelMetadata", + "TestVerifyWheelContentsRequiresExactWheelMetadata", + }, + {"REQ-PROOFKIT-QUALITY-019", "proofkit.supply-chain-quality.installed-package-json-abi-smoke"}: { + "TestExactTarballOnboardingTrace", + "TestInstalledInvocationRequiresAuthoredOrderAndExactCommandToken", + "TestInstalledREADMEFirstInputPreservesJSONExampleBytes", + "TestInstalledREADMEFirstInputUsesBoundedLiteralShellWords", + "TestLiteralShellWordsConsumesLongBackslashRun", + "TestOnboardingTraceCoversEveryDiscoveredPresetAndREADMEInput", + }, + {"REQ-PROOFKIT-QUALITY-022", "proofkit.supply-chain-quality.browser-failure-diagnostics-retention"}: { + "TestCIBrowserRuntimeRetainsFailureDiagnosticsWithoutPublishingProof", + }, + {"REQ-PROOFKIT-QUALITY-023", "proofkit.supply-chain-quality.python-wheel-platform-byte-compatibility"}: { + "TestMachOMinimumMacOSAcceptsLegacyVersionCommand", + "TestMachOMinimumMacOSRejectsTruncatedBuildVersion", + "TestVerifyWheelContentsAcceptsDarwinTagAtOrAboveMachOMinimum", + "TestVerifyWheelContentsRejectsDarwinTagBelowMachOMinimum", + }, + {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.release-change-record-projection"}: { + "TestAdmitEnforcesVersionedChangeClass", + "TestCurrentChangeRecordNamesReviewedSemanticChanges", + "TestRenderStatesPreOneExactPinPolicy", + }, + {"REQ-PROOFKIT-QUALITY-025", "proofkit.supply-chain-quality.workflow-source-oracles"}: { + "TestExistingReleasePathIsReadOnlyAndFailsOnDrift", + "TestWorkflowClosedKeyAdmission", + "TestWorkflowExternalActionsUseFullCommitSHAs", + }, + {"REQ-PROOFKIT-SPEC-011", "proofkit.spec-proof-core.adoption-contract-envelope-cli-abi"}: { + "TestAdoptionContractEnvelopeCLIABI", + }, + {"REQ-PROOFKIT-SPEC-021", "proofkit.spec-proof-core.requirement-browser-one-shot-cleanup"}: { + "TestServeOneShotDoesNotReadCompletedDoneTwice", + "TestServeOneShotReturnsCleanupFailuresWithoutWritingTerminalPacket", + "TestServeOneShotWaitsForDoneBeforeWritingTerminalPacket", + }, + } + requiredPaths := map[inventoryKey]string{ + {"REQ-PROOFKIT-PACKAGE-001", "proofkit.package-boundary.root-export-and-deep-import-denial"}: "internal/tools/packageverify/main_test.go", + {"REQ-PROOFKIT-PACKAGE-002", "proofkit.package-boundary.launcher-profile-admission"}: "internal/kernel/cliexec/cliexec_test.go", + {"REQ-PROOFKIT-PACKAGE-002", "proofkit.package-boundary.generated-command-field-inventory"}: "internal/app/invocation_profile_test.go", + {"REQ-PROOFKIT-PACKAGE-002", "proofkit.package-boundary.generated-command-caller-preservation"}: "internal/command/gradualadoption/gradualadoption_test.go", + {"REQ-PROOFKIT-PACKAGE-002", "proofkit.package-boundary.cli-output-root-witnesses"}: "internal/app/cli_abi_test.go", + {"REQ-PROOFKIT-PACKAGE-003", "proofkit.package-boundary.outside-consumer-artifact"}: "internal/tools/packageverify/main_test.go", + {"REQ-PROOFKIT-PACKAGE-004", "proofkit.package-boundary.ci-receipt-anchor"}: "scripts/validate-self-hosting-receipts_test.go", + {"REQ-PROOFKIT-PACKAGE-004", "proofkit.package-boundary.self-hosting-report-verdict"}: "scripts/validate-self-hosting-receipts_test.go", + {"REQ-PROOFKIT-PACKAGE-005", "proofkit.package-boundary.merge-critical-runtime-preconditions"}: "scripts/workflow_runtime_preconditions_test.go", + {"REQ-PROOFKIT-PACKAGE-006", "proofkit.package-boundary.python-wheel-candidate"}: "scripts/validate-self-hosting-receipts_test.go", + {"REQ-PROOFKIT-PACKAGE-006", "proofkit.package-boundary.python-wheel-generated-continuation"}: "internal/tools/pythonpackage/continuation_test.go", + {"REQ-PROOFKIT-PACKAGE-007", "proofkit.package-boundary.package-public-docs-no-mutable-release-facts"}: "internal/tools/packageverify/main_test.go", + {"REQ-PROOFKIT-QUALITY-004", "proofkit.supply-chain-quality.cli-abi-golden"}: "internal/app/cli_abi_test.go", + {"REQ-PROOFKIT-QUALITY-004", "proofkit.supply-chain-quality.cli-contract-topology"}: "internal/app/cli_contract_test.go", + {"REQ-PROOFKIT-QUALITY-004", "proofkit.supply-chain-quality.cli-output-witness-contract"}: "internal/app/cli_output_witness_contract_test.go", + {"REQ-PROOFKIT-QUALITY-005", "proofkit.supply-chain-quality.codeql-permission-separation"}: "scripts/workflow_security_scanner_oracles_test.go", + {"REQ-PROOFKIT-QUALITY-006", "proofkit.supply-chain-quality.osv-permission-separation"}: "scripts/workflow_security_scanner_oracles_test.go", + {"REQ-PROOFKIT-QUALITY-007", "proofkit.supply-chain-quality.scorecard-permission-and-publication-inputs"}: "scripts/workflow_security_scanner_oracles_test.go", + {"REQ-PROOFKIT-QUALITY-010", "proofkit.supply-chain-quality.binding-selector-executability"}: "internal/tools/coveragemetrics/main_test.go", + {"REQ-PROOFKIT-QUALITY-010", "proofkit.supply-chain-quality.coverage-metrics"}: "internal/tools/coveragemetrics/main_test.go", + {"REQ-PROOFKIT-QUALITY-011", "proofkit.supply-chain-quality.ci-required-aggregate-exactness"}: "scripts/workflow_package_gate_oracle_test.go", + {"REQ-PROOFKIT-QUALITY-013", "proofkit.supply-chain-quality.workflow-package-gate-oracle"}: "scripts/workflow_package_gate_oracle_test.go", + {"REQ-PROOFKIT-QUALITY-016", "proofkit.supply-chain-quality.release-platform-python-wheels"}: "internal/tools/pythonpackage/metadata_test.go", + {"REQ-PROOFKIT-QUALITY-019", "proofkit.supply-chain-quality.installed-package-json-abi-smoke"}: "internal/tools/packageverify/main_test.go", + {"REQ-PROOFKIT-QUALITY-022", "proofkit.supply-chain-quality.browser-failure-diagnostics-retention"}: "scripts/workflow_browser_runtime_oracle_test.go", + {"REQ-PROOFKIT-QUALITY-023", "proofkit.supply-chain-quality.python-wheel-platform-byte-compatibility"}: "internal/tools/pythonpackage/metadata_test.go", + {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.release-change-record-projection"}: "internal/tools/releasechange/record_test.go", + {"REQ-PROOFKIT-QUALITY-025", "proofkit.supply-chain-quality.workflow-source-oracles"}: "scripts/workflow_source_oracles_test.go", + {"REQ-PROOFKIT-SPEC-011", "proofkit.spec-proof-core.adoption-contract-envelope-cli-abi"}: "internal/app/cli_abi_test.go", + {"REQ-PROOFKIT-SPEC-021", "proofkit.spec-proof-core.requirement-browser-one-shot-cleanup"}: "internal/command/requirementbrowser/server_test.go", + } + if len(requiredPaths) != len(required) { + return fmt.Errorf("required selector path inventory=%d, selector inventory=%d", len(requiredPaths), len(required)) + } + seenRequired := map[inventoryKey]struct{}{} + for _, binding := range bindings.Bindings { + key := inventoryKey{requirementID: binding.RequirementID, scenarioID: binding.ScenarioID} + want, isRequired := required[key] + if !isRequired { + continue + } + wantPath, hasRequiredPath := requiredPaths[key] + if !hasRequiredPath { + return fmt.Errorf("binding %s has selectors but no exact witness path inventory", binding.ScenarioID) + } + if binding.WitnessPath != wantPath { + return fmt.Errorf("binding %s witness path=%q, want exact %q", binding.ScenarioID, binding.WitnessPath, wantPath) + } + seenRequired[key] = struct{}{} + got := make([]string, 0, len(binding.WitnessSelectors)) + for _, selector := range binding.WitnessSelectors { + got = append(got, selector.Selector) + } + sort.Strings(got) + if !equalStrings(got, want) { + return fmt.Errorf("binding %s witness selectors=%v, want %v", binding.ScenarioID, got, want) + } + } + for key := range required { + if _, ok := requiredPaths[key]; !ok { + return fmt.Errorf("required exact witness path is missing: %s/%s", key.requirementID, key.scenarioID) + } + if _, ok := seenRequired[key]; !ok { + return fmt.Errorf("required independent-falsifier binding is missing: %s/%s", key.requirementID, key.scenarioID) + } + } + return nil +} + +func validateBindingWitnessSelectorExecutabilityAtRoot(root string, bindings bindingFile) error { + activeWitnessFiles := map[string]bool{} + for _, binding := range bindings.Bindings { + if len(binding.WitnessSelectors) == 0 { + continue + } + path := filepath.Join(root, filepath.FromSlash(binding.WitnessPath)) + source, err := parser.ParseFile(token.NewFileSet(), path, nil, 0) + if err != nil { + return fmt.Errorf("parse binding witness %s: %w", binding.WitnessPath, err) + } + functions := map[string]*ast.FuncDecl{} + for _, declaration := range source.Decls { + function, ok := declaration.(*ast.FuncDecl) + if ok && function.Recv == nil { + functions[function.Name.Name] = function + } + } + testingAliases, dotImportedTesting := importedTestingNames(source) + packagePath := "./" + filepath.ToSlash(filepath.Dir(binding.WitnessPath)) + for _, selector := range binding.WitnessSelectors { + function, ok := functions[selector.Selector] + if !ok { + return fmt.Errorf("binding %s selector %s is missing from %s", binding.ScenarioID, selector.Selector, binding.WitnessPath) + } + if !validGoTestFunction(function, testingAliases, dotImportedTesting) { + return fmt.Errorf("binding %s selector %s is not a valid Go test function", binding.ScenarioID, selector.Selector) + } + expectedCommand := fmt.Sprintf("go test %s -run '^%s$'", packagePath, selector.Selector) + if selector.Command != expectedCommand { + return fmt.Errorf("binding %s selector command=%q, want %q", binding.ScenarioID, selector.Command, expectedCommand) + } + } + if !strings.HasSuffix(binding.WitnessPath, "_test.go") { + return fmt.Errorf("binding %s witness %s must be an active _test.go file", binding.ScenarioID, binding.WitnessPath) + } + active, checked := activeWitnessFiles[binding.WitnessPath] + if !checked { + active, err = activeGoTestFile(root, packagePath, binding.WitnessPath) + if err != nil { + return fmt.Errorf("discover binding witness %s: %w", binding.WitnessPath, err) + } + activeWitnessFiles[binding.WitnessPath] = active + } + if !active { + return fmt.Errorf("binding %s witness %s is not active for the current Go build", binding.ScenarioID, binding.WitnessPath) + } + } + return nil +} + +func activeGoTestFile(root, packagePath, witnessPath string) (bool, error) { + command := exec.Command("go", "list", "-json", packagePath) + command.Dir = root + output, err := command.CombinedOutput() + if err != nil { + return false, fmt.Errorf("go list %s: %w: %s", packagePath, err, strings.TrimSpace(string(output))) + } + var listed struct { + Dir string + TestGoFiles []string + XTestGoFiles []string + } + if err := json.Unmarshal(output, &listed); err != nil { + return false, fmt.Errorf("decode go list %s: %w", packagePath, err) + } + witnessAbsolute, err := filepath.Abs(filepath.Join(root, filepath.FromSlash(witnessPath))) + if err != nil { + return false, err + } + for _, file := range append(listed.TestGoFiles, listed.XTestGoFiles...) { + activeAbsolute, err := filepath.Abs(filepath.Join(listed.Dir, file)) + if err != nil { + return false, err + } + if filepath.Clean(activeAbsolute) == filepath.Clean(witnessAbsolute) { + return true, nil + } + } + return false, nil +} + +func importedTestingNames(source *ast.File) (map[string]struct{}, bool) { + aliases := map[string]struct{}{} + dotImported := false + for _, specification := range source.Imports { + path, err := strconv.Unquote(specification.Path.Value) + if err != nil || path != "testing" { + continue + } + switch { + case specification.Name == nil: + aliases["testing"] = struct{}{} + case specification.Name.Name == ".": + dotImported = true + case specification.Name.Name != "_": + aliases[specification.Name.Name] = struct{}{} + } + } + return aliases, dotImported +} + +func validGoTestFunction(function *ast.FuncDecl, testingAliases map[string]struct{}, dotImportedTesting bool) bool { + if !validGoTestName(function.Name.Name) || + function.Type.TypeParams != nil || + function.Type.Results != nil && len(function.Type.Results.List) != 0 || + function.Type.Params == nil || len(function.Type.Params.List) != 1 { + return false + } + parameter := function.Type.Params.List[0] + if len(parameter.Names) > 1 { + return false + } + pointer, ok := parameter.Type.(*ast.StarExpr) + if !ok { + return false + } + if identifier, ok := pointer.X.(*ast.Ident); ok { + return dotImportedTesting && identifier.Name == "T" + } + selector, ok := pointer.X.(*ast.SelectorExpr) + if !ok || selector.Sel.Name != "T" { + return false + } + identifier, ok := selector.X.(*ast.Ident) + if !ok { + return false + } + _, ok = testingAliases[identifier.Name] + return ok +} + +func validGoTestName(name string) bool { + if !strings.HasPrefix(name, "Test") { + return false + } + suffix := strings.TrimPrefix(name, "Test") + if suffix == "" { + return true + } + first, _ := utf8.DecodeRuneInString(suffix) + return !unicode.IsLower(first) +} + +func equalStrings(left, right []string) bool { + return len(left) == len(right) && strings.Join(left, "\x00") == strings.Join(right, "\x00") +} + func bindCurrentSourceProvenance(out *metrics) error { revision, sourceDigest, err := packageartifactrecord.SourceSnapshot(".") if err != nil { diff --git a/internal/tools/coveragemetrics/main_test.go b/internal/tools/coveragemetrics/main_test.go index 47dd54a..f7677ee 100644 --- a/internal/tools/coveragemetrics/main_test.go +++ b/internal/tools/coveragemetrics/main_test.go @@ -2,7 +2,9 @@ package main import ( "errors" + "fmt" "os" + "os/exec" "path/filepath" "strings" "testing" @@ -289,35 +291,362 @@ func TestRunWritesCurrentMetricsWhenCommandRouteInventoryBuilderFails(t *testing } } -func TestRequireCommandRouteInventoryClosureFailsClosed(t *testing.T) { - err := requireCommandRouteInventoryClosure(commandRouteMetrics{ - CommandsWithoutProofRouteCandidate: []string{"route-only"}, - }) - if err == nil || !strings.Contains(err.Error(), "missingCandidates=[route-only]") { - t.Fatalf("requireCommandRouteInventoryClosure() error = %v, want candidate-route failure", err) +func TestEachCommandRouteClosureConjunctHasIndependentFalsifier(t *testing.T) { + cases := []struct { + name string + metrics commandRouteMetrics + want string + }{ + {name: "missing candidate", metrics: commandRouteMetrics{CommandsWithoutProofRouteCandidate: []string{"target"}}, want: "missingCandidates=[target]"}, + {name: "unknown candidate ref", metrics: commandRouteMetrics{UnknownProofRouteCandidateRefs: []string{"proofkit.unknown"}}, want: "unknownCandidateRefs=[proofkit.unknown]"}, + {name: "unknown semantic ref", metrics: commandRouteMetrics{UnknownSemanticCommandRefs: []string{"proofkit.unknown"}}, want: "unknownSemanticRefs=[proofkit.unknown]"}, + {name: "contract only", metrics: commandRouteMetrics{ContractOnlyCommands: []string{"contract-only"}}, want: "contractOnly=[contract-only]"}, + {name: "route only", metrics: commandRouteMetrics{RouteOnlyCommands: []string{"route-only"}}, want: "routeOnly=[route-only]"}, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + err := requireCommandRouteInventoryClosure(test.metrics) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("requireCommandRouteInventoryClosure() error = %v, want %q", err, test.want) + } + }) } - if err := requireCommandRouteInventoryClosure(commandRouteMetrics{}); err != nil { t.Fatalf("requireCommandRouteInventoryClosure() error = %v, want nil", err) } } -func TestRequireNoLinkageDeadZonesFailsClosed(t *testing.T) { - err := requireNoLinkageDeadZones(deadZoneMetrics{ - BindingWithoutRequirementIDs: []string{"REQ-STALE"}, - RequirementWithoutBindingIDs: []string{"REQ-MISSING"}, - ScenarioWithoutCommandIDs: []string{"scenario.missing-command"}, - ScenarioWithoutRequirementIDs: []string{"scenario.missing-requirement"}, - }) - if err == nil || !strings.Contains(err.Error(), "requirement/proof linkage dead zones") { - t.Fatalf("requireNoLinkageDeadZones() error = %v, want dead-zone failure", err) +func TestEachLinkageDeadZoneConjunctHasIndependentFalsifier(t *testing.T) { + cases := []struct { + name string + metrics deadZoneMetrics + want string + }{ + {name: "binding without requirement", metrics: deadZoneMetrics{BindingWithoutRequirementIDs: []string{"REQ-STALE"}}, want: "bindingWithoutRequirement=[REQ-STALE]"}, + {name: "requirement without binding", metrics: deadZoneMetrics{RequirementWithoutBindingIDs: []string{"REQ-MISSING"}}, want: "requirementWithoutBinding=[REQ-MISSING]"}, + {name: "scenario without command", metrics: deadZoneMetrics{ScenarioWithoutCommandIDs: []string{"scenario.missing-command"}}, want: "scenarioWithoutCommand=[scenario.missing-command]"}, + {name: "scenario without requirement", metrics: deadZoneMetrics{ScenarioWithoutRequirementIDs: []string{"scenario.missing-requirement"}}, want: "scenarioWithoutRequirement=[scenario.missing-requirement]"}, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + err := requireNoLinkageDeadZones(test.metrics) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("requireNoLinkageDeadZones() error = %v, want %q", err, test.want) + } + }) } - if err := requireNoLinkageDeadZones(deadZoneMetrics{}); err != nil { t.Fatalf("requireNoLinkageDeadZones() error = %v, want nil", err) } } +func TestBindingWitnessSelectorsRejectMissingSemanticOwner(t *testing.T) { + root := filepath.Join("..", "..", "..") + bindings, err := readJSON[bindingFile](filepath.Join(root, "proofkit", "requirement-bindings.json")) + if err != nil { + t.Fatal(err) + } + if err := validateBindingWitnessSelectorsAtRoot(root, bindings); err != nil { + t.Fatalf("current binding selectors are invalid: %v", err) + } + for index := range bindings.Bindings { + if bindings.Bindings[index].ScenarioID != "proofkit.spec-proof-core.installed-readme-first-input" { + continue + } + bindings.Bindings[index].WitnessSelectors[0].Selector = "TestDeletedSemanticOwner" + bindings.Bindings[index].WitnessSelectors[0].Command = "go test ./internal/tools/packageverify -run '^TestDeletedSemanticOwner$'" + err := validateBindingWitnessSelectorsAtRoot(root, bindings) + if err == nil || !strings.Contains(err.Error(), "selector TestDeletedSemanticOwner is missing") { + t.Fatalf("missing selector error=%v", err) + } + return + } + t.Fatal("installed README binding missing") +} + +func TestBindingWitnessSelectorsRequireExactCriticalInventories(t *testing.T) { + root := filepath.Join("..", "..", "..") + bindings, err := readJSON[bindingFile](filepath.Join(root, "proofkit", "requirement-bindings.json")) + if err != nil { + t.Fatal(err) + } + if err := validateBindingWitnessSelectorsAtRoot(root, bindings); err != nil { + t.Fatalf("current binding selectors are invalid: %v", err) + } + + for _, scenarioID := range []string{ + "proofkit.package-boundary.cli-output-root-witnesses", + "proofkit.package-boundary.generated-command-caller-preservation", + "proofkit.package-boundary.generated-command-field-inventory", + "proofkit.package-boundary.launcher-profile-admission", + "proofkit.package-boundary.merge-critical-runtime-preconditions", + "proofkit.package-boundary.outside-consumer-artifact", + "proofkit.package-boundary.package-public-docs-no-mutable-release-facts", + "proofkit.package-boundary.python-wheel-generated-continuation", + "proofkit.supply-chain-quality.binding-selector-executability", + "proofkit.supply-chain-quality.browser-failure-diagnostics-retention", + "proofkit.supply-chain-quality.ci-required-aggregate-exactness", + "proofkit.supply-chain-quality.cli-abi-golden", + "proofkit.supply-chain-quality.cli-contract-topology", + "proofkit.supply-chain-quality.cli-output-witness-contract", + "proofkit.supply-chain-quality.codeql-permission-separation", + "proofkit.supply-chain-quality.installed-package-json-abi-smoke", + "proofkit.supply-chain-quality.osv-permission-separation", + "proofkit.supply-chain-quality.python-wheel-platform-byte-compatibility", + "proofkit.supply-chain-quality.release-platform-python-wheels", + "proofkit.supply-chain-quality.release-change-record-projection", + "proofkit.supply-chain-quality.scorecard-permission-and-publication-inputs", + "proofkit.supply-chain-quality.workflow-package-gate-oracle", + "proofkit.supply-chain-quality.workflow-source-oracles", + "proofkit.spec-proof-core.adoption-contract-envelope-cli-abi", + "proofkit.spec-proof-core.requirement-browser-one-shot-cleanup", + } { + index := -1 + for candidate := range bindings.Bindings { + if bindings.Bindings[candidate].ScenarioID == scenarioID { + index = candidate + break + } + } + if index < 0 { + t.Fatalf("critical binding %s is missing", scenarioID) + } + original := append([]witnessSelector(nil), bindings.Bindings[index].WitnessSelectors...) + if len(original) < 1 { + t.Fatalf("critical binding %s has no removable selector", scenarioID) + } + + t.Run(scenarioID+"/empty", func(t *testing.T) { + mutated := cloneBindingFile(bindings) + mutated.Bindings[index].WitnessSelectors = nil + err := validateBindingWitnessSelectorsAtRoot(root, mutated) + if err == nil || !strings.Contains(err.Error(), "witness selectors=[]") { + t.Fatalf("empty selector error=%v", err) + } + }) + t.Run(scenarioID+"/missing", func(t *testing.T) { + mutated := cloneBindingFile(bindings) + mutated.Bindings[index].WitnessSelectors = append([]witnessSelector(nil), original[:len(original)-1]...) + err := validateBindingWitnessSelectorsAtRoot(root, mutated) + if err == nil || !strings.Contains(err.Error(), "witness selectors=") { + t.Fatalf("missing selector error=%v", err) + } + }) + t.Run(scenarioID+"/surplus", func(t *testing.T) { + mutated := cloneBindingFile(bindings) + mutated.Bindings[index].WitnessSelectors = append(append([]witnessSelector(nil), original...), original[0]) + err := validateBindingWitnessSelectorsAtRoot(root, mutated) + if err == nil || !strings.Contains(err.Error(), "witness selectors=") { + t.Fatalf("surplus selector error=%v", err) + } + }) + t.Run(scenarioID+"/owner-transfer", func(t *testing.T) { + mutated := cloneBindingFile(bindings) + mutated.Bindings[index].RequirementID = "REQ-PROOFKIT-QUALITY-009" + err := validateBindingWitnessSelectorsAtRoot(root, mutated) + if err == nil || !strings.Contains(err.Error(), "required independent-falsifier binding is missing") { + t.Fatalf("owner-transfer error=%v", err) + } + }) + t.Run(scenarioID+"/scenario-transfer", func(t *testing.T) { + mutated := cloneBindingFile(bindings) + mutated.Bindings[index].ScenarioID += ".transferred" + err := validateBindingWitnessSelectorsAtRoot(root, mutated) + if err == nil || !strings.Contains(err.Error(), "required independent-falsifier binding is missing") { + t.Fatalf("scenario-transfer error=%v", err) + } + }) + t.Run(scenarioID+"/selector-substitution", func(t *testing.T) { + mutated := cloneBindingFile(bindings) + mutated.Bindings[index].WitnessSelectors[0].Selector = "TestSubstitutedSemanticOwner" + err := validateBindingWitnessSelectorsAtRoot(root, mutated) + if err == nil || !strings.Contains(err.Error(), "witness selectors=") { + t.Fatalf("selector-substitution error=%v", err) + } + }) + t.Run(scenarioID+"/command-drift", func(t *testing.T) { + mutated := cloneBindingFile(bindings) + mutated.Bindings[index].WitnessSelectors[0].Command = "go test ./internal/app -run '^TestSubstitutedSemanticOwner$'" + err := validateBindingWitnessSelectorsAtRoot(root, mutated) + if err == nil || !strings.Contains(err.Error(), "selector command=") { + t.Fatalf("command-drift error=%v", err) + } + }) + } + + for _, scenarioID := range []string{ + "proofkit.package-boundary.cli-output-root-witnesses", + "proofkit.package-boundary.generated-command-caller-preservation", + "proofkit.package-boundary.generated-command-field-inventory", + "proofkit.package-boundary.launcher-profile-admission", + "proofkit.package-boundary.merge-critical-runtime-preconditions", + "proofkit.package-boundary.outside-consumer-artifact", + "proofkit.package-boundary.package-public-docs-no-mutable-release-facts", + "proofkit.package-boundary.python-wheel-generated-continuation", + "proofkit.supply-chain-quality.browser-failure-diagnostics-retention", + "proofkit.supply-chain-quality.cli-abi-golden", + "proofkit.supply-chain-quality.cli-contract-topology", + "proofkit.supply-chain-quality.cli-output-witness-contract", + "proofkit.supply-chain-quality.codeql-permission-separation", + "proofkit.supply-chain-quality.installed-package-json-abi-smoke", + "proofkit.supply-chain-quality.osv-permission-separation", + "proofkit.supply-chain-quality.python-wheel-platform-byte-compatibility", + "proofkit.supply-chain-quality.release-platform-python-wheels", + "proofkit.supply-chain-quality.scorecard-permission-and-publication-inputs", + "proofkit.supply-chain-quality.workflow-source-oracles", + "proofkit.spec-proof-core.adoption-contract-envelope-cli-abi", + "proofkit.spec-proof-core.requirement-browser-one-shot-cleanup", + } { + index := -1 + for candidate := range bindings.Bindings { + if bindings.Bindings[candidate].ScenarioID == scenarioID { + index = candidate + break + } + } + if index < 0 { + t.Fatalf("split binding %s is missing", scenarioID) + } + t.Run(scenarioID+"/relocated-witness-path", func(t *testing.T) { + mutated := cloneBindingFile(bindings) + mutated.Bindings[index].WitnessPath = "scripts/workflow_package_gate_oracle_test.go" + err := validateBindingWitnessSelectorsAtRoot(root, mutated) + if err == nil || !strings.Contains(err.Error(), "witness path=") || + !strings.Contains(err.Error(), ", want exact ") { + t.Fatalf("relocated witness path error=%v", err) + } + }) + } +} + +func cloneBindingFile(source bindingFile) bindingFile { + clone := bindingFile{ + Requirements: append([]bindingRequirement(nil), source.Requirements...), + Bindings: append([]bindingScenario(nil), source.Bindings...), + } + for index := range clone.Bindings { + clone.Bindings[index].CommandIDs = append([]string(nil), source.Bindings[index].CommandIDs...) + clone.Bindings[index].WitnessSelectors = append([]witnessSelector(nil), source.Bindings[index].WitnessSelectors...) + } + return clone +} + +func TestBindingWitnessSelectorsAcceptUnnamedGoTestParameter(t *testing.T) { + root := t.TempDir() + witnessPath := filepath.Join("internal", "sample", "sample_test.go") + if err := os.MkdirAll(filepath.Join(root, filepath.Dir(witnessPath)), 0o755); err != nil { + t.Fatalf("mkdir witness directory: %v", err) + } + if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module example.com/sample\n\ngo 1.25.0\n"), 0o644); err != nil { + t.Fatalf("write go.mod fixture: %v", err) + } + source := "package sample\n\nimport \"testing\"\n\nfunc TestRunnable(*testing.T) {}\n" + if err := os.WriteFile(filepath.Join(root, witnessPath), []byte(source), 0o644); err != nil { + t.Fatalf("write witness fixture: %v", err) + } + bindings := bindingFile{ + Bindings: []bindingScenario{bindingSelectorFixture( + "scenario.unnamed-parameter", witnessPath, "TestRunnable", + )}, + } + + command := exec.Command("go", "test", "./internal/sample", "-run", "^TestRunnable$", "-count=1") + command.Dir = root + if output, err := command.CombinedOutput(); err != nil { + t.Fatalf("Go toolchain rejected unnamed test parameter: %v\n%s", err, output) + } + if err := validateBindingWitnessSelectorExecutabilityAtRoot(root, bindings); err != nil { + t.Fatalf("valid unnamed Go test parameter rejected: %v", err) + } +} + +func TestBindingWitnessSelectorsRejectInvalidGoTestSignature(t *testing.T) { + root := t.TempDir() + witnessPath := filepath.Join("internal", "sample", "sample_test.go") + if err := os.MkdirAll(filepath.Join(root, filepath.Dir(witnessPath)), 0o755); err != nil { + t.Fatalf("mkdir witness directory: %v", err) + } + source := "package sample\n\nimport \"testing\"\n\nfunc TestNotRunnable() {}\n" + if err := os.WriteFile(filepath.Join(root, witnessPath), []byte(source), 0o644); err != nil { + t.Fatalf("write witness fixture: %v", err) + } + bindings := bindingFile{ + Bindings: []bindingScenario{{ + ScenarioID: "scenario.invalid-signature", + WitnessPath: witnessPath, + WitnessSelectors: []witnessSelector{{ + Selector: "TestNotRunnable", + Command: "go test ./internal/sample -run '^TestNotRunnable$'", + }}, + }}, + } + + err := validateBindingWitnessSelectorExecutabilityAtRoot(root, bindings) + if err == nil || !strings.Contains(err.Error(), "is not a valid Go test function") { + t.Fatalf("invalid signature error=%v", err) + } +} + +func TestBindingWitnessSelectorsRejectNonTestAndBuildExcludedFiles(t *testing.T) { + t.Run("non-test source", func(t *testing.T) { + root := t.TempDir() + witnessPath := filepath.Join("internal", "sample", "sample.go") + if err := os.MkdirAll(filepath.Join(root, filepath.Dir(witnessPath)), 0o755); err != nil { + t.Fatalf("mkdir witness directory: %v", err) + } + source := "package sample\n\nimport \"testing\"\n\nfunc TestLooksRunnable(t *testing.T) {}\n" + if err := os.WriteFile(filepath.Join(root, witnessPath), []byte(source), 0o644); err != nil { + t.Fatalf("write witness fixture: %v", err) + } + bindings := bindingFile{Bindings: []bindingScenario{bindingSelectorFixture( + "scenario.non-test", witnessPath, "TestLooksRunnable", + )}} + + err := validateBindingWitnessSelectorExecutabilityAtRoot(root, bindings) + if err == nil || !strings.Contains(err.Error(), "must be an active _test.go file") { + t.Fatalf("non-test witness error=%v", err) + } + }) + + t.Run("build-excluded test", func(t *testing.T) { + root := t.TempDir() + packageDir := filepath.Join(root, "internal", "sample") + if err := os.MkdirAll(packageDir, 0o755); err != nil { + t.Fatalf("mkdir witness directory: %v", err) + } + if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module example.invalid/fixture\n\ngo 1.25.0\n"), 0o644); err != nil { + t.Fatalf("write go.mod: %v", err) + } + if err := os.WriteFile(filepath.Join(packageDir, "sample.go"), []byte("package sample\n"), 0o644); err != nil { + t.Fatalf("write package fixture: %v", err) + } + witnessPath := filepath.Join("internal", "sample", "excluded_test.go") + source := "//go:build proofkit_never\n\npackage sample\n\nimport \"testing\"\n\nfunc TestExcluded(t *testing.T) {}\n" + if err := os.WriteFile(filepath.Join(root, witnessPath), []byte(source), 0o644); err != nil { + t.Fatalf("write excluded witness: %v", err) + } + bindings := bindingFile{Bindings: []bindingScenario{bindingSelectorFixture( + "scenario.excluded", witnessPath, "TestExcluded", + )}} + + err := validateBindingWitnessSelectorExecutabilityAtRoot(root, bindings) + if err == nil || !strings.Contains(err.Error(), "is not active for the current Go build") { + t.Fatalf("build-excluded witness error=%v", err) + } + }) +} + +func bindingSelectorFixture(scenarioID, witnessPath, selector string) bindingScenario { + return bindingScenario{ + ScenarioID: scenarioID, + WitnessPath: witnessPath, + WitnessSelectors: []witnessSelector{{ + Selector: selector, + Command: fmt.Sprintf("go test ./%s -run '^%s$'", filepath.ToSlash(filepath.Dir(witnessPath)), selector), + }}, + } +} + func TestBuildMetricsReportsScenarioWithoutRequirement(t *testing.T) { metrics := buildMetrics( []requirementRecord{{RequirementID: "REQ-OK", ClaimLevel: "blocking", Lifecycle: lifecycle{State: "active"}}}, diff --git a/internal/tools/packageartifactrecord/record.go b/internal/tools/packageartifactrecord/record.go index 141b198..df529ab 100644 --- a/internal/tools/packageartifactrecord/record.go +++ b/internal/tools/packageartifactrecord/record.go @@ -560,7 +560,30 @@ func ToolchainDigest() (string, error) { } func gitPaths(root string) ([]string, error) { - command := exec.Command("git", "ls-files", "-z", "--cached", "--others", "--exclude-standard") + paths, err := gitNullPaths(root, "ls-files", "-z", "--cached", "--others", "--exclude-standard") + if err != nil { + return nil, err + } + deleted, err := gitNullPaths(root, "ls-files", "-z", "--deleted") + if err != nil { + return nil, err + } + deletedSet := make(map[string]struct{}, len(deleted)) + for _, path := range deleted { + deletedSet[path] = struct{}{} + } + current := make([]string, 0, len(paths)) + for _, path := range paths { + if _, removed := deletedSet[path]; !removed { + current = append(current, path) + } + } + sort.Strings(current) + return current, nil +} + +func gitNullPaths(root string, args ...string) ([]string, error) { + command := exec.Command("git", args...) command.Dir = root output, err := command.Output() if err != nil { @@ -573,7 +596,6 @@ func gitPaths(root string) ([]string, error) { paths = append(paths, filepath.ToSlash(path)) } } - sort.Strings(paths) return paths, nil } diff --git a/internal/tools/packageartifactrecord/record_test.go b/internal/tools/packageartifactrecord/record_test.go index 6b296a4..cbc2906 100644 --- a/internal/tools/packageartifactrecord/record_test.go +++ b/internal/tools/packageartifactrecord/record_test.go @@ -100,6 +100,33 @@ func TestSourceSnapshotRejectsSymlink(t *testing.T) { } } +func TestSourceSnapshotModelsTrackedDeletionBeforeStaging(t *testing.T) { + root := t.TempDir() + runGit(t, root, "init") + runGit(t, root, "config", "user.email", "proofkit@example.invalid") + runGit(t, root, "config", "user.name", "Proofkit Test") + writeFixture(t, root, "release/change-record.v1.json", "legacy") + runGit(t, root, "add", "release/change-record.v1.json") + runGit(t, root, "commit", "-m", "fixture") + + if err := os.Remove(filepath.Join(root, "release", "change-record.v1.json")); err != nil { + t.Fatal(err) + } + writeFixture(t, root, "release/change-record.v2.json", "current") + revisionBefore, digestBefore, err := SourceSnapshot(root) + if err != nil { + t.Fatalf("SourceSnapshot() before staging error=%v", err) + } + runGit(t, root, "add", "-A") + revisionAfter, digestAfter, err := SourceSnapshot(root) + if err != nil { + t.Fatalf("SourceSnapshot() after staging error=%v", err) + } + if revisionBefore != revisionAfter || digestBefore != digestAfter { + t.Fatalf("unstaged/staged deletion snapshots differ: before=(%s,%s) after=(%s,%s)", revisionBefore, digestBefore, revisionAfter, digestAfter) + } +} + func TestArtifactSnapshotRejectsSymlink(t *testing.T) { root := t.TempDir() writeFixture(t, root, "artifact-target.txt", "target") diff --git a/internal/tools/packagebuild/main.go b/internal/tools/packagebuild/main.go index ad42585..761fdc3 100644 --- a/internal/tools/packagebuild/main.go +++ b/internal/tools/packagebuild/main.go @@ -146,6 +146,11 @@ if [ ! -x "$binary" ]; then exit 1 fi +AGENTIC_PROOFKIT_LAUNCHER_PROFILE=npm_offline +AGENTIC_PROOFKIT_PYTHON_EXECUTABLE= +export AGENTIC_PROOFKIT_LAUNCHER_PROFILE +export AGENTIC_PROOFKIT_PYTHON_EXECUTABLE + exec "$binary" "$@" `, platformCases), nil } diff --git a/internal/tools/packagebuild/main_test.go b/internal/tools/packagebuild/main_test.go index 51f33a1..7e6a6d3 100644 --- a/internal/tools/packagebuild/main_test.go +++ b/internal/tools/packagebuild/main_test.go @@ -51,6 +51,17 @@ func TestWrapperScriptRoutesEveryReleasePlatformTarget(t *testing.T) { } } } + for _, required := range []string{ + "AGENTIC_PROOFKIT_LAUNCHER_PROFILE=npm_offline\n", + "AGENTIC_PROOFKIT_PYTHON_EXECUTABLE=\n", + "export AGENTIC_PROOFKIT_LAUNCHER_PROFILE\n", + "export AGENTIC_PROOFKIT_PYTHON_EXECUTABLE\n", + "exec \"$binary\" \"$@\"\n", + } { + if !strings.Contains(wrapper, required) { + t.Fatalf("wrapperScript() missing launcher-profile boundary %q:\n%s", required, wrapper) + } + } } func TestWrapperPlatformCasesRejectDuplicatePatterns(t *testing.T) { diff --git a/internal/tools/packageverify/main.go b/internal/tools/packageverify/main.go index ae9e57b..3099926 100644 --- a/internal/tools/packageverify/main.go +++ b/internal/tools/packageverify/main.go @@ -15,9 +15,9 @@ import ( "maps" "os" "os/exec" + pathpkg "path" "path/filepath" "regexp" - "runtime" "sort" "strings" @@ -29,6 +29,7 @@ import ( const rootPackageName = "@research-engineering/agentic-proofkit" const rootBinaryName = "agentic-proofkit" +const installedNPMExecCommandPrefix = "npm exec --offline -- agentic-proofkit " const maxTarEntryBytes = 128 << 20 const maxEmbeddedBinaryBytes = 64 << 20 @@ -220,6 +221,9 @@ func runVerifier() error { if err := verifySpecReferenceClosure(rootPackage, toSet(rootPackage.Entries)); err != nil { return err } + if err := verifyPackagePublicReferenceClosure(rootPackage, toSet(rootPackage.Entries)); err != nil { + return err + } return verifyOutsideConsumer(rootPackage) } @@ -312,8 +316,6 @@ func verifyPackRecordContent(record packRecord, content []byte) error { func requiredRootEntries() []string { required := []string{ "package/ADOPTION.md", - "package/AGENTS.md", - "package/CONTRIBUTING.md", "package/LICENSE", "package/NON_CLAIMS.md", "package/README.md", @@ -532,23 +534,21 @@ func forbiddenRootEntry(path string) bool { func allowedRootEntry(path string) bool { allowedExact := map[string]struct{}{ - "package/ADOPTION.md": {}, - "package/AGENTS.md": {}, - "package/CONTRIBUTING.md": {}, - "package/LICENSE": {}, - "package/NON_CLAIMS.md": {}, - "package/README.md": {}, - "package/SECURITY.md": {}, - "package/dist/agentic-proofkit": {}, - "package/docs/proofkit-contract-map.md": {}, - "package/docs/release-process.md": {}, - "package/package.json": {}, - "package/proofkit/cli-contract.v2.json": {}, - "package/proofkit/command-families.v1.json": {}, - "package/proofkit/receipt-producer-policy.json": {}, - "package/proofkit/requirement-bindings.json": {}, - "package/proofkit/witness-plan.json": {}, - "package/docs/specs/proofkit-consumer-infra-retirement/overview.md": {}, + "package/ADOPTION.md": {}, + "package/LICENSE": {}, + "package/NON_CLAIMS.md": {}, + "package/README.md": {}, + "package/SECURITY.md": {}, + "package/dist/agentic-proofkit": {}, + "package/docs/proofkit-contract-map.md": {}, + "package/docs/release-process.md": {}, + "package/package.json": {}, + "package/proofkit/cli-contract.v2.json": {}, + "package/proofkit/command-families.v1.json": {}, + "package/proofkit/receipt-producer-policy.json": {}, + "package/proofkit/requirement-bindings.json": {}, + "package/proofkit/witness-plan.json": {}, + "package/docs/specs/proofkit-consumer-infra-retirement/overview.md": {}, "package/docs/specs/proofkit-consumer-infra-retirement/requirements.v1.json": {}, "package/docs/specs/proofkit-package-boundary/overview.md": {}, "package/docs/specs/proofkit-package-boundary/requirements.v1.json": {}, @@ -607,7 +607,7 @@ func verifyRootManifestBoundary(artifact rootPackageArtifact) error { if manifest.SideEffects { return fmt.Errorf("root package sideEffects must be false") } - expectedDevDependencies := map[string]string{"@axe-core/playwright": "4.12.1", "@playwright/test": "1.61.1", "typescript": "7.0.2"} + expectedDevDependencies := map[string]string{"@playwright/test": "1.62.0", "axe-core": "4.12.1", "typescript": "7.0.2"} if !maps.Equal(manifest.DevDependencies, expectedDevDependencies) { return fmt.Errorf("root package devDependencies must equal the source-only browser proof toolchain") } @@ -634,8 +634,6 @@ func verifyRootManifestBoundary(artifact rootPackageArtifact) error { } expectedFiles := []string{ "ADOPTION.md", - "AGENTS.md", - "CONTRIBUTING.md", "LICENSE", "NON_CLAIMS.md", "README.md", @@ -814,8 +812,6 @@ func readTarTextEntriesFromGzip(gzipReader io.Reader) (map[string]string, error) func packageTextEntry(path string) bool { switch path { case "package/ADOPTION.md", - "package/AGENTS.md", - "package/CONTRIBUTING.md", "package/NON_CLAIMS.md", "package/README.md", "package/SECURITY.md": @@ -851,219 +847,1642 @@ func verifySpecReferenceClosure(artifact rootPackageArtifact, entries map[string return nil } -func decodeRequirementBindings(content []byte) (requirementBindings, error) { - return admission.DecodeTypedJSON[requirementBindings](bytes.NewReader(content), int64(len(content))) -} - -func readAdmittedJSON[T any](path string) (T, error) { - var out T - file, err := os.Open(path) - if err != nil { - return out, err - } - defer file.Close() - value, err := admission.DecodeTypedJSON[T](file, 16<<20) - if err != nil { - return out, fmt.Errorf("decode %s: %w", path, err) - } - return value, nil -} - -func verifyOutsideConsumer(artifact rootPackageArtifact) error { - return verifyExactTarballConsumer(artifact) -} +var markdownInlineDestinationPattern = regexp.MustCompile(`\[[^\]\r\n]*\]\(\s*(?:<([^>\r\n]+)>|([^)\s\r\n]+))(?:\s+(?:"[^"\r\n]*"|'[^'\r\n]*'|\([^)\r\n]*\)))?\s*\)`) +var markdownReferenceDestinationPattern = regexp.MustCompile(`(?m)^[ \t]{0,3}\[[^\]\r\n]+\]:[ \t]*(?:<([^>\r\n]+)>|([^ \t\r\n]+))`) +var markdownCodeSpanPattern = regexp.MustCompile("`([^`]+)`") +var readmePublicNavigationPattern = regexp.MustCompile( + `(?s)The full machine-readable command inventory remains\s+` + + "`([^`\r\n]+)`" + + `; the human route map is\s+` + + "`([^`\r\n]+)`" + + `\.`, +) -func verifyExactTarballConsumer(artifact rootPackageArtifact) error { - consumer, err := os.MkdirTemp("", "proofkit-consumer-*") +func verifyPackagePublicReferenceClosure(artifact rootPackageArtifact, entries map[string]struct{}) error { + textEntries, err := readTarTextEntriesFromBytes(artifact.Content) if err != nil { return err } - defer os.RemoveAll(consumer) - tarballPath := filepath.Join(consumer, artifact.Record.Filename) - if err := os.WriteFile(tarballPath, artifact.Content, 0o644); err != nil { - return fmt.Errorf("write snapshot package tarball: %w", err) + for entry, content := range textEntries { + if strings.HasSuffix(entry, ".md") { + if err := verifyMarkdownDestinations(entry, content, entries); err != nil { + return err + } + } } - packageJSON := fmt.Sprintf( - `{"private":true,"type":"module","dependencies":{"%s":"file:%s"}}`+"\n", - rootPackageName, - tarballPath, - ) - if err := os.WriteFile(filepath.Join(consumer, "package.json"), []byte(packageJSON), 0o644); err != nil { - return err + knownMachineEntries := map[string]struct{}{ + "package/proofkit/cli-contract.v2.json": {}, + "package/proofkit/command-families.v1.json": {}, + "package/proofkit/receipt-producer-policy.json": {}, + "package/proofkit/requirement-bindings.json": {}, + "package/proofkit/witness-plan.json": {}, } - if output, err := run(consumer, "npm", "install", "--ignore-scripts", "--no-audit", "--no-fund"); err != nil { - return fmt.Errorf("outside consumer install failed: %w\n%s", err, output) + for entry, content := range textEntries { + if !strings.HasSuffix(entry, ".json") { + continue + } + if strings.HasPrefix(entry, "package/docs/specs/") && strings.HasSuffix(entry, "/requirements.v1.json") { + if err := verifyRequirementSourceReferences(entry, content, entries); err != nil { + return err + } + continue + } + if _, known := knownMachineEntries[entry]; !known { + return fmt.Errorf("package contains unclassified machine projection %s", entry) + } } - binPath := filepath.Join(consumer, "node_modules", ".bin", "agentic-proofkit") - if runtime.GOOS == "windows" { - binPath += ".cmd" + if readme, ok := textEntries["package/README.md"]; ok { + if err := verifyREADMEInstallPolicy(readme); err != nil { + return err + } + if err := verifyREADMEPublicNavigation(readme, entries); err != nil { + return err + } + if err := verifyREADMEOwnerTable(readme, entries); err != nil { + return err + } } - output, err := run(consumer, binPath, "--help") - if err != nil { - return fmt.Errorf("outside consumer binary smoke failed: %w\n%s", err, output) + if err := verifyRequirementBindingReferences(textEntries["package/proofkit/requirement-bindings.json"], entries); err != nil { + return err } - if !bytes.Contains(output, []byte("CLI/JSON is the public cross-language contract")) { - return fmt.Errorf("outside consumer binary smoke did not expose CLI contract") + if err := verifyWitnessPlanReferences(textEntries["package/proofkit/witness-plan.json"]); err != nil { + return err } - if err := verifyInstalledJSONABI(consumer, binPath); err != nil { + if err := verifyCommandFamilyReferenceInventory(textEntries["package/proofkit/command-families.v1.json"]); err != nil { return err } - if err := verifyOutsideConsumerImports(consumer); err != nil { + if err := verifyReceiptPolicyReferences(textEntries["package/proofkit/receipt-producer-policy.json"], entries); err != nil { return err } - return nil + return verifyCLIContractSourceClassifications(textEntries["package/proofkit/cli-contract.v2.json"], entries) } -func run(dir string, name string, args ...string) ([]byte, error) { - command := exec.Command(name, args...) - command.Dir = dir - return command.CombinedOutput() +const readmePreOneExactPinPolicy = "Pre-1.0 releases may contain owner-declared breaking changes, so npm consumers\nmust retain the exact saved version instead of replacing it with a version\nrange." + +func verifyREADMEInstallPolicy(content string) error { + if strings.Count(content, readmePreOneExactPinPolicy) != 1 { + return fmt.Errorf("package README must state the exact pre-1.0 npm pin policy once") + } + return nil } -func verifyInstalledJSONABI(consumer string, binPath string) error { - if err := os.WriteFile(filepath.Join(consumer, "unlisted-poison.md"), []byte("bad trailing \n"), 0o644); err != nil { - return fmt.Errorf("write unlisted poison file: %w", err) +func verifyMarkdownDestinations(entry, content string, entries map[string]struct{}) error { + matches := append( + markdownInlineDestinationPattern.FindAllStringSubmatch(content, -1), + markdownReferenceDestinationPattern.FindAllStringSubmatch(content, -1)..., + ) + for _, match := range matches { + destination := firstNonEmptyCapture(match[1:]) + if destination == "" || strings.HasPrefix(destination, "#") || strings.Contains(destination, "://") || strings.HasPrefix(destination, "mailto:") { + continue + } + destination = strings.SplitN(destination, "#", 2)[0] + destination = strings.SplitN(destination, "?", 2)[0] + resolved := pathpkg.Clean(pathpkg.Join(pathpkg.Dir(entry), destination)) + if !safePackageEntryReference(resolved) { + return fmt.Errorf("%s contains unsafe Markdown destination %s", entry, destination) + } + if _, ok := entries[resolved]; !ok { + return fmt.Errorf("%s contains dangling package-public Markdown destination %s", entry, destination) + } } - success, err := runWithInput(consumer, binPath, packageSmokeSuccessInput(), "text-policy", "--input", "-") + return nil +} + +func verifyRequirementSourceReferences(entry, content string, entries map[string]struct{}) error { + value, err := decodePackageJSONObject(content, entry) if err != nil { - return fmt.Errorf("outside consumer JSON success smoke failed to run: %w", err) + return err } - if err := verifyTextPolicySmokeReport(success, "proofkit.package-smoke.success", "passed", 0, textPolicySmokeSummary{ - CheckedTextFileCount: 1, - FailureCount: 0, - InputFileCount: 1, + if err := verifyClosedReferenceInventory(entry, value, map[string]string{ + "/specPackagePath": "package_public_directory", + "/overviewPath": "package_public", + "/requirementsPath": "package_public", + "/requirements/*/proofBindingRefs": "package_public", + "/requirements/*/nonClaimRefs": "non_claim_identifier", + "/requirements/*/lifecycle/evidenceRefs": "package_public_or_evidence_identifier", }); err != nil { - return fmt.Errorf("outside consumer JSON success smoke failed: %w", err) + return err } - compactSuccess, err := runWithInput(consumer, binPath, packageSmokeSuccessInput(), "--json-layout", "compact", "text-policy", "--input", "-") - if err != nil { - return fmt.Errorf("outside consumer compact JSON success smoke failed to run: %w", err) + specPackagePath := stringField(value, "specPackagePath") + if err := requireShippedRootPrefix(entry+" specPackagePath", specPackagePath, entries); err != nil { + return err } - if err := verifyTextPolicySmokeReport(compactSuccess, "proofkit.package-smoke.success", "passed", 0, textPolicySmokeSummary{CheckedTextFileCount: 1, FailureCount: 0, InputFileCount: 1}); err != nil { - return fmt.Errorf("outside consumer compact JSON success smoke failed: %w", err) + for _, field := range []string{"overviewPath", "requirementsPath"} { + if err := requireShippedRootReference(entry+" "+field, stringField(value, field), entries); err != nil { + return err + } } - failed, err := runWithInput(consumer, binPath, packageSmokeFailureInput(), "text-policy", "--input", "-") - if err != nil { - return fmt.Errorf("outside consumer JSON failure smoke failed to run: %w", err) + requirements, ok := value["requirements"].([]any) + if !ok { + return fmt.Errorf("package %s requirements must be an array", entry) } - if err := verifyTextPolicySmokeReport(failed, "proofkit.package-smoke.failure", "failed", 1, textPolicySmokeSummary{ - CheckedTextFileCount: 1, - FailureCount: 1, - InputFileCount: 1, - }); err != nil { - return fmt.Errorf("outside consumer JSON failure smoke failed: %w", err) + for _, raw := range requirements { + requirement, ok := raw.(map[string]any) + if !ok { + return fmt.Errorf("package %s requirement must be an object", entry) + } + for _, reference := range stringArrayField(requirement, "proofBindingRefs") { + if err := requireShippedRootReference(entry+" proofBindingRef", reference, entries); err != nil { + return err + } + } + for _, reference := range stringArrayField(requirement, "nonClaimRefs") { + if !strings.HasPrefix(reference, "NC-") { + return fmt.Errorf("package %s nonClaimRef must be an NC-* identifier", entry) + } + } + lifecycle, _ := requirement["lifecycle"].(map[string]any) + for _, reference := range stringArrayField(lifecycle, "evidenceRefs") { + if looksLikeRepositoryPath(reference) { + if err := requireShippedRootReference(entry+" lifecycle evidenceRef", reference, entries); err != nil { + return err + } + } else if reference == "" { + return fmt.Errorf("package %s lifecycle evidenceRef must be non-empty", entry) + } + } } - compactFailed, err := runWithInput(consumer, binPath, packageSmokeFailureInput(), "--json-layout", "compact", "text-policy", "--input", "-") - if err != nil { - return fmt.Errorf("outside consumer compact JSON failure smoke failed to run: %w", err) + return nil +} + +func firstNonEmptyCapture(captures []string) string { + for _, capture := range captures { + if capture != "" { + return strings.TrimSpace(capture) + } } - if err := verifyTextPolicySmokeReport(compactFailed, "proofkit.package-smoke.failure", "failed", 1, textPolicySmokeSummary{CheckedTextFileCount: 1, FailureCount: 1, InputFileCount: 1}); err != nil { - return fmt.Errorf("outside consumer compact JSON failure smoke failed: %w", err) + return "" +} + +func verifyREADMEPublicNavigation(content string, entries map[string]struct{}) error { + match := readmePublicNavigationPattern.FindStringSubmatch(content) + if len(match) != 3 { + return fmt.Errorf("package README public command navigation is missing") } - if err := verifyJSONAdapterSourceSmoke(consumer, binPath); err != nil { - return err + for _, reference := range match[1:] { + if err := requireShippedRootReference("package README public command navigation", reference, entries); err != nil { + return err + } } return nil } -func verifyJSONAdapterSourceSmoke(consumer string, binPath string) error { - command := exec.Command(binPath, "json-report-cli-adapter-source", "--language", "typescript") - command.Dir = consumer - var stdout bytes.Buffer - var stderr bytes.Buffer - command.Stdout = &stdout - command.Stderr = &stderr - if err := command.Run(); err != nil { - return fmt.Errorf("outside consumer JSON adapter source smoke failed to run: %w; stderr=%s", err, stderr.String()) +func verifyREADMEOwnerTable(content string, entries map[string]struct{}) error { + inTable := false + for _, line := range strings.Split(content, "\n") { + if line == "| Need | Owner |" { + inTable = true + continue + } + if !inTable { + continue + } + if line == "" { + break + } + if strings.HasPrefix(line, "|---") { + continue + } + cells := strings.Split(line, "|") + if len(cells) != 4 { + return fmt.Errorf("package README owner table must contain exactly two cells") + } + for _, match := range markdownCodeSpanPattern.FindAllStringSubmatch(cells[2], -1) { + reference := strings.TrimSpace(match[1]) + if !looksLikeRepositoryPath(reference) { + continue + } + if err := requireShippedRootReference("package README owner table", reference, entries); err != nil { + return err + } + } } - return verifyJSONAdapterSourceSmokeReport(installedCommandResult{ - ExitCode: 0, - Stdout: stdout.Bytes(), - Stderr: stderr.Bytes(), - }, jsonreportcliadaptersource.TypeScriptSource()) + if !inTable { + return fmt.Errorf("package README owner table is missing") + } + return nil } -func verifyJSONAdapterSourceSmokeReport(result installedCommandResult, expectedSource string) error { - if result.ExitCode != 0 { - return fmt.Errorf("exit code %d, want 0; stdout=%s stderr=%s", result.ExitCode, result.Stdout, result.Stderr) +func verifyRequirementBindingReferences(content string, entries map[string]struct{}) error { + value, err := decodePackageJSONObject(content, "requirement bindings") + if err != nil { + return err } - if len(result.Stderr) != 0 { - return fmt.Errorf("stderr must be empty for json adapter source smoke, got %q", string(result.Stderr)) + if err := verifyClosedReferenceInventory("requirement bindings", value, map[string]string{ + "/requirements/*/specPath": "package_public", + "/bindings/*/witnessPath": "source_checkout", + "/bindings/*/witnessSelectors": "source_checkout_selector_set", + "/bindings/*/witnessSelectors/*/selector": "source_checkout_test_selector", + }); err != nil { + return err } - report, err := admission.DecodeTypedJSON[jsonAdapterSourceSmokeReport](bytes.NewReader(result.Stdout), 8<<20) - if err != nil { - return fmt.Errorf("json adapter source smoke stdout must be one JSON report: %w; stdout=%s", err, result.Stdout) + requirements, ok := value["requirements"].([]any) + if !ok { + return fmt.Errorf("package requirement bindings requirements must be an array") } - if report.SchemaVersion != 1 { - return fmt.Errorf("schemaVersion=%d, want 1", report.SchemaVersion) + for _, raw := range requirements { + record, ok := raw.(map[string]any) + if !ok { + return fmt.Errorf("package requirement binding requirement must be an object") + } + specPath, ok := record["specPath"].(string) + if !ok || specPath == "" { + return fmt.Errorf("package requirement binding specPath must be a string") + } + if err := requireShippedRootReference("package requirement binding specPath", specPath, entries); err != nil { + return err + } } - if report.ArtifactKind != "proofkit.json-report-cli-adapter-source" { - return fmt.Errorf("artifactKind=%s, want proofkit.json-report-cli-adapter-source", report.ArtifactKind) + bindings, _ := value["bindings"].([]any) + for _, raw := range bindings { + record, ok := raw.(map[string]any) + if !ok { + return fmt.Errorf("package requirement binding witness must be an object") + } + witnessPath, ok := record["witnessPath"].(string) + if !ok || witnessPath == "" { + return fmt.Errorf("package requirement binding witnessPath must be a string") + } + if err := requireSourceCheckoutReference("package requirement binding witnessPath", witnessPath); err != nil { + return err + } + selectors, _ := record["witnessSelectors"].([]any) + for _, rawSelector := range selectors { + selector, ok := rawSelector.(map[string]any) + if !ok { + return fmt.Errorf("package requirement binding witnessSelector must be an object") + } + if value, _ := selector["selector"].(string); value == "" { + return fmt.Errorf("package requirement binding witness selector must be non-empty") + } + if value, _ := selector["command"].(string); value == "" { + return fmt.Errorf("package requirement binding witness command must be non-empty") + } + } } - if report.Language != "typescript" { - return fmt.Errorf("language=%s, want typescript", report.Language) + return nil +} + +func verifyWitnessPlanReferences(content string) error { + value, err := decodePackageJSONObject(content, "witness plan") + if err != nil { + return err } - if report.Format != "json" { - return fmt.Errorf("format=%s, want json", report.Format) + if err := verifyClosedReferenceInventory("witness plan", value, map[string]string{ + "/commands/*/cwd": "source_checkout", + "/commands/*/expectedArtifacts/*/path": "generated_output", + "/policies/*/inputSelectors": "source_checkout_or_generated_input", + "/policies/*/outputSelectors": "generated_output", + "/policies/*/cacheAdmissionRefs": "policy_identifier", + }); err != nil { + return err } - if report.SourceFileName != "proofkit-json-report-cli-adapter.ts" { - return fmt.Errorf("sourceFileName=%s, want proofkit-json-report-cli-adapter.ts", report.SourceFileName) + commands, ok := value["commands"].([]any) + if !ok { + return fmt.Errorf("package witness plan commands must be an array") } - if report.GeneratorID != "proofkit.json-report-cli-adapter-source.typescript.v1" { - return fmt.Errorf("generatorId=%s, want proofkit.json-report-cli-adapter-source.typescript.v1", report.GeneratorID) + for _, raw := range commands { + command, ok := raw.(map[string]any) + if !ok { + return fmt.Errorf("package witness plan command must be an object") + } + cwd, _ := command["cwd"].(string) + if cwd != "." { + return fmt.Errorf("package witness plan command cwd must be source checkout root") + } + artifacts, _ := command["expectedArtifacts"].([]any) + for _, rawArtifact := range artifacts { + artifact, ok := rawArtifact.(map[string]any) + if !ok { + return fmt.Errorf("package witness plan expectedArtifact must be an object") + } + if err := requireGeneratedOutputReference("package witness plan expectedArtifact", stringField(artifact, "path")); err != nil { + return err + } + } } - if report.Source != expectedSource { - return fmt.Errorf("json adapter source does not match current owner source") + policies, ok := value["policies"].([]any) + if !ok { + return fmt.Errorf("package witness plan policies must be an array") } - if report.SourceSha256 != digest.SHA256TextRef(report.Source) { - return fmt.Errorf("json adapter source hash mismatch") + for _, raw := range policies { + policy, ok := raw.(map[string]any) + if !ok { + return fmt.Errorf("package witness plan policy must be an object") + } + for _, reference := range stringArrayField(policy, "inputSelectors") { + if strings.HasPrefix(reference, "artifacts/") { + if err := requireGeneratedOutputReference("package witness plan generated inputSelector", reference); err != nil { + return err + } + continue + } + if err := requireSourceCheckoutReference("package witness plan source inputSelector", reference); err != nil { + return err + } + } + for _, reference := range stringArrayField(policy, "outputSelectors") { + if err := requireGeneratedOutputReference("package witness plan outputSelector", reference); err != nil { + return err + } + } } - if report.Summary.ExportedSymbolCount < 20 { - return fmt.Errorf("summary.exportedSymbolCount=%d, want at least 20", report.Summary.ExportedSymbolCount) + return nil +} + +func verifyCommandFamilyReferenceInventory(content string) error { + value, err := decodePackageJSONObject(content, "command family catalog") + if err != nil { + return err } - if report.Summary.LineCount < 500 { - return fmt.Errorf("summary.lineCount=%d, want at least 500", report.Summary.LineCount) + if err := verifyClosedReferenceInventory("command family catalog", value, map[string]string{}); err != nil { + return err } - for _, token := range []string{ - "function readProofkitBoundedTextFile", - "export function runProofkitNoInputJsonCommand", - "options.inputMode === \"none\"", - "stable JSON value must not contain unsafe integer numbers", - "openSync(filePath, \"r\")", - } { - if !strings.Contains(report.Source, token) { - return fmt.Errorf("json adapter source missing required token %q", token) - } + families, ok := value["families"].([]any) + if !ok { + return fmt.Errorf("package command family catalog families must be an array") } - for _, token := range []string{ - "readFileSync(filePath, \"utf8\")", - "JSON.parse(text)", - } { - if strings.Contains(report.Source, token) { - return fmt.Errorf("json adapter source contains forbidden stale token %q", token) + for _, raw := range families { + family, ok := raw.(map[string]any) + if !ok { + return fmt.Errorf("package command family must be an object") + } + for _, command := range stringArrayField(family, "commands") { + if looksLikeRepositoryPath(command) { + return fmt.Errorf("package command family command must be a command identifier, got route %s", command) + } } } return nil } -func runWithInput(dir string, name string, input []byte, args ...string) (installedCommandResult, error) { - command := exec.Command(name, args...) - command.Dir = dir - command.Stdin = bytes.NewReader(input) - var stdout bytes.Buffer - var stderr bytes.Buffer - command.Stdout = &stdout - command.Stderr = &stderr - err := command.Run() - exitCode := 0 +func verifyReceiptPolicyReferences(content string, entries map[string]struct{}) error { + value, err := decodePackageJSONObject(content, "receipt producer policy") if err != nil { - var exitErr *exec.ExitError - if !errors.As(err, &exitErr) { - return installedCommandResult{}, err + return err + } + if err := verifyClosedReferenceInventory("receipt producer policy", value, map[string]string{ + "/producers/*/evidenceRefs": "package_public_or_source_checkout", + }); err != nil { + return err + } + producers, ok := value["producers"].([]any) + if !ok { + return fmt.Errorf("package receipt producer policy producers must be an array") + } + for _, raw := range producers { + record, ok := raw.(map[string]any) + if !ok { + return fmt.Errorf("package receipt producer must be an object") + } + refs, ok := record["evidenceRefs"].([]any) + if !ok { + return fmt.Errorf("package receipt producer evidenceRefs must be an array") + } + for _, rawRef := range refs { + reference, ok := rawRef.(string) + if !ok { + return fmt.Errorf("package receipt producer evidenceRef must be a string") + } + if record["producerId"] == "github.actions.package" && reference == ".github/workflows/ci.yml" { + if err := requireSourceCheckoutReference("package receipt producer source evidenceRef", reference); err != nil { + return err + } + continue + } + if err := requireShippedRootReference("package receipt producer evidenceRef", reference, entries); err != nil { + return err + } } - exitCode = exitErr.ExitCode() } - return installedCommandResult{ExitCode: exitCode, Stdout: stdout.Bytes(), Stderr: stderr.Bytes()}, nil + return nil +} + +func verifyCLIContractSourceClassifications(content string, entries map[string]struct{}) error { + value, err := decodePackageJSONObject(content, "CLI contract") + if err != nil { + return err + } + if err := verifyClosedReferenceInventory("CLI contract", value, map[string]string{ + "/processContract/helpGrammar/helpCatalogFormsSource": "package_public", + "/commands/*/inputContract/fields/availableInputs/item/ref": "runtime_field", + "/commands/*/inputContract/fields/knownChangedPaths": "runtime_field", + "/commands/*/inputContract/fields/observedReports/item/ref": "runtime_field", + "/commands/*/inputContract/nativeAdmissionWitnessSelector": "source_checkout_selector", + "/commands/*/inputContract/nativeAdmissionWitnessSelector/path": "source_checkout", + "/commands/*/inputContract/nativeSource/path": "source_checkout", + "/commands/*/inputContract/nativeSources/*/path": "source_checkout", + "/commands/*/inputContract/ownerRequirementRefs": "requirement_identifier", + "/commands/*/inputContract/rootDefinitionRef": "schema_identifier", + "/commands/*/outputContract/nativeOutputWitnessSelector": "source_checkout_selector", + "/commands/*/outputContract/nativeOutputWitnessSelector/path": "source_checkout", + "/commands/*/outputContract/nativeSource/path": "source_checkout", + "/commands/*/outputContract/nativeSources/*/path": "source_checkout", + "/commands/*/outputContract/ownerRequirementRefs": "requirement_identifier", + "/commands/*/outputContract/qualityFindingFields/evidenceRefs": "runtime_field", + "/commands/*/outputContract/records/dependencyRef": "runtime_field", + "/commands/*/outputContract/rootDefinitionRef": "schema_identifier", + "/contractDefinitions/*/definitionRefs": "schema_identifier", + }); err != nil { + return err + } + if processContract, _ := value["processContract"].(map[string]any); processContract != nil { + helpGrammar, _ := processContract["helpGrammar"].(map[string]any) + if helpGrammar == nil { + return fmt.Errorf("package CLI contract processContract.helpGrammar must be an object") + } + if err := requireShippedRootReference( + "package CLI contract helpCatalogFormsSource", + stringField(helpGrammar, "helpCatalogFormsSource"), + entries, + ); err != nil { + return err + } + } + commands, ok := value["commands"].([]any) + if !ok { + return fmt.Errorf("package CLI contract commands must be an array") + } + for _, rawCommand := range commands { + command, ok := rawCommand.(map[string]any) + if !ok { + return fmt.Errorf("package CLI contract command must be an object") + } + for _, contractKey := range []string{"inputContract", "outputContract"} { + contract, _ := command[contractKey].(map[string]any) + if contract == nil { + continue + } + _, hasSource := contract["nativeSource"] + _, hasSources := contract["nativeSources"] + if hasSource == hasSources { + return fmt.Errorf("package CLI contract %s must declare exactly one of nativeSource or nativeSources", contractKey) + } + for _, sourceKey := range []string{"nativeSource", "nativeAdmissionWitnessSelector", "nativeOutputWitnessSelector"} { + source, _ := contract[sourceKey].(map[string]any) + if source == nil { + continue + } + reference, _ := source["path"].(string) + if reference == "" { + return fmt.Errorf("package CLI contract %s.%s path must be non-empty", contractKey, sourceKey) + } + if source["evidenceClass"] != "source_checkout" { + return fmt.Errorf("package CLI contract source-only %s.%s must declare evidenceClass=source_checkout", contractKey, sourceKey) + } + if err := requireSourceCheckoutReference("package CLI contract source path", reference); err != nil { + return err + } + } + if rawSources, ok := contract["nativeSources"].([]any); ok { + for index, rawSource := range rawSources { + source, ok := rawSource.(map[string]any) + if !ok { + return fmt.Errorf("package CLI contract %s.nativeSources[%d] must be an object", contractKey, index) + } + reference, _ := source["path"].(string) + if reference == "" { + return fmt.Errorf("package CLI contract %s.nativeSources[%d] path must be non-empty", contractKey, index) + } + if source["evidenceClass"] != "source_checkout" { + return fmt.Errorf("package CLI contract source-only %s.nativeSources[%d] must declare evidenceClass=source_checkout", contractKey, index) + } + if err := requireSourceCheckoutReference("package CLI contract source path", reference); err != nil { + return err + } + } + } + } + } + return nil +} + +func verifyClosedReferenceInventory(label string, value any, classifications map[string]string) error { + var walk func(any, []string) error + walk = func(current any, route []string) error { + switch typed := current.(type) { + case map[string]any: + keys := make([]string, 0, len(typed)) + for key := range typed { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + fieldRoute := append(append([]string{}, route...), key) + if referenceBearingField(key) { + pointer := "/" + strings.Join(fieldRoute, "/") + if _, admitted := classifications[pointer]; !admitted { + return fmt.Errorf("package %s contains unclassified reference-bearing field %s", label, pointer) + } + } + if err := walk(typed[key], fieldRoute); err != nil { + return err + } + } + case []any: + for _, item := range typed { + if err := walk(item, append(append([]string{}, route...), "*")); err != nil { + return err + } + } + } + return nil + } + return walk(value, nil) +} + +func referenceBearingField(key string) bool { + if key == "helpCatalogFormsSource" { + return true + } + lower := strings.ToLower(key) + if lower == "cwd" { + return true + } + for _, suffix := range []string{"path", "paths", "ref", "refs", "selector", "selectors"} { + if strings.HasSuffix(lower, suffix) { + return true + } + } + return false +} + +func requireSourceCheckoutReference(context, reference string) error { + static, err := safeRepositoryReference(reference) + if err != nil { + return fmt.Errorf("%s contains unsafe source-checkout route %s: %w", context, reference, err) + } + root, err := findRepositoryRoot() + if err != nil { + return fmt.Errorf("%s: %w", context, err) + } + current := root + if static == "." { + return nil + } + for _, component := range strings.Split(static, "/") { + current = filepath.Join(current, component) + info, err := os.Lstat(current) + if err != nil { + return fmt.Errorf("%s contains dangling source-checkout route %s", context, reference) + } + if info.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf("%s source-checkout route traverses symlink %s", context, reference) + } + } + return nil +} + +func requireGeneratedOutputReference(context, reference string) error { + static, err := safeRepositoryReference(reference) + if err != nil { + return fmt.Errorf("%s contains unsafe generated-output route %s: %w", context, reference, err) + } + if static != "artifacts" && !strings.HasPrefix(static, "artifacts/") && + static != "dist" && !strings.HasPrefix(static, "dist/") { + return fmt.Errorf("%s must classify an artifacts/ or dist/ generated-output route, got %s", context, reference) + } + return nil +} + +func safeRepositoryReference(reference string) (string, error) { + if reference == "" || strings.HasPrefix(reference, "/") || strings.Contains(reference, `\`) { + return "", fmt.Errorf("route must be non-empty repository-relative POSIX syntax") + } + clean := pathpkg.Clean(reference) + if clean == ".." || strings.HasPrefix(clean, "../") { + return "", fmt.Errorf("route escapes repository root") + } + static := clean + if index := strings.IndexAny(static, "*?["); index >= 0 { + static = strings.TrimSuffix(static[:index], "/") + if static == "" { + return "", fmt.Errorf("glob must have an existing static prefix") + } + } + return static, nil +} + +func findRepositoryRoot() (string, error) { + current, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("resolve working directory: %w", err) + } + for { + if info, err := os.Stat(filepath.Join(current, "go.mod")); err == nil && info.Mode().IsRegular() { + return current, nil + } + parent := filepath.Dir(current) + if parent == current { + return "", fmt.Errorf("repository root containing go.mod not found") + } + current = parent + } +} + +func stringField(record map[string]any, key string) string { + value, _ := record[key].(string) + return value +} + +func stringArrayField(record map[string]any, key string) []string { + values, _ := record[key].([]any) + result := make([]string, 0, len(values)) + for _, raw := range values { + value, ok := raw.(string) + if !ok { + return []string{""} + } + result = append(result, value) + } + return result +} + +func decodePackageJSONObject(content, label string) (map[string]any, error) { + if content == "" { + return nil, fmt.Errorf("package %s is missing", label) + } + value, err := admission.DecodeJSON(strings.NewReader(content), int64(len(content))) + if err != nil { + return nil, fmt.Errorf("decode package %s: %w", label, err) + } + record, ok := value.(map[string]any) + if !ok { + return nil, fmt.Errorf("package %s must be an object", label) + } + return record, nil +} + +func requireShippedRootReference(context, reference string, entries map[string]struct{}) error { + if !looksLikeRepositoryPath(reference) || strings.HasPrefix(reference, "/") { + return fmt.Errorf("%s contains unsafe route %s", context, reference) + } + target := pathpkg.Clean("package/" + reference) + if !safePackageEntryReference(target) { + return fmt.Errorf("%s contains unsafe route %s", context, reference) + } + if _, ok := entries[target]; !ok { + return fmt.Errorf("%s contains dangling package-public route %s", context, reference) + } + return nil +} + +func requireShippedRootPrefix(context, reference string, entries map[string]struct{}) error { + if !looksLikeRepositoryPath(reference) || strings.HasPrefix(reference, "/") { + return fmt.Errorf("%s contains unsafe route %s", context, reference) + } + target := pathpkg.Clean("package/" + reference) + if !safePackageEntryReference(target) { + return fmt.Errorf("%s contains unsafe route %s", context, reference) + } + prefix := strings.TrimSuffix(target, "/") + "/" + for entry := range entries { + if strings.HasPrefix(entry, prefix) { + return nil + } + } + return fmt.Errorf("%s contains dangling package-public route %s", context, reference) +} + +func safePackageEntryReference(reference string) bool { + return reference != "package" && + strings.HasPrefix(reference, "package/") && + !strings.Contains(reference, `\`) && + !strings.Contains(reference, "../") +} + +func looksLikeRepositoryPath(reference string) bool { + return strings.Contains(reference, "/") || strings.Contains(pathpkg.Base(reference), ".") +} + +func decodeRequirementBindings(content []byte) (requirementBindings, error) { + return admission.DecodeTypedJSON[requirementBindings](bytes.NewReader(content), int64(len(content))) +} + +func readAdmittedJSON[T any](path string) (T, error) { + var out T + file, err := os.Open(path) + if err != nil { + return out, err + } + defer file.Close() + value, err := admission.DecodeTypedJSON[T](file, 16<<20) + if err != nil { + return out, fmt.Errorf("decode %s: %w", path, err) + } + return value, nil +} + +func verifyOutsideConsumer(artifact rootPackageArtifact) error { + return verifyExactTarballConsumer(artifact) +} + +func verifyExactTarballConsumer(artifact rootPackageArtifact) error { + return withExactTarballConsumer(artifact, func(consumer string) error { + if err := verifyInstalledOnboardingTrace(consumer, runInstalledWithInput); err != nil { + return err + } + if err := verifyInstalledJSONABI(consumer); err != nil { + return err + } + return verifyOutsideConsumerImports(consumer) + }) +} + +func withExactTarballConsumer(artifact rootPackageArtifact, verify func(string) error) error { + consumer, err := os.MkdirTemp("", "proofkit-consumer-*") + if err != nil { + return err + } + defer os.RemoveAll(consumer) + tarballPath := filepath.Join(consumer, artifact.Record.Filename) + if err := os.WriteFile(tarballPath, artifact.Content, 0o644); err != nil { + return fmt.Errorf("write snapshot package tarball: %w", err) + } + packageJSON, err := json.Marshal(map[string]any{ + "private": true, + "type": "module", + "dependencies": map[string]string{ + rootPackageName: "file:" + tarballPath, + }, + }) + if err != nil { + return fmt.Errorf("encode outside consumer package manifest: %w", err) + } + if err := os.WriteFile(filepath.Join(consumer, "package.json"), append(packageJSON, '\n'), 0o644); err != nil { + return err + } + if output, err := run(consumer, "npm", "install", "--ignore-scripts", "--no-audit", "--no-fund"); err != nil { + return fmt.Errorf("outside consumer install failed: %w\n%s", err, output) + } + return verify(consumer) +} + +func run(dir string, name string, args ...string) ([]byte, error) { + command := exec.Command(name, args...) + command.Dir = dir + return command.CombinedOutput() +} + +type installedCommandOperation func(string, []byte, ...string) (installedCommandResult, error) + +type installedHelpRoute struct { + ID string + Argv []string +} + +func verifyInstalledOnboardingTrace(consumer string, execute installedCommandOperation) error { + rootHelp, err := execute(consumer, nil, "help") + if err != nil { + return fmt.Errorf("outside consumer root help failed to run: %w", err) + } + if err := requireInstalledTextSuccess(rootHelp, "root help"); err != nil { + return err + } + familyArgv, err := installedRootHelpFamilyArgv(rootHelp.Stdout) + if err != nil { + return err + } + if !bytes.Contains(rootHelp.Stdout, []byte("CLI/JSON is the public cross-language contract")) { + return fmt.Errorf("outside consumer root help did not expose the family discovery and CLI contract routes") + } + families, err := execute(consumer, nil, familyArgv...) + if err != nil { + return fmt.Errorf("outside consumer family help failed to run: %w", err) + } + if err := requireInstalledTextSuccess(families, "family help"); err != nil { + return err + } + familyRoutes, err := parseInstalledFamilyRoutes(string(families.Stdout)) + if err != nil { + return err + } + var stackHelp installedCommandResult + var requirementSourceHelp installedCommandResult + stackHelpFound := false + requirementSourceHelpFound := false + for _, familyRoute := range familyRoutes { + family, err := execute(consumer, nil, familyRoute.Argv...) + if err != nil { + return fmt.Errorf("outside consumer family %s failed to run: %w", familyRoute.ID, err) + } + if err := requireInstalledTextSuccess(family, "family "+familyRoute.ID); err != nil { + return err + } + leafRoutes, err := parseInstalledLeafHelpRoutes(string(family.Stdout)) + if err != nil { + return fmt.Errorf("outside consumer family %s: %w", familyRoute.ID, err) + } + for _, leafRoute := range leafRoutes { + leafHelp, err := execute(consumer, nil, leafRoute.Argv...) + if err != nil { + return fmt.Errorf("outside consumer %s help failed to run: %w", leafRoute.ID, err) + } + if err := requireInstalledTextSuccess(leafHelp, leafRoute.ID+" help"); err != nil { + return err + } + if err := requireInstalledInvocationSyntax(leafHelp.Stdout, leafRoute.ID); err != nil { + return err + } + switch leafRoute.ID { + case "stack-preset": + if stackHelpFound { + return fmt.Errorf("outside consumer family navigation exposed stack-preset more than once") + } + stackHelp = leafHelp + stackHelpFound = true + case "requirement-source-admission": + if requirementSourceHelpFound { + return fmt.Errorf("outside consumer family navigation exposed requirement-source-admission more than once") + } + requirementSourceHelp = leafHelp + requirementSourceHelpFound = true + } + } + } + if !stackHelpFound { + return fmt.Errorf("outside consumer family navigation did not expose stack-preset") + } + if !requirementSourceHelpFound { + return fmt.Errorf("outside consumer family navigation did not expose requirement-source-admission") + } + presetRoutes, err := parseInstalledPresetRoutes(string(stackHelp.Stdout)) + if err != nil { + return err + } + contractPath := filepath.Join(consumer, "node_modules", "@research-engineering", "agentic-proofkit", "proofkit", "cli-contract.v2.json") + contractContent, err := os.ReadFile(contractPath) + if err != nil { + return fmt.Errorf("read installed CLI contract: %w", err) + } + contractPresetIDs, err := installedContractPresetIDs(contractContent) + if err != nil { + return err + } + helpPresetIDs := make([]string, 0, len(presetRoutes)) + for _, route := range presetRoutes { + helpPresetIDs = append(helpPresetIDs, route.ID) + } + if !sameStrings(helpPresetIDs, contractPresetIDs) { + return fmt.Errorf("installed stack-preset help ids=%v contract ids=%v", helpPresetIDs, contractPresetIDs) + } + var firstSelfContinuation []string + for _, presetRoute := range presetRoutes { + result, err := execute(consumer, nil, presetRoute.Argv...) + if err != nil { + return fmt.Errorf("outside consumer stack preset %s failed to run: %w", presetRoute.ID, err) + } + if err := requirePassedJSON(result, "stack preset "+presetRoute.ID); err != nil { + return err + } + continuations, err := installedPresetContinuationArgv(result, presetRoute.ID) + if err != nil { + return err + } + if firstSelfContinuation == nil { + firstSelfContinuation = append([]string{}, continuations[0]...) + } + } + if len(firstSelfContinuation) == 0 { + return fmt.Errorf("outside consumer stack presets exposed no executable self-continuation") + } + continuation, err := execute(consumer, nil, firstSelfContinuation...) + if err != nil { + return fmt.Errorf("outside consumer stack preset self-continuation failed to run: %w", err) + } + if err := requirePassedJSON(continuation, "stack preset self-continuation"); err != nil { + return err + } + readmeRelativePath, err := installedREADMEPath(requirementSourceHelp.Stdout) + if err != nil { + return err + } + readmePath := filepath.Join(consumer, filepath.FromSlash(readmeRelativePath)) + readme, err := os.ReadFile(readmePath) + if err != nil { + return fmt.Errorf("read installed README: %w", err) + } + argv, input, err := installedREADMEFirstInput(readme) + if err != nil { + return err + } + result, err := execute(consumer, input, argv...) + if err != nil { + return fmt.Errorf("outside consumer README first-input command failed to run: %w", err) + } + return requirePassedJSON(result, "README first-input command") +} + +func installedRootHelpFamilyArgv(content []byte) ([]string, error) { + const routeFragment = "agentic-proofkit help families" + var command string + matchCount := 0 + for _, line := range strings.Split(string(content), "\n") { + if !strings.Contains(line, routeFragment) { + continue + } + command = strings.TrimLeft(line, " \t") + matchCount++ + } + if matchCount != 1 { + return nil, fmt.Errorf("outside consumer root help must expose exactly one family discovery route") + } + if !strings.HasPrefix(command, installedNPMExecCommandPrefix) { + return nil, fmt.Errorf("outside consumer root help family route must use npm exec --offline") + } + argv, err := parseLiteralShellWords(strings.TrimPrefix(command, installedNPMExecCommandPrefix)) + if err != nil { + return nil, fmt.Errorf("outside consumer root help family route must use bounded literal shell words: %w", err) + } + if len(argv) != 2 || argv[0] != "help" || argv[1] != "families" { + return nil, fmt.Errorf("outside consumer root help family route must resolve to help families") + } + return argv, nil +} + +func parseInstalledFamilyRoutes(help string) ([]installedHelpRoute, error) { + ids, err := parseInstalledFamilyIDs(help) + if err != nil { + return nil, err + } + routeByID := map[string][]string{} + for _, line := range strings.Split(help, "\n") { + if !strings.Contains(line, "agentic-proofkit help family ") { + continue + } + argv, err := installedNPMExecArgv(line, "family discovery") + if err != nil { + return nil, err + } + if len(argv) != 3 || argv[0] != "help" || argv[1] != "family" || argv[2] == "" { + return nil, fmt.Errorf("outside consumer family discovery route must resolve to help family ") + } + if _, exists := routeByID[argv[2]]; exists { + return nil, fmt.Errorf("outside consumer family discovery route %q is duplicated", argv[2]) + } + routeByID[argv[2]] = argv + } + if len(routeByID) != len(ids) { + return nil, fmt.Errorf("outside consumer family discovery routes=%d family ids=%d", len(routeByID), len(ids)) + } + routes := make([]installedHelpRoute, 0, len(ids)) + for _, id := range ids { + argv, ok := routeByID[id] + if !ok { + return nil, fmt.Errorf("outside consumer family %q has no copyable discovery route", id) + } + routes = append(routes, installedHelpRoute{ID: id, Argv: argv}) + } + return routes, nil +} + +func parseInstalledLeafHelpRoutes(help string) ([]installedHelpRoute, error) { + commandIDs := []string{} + seenCommandIDs := map[string]struct{}{} + inCommands := false + for _, line := range strings.Split(help, "\n") { + if line == "Commands:" { + inCommands = true + continue + } + if !inCommands || !strings.HasPrefix(line, " ") || strings.HasPrefix(line, " ") { + continue + } + commandID := strings.TrimSpace(line) + if commandID == "" || strings.ContainsAny(commandID, " \t") { + return nil, fmt.Errorf("command family has an invalid command id %q", commandID) + } + if _, exists := seenCommandIDs[commandID]; exists { + return nil, fmt.Errorf("command family has duplicate command id %q", commandID) + } + seenCommandIDs[commandID] = struct{}{} + commandIDs = append(commandIDs, commandID) + } + if len(commandIDs) == 0 { + return nil, fmt.Errorf("command family exposed no command ids") + } + + routeByID := map[string][]string{} + for _, line := range strings.Split(help, "\n") { + if !strings.Contains(line, "agentic-proofkit help ") { + continue + } + argv, err := installedNPMExecArgv(line, "leaf help") + if err != nil { + return nil, err + } + if len(argv) != 2 || argv[0] != "help" || argv[1] == "" { + return nil, fmt.Errorf("outside consumer leaf help route must resolve to help ") + } + if _, exists := routeByID[argv[1]]; exists { + return nil, fmt.Errorf("outside consumer command %q has duplicate copyable help routes", argv[1]) + } + routeByID[argv[1]] = argv + } + if len(routeByID) != len(commandIDs) { + return nil, fmt.Errorf("outside consumer leaf help routes=%d command ids=%d", len(routeByID), len(commandIDs)) + } + routes := make([]installedHelpRoute, 0, len(commandIDs)) + for _, commandID := range commandIDs { + argv, ok := routeByID[commandID] + if !ok { + return nil, fmt.Errorf("outside consumer command %q has no copyable help route", commandID) + } + routes = append(routes, installedHelpRoute{ID: commandID, Argv: argv}) + } + return routes, nil +} + +func requireInstalledInvocationSyntax(content []byte, command string) error { + lines := strings.Split(string(content), "\n") + var usage string + var installed string + usageCount := 0 + installedCount := 0 + usageIndex := -1 + installedIndex := -1 + for index, line := range lines { + if line == "Usage:" { + usageCount++ + if index+1 >= len(lines) { + return fmt.Errorf("outside consumer command %q has no usage line", command) + } + usage = strings.TrimLeft(lines[index+1], " \t") + usageIndex = index + } + if line == "Installed invocation:" { + installedCount++ + if index+1 >= len(lines) { + return fmt.Errorf("outside consumer command %q has no installed invocation line", command) + } + installed = strings.TrimLeft(lines[index+1], " \t") + installedIndex = index + } + } + commandPrefix := "agentic-proofkit " + command + if usageCount != 1 || installedCount != 1 || + installedIndex != usageIndex+3 || + (usage != commandPrefix && !strings.HasPrefix(usage, commandPrefix+" ")) { + return fmt.Errorf("outside consumer command %q must expose one exact bare usage and installed invocation", command) + } + if installed != "npm exec --offline -- "+usage { + return fmt.Errorf("outside consumer command %q installed invocation must prefix its exact usage with npm exec --offline", command) + } + return nil +} + +func parseInstalledPresetRoutes(help string) ([]installedHelpRoute, error) { + ids, err := parseInstalledPresetIDs(help) + if err != nil { + return nil, err + } + routeByID := map[string][]string{} + for _, line := range strings.Split(help, "\n") { + if !strings.Contains(line, "agentic-proofkit stack-preset --preset ") { + continue + } + if strings.Contains(line, "<") { + continue + } + argv, err := installedNPMExecArgv(line, "stack preset") + if err != nil { + return nil, err + } + if len(argv) != 3 || argv[0] != "stack-preset" || argv[1] != "--preset" || argv[2] == "" { + return nil, fmt.Errorf("outside consumer stack-preset route must resolve to stack-preset --preset ") + } + if _, exists := routeByID[argv[2]]; exists { + return nil, fmt.Errorf("outside consumer stack-preset route %q is duplicated", argv[2]) + } + routeByID[argv[2]] = argv + } + if len(routeByID) != len(ids) { + return nil, fmt.Errorf("outside consumer stack-preset routes=%d preset ids=%d", len(routeByID), len(ids)) + } + routes := make([]installedHelpRoute, 0, len(ids)) + for _, id := range ids { + argv, ok := routeByID[id] + if !ok { + return nil, fmt.Errorf("outside consumer preset %q has no copyable execution route", id) + } + routes = append(routes, installedHelpRoute{ID: id, Argv: argv}) + } + return routes, nil +} + +func installedREADMEPath(content []byte) (string, error) { + const prefix = "Path: " + const expected = "node_modules/@research-engineering/agentic-proofkit/README.md" + var discovered string + matchCount := 0 + for _, line := range strings.Split(string(content), "\n") { + line = strings.TrimLeft(line, " \t") + if !strings.HasPrefix(line, prefix) { + continue + } + discovered = strings.TrimPrefix(line, prefix) + matchCount++ + } + if matchCount != 1 || discovered != expected { + return "", fmt.Errorf("outside consumer requirement-source-admission help must expose the exact installed README path") + } + return discovered, nil +} + +func installedNPMExecArgv(line string, label string) ([]string, error) { + command := strings.TrimLeft(line, " \t") + if !strings.HasPrefix(command, installedNPMExecCommandPrefix) { + return nil, fmt.Errorf("outside consumer %s route must use npm exec --offline", label) + } + argv, err := parseLiteralShellWords(strings.TrimPrefix(command, installedNPMExecCommandPrefix)) + if err != nil { + return nil, fmt.Errorf("outside consumer %s route must use bounded literal shell words: %w", label, err) + } + return argv, nil +} + +func installedPresetContinuationArgv(result installedCommandResult, presetID string) ([][]string, error) { + value, err := admission.DecodeJSON(bytes.NewReader(result.Stdout), 8<<20) + if err != nil { + return nil, fmt.Errorf("outside consumer stack preset %s stdout must be one JSON value: %w", presetID, err) + } + record, ok := value.(map[string]any) + if !ok { + return nil, fmt.Errorf("outside consumer stack preset %s output must be an object", presetID) + } + diagnostics, ok := record["diagnostics"].([]any) + if !ok { + return nil, fmt.Errorf("outside consumer stack preset %s output must expose diagnostics", presetID) + } + var presetValue map[string]any + presetDiagnosticCount := 0 + for _, rawDiagnostic := range diagnostics { + diagnostic, ok := rawDiagnostic.(map[string]any) + if !ok { + return nil, fmt.Errorf("outside consumer stack preset %s diagnostics must contain objects", presetID) + } + if diagnostic["key"] != "preset" { + continue + } + presetDiagnosticCount++ + presetValue, ok = diagnostic["value"].(map[string]any) + if !ok { + return nil, fmt.Errorf("outside consumer stack preset %s preset diagnostic must contain an object value", presetID) + } + } + if presetDiagnosticCount != 1 { + return nil, fmt.Errorf("outside consumer stack preset %s must expose exactly one preset diagnostic", presetID) + } + rawCommands, ok := presetValue["suggestedCommands"].([]any) + if !ok || len(rawCommands) == 0 { + return nil, fmt.Errorf("outside consumer stack preset %s must expose non-empty suggestedCommands", presetID) + } + continuations := make([][]string, 0, len(rawCommands)) + firstCommand := "" + for index, rawCommand := range rawCommands { + command, ok := rawCommand.(string) + if !ok || command == "" { + return nil, fmt.Errorf("outside consumer stack preset %s suggestedCommands[%d] must be a non-empty string", presetID, index) + } + if command != strings.TrimSpace(command) || !strings.HasPrefix(command, installedNPMExecCommandPrefix) { + return nil, fmt.Errorf("outside consumer stack preset %s suggestedCommands[%d] must use the exact npm exec --offline prefix", presetID, index) + } + argv, err := parseLiteralShellWords(strings.TrimPrefix(command, installedNPMExecCommandPrefix)) + if err != nil { + return nil, fmt.Errorf("outside consumer stack preset %s suggestedCommands[%d] must use bounded literal shell words: %w", presetID, index, err) + } + if len(argv) == 0 { + return nil, fmt.Errorf("outside consumer stack preset %s suggestedCommands[%d] has no Proofkit argv", presetID, index) + } + if index == 0 { + firstCommand = command + } + continuations = append(continuations, argv) + } + expectedSelfContinuation := installedNPMExecCommandPrefix + "stack-preset --preset " + presetID + if firstCommand != expectedSelfContinuation { + return nil, fmt.Errorf("outside consumer stack preset %s first suggested command must be its exact self-continuation", presetID) + } + return continuations, nil +} + +func requireInstalledTextSuccess(result installedCommandResult, label string) error { + if result.ExitCode != 0 || len(result.Stderr) != 0 || len(result.Stdout) == 0 { + return fmt.Errorf("outside consumer %s exit=%d stdout=%q stderr=%q", label, result.ExitCode, result.Stdout, result.Stderr) + } + return nil +} + +func requirePassedJSON(result installedCommandResult, label string) error { + if result.ExitCode != 0 || len(result.Stderr) != 0 { + return fmt.Errorf("outside consumer %s exit=%d stdout=%q stderr=%q", label, result.ExitCode, result.Stdout, result.Stderr) + } + value, err := admission.DecodeJSON(bytes.NewReader(result.Stdout), 8<<20) + if err != nil { + return fmt.Errorf("outside consumer %s stdout must be one JSON value: %w", label, err) + } + record, ok := value.(map[string]any) + if !ok || record["state"] != "passed" { + return fmt.Errorf("outside consumer %s state is not passed", label) + } + return nil +} + +func parseInstalledFamilyIDs(help string) ([]string, error) { + ids := []string{} + seen := map[string]struct{}{} + for _, line := range strings.Split(help, "\n") { + if !strings.HasPrefix(line, " ") || !strings.Contains(line, "\t") { + continue + } + id := strings.TrimSpace(strings.SplitN(strings.TrimSpace(line), "\t", 2)[0]) + if id != "" { + if _, exists := seen[id]; exists { + return nil, fmt.Errorf("outside consumer family help duplicated family id %q", id) + } + seen[id] = struct{}{} + ids = append(ids, id) + } + } + if len(ids) == 0 { + return nil, fmt.Errorf("outside consumer family help exposed no family ids") + } + return ids, nil +} + +func parseInstalledPresetIDs(help string) ([]string, error) { + const prefix = "agentic-proofkit stack-preset --preset <" + start := strings.Index(help, prefix) + if start < 0 { + return nil, fmt.Errorf("outside consumer stack-preset help omitted preset vocabulary") + } + start += len(prefix) + end := strings.Index(help[start:], ">") + if end < 0 { + return nil, fmt.Errorf("outside consumer stack-preset help has an unclosed preset vocabulary") + } + ids := strings.Split(help[start:start+end], "|") + if len(ids) == 0 || !sort.StringsAreSorted(ids) { + return nil, fmt.Errorf("outside consumer stack-preset help ids must be non-empty and sorted") + } + return ids, nil +} + +func installedContractPresetIDs(content []byte) ([]string, error) { + value, err := admission.DecodeJSON(bytes.NewReader(content), int64(len(content))) + if err != nil { + return nil, fmt.Errorf("decode installed CLI contract: %w", err) + } + record, ok := value.(map[string]any) + if !ok { + return nil, fmt.Errorf("installed CLI contract must be an object") + } + commands, ok := record["commands"].([]any) + if !ok { + return nil, fmt.Errorf("installed CLI contract commands must be an array") + } + for _, raw := range commands { + command, _ := raw.(map[string]any) + if command["command"] != "stack-preset" { + continue + } + output, _ := command["outputContract"].(map[string]any) + choices, _ := output["flagChoices"].(map[string]any) + rawIDs, _ := choices["--preset"].([]any) + ids := make([]string, 0, len(rawIDs)) + for _, rawID := range rawIDs { + id, ok := rawID.(string) + if !ok || id == "" { + return nil, fmt.Errorf("installed CLI contract stack-preset choices must be non-empty strings") + } + ids = append(ids, id) + } + if len(ids) == 0 || !sort.StringsAreSorted(ids) { + return nil, fmt.Errorf("installed CLI contract stack-preset choices must be non-empty and sorted") + } + return ids, nil + } + return nil, fmt.Errorf("installed CLI contract omitted stack-preset") +} + +func installedREADMEFirstInput(content []byte) ([]string, []byte, error) { + text := string(content) + const startMarker = "" + const endMarker = "" + if strings.Count(text, startMarker) != 1 || strings.Count(text, endMarker) != 1 { + return nil, nil, fmt.Errorf("installed README first-input markers must occur exactly once") + } + start := strings.Index(text, startMarker) + len(startMarker) + end := strings.Index(text, endMarker) + if start >= end { + return nil, nil, fmt.Errorf("installed README first-input marker order is invalid") + } + block := text[start:end] + fenceLines := regexp.MustCompile("(?m)^[ \\t]*(?:`{3,}|~{3,})[^\\r\\n]*$").FindAllString(block, -1) + if len(fenceLines) != 4 { + return nil, nil, fmt.Errorf("installed README first-input block must contain one bash command and one JSON value") + } + for _, line := range fenceLines { + if strings.Contains(line, "~") { + return nil, nil, fmt.Errorf("installed README first-input block must contain one bash command and one JSON value") + } + } + match := regexp.MustCompile("(?s)^\\s*```bash[ \\t]*\\r?\\n([^\\r\\n]+)\\r?\\n```[ \\t]*\\r?\\n\\s*```json[ \\t]*\\r?\\n(.*?)\\r?\\n```[ \\t]*\\s*$").FindStringSubmatch(block) + if len(match) != 3 { + return nil, nil, fmt.Errorf("installed README first-input block must contain one bash command and one JSON value") + } + command := strings.TrimLeft(match[1], " \t") + if !strings.HasPrefix(command, installedNPMExecCommandPrefix) { + return nil, nil, fmt.Errorf("installed README first-input command must use npm exec --offline") + } + argv, err := parseLiteralShellWords(strings.TrimPrefix(command, installedNPMExecCommandPrefix)) + if err != nil { + return nil, nil, fmt.Errorf("installed README first-input command must use bounded literal shell words: %w", err) + } + if len(argv) == 0 { + return nil, nil, fmt.Errorf("installed README first-input command has no Proofkit argv") + } + input := []byte(match[2] + "\n") + if _, err := admission.DecodeJSON(bytes.NewReader(input), int64(len(input))); err != nil { + return nil, nil, fmt.Errorf("installed README first-input JSON is invalid: %w", err) + } + return argv, input, nil +} + +func parseLiteralShellWords(command string) ([]string, error) { + var words []string + var word strings.Builder + var quote byte + wordStarted := false + + backslashRunEnd := func(index int) int { + end := index + for end < len(command) && command[end] == '\\' { + end++ + } + return end + } + writeBackslashes := func(count int) { + for range count { + word.WriteByte('\\') + } + } + flush := func() { + if !wordStarted { + return + } + words = append(words, word.String()) + word.Reset() + wordStarted = false + } + for index := 0; index < len(command); index++ { + character := command[index] + if character == 0 { + return nil, fmt.Errorf("NUL is not allowed") + } + if character == '\r' || character == '\n' { + return nil, fmt.Errorf("line breaks are not allowed") + } + switch quote { + case '\'': + if character == '\'' { + quote = 0 + } else { + word.WriteByte(character) + } + continue + case '"': + switch character { + case '"': + quote = 0 + case '$', '`': + return nil, fmt.Errorf("shell expansion is not allowed") + case '!': + return nil, fmt.Errorf("history expansion is not allowed") + case '\\': + end := backslashRunEnd(index) + count := end - index + if end == len(command) { + return nil, fmt.Errorf("trailing escape in double-quoted word") + } + next := command[end] + if next == 0 { + return nil, fmt.Errorf("NUL is not allowed") + } + if next == '\r' || next == '\n' { + return nil, fmt.Errorf("line continuation is not allowed") + } + if next == '!' { + writeBackslashes((count + 1) / 2) + word.WriteByte(next) + index = end + continue + } + writeBackslashes(count / 2) + if count%2 != 0 && strings.ContainsRune("\"\\$`", rune(next)) { + word.WriteByte(next) + index = end + } else { + if count%2 != 0 { + word.WriteByte('\\') + } + index = end - 1 + } + default: + word.WriteByte(character) + } + continue + } + + switch character { + case ' ', '\t': + flush() + case '\'', '"': + quote = character + wordStarted = true + case '\\': + end := backslashRunEnd(index) + count := end - index + if end == len(command) { + if count%2 != 0 { + return nil, fmt.Errorf("trailing escape") + } + writeBackslashes(count / 2) + wordStarted = true + index = end - 1 + continue + } + next := command[end] + if next == 0 { + return nil, fmt.Errorf("NUL is not allowed") + } + if next == '\r' || next == '\n' { + return nil, fmt.Errorf("line continuation is not allowed") + } + writeBackslashes(count / 2) + if next == '!' || count%2 != 0 { + word.WriteByte(next) + index = end + } else { + index = end - 1 + } + wordStarted = true + default: + if strings.ContainsRune(";&|<>()$`*?[]{}~#!", rune(character)) { + return nil, fmt.Errorf("shell operators and expansion syntax are not allowed") + } + word.WriteByte(character) + wordStarted = true + } + } + if quote != 0 { + return nil, fmt.Errorf("unterminated quoted word") + } + flush() + return words, nil +} + +func verifyInstalledJSONABI(consumer string) error { + if err := os.WriteFile(filepath.Join(consumer, "unlisted-poison.md"), []byte("bad trailing \n"), 0o644); err != nil { + return fmt.Errorf("write unlisted poison file: %w", err) + } + success, err := runInstalledWithInput(consumer, packageSmokeSuccessInput(), "text-policy", "--input", "-") + if err != nil { + return fmt.Errorf("outside consumer JSON success smoke failed to run: %w", err) + } + if err := verifyTextPolicySmokeReport(success, "proofkit.package-smoke.success", "passed", 0, textPolicySmokeSummary{ + CheckedTextFileCount: 1, + FailureCount: 0, + InputFileCount: 1, + }); err != nil { + return fmt.Errorf("outside consumer JSON success smoke failed: %w", err) + } + compactSuccess, err := runInstalledWithInput(consumer, packageSmokeSuccessInput(), "--json-layout", "compact", "text-policy", "--input", "-") + if err != nil { + return fmt.Errorf("outside consumer compact JSON success smoke failed to run: %w", err) + } + if err := verifyTextPolicySmokeReport(compactSuccess, "proofkit.package-smoke.success", "passed", 0, textPolicySmokeSummary{CheckedTextFileCount: 1, FailureCount: 0, InputFileCount: 1}); err != nil { + return fmt.Errorf("outside consumer compact JSON success smoke failed: %w", err) + } + failed, err := runInstalledWithInput(consumer, packageSmokeFailureInput(), "text-policy", "--input", "-") + if err != nil { + return fmt.Errorf("outside consumer JSON failure smoke failed to run: %w", err) + } + if err := verifyTextPolicySmokeReport(failed, "proofkit.package-smoke.failure", "failed", 1, textPolicySmokeSummary{ + CheckedTextFileCount: 1, + FailureCount: 1, + InputFileCount: 1, + }); err != nil { + return fmt.Errorf("outside consumer JSON failure smoke failed: %w", err) + } + compactFailed, err := runInstalledWithInput(consumer, packageSmokeFailureInput(), "--json-layout", "compact", "text-policy", "--input", "-") + if err != nil { + return fmt.Errorf("outside consumer compact JSON failure smoke failed to run: %w", err) + } + if err := verifyTextPolicySmokeReport(compactFailed, "proofkit.package-smoke.failure", "failed", 1, textPolicySmokeSummary{CheckedTextFileCount: 1, FailureCount: 1, InputFileCount: 1}); err != nil { + return fmt.Errorf("outside consumer compact JSON failure smoke failed: %w", err) + } + if err := verifyJSONAdapterSourceSmoke(consumer); err != nil { + return err + } + return nil +} + +func verifyJSONAdapterSourceSmoke(consumer string) error { + result, err := runInstalledWithInput(consumer, nil, "json-report-cli-adapter-source", "--language", "typescript") + if err != nil { + return fmt.Errorf("outside consumer JSON adapter source smoke failed to run: %w", err) + } + return verifyJSONAdapterSourceSmokeReport(result, jsonreportcliadaptersource.TypeScriptSource()) +} + +func verifyJSONAdapterSourceSmokeReport(result installedCommandResult, expectedSource string) error { + if result.ExitCode != 0 { + return fmt.Errorf("exit code %d, want 0; stdout=%s stderr=%s", result.ExitCode, result.Stdout, result.Stderr) + } + if len(result.Stderr) != 0 { + return fmt.Errorf("stderr must be empty for json adapter source smoke, got %q", string(result.Stderr)) + } + report, err := admission.DecodeTypedJSON[jsonAdapterSourceSmokeReport](bytes.NewReader(result.Stdout), 8<<20) + if err != nil { + return fmt.Errorf("json adapter source smoke stdout must be one JSON report: %w; stdout=%s", err, result.Stdout) + } + if report.SchemaVersion != 1 { + return fmt.Errorf("schemaVersion=%d, want 1", report.SchemaVersion) + } + if report.ArtifactKind != "proofkit.json-report-cli-adapter-source" { + return fmt.Errorf("artifactKind=%s, want proofkit.json-report-cli-adapter-source", report.ArtifactKind) + } + if report.Language != "typescript" { + return fmt.Errorf("language=%s, want typescript", report.Language) + } + if report.Format != "json" { + return fmt.Errorf("format=%s, want json", report.Format) + } + if report.SourceFileName != "proofkit-json-report-cli-adapter.ts" { + return fmt.Errorf("sourceFileName=%s, want proofkit-json-report-cli-adapter.ts", report.SourceFileName) + } + if report.GeneratorID != "proofkit.json-report-cli-adapter-source.typescript.v1" { + return fmt.Errorf("generatorId=%s, want proofkit.json-report-cli-adapter-source.typescript.v1", report.GeneratorID) + } + if report.Source != expectedSource { + return fmt.Errorf("json adapter source does not match current owner source") + } + if report.SourceSha256 != digest.SHA256TextRef(report.Source) { + return fmt.Errorf("json adapter source hash mismatch") + } + if report.Summary.ExportedSymbolCount < 20 { + return fmt.Errorf("summary.exportedSymbolCount=%d, want at least 20", report.Summary.ExportedSymbolCount) + } + if report.Summary.LineCount < 500 { + return fmt.Errorf("summary.lineCount=%d, want at least 500", report.Summary.LineCount) + } + for _, token := range []string{ + "function readProofkitBoundedTextFile", + "export function runProofkitNoInputJsonCommand", + "options.inputMode === \"none\"", + "stable JSON value must not contain unsafe integer numbers", + "openSync(filePath, \"r\")", + } { + if !strings.Contains(report.Source, token) { + return fmt.Errorf("json adapter source missing required token %q", token) + } + } + for _, token := range []string{ + "readFileSync(filePath, \"utf8\")", + "JSON.parse(text)", + } { + if strings.Contains(report.Source, token) { + return fmt.Errorf("json adapter source contains forbidden stale token %q", token) + } + } + return nil +} + +func runWithInput(dir string, name string, input []byte, args ...string) (installedCommandResult, error) { + command := exec.Command(name, args...) + command.Dir = dir + command.Stdin = bytes.NewReader(input) + var stdout bytes.Buffer + var stderr bytes.Buffer + command.Stdout = &stdout + command.Stderr = &stderr + err := command.Run() + exitCode := 0 + if err != nil { + var exitErr *exec.ExitError + if !errors.As(err, &exitErr) { + return installedCommandResult{}, err + } + exitCode = exitErr.ExitCode() + } + return installedCommandResult{ExitCode: exitCode, Stdout: stdout.Bytes(), Stderr: stderr.Bytes()}, nil +} + +func runInstalledWithInput(dir string, input []byte, args ...string) (installedCommandResult, error) { + npmArgs := append([]string{"exec", "--offline", "--", "agentic-proofkit"}, args...) + return runWithInput(dir, "npm", input, npmArgs...) } func verifyTextPolicySmokeReport(result installedCommandResult, reportID string, state string, exitCode int, summary textPolicySmokeSummary) error { diff --git a/internal/tools/packageverify/main_test.go b/internal/tools/packageverify/main_test.go index c1e57ee..8856cf8 100644 --- a/internal/tools/packageverify/main_test.go +++ b/internal/tools/packageverify/main_test.go @@ -9,7 +9,9 @@ import ( "encoding/hex" "encoding/json" "os" + "os/exec" "path/filepath" + "slices" "strings" "testing" @@ -226,6 +228,62 @@ func TestVerifyRootManifestBoundaryRejectsPackageManifestRuntimeDrift(t *testing } } +func TestVerifyRootManifestBoundaryRejectsDevDependencyDrift(t *testing.T) { + cases := []struct { + name string + patch func(string) string + }{ + { + name: "missing dependency", + patch: func(manifest string) string { + return strings.Replace(manifest, " \"axe-core\": \"4.12.1\",\n", "", 1) + }, + }, + { + name: "wrong dependency version", + patch: func(manifest string) string { + return strings.Replace(manifest, "\"axe-core\": \"4.12.1\"", "\"axe-core\": \"4.12.0\"", 1) + }, + }, + { + name: "surplus wrapper dependency", + patch: func(manifest string) string { + return strings.Replace( + manifest, + " \"devDependencies\": {\n", + " \"devDependencies\": {\n \"@axe-core/playwright\": \"4.12.1\",\n", + 1, + ) + }, + }, + } + for _, item := range cases { + t.Run(item.name, func(t *testing.T) { + filename := "agentic-proofkit-1.2.3.tgz" + manifest := item.patch(packageManifestFixture("git+https://github.com/research-engineering/agentic-proofkit.git")) + tarball := writePackageTarball(t, map[string]string{ + "package/package.json": manifest, + }) + content, err := os.ReadFile(tarball) + if err != nil { + t.Fatalf("read package tarball: %v", err) + } + record := packRecord{ + Filename: filename, + Integrity: testNPMIntegrity(content), + Name: rootPackageName, + Shasum: testSHA1(content), + Version: "1.2.3", + } + + err = verifyRootManifestBoundary(rootPackageArtifact{Content: content, Record: record}) + if err == nil || !strings.Contains(err.Error(), "devDependencies must equal") { + t.Fatalf("verifyRootManifestBoundary() error=%v, want devDependencies failure", err) + } + }) + } +} + func TestReadManifestFromTarRejectsUnknownPackageManifestFields(t *testing.T) { secretShapedKey := "api_key=ghp_1234567890abcdefghijklmnopqrstuvwx" tarball := writePackageTarball(t, map[string]string{ @@ -269,7 +327,7 @@ func TestVerifyRootManifestBoundaryRejectsLifecycleScripts(t *testing.T) { func TestSnapshotReadersDoNotRereadMutableTarballPath(t *testing.T) { tarball := writePackageTarball(t, map[string]string{ "package/ADOPTION.md": "package docs describe embedded Go binaries.", - "package/AGENTS.md": "package docs describe embedded Go binaries.", + "package/NON_CLAIMS.md": "package docs describe embedded Go binaries.", "package/docs/proofkit-contract-map.md": "package docs describe embedded Go binaries.", "package/package.json": packageManifestFixture("git+https://github.com/research-engineering/agentic-proofkit.git"), "package/proofkit/requirement-bindings.json": `{"requirements":[{"specPath":"docs/specs/example/requirements.v1.json"}]}`, @@ -592,6 +650,768 @@ func TestVerifyRequiredRootEntriesRequiresPackagePublicDocs(t *testing.T) { } } +func TestPackagePublicReferenceClosure(t *testing.T) { + cases := []struct { + name string + mutate func(map[string]string) + want string + }{ + {name: "valid closure"}, + { + name: "source-only backlog owner row", + mutate: func(entries map[string]string) { + entries["package/README.md"] = strings.Replace( + entries["package/README.md"], + "\n\n[Adoption]", + "\n| Active work ledger | `BACKLOG.md` |\n\n[Adoption]", + 1, + ) + }, + want: "dangling package-public route BACKLOG.md", + }, + { + name: "dangling Markdown destination", + mutate: func(entries map[string]string) { + entries["package/README.md"] += "[Missing](MISSING.md)\n" + }, + want: "dangling package-public Markdown destination MISSING.md", + }, + { + name: "dangling reference-style Markdown destination", + mutate: func(entries map[string]string) { + entries["package/README.md"] += "[Missing][missing]\n[missing]: MISSING.md\n" + }, + want: "dangling package-public Markdown destination MISSING.md", + }, + { + name: "dangling route-bearing README code span", + mutate: func(entries map[string]string) { + entries["package/README.md"] = strings.Replace( + entries["package/README.md"], + "docs/proofkit-contract-map.md", + "docs/MISSING.md", + 1, + ) + }, + want: "dangling package-public route docs/MISSING.md", + }, + { + name: "dangling machine route", + mutate: func(entries map[string]string) { + entries["package/proofkit/receipt-producer-policy.json"] = `{"producers":[{"producerId":"local.developer","evidenceRefs":["docs/missing.json"]}]}` + }, + want: "dangling package-public route docs/missing.json", + }, + { + name: "source-only witness misclassified as package public", + mutate: func(entries map[string]string) { + entries["package/proofkit/cli-contract.v2.json"] = `{"commands":[{"command":"fixture","inputContract":{"nativeSource":{"path":"internal/fixture.go","evidenceClass":"package_public"}}}]}` + }, + want: "must declare evidenceClass=source_checkout", + }, + { + name: "dangling help catalog source", + mutate: func(entries map[string]string) { + entries["package/proofkit/cli-contract.v2.json"] = `{"processContract":{"helpGrammar":{"helpCatalogFormsSource":"MISSING.json"}},"commands":[]}` + }, + want: "dangling package-public route MISSING.json", + }, + { + name: "dangling binding witness path", + mutate: func(entries map[string]string) { + entries["package/proofkit/requirement-bindings.json"] = `{"requirements":[{"specPath":"docs/specs/example/requirements.v1.json"}],"bindings":[{"witnessPath":"MISSING.go"}]}` + }, + want: "dangling source-checkout route MISSING.go", + }, + { + name: "dangling requirement source overview path", + mutate: func(entries map[string]string) { + entries["package/docs/specs/example/requirements.v1.json"] = `{"specPackagePath":"docs/specs/example","overviewPath":"MISSING.md","requirementsPath":"docs/specs/example/requirements.v1.json","requirements":[]}` + }, + want: "dangling package-public route MISSING.md", + }, + { + name: "dangling witness plan source selector", + mutate: func(entries map[string]string) { + entries["package/proofkit/witness-plan.json"] = `{"commands":[],"policies":[{"inputSelectors":["MISSING.md"],"outputSelectors":[],"cacheAdmissionRefs":[]}]}` + }, + want: "dangling source-checkout route MISSING.md", + }, + { + name: "unclassified future machine reference", + mutate: func(entries map[string]string) { + entries["package/proofkit/command-families.v1.json"] = `{"families":[],"documentationPath":"MISSING.md"}` + }, + want: "unclassified reference-bearing field /documentationPath", + }, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + entries := packageReferenceClosureFixture() + if test.mutate != nil { + test.mutate(entries) + } + tarball := writePackageTarball(t, entries) + artifact := tarballArtifact(t, tarball) + entrySet := map[string]struct{}{} + for entry := range entries { + entrySet[entry] = struct{}{} + } + err := verifyPackagePublicReferenceClosure(artifact, entrySet) + if test.want == "" { + if err != nil { + t.Fatalf("verifyPackagePublicReferenceClosure() error=%v, want nil", err) + } + return + } + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("verifyPackagePublicReferenceClosure() error=%v, want %q", err, test.want) + } + }) + } +} + +func TestExactTarballOnboardingTrace(t *testing.T) { + readme := mustReadBytes(t, filepath.Join("..", "..", "..", "README.md")) + contract := mustReadBytes(t, filepath.Join("..", "..", "..", "proofkit", "cli-contract.v2.json")) + target, err := releaseplatform.CurrentTarget() + if err != nil { + t.Skipf("current platform has no admitted npm package target: %v", err) + } + binaryPath := filepath.Join(t.TempDir(), "agentic-proofkit") + build := exec.Command("go", "build", "-o", binaryPath, "./cmd/agentic-proofkit") + build.Dir = filepath.Join("..", "..", "..") + if output, err := build.CombinedOutput(); err != nil { + t.Fatalf("build current product binary: %v\n%s", err, output) + } + binary := mustReadBytes(t, binaryPath) + tarball := writePackageTarball(t, map[string]string{ + "package/package.json": `{"name":"@research-engineering/agentic-proofkit","version":"1.2.3","bin":{"agentic-proofkit":"dist/agentic-proofkit"}}`, + "package/dist/agentic-proofkit": installedNPMWrapperFixture(target.PlatformSuffix), + target.PackageTarEntry: string(binary), + "package/README.md": string(readme), + "package/proofkit/cli-contract.v2.json": string(contract), + }) + content := mustReadBytes(t, tarball) + artifact := rootPackageArtifact{ + Content: content, + Record: packRecord{Filename: "agentic-proofkit-1.2.3.tgz"}, + } + quotedTempRoot := filepath.Join(t.TempDir(), `proof"kit`) + if err := os.Mkdir(quotedTempRoot, 0o700); err != nil { + t.Fatal(err) + } + t.Setenv("TMPDIR", quotedTempRoot) + if err := withExactTarballConsumer(artifact, func(consumer string) error { + return verifyInstalledOnboardingTrace(consumer, runInstalledWithInput) + }); err != nil { + t.Fatalf("exact tarball onboarding trace failed: %v", err) + } +} + +func installedNPMWrapperFixture(platformSuffix string) string { + return `#!/usr/bin/env sh +set -eu + +script="$0" +while [ -L "$script" ]; do + link=$(readlink "$script") + case "$link" in + /*) script="$link" ;; + *) script=$(CDPATH= cd -- "$(dirname -- "$script")" && pwd)/"$link" ;; + esac +done + +dir=$(CDPATH= cd -- "$(dirname -- "$script")" && pwd) +package_dir=$(CDPATH= cd -- "$dir/.." && pwd) +binary="$package_dir/dist/platform/` + platformSuffix + `/agentic-proofkit" + +AGENTIC_PROOFKIT_LAUNCHER_PROFILE=npm_offline +AGENTIC_PROOFKIT_PYTHON_EXECUTABLE= +export AGENTIC_PROOFKIT_LAUNCHER_PROFILE +export AGENTIC_PROOFKIT_PYTHON_EXECUTABLE + +exec "$binary" "$@" +` +} + +func TestREADMEInstallPolicyRequiresPreOneExactPinExplanation(t *testing.T) { + if err := verifyREADMEInstallPolicy(readmePreOneExactPinPolicy); err != nil { + t.Fatalf("owner policy rejected: %v", err) + } + for _, content := range []string{ + "", + "npm install --save-dev --save-exact @research-engineering/agentic-proofkit", + readmePreOneExactPinPolicy + "\n" + readmePreOneExactPinPolicy, + } { + if err := verifyREADMEInstallPolicy(content); err == nil { + t.Fatalf("invalid README install policy was admitted: %q", content) + } + } +} + +func TestOnboardingTraceCoversEveryDiscoveredPresetAndREADMEInput(t *testing.T) { + consumer := t.TempDir() + rootHelpRoute := "npm exec --offline -- agentic-proofkit help families" + familyRoutePrefix := "npm exec --offline -- agentic-proofkit help family " + stackHelpRoute := "npm exec --offline -- agentic-proofkit help stack-preset" + requirementSourceHelpRoute := "npm exec --offline -- agentic-proofkit help requirement-source-admission" + selfCheckHelpRoute := "npm exec --offline -- agentic-proofkit help self-check" + stackInstalledInvocation := "" + requirementSourceInstalledInvocation := "npm exec --offline -- agentic-proofkit requirement-source-admission --input [--input-pointer ]" + selfCheckInstalledInvocation := "npm exec --offline -- agentic-proofkit self-check --input " + presetRoutePrefix := "npm exec --offline -- agentic-proofkit stack-preset --preset " + readmeContinuation := "Path: node_modules/@research-engineering/agentic-proofkit/README.md" + installedRoot := filepath.Join(consumer, "node_modules", "@research-engineering", "agentic-proofkit") + for _, source := range []string{"README.md", "proofkit/cli-contract.v2.json"} { + content, err := os.ReadFile(filepath.Join("..", "..", "..", filepath.FromSlash(source))) + if err != nil { + t.Fatal(err) + } + writeFileBytes(t, filepath.Join(installedRoot, filepath.FromSlash(source)), content) + } + choices, err := installedContractPresetIDs(mustReadBytes(t, filepath.Join(installedRoot, "proofkit", "cli-contract.v2.json"))) + if err != nil { + t.Fatal(err) + } + stackInstalledInvocation = "npm exec --offline -- agentic-proofkit stack-preset --preset <" + strings.Join(choices, "|") + ">" + seenPresets := map[string]struct{}{} + presetExecutionCounts := map[string]int{} + presetSuggestedCommands := func(presetID string) []string { + return []string{ + installedNPMExecCommandPrefix + "stack-preset --preset " + presetID, + installedNPMExecCommandPrefix + "self-check --input -", + } + } + execute := func(_ string, input []byte, args ...string) (installedCommandResult, error) { + stdout := "" + switch { + case slices.Equal(args, []string{"help"}): + stdout = rootHelpRoute + "\nCLI/JSON is the public cross-language contract.\n" + case slices.Equal(args, []string{"help", "families"}): + stdout = "Command families:\n" + + " scaffolding\tScaffolding\n" + + " Scaffold projects.\n" + + " " + familyRoutePrefix + "scaffolding\n" + + " quality\tQuality\n" + + " Verify quality.\n" + + " " + familyRoutePrefix + "quality\n" + + " requirement-source-lifecycle\tRequirement source lifecycle\n" + + " Admit requirement sources.\n" + + " " + familyRoutePrefix + "requirement-source-lifecycle\n" + case slices.Equal(args, []string{"help", "family", "scaffolding"}): + stdout = "Commands:\n stack-preset\n " + stackHelpRoute + "\n" + case slices.Equal(args, []string{"help", "family", "quality"}): + stdout = "Commands:\n self-check\n " + selfCheckHelpRoute + "\n" + case slices.Equal(args, []string{"help", "family", "requirement-source-lifecycle"}): + stdout = "Commands:\n requirement-source-admission\n " + requirementSourceHelpRoute + "\n" + case slices.Equal(args, []string{"help", "stack-preset"}): + stdout = "Usage:\n agentic-proofkit stack-preset --preset <" + strings.Join(choices, "|") + ">\n" + + "\nInstalled invocation:\n " + stackInstalledInvocation + "\n" + + "Copyable preset commands:\n" + for _, choice := range choices { + stdout += " " + presetRoutePrefix + choice + "\n" + } + case slices.Equal(args, []string{"help", "requirement-source-admission"}): + stdout = "Usage:\n agentic-proofkit requirement-source-admission --input [--input-pointer ]\n" + + "\nInstalled invocation:\n " + requirementSourceInstalledInvocation + "\n" + + "Continue with the installed README first-valid-input example:\n " + readmeContinuation + "\n" + case slices.Equal(args, []string{"help", "self-check"}): + stdout = "Usage:\n agentic-proofkit self-check --input \n" + + "\nInstalled invocation:\n " + selfCheckInstalledInvocation + "\n" + case len(args) == 3 && args[0] == "stack-preset" && args[1] == "--preset": + seenPresets[args[2]] = struct{}{} + presetExecutionCounts[args[2]]++ + content, err := json.Marshal(map[string]any{ + "diagnostics": []any{map[string]any{ + "key": "preset", + "value": map[string]any{ + "suggestedCommands": presetSuggestedCommands(args[2]), + }, + }}, + "state": "passed", + }) + if err != nil { + t.Fatal(err) + } + stdout = string(content) + "\n" + case slices.Equal(args, []string{"requirement-source-admission", "--input", "-"}): + if len(input) == 0 { + t.Fatal("README first-input command received empty stdin") + } + stdout = `{"state":"passed"}` + "\n" + default: + t.Fatalf("unexpected installed argv: %v", args) + } + return installedCommandResult{Stdout: []byte(stdout)}, nil + } + if err := verifyInstalledOnboardingTrace(consumer, execute); err != nil { + t.Fatalf("verifyInstalledOnboardingTrace() error=%v", err) + } + if len(seenPresets) != len(choices) { + t.Fatalf("executed preset count=%d, want %d", len(seenPresets), len(choices)) + } + for _, choice := range choices { + if _, ok := seenPresets[choice]; !ok { + t.Fatalf("installed onboarding did not execute preset %s", choice) + } + } + for index, choice := range choices { + want := 1 + if index == 0 { + want = 2 + } + if presetExecutionCounts[choice] != want { + t.Fatalf("installed preset %s execution count=%d, want %d", choice, presetExecutionCounts[choice], want) + } + } + + validPresetSuggestedCommands := presetSuggestedCommands + for _, mutant := range []struct { + name string + commands func(string) []string + want string + }{ + { + name: "missing generated commands", + commands: func(string) []string { + return nil + }, + want: "must expose non-empty suggestedCommands", + }, + { + name: "bare non-first generated command", + commands: func(presetID string) []string { + return []string{ + installedNPMExecCommandPrefix + "stack-preset --preset " + presetID, + "agentic-proofkit self-check --input -", + } + }, + want: "suggestedCommands[1] must use the exact npm exec --offline prefix", + }, + { + name: "wrong self-continuation", + commands: func(string) []string { + return []string{installedNPMExecCommandPrefix + "stack-preset --preset other"} + }, + want: "first suggested command must be its exact self-continuation", + }, + { + name: "equivalent but non-canonical self-continuation", + commands: func(presetID string) []string { + return []string{installedNPMExecCommandPrefix + "stack-preset --preset '" + presetID + "'"} + }, + want: "first suggested command must be its exact self-continuation", + }, + } { + presetSuggestedCommands = mutant.commands + if err := verifyInstalledOnboardingTrace(consumer, execute); err == nil || + !strings.Contains(err.Error(), mutant.want) { + t.Fatalf("%s error=%v, want %q", mutant.name, err, mutant.want) + } + } + presetSuggestedCommands = validPresetSuggestedCommands + + for _, mutant := range []struct { + name string + route string + want string + }{ + {name: "bare executable", route: "agentic-proofkit help families", want: "must use npm exec --offline"}, + {name: "leading NBSP", route: "\u00a0npm exec --offline -- agentic-proofkit help families", want: "must use npm exec --offline"}, + {name: "trailing NBSP", route: "npm exec --offline -- agentic-proofkit help families\u00a0", want: "must resolve to help families"}, + } { + rootHelpRoute = mutant.route + if err := verifyInstalledOnboardingTrace(consumer, execute); err == nil || + !strings.Contains(err.Error(), mutant.want) { + t.Fatalf("%s root-help route error=%v, want %q", mutant.name, err, mutant.want) + } + } + rootHelpRoute = "npm exec --offline -- agentic-proofkit help families" + + familyRoutePrefix = "agentic-proofkit help family " + if err := verifyInstalledOnboardingTrace(consumer, execute); err == nil || + !strings.Contains(err.Error(), "family discovery route must use npm exec --offline") { + t.Fatalf("bare family route error=%v, want npm exec --offline rejection", err) + } + familyRoutePrefix = "npm exec --offline -- agentic-proofkit help family " + + stackHelpRoute = "agentic-proofkit help stack-preset" + if err := verifyInstalledOnboardingTrace(consumer, execute); err == nil || + !strings.Contains(err.Error(), "leaf help route must use npm exec --offline") { + t.Fatalf("bare leaf route error=%v, want npm exec --offline rejection", err) + } + stackHelpRoute = "npm exec --offline -- agentic-proofkit help stack-preset" + + requirementSourceHelpRoute = "agentic-proofkit help requirement-source-admission" + if err := verifyInstalledOnboardingTrace(consumer, execute); err == nil || + !strings.Contains(err.Error(), "leaf help route must use npm exec --offline") { + t.Fatalf("bare requirement-source leaf route error=%v, want npm exec --offline rejection", err) + } + requirementSourceHelpRoute = "npm exec --offline -- agentic-proofkit help requirement-source-admission" + + selfCheckHelpRoute = "agentic-proofkit help self-check" + if err := verifyInstalledOnboardingTrace(consumer, execute); err == nil || + !strings.Contains(err.Error(), "leaf help route must use npm exec --offline") { + t.Fatalf("bare ordinary leaf route error=%v, want npm exec --offline rejection", err) + } + selfCheckHelpRoute = "npm exec --offline -- agentic-proofkit help self-check" + + stackInstalledInvocation = "agentic-proofkit stack-preset --preset <" + strings.Join(choices, "|") + ">" + if err := verifyInstalledOnboardingTrace(consumer, execute); err == nil || + !strings.Contains(err.Error(), "installed invocation must prefix its exact usage with npm exec --offline") { + t.Fatalf("bare stack invocation error=%v, want installed invocation rejection", err) + } + stackInstalledInvocation = "npm exec --offline -- agentic-proofkit stack-preset --preset <" + strings.Join(choices, "|") + ">" + + requirementSourceInstalledInvocation = "agentic-proofkit requirement-source-admission --input [--input-pointer ]" + if err := verifyInstalledOnboardingTrace(consumer, execute); err == nil || + !strings.Contains(err.Error(), "installed invocation must prefix its exact usage with npm exec --offline") { + t.Fatalf("bare requirement-source invocation error=%v, want installed invocation rejection", err) + } + requirementSourceInstalledInvocation = "npm exec --offline -- agentic-proofkit requirement-source-admission --input [--input-pointer ]" + + selfCheckInstalledInvocation = "agentic-proofkit self-check --input " + if err := verifyInstalledOnboardingTrace(consumer, execute); err == nil || + !strings.Contains(err.Error(), "installed invocation must prefix its exact usage with npm exec --offline") { + t.Fatalf("bare ordinary invocation error=%v, want installed invocation rejection", err) + } + selfCheckInstalledInvocation = "npm exec --offline -- agentic-proofkit self-check --input " + + presetRoutePrefix = "agentic-proofkit stack-preset --preset " + if err := verifyInstalledOnboardingTrace(consumer, execute); err == nil || + !strings.Contains(err.Error(), "stack preset route must use npm exec --offline") { + t.Fatalf("bare preset route error=%v, want npm exec --offline rejection", err) + } + presetRoutePrefix = "npm exec --offline -- agentic-proofkit stack-preset --preset " + + readmeContinuation = "" + if err := verifyInstalledOnboardingTrace(consumer, execute); err == nil || + !strings.Contains(err.Error(), "must expose the exact installed README path") { + t.Fatalf("missing README continuation error=%v, want exact path rejection", err) + } +} + +func TestInstalledHelpRouteParsersRejectDuplicateOwnerIDs(t *testing.T) { + familyHelp := "Command families:\n" + + " quality\tQuality\n" + + " npm exec --offline -- agentic-proofkit help family quality\n" + + " quality\tDuplicate\n" + + " npm exec --offline -- agentic-proofkit help family other\n" + if _, err := parseInstalledFamilyRoutes(familyHelp); err == nil || + !strings.Contains(err.Error(), "duplicated family id") { + t.Fatalf("duplicate family id error=%v", err) + } + + leafHelp := "Commands:\n" + + " self-check\n" + + " npm exec --offline -- agentic-proofkit help self-check\n" + + " self-check\n" + + " npm exec --offline -- agentic-proofkit help other\n" + if _, err := parseInstalledLeafHelpRoutes(leafHelp); err == nil || + !strings.Contains(err.Error(), "duplicate command id") { + t.Fatalf("duplicate command id error=%v", err) + } +} + +func TestInstalledInvocationRequiresAuthoredOrderAndExactCommandToken(t *testing.T) { + valid := "Usage:\n" + + " agentic-proofkit self-check --input \n\n" + + "Installed invocation:\n" + + " npm exec --offline -- agentic-proofkit self-check --input \n" + if err := requireInstalledInvocationSyntax([]byte(valid), "self-check"); err != nil { + t.Fatalf("owner installed invocation: %v", err) + } + cases := []struct { + name string + content string + }{ + { + name: "installed block before usage", + content: "Installed invocation:\n" + + " npm exec --offline -- \n" + + "Usage:\n" + + " agentic-proofkit self-check --input \n", + }, + { + name: "command token prefix collision", + content: "Usage:\n" + + " agentic-proofkit self-checkevil --input \n\n" + + "Installed invocation:\n" + + " npm exec --offline -- agentic-proofkit self-checkevil --input \n", + }, + } + for _, item := range cases { + t.Run(item.name, func(t *testing.T) { + if err := requireInstalledInvocationSyntax([]byte(item.content), "self-check"); err == nil { + t.Fatal("invalid installed invocation was admitted") + } + }) + } +} + +func TestInstalledREADMEFirstInputRejectsAmbiguousFences(t *testing.T) { + valid := ` +` + "```bash\nnpm exec --offline -- agentic-proofkit requirement-source-admission --input -\n```\n\n" + "```json\n{}\n```\n" + `` + cases := []struct { + name string + content string + }{ + { + name: "duplicate bash", + content: strings.Replace(valid, "```json", "```bash\nnpm exec --offline -- agentic-proofkit help\n```\n\n```json", 1), + }, + { + name: "duplicate JSON", + content: strings.Replace(valid, "", "```json\n{}\n```\n", 1), + }, + { + name: "tilde bash", + content: strings.Replace(valid, "", "~~~bash\nnpm exec --offline -- agentic-proofkit help\n~~~\n", 1), + }, + { + name: "tilde JSON", + content: strings.Replace(valid, "", "~~~json\n{}\n~~~\n", 1), + }, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + _, _, err := installedREADMEFirstInput([]byte(test.content)) + if err == nil || !strings.Contains(err.Error(), "must contain one bash command and one JSON value") { + t.Fatalf("installedREADMEFirstInput() error=%v", err) + } + }) + } +} + +func TestInstalledREADMEFirstInputUsesBoundedLiteralShellWords(t *testing.T) { + readme := func(command string) []byte { + return []byte(` +` + "```bash\n" + command + "\n```\n\n```json\n{}\n```\n" + ``) + } + accepted := []struct { + name string + command string + want []string + }{ + { + name: "single and double quotes", + command: `npm exec --offline -- agentic-proofkit "requirement-source-admission" --input '-'`, + want: []string{"requirement-source-admission", "--input", "-"}, + }, + { + name: "escaped literal", + command: `npm exec --offline -- agentic-proofkit requirement-source-admission --input \-`, + want: []string{"requirement-source-admission", "--input", "-"}, + }, + { + name: "escaped trailing space", + command: `npm exec --offline -- agentic-proofkit requirement-source-admission --input value\ `, + want: []string{"requirement-source-admission", "--input", "value "}, + }, + { + name: "escaped trailing tab", + command: "npm exec --offline -- agentic-proofkit requirement-source-admission --input value\\\t", + want: []string{"requirement-source-admission", "--input", "value\t"}, + }, + { + name: "concatenated literal segments", + command: `npm exec --offline -- agentic-proofkit 'requirement-'source-admission --input "-"`, + want: []string{"requirement-source-admission", "--input", "-"}, + }, + { + name: "vertical tab is literal", + command: "npm exec --offline -- agentic-proofkit requirement-source-admission --input -\v", + want: []string{"requirement-source-admission", "--input", "-\v"}, + }, + { + name: "non-breaking space is literal", + command: "npm exec --offline -- agentic-proofkit requirement-source-admission --input -\u00a0", + want: []string{"requirement-source-admission", "--input", "-\u00a0"}, + }, + { + name: "double quoted one-backslash history marker", + command: `npm exec --offline -- agentic-proofkit requirement-source-admission --input "\!"`, + want: []string{"requirement-source-admission", "--input", `\!`}, + }, + { + name: "double quoted two-backslash history marker", + command: `npm exec --offline -- agentic-proofkit requirement-source-admission --input "\\!"`, + want: []string{"requirement-source-admission", "--input", `\!`}, + }, + { + name: "double quoted three-backslash history marker", + command: `npm exec --offline -- agentic-proofkit requirement-source-admission --input "\\\!"`, + want: []string{"requirement-source-admission", "--input", `\\!`}, + }, + { + name: "double quoted four-backslash history marker", + command: `npm exec --offline -- agentic-proofkit requirement-source-admission --input "\\\\!"`, + want: []string{"requirement-source-admission", "--input", `\\!`}, + }, + { + name: "unquoted one-backslash history marker", + command: `npm exec --offline -- agentic-proofkit requirement-source-admission --input \!`, + want: []string{"requirement-source-admission", "--input", `!`}, + }, + { + name: "unquoted two-backslash history marker", + command: `npm exec --offline -- agentic-proofkit requirement-source-admission --input \\!`, + want: []string{"requirement-source-admission", "--input", `\!`}, + }, + { + name: "unquoted three-backslash history marker", + command: `npm exec --offline -- agentic-proofkit requirement-source-admission --input \\\!`, + want: []string{"requirement-source-admission", "--input", `\!`}, + }, + { + name: "unquoted four-backslash history marker", + command: `npm exec --offline -- agentic-proofkit requirement-source-admission --input \\\\!`, + want: []string{"requirement-source-admission", "--input", `\\!`}, + }, + } + for _, test := range accepted { + t.Run("accept/"+test.name, func(t *testing.T) { + got, _, err := installedREADMEFirstInput(readme(test.command)) + if err != nil { + t.Fatalf("installedREADMEFirstInput() error=%v", err) + } + if !slices.Equal(got, test.want) { + t.Fatalf("installedREADMEFirstInput() argv=%v, want %v", got, test.want) + } + }) + } + + rejected := []struct { + name string + command string + }{ + {name: "unquoted expansion", command: `npm exec --offline -- agentic-proofkit requirement-source-admission --input $INPUT`}, + {name: "double quoted expansion", command: `npm exec --offline -- agentic-proofkit requirement-source-admission --input "$INPUT"`}, + {name: "double quoted history expansion", command: `npm exec --offline -- agentic-proofkit requirement-source-admission --input "!!"`}, + {name: "control operator", command: `npm exec --offline -- agentic-proofkit requirement-source-admission --input -; true`}, + {name: "glob", command: `npm exec --offline -- agentic-proofkit requirement-source-admission --input *`}, + {name: "unterminated quote", command: `npm exec --offline -- agentic-proofkit requirement-source-admission --input '-'` + "'"}, + {name: "trailing escape", command: `npm exec --offline -- agentic-proofkit requirement-source-admission --input \`}, + {name: "unquoted NUL", command: "npm exec --offline -- agentic-proofkit requirement-source-admission --input \x00"}, + {name: "single quoted NUL", command: "npm exec --offline -- agentic-proofkit requirement-source-admission --input '\x00'"}, + {name: "double quoted NUL", command: "npm exec --offline -- agentic-proofkit requirement-source-admission --input \"\x00\""}, + {name: "escaped NUL", command: "npm exec --offline -- agentic-proofkit requirement-source-admission --input \\" + "\x00"}, + } + for _, test := range rejected { + t.Run("reject/"+test.name, func(t *testing.T) { + _, _, err := installedREADMEFirstInput(readme(test.command)) + if err == nil || !strings.Contains(err.Error(), "bounded literal shell words") { + t.Fatalf("installedREADMEFirstInput() error=%v", err) + } + }) + } +} + +func TestInstalledREADMEFirstInputPreservesJSONExampleBytes(t *testing.T) { + readme := func(input string) []byte { + return []byte(` +` + "```bash\nnpm exec --offline -- agentic-proofkit requirement-source-admission --input -\n```\n\n```json\n" + input + "\n```\n" + ``) + } + + const validInput = "{\"requirements\":[]} \t" + _, got, err := installedREADMEFirstInput(readme(validInput)) + if err != nil { + t.Fatalf("installedREADMEFirstInput() error=%v", err) + } + if want := validInput + "\n"; string(got) != want { + t.Fatalf("installedREADMEFirstInput() input=%q, want %q", got, want) + } + + _, _, err = installedREADMEFirstInput(readme("{}\u00a0")) + if err == nil || !strings.Contains(err.Error(), "JSON is invalid") { + t.Fatalf("installedREADMEFirstInput() NBSP error=%v", err) + } +} + +func TestLiteralShellWordsConsumesLongBackslashRun(t *testing.T) { + const backslashCount = 128 << 10 + command := strings.Repeat("\\", backslashCount) + "value" + got, err := parseLiteralShellWords(command) + if err != nil { + t.Fatalf("parseLiteralShellWords() error=%v", err) + } + if len(got) != 1 { + t.Fatalf("parseLiteralShellWords() word count=%d, want 1", len(got)) + } + want := strings.Repeat("\\", backslashCount/2) + "value" + if got[0] != want { + t.Fatalf("parseLiteralShellWords() word length=%d, want %d", len(got[0]), len(want)) + } +} + +func mustReadBytes(t *testing.T, path string) []byte { + t.Helper() + content, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return content +} + +func packageReferenceClosureFixture() map[string]string { + return map[string]string{ + "package/README.md": readmePreOneExactPinPolicy + "\n\nThe full machine-readable command inventory remains\n`proofkit/cli-contract.v2.json`; the human route map is\n`docs/proofkit-contract-map.md`.\n\n| Need | Owner |\n|---|---|\n| Adoption | `ADOPTION.md` |\n\n[Adoption](ADOPTION.md \"Guide\")\n[Adoption reference][adoption]\n[adoption]: ADOPTION.md \"Guide\"\n", + "package/ADOPTION.md": "Adoption.\n", + "package/docs/proofkit-contract-map.md": "Contract map.\n", + "package/docs/specs/example/overview.md": "Example.\n", + "package/docs/specs/example/requirements.v1.json": `{"specPackagePath":"docs/specs/example","overviewPath":"docs/specs/example/overview.md","requirementsPath":"docs/specs/example/requirements.v1.json","requirements":[]}`, + "package/proofkit/requirement-bindings.json": `{"requirements":[{"specPath":"docs/specs/example/requirements.v1.json"}],"bindings":[{"witnessPath":"internal/tools/packageverify/main_test.go","witnessSelectors":[{"selector":"TestPackagePublicReferenceClosure","command":"go test ./internal/tools/packageverify -run '^TestPackagePublicReferenceClosure$'"}]}]}`, + "package/proofkit/witness-plan.json": `{"commands":[],"policies":[]}`, + "package/proofkit/command-families.v1.json": `{"families":[]}`, + "package/proofkit/receipt-producer-policy.json": `{"producers":[{"producerId":"local.developer","evidenceRefs":["docs/specs/example/requirements.v1.json"]}]}`, + "package/proofkit/cli-contract.v2.json": `{"processContract":{"helpGrammar":{"helpCatalogFormsSource":"proofkit/command-families.v1.json"}},"commands":[{"command":"fixture","inputContract":{"nativeSource":{"path":"internal/tools/packageverify/main.go","evidenceClass":"source_checkout"}}}]}`, + } +} + +func TestVerifyRootPackageRejectsEachForbiddenRootEntry(t *testing.T) { + forbiddenEntries := []string{ + "package/bun.lock", + "package/dist/cli.js", + "package/dist/index.js", + "package/proofkit/sdk-cli-parity.v1.json", + "package/tsconfig.json", + "package/dist/fixture.d.ts", + "package/dist/fixture.ts", + "package/dist/fixture.map", + } + for _, forbidden := range forbiddenEntries { + t.Run(forbidden, func(t *testing.T) { + root := t.TempDir() + withWorkingDirectory(t, root) + entries := map[string]string{} + for _, required := range requiredRootEntries() { + entries[required] = "fixture" + } + entries[forbidden] = "forbidden" + tarball := writePackageTarball(t, entries) + content, err := os.ReadFile(tarball) + if err != nil { + t.Fatalf("read package tarball: %v", err) + } + filename := "agentic-proofkit-1.2.3.tgz" + writeFileBytes(t, filepath.Join(root, "artifacts", "package", filename), content) + record := packRecord{ + Filename: filename, + Integrity: testNPMIntegrity(content), + Name: rootPackageName, + Shasum: testSHA1(content), + Version: "1.2.3", + } + + _, err = verifyRootPackage(record) + if err == nil || !strings.Contains(err.Error(), "forbidden entry "+forbidden) { + t.Fatalf("verifyRootPackage() error=%v, want rejection of %s", err, forbidden) + } + }) + } +} + func TestVerifyTextPolicySmokeReportRequiresJSONABI(t *testing.T) { wantSummary := textPolicySmokeSummary{ CheckedTextFileCount: 1, @@ -786,8 +1606,6 @@ func quotedJSON(value string) string { func packageDocEntries(content string) map[string]string { return map[string]string{ "package/ADOPTION.md": content, - "package/AGENTS.md": content, - "package/CONTRIBUTING.md": content, "package/NON_CLAIMS.md": content, "package/README.md": content, "package/SECURITY.md": content, @@ -809,9 +1627,13 @@ func writePackageTarball(t *testing.T, entries map[string]string) string { tarWriter := tar.NewWriter(gzipWriter) for name, content := range entries { body := []byte(content) + mode := int64(0o644) + if rootBinaryEntry(name) { + mode = 0o755 + } if err := tarWriter.WriteHeader(&tar.Header{ Name: name, - Mode: 0o644, + Mode: mode, Size: int64(len(body)), Typeflag: tar.TypeReg, }); err != nil { @@ -888,8 +1710,8 @@ func packageManifestFixture(repositoryURL string) string { "type": "module", "sideEffects": false, "devDependencies": { - "@axe-core/playwright": "4.12.1", - "@playwright/test": "1.61.1", + "@playwright/test": "1.62.0", + "axe-core": "4.12.1", "typescript": "7.0.2" }, "repository": { @@ -914,11 +1736,9 @@ func packageManifestFixture(repositoryURL string) string { "arm64", "x64" ], - "files": [ - "ADOPTION.md", - "AGENTS.md", - "CONTRIBUTING.md", - "LICENSE", + "files": [ + "ADOPTION.md", + "LICENSE", "NON_CLAIMS.md", "README.md", "SECURITY.md", diff --git a/internal/tools/pythonpackage/continuation_test.go b/internal/tools/pythonpackage/continuation_test.go new file mode 100644 index 0000000..5c539eb --- /dev/null +++ b/internal/tools/pythonpackage/continuation_test.go @@ -0,0 +1,89 @@ +package main + +import ( + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" +) + +func TestExactDisplayedRouteOperandsRejectsWhitespaceAndExpansionMutants(t *testing.T) { + const prefix = "/venv/bin/python -m agentic_proofkit help " + operands, err := exactDisplayedRouteOperands([]byte("Commands:\n "+prefix+"self-check\n"), prefix, "test routes") + if err != nil || len(operands) != 1 || operands[0] != "self-check" { + t.Fatalf("exact route operands=%v error=%v, want [self-check]", operands, err) + } + mutants := map[string]string{ + "two-space indent": " " + prefix + "self-check\n", + "leading NBSP": "\u00a0 " + prefix + "self-check\n", + "trailing NBSP": " " + prefix + "self-check\u00a0\n", + "semicolon": " " + prefix + "self-check;touch-pwned\n", + "expansion": " " + prefix + "$(touch-pwned)\n", + "duplicate": " " + prefix + "self-check\n " + prefix + "self-check\n", + "empty operand": " " + prefix + "\n", + } + for name, output := range mutants { + t.Run(name, func(t *testing.T) { + if _, err := exactDisplayedRouteOperands([]byte(output), prefix, "test routes"); err == nil { + t.Fatalf("mutant survived exact route admission: %q", strings.TrimSuffix(output, "\n")) + } + }) + } +} + +func TestInstalledWheelContinuationUsesExactPythonModuleProfileWithoutNPM(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Windows wheels are not supported") + } + repositoryRoot, err := filepath.Abs(filepath.Join("..", "..", "..")) + if err != nil { + t.Fatal(err) + } + withWorkingDirectory(t, repositoryRoot, func() { + runInstalledWheelContinuationWitness(t, repositoryRoot) + }) +} + +func runInstalledWheelContinuationWitness(t *testing.T, repositoryRoot string) { + t.Helper() + target, err := currentTarget() + if err != nil { + t.Skipf("current platform has no admitted wheel target: %v", err) + } + manifest, err := readPackageJSON() + if err != nil { + t.Fatal(err) + } + binaryPath := filepath.Join(t.TempDir(), "agentic-proofkit") + build := exec.Command("go", "build", "-o", binaryPath, "./cmd/agentic-proofkit") + build.Dir = repositoryRoot + if output, err := build.CombinedOutput(); err != nil { + t.Fatalf("build current native CLI: %v\n%s", err, output) + } + binaryContent, err := os.ReadFile(binaryPath) + if err != nil { + t.Fatal(err) + } + entries, err := wheelEntries(manifest, target, binaryContent) + if err != nil { + t.Fatal(err) + } + wheelPath := filepath.Join(t.TempDir(), wheelFilename(manifest.Version, target)) + if err := writeWheel(wheelPath, entries); err != nil { + t.Fatal(err) + } + python, err := exec.LookPath("python3") + if err != nil { + t.Fatalf("python3 is required for the installed wheel continuation witness: %v", err) + } + consumer := t.TempDir() + if output, err := runCommand("", python, "-m", "venv", consumer); err != nil { + t.Fatalf("create Python consumer venv: %v\n%s", err, output) + } + venvPython := filepath.Join(consumer, "bin", "python") + if err := verifyInstalledPythonWheel(consumer, venvPython, wheelPath); err != nil { + t.Fatal(err) + } +} diff --git a/internal/tools/pythonpackage/exec_lifecycle_test.go b/internal/tools/pythonpackage/exec_lifecycle_test.go index cf0a2ed..a632750 100644 --- a/internal/tools/pythonpackage/exec_lifecycle_test.go +++ b/internal/tools/pythonpackage/exec_lifecycle_test.go @@ -43,6 +43,7 @@ def raise_exit(): signal.signal(signal.SIGTERM, lambda _signum, _frame: raise_exit()) pid_path = os.environ["PROOFKIT_TEST_PID_FILE"] +profile_path = os.environ["PROOFKIT_TEST_PROFILE_FILE"] temporary_pid_path = pid_path + ".tmp" with open(temporary_pid_path, "w", encoding="utf-8") as handle: handle.write(str(os.getpid())) @@ -50,6 +51,10 @@ with open(temporary_pid_path, "w", encoding="utf-8") as handle: os.fsync(handle.fileno()) os.replace(temporary_pid_path, pid_path) +with open(profile_path, "w", encoding="utf-8") as handle: + handle.write(os.environ["AGENTIC_PROOFKIT_LAUNCHER_PROFILE"] + "\n") + handle.write(os.environ["AGENTIC_PROOFKIT_PYTHON_EXECUTABLE"] + "\n") + while True: time.sleep(0.1) ` @@ -57,8 +62,16 @@ while True: t.Fatal(err) } pidFile := filepath.Join(root, "child.pid") + profileFile := filepath.Join(root, "launcher-profile.txt") command := exec.Command(python, "-m", "agentic_proofkit") - command.Env = append(os.Environ(), "PYTHONPATH="+root, "PROOFKIT_TEST_PID_FILE="+pidFile) + command.Env = append( + os.Environ(), + "PYTHONPATH="+root, + "PROOFKIT_TEST_PID_FILE="+pidFile, + "PROOFKIT_TEST_PROFILE_FILE="+profileFile, + "AGENTIC_PROOFKIT_LAUNCHER_PROFILE=hostile", + "AGENTIC_PROOFKIT_PYTHON_EXECUTABLE=relative/python", + ) if err := command.Start(); err != nil { t.Fatal(err) } @@ -72,6 +85,22 @@ while True: if pid != command.Process.Pid { t.Fatalf("wrapper pid=%d embedded CLI pid=%d, want exec-preserved identity", command.Process.Pid, pid) } + profile := waitForFile(t, profileFile) + profileLines := strings.Split(strings.TrimSuffix(profile, "\n"), "\n") + if len(profileLines) != 2 || profileLines[0] != "python_module" || !filepath.IsAbs(profileLines[1]) { + t.Fatalf("wrapper launcher profile=%q, want overwritten python_module and absolute interpreter", profile) + } + wrapperPython, err := os.Stat(profileLines[1]) + if err != nil { + t.Fatalf("stat wrapper interpreter: %v", err) + } + invokedPython, err := os.Stat(python) + if err != nil { + t.Fatalf("stat invoked interpreter: %v", err) + } + if !os.SameFile(wrapperPython, invokedPython) { + t.Fatalf("wrapper interpreter=%q does not identify invoked interpreter %q", profileLines[1], python) + } if err := command.Process.Signal(syscall.SIGTERM); err != nil { t.Fatal(err) } @@ -110,6 +139,20 @@ func waitForPID(t *testing.T, path string, expected int) int { return 0 } +func waitForFile(t *testing.T, path string) string { + t.Helper() + deadline := time.Now().Add(5 * time.Second) + for time.Now().Before(deadline) { + content, err := os.ReadFile(path) + if err == nil && len(content) > 0 { + return string(content) + } + time.Sleep(10 * time.Millisecond) + } + t.Fatalf("timed out waiting for %s", path) + return "" +} + func TestWaitForPIDIgnoresPartialMalformedAndStaleValues(t *testing.T) { path := filepath.Join(t.TempDir(), "child.pid") expected := 424242 diff --git a/internal/tools/pythonpackage/metadata_test.go b/internal/tools/pythonpackage/metadata_test.go index fa5a143..d24bd77 100644 --- a/internal/tools/pythonpackage/metadata_test.go +++ b/internal/tools/pythonpackage/metadata_test.go @@ -7,8 +7,12 @@ import ( "fmt" "os" "path/filepath" + "regexp" + "slices" "strings" "testing" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/releaseplatform" ) const testLicenseContent = "MIT License\n" @@ -115,6 +119,112 @@ func TestMetadataUsesCoreMetadata24LicenseFields(t *testing.T) { } } +func TestREADMEPlatformAndPythonProjection(t *testing.T) { + content, err := os.ReadFile(filepath.Join("..", "..", "..", "README.md")) + if err != nil { + t.Fatal(err) + } + block := markerBlock(t, string(content), "", "") + normalizedBlock := strings.Join(strings.Fields(block), " ") + ownerMinimums := map[string]string{} + ownerArchitectures := map[string][]string{} + for _, target := range releaseplatform.Targets() { + var minimum string + switch target.GOOS { + case "darwin": + match := regexp.MustCompile(`^macosx_([0-9]+)_0_(?:arm64|x86_64)$`).FindStringSubmatch(target.PlatformTag) + if len(match) != 2 { + t.Fatalf("README macOS projection owner has unexpected tag %s", target.PlatformTag) + } + minimum = match[1] + case "linux": + match := regexp.MustCompile(`^manylinux_([0-9]+)_([0-9]+)_(?:aarch64|x86_64)$`).FindStringSubmatch(target.PlatformTag) + if len(match) != 3 { + t.Fatalf("README Linux projection owner has unexpected tag %s", target.PlatformTag) + } + minimum = match[1] + "." + match[2] + default: + t.Fatalf("README projection owner contains undocumented OS %s", target.GOOS) + } + if prior := ownerMinimums[target.GOOS]; prior != "" && prior != minimum { + t.Fatalf("release targets for %s disagree on minimum %s versus %s", target.GOOS, prior, minimum) + } + ownerMinimums[target.GOOS] = minimum + ownerArchitectures[target.GOOS] = append(ownerArchitectures[target.GOOS], target.NPMCPU) + } + if len(ownerMinimums) != 2 { + t.Fatalf("README supported/unsupported OS projection is not closed: owners=%v", ownerMinimums) + } + expectedBlock := strings.Join([]string{ + fmt.Sprintf("Supported binary targets are macOS %s or later on %s.", ownerMinimums["darwin"], strings.Join(ownerArchitectures["darwin"], " or ")), + fmt.Sprintf("Linux manylinux %s or later is supported on %s. Windows is unsupported. The Python", ownerMinimums["linux"], strings.Join(ownerArchitectures["linux"], " or ")), + "runner requires Python 3.9 or later and wraps the same Go CLI; it is not a", + "Python SDK.", + "", + "After an exact Python package version is available from an admitted channel,", + "use one complete package-manager chain:", + "", + "```bash", + "python -m pip install agentic-proofkit==", + "python -m agentic_proofkit help", + "```", + "", + "or:", + "", + "```bash", + "uv add --dev agentic-proofkit==", + "uv run agentic-proofkit help", + "```", + "", + "These conditional commands do not claim that any current version is available", + "on PyPI.", + }, "\n") + expectedNormalized := strings.Join(strings.Fields(expectedBlock), " ") + if normalizedBlock != expectedNormalized { + t.Fatalf("README platform/Python block is not the exact owner projection:\ngot: %s\nwant: %s", normalizedBlock, expectedNormalized) + } + if !strings.Contains(metadata(testPackageManifest("1.2.3")), "Requires-Python: >=3.9\n") { + t.Fatal("wheel metadata no longer matches the README Python minimum") + } +} + +func TestReleaseTargetsProjectExactPythonWheelMetadata(t *testing.T) { + ownerTargets := releaseplatform.Targets() + targets := releaseTargets() + if !slices.Equal(targets, ownerTargets) { + t.Fatalf("releaseTargets() = %#v, want owner targets %#v", targets, ownerTargets) + } + for _, target := range ownerTargets { + wantMetadata := strings.Join([]string{ + "Wheel-Version: 1.0", + "Generator: agentic-proofkit", + "Root-Is-Purelib: false", + "Tag: " + target.WheelTag, + "", + }, "\n") + if got := wheelMetadata(target); got != wantMetadata { + t.Fatalf("wheelMetadata(%s) = %q, want %q", target.PlatformSuffix, got, wantMetadata) + } + wantFilename := "agentic_proofkit-1.2.3-" + target.WheelTag + ".whl" + if got := wheelFilename("1.2.3", target); got != wantFilename { + t.Fatalf("wheelFilename(%s) = %q, want %q", target.PlatformSuffix, got, wantFilename) + } + } +} + +func markerBlock(t *testing.T, content, start, end string) string { + t.Helper() + if strings.Count(content, start) != 1 || strings.Count(content, end) != 1 { + t.Fatalf("marker counts start=%d end=%d, want one each", strings.Count(content, start), strings.Count(content, end)) + } + startIndex := strings.Index(content, start) + len(start) + endIndex := strings.Index(content, end) + if startIndex >= endIndex { + t.Fatal("marker block is empty or reversed") + } + return content[startIndex:endIndex] +} + func TestWheelEntriesIncludeCanonicalLicense(t *testing.T) { root := t.TempDir() for _, source := range []string{ diff --git a/internal/tools/pythonpackage/verify.go b/internal/tools/pythonpackage/verify.go index d9e0316..2a9a492 100644 --- a/internal/tools/pythonpackage/verify.go +++ b/internal/tools/pythonpackage/verify.go @@ -7,6 +7,7 @@ import ( "debug/macho" "encoding/base64" "encoding/csv" + "encoding/json" "fmt" "io" "os" @@ -16,6 +17,9 @@ import ( "sort" "strconv" "strings" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" ) const ( @@ -380,6 +384,10 @@ func verifyLocalPythonConsumer(recordsBySuffix map[string]wheelRecord) error { if err != nil { return err } + return verifyInstalledPythonWheel(consumer, venvPython, wheelPath) +} + +func verifyInstalledPythonWheel(consumer string, venvPython string, wheelPath string) error { if output, err := runCommand("", venvPython, "-m", "pip", "install", "--no-index", wheelPath); err != nil { return fmt.Errorf("install local Python wheel: %w\n%s", err, output) } @@ -401,9 +409,338 @@ func verifyLocalPythonConsumer(recordsBySuffix map[string]wheelRecord) error { if !bytes.Contains(output, []byte("CLI/JSON is the public cross-language contract")) { return fmt.Errorf("python console script smoke did not expose CLI contract") } + return verifyInstalledPythonPresetContinuation(consumer, venvPython) +} + +func verifyInstalledPythonPresetContinuation(consumer string, venvPython string) error { + emptyPath := filepath.Join(consumer, "empty-path") + if err := os.Mkdir(emptyPath, 0o700); err != nil && !os.IsExist(err) { + return fmt.Errorf("create npm-free PATH: %w", err) + } + environment := environmentWithOverrides(os.Environ(), map[string]string{ + "PATH": emptyPath, + cliexec.LauncherProfileEnvironment: cliexec.ProfilePath, + cliexec.PythonExecutableEnvironment: filepath.Join(consumer, "wrong-python"), + }) + executableOutput, err := runCommandWithEnvironment("", environment, venvPython, "-c", "import sys; print(sys.executable)") + if err != nil { + return fmt.Errorf("resolve installed Python wheel interpreter identity: %w\n%s", err, executableOutput) + } + pythonExecutable := strings.TrimSpace(string(executableOutput)) + if pythonExecutable == "" || !filepath.IsAbs(pythonExecutable) || + string(executableOutput) != pythonExecutable+"\n" { + return fmt.Errorf("installed Python wheel interpreter identity must be one absolute newline-terminated path, got %q", executableOutput) + } + output, err := runCommandWithEnvironment("", environment, pythonExecutable, "-m", "agentic_proofkit", "stack-preset", "--preset", "agentic_runtime_repo") + if err != nil { + return fmt.Errorf("installed Python wheel stack preset failed with npm absent from PATH: %w\n%s", err, output) + } + commands, err := installedPythonPresetSuggestedCommands(output, "agentic_runtime_repo") + if err != nil { + return err + } + renderer, err := cliexec.AdmitLauncherProfile(cliexec.ProfilePythonModule, pythonExecutable) + if err != nil { + return fmt.Errorf("construct expected Python module renderer: %w", err) + } + prefix := renderer.DisplayCommand() + " " + for index, command := range commands { + if !strings.HasPrefix(command, prefix) { + return fmt.Errorf("installed Python wheel suggestedCommands[%d]=%q must use exact interpreter module prefix %q", index, command, prefix) + } + } + expectedSelfContinuation := renderer.DisplayCommand("stack-preset", "--preset", "agentic_runtime_repo") + if commands[0] != expectedSelfContinuation { + return fmt.Errorf("installed Python wheel first suggested command must be exact self-continuation %q", expectedSelfContinuation) + } + output, err = runArgvWithEnvironment(consumer, environment, renderer.Argv("stack-preset", "--preset", "agentic_runtime_repo")) + if err != nil { + return fmt.Errorf("installed Python wheel self-continuation failed with npm absent from PATH: %w\n%s", err, output) + } + if err := requirePythonPassedJSON(output, "installed Python wheel self-continuation"); err != nil { + return err + } + return verifyInstalledPythonHelpAndAgentRouteContinuity(consumer, environment, renderer) +} + +func verifyInstalledPythonHelpAndAgentRouteContinuity(consumer string, environment []string, renderer cliexec.Renderer) error { + rootHelp, err := runArgvWithEnvironment(consumer, environment, renderer.Argv("help")) + if err != nil { + return fmt.Errorf("installed Python wheel root help route failed: %w\n%s", err, rootHelp) + } + familiesCommand := renderer.DisplayCommand("help", "families") + if !strings.Contains(string(rootHelp), "Discover command families:\n "+familiesCommand+"\n") { + return fmt.Errorf("installed Python wheel root help does not expose exact family route %q", familiesCommand) + } + familiesHelp, err := runArgvWithEnvironment(consumer, environment, renderer.Argv("help", "families")) + if err != nil { + return fmt.Errorf("installed Python wheel family discovery route failed: %w\n%s", err, familiesHelp) + } + familyPrefix := renderer.DisplayCommand() + " help family " + familyIDs, err := exactDisplayedRouteOperands(familiesHelp, familyPrefix, "installed Python wheel family routes") + if err != nil { + return err + } + leafPrefix := renderer.DisplayCommand() + " help " + leafRouteCount := 0 + for _, familyID := range familyIDs { + familyRoute := renderer.DisplayCommand("help", "family", familyID) + familyHelp, err := runArgvWithEnvironment(consumer, environment, renderer.Argv("help", "family", familyID)) + if err != nil { + return fmt.Errorf("installed Python wheel family route %q failed: %w\n%s", familyRoute, err, familyHelp) + } + leafCommands, err := exactDisplayedRouteOperands(familyHelp, leafPrefix, "installed Python wheel leaf routes") + if err != nil { + return err + } + leafRouteCount += len(leafCommands) + for _, leafCommand := range leafCommands { + leafRoute := renderer.DisplayCommand("help", leafCommand) + if _, err := runArgvWithEnvironment(consumer, environment, renderer.Argv("help", leafCommand)); err != nil { + return fmt.Errorf("installed Python wheel leaf help route %q failed: %w", leafRoute, err) + } + } + } + if leafRouteCount == 0 { + return fmt.Errorf("installed Python wheel help chain exposed no leaf routes") + } + + requirementSourceRef := "requirements.v1.json" + requirementSourcePath := filepath.Join(consumer, requirementSourceRef) + requirementSource := map[string]any{ + "schemaVersion": 1, + "sourceId": "proofkit.python.consumer.requirements", + "specPackagePath": "docs/specs/python-consumer", + "overviewPath": "docs/specs/python-consumer/overview.md", + "requirementsPath": "docs/specs/python-consumer/requirements.v1.json", + "nonClaims": []string{"Installed Python consumer fixture does not execute native witnesses."}, + "requirements": []any{map[string]any{ + "claimLevel": "blocking", + "deferral": nil, + "invariant": "Installed Python consumer routes preserve the active launcher.", + "lifecycle": map[string]any{"evidenceRefs": []any{}, "replacementRequirementIds": []any{}, "state": "active"}, + "nonClaimRefs": []any{}, + "nonClaims": []string{"Installed Python consumer route does not prove publication."}, + "ownerId": "proofkit.python.consumer", + "proofBindingRefs": []string{"proofkit/requirement-bindings.json"}, + "requirementId": "REQ-PROOFKIT-PYTHON-CONSUMER-001", + "riskClass": "medium", + "updatePolicy": map[string]any{ + "requiresImpactDeclaration": true, + "requiresProofBindingReview": true, + "reviewOwnerId": "proofkit.python.consumer", + }, + }}, + } + if err := writeJSONFixture(requirementSourcePath, requirementSource); err != nil { + return err + } + agentRoutePath := filepath.Join(consumer, "agent-route.json") + agentRoute := map[string]any{ + "schemaVersion": 1, + "routeId": "proofkit.python.consumer.route", + "goal": "validate_requirement_source", + "mode": "observe", + "availableInputs": []any{ + map[string]any{"kind": "requirement_source", "ref": requirementSourceRef}, + }, + } + if err := writeJSONFixture(agentRoutePath, agentRoute); err != nil { + return err + } + routeOutput, err := runArgvWithEnvironment(consumer, environment, renderer.Argv("agent-route", "--input", agentRoutePath)) + if err != nil { + return fmt.Errorf("installed Python wheel agent route failed: %w\n%s", err, routeOutput) + } + value, err := admission.DecodeJSON(bytes.NewReader(routeOutput), 8<<20) + if err != nil { + return fmt.Errorf("installed Python wheel agent route output must be one JSON value: %w", err) + } + record, ok := value.(map[string]any) + if !ok { + return fmt.Errorf("installed Python wheel agent route output must be an object") + } + rawCommands, ok := record["nextCommands"].([]any) + if !ok || len(rawCommands) != 1 { + return fmt.Errorf("installed Python wheel agent route must expose exactly one next command") + } + command, ok := rawCommands[0].(map[string]any) + if !ok { + return fmt.Errorf("installed Python wheel next command must be an object") + } + argv, err := exactStringArray(command["argv"], "installed Python wheel next command argv") + if err != nil { + return err + } + prefix := renderer.Argv() + if len(argv) < len(prefix) || !equalStrings(argv[:len(prefix)], prefix) { + return fmt.Errorf("installed Python wheel next command argv does not use the active launcher prefix") + } + commandOutput, err := runArgvWithEnvironment(consumer, environment, argv) + if err != nil { + return fmt.Errorf("installed Python wheel emitted agent-route argv failed: %w\n%s", err, commandOutput) + } + return requirePythonPassedJSON(commandOutput, "installed Python wheel emitted agent-route argv") +} + +func exactDisplayedRouteOperands(output []byte, prefix string, context string) ([]string, error) { + operands := []string{} + seen := map[string]struct{}{} + for _, line := range strings.Split(strings.TrimSuffix(string(output), "\n"), "\n") { + route, admitted := strings.CutPrefix(line, " ") + if !admitted { + continue + } + if !strings.HasPrefix(route, prefix) { + continue + } + operand := strings.TrimPrefix(route, prefix) + if !isCommandRouteOperand(operand) || route != prefix+operand { + return nil, fmt.Errorf("%s contain non-canonical route %q", context, route) + } + if _, duplicate := seen[operand]; duplicate { + return nil, fmt.Errorf("%s contain duplicate route operand %q", context, operand) + } + seen[operand] = struct{}{} + operands = append(operands, operand) + } + if len(operands) == 0 { + return nil, fmt.Errorf("%s are empty", context) + } + sort.Strings(operands) + return operands, nil +} + +func isCommandRouteOperand(value string) bool { + if value == "" { + return false + } + for _, character := range value { + if (character < 'a' || character > 'z') && (character < '0' || character > '9') && character != '-' { + return false + } + } + return true +} + +func writeJSONFixture(path string, value any) error { + content, err := json.Marshal(value) + if err != nil { + return fmt.Errorf("marshal installed Python consumer fixture: %w", err) + } + if err := os.WriteFile(path, append(content, '\n'), 0o600); err != nil { + return fmt.Errorf("write installed Python consumer fixture: %w", err) + } + return nil +} + +func exactStringArray(raw any, context string) ([]string, error) { + values, ok := raw.([]any) + if !ok || len(values) == 0 { + return nil, fmt.Errorf("%s must be a non-empty array", context) + } + result := make([]string, 0, len(values)) + for _, rawValue := range values { + value, ok := rawValue.(string) + if !ok || value == "" { + return nil, fmt.Errorf("%s must contain non-empty strings", context) + } + result = append(result, value) + } + return result, nil +} + +func equalStrings(left []string, right []string) bool { + if len(left) != len(right) { + return false + } + for index := range left { + if left[index] != right[index] { + return false + } + } + return true +} + +func installedPythonPresetSuggestedCommands(output []byte, presetID string) ([]string, error) { + value, err := admission.DecodeJSON(bytes.NewReader(output), 8<<20) + if err != nil { + return nil, fmt.Errorf("installed Python wheel stack preset %s stdout must be one JSON value: %w", presetID, err) + } + record, ok := value.(map[string]any) + if !ok || record["state"] != "passed" { + return nil, fmt.Errorf("installed Python wheel stack preset %s state is not passed", presetID) + } + diagnostics, ok := record["diagnostics"].([]any) + if !ok { + return nil, fmt.Errorf("installed Python wheel stack preset %s output must expose diagnostics", presetID) + } + var presetValue map[string]any + presetDiagnosticCount := 0 + for _, rawDiagnostic := range diagnostics { + diagnostic, ok := rawDiagnostic.(map[string]any) + if !ok { + return nil, fmt.Errorf("installed Python wheel stack preset %s diagnostics must contain objects", presetID) + } + if diagnostic["key"] != "preset" { + continue + } + presetDiagnosticCount++ + presetValue, ok = diagnostic["value"].(map[string]any) + if !ok { + return nil, fmt.Errorf("installed Python wheel stack preset %s preset diagnostic must contain an object value", presetID) + } + } + if presetDiagnosticCount != 1 { + return nil, fmt.Errorf("installed Python wheel stack preset %s must expose exactly one preset diagnostic", presetID) + } + rawCommands, ok := presetValue["suggestedCommands"].([]any) + if !ok || len(rawCommands) == 0 { + return nil, fmt.Errorf("installed Python wheel stack preset %s must expose non-empty suggestedCommands", presetID) + } + commands := make([]string, 0, len(rawCommands)) + for index, rawCommand := range rawCommands { + command, ok := rawCommand.(string) + if !ok || command == "" { + return nil, fmt.Errorf("installed Python wheel stack preset %s suggestedCommands[%d] must be a non-empty string", presetID, index) + } + commands = append(commands, command) + } + return commands, nil +} + +func requirePythonPassedJSON(output []byte, label string) error { + value, err := admission.DecodeJSON(bytes.NewReader(output), 8<<20) + if err != nil { + return fmt.Errorf("%s stdout must be one JSON value: %w", label, err) + } + record, ok := value.(map[string]any) + if !ok || record["state"] != "passed" { + return fmt.Errorf("%s state is not passed", label) + } return nil } +func environmentWithOverrides(environment []string, overrides map[string]string) []string { + result := make([]string, 0, len(environment)+len(overrides)) + for _, item := range environment { + name, _, ok := strings.Cut(item, "=") + if _, replaced := overrides[name]; ok && replaced { + continue + } + result = append(result, item) + } + names := make([]string, 0, len(overrides)) + for name := range overrides { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + result = append(result, name+"="+overrides[name]) + } + return result +} + func readZipFile(file *zip.File) ([]byte, error) { reader, err := file.Open() if err != nil { @@ -421,6 +758,22 @@ func runCommand(dir string, name string, args ...string) ([]byte, error) { return command.CombinedOutput() } +func runCommandWithEnvironment(dir string, environment []string, name string, args ...string) ([]byte, error) { + command := exec.Command(name, args...) + command.Env = environment + if dir != "" { + command.Dir = dir + } + return command.CombinedOutput() +} + +func runArgvWithEnvironment(dir string, environment []string, argv []string) ([]byte, error) { + if len(argv) == 0 { + return nil, fmt.Errorf("run argv requires at least one token") + } + return runCommandWithEnvironment(dir, environment, argv[0], argv[1:]...) +} + func contains(values []string, candidate string) bool { for _, value := range values { if value == candidate { diff --git a/internal/tools/releasechange/record.go b/internal/tools/releasechange/record.go index 0b3c302..a3b954e 100644 --- a/internal/tools/releasechange/record.go +++ b/internal/tools/releasechange/record.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "os" + "strconv" "strings" "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" @@ -11,7 +12,7 @@ import ( ) const ( - RecordPath = "release/change-record.v1.json" + RecordPath = "release/change-record.v2.json" maxRecordBytes = 1 << 20 maxListItems = 128 maxEntryTextByte = 4096 @@ -20,9 +21,11 @@ const ( type Record struct { Additions []Change BreakingChanges []Change + ChangeClass string KnownLimitations []string Migration Migration PlatformRequirements []string + PreviousVersion string RollbackStrategy string SchemaVersion int Version string @@ -55,16 +58,35 @@ func Admit(raw any) (Record, error) { if !ok { return Record{}, fmt.Errorf("release change record must be an object") } - if err := admit.KnownKeys(record, []string{"additions", "breakingChanges", "knownLimitations", "migration", "platformRequirements", "rollback", "schemaVersion", "version"}, "release change record"); err != nil { + if err := admit.KnownKeys(record, []string{"additions", "breakingChanges", "changeClass", "knownLimitations", "migration", "platformRequirements", "previousVersion", "rollback", "schemaVersion", "version"}, "release change record"); err != nil { return Record{}, err } - if !admit.JSONNumberEquals(record["schemaVersion"], 1) { - return Record{}, fmt.Errorf("release change record schemaVersion must be 1") + if !admit.JSONNumberEquals(record["schemaVersion"], 2) { + return Record{}, fmt.Errorf("release change record schemaVersion must be 2") } version, err := boundedText(record["version"], "release change record version") if err != nil { return Record{}, err } + previousVersion, err := boundedText(record["previousVersion"], "release change record previousVersion") + if err != nil { + return Record{}, err + } + currentSemVer, err := parseCanonicalSemVer(version) + if err != nil { + return Record{}, fmt.Errorf("release change record version: %w", err) + } + previousSemVer, err := parseCanonicalSemVer(previousVersion) + if err != nil { + return Record{}, fmt.Errorf("release change record previousVersion: %w", err) + } + if compareSemVer(currentSemVer, previousSemVer) <= 0 { + return Record{}, fmt.Errorf("release change record version must be greater than previousVersion") + } + changeClass, err := admit.Enum(record["changeClass"], map[string]struct{}{"breaking": {}, "compatible": {}}, "release change record changeClass") + if err != nil { + return Record{}, err + } breaking, err := admitChanges(record["breakingChanges"], "release change record breakingChanges") if err != nil { return Record{}, err @@ -80,6 +102,19 @@ func Admit(raw any) (Record, error) { if err != nil { return Record{}, err } + requiredClass := "compatible" + if len(breaking) > 0 || migration.Required { + requiredClass = "breaking" + } + if changeClass != requiredClass { + return Record{}, fmt.Errorf("release change record changeClass does not match breakingChanges and migration") + } + if changeClass == "breaking" && !isBreakingBump(previousSemVer, currentSemVer) { + return Record{}, fmt.Errorf("release change record breaking change requires a SemVer major bump or a pre-1.0 minor bump") + } + if changeClass == "compatible" && currentSemVer.Major != previousSemVer.Major { + return Record{}, fmt.Errorf("release change record compatible change must not change the SemVer major version") + } platforms, err := orderedUniqueText(record["platformRequirements"], "release change record platformRequirements", true) if err != nil { return Record{}, err @@ -103,9 +138,9 @@ func Admit(raw any) (Record, error) { return Record{}, fmt.Errorf("release change record rollback strategy must be previous_admitted_version") } return Record{ - Additions: additions, BreakingChanges: breaking, KnownLimitations: limitations, + Additions: additions, BreakingChanges: breaking, ChangeClass: changeClass, KnownLimitations: limitations, Migration: migration, PlatformRequirements: platforms, RollbackStrategy: strategy, - SchemaVersion: 1, Version: version, + PreviousVersion: previousVersion, SchemaVersion: 2, Version: version, }, nil } @@ -139,8 +174,11 @@ func RenderMarkdown(record Record, npmPackage, pythonPackage string, pypiPublish lines = appendTextList(lines, record.KnownLimitations) lines = append(lines, "", "## Install", "", "Primary npm channel:", "", "```bash", - fmt.Sprintf("npm install -D %s@%s", npmPackage, record.Version), "```", + fmt.Sprintf("npm install --save-dev --save-exact %s@%s", npmPackage, record.Version), "```", ) + if current, err := parseCanonicalSemVer(record.Version); err == nil && current.Major == 0 { + lines = append(lines, "", "Pre-1.0 npm consumers must keep this dependency exact-pinned.") + } if pypiPublished { lines = append(lines, "", "Python/uv channel:", "", "```bash", @@ -152,15 +190,67 @@ func RenderMarkdown(record Record, npmPackage, pythonPackage string, pypiPublish lines = append(lines, "", "GitHub Release assets and checksums are archive and provenance evidence, not package-manager dependency authority.", "", "## Rollback", "", - fmt.Sprintf("- Pin npm consumers to the previous admitted version with `npm install -D %s@`.", npmPackage), + fmt.Sprintf("- Pin npm consumers to the previous admitted version %s with `npm install --save-dev --save-exact %s@%s`.", record.PreviousVersion, npmPackage, record.PreviousVersion), ) if pypiPublished { - lines = append(lines, fmt.Sprintf("- Pin Python/uv consumers with `uv tool install %s==`.", pythonPackage)) + lines = append(lines, fmt.Sprintf("- Pin Python/uv consumers with `uv tool install %s==%s`.", pythonPackage, record.PreviousVersion)) } lines = append(lines, "- Treat local package artifacts as candidates until registry identity is proven.") return strings.Join(lines, "\n") + "\n" } +type semVer struct { + Major uint64 + Minor uint64 + Patch uint64 +} + +func parseCanonicalSemVer(value string) (semVer, error) { + parts := strings.Split(value, ".") + if len(parts) != 3 { + return semVer{}, fmt.Errorf("must be canonical SemVer") + } + numbers := [3]uint64{} + for index, part := range parts { + if part == "" || (len(part) > 1 && part[0] == '0') { + return semVer{}, fmt.Errorf("must be canonical SemVer") + } + for _, char := range part { + if char < '0' || char > '9' { + return semVer{}, fmt.Errorf("must be canonical SemVer") + } + } + number, err := strconv.ParseUint(part, 10, 64) + if err != nil { + return semVer{}, fmt.Errorf("must be canonical SemVer") + } + numbers[index] = number + } + return semVer{Major: numbers[0], Minor: numbers[1], Patch: numbers[2]}, nil +} + +func compareSemVer(left, right semVer) int { + for _, pair := range [][2]uint64{{left.Major, right.Major}, {left.Minor, right.Minor}, {left.Patch, right.Patch}} { + if pair[0] < pair[1] { + return -1 + } + if pair[0] > pair[1] { + return 1 + } + } + return 0 +} + +func isBreakingBump(previous, current semVer) bool { + if current.Major > previous.Major { + return true + } + if previous.Major == 0 && current.Major == 0 && current.Minor > previous.Minor { + return true + } + return false +} + func admitChanges(raw any, context string) ([]Change, error) { values, ok := raw.([]any) if !ok || len(values) > maxListItems { diff --git a/internal/tools/releasechange/record_test.go b/internal/tools/releasechange/record_test.go index 82a4053..d8cb4ee 100644 --- a/internal/tools/releasechange/record_test.go +++ b/internal/tools/releasechange/record_test.go @@ -2,7 +2,10 @@ package releasechange import ( "encoding/json" + "fmt" "os" + "path/filepath" + "slices" "strings" "testing" ) @@ -12,15 +15,17 @@ func TestAdmitAndRenderVersionBoundChangeRecord(t *testing.T) { if err != nil { t.Fatal(err) } - if err := RequireVersion(record, "1.2.3"); err != nil { + if err := RequireVersion(record, "2.0.0"); err != nil { t.Fatal(err) } notes := RenderMarkdown(record, "@research-engineering/agentic-proofkit", "agentic-proofkit", true) for _, expected := range []string{ "## Breaking Contract Changes", "## Additions", "## Migration", "Migration is required:", "## Platform Requirements", "## Known Limitations", "## Install", "## Rollback", - "npm install -D @research-engineering/agentic-proofkit@1.2.3", - "uv tool install agentic-proofkit==1.2.3", + "npm install --save-dev --save-exact @research-engineering/agentic-proofkit@2.0.0", + "uv tool install agentic-proofkit==2.0.0", + "previous admitted version 1.2.2", + "`npm install --save-dev --save-exact @research-engineering/agentic-proofkit@1.2.2`", "`proofkit.contract.breaking`: Remove the inert field.", } { if !strings.Contains(notes, expected) { @@ -29,6 +34,322 @@ func TestAdmitAndRenderVersionBoundChangeRecord(t *testing.T) { } } +func TestAdmitEnforcesVersionedChangeClass(t *testing.T) { + tests := []struct { + name string + previous string + version string + changeClass string + breaking bool + wantErrorText string + }{ + {name: "breaking patch is rejected", previous: "0.1.159", version: "0.1.160", changeClass: "breaking", breaking: true, wantErrorText: "breaking change requires"}, + {name: "breaking zero-major minor is admitted", previous: "0.1.160", version: "0.2.0", changeClass: "breaking", breaking: true}, + {name: "breaking zero-major minor with patch is admitted", previous: "0.1.3", version: "0.2.1", changeClass: "breaking", breaking: true}, + {name: "breaking stable major with minor is admitted", previous: "1.2.3", version: "2.1.0", changeClass: "breaking", breaking: true}, + {name: "breaking stable patch is rejected", previous: "1.2.3", version: "1.2.4", changeClass: "breaking", breaking: true, wantErrorText: "breaking change requires"}, + {name: "compatible minor is admitted", previous: "1.2.3", version: "1.3.0", changeClass: "compatible"}, + {name: "nonmonotonic equal is rejected", previous: "1.2.3", version: "1.2.3", changeClass: "compatible", wantErrorText: "greater than previousVersion"}, + {name: "nonmonotonic lower is rejected", previous: "1.2.4", version: "1.2.3", changeClass: "compatible", wantErrorText: "greater than previousVersion"}, + {name: "breaking declaration without breaking facts is rejected", previous: "1.2.3", version: "2.0.0", changeClass: "breaking", wantErrorText: "changeClass does not match"}, + {name: "compatible declaration with migration is rejected", previous: "1.2.3", version: "1.3.0", changeClass: "compatible", breaking: true, wantErrorText: "changeClass does not match"}, + {name: "leading zero is rejected", previous: "1.2.3", version: "1.03.0", changeClass: "compatible", wantErrorText: "canonical SemVer"}, + } + for _, item := range tests { + t.Run(item.name, func(t *testing.T) { + record := validRecord() + record["previousVersion"] = item.previous + record["version"] = item.version + record["changeClass"] = item.changeClass + if !item.breaking { + record["breakingChanges"] = []any{} + record["migration"] = map[string]any{"required": false, "steps": []any{}} + } + _, err := Admit(record) + if item.wantErrorText == "" { + if err != nil { + t.Fatalf("Admit() error = %v", err) + } + return + } + if err == nil || !strings.Contains(err.Error(), item.wantErrorText) { + t.Fatalf("Admit() error = %v, want %q", err, item.wantErrorText) + } + }) + } +} + +func TestRenderStatesPreOneExactPinPolicy(t *testing.T) { + value := validRecord() + value["previousVersion"] = "0.1.160" + value["version"] = "0.2.0" + record, err := Admit(value) + if err != nil { + t.Fatal(err) + } + notes := RenderMarkdown(record, "@research-engineering/agentic-proofkit", "agentic-proofkit", false) + for _, expected := range []string{ + "npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.2.0", + "Pre-1.0 npm consumers must keep this dependency exact-pinned.", + "`npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.1.160`", + } { + if !strings.Contains(notes, expected) { + t.Fatalf("rendered notes missing %q:\n%s", expected, notes) + } + } +} + +func TestCurrentChangeRecordNamesReviewedSemanticChanges(t *testing.T) { + record, err := Read(filepath.Join("..", "..", "..", RecordPath)) + if err != nil { + t.Fatal(err) + } + notes := RenderMarkdown(record, "@research-engineering/agentic-proofkit", "agentic-proofkit", false) + if err := validateCurrentChangeRecord(record, notes); err != nil { + t.Fatal(err) + } + + for index := range record.BreakingChanges { + mutant := cloneReleaseChangeRecord(record) + mutant.BreakingChanges = deleteReleaseChangeAt(mutant.BreakingChanges, index) + assertCurrentChangeRecordRejected(t, "missing breaking "+record.BreakingChanges[index].ChangeID, mutant) + mutant = cloneReleaseChangeRecord(record) + mutant.BreakingChanges[index].ChangeID += ".drift" + assertCurrentChangeRecordRejected(t, "substituted breaking id "+record.BreakingChanges[index].ChangeID, mutant) + mutant = cloneReleaseChangeRecord(record) + mutant.BreakingChanges[index].Summary += " Drift." + assertCurrentChangeRecordRejected(t, "substituted breaking summary "+record.BreakingChanges[index].ChangeID, mutant) + } + for index := range record.Additions { + mutant := cloneReleaseChangeRecord(record) + mutant.Additions = deleteReleaseChangeAt(mutant.Additions, index) + assertCurrentChangeRecordRejected(t, "missing addition "+record.Additions[index].ChangeID, mutant) + mutant = cloneReleaseChangeRecord(record) + mutant.Additions[index].ChangeID += ".drift" + assertCurrentChangeRecordRejected(t, "substituted addition id "+record.Additions[index].ChangeID, mutant) + mutant = cloneReleaseChangeRecord(record) + mutant.Additions[index].Summary += " Drift." + assertCurrentChangeRecordRejected(t, "substituted addition summary "+record.Additions[index].ChangeID, mutant) + } + for index := range record.Migration.Steps { + mutant := cloneReleaseChangeRecord(record) + mutant.Migration.Steps = deleteReleaseChangeAt(mutant.Migration.Steps, index) + assertCurrentChangeRecordRejected(t, fmt.Sprintf("missing migration %d", index), mutant) + mutant = cloneReleaseChangeRecord(record) + mutant.Migration.Steps[index] += " Drift." + assertCurrentChangeRecordRejected(t, fmt.Sprintf("substituted migration %d", index), mutant) + } + for index := 0; index+1 < len(record.BreakingChanges); index++ { + mutant := cloneReleaseChangeRecord(record) + mutant.BreakingChanges[index], mutant.BreakingChanges[index+1] = mutant.BreakingChanges[index+1], mutant.BreakingChanges[index] + assertCurrentChangeRecordRejected(t, fmt.Sprintf("reordered breaking %d", index), mutant) + } + for index := 0; index+1 < len(record.Additions); index++ { + mutant := cloneReleaseChangeRecord(record) + mutant.Additions[index], mutant.Additions[index+1] = mutant.Additions[index+1], mutant.Additions[index] + assertCurrentChangeRecordRejected(t, fmt.Sprintf("reordered addition %d", index), mutant) + } + for index := 0; index+1 < len(record.Migration.Steps); index++ { + mutant := cloneReleaseChangeRecord(record) + mutant.Migration.Steps[index], mutant.Migration.Steps[index+1] = mutant.Migration.Steps[index+1], mutant.Migration.Steps[index] + assertCurrentChangeRecordRejected(t, fmt.Sprintf("reordered migration %d", index), mutant) + } + breakingSurplus := cloneReleaseChangeRecord(record) + breakingSurplus.BreakingChanges = append(breakingSurplus.BreakingChanges, Change{ChangeID: "proofkit.surplus.breaking", Summary: "Surplus breaking change."}) + assertCurrentChangeRecordRejected(t, "surplus breaking", breakingSurplus) + additionSurplus := cloneReleaseChangeRecord(record) + additionSurplus.Additions = append(additionSurplus.Additions, Change{ChangeID: "proofkit.surplus.addition", Summary: "Surplus addition."}) + assertCurrentChangeRecordRejected(t, "surplus addition", additionSurplus) + migrationSurplus := cloneReleaseChangeRecord(record) + migrationSurplus.Migration.Steps = append(migrationSurplus.Migration.Steps, "Surplus migration step.") + assertCurrentChangeRecordRejected(t, "surplus migration", migrationSurplus) + + for _, projected := range currentChangeRecordProjectionStrings() { + mutantNotes := strings.Replace(notes, projected, "projection drift", 1) + if err := validateCurrentChangeRecord(record, mutantNotes); err == nil { + t.Fatalf("current release projection admitted missing %q", projected) + } + } + breakingReorderedNotes := swapAdjacentNoteItems(notes, currentChangeBullet(currentBreakingChanges[0]), currentChangeBullet(currentBreakingChanges[1])) + assertCurrentChangeRecordNotesRejected(t, "reordered breaking note", record, breakingReorderedNotes) + additionReorderedNotes := swapAdjacentNoteItems(notes, currentChangeBullet(currentAdditions[0]), currentChangeBullet(currentAdditions[1])) + assertCurrentChangeRecordNotesRejected(t, "reordered addition note", record, additionReorderedNotes) + migrationReorderedNotes := swapAdjacentNoteItems(notes, "- "+currentMigrationSteps[0], "- "+currentMigrationSteps[1]) + assertCurrentChangeRecordNotesRejected(t, "reordered migration note", record, migrationReorderedNotes) + relocatedNotes := strings.Replace(notes, currentChangeBullet(currentBreakingChanges[0])+"\n", "", 1) + relocatedNotes = strings.Replace(relocatedNotes, "## Additions\n\n", "## Additions\n\n"+currentChangeBullet(currentBreakingChanges[0])+"\n", 1) + assertCurrentChangeRecordNotesRejected(t, "breaking note relocated to additions", record, relocatedNotes) + surplusNotes := strings.Replace(notes, "\n\n## Additions", "\n- `proofkit.surplus.note`: Surplus note.\n\n## Additions", 1) + assertCurrentChangeRecordNotesRejected(t, "surplus breaking note", record, surplusNotes) + assertCurrentChangeRecordNotesRejected(t, "appended surplus change note", record, notes+"- `proofkit.surplus.appended`: Appended surplus note.\n") + assertCurrentChangeRecordNotesRejected(t, "appended duplicate change note", record, notes+currentChangeBullet(currentBreakingChanges[0])+"\n") + assertCurrentChangeRecordNotesRejected(t, "appended duplicate change section", record, notes+"## Breaking Contract Changes\n\n- `proofkit.surplus.section`: Surplus section.\n") +} + +var currentBreakingChanges = []Change{ + {ChangeID: "proofkit.adoption-doctor.advisory-rule-status", Summary: "Non-enforced adoption-doctor advisory gap rules now report skipped instead of passed, including observe-mode rules and gaps outside an enforce-touched selection; these gaps do not change the top-level outcome."}, + {ChangeID: "proofkit.adoption-doctor.blocked-prerequisites", Summary: "Adoption doctor now reports unresolved external prerequisites as blocked with exit code 1 in every adoption mode; observe and warn no longer relax them."}, + {ChangeID: "proofkit.browser.native-list-keyboard-contract", Summary: "Requirement browser navigation now uses native list and button semantics; the removed synthetic tree no longer provides ArrowUp or ArrowDown roving focus."}, + {ChangeID: "proofkit.cli.adoption-contract-single-value-flags", Summary: "Adoption contract envelope now rejects repeated --mode or --pilot flags and an explicitly empty --pilot value instead of changing or misreporting the selected root-shape variant."}, + {ChangeID: "proofkit.cli.invalid-input-channels", Summary: "Malformed ordinary command input now uses stderr while explicit agent envelopes retain machine-readable invalid-input output."}, + {ChangeID: "proofkit.cli.pilot-admission-single-value-selector", Summary: "Pilot admission now rejects repeated or mixed --pilot and --stack-diverse selectors instead of applying last-write-wins routing; the single --stack-diverse alias remains supported with direct or contract-envelope input."}, + {ChangeID: "proofkit.context.digest-coverage-v2", Summary: "Requirement context, diff, graph, and browser workspace contracts advance to version 2 with expectedDigestCoverage vocabulary."}, + {ChangeID: "proofkit.launcher.python-executable-format-controls", Summary: "Python-module launcher admission now rejects Unicode format characters as well as control characters in the absolute executable path before rendering display commands."}, + {ChangeID: "proofkit.onboarding.generated-command-invocation", Summary: "Proofkit-owned generated display commands and structured argv now use one explicit installed launcher channel across help, preset, bootstrap, project, route, workflow, and coverage surfaces: offline npm exec for npm consumers and the active absolute Python interpreter module route for wheel consumers; direct binary consumers retain caller-owned PATH resolution."}, + {ChangeID: "proofkit.package.installed-governance-routes", Summary: "The npm artifact no longer ships AGENTS.md or CONTRIBUTING.md; governance and contribution routes remain source-checkout-only."}, + {ChangeID: "proofkit.readiness-closeout.character-reference-policy", Summary: "Readiness closeout now decodes one strict semicolon-terminated CommonMark or HTML character reference pass before policy phrase matching; text that previously hid a forbidden phrase through one such reference now fails closed."}, + {ChangeID: "proofkit.typescript.absolute-symlink", Summary: "The TypeScript public API scanner rejects absolute symlink targets and requires confined relative in-root links."}, +} + +var currentAdditions = []Change{ + {ChangeID: "proofkit.browser.accessibility-state-matrix", Summary: "The requirement browser adds deterministic loading and failure states, native list semantics, bounded reflow, target-size, and contrast witnesses."}, + {ChangeID: "proofkit.cli.contract-closure", Summary: "The authored CLI contract now closes input and output compatibility projections, admits canonical machine-disjoint CLI variant condition models, and generates private runtime metadata."}, + {ChangeID: "proofkit.onboarding.installed-artifact-trace", Summary: "Canonical onboarding uses exact npm pins, copyable offline npm exec routes at every displayed help transition, exact preset commands, and a displayed installed-README first-input continuation."}, + {ChangeID: "proofkit.package.public-reference-closure", Summary: "The npm package excludes contributor-only governance files and closes package-public Markdown and machine references over shipped entries or explicit source-checkout evidence fields."}, + {ChangeID: "proofkit.pilot-admission.all-envelope", Summary: "Pilot admission contract envelopes now admit the existing all mode as one strict two-input envelope and return the ordered first and stack-diverse pilot reports."}, + {ChangeID: "proofkit.requirement-bindings.witness-selectors", Summary: "Requirement binding admission and output now preserve optional witnessSelectors records with exact selector and command fields."}, + {ChangeID: "proofkit.requirement-output.confined-atomic-publication", Summary: "Requirement view output now uses repository-confined same-parent atomic replacement after final destination-parent plus temporary-object identity, mode, and content admission."}, + {ChangeID: "proofkit.security-workflows.permission-separation", Summary: "Security workflow source contracts now keep advisory CodeQL, OSV, and Scorecard analysis read-only, isolate provider publication authority, and require exact Scorecard public-output inputs."}, + {ChangeID: "proofkit.release.artifact-honest-sbom", Summary: "CycloneDX runtime edges are emitted only from content-bound per-binary build information while source module inventory is excluded."}, + {ChangeID: "proofkit.release.existing-release-immutability", Summary: "Existing release verification is read-only and fails closed instead of uploading or backfilling missing assets."}, + {ChangeID: "proofkit.release.workflow-action-pins", Summary: "Every external GitHub Actions use is guarded by a full commit-SHA source oracle."}, +} + +var currentMigrationSteps = []string{ + "Update adoption-doctor consumers that inspect non-enforced advisory rule results to accept skipped instead of passed in observe mode and outside an enforce-touched selection; these gaps leave the top-level outcome unchanged.", + "Update adoption-doctor consumers to treat unresolved external prerequisites as blocked with a nonzero exit in every adoption mode; only advisory gaps remain mode-relaxable.", + "Update requirement-browser keyboard automation to use standard Tab and Shift+Tab traversal plus Enter or Space activation instead of synthetic ArrowUp or ArrowDown tree navigation.", + "Pass --mode and --pilot at most once to adoption-contract-envelope, provide a non-empty --pilot value, and split repeated-flag invocations into one unambiguous invocation.", + "Pass at most one of --pilot or --stack-diverse to pilot-admission; omit both to retain the default first pilot, and split repeated or mixed-selector invocations into one unambiguous invocation.", + "Update context, semantic-diff, graph, and workspace consumers to schemaVersion 2 and replace baselineVerification with expectedDigestCoverage.", + "Remove Unicode control or format characters from the absolute Python executable path supplied by the Python-module launcher profile.", + "Update consumers that compare, execute, or persist Proofkit-generated display commands or structured argv to accept channel-specific installed invocation prefixes across help, preset, bootstrap, project, route, workflow, and coverage surfaces; do not rewrite caller-owned bootstrap command text.", + "Update readiness-closeout consumers and fixtures to treat one strict semicolon-terminated CommonMark or HTML character reference pass as equivalent policy text before phrase matching.", + "Replace absolute symlinks traversed by the TypeScript public API scanner, including package-manifest ancestors and source paths, with relative in-root symlinks.", + "Update consumers that read AGENTS.md or CONTRIBUTING.md from the installed npm artifact to use a source checkout or their own admitted governance policy.", + "Read ordinary malformed-input diagnostics from stderr unless an explicit agent envelope was requested.", +} + +func validateCurrentChangeRecord(record Record, notes string) error { + if !slices.Equal(record.BreakingChanges, currentBreakingChanges) { + return fmt.Errorf("current release breaking inventory is not exact") + } + if !slices.Equal(record.Additions, currentAdditions) { + return fmt.Errorf("current release addition inventory is not exact") + } + if !slices.Equal(record.Migration.Steps, currentMigrationSteps) { + return fmt.Errorf("current release migration inventory is not exact") + } + if notes != currentExpectedReleaseNotes() { + return fmt.Errorf("rendered current release notes are not the exact reviewed projection") + } + return nil +} + +func currentExpectedReleaseNotes() string { + lines := []string{ + "# @research-engineering/agentic-proofkit 0.2.0", + "", + "## Breaking Contract Changes", + "", + } + for _, change := range currentBreakingChanges { + lines = append(lines, currentChangeBullet(change)) + } + lines = append(lines, "", "## Additions", "") + for _, change := range currentAdditions { + lines = append(lines, currentChangeBullet(change)) + } + lines = append(lines, "", "## Migration", "", "Migration is required:", "") + for _, step := range currentMigrationSteps { + lines = append(lines, "- "+step) + } + lines = append(lines, + "", + "## Platform Requirements", + "", + "- Published Darwin package binaries require macOS 12.0 or later on arm64 and x86_64.", + "", + "## Known Limitations", + "", + "- TSX source parsing remains unsupported.", + "- Static route coverage does not prove semantic execution coverage.", + "- The immutable 0.1.160 release is not modified, republished, or backfilled.", + "", + "## Install", + "", + "Primary npm channel:", + "", + "```bash", + "npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.2.0", + "```", + "", + "Pre-1.0 npm consumers must keep this dependency exact-pinned.", + "", + "Python wheels are candidate artifacts until a PyPI release workflow publishes them.", + "", + "GitHub Release assets and checksums are archive and provenance evidence, not package-manager dependency authority.", + "", + "## Rollback", + "", + "- Pin npm consumers to the previous admitted version 0.1.160 with `npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.1.160`.", + "- Treat local package artifacts as candidates until registry identity is proven.", + ) + return strings.Join(lines, "\n") + "\n" +} + +func currentChangeBullet(change Change) string { + return fmt.Sprintf("- `%s`: %s", change.ChangeID, change.Summary) +} + +func currentChangeRecordProjectionStrings() []string { + result := make([]string, 0, 2*(len(currentBreakingChanges)+len(currentAdditions))+len(currentMigrationSteps)) + for _, change := range currentBreakingChanges { + result = append(result, change.ChangeID, change.Summary) + } + for _, change := range currentAdditions { + result = append(result, change.ChangeID, change.Summary) + } + return append(result, currentMigrationSteps...) +} + +func assertCurrentChangeRecordRejected(t *testing.T, name string, record Record) { + t.Helper() + t.Run(name, func(t *testing.T) { + notes := RenderMarkdown(record, "@research-engineering/agentic-proofkit", "agentic-proofkit", false) + if err := validateCurrentChangeRecord(record, notes); err == nil { + t.Fatal("current release inventory mutant was admitted") + } + }) +} + +func assertCurrentChangeRecordNotesRejected(t *testing.T, name string, record Record, notes string) { + t.Helper() + t.Run(name, func(t *testing.T) { + if err := validateCurrentChangeRecord(record, notes); err == nil { + t.Fatal("current release note projection mutant was admitted") + } + }) +} + +func swapAdjacentNoteItems(notes string, first string, second string) string { + ordered := first + "\n" + second + reordered := second + "\n" + first + return strings.Replace(notes, ordered, reordered, 1) +} + +func cloneReleaseChangeRecord(record Record) Record { + record.BreakingChanges = append([]Change(nil), record.BreakingChanges...) + record.Additions = append([]Change(nil), record.Additions...) + record.Migration.Steps = append([]string(nil), record.Migration.Steps...) + return record +} + +func deleteReleaseChangeAt[T any](values []T, index int) []T { + result := append([]T(nil), values[:index]...) + return append(result, values[index+1:]...) +} + func TestAdmitRejectsAmbiguousOrIncompleteChangeRecords(t *testing.T) { t.Run("duplicate identity across sections", func(t *testing.T) { record := validRecord() @@ -56,7 +377,7 @@ func TestAdmitRejectsAmbiguousOrIncompleteChangeRecords(t *testing.T) { if err != nil { t.Fatal(err) } - if err := RequireVersion(record, "1.2.4"); err == nil || !strings.Contains(err.Error(), "does not match") { + if err := RequireVersion(record, "2.0.1"); err == nil || !strings.Contains(err.Error(), "does not match") { t.Fatalf("RequireVersion() error = %v, want mismatch", err) } }) @@ -99,8 +420,10 @@ func TestRenderRepresentsEmptyOptionalSectionsExplicitly(t *testing.T) { func validRecord() map[string]any { return map[string]any{ - "schemaVersion": json.Number("1"), - "version": "1.2.3", + "schemaVersion": json.Number("2"), + "previousVersion": "1.2.2", + "version": "2.0.0", + "changeClass": "breaking", "breakingChanges": []any{ map[string]any{"changeId": "proofkit.contract.breaking", "summary": "Remove the inert field."}, }, diff --git a/internal/tools/releasecloseoutinput/main_test.go b/internal/tools/releasecloseoutinput/main_test.go index 8f12e34..4915d7a 100644 --- a/internal/tools/releasecloseoutinput/main_test.go +++ b/internal/tools/releasecloseoutinput/main_test.go @@ -275,7 +275,7 @@ func TestBuildInputFailsClosedForEachBlockingEvidenceClass(t *testing.T) { name: "change record summary drift from release notes", criterionID: "proofkit.release_closeout.manifest_and_sbom", mutate: func(root string) { - path := filepath.Join(root, "release", "change-record.v1.json") + path := filepath.Join(root, "release", "change-record.v2.json") record := readJSONMap(t, path) additions := record["additions"].([]any) additions[0].(map[string]any)["summary"] = "Changed without regenerating release notes." @@ -286,7 +286,7 @@ func TestBuildInputFailsClosedForEachBlockingEvidenceClass(t *testing.T) { name: "change record version drift from package and release notes", criterionID: "proofkit.release_closeout.manifest_and_sbom", mutate: func(root string) { - path := filepath.Join(root, "release", "change-record.v1.json") + path := filepath.Join(root, "release", "change-record.v2.json") record := readJSONMap(t, path) record["version"] = "1.2.4" writeJSON(t, path, record) @@ -970,11 +970,13 @@ func releaseChangeRecordFixture() map[string]any { return map[string]any{ "additions": []any{map[string]any{"changeId": "proofkit.release.fixture", "summary": "Exercise release change projection."}}, "breakingChanges": []any{}, + "changeClass": "compatible", "knownLimitations": []any{"The fixture does not prove registry publication."}, "migration": map[string]any{"required": false, "steps": []any{}}, "platformRequirements": []any{"Use a supported fixture platform."}, + "previousVersion": "1.2.2", "rollback": map[string]any{"strategy": "previous_admitted_version"}, - "schemaVersion": 1, + "schemaVersion": 2, "version": "1.2.3", } } diff --git a/internal/tools/releasesbom/main.go b/internal/tools/releasesbom/main.go index ea17f4e..263c87c 100644 --- a/internal/tools/releasesbom/main.go +++ b/internal/tools/releasesbom/main.go @@ -1,8 +1,10 @@ package main import ( + "bytes" "crypto/sha1" "crypto/sha256" + gobuildinfo "debug/buildinfo" "encoding/hex" "encoding/json" "fmt" @@ -75,6 +77,7 @@ type cyclonedxComponent struct { Hashes []cyclonedxHash `json:"hashes,omitempty"` Licenses []cyclonedxLicense `json:"licenses,omitempty"` Properties []cyclonedxProperty `json:"properties,omitempty"` + Scope string `json:"scope,omitempty"` } type cyclonedxHash struct { @@ -112,17 +115,13 @@ func run() error { if err != nil { return err } - components, err := components(manifest) + components, dependencies, err := components(manifest) if err != nil { return err } sort.Slice(components, func(left int, right int) bool { return components[left].BOMRef < components[right].BOMRef }) - dependencies := []cyclonedxDependency{{ - Ref: rootRef(manifest), - DependsOn: componentRefs(components, rootRef(manifest)), - }} out := cyclonedxBOM{ BOMFormat: bomFormat, SpecVersion: specVersion, @@ -165,28 +164,32 @@ func readPackageJSON(path string) (packageJSON, error) { return manifest, nil } -func components(manifest packageJSON) ([]cyclonedxComponent, error) { - out := []cyclonedxComponent{} - goModules, err := goModuleComponents() +func components(manifest packageJSON) ([]cyclonedxComponent, []cyclonedxDependency, error) { + sourceModules, err := goModuleInventory() if err != nil { - return nil, err + return nil, nil, err } - out = append(out, goModules...) - fileComponents, err := releaseFileComponents(manifest) + paths, err := releaseFilePaths() if err != nil { - return nil, err + return nil, nil, err } + fileComponents, runtimeInventories, err := releaseFileEvidence(manifest, paths, nil) + if err != nil { + return nil, nil, err + } + goModules, dependencies := projectModuleEvidence(sourceModules, runtimeInventories) + out := append([]cyclonedxComponent{}, goModules...) out = append(out, fileComponents...) - return out, nil + return out, dependencies, nil } -func goModuleComponents() ([]cyclonedxComponent, error) { +func goModuleInventory() ([]goModuleRecord, error) { output, err := exec.Command("go", "list", "-m", "-json", "all").Output() if err != nil { return nil, fmt.Errorf("go list modules: %w", err) } decoder := json.NewDecoder(strings.NewReader(string(output))) - out := []cyclonedxComponent{} + out := []goModuleRecord{} for { var module goModuleRecord if err := decoder.Decode(&module); err != nil { @@ -198,60 +201,237 @@ func goModuleComponents() ([]cyclonedxComponent, error) { if module.Main { continue } - version := module.Version - pathValue := module.Path + module = canonicalModule(module) + if module.Path == "" { + continue + } + out = append(out, module) + } + return out, nil +} + +type artifactRuntimeInventory struct { + BinaryRef string + Modules []goModuleRecord +} + +func binaryRuntimeModules(reader io.ReaderAt) ([]goModuleRecord, error) { + info, err := gobuildinfo.Read(reader) + if err != nil { + // A stripped or otherwise non-introspectable binary has no admitted + // runtime-module evidence. The release file remains an SBOM subject. + return nil, nil + } + modules := make([]goModuleRecord, 0, len(info.Deps)) + for _, module := range info.Deps { + record := goModuleRecord{Path: module.Path, Version: module.Version} if module.Replace != nil { - pathValue = module.Replace.Path - version = module.Replace.Version + record.Replace = &goModuleRecord{Path: module.Replace.Path, Version: module.Replace.Version} } - if pathValue == "" { + record = canonicalModule(record) + if record.Path != "" { + modules = append(modules, record) + } + } + sort.Slice(modules, func(left, right int) bool { + return moduleKey(modules[left]) < moduleKey(modules[right]) + }) + return modules, nil +} + +func projectModuleEvidence(source []goModuleRecord, artifacts []artifactRuntimeInventory) ([]cyclonedxComponent, []cyclonedxDependency) { + components := map[string]cyclonedxComponent{} + for _, raw := range source { + module := canonicalModule(raw) + if module.Path == "" { continue } - out = append(out, cyclonedxComponent{ - Type: "library", - BOMRef: "go-module:" + pathValue + "@" + version, - Name: pathValue, - Version: version, - PackageURL: goPackageURL(pathValue, version), - Properties: []cyclonedxProperty{{ - Name: "proofkit:source", - Value: "go list -m -json all", - }}, + components[moduleKey(module)] = moduleComponent(module, "excluded", []cyclonedxProperty{ + {Name: "proofkit:evidence-class", Value: "source_build_inventory"}, + {Name: "proofkit:source", Value: "go list -m -json all"}, }) } - return out, nil + dependencies := make([]cyclonedxDependency, 0, len(artifacts)) + for _, artifact := range artifacts { + edges := []string{} + seen := map[string]struct{}{} + for _, raw := range artifact.Modules { + module := canonicalModule(raw) + if module.Path == "" { + continue + } + key := moduleKey(module) + component, exists := components[key] + if !exists { + component = moduleComponent(module, "required", nil) + } + component.Scope = "required" + component.Properties = appendUniqueProperty(component.Properties, cyclonedxProperty{ + Name: "proofkit:evidence-class", Value: "artifact_build_info", + }) + components[key] = component + if _, exists := seen[component.BOMRef]; !exists { + seen[component.BOMRef] = struct{}{} + edges = append(edges, component.BOMRef) + } + } + sort.Strings(edges) + dependencies = append(dependencies, cyclonedxDependency{Ref: artifact.BinaryRef, DependsOn: edges}) + } + out := make([]cyclonedxComponent, 0, len(components)) + for _, component := range components { + sort.Slice(component.Properties, func(left, right int) bool { + if component.Properties[left].Name != component.Properties[right].Name { + return component.Properties[left].Name < component.Properties[right].Name + } + return component.Properties[left].Value < component.Properties[right].Value + }) + out = append(out, component) + } + sort.Slice(out, func(left, right int) bool { return out[left].BOMRef < out[right].BOMRef }) + sort.Slice(dependencies, func(left, right int) bool { return dependencies[left].Ref < dependencies[right].Ref }) + return out, dependencies } -func releaseFileComponents(manifest packageJSON) ([]cyclonedxComponent, error) { - paths, err := releaseFilePaths() - if err != nil { - return nil, err +func canonicalModule(module goModuleRecord) goModuleRecord { + if module.Replace != nil { + return goModuleRecord{Path: module.Replace.Path, Version: module.Replace.Version} + } + return goModuleRecord{Path: module.Path, Version: module.Version} +} + +func moduleKey(module goModuleRecord) string { + return module.Path + "@" + module.Version +} + +func moduleComponent(module goModuleRecord, scope string, properties []cyclonedxProperty) cyclonedxComponent { + return cyclonedxComponent{ + Type: "library", + BOMRef: "go-module:" + moduleKey(module), + Name: module.Path, + Version: module.Version, + PackageURL: goPackageURL(module.Path, module.Version), + Properties: properties, + Scope: scope, + } +} + +func appendUniqueProperty(properties []cyclonedxProperty, property cyclonedxProperty) []cyclonedxProperty { + for _, existing := range properties { + if existing == property { + return properties + } + } + return append(properties, property) +} + +func releaseFileEvidence( + manifest packageJSON, + paths []string, + afterHash func(string) error, +) ([]cyclonedxComponent, []artifactRuntimeInventory, error) { + binaryPaths := map[string]struct{}{} + for _, path := range releaseplatform.BinaryPaths() { + binaryPaths[filepath.Clean(path)] = struct{}{} } out := make([]cyclonedxComponent, 0, len(paths)) + runtimeInventories := make([]artifactRuntimeInventory, 0, len(binaryPaths)) for _, path := range paths { - sum, err := fileSHA256(path) + component, modules, isBinary, err := admitReleaseFile(manifest, path, binaryPaths, afterHash) if err != nil { - return nil, err + return nil, nil, err + } + out = append(out, component) + if isBinary { + runtimeInventories = append(runtimeInventories, artifactRuntimeInventory{ + BinaryRef: component.BOMRef, + Modules: modules, + }) } - out = append(out, cyclonedxComponent{ - Type: componentType(path), - BOMRef: "file:" + filepath.ToSlash(path), - Name: filepath.Base(path), - Version: manifest.Version, - Hashes: []cyclonedxHash{{ - Alg: "SHA-256", - Content: sum, - }}, - Licenses: []cyclonedxLicense{{ - License: cyclonedxLicenseID{ID: manifest.License}, - }}, - Properties: []cyclonedxProperty{{ - Name: "proofkit:path", - Value: filepath.ToSlash(path), - }}, - }) } - return out, nil + return out, runtimeInventories, nil +} + +func admitReleaseFile( + manifest packageJSON, + path string, + binaryPaths map[string]struct{}, + afterHash func(string) error, +) (cyclonedxComponent, []goModuleRecord, bool, error) { + var zero cyclonedxComponent + routeInfo, err := os.Lstat(path) + if err != nil { + return zero, nil, false, err + } + if routeInfo.Mode()&os.ModeSymlink != 0 || !routeInfo.Mode().IsRegular() { + return zero, nil, false, fmt.Errorf("release file %s must be a non-symlink regular file", path) + } + file, err := os.Open(path) + if err != nil { + return zero, nil, false, err + } + defer file.Close() + before, err := file.Stat() + if err != nil { + return zero, nil, false, err + } + if !os.SameFile(routeInfo, before) { + return zero, nil, false, fmt.Errorf("release file %s changed before admission", path) + } + content, err := io.ReadAll(io.NewSectionReader(file, 0, before.Size())) + if err != nil { + return zero, nil, false, err + } + if int64(len(content)) != before.Size() { + return zero, nil, false, fmt.Errorf("release file %s changed during admission", path) + } + contentDigest := sha256.Sum256(content) + if afterHash != nil { + if err := afterHash(path); err != nil { + return zero, nil, false, err + } + } + _, isBinary := binaryPaths[filepath.Clean(path)] + var modules []goModuleRecord + if isBinary { + modules, err = binaryRuntimeModules(bytes.NewReader(content)) + if err != nil { + return zero, nil, false, err + } + } + currentDigest := sha256.New() + if _, err := io.Copy(currentDigest, io.NewSectionReader(file, 0, before.Size())); err != nil { + return zero, nil, false, err + } + after, err := file.Stat() + if err != nil { + return zero, nil, false, err + } + currentRoute, err := os.Lstat(path) + if err != nil || currentRoute.Mode()&os.ModeSymlink != 0 || + !os.SameFile(before, after) || !os.SameFile(before, currentRoute) || + before.Size() != after.Size() || + !bytes.Equal(contentDigest[:], currentDigest.Sum(nil)) { + return zero, nil, false, fmt.Errorf("release file %s changed during admission", path) + } + component := cyclonedxComponent{ + Type: componentType(path), + BOMRef: "file:" + filepath.ToSlash(path), + Name: filepath.Base(path), + Version: manifest.Version, + Hashes: []cyclonedxHash{{ + Alg: "SHA-256", + Content: hex.EncodeToString(contentDigest[:]), + }}, + Licenses: []cyclonedxLicense{{ + License: cyclonedxLicenseID{ID: manifest.License}, + }}, + Properties: []cyclonedxProperty{{ + Name: "proofkit:path", + Value: filepath.ToSlash(path), + }}, + } + return component, modules, isBinary, nil } func releaseFilePaths() ([]string, error) { @@ -344,33 +524,9 @@ func uuidV5(namespace [16]byte, name string) string { return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:16]) } -func componentRefs(components []cyclonedxComponent, root string) []string { - out := []string{} - for _, component := range components { - if component.BOMRef != root { - out = append(out, component.BOMRef) - } - } - sort.Strings(out) - return out -} - func goPackageURL(path string, version string) string { if version == "" { return "pkg:golang/" + path } return "pkg:golang/" + path + "@" + version } - -func fileSHA256(path string) (string, error) { - file, err := os.Open(path) - if err != nil { - return "", err - } - defer file.Close() - hash := sha256.New() - if _, err := io.Copy(hash, file); err != nil { - return "", err - } - return hex.EncodeToString(hash.Sum(nil)), nil -} diff --git a/internal/tools/releasesbom/main_test.go b/internal/tools/releasesbom/main_test.go index 1bb47b2..f2e22ba 100644 --- a/internal/tools/releasesbom/main_test.go +++ b/internal/tools/releasesbom/main_test.go @@ -3,12 +3,105 @@ package main import ( "os" "path/filepath" + "slices" "strings" "testing" "github.com/research-engineering/agentic-proofkit/internal/kernel/releaseplatform" ) +func TestArtifactSpecificRuntimeEdgesAndExcludedInventory(t *testing.T) { + source := []goModuleRecord{ + {Path: "example.invalid/runtime", Version: "v1.0.0"}, + {Path: "example.invalid/tool", Version: "v2.0.0"}, + } + artifacts := []artifactRuntimeInventory{ + { + BinaryRef: "file:dist/platform/linux-x64/agentic-proofkit", + Modules: []goModuleRecord{{Path: "example.invalid/runtime", Version: "v1.0.0"}}, + }, + { + BinaryRef: "file:dist/platform/darwin-arm64/agentic-proofkit", + Modules: nil, + }, + } + + components, dependencies := projectModuleEvidence(source, artifacts) + byRef := map[string]cyclonedxComponent{} + for _, component := range components { + byRef[component.BOMRef] = component + } + runtimeRef := "go-module:example.invalid/runtime@v1.0.0" + toolRef := "go-module:example.invalid/tool@v2.0.0" + if byRef[runtimeRef].Scope != "required" { + t.Fatalf("runtime scope = %q, want required", byRef[runtimeRef].Scope) + } + if byRef[toolRef].Scope != "excluded" { + t.Fatalf("tool scope = %q, want excluded", byRef[toolRef].Scope) + } + if !hasProperty(byRef[toolRef], "proofkit:evidence-class", "source_build_inventory") { + t.Fatalf("tool properties = %#v, want excluded source inventory evidence", byRef[toolRef].Properties) + } + dependencyByRef := map[string][]string{} + for _, dependency := range dependencies { + dependencyByRef[dependency.Ref] = dependency.DependsOn + } + if got := dependencyByRef[artifacts[0].BinaryRef]; !slices.Equal(got, []string{runtimeRef}) { + t.Fatalf("linux runtime edges = %v, want [%s]", got, runtimeRef) + } + if got := dependencyByRef[artifacts[1].BinaryRef]; len(got) != 0 { + t.Fatalf("stripped binary runtime edges = %v, want none", got) + } + for _, forbiddenRef := range []string{ + "pkg:npm/@research-engineering/agentic-proofkit@1.2.3", + "pkg:pypi/agentic-proofkit@1.2.3", + toolRef, + } { + for ref, edges := range dependencyByRef { + if slices.Contains(edges, forbiddenRef) { + t.Fatalf("%s invented runtime edge from %s", forbiddenRef, ref) + } + } + } +} + +func TestReleaseFileEvidenceRejectsDeterministicIdentitySwap(t *testing.T) { + root := t.TempDir() + path := filepath.Join(root, "agentic-proofkit") + replacement := filepath.Join(root, "replacement") + displaced := filepath.Join(root, "displaced") + writeFile(t, path, "first-binary") + writeFile(t, replacement, "other-binary") + manifest := packageJSON{Name: "@research-engineering/agentic-proofkit", Version: "1.2.3", License: "MIT"} + + _, _, err := releaseFileEvidence(manifest, []string{path}, func(selected string) error { + if selected != path { + t.Fatalf("afterHash selected %s, want %s", selected, path) + } + if err := os.Rename(path, displaced); err != nil { + return err + } + return os.Rename(replacement, path) + }) + if err == nil || !strings.Contains(err.Error(), "changed during admission") { + t.Fatalf("releaseFileEvidence() error=%v, want identity-swap rejection", err) + } +} + +func TestReleaseFileEvidenceRejectsDeterministicInPlaceMutation(t *testing.T) { + root := t.TempDir() + path := filepath.Join(root, "agentic-proofkit") + writeFile(t, path, "first-binary") + manifest := packageJSON{Name: "@research-engineering/agentic-proofkit", Version: "1.2.3", License: "MIT"} + + _, _, err := releaseFileEvidence(manifest, []string{path}, func(selected string) error { + return os.WriteFile(selected, []byte("other-binary"), 0o600) + }) + if err == nil || !strings.Contains(err.Error(), "changed during admission") { + t.Fatalf("releaseFileEvidence() error=%v, want in-place mutation rejection", err) + } +} + func TestReadPackageJSONRejectsAmbiguousJSON(t *testing.T) { path := filepath.Join(t.TempDir(), "package.json") if err := os.WriteFile(path, []byte(`{"name":"agentic-proofkit","name":"other","version":"1.2.3","license":"MIT"}`), 0o600); err != nil { @@ -96,6 +189,15 @@ func TestSBOMSerialNumberIsDeterministicCycloneDXURN(t *testing.T) { } } +func hasProperty(component cyclonedxComponent, name, value string) bool { + for _, property := range component.Properties { + if property.Name == name && property.Value == value { + return true + } + } + return false +} + func writeReleasePlatformBinaries(t *testing.T, paths []string) { t.Helper() for _, path := range paths { diff --git a/package-lock.json b/package-lock.json index 86f965f..b5cb120 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@research-engineering/agentic-proofkit", - "version": "0.1.160", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@research-engineering/agentic-proofkit", - "version": "0.1.160", + "version": "0.2.0", "cpu": [ "arm64", "x64" @@ -20,38 +20,25 @@ "agentic-proofkit": "dist/agentic-proofkit" }, "devDependencies": { - "@axe-core/playwright": "4.12.1", - "@playwright/test": "1.61.1", + "@playwright/test": "1.62.0", + "axe-core": "4.12.1", "typescript": "7.0.2" } }, - "node_modules/@axe-core/playwright": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.12.1.tgz", - "integrity": "sha512-rMd7xriptqKpP+w5265i4Hdkv2X5kbu6uiBi/B2I7uf3hieRBM3qDCfaKPtxfiYb2mKXfF+yLODJwIx+Jv1GDw==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "axe-core": "~4.12.1" - }, - "peerDependencies": { - "playwright-core": ">= 1.0.0" - } - }, "node_modules/@playwright/test": { - "version": "1.61.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", - "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.0.tgz", + "integrity": "sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.61.1" + "playwright": "1.62.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/@typescript/typescript-aix-ppc64": { @@ -420,35 +407,35 @@ } }, "node_modules/playwright": { - "version": "1.61.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", - "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz", + "integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.61.1" + "playwright-core": "1.62.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=20" }, "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.61.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", - "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz", + "integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==", "dev": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/typescript": { diff --git a/package.json b/package.json index 12f2b77..95e6ce3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@research-engineering/agentic-proofkit", "description": "Reusable proof profile, report, graph, and witness-planning primitives.", - "version": "0.1.160", + "version": "0.2.0", "type": "module", "license": "MIT", "sideEffects": false, @@ -22,8 +22,6 @@ }, "files": [ "ADOPTION.md", - "AGENTS.md", - "CONTRIBUTING.md", "LICENSE", "NON_CLAIMS.md", "README.md", @@ -36,7 +34,7 @@ ], "scripts": { "build": "go run ./internal/tools/packagebuild", - "check": "npm run npm:version && npm run source-hygiene && npm run command-family:check && npm run text-policy && npm run mermaid:check && npm run browser:check && npm run go:check && npm run package:artifact && npm run self:receipt && npm run self:coverage && npm run release:closeout", + "check": "npm run npm:version && npm run source-hygiene && npm run command-contract:check && npm run command-family:check && npm run text-policy && npm run mermaid:check && npm run browser:check && npm run go:check && npm run package:artifact && npm run self:receipt && npm run self:coverage && npm run release:closeout", "browser:check": "npm run browser:static-check && npm run browser:test", "browser:static-check": "tsc -p tsconfig.browser.json && node --test scripts/browser-proof-execution.test.mjs scripts/browser-proof-inputs.test.mjs scripts/browser-selection-authority.test.mjs", "browser:test": "go run ./internal/tools/browserproofverify --run", @@ -63,7 +61,8 @@ "text-policy": "go run ./internal/tools/textpolicyinput | go run ./cmd/agentic-proofkit text-policy --input -", "mermaid:check": "go run ./internal/tools/mermaidcheck", "go:staticcheck": "go tool staticcheck ./...", - "go:vulncheck": "go tool govulncheck ./..." + "go:vulncheck": "go tool govulncheck ./...", + "command-contract:check": "go run ./internal/tools/commandcontractgen --check" }, "os": [ "darwin", @@ -74,8 +73,8 @@ "x64" ], "devDependencies": { - "@axe-core/playwright": "4.12.1", - "@playwright/test": "1.61.1", + "@playwright/test": "1.62.0", + "axe-core": "4.12.1", "typescript": "7.0.2" } } diff --git a/playwright.config.mjs b/playwright.config.mjs index ff512af..38c1993 100644 --- a/playwright.config.mjs +++ b/playwright.config.mjs @@ -18,7 +18,13 @@ export default defineConfig({ workers: 1, use: { baseURL, - trace: "retain-on-failure", + screenshot: "only-on-failure", + trace: { + mode: "retain-on-failure", + screenshots: false, + snapshots: true, + sources: true, + }, }, projects: [ {name: "chromium", use: {...devices["Desktop Chrome"]}}, diff --git a/proofkit/cli-contract.v2.json b/proofkit/cli-contract.v2.json index 240bda2..7ed0cc0 100644 --- a/proofkit/cli-contract.v2.json +++ b/proofkit/cli-contract.v2.json @@ -46,7 +46,61 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.adoption-checklist.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.adoption-checklist.input.v1.root-shape", + "rootDefinitionDigest": "sha256:1526faa8d4517fcc1babd0cb4e46ffbd3764c2cdc9453e305efa93ea236c1110", + "nativeSource": { + "path": "internal/command/adoptionchecklist", + "canonicalDigest": "sha256:9958efabb310b40e782d8308cbf15482c95474816a6a2c3972c58300a178e6b8", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/adoptionchecklist/adoptionchecklist_test.go", + "test": "TestBuildClassifiesRequiredChecklistItemsAndPreservesOptionalNonFailures", + "command": "go test ./internal/command/adoptionchecklist -run '^TestBuildClassifiesRequiredChecklistItemsAndPreservesOptionalNonFailures$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.adoption-checklist.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.adoption-checklist.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.adoption-checklist.output.v1.root-shape", + "rootDefinitionDigest": "sha256:6834488d7bfbb4fbcd50dda8d26c65f0c72e15ce7fd2f53ac4a1ee396f72f106", + "nativeSource": { + "path": "internal/command/adoptionchecklist", + "canonicalDigest": "sha256:9958efabb310b40e782d8308cbf15482c95474816a6a2c3972c58300a178e6b8", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/adoptionchecklist/adoptionchecklist_test.go", + "test": "TestBuildClassifiesRequiredChecklistItemsAndPreservesOptionalNonFailures", + "command": "go test ./internal/command/adoptionchecklist -run '^TestBuildClassifiesRequiredChecklistItemsAndPreservesOptionalNonFailures$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.adoption-checklist.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "adoption-contract-envelope", @@ -70,7 +124,63 @@ ], "requiredFlags": [ "--mode" - ] + ], + "inputContract": { + "contractId": "proofkit.adoption-contract-envelope.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.adoption-contract-envelope.input.v1.root-shape", + "rootDefinitionDigest": "sha256:d12d8f01e350c871af39a1f518f08a86f59c0893c5d161ed1b4fe644e7b30f9c", + "nativeSource": { + "path": "internal/command/adoptioncontract", + "canonicalDigest": "sha256:0f306d18075e903b0a9223b7ef4b5dbb8a56e20afdf49007edcbd1a3702c68a5", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/adoptioncontract/adoptioncontract_test.go", + "test": "TestBuildDelegatesModesWithParity", + "command": "go test ./internal/command/adoptioncontract -run '^TestBuildDelegatesModesWithParity$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.adoption-contract-envelope.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-011" + ] + }, + "outputContract": { + "contractId": "proofkit.adoption-contract-envelope.output.v1", + "schemaVersion": 1, + "rootType": "union", + "closed": true, + "rootDefinitionRef": "proofkit.adoption-contract-envelope.output.v1.root-shape", + "rootDefinitionDigest": "sha256:6da2b641d38a05c97b6088bec7a38d8c9a76aa386eb101e48f9727bceae5cfa2", + "nativeSource": { + "path": "internal/command/adoptioncontract", + "canonicalDigest": "sha256:0f306d18075e903b0a9223b7ef4b5dbb8a56e20afdf49007edcbd1a3702c68a5", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/app/cli_abi_test.go", + "test": "TestAdoptionContractEnvelopeCLIABI", + "command": "go test ./internal/app -run '^TestAdoptionContractEnvelopeCLIABI$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.adoption-contract-envelope.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-011" + ] + } }, { "command": "adoption-doctor", @@ -86,7 +196,69 @@ "--agent-envelope", "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.adoption-doctor.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.adoption-doctor.input.v1.root-shape", + "rootDefinitionDigest": "sha256:104d1e8bef615582ead1c7c8c64b7e1643a76c2bc2a1f224189c372cf8cc4aa1", + "nativeSource": { + "path": "internal/command/adoptiondoctor", + "canonicalDigest": "sha256:95f909f36650142a58ca241c0210582c405f749187a16c03127242ba3d1b1507", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/app/cli_abi_test.go", + "test": "TestAdoptionDoctorCLIABI", + "command": "go test ./internal/app -run '^TestAdoptionDoctorCLIABI$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.adoption-doctor.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-QUALITY-021", + "REQ-PROOFKIT-SPEC-009", + "REQ-PROOFKIT-SPEC-011", + "REQ-PROOFKIT-SPEC-012" + ] + }, + "outputContract": { + "contractId": "proofkit.adoption-doctor.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.adoption-doctor.output.v1.root-shape", + "rootDefinitionDigest": "sha256:90583a47b9bdd605b2d3f24dcb1a5dbb2b4d9da20d7be097da381c9c77229e62", + "nativeSource": { + "path": "internal/command/adoptiondoctor", + "canonicalDigest": "sha256:95f909f36650142a58ca241c0210582c405f749187a16c03127242ba3d1b1507", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/app/cli_abi_test.go", + "test": "TestAdoptionDoctorCLIABI", + "command": "go test ./internal/app -run '^TestAdoptionDoctorCLIABI$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.adoption-doctor.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-QUALITY-021", + "REQ-PROOFKIT-SPEC-009", + "REQ-PROOFKIT-SPEC-011", + "REQ-PROOFKIT-SPEC-012" + ] + } }, { "command": "adoption-workflow-plan", @@ -104,7 +276,63 @@ "--contract-envelope", "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.adoption-workflow-plan.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.adoption-workflow-plan.input.v1.root-shape", + "rootDefinitionDigest": "sha256:d07b4695792339e226769c2ac77022b45ac921e3f4847c36677aea86efc9583d", + "nativeSource": { + "path": "internal/command/adoptionworkflow", + "canonicalDigest": "sha256:a4b12431af0dd1dbd764ab688a64566e9a7f34840bd874822360c670fa207d3c", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/adoptionworkflow/adoptionworkflow_test.go", + "test": "TestBuildGeneratesBoundedCommandArgv", + "command": "go test ./internal/command/adoptionworkflow -run '^TestBuildGeneratesBoundedCommandArgv$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.adoption-workflow-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-RETIRE-005" + ] + }, + "outputContract": { + "contractId": "proofkit.adoption-workflow-plan.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.adoption-workflow-plan.output.v1.root-shape", + "rootDefinitionDigest": "sha256:c06f7bfa9c10f73575aa5f927696d41db818697c9a3d320074b7c254011830d5", + "nativeSource": { + "path": "internal/command/adoptionworkflow", + "canonicalDigest": "sha256:a4b12431af0dd1dbd764ab688a64566e9a7f34840bd874822360c670fa207d3c", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/adoptionworkflow/adoptionworkflow_test.go", + "test": "TestBuildGeneratesBoundedCommandArgv", + "command": "go test ./internal/command/adoptionworkflow -run '^TestBuildGeneratesBoundedCommandArgv$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.adoption-workflow-plan.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-RETIRE-005" + ] + } }, { "command": "agent-route", @@ -275,6 +503,39 @@ "nonClaims": [ "This input contract describes agent-route admission only.", "It does not validate referenced file contents, execute commands, read repositories, or approve merge." + ], + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.agent-route.input.v1.root-shape", + "rootDefinitionDigest": "sha256:fd1121285d1b357ed848febc248700a75417d06619e22ed36991631d7526375b", + "nativeSource": { + "path": "internal/command/agentroute", + "canonicalDigest": "sha256:bcbf099540f203e32c0b6d9cb38dff0dc20e8018644569d647d2979d0d74ac15", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/agentroute/agentroute_test.go", + "test": "TestBuildRoutesRequirementSourceAndBlocksUnknownGoal", + "command": "go test ./internal/command/agentroute -run '^TestBuildRoutesRequirementSourceAndBlocksUnknownGoal$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "availableInputs", + "browserMode", + "goal", + "knownChangedPaths", + "mode", + "nonClaims", + "observedReports", + "openBrowser", + "routeId", + "schemaVersion", + "root-shape-only definition proofkit.agent-route.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-005" ] }, "outputContract": { @@ -318,6 +579,32 @@ "changesFromV1": [ "selectedFamily is replaced by selectedRouteFamily", "guidanceSlice.family is replaced by guidanceSlice.routeFamily" + ], + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.agent-route.output.v2.root-shape", + "rootDefinitionDigest": "sha256:db7d5b29083a939eea0f23253632394212b0d73b5f1b6206d0811d236d6a84ca", + "nativeSource": { + "path": "internal/command/agentroute", + "canonicalDigest": "sha256:bcbf099540f203e32c0b6d9cb38dff0dc20e8018644569d647d2979d0d74ac15", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/agentroute/agentroute_test.go", + "test": "TestBuildRoutesRequirementSourceAndBlocksUnknownGoal", + "command": "go test ./internal/command/agentroute -run '^TestBuildRoutesRequirementSourceAndBlocksUnknownGoal$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "guidanceSlice", + "schemaVersion", + "selectedRouteFamily", + "root-shape-only definition proofkit.agent-route.output.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-005" ] } }, @@ -333,7 +620,61 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.binding-partition.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.binding-partition.input.v1.root-shape", + "rootDefinitionDigest": "sha256:c8b0915d112ba868b97eabc113cb479304f52a545f9e5acc3ea89733d5c8793a", + "nativeSource": { + "path": "internal/command/bindingpartition", + "canonicalDigest": "sha256:ef4faf39c6638e23e0e7893b01022b7f78124d706ae998dc1fae8204f0ffd67a", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/bindingpartition/bindingpartition_test.go", + "test": "TestBuildRejectsCrossSurfaceRouteReferenceWithoutDelegation", + "command": "go test ./internal/command/bindingpartition -run '^TestBuildRejectsCrossSurfaceRouteReferenceWithoutDelegation$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.binding-partition.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.binding-partition.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.binding-partition.output.v1.root-shape", + "rootDefinitionDigest": "sha256:cf6c70aeabd7719b65c380d798c13eb06a7b332ef49276b690e75c4b6376537b", + "nativeSource": { + "path": "internal/command/bindingpartition", + "canonicalDigest": "sha256:ef4faf39c6638e23e0e7893b01022b7f78124d706ae998dc1fae8204f0ffd67a", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/bindingpartition/bindingpartition_test.go", + "test": "TestBuildRejectsCrossSurfaceRouteReferenceWithoutDelegation", + "command": "go test ./internal/command/bindingpartition -run '^TestBuildRejectsCrossSurfaceRouteReferenceWithoutDelegation$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.binding-partition.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "branch-authority", @@ -347,7 +688,61 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.branch-authority.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.branch-authority.input.v1.root-shape", + "rootDefinitionDigest": "sha256:c1ebe30c5e746195731c6f960278e7f3d5ec8394b0ddbc49905e406f35492037", + "nativeSource": { + "path": "internal/command/branchauthority", + "canonicalDigest": "sha256:69f826954d5d7955034aed4d5f8e4f8204d92d162045bdb3e49b172e3078c208", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/branchauthority/branchauthority_test.go", + "test": "TestBuildAdmitsAlignedRequiredBranchAndRejectsRequiredDrift", + "command": "go test ./internal/command/branchauthority -run '^TestBuildAdmitsAlignedRequiredBranchAndRejectsRequiredDrift$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.branch-authority.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.branch-authority.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.branch-authority.output.v1.root-shape", + "rootDefinitionDigest": "sha256:0504c246c8cd4573231d77c2bb1387d45b1a303034f2f80cebef3eaa88a985e3", + "nativeSource": { + "path": "internal/command/branchauthority", + "canonicalDigest": "sha256:69f826954d5d7955034aed4d5f8e4f8204d92d162045bdb3e49b172e3078c208", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/branchauthority/branchauthority_test.go", + "test": "TestBuildAdmitsAlignedRequiredBranchAndRejectsRequiredDrift", + "command": "go test ./internal/command/branchauthority -run '^TestBuildAdmitsAlignedRequiredBranchAndRejectsRequiredDrift$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.branch-authority.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "capability-map-admission", @@ -361,7 +756,63 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.capability-map-admission.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.capability-map-admission.input.v1.root-shape", + "rootDefinitionDigest": "sha256:9b1b0b9b884f84f856db7373844a1988bb7ef0198a9f125219025530ed6054b2", + "nativeSource": { + "path": "internal/command/capabilitymapadmission", + "canonicalDigest": "sha256:ec4f642c3c56483f098ce7eb6f3448ce114f5c87cb2e4d5d23a259e40c94a8f1", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/capabilitymapadmission/capability_map_admission_test.go", + "test": "TestBuildCodeBaselineFailsMissingCandidateRequirementAndAnchor", + "command": "go test ./internal/command/capabilitymapadmission -run '^TestBuildCodeBaselineFailsMissingCandidateRequirementAndAnchor$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.capability-map-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-017" + ] + }, + "outputContract": { + "contractId": "proofkit.capability-map-admission.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.capability-map-admission.output.v1.root-shape", + "rootDefinitionDigest": "sha256:d20d4d55690f7b98768d8bb1c2c313f78bdf9ac8163fa6d1ff694664b2d7d999", + "nativeSource": { + "path": "internal/command/capabilitymapadmission", + "canonicalDigest": "sha256:ec4f642c3c56483f098ce7eb6f3448ce114f5c87cb2e4d5d23a259e40c94a8f1", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/capabilitymapadmission/capability_map_admission_test.go", + "test": "TestBuildCodeBaselineFailsMissingCandidateRequirementAndAnchor", + "command": "go test ./internal/command/capabilitymapadmission -run '^TestBuildCodeBaselineFailsMissingCandidateRequirementAndAnchor$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.capability-map-admission.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-017" + ] + } }, { "command": "changed-path-set", @@ -377,7 +828,63 @@ "--agent-envelope", "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.changed-path-set.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.changed-path-set.input.v1.root-shape", + "rootDefinitionDigest": "sha256:12533579edbed52bc326d3775ef3c57665d1b4605d822d7824834343ef533e48", + "nativeSource": { + "path": "internal/command/changedpathset", + "canonicalDigest": "sha256:875bf6b303b8afa2bcb2e45ed0d9b1947ff26f7956f77a4715d847a73076c98e", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/changedpathset/changedpathset_test.go", + "test": "TestBuildDeduplicatesAndFailsClosedOnInvalidPaths", + "command": "go test ./internal/command/changedpathset -run '^TestBuildDeduplicatesAndFailsClosedOnInvalidPaths$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.changed-path-set.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-004" + ] + }, + "outputContract": { + "contractId": "proofkit.changed-path-set.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.changed-path-set.output.v1.root-shape", + "rootDefinitionDigest": "sha256:969e47d122156eb7bdc6a1ee8c194d9a07c90b9f7a40537bd9795c037baf84e2", + "nativeSource": { + "path": "internal/command/changedpathset", + "canonicalDigest": "sha256:875bf6b303b8afa2bcb2e45ed0d9b1947ff26f7956f77a4715d847a73076c98e", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/changedpathset/changedpathset_test.go", + "test": "TestBuildDeduplicatesAndFailsClosedOnInvalidPaths", + "command": "go test ./internal/command/changedpathset -run '^TestBuildDeduplicatesAndFailsClosedOnInvalidPaths$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.changed-path-set.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-004" + ] + } }, { "command": "completion-criteria", @@ -391,7 +898,61 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.completion-criteria.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.completion-criteria.input.v1.root-shape", + "rootDefinitionDigest": "sha256:ae98108a097a9f65a3a5f427f6bec09077367109941cd3aaa24cb32246e2ef41", + "nativeSource": { + "path": "internal/command/completioncriteria", + "canonicalDigest": "sha256:6b409e1d08f7e984f77891175b33d0b4edb1f88efb9684866f76d63b4c138dce", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/completioncriteria/completioncriteria_test.go", + "test": "TestBuildBlocksUnsatisfiedBlockingCriterion", + "command": "go test ./internal/command/completioncriteria -run '^TestBuildBlocksUnsatisfiedBlockingCriterion$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.completion-criteria.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.completion-criteria.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.completion-criteria.output.v1.root-shape", + "rootDefinitionDigest": "sha256:81b3eb5f072ad45d3d71954bc9dfcb46bcdc1e8afc9ade800ae1ee73dfb503d0", + "nativeSource": { + "path": "internal/command/completioncriteria", + "canonicalDigest": "sha256:6b409e1d08f7e984f77891175b33d0b4edb1f88efb9684866f76d63b4c138dce", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/completioncriteria/completioncriteria_test.go", + "test": "TestBuildBlocksUnsatisfiedBlockingCriterion", + "command": "go test ./internal/command/completioncriteria -run '^TestBuildBlocksUnsatisfiedBlockingCriterion$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.completion-criteria.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "conformance-profile", @@ -426,7 +987,61 @@ ], "value": "markdown" } - ] + ], + "inputContract": { + "contractId": "proofkit.conformance-profile.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.conformance-profile.input.v1.root-shape", + "rootDefinitionDigest": "sha256:6ebc6e3ceac697a483cd713833fd038086d49ec11718b7d1d3a210b3aae6dd22", + "nativeSource": { + "path": "internal/command/conformanceprofile", + "canonicalDigest": "sha256:5d71e7eea7501a11f5223c7626cddbf495b1c0fd16685d2432139b825bdef627", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/conformanceprofile/conformanceprofile_test.go", + "test": "TestBuildProfileResolvesRequiredSurfaceAndRejectsMissingSurface", + "command": "go test ./internal/command/conformanceprofile -run '^TestBuildProfileResolvesRequiredSurfaceAndRejectsMissingSurface$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.conformance-profile.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.conformance-profile.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.conformance-profile.output.v1.root-shape", + "rootDefinitionDigest": "sha256:11ee54032e139a25dda80337509ab6633ed31e297f9bc7461769badb2f5e0e0d", + "nativeSource": { + "path": "internal/command/conformanceprofile", + "canonicalDigest": "sha256:5d71e7eea7501a11f5223c7626cddbf495b1c0fd16685d2432139b825bdef627", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/conformanceprofile/conformanceprofile_test.go", + "test": "TestBuildProfileResolvesRequiredSurfaceAndRejectsMissingSurface", + "command": "go test ./internal/command/conformanceprofile -run '^TestBuildProfileResolvesRequiredSurfaceAndRejectsMissingSurface$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.conformance-profile.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "custom-rule-boundary", @@ -440,7 +1055,61 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.custom-rule-boundary.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.custom-rule-boundary.input.v1.root-shape", + "rootDefinitionDigest": "sha256:27fbb8d49c48e09422d37d0e0d17bf376cdb8a1d7dde2b6b763fe0edafb89809", + "nativeSource": { + "path": "internal/command/customruleboundary", + "canonicalDigest": "sha256:9b8e734953e1863dc99315dfa50f73c06213cecab0717cd616a09bde2a39efc0", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/customruleboundary/customruleboundary_test.go", + "test": "TestBuildAdmitsBoundedCustomRuleAndRejectsUnsafeEffects", + "command": "go test ./internal/command/customruleboundary -run '^TestBuildAdmitsBoundedCustomRuleAndRejectsUnsafeEffects$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.custom-rule-boundary.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.custom-rule-boundary.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.custom-rule-boundary.output.v1.root-shape", + "rootDefinitionDigest": "sha256:860b4edcf72ddc35fb6e39df858831371edb8d5b3c10356af9f217dbec2daf8f", + "nativeSource": { + "path": "internal/command/customruleboundary", + "canonicalDigest": "sha256:9b8e734953e1863dc99315dfa50f73c06213cecab0717cd616a09bde2a39efc0", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/customruleboundary/customruleboundary_test.go", + "test": "TestBuildAdmitsBoundedCustomRuleAndRejectsUnsafeEffects", + "command": "go test ./internal/command/customruleboundary -run '^TestBuildAdmitsBoundedCustomRuleAndRejectsUnsafeEffects$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.custom-rule-boundary.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "deployment-evidence-admission", @@ -454,7 +1123,63 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.deployment-evidence-admission.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.deployment-evidence-admission.input.v1.root-shape", + "rootDefinitionDigest": "sha256:674fb66250fbe3512562097e1b79c6b24b344f72392b8b0247afb73c6c5aa2e3", + "nativeSource": { + "path": "internal/command/deploymentevidenceadmission", + "canonicalDigest": "sha256:2c8994be0a5c992f78ac6e7137f2948a4e38848e787cc7d878898c6b33300edb", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/deploymentevidenceadmission/deployment_evidence_admission_test.go", + "test": "TestBuildAdmitsCandidateEvidenceAndRejectsUnpinnedImages", + "command": "go test ./internal/command/deploymentevidenceadmission -run '^TestBuildAdmitsCandidateEvidenceAndRejectsUnpinnedImages$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.deployment-evidence-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-QUALITY-017" + ] + }, + "outputContract": { + "contractId": "proofkit.deployment-evidence-admission.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.deployment-evidence-admission.output.v1.root-shape", + "rootDefinitionDigest": "sha256:c323ca5f340711192249783fbca21801b5f4f2bc06158e3f9b4d03a349c4c6a9", + "nativeSource": { + "path": "internal/command/deploymentevidenceadmission", + "canonicalDigest": "sha256:2c8994be0a5c992f78ac6e7137f2948a4e38848e787cc7d878898c6b33300edb", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/deploymentevidenceadmission/deployment_evidence_admission_test.go", + "test": "TestBuildAdmitsCandidateEvidenceAndRejectsUnpinnedImages", + "command": "go test ./internal/command/deploymentevidenceadmission -run '^TestBuildAdmitsCandidateEvidenceAndRejectsUnpinnedImages$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.deployment-evidence-admission.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-QUALITY-017" + ] + } }, { "command": "document-lifecycle-boundary", @@ -468,7 +1193,63 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.document-lifecycle-boundary.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.document-lifecycle-boundary.input.v1.root-shape", + "rootDefinitionDigest": "sha256:70cf3116106967a02a81583b000b2b06708d377973e5639278d5f958c6ad6a38", + "nativeSource": { + "path": "internal/command/documentlifecycle", + "canonicalDigest": "sha256:f00c50aa7e789d12ea0ac10c3599e7bed9d9f63a4229b1fcd33cfafcb13d6e35", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/documentlifecycle/documentlifecycle_test.go", + "test": "TestBuildAdmitsCurrentDurableDocumentAndRejectsAuthorityDrift", + "command": "go test ./internal/command/documentlifecycle -run '^TestBuildAdmitsCurrentDurableDocumentAndRejectsAuthorityDrift$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.document-lifecycle-boundary.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-014" + ] + }, + "outputContract": { + "contractId": "proofkit.document-lifecycle-boundary.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.document-lifecycle-boundary.output.v1.root-shape", + "rootDefinitionDigest": "sha256:59a170aece56eb913f83ff7c62817fcc070fa1b69c1cc7f8a065d561c02041c4", + "nativeSource": { + "path": "internal/command/documentlifecycle", + "canonicalDigest": "sha256:f00c50aa7e789d12ea0ac10c3599e7bed9d9f63a4229b1fcd33cfafcb13d6e35", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/documentlifecycle/documentlifecycle_test.go", + "test": "TestBuildAdmitsCurrentDurableDocumentAndRejectsAuthorityDrift", + "command": "go test ./internal/command/documentlifecycle -run '^TestBuildAdmitsCurrentDurableDocumentAndRejectsAuthorityDrift$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.document-lifecycle-boundary.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-014" + ] + } }, { "command": "evidence-graph", @@ -482,7 +1263,63 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.evidence-graph.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.evidence-graph.input.v1.root-shape", + "rootDefinitionDigest": "sha256:7ffe236ab203cf411dbfda6008ec8c10cc15691261031d4ed9ed90d81525dc12", + "nativeSource": { + "path": "internal/command/requirementbinding", + "canonicalDigest": "sha256:90bada8c868751cef16683475839174de48dd18936e3d79a74e24febcda1e09b", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/requirementbinding/projections_test.go", + "test": "TestBuildEvidenceGraphBuildsGraphAndRejectsFailedReport", + "command": "go test ./internal/command/requirementbinding -run '^TestBuildEvidenceGraphBuildsGraphAndRejectsFailedReport$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.evidence-graph.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-003" + ] + }, + "outputContract": { + "contractId": "proofkit.evidence-graph.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.evidence-graph.output.v1.root-shape", + "rootDefinitionDigest": "sha256:2be3545f9329bb4e4ba4971fc04bf5811959cf0087fd7ab3a7c2c8b45b8d714a", + "nativeSource": { + "path": "internal/command/requirementbinding", + "canonicalDigest": "sha256:90bada8c868751cef16683475839174de48dd18936e3d79a74e24febcda1e09b", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/requirementbinding/projections_test.go", + "test": "TestBuildEvidenceGraphBuildsGraphAndRejectsFailedReport", + "command": "go test ./internal/command/requirementbinding -run '^TestBuildEvidenceGraphBuildsGraphAndRejectsFailedReport$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.evidence-graph.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-003" + ] + } }, { "command": "external-consumer", @@ -496,7 +1333,63 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.external-consumer.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.external-consumer.input.v1.root-shape", + "rootDefinitionDigest": "sha256:8434bcea866d7e26d93fc896d88e16659252c6b5b7057a60525d9c1b64c1bf40", + "nativeSource": { + "path": "internal/command/externalconsumer", + "canonicalDigest": "sha256:2319bb14baed7ece382f022d3b397cdf241844a2d437716cc5098661486e9a25", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/externalconsumer/externalconsumer_test.go", + "test": "TestBuildAdmitsExternalConsumerProofAndRejectsWorkspaceLock", + "command": "go test ./internal/command/externalconsumer -run '^TestBuildAdmitsExternalConsumerProofAndRejectsWorkspaceLock$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.external-consumer.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-QUALITY-014" + ] + }, + "outputContract": { + "contractId": "proofkit.external-consumer.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.external-consumer.output.v1.root-shape", + "rootDefinitionDigest": "sha256:90c984c94def164352e2a79339ff0397975563248794854c5ba44842e67f1252", + "nativeSource": { + "path": "internal/command/externalconsumer", + "canonicalDigest": "sha256:2319bb14baed7ece382f022d3b397cdf241844a2d437716cc5098661486e9a25", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/externalconsumer/externalconsumer_test.go", + "test": "TestBuildAdmitsExternalConsumerProofAndRejectsWorkspaceLock", + "command": "go test ./internal/command/externalconsumer -run '^TestBuildAdmitsExternalConsumerProofAndRejectsWorkspaceLock$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.external-consumer.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-QUALITY-014" + ] + } }, { "command": "gradual-adoption", @@ -512,9 +1405,63 @@ "--contract-envelope", "--input", "--input-pointer" - ] - }, - { + ], + "inputContract": { + "contractId": "proofkit.gradual-adoption.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.gradual-adoption.input.v1.root-shape", + "rootDefinitionDigest": "sha256:b4a0fdae3fd0e7b7ef4c3565e4184b993c9d612e527489d6656ea2e5747d0e1f", + "nativeSource": { + "path": "internal/command/gradualadoption", + "canonicalDigest": "sha256:6f8130542341ff7954cc737cc79d6e8e797b64b6ab84b9eb3c598bd42c3291ba", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/gradualadoption/gradualadoption_test.go", + "test": "TestBuildRejectsRollbackShellControlCommand", + "command": "go test ./internal/command/gradualadoption -run '^TestBuildRejectsRollbackShellControlCommand$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.gradual-adoption.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.gradual-adoption.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.gradual-adoption.output.v1.root-shape", + "rootDefinitionDigest": "sha256:f780843a1d9afb5e7a82c5eefeca8f41d4ee8e852731ec3e690d1d8aedf46b16", + "nativeSource": { + "path": "internal/command/gradualadoption", + "canonicalDigest": "sha256:6f8130542341ff7954cc737cc79d6e8e797b64b6ab84b9eb3c598bd42c3291ba", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/gradualadoption/gradualadoption_test.go", + "test": "TestBuildRejectsRollbackShellControlCommand", + "command": "go test ./internal/command/gradualadoption -run '^TestBuildRejectsRollbackShellControlCommand$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.gradual-adoption.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } + }, + { "command": "gradual-adoption-bootstrap", "input": "required", "stdin": true, @@ -531,7 +1478,61 @@ "--input", "--input-pointer", "--materialization-manifest" - ] + ], + "inputContract": { + "contractId": "proofkit.gradual-adoption-bootstrap.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.gradual-adoption-bootstrap.input.v1.root-shape", + "rootDefinitionDigest": "sha256:e576d1d74d9eb01246ed78c668b09f84e2d7e256a83c08bfe3facdca3b27c00f", + "nativeSource": { + "path": "internal/command/gradualadoption", + "canonicalDigest": "sha256:6f8130542341ff7954cc737cc79d6e8e797b64b6ab84b9eb3c598bd42c3291ba", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/gradualadoption/gradualadoption_test.go", + "test": "TestBootstrapRejectsUnknownRootAndNestedFields", + "command": "go test ./internal/command/gradualadoption -run '^TestBootstrapRejectsUnknownRootAndNestedFields$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.gradual-adoption-bootstrap.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.gradual-adoption-bootstrap.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.gradual-adoption-bootstrap.output.v1.root-shape", + "rootDefinitionDigest": "sha256:3ac22a1a8e4a1d4e9793b575370abecfd3b5aa387dbeb6d78c4c59b325f55ab9", + "nativeSource": { + "path": "internal/command/gradualadoption", + "canonicalDigest": "sha256:6f8130542341ff7954cc737cc79d6e8e797b64b6ab84b9eb3c598bd42c3291ba", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/gradualadoption/gradualadoption_test.go", + "test": "TestBootstrapRejectsUnknownRootAndNestedFields", + "command": "go test ./internal/command/gradualadoption -run '^TestBootstrapRejectsUnknownRootAndNestedFields$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.gradual-adoption-bootstrap.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "gradual-adoption-guidance", @@ -552,7 +1553,63 @@ "--input", "--input-pointer", "--touched-rule-id" - ] + ], + "inputContract": { + "contractId": "proofkit.gradual-adoption-guidance.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.gradual-adoption-guidance.input.v1.root-shape", + "rootDefinitionDigest": "sha256:e25b962c3a1955e3f27b6449843f7fed214534c497e8a5082b21347ec1246ed3", + "nativeSource": { + "path": "internal/command/gradualadoption", + "canonicalDigest": "sha256:6f8130542341ff7954cc737cc79d6e8e797b64b6ab84b9eb3c598bd42c3291ba", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/gradualadoption/guidance_test.go", + "test": "TestGuidanceEnforcementFailsClosedForCandidateBoundaries", + "command": "go test ./internal/command/gradualadoption -run '^TestGuidanceEnforcementFailsClosedForCandidateBoundaries$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.gradual-adoption-guidance.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-RETIRE-007" + ] + }, + "outputContract": { + "contractId": "proofkit.gradual-adoption-guidance.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.gradual-adoption-guidance.output.v1.root-shape", + "rootDefinitionDigest": "sha256:311f17383cc0c6cff4468d590ca52fa99824af93e061a5e606d90916c16df48f", + "nativeSource": { + "path": "internal/command/gradualadoption", + "canonicalDigest": "sha256:6f8130542341ff7954cc737cc79d6e8e797b64b6ab84b9eb3c598bd42c3291ba", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/gradualadoption/guidance_test.go", + "test": "TestGuidanceEnforcementFailsClosedForCandidateBoundaries", + "command": "go test ./internal/command/gradualadoption -run '^TestGuidanceEnforcementFailsClosedForCandidateBoundaries$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.gradual-adoption-guidance.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-RETIRE-007" + ] + } }, { "command": "help", @@ -580,7 +1637,61 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.impact.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.impact.input.v1.root-shape", + "rootDefinitionDigest": "sha256:125e94c27e5de283c973643a90a95101f818545718d9cf12cf89a58d511c8cf1", + "nativeSource": { + "path": "internal/command/impact", + "canonicalDigest": "sha256:5acab1671cae3f05faf28519d08ecbc869be4043c2b10d196b6b6089b82dd4da", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/impact/impact_test.go", + "test": "TestBuildRoutesChangedRecordToObligationAndRejectsUnboundProofChange", + "command": "go test ./internal/command/impact -run '^TestBuildRoutesChangedRecordToObligationAndRejectsUnboundProofChange$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.impact.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.impact.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.impact.output.v1.root-shape", + "rootDefinitionDigest": "sha256:a979e09258d551c600da01624aab78353c1434bb92baf51e24b33ae6baa85044", + "nativeSource": { + "path": "internal/command/impact", + "canonicalDigest": "sha256:5acab1671cae3f05faf28519d08ecbc869be4043c2b10d196b6b6089b82dd4da", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/impact/impact_test.go", + "test": "TestBuildRoutesChangedRecordToObligationAndRejectsUnboundProofChange", + "command": "go test ./internal/command/impact -run '^TestBuildRoutesChangedRecordToObligationAndRejectsUnboundProofChange$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.impact.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "init", @@ -616,7 +1727,34 @@ "state", "summary" ], - "nonClaim": "Init does not read repository files, discover tests, write files, create requirements, or approve merge/release/rollout." + "nonClaim": "Init does not read repository files, discover tests, write files, create requirements, or approve merge/release/rollout.", + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.init.output.v1.root-shape", + "rootDefinitionDigest": "sha256:9f57ccd7905a2d8cbffeb8cf3495d338ea67c0ebfe66889b4d5ee16434be5f88", + "nativeSource": { + "path": "internal/command/initplan", + "canonicalDigest": "sha256:b7cf7f6d0cf534c062ac6b9aa20129308aae0b1a1716e2be74569a5f2bd7dfcf", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/app/cli_abi_test.go", + "test": "TestCLIABIGoldenCorpus", + "command": "go test ./internal/app -run '^TestCLIABIGoldenCorpus$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.init.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-QUALITY-021", + "REQ-PROOFKIT-SPEC-009", + "REQ-PROOFKIT-SPEC-011", + "REQ-PROOFKIT-SPEC-012" + ] } }, { @@ -654,6 +1792,29 @@ "nonClaims": [ "The generated source is not a package-root SDK contract or npm subpath API.", "The generated source does not resolve package pins, lockfiles, repository roots, installed binary paths, native witnesses, proof freshness, merge policy, or rollout policy." + ], + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.json-report-cli-adapter-source.output.v1.root-shape", + "rootDefinitionDigest": "sha256:9378c7663485f39089cf6c39a3d2f778e28b5801637229fe9524a9652a7b0737", + "nativeSource": { + "path": "internal/command/jsonreportcliadaptersource", + "canonicalDigest": "sha256:f806f423a29816180d2fc5d0b8735276d39bd86ec3cbaa8282a1b5a3015b7399", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source_test.go", + "test": "TestGeneratedTypeScriptAdapterExecutesCoreSemantics", + "command": "go test ./internal/command/jsonreportcliadaptersource -run '^TestGeneratedTypeScriptAdapterExecutesCoreSemantics$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.json-report-cli-adapter-source.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" ] } }, @@ -682,7 +1843,62 @@ "sourceProofOwners", "targetProofkitRefs" ], - "nonClaim": "Migration parity admission does not approve old-owner deletion, execute native witnesses, authenticate evidence, or prove semantic equivalence." + "nonClaim": "Migration parity admission does not approve old-owner deletion, execute native witnesses, authenticate evidence, or prove semantic equivalence.", + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.migration-parity-admission.input.v1.root-shape", + "rootDefinitionDigest": "sha256:a1f8ab777249d7ff613bfb3256cd96514235ac90d02ad39ce8720330cbd71d38", + "nativeSource": { + "path": "internal/command/migrationparityadmission", + "canonicalDigest": "sha256:2e19dca45e5cc77a74e65a30e0d2cbcd27279a5b17d43bd677686ade04b15585", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/migrationparityadmission/migrationparityadmission_test.go", + "test": "TestBuildAdmitsMatchedParityAndRejectsDigestDrift", + "command": "go test ./internal/command/migrationparityadmission -run '^TestBuildAdmitsMatchedParityAndRejectsDigestDrift$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "paritySetId", + "sourceProofOwners[]", + "targetProofkitRefs[]", + "parityRecords[]", + "nonClaims[]", + "root-shape-only definition proofkit.migration-parity-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.migration-parity-admission.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.migration-parity-admission.output.v1.root-shape", + "rootDefinitionDigest": "sha256:aaea5c7d9d12e1fbc406d080dcda5aca84351c5a2048d40535a1b3895cc37440", + "nativeSource": { + "path": "internal/command/migrationparityadmission", + "canonicalDigest": "sha256:2e19dca45e5cc77a74e65a30e0d2cbcd27279a5b17d43bd677686ade04b15585", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/migrationparityadmission/migrationparityadmission_test.go", + "test": "TestBuildAdmitsMatchedParityAndRejectsDigestDrift", + "command": "go test ./internal/command/migrationparityadmission -run '^TestBuildAdmitsMatchedParityAndRejectsDigestDrift$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.migration-parity-admission.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] } }, { @@ -713,7 +1929,65 @@ "sourceProofOwners", "targetProofkitRefs" ], - "nonClaim": "Migration plans route caller-owned review work only and do not edit files, delete old owners, approve merge, or prove proof freshness." + "nonClaim": "Migration plans route caller-owned review work only and do not edit files, delete old owners, approve merge, or prove proof freshness.", + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.migration-plan.input.v1.root-shape", + "rootDefinitionDigest": "sha256:aacf82037705e3c7e7e699b59856d9c36c68dec835cf5cbe1798f9a8761d4072", + "nativeSource": { + "path": "internal/command/migrationplan", + "canonicalDigest": "sha256:868b51b4584272e1bd9a448c245ce5cf382a412eb7b39ebbd35676a8440b4ebf", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/migrationplan/migrationplan_test.go", + "test": "TestSortedFollowUpCommandsRejectsShellControlTokens", + "command": "go test ./internal/command/migrationplan -run '^TestSortedFollowUpCommandsRejectsShellControlTokens$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "migrationId", + "sourceProofOwners[]", + "targetProofkitRefs[]", + "parityEvidenceRefs[]", + "retainedOwners[]", + "retirementCandidates[]", + "followUpCommands[]", + "nonClaims[]", + "root-shape-only definition proofkit.migration-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.migration-plan.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.migration-plan.output.v1.root-shape", + "rootDefinitionDigest": "sha256:e6a152b5ca41396f25874b1d1ddd1dbc323bd9e927a18c5fe5088ad527fe65cd", + "nativeSource": { + "path": "internal/command/migrationplan", + "canonicalDigest": "sha256:868b51b4584272e1bd9a448c245ce5cf382a412eb7b39ebbd35676a8440b4ebf", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/migrationplan/migrationplan_test.go", + "test": "TestSortedFollowUpCommandsRejectsShellControlTokens", + "command": "go test ./internal/command/migrationplan -run '^TestSortedFollowUpCommandsRejectsShellControlTokens$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.migration-plan.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] } }, { @@ -730,7 +2004,63 @@ "--agent-envelope", "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.obligation-decision.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.obligation-decision.input.v1.root-shape", + "rootDefinitionDigest": "sha256:5c9e25b7a7e379f21cd586693f1303a823699c348129a35c20c79fe0a225597f", + "nativeSource": { + "path": "internal/command/obligationdecision", + "canonicalDigest": "sha256:444d161fe3c88a3106b8a7f7db0125753870f85bc2afd32f00dbb11429f71a28", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/obligationdecision/obligationdecision_test.go", + "test": "TestBuildAdmitsSatisfiedBlockingObligationsAndRejectsMissingReceipt", + "command": "go test ./internal/command/obligationdecision -run '^TestBuildAdmitsSatisfiedBlockingObligationsAndRejectsMissingReceipt$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.obligation-decision.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-013" + ] + }, + "outputContract": { + "contractId": "proofkit.obligation-decision.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.obligation-decision.output.v1.root-shape", + "rootDefinitionDigest": "sha256:0a9bf16564faaf4362e84b490ec24e722ed5b98617d8f4d7f62d8ce30e21c2e5", + "nativeSource": { + "path": "internal/command/obligationdecision", + "canonicalDigest": "sha256:444d161fe3c88a3106b8a7f7db0125753870f85bc2afd32f00dbb11429f71a28", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/obligationdecision/obligationdecision_test.go", + "test": "TestBuildAdmitsSatisfiedBlockingObligationsAndRejectsMissingReceipt", + "command": "go test ./internal/command/obligationdecision -run '^TestBuildAdmitsSatisfiedBlockingObligationsAndRejectsMissingReceipt$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.obligation-decision.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-013" + ] + } }, { "command": "package-runtime-dependency-admission", @@ -771,7 +2101,65 @@ "resolvedEntryPoint" ], "lockfileIntegrity": "npm-style sha256, sha384, or sha512 integrity string admitted from caller-owned lockfile facts", - "nonClaim": "The command does not read lockfiles, resolve packages, authenticate registry access, execute witnesses, or prove proof freshness." + "nonClaim": "The command does not read lockfiles, resolve packages, authenticate registry access, execute witnesses, or prove proof freshness.", + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.package-runtime-dependency-admission.input.v1.root-shape", + "rootDefinitionDigest": "sha256:d831ad36980053791cd597bbba51921f2b72a9ba49d878415b945678c52c41ed", + "nativeSource": { + "path": "internal/command/packageruntimedependency", + "canonicalDigest": "sha256:165d2797cc5b7642ee294b344bdb7e4c7213717a0f167fc00073768b2a5b7f05", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/packageruntimedependency/package_runtime_dependency_test.go", + "test": "TestBuildAdmitsExternalRuntimeDependencyAndRejectsWorkspaceResolution", + "command": "go test ./internal/command/packageruntimedependency -run '^TestBuildAdmitsExternalRuntimeDependencyAndRejectsWorkspaceResolution$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "reportId", + "expectedDependencySpec", + "expectedLockfileIntegrity", + "expectedPackageName", + "expectedPackageVersion", + "admissibleLocations{}", + "packageResolution{}", + "nonClaims[]", + "root-shape-only definition proofkit.package-runtime-dependency-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.package-runtime-dependency-admission.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.package-runtime-dependency-admission.output.v1.root-shape", + "rootDefinitionDigest": "sha256:e6c9e94611b5de09befb45f56d1770bdf1e32a96752cf3e5fb8a58e8c6ecfc54", + "nativeSource": { + "path": "internal/command/packageruntimedependency", + "canonicalDigest": "sha256:165d2797cc5b7642ee294b344bdb7e4c7213717a0f167fc00073768b2a5b7f05", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/packageruntimedependency/package_runtime_dependency_test.go", + "test": "TestBuildAdmitsExternalRuntimeDependencyAndRejectsWorkspaceResolution", + "command": "go test ./internal/command/packageruntimedependency -run '^TestBuildAdmitsExternalRuntimeDependencyAndRejectsWorkspaceResolution$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.package-runtime-dependency-admission.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] } }, { @@ -790,7 +2178,68 @@ "--input-pointer", "--pilot", "--stack-diverse" - ] + ], + "inputContract": { + "contractId": "proofkit.pilot-admission.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.pilot-admission.input.v1.root-shape", + "rootDefinitionDigest": "sha256:984795f4b9c38763b001778fb2e072b0dc3e9eb6fac91b1ed966fd34382399c4", + "nativeSource": { + "path": "internal/command/pilotadmission", + "canonicalDigest": "sha256:bfb307fb7acea99f9ff9abf485416059f41c0330241bede17f20899b2d496a1b", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/pilotadmission/pilotadmission_test.go", + "test": "TestBuildRejectsUnknownPilotContractField", + "command": "go test ./internal/command/pilotadmission -run '^TestBuildRejectsUnknownPilotContractField$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.pilot-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.pilot-admission.output.v1", + "schemaVersion": 1, + "rootType": "union", + "closed": true, + "rootDefinitionRef": "proofkit.pilot-admission.output.v1.root-shape", + "rootDefinitionDigest": "sha256:b4e4dc05858e4ef86804e153db5e901c7c2af354d8d88b572ea86358e1194a57", + "nativeSources": [ + { + "path": "internal/app", + "canonicalDigest": "sha256:9a9280f7e5803767e0edc5aa5431d9163becc9265d767b2cc1acb22f454b70ea", + "evidenceClass": "source_checkout" + }, + { + "path": "internal/command/pilotadmission", + "canonicalDigest": "sha256:bfb307fb7acea99f9ff9abf485416059f41c0330241bede17f20899b2d496a1b", + "evidenceClass": "source_checkout" + } + ], + "nativeOutputWitnessSelector": { + "path": "internal/app/cli_abi_test.go", + "test": "TestStandaloneMultiVariantCommandsUseExactRootShapes", + "command": "go test ./internal/app -run '^TestStandaloneMultiVariantCommandsUseExactRootShapes$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.pilot-admission.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "producer-policy-self-proof", @@ -804,7 +2253,63 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.producer-policy-self-proof.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.producer-policy-self-proof.input.v1.root-shape", + "rootDefinitionDigest": "sha256:23c1bd4b44977a88008334b613ab484f43fd024aa5e43befde88d66910406713", + "nativeSource": { + "path": "internal/command/producerpolicyselfproof", + "canonicalDigest": "sha256:cb9631047161d951d0aa75db0cc60a7f9f7afe28a73dd7f1949cc568da7787f6", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/producerpolicyselfproof/producerpolicyselfproof_test.go", + "test": "TestBuildRejectsPolicyChangeProvedByNewlyAdmittedProducerTuple", + "command": "go test ./internal/command/producerpolicyselfproof -run '^TestBuildRejectsPolicyChangeProvedByNewlyAdmittedProducerTuple$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.producer-policy-self-proof.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-013" + ] + }, + "outputContract": { + "contractId": "proofkit.producer-policy-self-proof.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.producer-policy-self-proof.output.v1.root-shape", + "rootDefinitionDigest": "sha256:869f52f04871c604f7ee16118e5152ce761e9917374d96d0387858cfe9c132db", + "nativeSource": { + "path": "internal/command/producerpolicyselfproof", + "canonicalDigest": "sha256:cb9631047161d951d0aa75db0cc60a7f9f7afe28a73dd7f1949cc568da7787f6", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/producerpolicyselfproof/producerpolicyselfproof_test.go", + "test": "TestBuildRejectsPolicyChangeProvedByNewlyAdmittedProducerTuple", + "command": "go test ./internal/command/producerpolicyselfproof -run '^TestBuildRejectsPolicyChangeProvedByNewlyAdmittedProducerTuple$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.producer-policy-self-proof.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-013" + ] + } }, { "command": "proof-obligation-algebra", @@ -818,7 +2323,61 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.proof-obligation-algebra.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.proof-obligation-algebra.input.v1.root-shape", + "rootDefinitionDigest": "sha256:cf9e13e7e1186d4dcdcfcecfe33c4dc61f88791242b42a4a3a92b349c605b476", + "nativeSource": { + "path": "internal/command/proofobligationalgebra", + "canonicalDigest": "sha256:f8a7f7e89de6549acb0dc6853d8507916c6589617a4b2fbc22149eb3d7fce547", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/proofobligationalgebra/proof_obligation_algebra_test.go", + "test": "TestBuildAdmitsAtomicObligationAndRejectsMissingRoute", + "command": "go test ./internal/command/proofobligationalgebra -run '^TestBuildAdmitsAtomicObligationAndRejectsMissingRoute$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.proof-obligation-algebra.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.proof-obligation-algebra.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.proof-obligation-algebra.output.v1.root-shape", + "rootDefinitionDigest": "sha256:4133b3c92c688c56a04871ededc6a595b17690d02e70ee35b87f728c21109a37", + "nativeSource": { + "path": "internal/command/proofobligationalgebra", + "canonicalDigest": "sha256:f8a7f7e89de6549acb0dc6853d8507916c6589617a4b2fbc22149eb3d7fce547", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/proofobligationalgebra/proof_obligation_algebra_test.go", + "test": "TestBuildAdmitsAtomicObligationAndRejectsMissingRoute", + "command": "go test ./internal/command/proofobligationalgebra -run '^TestBuildAdmitsAtomicObligationAndRejectsMissingRoute$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.proof-obligation-algebra.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "proof-receipt-admission", @@ -832,7 +2391,63 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.proof-receipt-admission.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.proof-receipt-admission.input.v1.root-shape", + "rootDefinitionDigest": "sha256:de065dc77609d7ae2bbe2a5c78160c29b5b775dbd259e72935c97c5083af99dd", + "nativeSource": { + "path": "internal/command/proofreceiptadmission", + "canonicalDigest": "sha256:f811895de724d5233459617cb3353f563df397a4503ee2a10f518e8b38e8d677", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/proofreceiptadmission/proofreceiptadmission_test.go", + "test": "TestBuildAdmitsAdvisoryReceiptAndRejectsMergeSatisfyingWithoutProvenance", + "command": "go test ./internal/command/proofreceiptadmission -run '^TestBuildAdmitsAdvisoryReceiptAndRejectsMergeSatisfyingWithoutProvenance$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.proof-receipt-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-013" + ] + }, + "outputContract": { + "contractId": "proofkit.proof-receipt-admission.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.proof-receipt-admission.output.v1.root-shape", + "rootDefinitionDigest": "sha256:80c30c4767fb3e65972a382671c1f207c6150ba1a29612bee07915903bf8d46f", + "nativeSource": { + "path": "internal/command/proofreceiptadmission", + "canonicalDigest": "sha256:f811895de724d5233459617cb3353f563df397a4503ee2a10f518e8b38e8d677", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/proofreceiptadmission/proofreceiptadmission_test.go", + "test": "TestBuildAdmitsAdvisoryReceiptAndRejectsMergeSatisfyingWithoutProvenance", + "command": "go test ./internal/command/proofreceiptadmission -run '^TestBuildAdmitsAdvisoryReceiptAndRejectsMergeSatisfyingWithoutProvenance$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.proof-receipt-admission.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-013" + ] + } }, { "command": "proof-slice", @@ -846,7 +2461,63 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.proof-slice.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.proof-slice.input.v1.root-shape", + "rootDefinitionDigest": "sha256:71914c95c70dbace0ebce1dc4609624e3b91c994cd8c5adc20ed84d4200f0ac9", + "nativeSource": { + "path": "internal/command/requirementbinding", + "canonicalDigest": "sha256:90bada8c868751cef16683475839174de48dd18936e3d79a74e24febcda1e09b", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/requirementbinding/projections_test.go", + "test": "TestBuildProofSliceSelectsRequirementsAndRejectsFailedReport", + "command": "go test ./internal/command/requirementbinding -run '^TestBuildProofSliceSelectsRequirementsAndRejectsFailedReport$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.proof-slice.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-003" + ] + }, + "outputContract": { + "contractId": "proofkit.proof-slice.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.proof-slice.output.v1.root-shape", + "rootDefinitionDigest": "sha256:5fbdf25c90a2020525f97453c9d9fe5a774fe0e2eacf00d1b2aea3ff5f4fa0f0", + "nativeSource": { + "path": "internal/command/requirementbinding", + "canonicalDigest": "sha256:90bada8c868751cef16683475839174de48dd18936e3d79a74e24febcda1e09b", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/requirementbinding/projections_test.go", + "test": "TestBuildProofSliceSelectsRequirementsAndRejectsFailedReport", + "command": "go test ./internal/command/requirementbinding -run '^TestBuildProofSliceSelectsRequirementsAndRejectsFailedReport$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.proof-slice.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-003" + ] + } }, { "command": "readiness-closeout", @@ -860,7 +2531,63 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.readiness-closeout.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.readiness-closeout.input.v1.root-shape", + "rootDefinitionDigest": "sha256:7db0df3e9be3eba731937a4d887aa031cd70d3d5db36de3c630a1bced1d04bcf", + "nativeSource": { + "path": "internal/command/readinesscloseout", + "canonicalDigest": "sha256:8e706df865d00d2258328a6be9700a0200f856335b018209a63af5f06b0052d6", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/readinesscloseout/readinesscloseout_test.go", + "test": "TestBuildRejectsBroadNegationAndFrontierOverclaim", + "command": "go test ./internal/command/readinesscloseout -run '^TestBuildRejectsBroadNegationAndFrontierOverclaim$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.readiness-closeout.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-QUALITY-017" + ] + }, + "outputContract": { + "contractId": "proofkit.readiness-closeout.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.readiness-closeout.output.v1.root-shape", + "rootDefinitionDigest": "sha256:0ef2b994e1ee6fed9ce10740c0464dda72c3df220a9453e7a6a221fba31cec29", + "nativeSource": { + "path": "internal/command/readinesscloseout", + "canonicalDigest": "sha256:8e706df865d00d2258328a6be9700a0200f856335b018209a63af5f06b0052d6", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/readinesscloseout/readinesscloseout_test.go", + "test": "TestBuildRejectsBroadNegationAndFrontierOverclaim", + "command": "go test ./internal/command/readinesscloseout -run '^TestBuildRejectsBroadNegationAndFrontierOverclaim$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.readiness-closeout.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-QUALITY-017" + ] + } }, { "command": "receipt-currentness-scope", @@ -874,7 +2601,61 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.receipt-currentness-scope.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.receipt-currentness-scope.input.v1.root-shape", + "rootDefinitionDigest": "sha256:912f60b637aa0ca917b1c90d4caa4e3e65ec60ad0c5529d0da9d815fc766c5f5", + "nativeSource": { + "path": "internal/command/receiptcurrentnessscope", + "canonicalDigest": "sha256:82b133cede2489ecf3b813b83f0453f525c623b53ac383607f3ff31e84a7c450", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/receiptcurrentnessscope/receipt_currentness_scope_test.go", + "test": "TestBuildAdmitsCurrentScopedReceiptAndRejectsStaleDigest", + "command": "go test ./internal/command/receiptcurrentnessscope -run '^TestBuildAdmitsCurrentScopedReceiptAndRejectsStaleDigest$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.receipt-currentness-scope.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.receipt-currentness-scope.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.receipt-currentness-scope.output.v1.root-shape", + "rootDefinitionDigest": "sha256:30a1b7ef616c1e44bbdbebc6e511231166a2d1ea600d8d5954484fe8c0e7cdf4", + "nativeSource": { + "path": "internal/command/receiptcurrentnessscope", + "canonicalDigest": "sha256:82b133cede2489ecf3b813b83f0453f525c623b53ac383607f3ff31e84a7c450", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/receiptcurrentnessscope/receipt_currentness_scope_test.go", + "test": "TestBuildAdmitsCurrentScopedReceiptAndRejectsStaleDigest", + "command": "go test ./internal/command/receiptcurrentnessscope -run '^TestBuildAdmitsCurrentScopedReceiptAndRejectsStaleDigest$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.receipt-currentness-scope.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "receipt-producer-admission", @@ -888,21 +2669,133 @@ "allowedFlags": [ "--input", "--input-pointer" - ] - }, - { - "command": "receipt-trust-class", - "input": "required", - "stdin": true, - "inputPointer": true, - "scopeClass": "explicit_caller_input", - "outputModes": [ - "json" ], - "allowedFlags": [ + "inputContract": { + "contractId": "proofkit.receipt-producer-admission.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.receipt-producer-admission.input.v1.root-shape", + "rootDefinitionDigest": "sha256:b62a461a38e3f878dc9bf41d508102ccd66d10fd659a4830a04d2cc67c0e74cc", + "nativeSource": { + "path": "internal/command/receiptproduceradmission", + "canonicalDigest": "sha256:9483469a2e0ed42726280709f7e7f4293337328ff210b2d8a7ce097dca413d71", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/receiptproduceradmission/receiptproduceradmission_test.go", + "test": "TestBuildRejectsAdvisoryProducerForMergeSatisfyingReceipt", + "command": "go test ./internal/command/receiptproduceradmission -run '^TestBuildRejectsAdvisoryProducerForMergeSatisfyingReceipt$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.receipt-producer-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-013" + ] + }, + "outputContract": { + "contractId": "proofkit.receipt-producer-admission.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.receipt-producer-admission.output.v1.root-shape", + "rootDefinitionDigest": "sha256:1704f4b6482065ded5ee235f1e32aad5b0ae83a8170968fcc1c617db3789c1c0", + "nativeSource": { + "path": "internal/command/receiptproduceradmission", + "canonicalDigest": "sha256:9483469a2e0ed42726280709f7e7f4293337328ff210b2d8a7ce097dca413d71", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/receiptproduceradmission/receiptproduceradmission_test.go", + "test": "TestBuildRejectsAdvisoryProducerForMergeSatisfyingReceipt", + "command": "go test ./internal/command/receiptproduceradmission -run '^TestBuildRejectsAdvisoryProducerForMergeSatisfyingReceipt$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.receipt-producer-admission.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-013" + ] + } + }, + { + "command": "receipt-trust-class", + "input": "required", + "stdin": true, + "inputPointer": true, + "scopeClass": "explicit_caller_input", + "outputModes": [ + "json" + ], + "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.receipt-trust-class.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.receipt-trust-class.input.v1.root-shape", + "rootDefinitionDigest": "sha256:19b0af54f38516daf4c0fb386a16284821f07ee14028738b5287a09f8b3b2122", + "nativeSource": { + "path": "internal/command/receipttrustclass", + "canonicalDigest": "sha256:7bc30b98ef1e7f0fdef09f8a00d0f10158e0a8dca6a676736151eeb939a364da", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/receipttrustclass/receipt_trust_class_test.go", + "test": "TestBuildAdmitsTrustedReceiptAndRejectsMissingProvenance", + "command": "go test ./internal/command/receipttrustclass -run '^TestBuildAdmitsTrustedReceiptAndRejectsMissingProvenance$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.receipt-trust-class.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-013" + ] + }, + "outputContract": { + "contractId": "proofkit.receipt-trust-class.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.receipt-trust-class.output.v1.root-shape", + "rootDefinitionDigest": "sha256:3914567b0719287a883352e711121f71610ab041df74a009edd2363fb6907872", + "nativeSource": { + "path": "internal/command/receipttrustclass", + "canonicalDigest": "sha256:7bc30b98ef1e7f0fdef09f8a00d0f10158e0a8dca6a676736151eeb939a364da", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/receipttrustclass/receipt_trust_class_test.go", + "test": "TestBuildAdmitsTrustedReceiptAndRejectsMissingProvenance", + "command": "go test ./internal/command/receipttrustclass -run '^TestBuildAdmitsTrustedReceiptAndRejectsMissingProvenance$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.receipt-trust-class.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-013" + ] + } }, { "command": "registry-consumer", @@ -916,7 +2809,63 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.registry-consumer.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.registry-consumer.input.v1.root-shape", + "rootDefinitionDigest": "sha256:cc490b41d9f9e68c5121f81e702222ed3e717d462fa2059a0e0b76c0c82fb5e5", + "nativeSource": { + "path": "internal/command/registryconsumer", + "canonicalDigest": "sha256:8b208777ccc022c5ea6b2a1d74366acd885ea0089a802999bc0a896c3b3a9c63", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/registryconsumer/registryconsumer_test.go", + "test": "TestRegistryConsumerAcceptsRegistryReleaseProof", + "command": "go test ./internal/command/registryconsumer -run '^TestRegistryConsumerAcceptsRegistryReleaseProof$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.registry-consumer.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-QUALITY-014" + ] + }, + "outputContract": { + "contractId": "proofkit.registry-consumer.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.registry-consumer.output.v1.root-shape", + "rootDefinitionDigest": "sha256:70d7eb27630638e9b38fa930cd61954bedbc84aeb3916490c036c33777767529", + "nativeSource": { + "path": "internal/command/registryconsumer", + "canonicalDigest": "sha256:8b208777ccc022c5ea6b2a1d74366acd885ea0089a802999bc0a896c3b3a9c63", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/registryconsumer/registryconsumer_test.go", + "test": "TestRegistryConsumerAcceptsRegistryReleaseProof", + "command": "go test ./internal/command/registryconsumer -run '^TestRegistryConsumerAcceptsRegistryReleaseProof$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.registry-consumer.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-QUALITY-014" + ] + } }, { "command": "registry-consumer-proof-input-compose", @@ -930,7 +2879,63 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.registry-consumer-proof-input-compose.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.registry-consumer-proof-input-compose.input.v1.root-shape", + "rootDefinitionDigest": "sha256:0e324e9ebd262c2efbbea3b08eae59c521cbb61f99c5194b72bf5eba414d6fbb", + "nativeSource": { + "path": "internal/command/registryconsumerinputcompose", + "canonicalDigest": "sha256:58bd41d1e2d04b9e2553f0723fa15a4fa357ab29e787881c9b9b8e118c9faad3", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/registryconsumerinputcompose/registry_consumer_input_compose_test.go", + "test": "TestBuildComposesInputAcceptedByRegistryConsumer", + "command": "go test ./internal/command/registryconsumerinputcompose -run '^TestBuildComposesInputAcceptedByRegistryConsumer$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.registry-consumer-proof-input-compose.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-RETIRE-010" + ] + }, + "outputContract": { + "contractId": "proofkit.registry-consumer-proof-input-compose.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.registry-consumer-proof-input-compose.output.v1.root-shape", + "rootDefinitionDigest": "sha256:597b79a019ad1a3caf29a2d5fe723cfaf0b185d6897724f598f7132a3e653a6d", + "nativeSource": { + "path": "internal/command/registryconsumerinputcompose", + "canonicalDigest": "sha256:58bd41d1e2d04b9e2553f0723fa15a4fa357ab29e787881c9b9b8e118c9faad3", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/registryconsumerinputcompose/registry_consumer_input_compose_test.go", + "test": "TestBuildComposesInputAcceptedByRegistryConsumer", + "command": "go test ./internal/command/registryconsumerinputcompose -run '^TestBuildComposesInputAcceptedByRegistryConsumer$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.registry-consumer-proof-input-compose.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-RETIRE-010" + ] + } }, { "command": "release-authority", @@ -944,7 +2949,61 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.release-authority.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.release-authority.input.v1.root-shape", + "rootDefinitionDigest": "sha256:1aa30fc2229803387d497d1de0b4d95c1b1d732a9c1fda65c66a6db0ebb802c3", + "nativeSource": { + "path": "internal/command/releaseauthority", + "canonicalDigest": "sha256:ba1fcaed234b38817ab0ccb26f81e953688d84e164da88eca3aa0bd8b1cce19b", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/releaseauthority/releaseauthority_test.go", + "test": "TestBuildRejectsPrivateSourceNPMProvenanceClaim", + "command": "go test ./internal/command/releaseauthority -run '^TestBuildRejectsPrivateSourceNPMProvenanceClaim$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.release-authority.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.release-authority.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.release-authority.output.v1.root-shape", + "rootDefinitionDigest": "sha256:95a36a8fe2976c8b7fccb4a83d8c9913b7c11dcb89f78990ce91422c194bcb3d", + "nativeSource": { + "path": "internal/command/releaseauthority", + "canonicalDigest": "sha256:ba1fcaed234b38817ab0ccb26f81e953688d84e164da88eca3aa0bd8b1cce19b", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/releaseauthority/releaseauthority_test.go", + "test": "TestBuildRejectsPrivateSourceNPMProvenanceClaim", + "command": "go test ./internal/command/releaseauthority -run '^TestBuildRejectsPrivateSourceNPMProvenanceClaim$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.release-authority.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "rendered-artifact-freshness", @@ -958,7 +3017,61 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.rendered-artifact-freshness.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.rendered-artifact-freshness.input.v1.root-shape", + "rootDefinitionDigest": "sha256:525ed9c2b820df9f52dce95442bf48bccfba1368f82e9bf68b92c275cf79b51c", + "nativeSource": { + "path": "internal/command/renderedartifactfreshness", + "canonicalDigest": "sha256:56ec807644da3f730e2ca55cb7b4dcded4bfab5e707657600ecbcd211ef17a4e", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/renderedartifactfreshness/rendered_artifact_freshness_test.go", + "test": "TestBuildAdmitsFreshRenderedArtifactAndRejectsDigestDrift", + "command": "go test ./internal/command/renderedartifactfreshness -run '^TestBuildAdmitsFreshRenderedArtifactAndRejectsDigestDrift$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.rendered-artifact-freshness.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.rendered-artifact-freshness.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.rendered-artifact-freshness.output.v1.root-shape", + "rootDefinitionDigest": "sha256:99c5af92d1f98744436493c4330c78ed409da2f72be0bb7bb82670cf7fb8fe2f", + "nativeSource": { + "path": "internal/command/renderedartifactfreshness", + "canonicalDigest": "sha256:56ec807644da3f730e2ca55cb7b4dcded4bfab5e707657600ecbcd211ef17a4e", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/renderedartifactfreshness/rendered_artifact_freshness_test.go", + "test": "TestBuildAdmitsFreshRenderedArtifactAndRejectsDigestDrift", + "command": "go test ./internal/command/renderedartifactfreshness -run '^TestBuildAdmitsFreshRenderedArtifactAndRejectsDigestDrift$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.rendered-artifact-freshness.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "repo-profile-admission", @@ -993,7 +3106,57 @@ "credentialClass" ], "nonClaim": "Repo-profile admission checks explicit caller-owned command policy facts and does not execute commands or discover repository files." - } + }, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.repo-profile-admission.input.v1.root-shape", + "rootDefinitionDigest": "sha256:7cb2357b227c2669b5322712ce6e032315e18b726dde8ead31b1feb29e14b418", + "nativeSource": { + "path": "internal/command/repoprofileadmission", + "canonicalDigest": "sha256:47c0521b8a99c2aa96016735995d700b555601d0dc3705b7248db6926e789b9f", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/repoprofileadmission/repo_profile_admission_test.go", + "test": "TestBuildAdmitsValidRepoProfileAndRejectsRootPackageMismatch", + "command": "go test ./internal/command/repoprofileadmission -run '^TestBuildAdmitsValidRepoProfileAndRejectsRootPackageMismatch$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.repo-profile-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.repo-profile-admission.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.repo-profile-admission.output.v1.root-shape", + "rootDefinitionDigest": "sha256:acf8e7db39bfb1325d83c2cdfe68a7c243847d207e56691a3b2937b28f95d3e7", + "nativeSource": { + "path": "internal/command/repoprofileadmission", + "canonicalDigest": "sha256:47c0521b8a99c2aa96016735995d700b555601d0dc3705b7248db6926e789b9f", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/repoprofileadmission/repo_profile_admission_test.go", + "test": "TestBuildAdmitsValidRepoProfileAndRejectsRootPackageMismatch", + "command": "go test ./internal/command/repoprofileadmission -run '^TestBuildAdmitsValidRepoProfileAndRejectsRootPackageMismatch$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.repo-profile-admission.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] } }, { @@ -1008,7 +3171,63 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.requirement-authoring-plan.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-authoring-plan.input.v1.root-shape", + "rootDefinitionDigest": "sha256:86f8f4913b5316caa63c40d79125199b282d108cbd8b9711db21c2023d1a4ca4", + "nativeSource": { + "path": "internal/command/requirementauthoringplan", + "canonicalDigest": "sha256:91d82aa48a4c10b33f89b7b2db7443182b7702de4b0c2763a8104f2b0a997bdf", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/requirementauthoringplan/requirement_authoring_plan_test.go", + "test": "TestBuildRejectsCandidateSourceAdmissionFailure", + "command": "go test ./internal/command/requirementauthoringplan -run '^TestBuildRejectsCandidateSourceAdmissionFailure$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-authoring-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-012" + ] + }, + "outputContract": { + "contractId": "proofkit.requirement-authoring-plan.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-authoring-plan.output.v1.root-shape", + "rootDefinitionDigest": "sha256:019c920b72a4d2fba8f6364bff554442b0194ac42364d645d5b0b71f28df24b1", + "nativeSource": { + "path": "internal/command/requirementauthoringplan", + "canonicalDigest": "sha256:91d82aa48a4c10b33f89b7b2db7443182b7702de4b0c2763a8104f2b0a997bdf", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/requirementauthoringplan/requirement_authoring_plan_test.go", + "test": "TestBuildRejectsCandidateSourceAdmissionFailure", + "command": "go test ./internal/command/requirementauthoringplan -run '^TestBuildRejectsCandidateSourceAdmissionFailure$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-authoring-plan.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-012" + ] + } }, { "command": "requirement-bindings", @@ -1022,7 +3241,63 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.requirement-bindings.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-bindings.input.v1.root-shape", + "rootDefinitionDigest": "sha256:e7da70a2267f2ca13771e864bba6a1a0416132c2d56decb5db9f9694de021daf", + "nativeSource": { + "path": "internal/command/requirementbinding", + "canonicalDigest": "sha256:90bada8c868751cef16683475839174de48dd18936e3d79a74e24febcda1e09b", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/requirementbinding/projections_test.go", + "test": "TestBuildReportFailsUnknownRequirementBinding", + "command": "go test ./internal/command/requirementbinding -run '^TestBuildReportFailsUnknownRequirementBinding$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-bindings.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-003" + ] + }, + "outputContract": { + "contractId": "proofkit.requirement-bindings.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-bindings.output.v1.root-shape", + "rootDefinitionDigest": "sha256:f31dd686f28fce4bf74265b557dc25428179cbc282753b4bea01439cd98f73c2", + "nativeSource": { + "path": "internal/command/requirementbinding", + "canonicalDigest": "sha256:90bada8c868751cef16683475839174de48dd18936e3d79a74e24febcda1e09b", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/requirementbinding/projections_test.go", + "test": "TestBuildReportFailsUnknownRequirementBinding", + "command": "go test ./internal/command/requirementbinding -run '^TestBuildReportFailsUnknownRequirementBinding$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-bindings.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-003" + ] + } }, { "command": "requirement-browser-server", @@ -1063,7 +3338,7 @@ } ], "inputContract": { - "schemaVersion": 1, + "schemaVersion": 2, "workspaceRequiredFields": [ "context", "schemaVersion", @@ -1087,7 +3362,70 @@ "serve": true, "view": "workspace" }, - "jsonLayoutRule": "allowed for non-serving JSON plans; serving and one-shot modes reject the global layout option because one-shot terminal packets are fixed compact JSON" + "jsonLayoutRule": "allowed for non-serving JSON plans; serving and one-shot modes reject the global layout option because one-shot terminal packets are fixed compact JSON", + "contractId": "proofkit.requirement-browser-server.input.v2", + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-browser-server.input.v2.root-shape", + "rootDefinitionDigest": "sha256:50a7bb1b477b6067d410cf22db7e7564a9742aa7f6a270642149d372f0156007", + "nativeSource": { + "path": "internal/command/requirementbrowser", + "canonicalDigest": "sha256:db68fadae3f68184d118cfdd39befedde9783cbcdd798cf56337b43fc0f7d018", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/requirementbrowser/workspace_test.go", + "test": "TestV2DigestCoverageProjections", + "command": "go test ./internal/command/requirementbrowser -run '^TestV2DigestCoverageProjections$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "workspace mode: schemaVersion=2", + "workspace mode: workspaceId", + "workspace mode: context=proofkit.requirement-context schemaVersion=2 with strict v1 adapter", + "workspace mode: diffInput=proofkit.requirement-semantic-diff-input schemaVersion=2 (optional)", + "workspace mode: graphInput=proofkit.requirement-traceability-graph-input schemaVersion=2 (optional)", + "--session-mode values: browse|one-shot-question", + "one-shot-question requires --view workspace --serve --open", + "--session-timeout-seconds is 1..7200 and requires one-shot-question", + "source|proof|coverage|spec-tree modes retain their owner input contracts", + "root-shape-only definition proofkit.requirement-browser-server.input.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-009", + "REQ-PROOFKIT-SPEC-021" + ] + }, + "outputContract": { + "contractId": "proofkit.requirement-browser-server.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-browser-server.output.v1.root-shape", + "rootDefinitionDigest": "sha256:c2e7d851c7928560d4267fe85ebc0ae61e33c4c0ef15f7f7b01deb73bb80eda7", + "nativeSource": { + "path": "internal/command/requirementbrowser", + "canonicalDigest": "sha256:db68fadae3f68184d118cfdd39befedde9783cbcdd798cf56337b43fc0f7d018", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/requirementbrowser/workspace_test.go", + "test": "TestOneShotOutputVariantsUseExactRootShapes", + "command": "go test ./internal/command/requirementbrowser -run '^TestOneShotOutputVariantsUseExactRootShapes$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-browser-server.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-009", + "REQ-PROOFKIT-SPEC-021" + ] } }, { @@ -1106,7 +3444,80 @@ ], "requiredFlags": [ "--repo-root" - ] + ], + "inputContract": { + "contractId": "proofkit.requirement-context-compose.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-context-compose.input.v1.root-shape", + "rootDefinitionDigest": "sha256:41bc233c96bd468bc96eeb0447e037cacdbcf92bb04161052303e8747e4282f2", + "nativeSource": { + "path": "internal/command/requirementcontext", + "canonicalDigest": "sha256:120f236dc1e9e11e5cecb6237bf727fe513b75e83783f52d31872c6c2b20ae7e", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/app/requirement_context_cli_test.go", + "test": "TestRequirementContextCommandsComposeThroughWholeCLI", + "command": "go test ./internal/app -run '^TestRequirementContextCommandsComposeThroughWholeCLI$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "catalogId", + "specTree.path", + "requirementSources[] (non-empty)", + "requirementSources[].nodeId", + "requirementSources[].path", + "expectedSourceDigest (optional sha256 ref)", + "proofBinding.path (optional)", + "coverage.path (optional)", + "exact catalog paths only; no discovery", + "root-shape-only definition proofkit.requirement-context-compose.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-019", + "REQ-PROOFKIT-SPEC-020", + "REQ-PROOFKIT-SPEC-022", + "REQ-PROOFKIT-SPEC-023" + ] + }, + "outputContract": { + "contractId": "proofkit.requirement-context-compose.output.v2", + "schemaVersion": 2, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-context-compose.output.v2.root-shape", + "rootDefinitionDigest": "sha256:cdadd7b589d682a122cfe2b801f001a3b22a2269aa4977ebe073932d11e1f816", + "nativeSource": { + "path": "internal/command/requirementcontext", + "canonicalDigest": "sha256:120f236dc1e9e11e5cecb6237bf727fe513b75e83783f52d31872c6c2b20ae7e", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/requirementcontext/requirementcontext_test.go", + "test": "TestV1DigestCoverageAdapters", + "command": "go test ./internal/command/requirementcontext -run '^TestV1DigestCoverageAdapters$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=2", + "expectedDigestCoverage=none|partial|all", + "strict schemaVersion=1 adapter rejects mixed vocabulary", + "root-shape-only definition proofkit.requirement-context-compose.output.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-019", + "REQ-PROOFKIT-SPEC-020", + "REQ-PROOFKIT-SPEC-022", + "REQ-PROOFKIT-SPEC-023" + ] + } }, { "command": "requirement-context-slice", @@ -1120,7 +3531,76 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.requirement-context-slice.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-context-slice.input.v1.root-shape", + "rootDefinitionDigest": "sha256:43852cf1a52b3c1f000ec95e460fbad157a6b6a39958d20f81c9e1e4d31e3c6f", + "nativeSource": { + "path": "internal/command/requirementcontext", + "canonicalDigest": "sha256:120f236dc1e9e11e5cecb6237bf727fe513b75e83783f52d31872c6c2b20ae7e", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/app/requirement_context_cli_test.go", + "test": "TestRequirementContextCommandsComposeThroughWholeCLI", + "command": "go test ./internal/app -run '^TestRequirementContextCommandsComposeThroughWholeCLI$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "sliceId", + "context=proofkit.requirement-context schemaVersion=2 with strict v1 adapter", + "query.profile=routing|specification|proof|coverage|review", + "query.nodeIds[]|requirementIds[]|ownerIds[]|lifecycleStates[]", + "query.maxDepth=0..512", + "query.maxNodes=1..4096", + "query.maxRequirements=1..16384", + "root-shape-only definition proofkit.requirement-context-slice.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-019", + "REQ-PROOFKIT-SPEC-020", + "REQ-PROOFKIT-SPEC-022", + "REQ-PROOFKIT-SPEC-023" + ] + }, + "outputContract": { + "contractId": "proofkit.requirement-context-slice.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-context-slice.output.v1.root-shape", + "rootDefinitionDigest": "sha256:61f7fd43e2e9da5bde9b61bb86e02cbf337a43c6891ca888c1d42936e5b16456", + "nativeSource": { + "path": "internal/command/requirementcontext", + "canonicalDigest": "sha256:120f236dc1e9e11e5cecb6237bf727fe513b75e83783f52d31872c6c2b20ae7e", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/app/requirement_context_cli_test.go", + "test": "TestRequirementContextCommandsComposeThroughWholeCLI", + "command": "go test ./internal/app -run '^TestRequirementContextCommandsComposeThroughWholeCLI$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-context-slice.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-019", + "REQ-PROOFKIT-SPEC-020", + "REQ-PROOFKIT-SPEC-022", + "REQ-PROOFKIT-SPEC-023" + ] + } }, { "command": "requirement-coverage-input-compose", @@ -1180,6 +3660,30 @@ }, "nonClaims": [ "Coverage input composition does not execute native tests, authenticate receipts, decide freshness, or approve merge." + ], + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-coverage-input-compose.input.v1.root-shape", + "rootDefinitionDigest": "sha256:06fa8d0a592549cebdea1d7b9d428e090de7b57e265dbc263699b28dbe6eede8", + "nativeSource": { + "path": "internal/command/requirementcoverageinput", + "canonicalDigest": "sha256:bed47612a7a300b548f78fae3011282cd09caae98e9ad13628c5602cf364747a", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/requirementcoverageinput/requirementcoverageinput_test.go", + "test": "TestBuildComposesInputPreservesDeclaredUniverseAndAllowsDownstreamFailures", + "command": "go test ./internal/command/requirementcoverageinput -run '^TestBuildComposesInputPreservesDeclaredUniverseAndAllowsDownstreamFailures$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-coverage-input-compose.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-006" ] }, "outputContract": { @@ -1204,6 +3708,39 @@ "provenanceRule": "When normalizedTestEvidenceInventory is present, it preserves the admitted source-set envelope and its nested inventory must equal testEvidenceInventory.", "nonClaims": [ "Coverage input composition emits a direct requirement-coverage-view input and does not execute native tests, authenticate receipts, decide freshness, or approve merge." + ], + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-coverage-input-compose.output.v1.root-shape", + "rootDefinitionDigest": "sha256:7e913a998ba47601fd054dbec429d96cb2a546c8925d311f45aca86f1e038bec", + "nativeSource": { + "path": "internal/command/requirementcoverageinput", + "canonicalDigest": "sha256:bed47612a7a300b548f78fae3011282cd09caae98e9ad13628c5602cf364747a", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/requirementcoverageinput/requirementcoverageinput_test.go", + "test": "TestBuildComposesInputPreservesDeclaredUniverseAndAllowsDownstreamFailures", + "command": "go test ./internal/command/requirementcoverageinput -run '^TestBuildComposesInputPreservesDeclaredUniverseAndAllowsDownstreamFailures$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "compactProofContract", + "coverageUniverse", + "localEnvironmentPolicy", + "options", + "ownerInvariantRegistry", + "requirementProofBinding", + "requirementSource", + "schemaVersion", + "testEvidenceInventory", + "viewInputId", + "root-shape-only definition proofkit.requirement-coverage-input-compose.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-006" ] } }, @@ -1261,6 +3798,63 @@ ], "nonClaims": [ "Classification records describe Proofkit coverage diagnostics only; consumer repositories own severity policy, merge admission, escalation, and native witness execution." + ], + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-coverage-view.output.v1.root-shape", + "rootDefinitionDigest": "sha256:697d24d12d4772b67283f24da2f265372de241bd4741ac23f7b984ca96023818", + "nativeSource": { + "path": "internal/command/requirementcoverageview", + "canonicalDigest": "sha256:446947f5ccedc573a0839c4dfbf0ea35bbf529035838035e923f71f10a1f5695", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/requirementcoverageview/requirementcoverageview_test.go", + "test": "TestBuildJSONRejectsRouteOnlyCoverageForBlockingRequirement", + "command": "go test ./internal/command/requirementcoverageview -run '^TestBuildJSONRejectsRouteOnlyCoverageForBlockingRequirement$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "failureClassifications", + "failures", + "ownerInvariantCoverage", + "state", + "warningClassifications", + "warnings", + "root-shape-only definition proofkit.requirement-coverage-view.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-006" + ] + }, + "inputContract": { + "contractId": "proofkit.requirement-coverage-view.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-coverage-view.input.v1.root-shape", + "rootDefinitionDigest": "sha256:b5ddab0a55a381f002b39ece0f5727171f7984856e553200bbb62a49821ff6ac", + "nativeSource": { + "path": "internal/command/requirementcoverageview", + "canonicalDigest": "sha256:446947f5ccedc573a0839c4dfbf0ea35bbf529035838035e923f71f10a1f5695", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/requirementcoverageview/requirementcoverageview_test.go", + "test": "TestBuildJSONRejectsRouteOnlyCoverageForBlockingRequirement", + "command": "go test ./internal/command/requirementcoverageview -run '^TestBuildJSONRejectsRouteOnlyCoverageForBlockingRequirement$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-coverage-view.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-006" ] } }, @@ -1303,6 +3897,72 @@ ], "nonClaims": [ "Impact input composition emits a direct impact command input and does not execute native tests, authenticate receipts, decide freshness, or approve merge." + ], + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-impact-input-compose.output.v1.root-shape", + "rootDefinitionDigest": "sha256:9fd57d299d4e76bb3b0bf10f313a8dd9bfd593b8d15fa4844d5e342c4d929ee9", + "nativeSource": { + "path": "internal/command/requirementimpactinput", + "canonicalDigest": "sha256:014ecba30947ebee4f45c22ed0a06bfb2d47392e0d3420d1e4480a1c0111dd81", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/requirementimpactinput/requirementimpactinput_test.go", + "test": "TestBuildComposesInputAndRoutesChangedBlockingRequirement", + "command": "go test ./internal/command/requirementimpactinput -run '^TestBuildComposesInputAndRoutesChangedBlockingRequirement$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "baseCommit", + "baseRef", + "changedBindingRecordIds", + "changedPaths", + "changedRecordIds", + "changedWitnessPathCoverage", + "generatedArtifactRules", + "headCommit", + "headRef", + "ignoredProofLikePaths", + "nonClaims", + "obligationCatalog", + "preexistingFailures", + "proofLikePaths", + "schemaVersion", + "root-shape-only definition proofkit.requirement-impact-input-compose.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-010" + ] + }, + "inputContract": { + "contractId": "proofkit.requirement-impact-input-compose.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-impact-input-compose.input.v1.root-shape", + "rootDefinitionDigest": "sha256:e6af8f427f781395710407a87bfd5fa91a0b3c8aaaee437c2487472a243b22a6", + "nativeSource": { + "path": "internal/command/requirementimpactinput", + "canonicalDigest": "sha256:014ecba30947ebee4f45c22ed0a06bfb2d47392e0d3420d1e4480a1c0111dd81", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/requirementimpactinput/requirementimpactinput_test.go", + "test": "TestBuildComposesInputAndRoutesChangedBlockingRequirement", + "command": "go test ./internal/command/requirementimpactinput -run '^TestBuildComposesInputAndRoutesChangedBlockingRequirement$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-impact-input-compose.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-010" ] } }, @@ -1326,8 +3986,62 @@ "--empty-local-environment-policy", "--local-environment-class" ] - ] - }, + ], + "inputContract": { + "contractId": "proofkit.requirement-proof-resolver.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-proof-resolver.input.v1.root-shape", + "rootDefinitionDigest": "sha256:68ade17ac7e9fb3496ad0e7ea8ae9fbcf734afa4e6b8a44442e1af3c4f87bf15", + "nativeSource": { + "path": "internal/command/requirementbinding", + "canonicalDigest": "sha256:90bada8c868751cef16683475839174de48dd18936e3d79a74e24febcda1e09b", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/requirementbinding/compact_contract_test.go", + "test": "TestBuildResolverRejectsUnscopedCompactIdentity", + "command": "go test ./internal/command/requirementbinding -run '^TestBuildResolverRejectsUnscopedCompactIdentity$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-proof-resolver.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.requirement-proof-resolver.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-proof-resolver.output.v1.root-shape", + "rootDefinitionDigest": "sha256:750c75c08376a986a18e8739b7476d58af4133c19c40f43d216ee009607be225", + "nativeSource": { + "path": "internal/command/requirementbinding", + "canonicalDigest": "sha256:90bada8c868751cef16683475839174de48dd18936e3d79a74e24febcda1e09b", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/requirementbinding/compact_contract_test.go", + "test": "TestBuildResolverRejectsUnscopedCompactIdentity", + "command": "go test ./internal/command/requirementbinding -run '^TestBuildResolverRejectsUnscopedCompactIdentity$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-proof-resolver.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } + }, { "command": "requirement-proof-source-set", "input": "required", @@ -1373,6 +4087,32 @@ }, "nonClaims": [ "The command does not discover repository files, infer source completeness, execute witnesses, decide proof freshness, approve merge, or approve rollout." + ], + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-proof-source-set.input.v1.root-shape", + "rootDefinitionDigest": "sha256:8003f7feca3b82413cbc2bfdbc3b754508ec3d7e1706df54431b6332d19ec5fd", + "nativeSource": { + "path": "internal/command/requirementproofsourceset", + "canonicalDigest": "sha256:bd5377b5ec6b2a5a506e2702ed8dacb9a25908aa9f63002a4cf88f63f4641ba7", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/requirementproofsourceset/requirementproofsourceset_test.go", + "test": "TestBuildSelectsSourceSetRowsAndEmitsResolverInput", + "command": "go test ./internal/command/requirementproofsourceset -run '^TestBuildSelectsSourceSetRowsAndEmitsResolverInput$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "canonicalEnvelope", + "sourceSet", + "sources", + "root-shape-only definition proofkit.requirement-proof-source-set.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-002" ] }, "outputContract": { @@ -1397,6 +4137,30 @@ ], "nonClaims": [ "Resolver projection is a shape-compatible data product, not proof freshness, native test execution, or consumer merge authority." + ], + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-proof-source-set.output.v1.root-shape", + "rootDefinitionDigest": "sha256:57993914adfd60d6cc1a66bfef0b6a4c3c0162fd102f2629d497a28e839f1228", + "nativeSource": { + "path": "internal/command/requirementproofsourceset", + "canonicalDigest": "sha256:bd5377b5ec6b2a5a506e2702ed8dacb9a25908aa9f63002a4cf88f63f4641ba7", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/requirementproofsourceset/requirementproofsourceset_test.go", + "test": "TestBuildSelectsSourceSetRowsAndEmitsResolverInput", + "command": "go test ./internal/command/requirementproofsourceset -run '^TestBuildSelectsSourceSetRowsAndEmitsResolverInput$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-proof-source-set.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-002" ] } }, @@ -1418,7 +4182,61 @@ "--input-pointer", "--local-environment-class", "--scope" - ] + ], + "inputContract": { + "contractId": "proofkit.requirement-proof-view.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-proof-view.input.v1.root-shape", + "rootDefinitionDigest": "sha256:5aed3bcb99337d4937243d61d1f8ed1b1b358108a7710152221bfd5961a3aeb7", + "nativeSource": { + "path": "internal/command/requirementproofview", + "canonicalDigest": "sha256:76e16289eb4e8e4d6ae3d9ac9ee52b22335338bdf0467c5968894f92bfa09895", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/requirementproofview/requirementproofview_test.go", + "test": "TestBuildMarkdownEscapesCallerControlledCompactFields", + "command": "go test ./internal/command/requirementproofview -run '^TestBuildMarkdownEscapesCallerControlledCompactFields$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-proof-view.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.requirement-proof-view.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-proof-view.output.v1.root-shape", + "rootDefinitionDigest": "sha256:d1da2a4593aa2f4a1531e1a5be8e3bc877a5ff45f978b7f39a0bf708b01cb564", + "nativeSource": { + "path": "internal/command/requirementproofview", + "canonicalDigest": "sha256:76e16289eb4e8e4d6ae3d9ac9ee52b22335338bdf0467c5968894f92bfa09895", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/requirementproofview/requirementproofview_test.go", + "test": "TestBuildMarkdownEscapesCallerControlledCompactFields", + "command": "go test ./internal/command/requirementproofview -run '^TestBuildMarkdownEscapesCallerControlledCompactFields$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-proof-view.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "requirement-semantic-diff", @@ -1432,7 +4250,79 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.requirement-semantic-diff.input.v2", + "schemaVersion": 2, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-semantic-diff.input.v2.root-shape", + "rootDefinitionDigest": "sha256:1f30133908a40c16f8ed236725e36b1999fc125f1450efa1c28264c42e6c8a63", + "nativeSource": { + "path": "internal/command/requirementdiff", + "canonicalDigest": "sha256:046132ae2b29d60ca2a21b55a1d57a56930ae15db701595a1ccbc4b503aa7b94", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/requirementdiff/requirementdiff_test.go", + "test": "TestDigestCoverageAdaptersPreserveSemanticDiffV2", + "command": "go test ./internal/command/requirementdiff -run '^TestDigestCoverageAdaptersPreserveSemanticDiffV2$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=2", + "diffId", + "baseContext=proofkit.requirement-context schemaVersion=2", + "currentContext=proofkit.requirement-context schemaVersion=2", + "strict schemaVersion=1 adapter requires two v1 contexts", + "query.requirementIds[] (optional)", + "query.ownerIds[] (optional)", + "query.maxChanges=1..8192 (optional)", + "root-shape-only definition proofkit.requirement-semantic-diff.input.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-019", + "REQ-PROOFKIT-SPEC-020", + "REQ-PROOFKIT-SPEC-022", + "REQ-PROOFKIT-SPEC-023" + ] + }, + "outputContract": { + "contractId": "proofkit.requirement-semantic-diff.output.v2", + "schemaVersion": 2, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-semantic-diff.output.v2.root-shape", + "rootDefinitionDigest": "sha256:abe2eda1ca5496bd33cd659a419a46d7b69a2b8cc50203c7ba6acd31625b760f", + "nativeSource": { + "path": "internal/command/requirementdiff", + "canonicalDigest": "sha256:046132ae2b29d60ca2a21b55a1d57a56930ae15db701595a1ccbc4b503aa7b94", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/requirementdiff/requirementdiff_test.go", + "test": "TestDigestCoverageAdaptersPreserveSemanticDiffV2", + "command": "go test ./internal/command/requirementdiff -run '^TestDigestCoverageAdaptersPreserveSemanticDiffV2$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=2", + "baseExpectedDigestCoverage=none|partial|all", + "currentExpectedDigestCoverage=none|partial|all", + "strict schemaVersion=1 output adapter rejects mixed vocabulary", + "root-shape-only definition proofkit.requirement-semantic-diff.output.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-019", + "REQ-PROOFKIT-SPEC-020", + "REQ-PROOFKIT-SPEC-022", + "REQ-PROOFKIT-SPEC-023" + ] + } }, { "command": "requirement-source-admission", @@ -1446,7 +4336,61 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.requirement-source-admission.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-source-admission.input.v1.root-shape", + "rootDefinitionDigest": "sha256:b2b3982ca053794da607673db6258933960a4f67653871205ff03caab2012a9f", + "nativeSource": { + "path": "internal/command/requirementsourceadmission", + "canonicalDigest": "sha256:5767016688b34b4843a4c2da3f817b354c2ade0e9dfcfae5009b8fbf7cf5f76d", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/requirementsourceadmission/requirementsourceadmission_test.go", + "test": "TestEvaluateRejectsBlockingRequirementWithoutProofRoute", + "command": "go test ./internal/command/requirementsourceadmission -run '^TestEvaluateRejectsBlockingRequirementWithoutProofRoute$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-source-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.requirement-source-admission.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-source-admission.output.v1.root-shape", + "rootDefinitionDigest": "sha256:2527a19394b22efb4b14e39bf3b1f14283f179592c369b9c2ae34b612c7c69af", + "nativeSource": { + "path": "internal/command/requirementsourceadmission", + "canonicalDigest": "sha256:5767016688b34b4843a4c2da3f817b354c2ade0e9dfcfae5009b8fbf7cf5f76d", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/requirementsourceadmission/requirementsourceadmission_test.go", + "test": "TestEvaluateRejectsBlockingRequirementWithoutProofRoute", + "command": "go test ./internal/command/requirementsourceadmission -run '^TestEvaluateRejectsBlockingRequirementWithoutProofRoute$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-source-admission.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "requirement-source-transition", @@ -1460,7 +4404,63 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.requirement-source-transition.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-source-transition.input.v1.root-shape", + "rootDefinitionDigest": "sha256:f6783a812e7d0addd6fa4f077cd43b47df87c7b9fee93cdb3df429ca22cd238f", + "nativeSource": { + "path": "internal/command/requirementsourcetransition", + "canonicalDigest": "sha256:4e63aa69d44c0f33fae6c91ebe3343b0d6e1ba6d74db4784b0d495e1ca51abf5", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/requirementsourcetransition/requirementsourcetransition_test.go", + "test": "TestBuildRejectsRequirementSourceTransitionContractViolations", + "command": "go test ./internal/command/requirementsourcetransition -run '^TestBuildRejectsRequirementSourceTransitionContractViolations$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-source-transition.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-016" + ] + }, + "outputContract": { + "contractId": "proofkit.requirement-source-transition.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-source-transition.output.v1.root-shape", + "rootDefinitionDigest": "sha256:c1dc42e4388b9a43edd7254f8d4fff8076185c3fd05357296d7efb9fef8daf51", + "nativeSource": { + "path": "internal/command/requirementsourcetransition", + "canonicalDigest": "sha256:4e63aa69d44c0f33fae6c91ebe3343b0d6e1ba6d74db4784b0d495e1ca51abf5", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/requirementsourcetransition/requirementsourcetransition_test.go", + "test": "TestBuildRejectsRequirementSourceTransitionContractViolations", + "command": "go test ./internal/command/requirementsourcetransition -run '^TestBuildRejectsRequirementSourceTransitionContractViolations$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-source-transition.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-016" + ] + } }, { "command": "requirement-source-view", @@ -1477,7 +4477,61 @@ "--format", "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.requirement-source-view.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-source-view.input.v1.root-shape", + "rootDefinitionDigest": "sha256:79f46f80d0f83e720cab2ccb2bf50675371770521ccb64709d53c79c6db0f1ef", + "nativeSource": { + "path": "internal/command/requirementsourceview", + "canonicalDigest": "sha256:c20ae275d70ccc804707d3fdbbde8c873f71d692de416ac8ecbc1b291726f7f6", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/requirementsourceview/requirementsourceview_test.go", + "test": "TestBuildMarkdownEscapesCallerControlledText", + "command": "go test ./internal/command/requirementsourceview -run '^TestBuildMarkdownEscapesCallerControlledText$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-source-view.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.requirement-source-view.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-source-view.output.v1.root-shape", + "rootDefinitionDigest": "sha256:30ba0ef3fb0c042ccea37068fb2c17e5d58d0abf749b2f69b4d9e07c4b24d10e", + "nativeSource": { + "path": "internal/command/requirementsourceview", + "canonicalDigest": "sha256:c20ae275d70ccc804707d3fdbbde8c873f71d692de416ac8ecbc1b291726f7f6", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/requirementsourceview/requirementsourceview_test.go", + "test": "TestBuildMarkdownEscapesCallerControlledText", + "command": "go test ./internal/command/requirementsourceview -run '^TestBuildMarkdownEscapesCallerControlledText$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-source-view.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "requirement-spec-tree", @@ -1496,7 +4550,39 @@ "edges", "overlays" ], - "callerTextAuthority": "callerAnnotations fields are untrusted presentation annotations and never command-owned nonClaims" + "callerTextAuthority": "callerAnnotations fields are untrusted presentation annotations and never command-owned nonClaims", + "contractId": "proofkit.requirement-spec-tree.input.v1", + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-spec-tree.input.v1.root-shape", + "rootDefinitionDigest": "sha256:7cfe8b0685164a3c7dcc8432fcfd72f3944cef4057bb7420e81d0167e43815ba", + "nativeSource": { + "path": "internal/command/requirementspectree", + "canonicalDigest": "sha256:9b7a11162751627c7432e9bdb07a255c8296942e891fc3933dc5b6aaf0436586", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/requirementspectree/requirementspectree_test.go", + "test": "TestBuildRejectsDAGAndStaleDigest", + "command": "go test ./internal/command/requirementspectree -run '^TestBuildRejectsDAGAndStaleDigest$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion", + "treeId", + "rootNodeId", + "callerAnnotations", + "nodes", + "edges", + "overlays", + "root-shape-only definition proofkit.requirement-spec-tree.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-008", + "REQ-PROOFKIT-SPEC-009" + ] }, "outputModes": [ "json" @@ -1504,7 +4590,36 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "outputContract": { + "contractId": "proofkit.requirement-spec-tree.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-spec-tree.output.v1.root-shape", + "rootDefinitionDigest": "sha256:c99be2fb843ccb7b3a91b21ca25852d6e1b1cf565f95a156fe33ce0f1d9e7fdd", + "nativeSource": { + "path": "internal/command/requirementspectree", + "canonicalDigest": "sha256:9b7a11162751627c7432e9bdb07a255c8296942e891fc3933dc5b6aaf0436586", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/requirementspectree/requirementspectree_test.go", + "test": "TestBuildRejectsDAGAndStaleDigest", + "command": "go test ./internal/command/requirementspectree -run '^TestBuildRejectsDAGAndStaleDigest$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-spec-tree.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-008", + "REQ-PROOFKIT-SPEC-009" + ] + } }, { "command": "requirement-spec-tree-view", @@ -1523,7 +4638,39 @@ "edges", "overlays" ], - "callerTextAuthority": "callerAnnotations fields are untrusted presentation annotations and never command-owned nonClaims" + "callerTextAuthority": "callerAnnotations fields are untrusted presentation annotations and never command-owned nonClaims", + "contractId": "proofkit.requirement-spec-tree-view.input.v1", + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-spec-tree-view.input.v1.root-shape", + "rootDefinitionDigest": "sha256:081903f08edd082447a7225834cb12785ebe03d0c262f464f7cd26daf8789c44", + "nativeSource": { + "path": "internal/command/requirementspectree", + "canonicalDigest": "sha256:9b7a11162751627c7432e9bdb07a255c8296942e891fc3933dc5b6aaf0436586", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/requirementspectree/requirementspectree_test.go", + "test": "TestBuildViewMarkdownAndHTMLAreDeterministicAndEscaped", + "command": "go test ./internal/command/requirementspectree -run '^TestBuildViewMarkdownAndHTMLAreDeterministicAndEscaped$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion", + "treeId", + "rootNodeId", + "callerAnnotations", + "nodes", + "edges", + "overlays", + "root-shape-only definition proofkit.requirement-spec-tree-view.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-008", + "REQ-PROOFKIT-SPEC-009" + ] }, "outputModes": [ "html", @@ -1554,6 +4701,35 @@ "nonClaims": [ "Requirement spec tree views are presentation projections only.", "Requirement spec tree views do not scan repositories, compute source freshness, execute witnesses, approve merge, or replace the requirement spec tree admission report." + ], + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-spec-tree-view.output.v2.root-shape", + "rootDefinitionDigest": "sha256:e788ba6455ad69bdc7ff341888fff5ae74351a1316d19fd708734626df5153c7", + "nativeSource": { + "path": "internal/command/requirementspectree", + "canonicalDigest": "sha256:9b7a11162751627c7432e9bdb07a255c8296942e891fc3933dc5b6aaf0436586", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/requirementspectree/requirementspectree_test.go", + "test": "TestBuildViewMarkdownAndHTMLAreDeterministicAndEscaped", + "command": "go test ./internal/command/requirementspectree -run '^TestBuildViewMarkdownAndHTMLAreDeterministicAndEscaped$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "authority", + "nodeCount", + "rootNodeId", + "treeId", + "viewKind", + "root-shape-only definition proofkit.requirement-spec-tree-view.output.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-008", + "REQ-PROOFKIT-SPEC-009" ] } }, @@ -1569,7 +4745,76 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.requirement-traceability-graph.input.v2", + "schemaVersion": 2, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-traceability-graph.input.v2.root-shape", + "rootDefinitionDigest": "sha256:6487ff537380d1cbffe5a72b9688e9ce6baffa218e76f8dbbfe8f50d14509219", + "nativeSource": { + "path": "internal/command/requirementgraph", + "canonicalDigest": "sha256:7aba61b6b6a66f53d9ffde32867bf4f52dd32156debc75a9d6d5da8beba39cb4", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/requirementgraph/requirementgraph_test.go", + "test": "TestBuildConsumesNormalizedV1AndV2ContextSnapshots", + "command": "go test ./internal/command/requirementgraph -run '^TestBuildConsumesNormalizedV1AndV2ContextSnapshots$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=2", + "graphId", + "context=proofkit.requirement-context schemaVersion=2 with strict v1 adapter", + "codeSources[].path+content (optional, bounded UTF-8)", + "codeTopology.nodes[].abstractionLevel=repository|package|module|file|symbol|source_range", + "codeTopology.nodes[].sourceDigest+currentnessState", + "codeTopology.edges[].evidenceRefs+authorityClass+currentnessState", + "codeTopology.nativeCoverage[].producerId+evidenceRef+authorityClass+currentnessState+state", + "root-shape-only definition proofkit.requirement-traceability-graph.input.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-019", + "REQ-PROOFKIT-SPEC-020", + "REQ-PROOFKIT-SPEC-022", + "REQ-PROOFKIT-SPEC-023" + ] + }, + "outputContract": { + "contractId": "proofkit.requirement-traceability-graph.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.requirement-traceability-graph.output.v1.root-shape", + "rootDefinitionDigest": "sha256:37d07abc4d9122176ffab9842b5d85fbfdbe068ee9e4097da8fc21206aae1d47", + "nativeSource": { + "path": "internal/command/requirementgraph", + "canonicalDigest": "sha256:7aba61b6b6a66f53d9ffde32867bf4f52dd32156debc75a9d6d5da8beba39cb4", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/app/requirement_context_cli_test.go", + "test": "TestRequirementContextCommandsComposeThroughWholeCLI", + "command": "go test ./internal/app -run '^TestRequirementContextCommandsComposeThroughWholeCLI$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-traceability-graph.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-019", + "REQ-PROOFKIT-SPEC-020", + "REQ-PROOFKIT-SPEC-022", + "REQ-PROOFKIT-SPEC-023" + ] + } }, { "command": "scaffold-profile-plan", @@ -1583,7 +4828,61 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.scaffold-profile-plan.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.scaffold-profile-plan.input.v1.root-shape", + "rootDefinitionDigest": "sha256:48191336813e493288f5b1e24460316b8d3cd41bb334d96485cc6f1fe88e05a6", + "nativeSource": { + "path": "internal/command/scaffoldprofileplan", + "canonicalDigest": "sha256:876b29d7b7db87864cbc530c8628996d94bea323cdb7d8fa334c1d1a616190b2", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/scaffoldprofileplan/scaffoldprofileplan_test.go", + "test": "TestBuildAcceptsCommandMatcherHints", + "command": "go test ./internal/command/scaffoldprofileplan -run '^TestBuildAcceptsCommandMatcherHints$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.scaffold-profile-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.scaffold-profile-plan.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.scaffold-profile-plan.output.v1.root-shape", + "rootDefinitionDigest": "sha256:377182555fb39dfcf4f0a7011c431c6954aca2560f663adece346da3affc556b", + "nativeSource": { + "path": "internal/command/scaffoldprofileplan", + "canonicalDigest": "sha256:876b29d7b7db87864cbc530c8628996d94bea323cdb7d8fa334c1d1a616190b2", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/scaffoldprofileplan/scaffoldprofileplan_test.go", + "test": "TestBuildAcceptsCommandMatcherHints", + "command": "go test ./internal/command/scaffoldprofileplan -run '^TestBuildAcceptsCommandMatcherHints$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.scaffold-profile-plan.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "scaffold-project-structure", @@ -1599,7 +4898,61 @@ "--agent-envelope", "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.scaffold-project-structure.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.scaffold-project-structure.input.v1.root-shape", + "rootDefinitionDigest": "sha256:3f696dff7be322cfca8b24df9feabccf935d418c68839f19f5f844c39ab2a980", + "nativeSource": { + "path": "internal/command/projectstructure", + "canonicalDigest": "sha256:c48668ff539c58ed4eb7185a41a2b566b5db128aea8df8a3e53b26a3cbbbcc09", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/projectstructure/projectstructure_test.go", + "test": "TestBuildAdmitsProjectStructureScaffoldAndEmitsBoundedEnvelope", + "command": "go test ./internal/command/projectstructure -run '^TestBuildAdmitsProjectStructureScaffoldAndEmitsBoundedEnvelope$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.scaffold-project-structure.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.scaffold-project-structure.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.scaffold-project-structure.output.v1.root-shape", + "rootDefinitionDigest": "sha256:903ec3a1c0842f58039df76b6c16f851f3d9ad7353287bd244ad86e211c9b13d", + "nativeSource": { + "path": "internal/command/projectstructure", + "canonicalDigest": "sha256:c48668ff539c58ed4eb7185a41a2b566b5db128aea8df8a3e53b26a3cbbbcc09", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/projectstructure/projectstructure_test.go", + "test": "TestBuildAdmitsProjectStructureScaffoldAndEmitsBoundedEnvelope", + "command": "go test ./internal/command/projectstructure -run '^TestBuildAdmitsProjectStructureScaffoldAndEmitsBoundedEnvelope$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.scaffold-project-structure.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "secret-scan", @@ -1657,7 +5010,33 @@ "suppression reason is report-visible caller text and is admitted through secret-like text rejection" ] }, - "nonClaim": "Secret scan does not discover repository files or replace provider secret scanning." + "nonClaim": "Secret scan does not discover repository files or replace provider secret scanning.", + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.secret-scan.input.v1.root-shape", + "rootDefinitionDigest": "sha256:6477a651d88632cbdd4cdde0877b95faa1efaeaa16ec8d16a913313517d591ce", + "nativeSource": { + "path": "internal/command/secretscan", + "canonicalDigest": "sha256:7fe8eb1fe43973b321fce0edce337b565d4a51e86e244bce9c3a57039209b0e3", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/secretscan/secretscan_test.go", + "test": "TestBuildFindsSecretLikeTextWithoutLeakingValue", + "command": "go test ./internal/command/secretscan -run '^TestBuildFindsSecretLikeTextWithoutLeakingValue$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "files", + "nonClaims", + "reportId", + "schemaVersion", + "root-shape-only definition proofkit.secret-scan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] }, "outputContract": { "contractId": "proofkit.secret-scan.output.v1", @@ -1673,7 +5052,30 @@ "unusedSuppressions" ], "redactionRule": "matched secret-like values are never emitted to stdout reports", - "nonClaim": "Findings are pattern detections, not credential validity proof." + "nonClaim": "Findings are pattern detections, not credential validity proof.", + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.secret-scan.output.v1.root-shape", + "rootDefinitionDigest": "sha256:e4113bfd0c2affe54c8f231ca280a405ba24adfb5e219ad548bd5c175c3d5f54", + "nativeSource": { + "path": "internal/command/secretscan", + "canonicalDigest": "sha256:7fe8eb1fe43973b321fce0edce337b565d4a51e86e244bce9c3a57039209b0e3", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/secretscan/secretscan_test.go", + "test": "TestBuildFindsSecretLikeTextWithoutLeakingValue", + "command": "go test ./internal/command/secretscan -run '^TestBuildFindsSecretLikeTextWithoutLeakingValue$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.secret-scan.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] } }, { @@ -1690,7 +5092,63 @@ "--agent-envelope", "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.selective-gate-evidence.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.selective-gate-evidence.input.v1.root-shape", + "rootDefinitionDigest": "sha256:0615e61b546c079fa662e7a83e5472bdc137eceafd42700dcfa9e78c8c5397b0", + "nativeSource": { + "path": "internal/command/selectivegateevidence", + "canonicalDigest": "sha256:ddee1b6e09f88546d237fe32b4f4c98b62406efe0556273f382d1e6be577c4ef", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/selectivegateevidence/selectivegateevidence_test.go", + "test": "TestBuildRejectsMergeSatisfyingEvidenceWithoutProducerAdmission", + "command": "go test ./internal/command/selectivegateevidence -run '^TestBuildRejectsMergeSatisfyingEvidenceWithoutProducerAdmission$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.selective-gate-evidence.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-013" + ] + }, + "outputContract": { + "contractId": "proofkit.selective-gate-evidence.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.selective-gate-evidence.output.v1.root-shape", + "rootDefinitionDigest": "sha256:8b8537b08340434bcb6c91fc446d8bf7aaa782e8e88763a31aa19720bf49a113", + "nativeSource": { + "path": "internal/command/selectivegateevidence", + "canonicalDigest": "sha256:ddee1b6e09f88546d237fe32b4f4c98b62406efe0556273f382d1e6be577c4ef", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/selectivegateevidence/selectivegateevidence_test.go", + "test": "TestBuildRejectsMergeSatisfyingEvidenceWithoutProducerAdmission", + "command": "go test ./internal/command/selectivegateevidence -run '^TestBuildRejectsMergeSatisfyingEvidenceWithoutProducerAdmission$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.selective-gate-evidence.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-013" + ] + } }, { "command": "selective-gate-obligation-decision-input", @@ -1704,7 +5162,63 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.selective-gate-obligation-decision-input.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.selective-gate-obligation-decision-input.input.v1.root-shape", + "rootDefinitionDigest": "sha256:f488e84a8a588593d41cdbea8991fbe549239580292db573d215c8a4725ad2ee", + "nativeSource": { + "path": "internal/command/selectivegateevidence", + "canonicalDigest": "sha256:ddee1b6e09f88546d237fe32b4f4c98b62406efe0556273f382d1e6be577c4ef", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/selectivegateevidence/selectivegateevidence_test.go", + "test": "TestProjectObligationDecisionBuildsInputAndRejectsUnroutedCommand", + "command": "go test ./internal/command/selectivegateevidence -run '^TestProjectObligationDecisionBuildsInputAndRejectsUnroutedCommand$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.selective-gate-obligation-decision-input.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-013" + ] + }, + "outputContract": { + "contractId": "proofkit.selective-gate-obligation-decision-input.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.selective-gate-obligation-decision-input.output.v1.root-shape", + "rootDefinitionDigest": "sha256:f4c5b8430047a0a9c19d6c180bdf8b20b1980519dbdbf18e05870ebe5ea51942", + "nativeSource": { + "path": "internal/command/selectivegateevidence", + "canonicalDigest": "sha256:ddee1b6e09f88546d237fe32b4f4c98b62406efe0556273f382d1e6be577c4ef", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/selectivegateevidence/selectivegateevidence_test.go", + "test": "TestProjectObligationDecisionBuildsInputAndRejectsUnroutedCommand", + "command": "go test ./internal/command/selectivegateevidence -run '^TestProjectObligationDecisionBuildsInputAndRejectsUnroutedCommand$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.selective-gate-obligation-decision-input.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-013" + ] + } }, { "command": "selective-gate-plan", @@ -1751,7 +5265,59 @@ "caller_owned_external requires reason external_secret_scan and must not use Proofkit-owned command ids text-policy or secret-scan" ], "nonClaim": "Selective gate planning admits scan obligations but does not execute scanner commands or prove repository-wide credential absence." - } + }, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.selective-gate-plan.input.v1.root-shape", + "rootDefinitionDigest": "sha256:b2f202d51f98d0e43191b015591f88b493916254d7d7fcc5942bd7c5fff2e6f8", + "nativeSource": { + "path": "internal/command/selectivegateplan", + "canonicalDigest": "sha256:82adf4bd62e22da0c96d717f496dd3b76c20378129bf0f787937a27b6ecbd18c", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/selectivegateplan/selectivegateplan_test.go", + "test": "TestBuildFailsClosedForUncoveredUnknownEdge", + "command": "go test ./internal/command/selectivegateplan -run '^TestBuildFailsClosedForUncoveredUnknownEdge$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.selective-gate-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-013" + ] + }, + "outputContract": { + "contractId": "proofkit.selective-gate-plan.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.selective-gate-plan.output.v1.root-shape", + "rootDefinitionDigest": "sha256:45fd178ec9319a1f2394b45508cdd7164d5a836c06064ebc3f8ea8f4ba1349af", + "nativeSource": { + "path": "internal/command/selectivegateplan", + "canonicalDigest": "sha256:82adf4bd62e22da0c96d717f496dd3b76c20378129bf0f787937a27b6ecbd18c", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/selectivegateplan/selectivegateplan_test.go", + "test": "TestBuildFailsClosedForUncoveredUnknownEdge", + "command": "go test ./internal/command/selectivegateplan -run '^TestBuildFailsClosedForUncoveredUnknownEdge$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.selective-gate-plan.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-013" + ] } }, { @@ -1765,21 +5331,135 @@ ], "allowedFlags": [ "--input" - ] - }, - { - "command": "spec-overview-claims", - "input": "required", - "stdin": true, - "inputPointer": true, - "scopeClass": "explicit_caller_input", + ], + "inputContract": { + "contractId": "proofkit.self-check.input.v1", + "schemaVersion": 1, + "rootType": "json_value", + "closed": true, + "rootDefinitionRef": "proofkit.self-check.input.v1.root-shape", + "rootDefinitionDigest": "sha256:3c842174dff5361e7f83166469b832805e05aa314b073c16234b5b64e346281e", + "nativeSource": { + "path": "internal/app", + "canonicalDigest": "sha256:9a9280f7e5803767e0edc5aa5431d9163becc9265d767b2cc1acb22f454b70ea", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/app/app_test.go", + "test": "TestSelfCheckRejectsDuplicateKeys", + "command": "go test ./internal/app -run '^TestSelfCheckRejectsDuplicateKeys$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.self-check.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-QUALITY-021", + "REQ-PROOFKIT-RETIRE-007" + ] + }, + "outputContract": { + "contractId": "proofkit.self-check.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.self-check.output.v1.root-shape", + "rootDefinitionDigest": "sha256:0ea95e277ebe44cd2de42c29b47c38686ac0b6b390d8965367437b3fe138e209", + "nativeSource": { + "path": "internal/app", + "canonicalDigest": "sha256:9a9280f7e5803767e0edc5aa5431d9163becc9265d767b2cc1acb22f454b70ea", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/app/cli_abi_test.go", + "test": "TestSelfCheckOutputUsesExactRootShape", + "command": "go test ./internal/app -run '^TestSelfCheckOutputUsesExactRootShape$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.self-check.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-QUALITY-021", + "REQ-PROOFKIT-RETIRE-007" + ] + } + }, + { + "command": "spec-overview-claims", + "input": "required", + "stdin": true, + "inputPointer": true, + "scopeClass": "explicit_caller_input", "outputModes": [ "json" ], "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.spec-overview-claims.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.spec-overview-claims.input.v1.root-shape", + "rootDefinitionDigest": "sha256:c753b344fa6f5b4ab3f65f2e4e99cfc5adbc24b31030b3175889e2d2d34eee43", + "nativeSource": { + "path": "internal/command/specoverviewclaims", + "canonicalDigest": "sha256:6ab6a9731742c278136d4e1195380d691b2dddb63ba51522c7c455eccbd32f22", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/specoverviewclaims/specoverviewclaims_test.go", + "test": "TestBuildRejectsInvalidOverviewClaimBoundaryFacts", + "command": "go test ./internal/command/specoverviewclaims -run '^TestBuildRejectsInvalidOverviewClaimBoundaryFacts$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.spec-overview-claims.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-015" + ] + }, + "outputContract": { + "contractId": "proofkit.spec-overview-claims.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.spec-overview-claims.output.v1.root-shape", + "rootDefinitionDigest": "sha256:7faf2c5a579206f187e480e6b4c12b8da0a3578a36037af602962e16a3465353", + "nativeSource": { + "path": "internal/command/specoverviewclaims", + "canonicalDigest": "sha256:6ab6a9731742c278136d4e1195380d691b2dddb63ba51522c7c455eccbd32f22", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/specoverviewclaims/specoverviewclaims_test.go", + "test": "TestBuildRejectsInvalidOverviewClaimBoundaryFacts", + "command": "go test ./internal/command/specoverviewclaims -run '^TestBuildRejectsInvalidOverviewClaimBoundaryFacts$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.spec-overview-claims.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-015" + ] + } }, { "command": "spec-proof-bundle-admission", @@ -1793,7 +5473,61 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.spec-proof-bundle-admission.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.spec-proof-bundle-admission.input.v1.root-shape", + "rootDefinitionDigest": "sha256:1aa48d72b9159b42fdf51faed4efaf406901c6de7ba4180041cb439b54a8fa85", + "nativeSource": { + "path": "internal/command/specproofbundleadmission", + "canonicalDigest": "sha256:85a298f55e320ed7fecfcc4abcd1cd0bbe9cb6c032f45a8af6431fd670216253", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/specproofbundleadmission/specproofbundleadmission_test.go", + "test": "TestBuildRejectsForgedReceiptAdmissionChild", + "command": "go test ./internal/command/specproofbundleadmission -run '^TestBuildRejectsForgedReceiptAdmissionChild$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.spec-proof-bundle-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.spec-proof-bundle-admission.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.spec-proof-bundle-admission.output.v1.root-shape", + "rootDefinitionDigest": "sha256:96885d5ea11cf23867de200f29a0fe8f188e723c7bd469935086d90f42e05b33", + "nativeSource": { + "path": "internal/command/specproofbundleadmission", + "canonicalDigest": "sha256:85a298f55e320ed7fecfcc4abcd1cd0bbe9cb6c032f45a8af6431fd670216253", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/specproofbundleadmission/specproofbundleadmission_test.go", + "test": "TestBuildRejectsForgedReceiptAdmissionChild", + "command": "go test ./internal/command/specproofbundleadmission -run '^TestBuildRejectsForgedReceiptAdmissionChild$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.spec-proof-bundle-admission.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "stack-preset", @@ -1809,7 +5543,45 @@ ], "requiredFlags": [ "--preset" - ] + ], + "outputContract": { + "contractId": "proofkit.stack-preset.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.stack-preset.output.v1.root-shape", + "rootDefinitionDigest": "sha256:338f720a24bd36f4ab34cad9b14d3bd26389119e5c713146f1c4eb019f4a0637", + "nativeSource": { + "path": "internal/command/stackpreset", + "canonicalDigest": "sha256:21977828a569c4fca863ca3f4ea143650f5545088568fae90a7ca936d855fe87", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/app/command_coverage_test.go", + "test": "TestNoInputCommandsHaveCommandSpecificBehavior", + "command": "go test ./internal/app -run '^TestNoInputCommandsHaveCommandSpecificBehavior$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.stack-preset.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "flagChoices": { + "--preset": [ + "agentic_runtime_repo", + "generated_docs_contract_repo", + "python_service", + "python_typescript_service", + "typescript_monorepo", + "typescript_workspace" + ] + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-QUALITY-010" + ] + } }, { "command": "test-evidence-inventory", @@ -1926,6 +5698,37 @@ "Discovery draft candidate output must be materialized into strict inventory before it can affect coverage.", "Proof-binding-derived routes remain proof_route_candidate entries until a consumer owner materializes an executable semantic oracle.", "The derived inventory remains caller-owned input for downstream coverage views." + ], + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.test-evidence-inventory.input.v1.root-shape", + "rootDefinitionDigest": "sha256:4006bfcaab9bd8cb781595aa14f48768cbee996f5e866757fa73be1c9938fa98", + "nativeSources": [ + { + "path": "internal/command/proofbindingtestinventory", + "canonicalDigest": "sha256:87d5871167fd326398dd3d9c9424401c9414cae1d212bdbbd187a7dfe5730fb0", + "evidenceClass": "source_checkout" + }, + { + "path": "internal/command/testevidenceinventory", + "canonicalDigest": "sha256:04febdd7e002567d50bb16fded119ef6ca0ce0d2eb063c92b1664312652ac395", + "evidenceClass": "source_checkout" + } + ], + "nativeAdmissionWitnessSelector": { + "path": "internal/command/testevidenceinventory/testevidenceinventory_test.go", + "test": "TestBuildRejectsWeakOracleAndDuplicateFalsifier", + "command": "go test ./internal/command/testevidenceinventory -run '^TestBuildRejectsWeakOracleAndDuplicateFalsifier$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.test-evidence-inventory.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-006" ] }, "outputContract": { @@ -1992,6 +5795,37 @@ "nonClaims": [ "Test evidence inventory classifications describe admitted caller-owned inventory facts and Proofkit-derived structural diagnostics only.", "Proofkit does not infer weak tests from names, prose, mocks, sleeps, snapshots, or repository scans." + ], + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.test-evidence-inventory.output.v1.root-shape", + "rootDefinitionDigest": "sha256:aa2813ab8406c0dba4a9ac4fc215cb607e2c2003e9b0921ac1f6a68faaacff0d", + "nativeSources": [ + { + "path": "internal/command/proofbindingtestinventory", + "canonicalDigest": "sha256:87d5871167fd326398dd3d9c9424401c9414cae1d212bdbbd187a7dfe5730fb0", + "evidenceClass": "source_checkout" + }, + { + "path": "internal/command/testevidenceinventory", + "canonicalDigest": "sha256:04febdd7e002567d50bb16fded119ef6ca0ce0d2eb063c92b1664312652ac395", + "evidenceClass": "source_checkout" + } + ], + "nativeOutputWitnessSelector": { + "path": "internal/command/testevidenceinventory/testevidenceinventory_test.go", + "test": "TestBuildRejectsWeakOracleAndDuplicateFalsifier", + "command": "go test ./internal/command/testevidenceinventory -run '^TestBuildRejectsWeakOracleAndDuplicateFalsifier$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.test-evidence-inventory.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-006" ] } }, @@ -2007,7 +5841,61 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.text-policy.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.text-policy.input.v1.root-shape", + "rootDefinitionDigest": "sha256:af1e6196ddee9ee4262ca14c9cca1451c89241ebeef177af4a12be866b088f9a", + "nativeSource": { + "path": "internal/command/textpolicy", + "canonicalDigest": "sha256:d15d33f5481f994fecc70b5ab159ea16d5bed8d0ccbe3cf840a55ed18f168e79", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/textpolicy/textpolicy_test.go", + "test": "TestEvaluatePreservesUTF8ASCIIWhitespaceAndBinaryFalsifiers", + "command": "go test ./internal/command/textpolicy -run '^TestEvaluatePreservesUTF8ASCIIWhitespaceAndBinaryFalsifiers$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.text-policy.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.text-policy.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.text-policy.output.v1.root-shape", + "rootDefinitionDigest": "sha256:a47795a1a72dcece9e3e513c8d33829fed187a959450d1ce007697a434b26df1", + "nativeSource": { + "path": "internal/command/textpolicy", + "canonicalDigest": "sha256:d15d33f5481f994fecc70b5ab159ea16d5bed8d0ccbe3cf840a55ed18f168e79", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/textpolicy/textpolicy_test.go", + "test": "TestEvaluatePreservesUTF8ASCIIWhitespaceAndBinaryFalsifiers", + "command": "go test ./internal/command/textpolicy -run '^TestEvaluatePreservesUTF8ASCIIWhitespaceAndBinaryFalsifiers$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.text-policy.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "typescript-public-api-surfaces", @@ -2088,6 +5976,71 @@ "The command does not parse JSX or admit TSX source files.", "The command does not parse unrestricted TypeScript; sources outside the documented export grammar fail closed.", "Caller-owned source-to-condition mappings do not prove compiler output provenance, checkout freshness, package-manager truth, merge readiness, or rollout." + ], + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.typescript-public-api-surfaces.input.v1.root-shape", + "rootDefinitionDigest": "sha256:687efe1fe441443ad7bcab69f05987b47a25672da8032e7471bd7c48ebd95a30", + "nativeSource": { + "path": "internal/command/publicapi", + "canonicalDigest": "sha256:9f21766bc6d4dd042ff0e3bd3f4f7278448a9822e48399e56f9c5d2d9b02351b", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/publicapi/public_api_test.go", + "test": "TestVerifyTypeScriptPackagePublicAPIRejectsExportStar", + "command": "go test ./internal/command/publicapi -run '^TestVerifyTypeScriptPackagePublicAPIRejectsExportStar$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "machineContract=public_api_surfaces", + "entries[].packageManifestPath", + "entries[].packageName", + "entries[].exportKey", + "entries[].exportConditions[] (non-empty, sorted unique by condition)", + "entries[].exportConditions[].condition", + "entries[].exportConditions[].path", + "entries[].exportConditions[].sourcePath (declared and canonical target .ts/.mts/.cts)", + "entries[].runtimeExports[]", + "entries[].typeExports[]", + "entries[].deniedExportKeys[] (optional)", + "sourceGrammar=fail_closed_restricted_typescript_exports_v1", + "maxSourceFileBytes=8388608", + "maxPackageManifestBytes=262144", + "maxAggregateFileReadBytes=67108864", + "root-shape-only definition proofkit.typescript-public-api-surfaces.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.typescript-public-api-surfaces.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.typescript-public-api-surfaces.output.v1.root-shape", + "rootDefinitionDigest": "sha256:93da5f4d5efd4c482cb5da37bb60a80f8e47e4eab549428790ddd1a4e680009d", + "nativeSource": { + "path": "internal/command/publicapi", + "canonicalDigest": "sha256:9f21766bc6d4dd042ff0e3bd3f4f7278448a9822e48399e56f9c5d2d9b02351b", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/publicapi/public_api_test.go", + "test": "TestVerifyTypeScriptPackagePublicAPIRejectsExportStar", + "command": "go test ./internal/command/publicapi -run '^TestVerifyTypeScriptPackagePublicAPIRejectsExportStar$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.typescript-public-api-surfaces.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" ] } }, @@ -2139,7 +6092,59 @@ ], "nonClaim": "Binding-derived witness-plan projection avoids duplicate command identity but does not prove command pass evidence or native witness freshness." } - } + }, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.witness-plan.input.v1.root-shape", + "rootDefinitionDigest": "sha256:6670d1db21e0cc51e8811224b98e9160c6b8c52e841b18ba67e3e6426004831c", + "nativeSource": { + "path": "internal/command/witnessplan", + "canonicalDigest": "sha256:4c8926125cf023f86b37ea7b6ab7dda15af60b1667a1233de1b616901602a7e7", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/witnessplan/witnessplan_test.go", + "test": "TestBuildAdmitsSafeCommandAndRejectsShellCommand", + "command": "go test ./internal/command/witnessplan -run '^TestBuildAdmitsSafeCommandAndRejectsShellCommand$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.witness-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-003" + ] + }, + "outputContract": { + "contractId": "proofkit.witness-plan.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.witness-plan.output.v1.root-shape", + "rootDefinitionDigest": "sha256:77a320105ae18dd507d1d44c51e3fe26ebe7efc8decdd436f9311c0daae57d57", + "nativeSource": { + "path": "internal/command/witnessplan", + "canonicalDigest": "sha256:4c8926125cf023f86b37ea7b6ab7dda15af60b1667a1233de1b616901602a7e7", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/witnessplan/witnessplan_test.go", + "test": "TestBuildAdmitsSafeCommandAndRejectsShellCommand", + "command": "go test ./internal/command/witnessplan -run '^TestBuildAdmitsSafeCommandAndRejectsShellCommand$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.witness-plan.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-003" + ] } }, { @@ -2154,7 +6159,61 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.witness-scheduler-plan.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.witness-scheduler-plan.input.v1.root-shape", + "rootDefinitionDigest": "sha256:56f26197b5ee72e825a9c01b76c9e3683aac89241a808df657ee7d556f1806bf", + "nativeSource": { + "path": "internal/command/witnessschedulerplan", + "canonicalDigest": "sha256:384a39907f7dcad2f7d84492463a51f1acb34221c4d5aaae74633c1c5660b5d3", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/witnessschedulerplan/witnessschedulerplan_test.go", + "test": "TestBuildRejectsUnsafeParallelWriteCollision", + "command": "go test ./internal/command/witnessschedulerplan -run '^TestBuildRejectsUnsafeParallelWriteCollision$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.witness-scheduler-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.witness-scheduler-plan.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.witness-scheduler-plan.output.v1.root-shape", + "rootDefinitionDigest": "sha256:520c73c13a4f47e396da9a6f618a67f95ced3f18804137f11f597a4d98b81830", + "nativeSource": { + "path": "internal/command/witnessschedulerplan", + "canonicalDigest": "sha256:384a39907f7dcad2f7d84492463a51f1acb34221c4d5aaae74633c1c5660b5d3", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/witnessschedulerplan/witnessschedulerplan_test.go", + "test": "TestBuildRejectsUnsafeParallelWriteCollision", + "command": "go test ./internal/command/witnessschedulerplan -run '^TestBuildRejectsUnsafeParallelWriteCollision$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.witness-scheduler-plan.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "workspace-changed-package-plan", @@ -2170,7 +6229,61 @@ "--agent-envelope", "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.workspace-changed-package-plan.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.workspace-changed-package-plan.input.v1.root-shape", + "rootDefinitionDigest": "sha256:34d8ad1aab208d578000ef39b9eaf7503dcbd2b97645a1444d6bb8a0f63db482", + "nativeSource": { + "path": "internal/command/workspaceplanning", + "canonicalDigest": "sha256:8e5180fc773b898536503c8189c69f67bce5d3ebb9e60dea3e9ec358b419f1d5", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/workspaceplanning/workspaceplanning_test.go", + "test": "TestChangedPackagePlanAdmitsPackagesRootAndSchema", + "command": "go test ./internal/command/workspaceplanning -run '^TestChangedPackagePlanAdmitsPackagesRootAndSchema$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.workspace-changed-package-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.workspace-changed-package-plan.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.workspace-changed-package-plan.output.v1.root-shape", + "rootDefinitionDigest": "sha256:e6df6d1141707abfd3122d0e538418ee3755ab248f1e4d061f957bfbb7ee26b7", + "nativeSource": { + "path": "internal/command/workspaceplanning", + "canonicalDigest": "sha256:8e5180fc773b898536503c8189c69f67bce5d3ebb9e60dea3e9ec358b419f1d5", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/workspaceplanning/workspaceplanning_test.go", + "test": "TestChangedPackagePlanAdmitsPackagesRootAndSchema", + "command": "go test ./internal/command/workspaceplanning -run '^TestChangedPackagePlanAdmitsPackagesRootAndSchema$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.workspace-changed-package-plan.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "workspace-manifest-facts", @@ -2275,6 +6388,72 @@ }, "nonClaims": [ "Workspace manifest facts project explicit caller-owned manifest records and do not scan repositories, read package files, interpret package-manager policy, execute commands, or approve merge." + ], + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.workspace-manifest-facts.output.v1.root-shape", + "rootDefinitionDigest": "sha256:ec6ea756481bd02822e396e90849a9a2757c9f831f72340fef491f8fccbd53b6", + "nativeSource": { + "path": "internal/command/workspacemanifestfacts", + "canonicalDigest": "sha256:a689157cd4a7ea29c37c030f58d0a64fd605c6a8e7fdf8c65344b3280d1bbc41", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/workspacemanifestfacts/workspace_manifest_facts_test.go", + "test": "TestBuildProjectsManifestFactsAndPlanningInputs", + "command": "go test ./internal/command/workspacemanifestfacts -run '^TestBuildProjectsManifestFactsAndPlanningInputs$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "changedPackagePlanPackages", + "diagnostics", + "knownPackageNames", + "manifestSources", + "nonClaims", + "packageUniverse", + "packages", + "projectionId", + "reportId", + "reportKind", + "root", + "schemaVersion", + "shardPartitionPackages", + "state", + "summary", + "root-shape-only definition proofkit.workspace-manifest-facts.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-RETIRE-009" + ] + }, + "inputContract": { + "contractId": "proofkit.workspace-manifest-facts.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.workspace-manifest-facts.input.v1.root-shape", + "rootDefinitionDigest": "sha256:af1c88e54f82c48f1fad068efeb74fdc8f74e2b8ff618081ea496f1bfe609408", + "nativeSource": { + "path": "internal/command/workspacemanifestfacts", + "canonicalDigest": "sha256:a689157cd4a7ea29c37c030f58d0a64fd605c6a8e7fdf8c65344b3280d1bbc41", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/workspacemanifestfacts/workspace_manifest_facts_test.go", + "test": "TestBuildProjectsManifestFactsAndPlanningInputs", + "command": "go test ./internal/command/workspacemanifestfacts -run '^TestBuildProjectsManifestFactsAndPlanningInputs$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.workspace-manifest-facts.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-RETIRE-009" ] } }, @@ -2290,7 +6469,61 @@ "allowedFlags": [ "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.workspace-registry.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.workspace-registry.input.v1.root-shape", + "rootDefinitionDigest": "sha256:55d09e1672bc094c0ef564c578c719d8f4a193636747768593d784c8addce631", + "nativeSource": { + "path": "internal/command/workspaceregistry", + "canonicalDigest": "sha256:69eb0dbba57269f3dee75ecd40c7ec87519e72817a0efb3d5862dca4f62d0740", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/workspaceregistry/workspaceregistry_test.go", + "test": "TestBuildAdmitsWorkspaceRegistryAndRejectsMissingScriptTarget", + "command": "go test ./internal/command/workspaceregistry -run '^TestBuildAdmitsWorkspaceRegistryAndRejectsMissingScriptTarget$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.workspace-registry.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.workspace-registry.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.workspace-registry.output.v1.root-shape", + "rootDefinitionDigest": "sha256:36644320c351b3f278dcadc38bd27bb492f5c042fe7a2cc9a0b0756655d4e097", + "nativeSource": { + "path": "internal/command/workspaceregistry", + "canonicalDigest": "sha256:69eb0dbba57269f3dee75ecd40c7ec87519e72817a0efb3d5862dca4f62d0740", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/workspaceregistry/workspaceregistry_test.go", + "test": "TestBuildAdmitsWorkspaceRegistryAndRejectsMissingScriptTarget", + "command": "go test ./internal/command/workspaceregistry -run '^TestBuildAdmitsWorkspaceRegistryAndRejectsMissingScriptTarget$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.workspace-registry.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } }, { "command": "workspace-shard-partition", @@ -2306,7 +6539,8445 @@ "--agent-envelope", "--input", "--input-pointer" - ] + ], + "inputContract": { + "contractId": "proofkit.workspace-shard-partition.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.workspace-shard-partition.input.v1.root-shape", + "rootDefinitionDigest": "sha256:265fbbf98e23fb32cb3d9f585b7c31ed181ca5a185416dd9088fa21d30ec8eec", + "nativeSource": { + "path": "internal/command/workspaceplanning", + "canonicalDigest": "sha256:8e5180fc773b898536503c8189c69f67bce5d3ebb9e60dea3e9ec358b419f1d5", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/workspaceplanning/workspaceplanning_test.go", + "test": "TestShardPartitionAdmitsCoveredRootsAndRejectsMissingDependency", + "command": "go test ./internal/command/workspaceplanning -run '^TestShardPartitionAdmitsCoveredRootsAndRejectsMissingDependency$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.workspace-shard-partition.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + }, + "outputContract": { + "contractId": "proofkit.workspace-shard-partition.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.workspace-shard-partition.output.v1.root-shape", + "rootDefinitionDigest": "sha256:2715c816e36923fb506233f241b5b6172ec6d0640ec27a2738f20594cae9e960", + "nativeSource": { + "path": "internal/command/workspaceplanning", + "canonicalDigest": "sha256:8e5180fc773b898536503c8189c69f67bce5d3ebb9e60dea3e9ec358b419f1d5", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/workspaceplanning/workspaceplanning_test.go", + "test": "TestShardPartitionAdmitsCoveredRootsAndRejectsMissingDependency", + "command": "go test ./internal/command/workspaceplanning -run '^TestShardPartitionAdmitsCoveredRootsAndRejectsMissingDependency$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.workspace-shard-partition.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ] + } + } + ], + "contractDefinitions": [ + { + "definitionId": "proofkit.adoption-checklist.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "checklistId", + "items", + "nextCommandRefs", + "nonClaims", + "requiredItemIds", + "scenario", + "schemaVersion" + ], + "requiredFields": [ + "checklistId", + "items", + "nextCommandRefs", + "nonClaims", + "requiredItemIds", + "scenario", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:1526faa8d4517fcc1babd0cb4e46ffbd3764c2cdc9453e305efa93ea236c1110" + }, + { + "definitionId": "proofkit.adoption-checklist.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:6834488d7bfbb4fbcd50dda8d26c65f0c72e15ce7fd2f53ac4a1ee396f72f106" + }, + { + "definitionId": "proofkit.adoption-contract-envelope.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "envelopeId", + "gradual", + "nonClaims", + "pilot", + "schema", + "workflow" + ], + "requiredFields": [ + "envelopeId", + "gradual", + "nonClaims", + "pilot", + "schema", + "workflow" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "all --mode values" + ] + } + ] + }, + "canonicalDigest": "sha256:d12d8f01e350c871af39a1f518f08a86f59c0893c5d161ed1b4fe644e7b30f9c" + }, + { + "definitionId": "proofkit.adoption-contract-envelope.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "union", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "conditionModel": "cli_flag_conjunction_v1", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-adoption-report", + "when": [ + "--agent-envelope=absent --materialization-manifest=absent --mode=adoption --pilot=absent" + ] + }, + { + "allowedFields": [ + "agentActionPlan", + "exitCode", + "nextCommands", + "payloads", + "plannedFiles", + "report", + "witnessPlan" + ], + "requiredFields": [ + "agentActionPlan", + "exitCode", + "nextCommands", + "payloads", + "plannedFiles", + "report", + "witnessPlan" + ], + "rootKind": "object", + "variantId": "02-bootstrap", + "when": [ + "--agent-envelope=absent --materialization-manifest=absent --mode=bootstrap --pilot=absent" + ] + }, + { + "allowedFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "requiredFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "rootKind": "object", + "variantId": "03-bootstrap-agent", + "when": [ + "--agent-envelope=present --materialization-manifest=absent --mode=bootstrap --pilot=absent" + ] + }, + { + "allowedFields": [ + "fileCount", + "files", + "manifestId", + "manifestKind", + "nextCommands", + "nonClaims", + "payloadFileCount", + "schemaVersion", + "sourceReport" + ], + "requiredFields": [ + "fileCount", + "files", + "manifestId", + "manifestKind", + "nextCommands", + "nonClaims", + "payloadFileCount", + "schemaVersion", + "sourceReport" + ], + "rootKind": "object", + "variantId": "04-bootstrap-materialization", + "when": [ + "--agent-envelope=absent --materialization-manifest=present --mode=bootstrap --pilot=absent" + ] + }, + { + "allowedFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "requiredFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "rootKind": "object", + "variantId": "05-guidance-agent", + "when": [ + "--agent-envelope=present --materialization-manifest=absent --mode=guidance --pilot=absent" + ] + }, + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "06-guidance-report", + "when": [ + "--agent-envelope=absent --materialization-manifest=absent --mode=guidance --pilot=absent" + ] + }, + { + "allowedFields": [], + "requiredFields": [], + "rootKind": "array", + "variantId": "07-pilot-all", + "when": [ + "--agent-envelope=absent --materialization-manifest=absent --mode=pilot --pilot=all" + ] + }, + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "08-pilot-first", + "when": [ + "--agent-envelope=absent --materialization-manifest=absent --mode=pilot --pilot=absent", + "--agent-envelope=absent --materialization-manifest=absent --mode=pilot --pilot=first" + ] + }, + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "09-pilot-stack-diverse", + "when": [ + "--agent-envelope=absent --materialization-manifest=absent --mode=pilot --pilot=stack-diverse" + ] + }, + { + "allowedFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "requiredFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "rootKind": "object", + "variantId": "10-workflow-agent", + "when": [ + "--agent-envelope=present --materialization-manifest=absent --mode=workflow --pilot=absent" + ] + }, + { + "allowedFields": [ + "blockers", + "inputRefs", + "nonClaims", + "phases", + "planKind", + "planState", + "scenario", + "schemaVersion", + "workflowId" + ], + "requiredFields": [ + "blockers", + "inputRefs", + "nonClaims", + "phases", + "planKind", + "planState", + "scenario", + "schemaVersion", + "workflowId" + ], + "rootKind": "object", + "variantId": "11-workflow-plan", + "when": [ + "--agent-envelope=absent --materialization-manifest=absent --mode=workflow --pilot=absent" + ] + } + ] + }, + "canonicalDigest": "sha256:6da2b641d38a05c97b6088bec7a38d8c9a76aa386eb101e48f9727bceae5cfa2" + }, + { + "definitionId": "proofkit.adoption-doctor.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "blockedPreconditions", + "checkedScope", + "childReports", + "doctorId", + "mode", + "modernization", + "nonClaims", + "ownerRoutes", + "schemaVersion", + "staleAuthority", + "touchedRuleIds" + ], + "requiredFields": [ + "blockedPreconditions", + "checkedScope", + "childReports", + "doctorId", + "mode", + "modernization", + "nonClaims", + "ownerRoutes", + "schemaVersion", + "staleAuthority" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:104d1e8bef615582ead1c7c8c64b7e1643a76c2bc2a1f224189c372cf8cc4aa1" + }, + { + "definitionId": "proofkit.adoption-doctor.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "requiredFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "rootKind": "object", + "variantId": "01-agent-envelope", + "when": [ + "--agent-envelope" + ] + }, + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "02-report", + "when": [ + "without --agent-envelope" + ] + } + ] + }, + "canonicalDigest": "sha256:90583a47b9bdd605b2d3f24dcb1a5dbb2b4d9da20d7be097da381c9c77229e62" + }, + { + "definitionId": "proofkit.adoption-workflow-plan.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "schema", + "workflow" + ], + "requiredFields": [ + "schema", + "workflow" + ], + "rootKind": "object", + "variantId": "01-contract-envelope", + "when": [ + "--contract-envelope" + ] + }, + { + "allowedFields": [ + "inputRefs", + "nonClaims", + "presetId", + "scenario", + "schemaVersion", + "workflowId" + ], + "requiredFields": [ + "inputRefs", + "nonClaims", + "presetId", + "scenario", + "schemaVersion", + "workflowId" + ], + "rootKind": "object", + "variantId": "02-direct", + "when": [ + "without --contract-envelope" + ] + } + ] + }, + "canonicalDigest": "sha256:d07b4695792339e226769c2ac77022b45ac921e3f4847c36677aea86efc9583d" + }, + { + "definitionId": "proofkit.adoption-workflow-plan.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "requiredFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "rootKind": "object", + "variantId": "01-agent-envelope", + "when": [ + "--agent-envelope" + ] + }, + { + "allowedFields": [ + "blockers", + "inputRefs", + "nonClaims", + "phases", + "planKind", + "planState", + "scenario", + "schemaVersion", + "workflowId" + ], + "requiredFields": [ + "blockers", + "inputRefs", + "nonClaims", + "phases", + "planKind", + "planState", + "scenario", + "schemaVersion", + "workflowId" + ], + "rootKind": "object", + "variantId": "02-workflow-plan", + "when": [ + "without --agent-envelope" + ] + } + ] + }, + "canonicalDigest": "sha256:c06f7bfa9c10f73575aa5f927696d41db818697c9a3d320074b7c254011830d5" + }, + { + "definitionId": "proofkit.agent-route.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "availableInputs", + "browserMode", + "goal", + "knownChangedPaths", + "mode", + "nonClaims", + "observedReports", + "openBrowser", + "routeId", + "schemaVersion" + ], + "requiredFields": [ + "goal", + "mode", + "routeId", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-direct", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:fd1121285d1b357ed848febc248700a75417d06619e22ed36991631d7526375b" + }, + { + "definitionId": "proofkit.agent-route.output.v2.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "requiredFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "rootKind": "object", + "variantId": "01-agent-envelope", + "when": [ + "--agent-envelope" + ] + }, + { + "allowedFields": [ + "callerNonClaims", + "diagnostics", + "escalations", + "guidanceSlice", + "nextCommands", + "nonClaims", + "observedReports", + "omitted", + "reportId", + "reportKind", + "requiredInputs", + "schemaVersion", + "selectedRouteFamily", + "state", + "stopConditions", + "summary" + ], + "requiredFields": [ + "callerNonClaims", + "diagnostics", + "escalations", + "guidanceSlice", + "nextCommands", + "nonClaims", + "observedReports", + "omitted", + "reportId", + "reportKind", + "requiredInputs", + "schemaVersion", + "selectedRouteFamily", + "state", + "stopConditions", + "summary" + ], + "rootKind": "object", + "variantId": "02-routed", + "when": [ + "without --agent-envelope; known goal" + ] + }, + { + "allowedFields": [ + "callerNonClaims", + "diagnostics", + "escalations", + "guidanceSlice", + "nextCommands", + "nonClaims", + "omitted", + "reportId", + "reportKind", + "requiredInputs", + "schemaVersion", + "selectedRouteFamily", + "state", + "stopConditions", + "summary" + ], + "requiredFields": [ + "callerNonClaims", + "diagnostics", + "escalations", + "guidanceSlice", + "nextCommands", + "nonClaims", + "omitted", + "reportId", + "reportKind", + "requiredInputs", + "schemaVersion", + "selectedRouteFamily", + "state", + "stopConditions", + "summary" + ], + "rootKind": "object", + "variantId": "03-unknown-goal", + "when": [ + "without --agent-envelope; goal=unknown" + ] + } + ] + }, + "canonicalDigest": "sha256:db7d5b29083a939eea0f23253632394212b0d73b5f1b6206d0811d236d6a84ca" + }, + { + "definitionId": "proofkit.binding-partition.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "bindingSurfaces", + "delegations", + "nonClaims", + "partitionId", + "proofRouteRefs", + "routeOwners", + "routeReferences", + "schemaVersion", + "surfaceThresholds" + ], + "requiredFields": [ + "bindingSurfaces", + "delegations", + "nonClaims", + "partitionId", + "proofRouteRefs", + "routeOwners", + "routeReferences", + "schemaVersion", + "surfaceThresholds" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:c8b0915d112ba868b97eabc113cb479304f52a545f9e5acc3ea89733d5c8793a" + }, + { + "definitionId": "proofkit.binding-partition.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:cf6c70aeabd7719b65c380d798c13eb06a7b332ef49276b690e75c4b6376537b" + }, + { + "definitionId": "proofkit.branch-authority.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "branchRefs", + "nonClaims", + "preexistingFailures", + "reportId", + "schemaVersion" + ], + "requiredFields": [ + "branchRefs", + "nonClaims", + "preexistingFailures", + "reportId", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:c1ebe30c5e746195731c6f960278e7f3d5ec8394b0ddbc49905e406f35492037" + }, + { + "definitionId": "proofkit.branch-authority.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:0504c246c8cd4573231d77c2bb1387d45b1a303034f2f80cebef3eaa88a985e3" + }, + { + "definitionId": "proofkit.capability-map-admission.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "authority", + "capabilities", + "mapId", + "nonClaims", + "proofScope", + "repository", + "requiredVerification", + "scenarioAnchors", + "schemaVersion", + "trustMode" + ], + "requiredFields": [ + "authority", + "capabilities", + "mapId", + "nonClaims", + "proofScope", + "repository", + "requiredVerification", + "scenarioAnchors", + "schemaVersion", + "trustMode" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:9b1b0b9b884f84f856db7373844a1988bb7ef0198a9f125219025530ed6054b2" + }, + { + "definitionId": "proofkit.capability-map-admission.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:d20d4d55690f7b98768d8bb1c2c313f78bdf9ac8163fa6d1ff694664b2d7d999" + }, + { + "definitionId": "proofkit.changed-path-set.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "nonClaims", + "preexistingFailures", + "reportId", + "schemaVersion", + "sources" + ], + "requiredFields": [ + "nonClaims", + "preexistingFailures", + "reportId", + "schemaVersion", + "sources" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:12533579edbed52bc326d3775ef3c57665d1b4605d822d7824834343ef533e48" + }, + { + "definitionId": "proofkit.changed-path-set.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "requiredFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "rootKind": "object", + "variantId": "01-agent-envelope", + "when": [ + "--agent-envelope" + ] + }, + { + "allowedFields": [ + "changedPathSetHash", + "changedPaths", + "diagnostics", + "duplicatePaths", + "failures", + "invalidPaths", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "sourceSummaries", + "state", + "summary" + ], + "requiredFields": [ + "changedPathSetHash", + "changedPaths", + "diagnostics", + "duplicatePaths", + "failures", + "invalidPaths", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "sourceSummaries", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "02-report", + "when": [ + "without --agent-envelope" + ] + } + ] + }, + "canonicalDigest": "sha256:969e47d122156eb7bdc6a1ee8c194d9a07c90b9f7a40537bd9795c037baf84e2" + }, + { + "definitionId": "proofkit.completion-criteria.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "completionId", + "criteria", + "nonClaims", + "schemaVersion" + ], + "requiredFields": [ + "completionId", + "criteria", + "nonClaims", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:ae98108a097a9f65a3a5f427f6bec09077367109941cd3aaa24cb32246e2ef41" + }, + { + "definitionId": "proofkit.completion-criteria.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:81b3eb5f072ad45d3d71954bc9dfcb46bcdc1e8afc9ade800ae1ee73dfb503d0" + }, + { + "definitionId": "proofkit.conformance-profile.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "manifest", + "policy", + "profileId", + "proofContract", + "schemaVersion" + ], + "requiredFields": [ + "manifest", + "policy", + "proofContract", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:6ebc6e3ceac697a483cd713833fd038086d49ec11718b7d1d3a210b3aae6dd22" + }, + { + "definitionId": "proofkit.conformance-profile.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "profiles" + ], + "requiredFields": [ + "profiles" + ], + "rootKind": "object", + "variantId": "01-list", + "when": [ + "--list" + ] + }, + { + "allowedFields": [ + "commandCount", + "commandExecutionState", + "environmentClasses", + "failures", + "nonClaims", + "optionalSurfaceCount", + "preconditionedRequirementCount", + "profileId", + "profileResolutionState", + "purpose", + "requiredSurfaceCount", + "requirementCount", + "scenarioCount", + "surfaceCount", + "surfaces", + "verifyCommands", + "witnessMappingCount" + ], + "requiredFields": [ + "commandCount", + "commandExecutionState", + "environmentClasses", + "failures", + "nonClaims", + "optionalSurfaceCount", + "preconditionedRequirementCount", + "profileId", + "profileResolutionState", + "purpose", + "requiredSurfaceCount", + "requirementCount", + "scenarioCount", + "surfaceCount", + "surfaces", + "verifyCommands", + "witnessMappingCount" + ], + "rootKind": "object", + "variantId": "02-profile", + "when": [ + "--profile --format json", + "--profile without --format (defaults json)" + ] + }, + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "03-verify", + "when": [ + "--verify" + ] + } + ] + }, + "canonicalDigest": "sha256:11ee54032e139a25dda80337509ab6633ed31e297f9bc7461769badb2f5e0e0d" + }, + { + "definitionId": "proofkit.custom-rule-boundary.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "boundaryId", + "nonClaims", + "profileRef", + "rules", + "schemaVersion" + ], + "requiredFields": [ + "boundaryId", + "nonClaims", + "profileRef", + "rules", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:27fbb8d49c48e09422d37d0e0d17bf376cdb8a1d7dde2b6b763fe0edafb89809" + }, + { + "definitionId": "proofkit.custom-rule-boundary.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:860b4edcf72ddc35fb6e39df858831371edb8d5b3c10356af9f217dbec2daf8f" + }, + { + "definitionId": "proofkit.deployment-evidence-admission.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "admissionId", + "evidence", + "nonClaims", + "policy", + "rawOperatorEvidence", + "schemaVersion" + ], + "requiredFields": [ + "admissionId", + "evidence", + "nonClaims", + "policy", + "rawOperatorEvidence", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:674fb66250fbe3512562097e1b79c6b24b344f72392b8b0247afb73c6c5aa2e3" + }, + { + "definitionId": "proofkit.deployment-evidence-admission.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:c323ca5f340711192249783fbca21801b5f4f2bc06158e3f9b4d03a349c4c6a9" + }, + { + "definitionId": "proofkit.document-lifecycle-boundary.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "boundaryId", + "documents", + "nonClaims", + "schemaVersion" + ], + "requiredFields": [ + "boundaryId", + "documents", + "nonClaims", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:70cf3116106967a02a81583b000b2b06708d377973e5639278d5f958c6ad6a38" + }, + { + "definitionId": "proofkit.document-lifecycle-boundary.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:59a170aece56eb913f83ff7c62817fcc070fa1b69c1cc7f8a065d561c02041c4" + }, + { + "definitionId": "proofkit.evidence-graph.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "bindingId", + "bindings", + "nonClaims", + "requirements", + "schemaVersion", + "selection", + "witnessCommands" + ], + "requiredFields": [ + "bindingId", + "bindings", + "nonClaims", + "requirements", + "schemaVersion", + "witnessCommands" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:7ffe236ab203cf411dbfda6008ec8c10cc15691261031d4ed9ed90d81525dc12" + }, + { + "definitionId": "proofkit.evidence-graph.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "bindingCount", + "bindingId", + "commandCount", + "graphKind", + "nonClaims", + "requirementCount", + "requirements", + "schemaVersion" + ], + "requiredFields": [ + "bindingCount", + "bindingId", + "commandCount", + "graphKind", + "nonClaims", + "requirementCount", + "requirements", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-evidence-graph", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:2be3545f9329bb4e4ba4971fc04bf5811959cf0087fd7ab3a7c2c8b45b8d714a" + }, + { + "definitionId": "proofkit.external-consumer.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "binarySmokeProbeRuleId", + "nonClaims", + "npmIntegrity", + "npmShasum", + "packMetadataPath", + "packMetadataSha256", + "packageName", + "packageVersion", + "pilotId", + "pilotMode", + "releaseAuthorityInput", + "rollback", + "schemaVersion", + "sourceArtifactName", + "sourceCommit", + "sourceRepository", + "sourceWorkflowRun", + "tarballPath", + "tarballSha256", + "witnessPlan" + ], + "requiredFields": [ + "binarySmokeProbeRuleId", + "nonClaims", + "npmIntegrity", + "npmShasum", + "packMetadataPath", + "packMetadataSha256", + "packageName", + "packageVersion", + "pilotId", + "pilotMode", + "releaseAuthorityInput", + "rollback", + "schemaVersion", + "sourceArtifactName", + "sourceCommit", + "sourceRepository", + "sourceWorkflowRun", + "tarballPath", + "tarballSha256", + "witnessPlan" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:8434bcea866d7e26d93fc896d88e16659252c6b5b7057a60525d9c1b64c1bf40" + }, + { + "definitionId": "proofkit.external-consumer.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:90c984c94def164352e2a79339ff0397975563248794854c5ba44842e67f1252" + }, + { + "definitionId": "proofkit.gradual-adoption-bootstrap.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "bootstrap", + "guidance", + "input", + "schema" + ], + "requiredFields": [ + "bootstrap", + "guidance", + "input", + "schema" + ], + "rootKind": "object", + "variantId": "01-contract-envelope", + "when": [ + "--contract-envelope" + ] + }, + { + "allowedFields": [ + "bootstrapId", + "budget", + "checkedScope", + "commands", + "guidanceMode", + "module", + "nativeWitnesses", + "nonClaims", + "ownerRoute", + "packageVersionRef", + "paths", + "proofBindingPath", + "repository", + "rollback", + "schemaVersion", + "touchedRuleIds" + ], + "requiredFields": [ + "bootstrapId", + "budget", + "checkedScope", + "commands", + "guidanceMode", + "module", + "nativeWitnesses", + "nonClaims", + "ownerRoute", + "packageVersionRef", + "paths", + "proofBindingPath", + "repository", + "rollback", + "schemaVersion", + "touchedRuleIds" + ], + "rootKind": "object", + "variantId": "02-direct", + "when": [ + "without --contract-envelope" + ] + } + ] + }, + "canonicalDigest": "sha256:e576d1d74d9eb01246ed78c668b09f84e2d7e256a83c08bfe3facdca3b27c00f" + }, + { + "definitionId": "proofkit.gradual-adoption-bootstrap.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "requiredFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "rootKind": "object", + "variantId": "01-agent-envelope", + "when": [ + "--agent-envelope" + ] + }, + { + "allowedFields": [ + "agentActionPlan", + "exitCode", + "nextCommands", + "payloads", + "plannedFiles", + "report", + "witnessPlan" + ], + "requiredFields": [ + "agentActionPlan", + "exitCode", + "nextCommands", + "payloads", + "plannedFiles", + "report", + "witnessPlan" + ], + "rootKind": "object", + "variantId": "02-bootstrap", + "when": [ + "without --agent-envelope and --materialization-manifest" + ] + }, + { + "allowedFields": [ + "fileCount", + "files", + "manifestId", + "manifestKind", + "nextCommands", + "nonClaims", + "payloadFileCount", + "schemaVersion", + "sourceReport" + ], + "requiredFields": [ + "fileCount", + "files", + "manifestId", + "manifestKind", + "nextCommands", + "nonClaims", + "payloadFileCount", + "schemaVersion", + "sourceReport" + ], + "rootKind": "object", + "variantId": "03-materialization", + "when": [ + "--materialization-manifest" + ] + } + ] + }, + "canonicalDigest": "sha256:3ac22a1a8e4a1d4e9793b575370abecfd3b5aa387dbeb6d78c4c59b325f55ab9" + }, + { + "definitionId": "proofkit.gradual-adoption-guidance.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "bootstrap", + "guidance", + "input", + "schema" + ], + "requiredFields": [ + "bootstrap", + "guidance", + "input", + "schema" + ], + "rootKind": "object", + "variantId": "01-contract-envelope", + "when": [ + "--contract-envelope" + ] + }, + { + "allowedFields": [ + "agentGuidance", + "guidanceId", + "guidanceMode", + "modernization", + "nonClaims", + "ownerRoute", + "schemaVersion", + "scopeEvidence", + "sourceReport" + ], + "requiredFields": [ + "agentGuidance", + "guidanceId", + "guidanceMode", + "modernization", + "nonClaims", + "ownerRoute", + "schemaVersion", + "scopeEvidence", + "sourceReport" + ], + "rootKind": "object", + "variantId": "02-direct", + "when": [ + "without --contract-envelope" + ] + } + ] + }, + "canonicalDigest": "sha256:e25b962c3a1955e3f27b6449843f7fed214534c497e8a5082b21347ec1246ed3" + }, + { + "definitionId": "proofkit.gradual-adoption-guidance.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "requiredFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "rootKind": "object", + "variantId": "01-agent-envelope", + "when": [ + "--agent-envelope" + ] + }, + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "02-report", + "when": [ + "without --agent-envelope" + ] + } + ] + }, + "canonicalDigest": "sha256:311f17383cc0c6cff4468d590ca52fa99824af93e061a5e606d90916c16df48f" + }, + { + "definitionId": "proofkit.gradual-adoption.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "input", + "schema" + ], + "requiredFields": [ + "input", + "schema" + ], + "rootKind": "object", + "variantId": "01-contract-envelope", + "when": [ + "--contract-envelope" + ] + }, + { + "allowedFields": [ + "adoptionId", + "adoptionMode", + "agentReport", + "budget", + "module", + "nativeWitnesses", + "nonClaims", + "packageVersionRef", + "proofBinding", + "repository", + "rollback", + "rolloutClaim", + "schemaVersion" + ], + "requiredFields": [ + "adoptionId", + "adoptionMode", + "agentReport", + "budget", + "module", + "nativeWitnesses", + "nonClaims", + "packageVersionRef", + "proofBinding", + "repository", + "rollback", + "rolloutClaim", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "02-direct", + "when": [ + "without --contract-envelope" + ] + } + ] + }, + "canonicalDigest": "sha256:b4a0fdae3fd0e7b7ef4c3565e4184b993c9d612e527489d6656ea2e5747d0e1f" + }, + { + "definitionId": "proofkit.gradual-adoption.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:f780843a1d9afb5e7a82c5eefeca8f41d4ee8e852731ec3e690d1d8aedf46b16" + }, + { + "definitionId": "proofkit.impact.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "baseCommit", + "baseRef", + "changedBindingRecordIds", + "changedPaths", + "changedRecordIds", + "changedWitnessPathCoverage", + "generatedArtifactRules", + "headCommit", + "headRef", + "ignoredProofLikePaths", + "nonClaims", + "obligationCatalog", + "preexistingFailures", + "proofLikePaths", + "schemaVersion", + "unboundProofChangeRationale" + ], + "requiredFields": [ + "baseCommit", + "baseRef", + "changedBindingRecordIds", + "changedPaths", + "changedRecordIds", + "changedWitnessPathCoverage", + "generatedArtifactRules", + "headCommit", + "headRef", + "ignoredProofLikePaths", + "obligationCatalog", + "preexistingFailures", + "proofLikePaths", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-direct", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:125e94c27e5de283c973643a90a95101f818545718d9cf12cf89a58d511c8cf1" + }, + { + "definitionId": "proofkit.impact.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "baseCommit", + "baseRef", + "changedPaths", + "changedRecordIds", + "failures", + "headCommit", + "headRef", + "impactState", + "nonClaims", + "obligations", + "schemaVersion", + "unboundProofChanges" + ], + "requiredFields": [ + "baseCommit", + "baseRef", + "changedPaths", + "changedRecordIds", + "failures", + "headCommit", + "headRef", + "impactState", + "nonClaims", + "obligations", + "schemaVersion", + "unboundProofChanges" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:a979e09258d551c600da01624aab78353c1434bb92baf51e24b33ae6baa85044" + }, + { + "definitionId": "proofkit.init.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:9f57ccd7905a2d8cbffeb8cf3495d338ea67c0ebfe66889b4d5ee16434be5f88" + }, + { + "definitionId": "proofkit.json-report-cli-adapter-source.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "artifactKind", + "exportedSymbols", + "format", + "generatorId", + "language", + "nonClaims", + "schemaVersion", + "source", + "sourceFileName", + "sourceSha256", + "summary" + ], + "requiredFields": [ + "artifactKind", + "exportedSymbols", + "format", + "generatorId", + "language", + "nonClaims", + "schemaVersion", + "source", + "sourceFileName", + "sourceSha256", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:9378c7663485f39089cf6c39a3d2f778e28b5801637229fe9524a9652a7b0737" + }, + { + "definitionId": "proofkit.migration-parity-admission.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "nonClaims", + "parityRecords", + "paritySetId", + "schemaVersion", + "sourceProofOwners", + "targetProofkitRefs" + ], + "requiredFields": [ + "nonClaims", + "parityRecords", + "paritySetId", + "schemaVersion", + "sourceProofOwners", + "targetProofkitRefs" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:a1f8ab777249d7ff613bfb3256cd96514235ac90d02ad39ce8720330cbd71d38" + }, + { + "definitionId": "proofkit.migration-parity-admission.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:aaea5c7d9d12e1fbc406d080dcda5aca84351c5a2048d40535a1b3895cc37440" + }, + { + "definitionId": "proofkit.migration-plan.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "followUpCommands", + "migrationId", + "nonClaims", + "parityEvidenceRefs", + "retainedOwners", + "retirementCandidates", + "schemaVersion", + "sourceProofOwners", + "targetProofkitRefs" + ], + "requiredFields": [ + "followUpCommands", + "migrationId", + "nonClaims", + "parityEvidenceRefs", + "retainedOwners", + "retirementCandidates", + "schemaVersion", + "sourceProofOwners", + "targetProofkitRefs" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:aacf82037705e3c7e7e699b59856d9c36c68dec835cf5cbe1798f9a8761d4072" + }, + { + "definitionId": "proofkit.migration-plan.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "blockers", + "migrationId", + "nonClaims", + "parityEvidenceRefs", + "phases", + "planKind", + "planState", + "retainedOwners", + "retirementCandidates", + "schemaVersion", + "sourceProofOwners", + "targetProofkitRefs" + ], + "requiredFields": [ + "blockers", + "migrationId", + "nonClaims", + "parityEvidenceRefs", + "phases", + "planKind", + "planState", + "retainedOwners", + "retirementCandidates", + "schemaVersion", + "sourceProofOwners", + "targetProofkitRefs" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:e6a152b5ca41396f25874b1d1ddd1dbc323bd9e927a18c5fe5088ad527fe65cd" + }, + { + "definitionId": "proofkit.obligation-decision.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "decisionId", + "nonClaims", + "obligations", + "schemaVersion" + ], + "requiredFields": [ + "decisionId", + "nonClaims", + "obligations", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:5c9e25b7a7e379f21cd586693f1303a823699c348129a35c20c79fe0a225597f" + }, + { + "definitionId": "proofkit.obligation-decision.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "requiredFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "rootKind": "object", + "variantId": "01-agent-envelope", + "when": [ + "--agent-envelope" + ] + }, + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "02-report", + "when": [ + "without --agent-envelope" + ] + } + ] + }, + "canonicalDigest": "sha256:0a9bf16564faaf4362e84b490ec24e722ed5b98617d8f4d7f62d8ce30e21c2e5" + }, + { + "definitionId": "proofkit.package-runtime-dependency-admission.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "admissibleLocations", + "expectedDependencySpec", + "expectedLockfileIntegrity", + "expectedPackageName", + "expectedPackageVersion", + "nonClaims", + "packageResolution", + "reportId", + "schemaVersion" + ], + "requiredFields": [ + "admissibleLocations", + "expectedDependencySpec", + "expectedLockfileIntegrity", + "expectedPackageName", + "expectedPackageVersion", + "nonClaims", + "packageResolution", + "reportId", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:d831ad36980053791cd597bbba51921f2b72a9ba49d878415b945678c52c41ed" + }, + { + "definitionId": "proofkit.package-runtime-dependency-admission.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:e6c9e94611b5de09befb45f56d1770bdf1e32a96752cf3e5fb8a58e8c6ecfc54" + }, + { + "definitionId": "proofkit.pilot-admission.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "input", + "schema", + "stackDiverseInput" + ], + "requiredFields": [ + "input", + "schema", + "stackDiverseInput" + ], + "rootKind": "object", + "variantId": "01-contract-all", + "when": [ + "--contract-envelope --pilot all" + ] + }, + { + "allowedFields": [ + "input", + "schema" + ], + "requiredFields": [ + "input", + "schema" + ], + "rootKind": "object", + "variantId": "02-contract-first", + "when": [ + "--contract-envelope --pilot first", + "--contract-envelope without --pilot (defaults first)" + ] + }, + { + "allowedFields": [ + "schema", + "stackDiverseInput" + ], + "requiredFields": [ + "schema", + "stackDiverseInput" + ], + "rootKind": "object", + "variantId": "03-contract-stack-diverse", + "when": [ + "--contract-envelope --pilot stack-diverse", + "--contract-envelope --stack-diverse" + ] + }, + { + "allowedFields": [ + "agentReportRoutes", + "blockingRequirements", + "cacheNegativeChecks", + "cacheScheduler", + "falsePositiveBudget", + "impactDemo", + "impactDemos", + "infrastructureBudget", + "metrics", + "nonClaims", + "packageVersionRef", + "pilotId", + "pilotMode", + "profile", + "rollback", + "rolloutClaim", + "schemaVersion", + "stackDiversity", + "timingBudget" + ], + "requiredFields": [ + "agentReportRoutes", + "blockingRequirements", + "cacheNegativeChecks", + "cacheScheduler", + "falsePositiveBudget", + "infrastructureBudget", + "nonClaims", + "packageVersionRef", + "pilotId", + "pilotMode", + "profile", + "rollback", + "rolloutClaim", + "schemaVersion", + "timingBudget" + ], + "rootKind": "object", + "variantId": "04-direct-first", + "when": [ + "without --contract-envelope; --pilot first", + "without --contract-envelope; without --pilot (defaults first)" + ] + }, + { + "allowedFields": [ + "agentReportRoutes", + "blockingRequirements", + "cacheNegativeChecks", + "cacheScheduler", + "falsePositiveBudget", + "impactDemo", + "impactDemos", + "infrastructureBudget", + "metrics", + "nonClaims", + "packageVersionRef", + "pilotId", + "pilotMode", + "profile", + "rollback", + "rolloutClaim", + "schemaVersion", + "stackDiversity", + "timingBudget" + ], + "requiredFields": [ + "agentReportRoutes", + "blockingRequirements", + "cacheNegativeChecks", + "cacheScheduler", + "falsePositiveBudget", + "infrastructureBudget", + "nonClaims", + "packageVersionRef", + "pilotId", + "pilotMode", + "profile", + "rollback", + "rolloutClaim", + "schemaVersion", + "stackDiversity", + "timingBudget" + ], + "rootKind": "object", + "variantId": "05-direct-stack-diverse", + "when": [ + "without --contract-envelope; --pilot stack-diverse", + "without --contract-envelope; --stack-diverse" + ] + } + ] + }, + "canonicalDigest": "sha256:984795f4b9c38763b001778fb2e072b0dc3e9eb6fac91b1ed966fd34382399c4" + }, + { + "definitionId": "proofkit.pilot-admission.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "union", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [], + "requiredFields": [], + "rootKind": "array", + "variantId": "01-all", + "when": [ + "--contract-envelope --pilot all" + ] + }, + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "02-report", + "when": [ + "--contract-envelope --pilot first", + "--contract-envelope --pilot stack-diverse", + "--contract-envelope --stack-diverse", + "--contract-envelope without --pilot (defaults first)", + "direct input" + ] + } + ] + }, + "canonicalDigest": "sha256:b4e4dc05858e4ef86804e153db5e901c7c2af354d8d88b572ea86358e1194a57" + }, + { + "definitionId": "proofkit.producer-policy-self-proof.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "admissionChanges", + "baselinePolicyDigest", + "guardId", + "mergeObligationReceiptRefs", + "nonClaimRefs", + "nonClaims", + "policyChangeDigest", + "policyChangeId", + "policyId", + "policyOwner", + "policySurfaceRefs", + "proposedPolicyDigest", + "schemaVersion" + ], + "requiredFields": [ + "admissionChanges", + "baselinePolicyDigest", + "guardId", + "mergeObligationReceiptRefs", + "nonClaimRefs", + "nonClaims", + "policyChangeDigest", + "policyChangeId", + "policyId", + "policyOwner", + "policySurfaceRefs", + "proposedPolicyDigest", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:23c1bd4b44977a88008334b613ab484f43fd024aa5e43befde88d66910406713" + }, + { + "definitionId": "proofkit.producer-policy-self-proof.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:869f52f04871c604f7ee16118e5152ce761e9917374d96d0387858cfe9c132db" + }, + { + "definitionId": "proofkit.proof-obligation-algebra.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "algebraId", + "nonClaims", + "obligations", + "schemaVersion" + ], + "requiredFields": [ + "algebraId", + "nonClaims", + "obligations", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:cf9e13e7e1186d4dcdcfcecfe33c4dc61f88791242b42a4a3a92b349c605b476" + }, + { + "definitionId": "proofkit.proof-obligation-algebra.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:4133b3c92c688c56a04871ededc6a595b17690d02e70ee35b87f728c21109a37" + }, + { + "definitionId": "proofkit.proof-receipt-admission.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "nonClaims", + "receiptSetId", + "receipts", + "schemaVersion" + ], + "requiredFields": [ + "nonClaims", + "receiptSetId", + "receipts", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:de065dc77609d7ae2bbe2a5c78160c29b5b775dbd259e72935c97c5083af99dd" + }, + { + "definitionId": "proofkit.proof-receipt-admission.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:80c30c4767fb3e65972a382671c1f207c6150ba1a29612bee07915903bf8d46f" + }, + { + "definitionId": "proofkit.proof-slice.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "bindingId", + "bindings", + "nonClaims", + "requirements", + "schemaVersion", + "selection", + "witnessCommands" + ], + "requiredFields": [ + "bindingId", + "bindings", + "nonClaims", + "requirements", + "schemaVersion", + "selection", + "witnessCommands" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:71914c95c70dbace0ebce1dc4609624e3b91c994cd8c5adc20ed84d4200f0ac9" + }, + { + "definitionId": "proofkit.proof-slice.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "bindingId", + "nonClaims", + "omittedRequirementCount", + "schemaVersion", + "selectedCommandIds", + "selectedRequirementCount", + "selectedRequirements", + "selection", + "sliceKind" + ], + "requiredFields": [ + "bindingId", + "nonClaims", + "omittedRequirementCount", + "schemaVersion", + "selectedCommandIds", + "selectedRequirementCount", + "selectedRequirements", + "selection", + "sliceKind" + ], + "rootKind": "object", + "variantId": "01-proof-slice", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:5fbdf25c90a2020525f97453c9d9fe5a774fe0e2eacf00d1b2aea3ff5f4fa0f0" + }, + { + "definitionId": "proofkit.readiness-closeout.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "environmentPreconditions", + "exactCommand", + "frontier", + "inputDefinitions", + "markdownText", + "negatedNonClaimPhrases", + "nonClaims", + "phraseRules", + "readinessRowPrefixes", + "readinessSections", + "reportId", + "runIdentity", + "schemaVersion" + ], + "requiredFields": [ + "environmentPreconditions", + "exactCommand", + "frontier", + "inputDefinitions", + "markdownText", + "negatedNonClaimPhrases", + "nonClaims", + "phraseRules", + "readinessRowPrefixes", + "readinessSections", + "reportId", + "runIdentity", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:7db0df3e9be3eba731937a4d887aa031cd70d3d5db36de3c630a1bced1d04bcf" + }, + { + "definitionId": "proofkit.readiness-closeout.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:0ef2b994e1ee6fed9ce10740c0464dda72c3df220a9453e7a6a221fba31cec29" + }, + { + "definitionId": "proofkit.receipt-currentness-scope.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "admissionId", + "nonClaims", + "obligationReceipts", + "schemaVersion" + ], + "requiredFields": [ + "admissionId", + "nonClaims", + "obligationReceipts", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:912f60b637aa0ca917b1c90d4caa4e3e65ec60ad0c5529d0da9d815fc766c5f5" + }, + { + "definitionId": "proofkit.receipt-currentness-scope.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:30a1b7ef616c1e44bbdbebc6e511231166a2d1ea600d8d5954484fe8c0e7cdf4" + }, + { + "definitionId": "proofkit.receipt-producer-admission.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "environmentClasses", + "nonClaims", + "policyId", + "producers", + "receiptKinds", + "receipts", + "schemaVersion" + ], + "requiredFields": [ + "environmentClasses", + "nonClaims", + "policyId", + "producers", + "receiptKinds", + "receipts", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:b62a461a38e3f878dc9bf41d508102ccd66d10fd659a4830a04d2cc67c0e74cc" + }, + { + "definitionId": "proofkit.receipt-producer-admission.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:1704f4b6482065ded5ee235f1e32aad5b0ae83a8170968fcc1c617db3789c1c0" + }, + { + "definitionId": "proofkit.receipt-trust-class.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "nonClaims", + "obligationReceipts", + "policyId", + "proofClasses", + "schemaVersion", + "trustClasses" + ], + "requiredFields": [ + "nonClaims", + "obligationReceipts", + "policyId", + "proofClasses", + "schemaVersion", + "trustClasses" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:19b0af54f38516daf4c0fb386a16284821f07ee14028738b5287a09f8b3b2122" + }, + { + "definitionId": "proofkit.receipt-trust-class.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:3914567b0719287a883352e711121f71610ab041df74a009edd2363fb6907872" + }, + { + "definitionId": "proofkit.registry-consumer-proof-input-compose.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "compositionId", + "consumerId", + "dependencyName", + "dependencySpec", + "frozenInstall", + "install", + "nonClaims", + "packageName", + "packageVersion", + "preconditions", + "registryMetadata", + "registryPackProof", + "registryUrl", + "releaseAuthorityInput", + "releaseAuthorityReport", + "rollback", + "rollbackVersionPin", + "schemaVersion", + "smoke" + ], + "requiredFields": [ + "compositionId", + "consumerId", + "dependencyName", + "dependencySpec", + "frozenInstall", + "install", + "nonClaims", + "packageName", + "packageVersion", + "preconditions", + "registryMetadata", + "registryPackProof", + "registryUrl", + "releaseAuthorityInput", + "releaseAuthorityReport", + "rollback", + "rollbackVersionPin", + "schemaVersion", + "smoke" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:0e324e9ebd262c2efbbea3b08eae59c521cbb61f99c5194b72bf5eba414d6fbb" + }, + { + "definitionId": "proofkit.registry-consumer-proof-input-compose.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "compositionId", + "compositionKind", + "nonClaims", + "registryConsumerInput", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "compositionId", + "compositionKind", + "nonClaims", + "registryConsumerInput", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:597b79a019ad1a3caf29a2d5fe723cfaf0b185d6897724f598f7132a3e653a6d" + }, + { + "definitionId": "proofkit.registry-consumer.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "consumerId", + "dependencyName", + "dependencySpec", + "nonClaims", + "packageName", + "packageVersion", + "registryUrl", + "releaseAuthorityInput", + "rollbackVersionPin", + "schemaVersion", + "tarballFileName", + "tarballIntegrity", + "tarballShasum" + ], + "requiredFields": [ + "consumerId", + "dependencyName", + "dependencySpec", + "nonClaims", + "packageName", + "packageVersion", + "registryUrl", + "releaseAuthorityInput", + "rollbackVersionPin", + "schemaVersion", + "tarballFileName", + "tarballIntegrity", + "tarballShasum" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:cc490b41d9f9e68c5121f81e702222ed3e717d462fa2059a0e0b76c0c82fb5e5" + }, + { + "definitionId": "proofkit.registry-consumer.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:70d7eb27630638e9b38fa930cd61954bedbc84aeb3916490c036c33777767529" + }, + { + "definitionId": "proofkit.release-authority.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "artifactProof", + "channel", + "consumerContract", + "nonClaims", + "package", + "registryAuthority", + "releaseId", + "rollback", + "rolloutClaim", + "schemaVersion" + ], + "requiredFields": [ + "artifactProof", + "channel", + "consumerContract", + "nonClaims", + "package", + "registryAuthority", + "releaseId", + "rollback", + "rolloutClaim", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:1aa30fc2229803387d497d1de0b4d95c1b1d732a9c1fda65c66a6db0ebb802c3" + }, + { + "definitionId": "proofkit.release-authority.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:95a36a8fe2976c8b7fccb4a83d8c9913b7c11dcb89f78990ce91422c194bcb3d" + }, + { + "definitionId": "proofkit.rendered-artifact-freshness.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "artifacts", + "freshnessSetId", + "nonClaims", + "schemaVersion" + ], + "requiredFields": [ + "artifacts", + "freshnessSetId", + "nonClaims", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:525ed9c2b820df9f52dce95442bf48bccfba1368f82e9bf68b92c275cf79b51c" + }, + { + "definitionId": "proofkit.rendered-artifact-freshness.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:99c5af92d1f98744436493c4330c78ed409da2f72be0bb7bb82670cf7fb8fe2f" + }, + { + "definitionId": "proofkit.repo-profile-admission.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "facts", + "policy", + "profile", + "schemaVersion" + ], + "requiredFields": [ + "facts", + "policy", + "profile" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:7cb2357b227c2669b5322712ce6e032315e18b726dde8ead31b1feb29e14b418" + }, + { + "definitionId": "proofkit.repo-profile-admission.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:acf8e7db39bfb1325d83c2cdfe68a7c243847d207e56691a3b2937b28f95d3e7" + }, + { + "definitionId": "proofkit.requirement-authoring-plan.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "authoringPlanId", + "authoringRefs", + "candidateUpdates", + "currentRequirementSource", + "mode", + "nonClaims", + "schemaVersion" + ], + "requiredFields": [ + "authoringPlanId", + "authoringRefs", + "candidateUpdates", + "currentRequirementSource", + "mode", + "nonClaims", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:86f8f4913b5316caa63c40d79125199b282d108cbd8b9711db21c2023d1a4ca4" + }, + { + "definitionId": "proofkit.requirement-authoring-plan.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "authoringPlanId", + "candidateChangeSet", + "mode", + "nonAuthoritativeAdmissionPreview", + "nonClaims", + "ownerReviewPlan", + "planKind", + "promotionPreconditions", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "authoringPlanId", + "candidateChangeSet", + "mode", + "nonAuthoritativeAdmissionPreview", + "nonClaims", + "ownerReviewPlan", + "planKind", + "promotionPreconditions", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:019c920b72a4d2fba8f6364bff554442b0194ac42364d645d5b0b71f28df24b1" + }, + { + "definitionId": "proofkit.requirement-bindings.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "bindingId", + "bindings", + "nonClaims", + "requirements", + "schemaVersion", + "selection", + "witnessCommands" + ], + "requiredFields": [ + "bindingId", + "bindings", + "nonClaims", + "requirements", + "schemaVersion", + "selection", + "witnessCommands" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:e7da70a2267f2ca13771e864bba6a1a0416132c2d56decb5db9f9694de021daf" + }, + { + "definitionId": "proofkit.requirement-bindings.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:f31dd686f28fce4bf74265b557dc25428179cbc282753b4bea01439cd98f73c2" + }, + { + "definitionId": "proofkit.requirement-browser-server.input.v2.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "compactProofContract", + "coverageUniverse", + "localEnvironmentPolicy", + "normalizedTestEvidenceInventory", + "options", + "ownerInvariantRegistry", + "requirementProofBinding", + "requirementSource", + "schemaVersion", + "testEvidenceInventory", + "viewInputId" + ], + "requiredFields": [ + "compactProofContract", + "coverageUniverse", + "localEnvironmentPolicy", + "requirementSource", + "schemaVersion", + "viewInputId" + ], + "rootKind": "object", + "variantId": "01-coverage-compact", + "when": [ + "--view coverage; compact proof" + ] + }, + { + "allowedFields": [ + "compactProofContract", + "coverageUniverse", + "localEnvironmentPolicy", + "normalizedTestEvidenceInventory", + "options", + "ownerInvariantRegistry", + "requirementProofBinding", + "requirementSource", + "schemaVersion", + "testEvidenceInventory", + "viewInputId" + ], + "requiredFields": [ + "coverageUniverse", + "requirementProofBinding", + "requirementSource", + "schemaVersion", + "viewInputId" + ], + "rootKind": "object", + "variantId": "02-coverage-structured", + "when": [ + "--view coverage; structured proof" + ] + }, + { + "allowedFields": [ + "authority_state", + "binding_columns", + "bindings", + "contract_id", + "contract_kind", + "non_claims", + "normalization_profile", + "schema_version", + "surface_columns", + "surfaces", + "witness_columns" + ], + "requiredFields": [ + "authority_state", + "binding_columns", + "bindings", + "contract_id", + "contract_kind", + "non_claims", + "normalization_profile", + "schema_version", + "surface_columns", + "surfaces", + "witness_columns" + ], + "rootKind": "object", + "variantId": "03-proof-compact", + "when": [ + "--view proof; compact proof" + ] + }, + { + "allowedFields": [ + "bindingId", + "bindings", + "nonClaims", + "requirements", + "schemaVersion", + "selection", + "witnessCommands" + ], + "requiredFields": [ + "bindingId", + "bindings", + "nonClaims", + "requirements", + "schemaVersion", + "selection", + "witnessCommands" + ], + "rootKind": "object", + "variantId": "04-proof-structured", + "when": [ + "--view proof; structured proof" + ] + }, + { + "allowedFields": [ + "nonClaims", + "overviewPath", + "requirements", + "requirementsPath", + "schemaVersion", + "sourceId", + "specPackagePath" + ], + "requiredFields": [ + "nonClaims", + "overviewPath", + "requirements", + "requirementsPath", + "schemaVersion", + "sourceId", + "specPackagePath" + ], + "rootKind": "object", + "variantId": "05-source", + "when": [ + "--view source" + ] + }, + { + "allowedFields": [ + "callerAnnotations", + "edges", + "nodes", + "overlays", + "rootNodeId", + "schemaVersion", + "treeId" + ], + "requiredFields": [ + "callerAnnotations", + "edges", + "nodes", + "overlays", + "rootNodeId", + "schemaVersion", + "treeId" + ], + "rootKind": "object", + "variantId": "06-spec-tree", + "when": [ + "--view spec-tree" + ] + }, + { + "allowedFields": [ + "context", + "diffInput", + "graphInput", + "schemaVersion", + "workspaceId" + ], + "requiredFields": [ + "context", + "schemaVersion", + "workspaceId" + ], + "rootKind": "object", + "variantId": "07-workspace", + "when": [ + "--view workspace" + ] + } + ] + }, + "canonicalDigest": "sha256:50a7bb1b477b6067d410cf22db7e7564a9742aa7f6a270642149d372f0156007" + }, + { + "definitionId": "proofkit.requirement-browser-server.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "authority", + "host", + "htmlByteLength", + "nonClaims", + "planKind", + "port", + "portSelection", + "renderedAuthority", + "renderedViewKind", + "schemaVersion", + "url", + "view" + ], + "requiredFields": [ + "authority", + "host", + "htmlByteLength", + "nonClaims", + "planKind", + "port", + "portSelection", + "renderedAuthority", + "renderedViewKind", + "schemaVersion", + "url", + "view" + ], + "rootKind": "object", + "variantId": "01-plan", + "when": [ + "without --serve; --view coverage", + "without --serve; --view proof", + "without --serve; --view source", + "without --serve; --view spec-tree", + "without --serve; --view workspace" + ] + }, + { + "allowedFields": [ + "handoffKind", + "nonClaims", + "schemaVersion", + "snapshotRefs", + "state" + ], + "requiredFields": [ + "handoffKind", + "nonClaims", + "schemaVersion", + "snapshotRefs", + "state" + ], + "rootKind": "object", + "variantId": "02-one-shot-terminal", + "when": [ + "--open; --serve; --session-mode one-shot-question; --view workspace; state=cancelled|expired" + ] + }, + { + "allowedFields": [ + "annotations", + "context", + "handoffKind", + "instructionAuthority", + "nonClaims", + "schemaVersion", + "snapshotRefs", + "sourceTextAuthority", + "state" + ], + "requiredFields": [ + "annotations", + "context", + "handoffKind", + "instructionAuthority", + "nonClaims", + "schemaVersion", + "snapshotRefs", + "sourceTextAuthority", + "state" + ], + "rootKind": "object", + "variantId": "03-one-shot-submitted", + "when": [ + "--open; --serve; --session-mode one-shot-question; --view workspace; state=submitted" + ] + } + ] + }, + "canonicalDigest": "sha256:c2e7d851c7928560d4267fe85ebc0ae61e33c4c0ef15f7f7b01deb73bb80eda7" + }, + { + "definitionId": "proofkit.requirement-context-compose.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "catalogId", + "coverage", + "proofBinding", + "requirementSources", + "schemaVersion", + "specTree" + ], + "requiredFields": [ + "catalogId", + "requirementSources", + "schemaVersion", + "specTree" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:41bc233c96bd468bc96eeb0447e037cacdbcf92bb04161052303e8747e4282f2" + }, + { + "definitionId": "proofkit.requirement-context-compose.output.v2.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "catalogId", + "contextKind", + "expectedDigestCoverage", + "nonClaims", + "projections", + "schemaVersion", + "snapshotId", + "sources" + ], + "requiredFields": [ + "catalogId", + "contextKind", + "expectedDigestCoverage", + "nonClaims", + "projections", + "schemaVersion", + "snapshotId", + "sources" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:cdadd7b589d682a122cfe2b801f001a3b22a2269aa4977ebe073932d11e1f816" + }, + { + "definitionId": "proofkit.requirement-context-slice.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "context", + "query", + "schemaVersion", + "sliceId" + ], + "requiredFields": [ + "context", + "query", + "schemaVersion", + "sliceId" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:43852cf1a52b3c1f000ec95e460fbad157a6b6a39958d20f81c9e1e4d31e3c6f" + }, + { + "definitionId": "proofkit.requirement-context-slice.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "contextKind", + "nonClaims", + "omissions", + "profile", + "projections", + "schemaVersion", + "sliceId", + "snapshotId", + "state" + ], + "requiredFields": [ + "contextKind", + "nonClaims", + "omissions", + "profile", + "projections", + "schemaVersion", + "sliceId", + "snapshotId", + "state" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:61f7fd43e2e9da5bde9b61bb86e02cbf337a43c6891ca888c1d42936e5b16456" + }, + { + "definitionId": "proofkit.requirement-coverage-input-compose.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "compactProofContract", + "composerInputId", + "coverageUniverse", + "localEnvironmentPolicy", + "normalizedTestEvidenceInventory", + "options", + "ownerInvariantRegistry", + "requirementProofBinding", + "requirementSource", + "schemaVersion", + "selectedOwnerIds", + "testEvidenceInventory", + "viewInputId" + ], + "requiredFields": [ + "composerInputId", + "coverageUniverse", + "localEnvironmentPolicy", + "options", + "ownerInvariantRegistry", + "requirementSource", + "schemaVersion", + "selectedOwnerIds", + "viewInputId" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:06fa8d0a592549cebdea1d7b9d428e090de7b57e265dbc263699b28dbe6eede8" + }, + { + "definitionId": "proofkit.requirement-coverage-input-compose.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "compactProofContract", + "coverageUniverse", + "localEnvironmentPolicy", + "options", + "ownerInvariantRegistry", + "requirementProofBinding", + "requirementSource", + "schemaVersion", + "testEvidenceInventory", + "viewInputId" + ], + "requiredFields": [ + "compactProofContract", + "coverageUniverse", + "localEnvironmentPolicy", + "options", + "ownerInvariantRegistry", + "requirementProofBinding", + "requirementSource", + "schemaVersion", + "testEvidenceInventory", + "viewInputId" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:7e913a998ba47601fd054dbec429d96cb2a546c8925d311f45aca86f1e038bec" + }, + { + "definitionId": "proofkit.requirement-coverage-view.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "compactProofContract", + "coverageUniverse", + "localEnvironmentPolicy", + "normalizedTestEvidenceInventory", + "options", + "ownerInvariantRegistry", + "requirementProofBinding", + "requirementSource", + "schemaVersion", + "testEvidenceInventory", + "viewInputId" + ], + "requiredFields": [ + "compactProofContract", + "coverageUniverse", + "localEnvironmentPolicy", + "requirementSource", + "schemaVersion", + "viewInputId" + ], + "rootKind": "object", + "variantId": "01-coverage-compact", + "when": [ + "compact proof" + ] + }, + { + "allowedFields": [ + "compactProofContract", + "coverageUniverse", + "localEnvironmentPolicy", + "normalizedTestEvidenceInventory", + "options", + "ownerInvariantRegistry", + "requirementProofBinding", + "requirementSource", + "schemaVersion", + "testEvidenceInventory", + "viewInputId" + ], + "requiredFields": [ + "coverageUniverse", + "requirementProofBinding", + "requirementSource", + "schemaVersion", + "viewInputId" + ], + "rootKind": "object", + "variantId": "02-coverage-structured", + "when": [ + "structured proof" + ] + } + ] + }, + "canonicalDigest": "sha256:b5ddab0a55a381f002b39ece0f5727171f7984856e553200bbb62a49821ff6ac" + }, + { + "definitionId": "proofkit.requirement-coverage-view.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "requiredFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "rootKind": "object", + "variantId": "01-agent-envelope", + "when": [ + "--agent-envelope" + ] + }, + { + "allowedFields": [ + "authority", + "bindingId", + "commandCoverage", + "commandCoverageCount", + "completenessDeclaration", + "contractId", + "coverageUniverseId", + "deadZones", + "failureClassifications", + "failureCount", + "failures", + "guidanceSummary", + "nonClaims", + "ownerInvariantCoverage", + "ownerInvariantCoverageCount", + "ownerInvariantRegistryId", + "proofMode", + "requirementCoverage", + "requirementCoverageCount", + "schemaVersion", + "sourceId", + "state", + "testInventoryId", + "viewInputId", + "viewKind", + "warningClassifications", + "warningCount", + "warnings" + ], + "requiredFields": [ + "authority", + "bindingId", + "commandCoverage", + "commandCoverageCount", + "completenessDeclaration", + "contractId", + "coverageUniverseId", + "deadZones", + "failureClassifications", + "failureCount", + "failures", + "guidanceSummary", + "nonClaims", + "ownerInvariantCoverage", + "ownerInvariantCoverageCount", + "ownerInvariantRegistryId", + "proofMode", + "requirementCoverage", + "requirementCoverageCount", + "schemaVersion", + "sourceId", + "state", + "testInventoryId", + "viewInputId", + "viewKind", + "warningClassifications", + "warningCount", + "warnings" + ], + "rootKind": "object", + "variantId": "02-report", + "when": [ + "without --agent-envelope" + ] + } + ] + }, + "canonicalDigest": "sha256:697d24d12d4772b67283f24da2f265372de241bd4741ac23f7b984ca96023818" + }, + { + "definitionId": "proofkit.requirement-impact-input-compose.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "baseCommit", + "baseCompactProofContract", + "baseRef", + "baseRequirementSources", + "changedPathSources", + "composerInputId", + "currentCompactProofContract", + "currentRequirementSources", + "generatedArtifactPolicyState", + "generatedArtifactRules", + "headCommit", + "headRef", + "localEnvironmentPolicy", + "nonClaims", + "preexistingFailures", + "proofBindingSourcePaths", + "proofLikePathPolicy", + "schemaVersion", + "unboundProofChangeRationale" + ], + "requiredFields": [ + "baseCommit", + "baseRef", + "baseRequirementSources", + "changedPathSources", + "composerInputId", + "currentCompactProofContract", + "currentRequirementSources", + "generatedArtifactPolicyState", + "generatedArtifactRules", + "headCommit", + "headRef", + "localEnvironmentPolicy", + "nonClaims", + "preexistingFailures", + "proofBindingSourcePaths", + "proofLikePathPolicy", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:e6af8f427f781395710407a87bfd5fa91a0b3c8aaaee437c2487472a243b22a6" + }, + { + "definitionId": "proofkit.requirement-impact-input-compose.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "baseCommit", + "baseRef", + "changedBindingRecordIds", + "changedPaths", + "changedRecordIds", + "changedWitnessPathCoverage", + "generatedArtifactRules", + "headCommit", + "headRef", + "ignoredProofLikePaths", + "nonClaims", + "obligationCatalog", + "preexistingFailures", + "proofLikePaths", + "schemaVersion", + "unboundProofChangeRationale" + ], + "requiredFields": [ + "baseCommit", + "baseRef", + "changedBindingRecordIds", + "changedPaths", + "changedRecordIds", + "changedWitnessPathCoverage", + "generatedArtifactRules", + "headCommit", + "headRef", + "ignoredProofLikePaths", + "nonClaims", + "obligationCatalog", + "preexistingFailures", + "proofLikePaths", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-impact-input", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:9fd57d299d4e76bb3b0bf10f313a8dd9bfd593b8d15fa4844d5e342c4d929ee9" + }, + { + "definitionId": "proofkit.requirement-proof-resolver.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "authority_state", + "binding_columns", + "bindings", + "contract_id", + "contract_kind", + "non_claims", + "normalization_profile", + "schema_version", + "surface_columns", + "surfaces", + "witness_columns" + ], + "requiredFields": [ + "authority_state", + "binding_columns", + "bindings", + "contract_id", + "contract_kind", + "non_claims", + "normalization_profile", + "schema_version", + "surface_columns", + "surfaces", + "witness_columns" + ], + "rootKind": "object", + "variantId": "01-compact", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:68ade17ac7e9fb3496ad0e7ea8ae9fbcf734afa4e6b8a44442e1af3c4f87bf15" + }, + { + "definitionId": "proofkit.requirement-proof-resolver.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "commands", + "conformanceProofContract", + "contractId", + "environmentClasses", + "localEnvironmentPolicy", + "nonClaims", + "projectionKind", + "requirements", + "scenarios", + "schemaVersion", + "surfaces", + "witnessSelectors" + ], + "requiredFields": [ + "commands", + "conformanceProofContract", + "contractId", + "environmentClasses", + "localEnvironmentPolicy", + "nonClaims", + "projectionKind", + "requirements", + "scenarios", + "schemaVersion", + "surfaces", + "witnessSelectors" + ], + "rootKind": "object", + "variantId": "01-resolver", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:750c75c08376a986a18e8739b7476d58af4133c19c40f43d216ee009607be225" + }, + { + "definitionId": "proofkit.requirement-proof-source-set.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "canonicalEnvelope", + "projection", + "sourceSet", + "sources" + ], + "requiredFields": [ + "canonicalEnvelope", + "projection", + "sourceSet", + "sources" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:8003f7feca3b82413cbc2bfdbc3b754508ec3d7e1706df54431b6332d19ec5fd" + }, + { + "definitionId": "proofkit.requirement-proof-source-set.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "contract", + "inputPaths", + "projectionKind", + "selectedSourceIds", + "sourceCount", + "sourceSetCount" + ], + "requiredFields": [ + "contract", + "inputPaths", + "projectionKind", + "selectedSourceIds", + "sourceCount", + "sourceSetCount" + ], + "rootKind": "object", + "variantId": "01-canonical-contract", + "when": [ + "projection.kind=canonical_contract" + ] + }, + { + "allowedFields": [ + "inputPaths", + "projectionKind", + "resolverInput", + "selectedSourceIds", + "sourceCount", + "sourceSetCount" + ], + "requiredFields": [ + "inputPaths", + "projectionKind", + "resolverInput", + "selectedSourceIds", + "sourceCount", + "sourceSetCount" + ], + "rootKind": "object", + "variantId": "02-resolver-input", + "when": [ + "projection.kind=resolver_input" + ] + } + ] + }, + "canonicalDigest": "sha256:57993914adfd60d6cc1a66bfef0b6a4c3c0162fd102f2629d497a28e839f1228" + }, + { + "definitionId": "proofkit.requirement-proof-view.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "authority_state", + "binding_columns", + "bindings", + "contract_id", + "contract_kind", + "non_claims", + "normalization_profile", + "schema_version", + "surface_columns", + "surfaces", + "witness_columns" + ], + "requiredFields": [ + "authority_state", + "binding_columns", + "bindings", + "contract_id", + "contract_kind", + "non_claims", + "normalization_profile", + "schema_version", + "surface_columns", + "surfaces", + "witness_columns" + ], + "rootKind": "object", + "variantId": "01-compact", + "when": [ + "compact proof contract root" + ] + }, + { + "allowedFields": [ + "bindingId", + "bindings", + "nonClaims", + "requirements", + "schemaVersion", + "selection", + "witnessCommands" + ], + "requiredFields": [ + "bindingId", + "bindings", + "nonClaims", + "requirements", + "schemaVersion", + "witnessCommands" + ], + "rootKind": "object", + "variantId": "02-structured", + "when": [ + "structured requirement proof binding root" + ] + } + ] + }, + "canonicalDigest": "sha256:5aed3bcb99337d4937243d61d1f8ed1b1b358108a7710152221bfd5961a3aeb7" + }, + { + "definitionId": "proofkit.requirement-proof-view.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "authority", + "commandCount", + "contractId", + "localEnvironmentPolicy", + "nonClaims", + "preconditionedRequirementCount", + "requirementCount", + "requirements", + "schemaVersion", + "viewKind" + ], + "requiredFields": [ + "authority", + "commandCount", + "contractId", + "localEnvironmentPolicy", + "nonClaims", + "preconditionedRequirementCount", + "requirementCount", + "requirements", + "schemaVersion", + "viewKind" + ], + "rootKind": "object", + "variantId": "01-compact", + "when": [ + "compact proof contract root" + ] + }, + { + "allowedFields": [ + "authority", + "bindingId", + "commandCount", + "nonClaims", + "omittedRequirementCount", + "requirementCount", + "requirements", + "schemaVersion", + "scope", + "viewKind" + ], + "requiredFields": [ + "authority", + "bindingId", + "commandCount", + "nonClaims", + "omittedRequirementCount", + "requirementCount", + "requirements", + "schemaVersion", + "scope", + "viewKind" + ], + "rootKind": "object", + "variantId": "02-structured", + "when": [ + "structured requirement proof binding root" + ] + } + ] + }, + "canonicalDigest": "sha256:d1da2a4593aa2f4a1531e1a5be8e3bc877a5ff45f978b7f39a0bf708b01cb564" + }, + { + "definitionId": "proofkit.requirement-semantic-diff.input.v2.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "baseContext", + "currentContext", + "diffId", + "query", + "schemaVersion" + ], + "requiredFields": [ + "baseContext", + "currentContext", + "diffId", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:1f30133908a40c16f8ed236725e36b1999fc125f1450efa1c28264c42e6c8a63" + }, + { + "definitionId": "proofkit.requirement-semantic-diff.output.v2.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "baseExpectedDigestCoverage", + "baseSnapshotId", + "changeCount", + "changes", + "currentExpectedDigestCoverage", + "currentSnapshotId", + "diffId", + "diffKind", + "nonClaims", + "schemaVersion" + ], + "requiredFields": [ + "baseExpectedDigestCoverage", + "baseSnapshotId", + "changeCount", + "changes", + "currentExpectedDigestCoverage", + "currentSnapshotId", + "diffId", + "diffKind", + "nonClaims", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:abe2eda1ca5496bd33cd659a419a46d7b69a2b8cc50203c7ba6acd31625b760f" + }, + { + "definitionId": "proofkit.requirement-source-admission.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "nonClaims", + "overviewPath", + "requirements", + "requirementsPath", + "schemaVersion", + "sourceId", + "specPackagePath" + ], + "requiredFields": [ + "nonClaims", + "overviewPath", + "requirements", + "requirementsPath", + "schemaVersion", + "sourceId", + "specPackagePath" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:b2b3982ca053794da607673db6258933960a4f67653871205ff03caab2012a9f" + }, + { + "definitionId": "proofkit.requirement-source-admission.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:2527a19394b22efb4b14e39bf3b1f14283f179592c369b9c2ae34b612c7c69af" + }, + { + "definitionId": "proofkit.requirement-source-transition.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "next", + "nonClaims", + "previous", + "schemaVersion", + "transitionId" + ], + "requiredFields": [ + "next", + "nonClaims", + "previous", + "schemaVersion", + "transitionId" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:f6783a812e7d0addd6fa4f077cd43b47df87c7b9fee93cdb3df429ca22cd238f" + }, + { + "definitionId": "proofkit.requirement-source-transition.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:c1dc42e4388b9a43edd7254f8d4fff8076185c3fd05357296d7efb9fef8daf51" + }, + { + "definitionId": "proofkit.requirement-source-view.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "nonClaims", + "overviewPath", + "requirements", + "requirementsPath", + "schemaVersion", + "sourceId", + "specPackagePath" + ], + "requiredFields": [ + "nonClaims", + "overviewPath", + "requirements", + "requirementsPath", + "schemaVersion", + "sourceId", + "specPackagePath" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:79f46f80d0f83e720cab2ccb2bf50675371770521ccb64709d53c79c6db0f1ef" + }, + { + "definitionId": "proofkit.requirement-source-view.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "activeRequirementCount", + "authority", + "blockingRequirementCount", + "deferredRequirementCount", + "nonClaims", + "overviewPath", + "requirementCount", + "requirements", + "requirementsPath", + "schemaVersion", + "sourceId", + "specPackagePath", + "viewKind" + ], + "requiredFields": [ + "activeRequirementCount", + "authority", + "blockingRequirementCount", + "deferredRequirementCount", + "nonClaims", + "overviewPath", + "requirementCount", + "requirements", + "requirementsPath", + "schemaVersion", + "sourceId", + "specPackagePath", + "viewKind" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:30ba0ef3fb0c042ccea37068fb2c17e5d58d0abf749b2f69b4d9e07c4b24d10e" + }, + { + "definitionId": "proofkit.requirement-spec-tree-view.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "callerAnnotations", + "edges", + "nodes", + "overlays", + "rootNodeId", + "schemaVersion", + "treeId" + ], + "requiredFields": [ + "callerAnnotations", + "edges", + "nodes", + "overlays", + "rootNodeId", + "schemaVersion", + "treeId" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:081903f08edd082447a7225834cb12785ebe03d0c262f464f7cd26daf8789c44" + }, + { + "definitionId": "proofkit.requirement-spec-tree-view.output.v2.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "authority", + "callerAnnotationAuthority", + "callerAnnotationCount", + "callerAnnotations", + "edgeCount", + "maxDepth", + "nodeCount", + "nodes", + "nonClaims", + "overlayCount", + "rootNodeId", + "schemaVersion", + "sourceRefCount", + "staleSourceRefCount", + "state", + "treeId", + "viewKind" + ], + "requiredFields": [ + "authority", + "callerAnnotationAuthority", + "callerAnnotationCount", + "callerAnnotations", + "edgeCount", + "maxDepth", + "nodeCount", + "nodes", + "nonClaims", + "overlayCount", + "rootNodeId", + "schemaVersion", + "sourceRefCount", + "staleSourceRefCount", + "state", + "treeId", + "viewKind" + ], + "rootKind": "object", + "variantId": "01-view", + "when": [ + "--format json" + ] + } + ] + }, + "canonicalDigest": "sha256:e788ba6455ad69bdc7ff341888fff5ae74351a1316d19fd708734626df5153c7" + }, + { + "definitionId": "proofkit.requirement-spec-tree.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "callerAnnotations", + "edges", + "nodes", + "overlays", + "rootNodeId", + "schemaVersion", + "treeId" + ], + "requiredFields": [ + "callerAnnotations", + "edges", + "nodes", + "overlays", + "rootNodeId", + "schemaVersion", + "treeId" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:7cfe8b0685164a3c7dcc8432fcfd72f3944cef4057bb7420e81d0167e43815ba" + }, + { + "definitionId": "proofkit.requirement-spec-tree.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:c99be2fb843ccb7b3a91b21ca25852d6e1b1cf565f95a156fe33ce0f1d9e7fdd" + }, + { + "definitionId": "proofkit.requirement-traceability-graph.input.v2.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "codeSources", + "codeTopology", + "context", + "graphId", + "schemaVersion" + ], + "requiredFields": [ + "codeSources", + "context", + "graphId", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:6487ff537380d1cbffe5a72b9688e9ce6baffa218e76f8dbbfe8f50d14509219" + }, + { + "definitionId": "proofkit.requirement-traceability-graph.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "edgeCount", + "edges", + "graphId", + "graphKind", + "nodeCount", + "nodes", + "nonClaims", + "schemaVersion", + "snapshotId" + ], + "requiredFields": [ + "edgeCount", + "edges", + "graphId", + "graphKind", + "nodeCount", + "nodes", + "nonClaims", + "schemaVersion", + "snapshotId" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:37d07abc4d9122176ffab9842b5d85fbfdbe068ee9e4097da8fc21206aae1d47" + }, + { + "definitionId": "proofkit.scaffold-profile-plan.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "commandMatcherHints", + "environmentClasses", + "nonClaims", + "paths", + "planId", + "presetId", + "repository", + "requirements", + "schemaVersion" + ], + "requiredFields": [ + "commandMatcherHints", + "environmentClasses", + "nonClaims", + "paths", + "planId", + "presetId", + "repository", + "requirements", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:48191336813e493288f5b1e24460316b8d3cd41bb334d96485cc6f1fe88e05a6" + }, + { + "definitionId": "proofkit.scaffold-profile-plan.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "callerRequiredGaps", + "nextCommands", + "nonClaims", + "planId", + "planKind", + "presetId", + "profilePath", + "provenance", + "repoProfileDraft", + "schemaVersion" + ], + "requiredFields": [ + "callerRequiredGaps", + "nextCommands", + "nonClaims", + "planId", + "planKind", + "presetId", + "profilePath", + "provenance", + "repoProfileDraft", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-plan", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:377182555fb39dfcf4f0a7011c431c6954aca2560f663adece346da3affc556b" + }, + { + "definitionId": "proofkit.scaffold-project-structure.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "bootstrap", + "nonClaims", + "paths", + "repoProfileScaffold", + "scaffoldId", + "schemaVersion", + "workflow" + ], + "requiredFields": [ + "bootstrap", + "nonClaims", + "paths", + "repoProfileScaffold", + "scaffoldId", + "schemaVersion", + "workflow" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:3f696dff7be322cfca8b24df9feabccf935d418c68839f19f5f844c39ab2a980" + }, + { + "definitionId": "proofkit.scaffold-project-structure.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "requiredFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "rootKind": "object", + "variantId": "01-agent-envelope", + "when": [ + "--agent-envelope" + ] + }, + { + "allowedFields": [ + "adoptionWorkflowInput", + "adoptionWorkflowPlan", + "bootstrapReport", + "exitCode", + "materializationManifest", + "repoProfileScaffoldPlan", + "report" + ], + "requiredFields": [ + "adoptionWorkflowInput", + "adoptionWorkflowPlan", + "bootstrapReport", + "exitCode", + "materializationManifest", + "repoProfileScaffoldPlan", + "report" + ], + "rootKind": "object", + "variantId": "02-scaffold", + "when": [ + "without --agent-envelope" + ] + } + ] + }, + "canonicalDigest": "sha256:903ec3a1c0842f58039df76b6c16f851f3d9ad7353287bd244ad86e211c9b13d" + }, + { + "definitionId": "proofkit.secret-scan.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "files", + "nonClaims", + "reportId", + "schemaVersion", + "suppressions" + ], + "requiredFields": [ + "files", + "nonClaims", + "reportId", + "schemaVersion", + "suppressions" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:6477a651d88632cbdd4cdde0877b95faa1efaeaa16ec8d16a913313517d591ce" + }, + { + "definitionId": "proofkit.secret-scan.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:e4113bfd0c2affe54c8f231ca280a405ba24adfb5e219ad548bd5c175c3d5f54" + }, + { + "definitionId": "proofkit.selective-gate-evidence.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "evidenceClass", + "evidenceId", + "nonClaims", + "plan", + "preexistingFailures", + "producerAdmission", + "receipts", + "schemaVersion" + ], + "requiredFields": [ + "evidenceClass", + "evidenceId", + "nonClaims", + "plan", + "preexistingFailures", + "producerAdmission", + "receipts", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:0615e61b546c079fa662e7a83e5472bdc137eceafd42700dcfa9e78c8c5397b0" + }, + { + "definitionId": "proofkit.selective-gate-evidence.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "requiredFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "rootKind": "object", + "variantId": "01-agent-envelope", + "when": [ + "--agent-envelope" + ] + }, + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "02-report", + "when": [ + "without --agent-envelope" + ] + } + ] + }, + "canonicalDigest": "sha256:8b8537b08340434bcb6c91fc446d8bf7aaa782e8e88763a31aa19720bf49a113" + }, + { + "definitionId": "proofkit.selective-gate-obligation-decision-input.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "evidenceClass", + "evidenceId", + "nonClaims", + "plan", + "preexistingFailures", + "producerAdmission", + "receipts", + "schemaVersion" + ], + "requiredFields": [ + "evidenceClass", + "evidenceId", + "nonClaims", + "plan", + "preexistingFailures", + "producerAdmission", + "receipts", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:f488e84a8a588593d41cdbea8991fbe549239580292db573d215c8a4725ad2ee" + }, + { + "definitionId": "proofkit.selective-gate-obligation-decision-input.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "decisionId", + "nonClaims", + "obligations", + "schemaVersion" + ], + "requiredFields": [ + "decisionId", + "nonClaims", + "obligations", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-projection", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:f4c5b8430047a0a9c19d6c180bdf8b20b1980519dbdbf18e05870ebe5ea51942" + }, + { + "definitionId": "proofkit.selective-gate-plan.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "archiveOrBinaryPathPatterns", + "artifactIntegrityPolicies", + "baseCommands", + "changedPaths", + "dependencyFreshness", + "fallbackCoverage", + "fullWorkspaceCommand", + "generatedArtifactRules", + "ignoredProofLikePaths", + "nonClaims", + "packageCommands", + "pathTriggeredCommands", + "preexistingFailures", + "privatePathPrefixes", + "proofLikePathPatterns", + "publicApi", + "requirementImpact", + "scanObligation", + "schemaVersion", + "touchedRequirementWitnesses", + "unknownEdges" + ], + "requiredFields": [ + "archiveOrBinaryPathPatterns", + "artifactIntegrityPolicies", + "baseCommands", + "changedPaths", + "dependencyFreshness", + "fallbackCoverage", + "fullWorkspaceCommand", + "generatedArtifactRules", + "ignoredProofLikePaths", + "nonClaims", + "packageCommands", + "pathTriggeredCommands", + "preexistingFailures", + "privatePathPrefixes", + "proofLikePathPatterns", + "publicApi", + "requirementImpact", + "schemaVersion", + "touchedRequirementWitnesses", + "unknownEdges" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:b2f202d51f98d0e43191b015591f88b493916254d7d7fcc5942bd7c5fff2e6f8" + }, + { + "definitionId": "proofkit.selective-gate-plan.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "requiredFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "rootKind": "object", + "variantId": "01-agent-envelope", + "when": [ + "--agent-envelope" + ] + }, + { + "allowedFields": [ + "artifactIntegrity", + "changedPaths", + "failures", + "fallbackCoverage", + "generatedArtifacts", + "nonClaims", + "planState", + "privatePathExclusions", + "proofLikePaths", + "publicApiContractTouched", + "requiredCommands", + "scanObligation", + "schemaVersion", + "skippedGates", + "touchedRequirementWitnesses", + "unknownEdges" + ], + "requiredFields": [ + "artifactIntegrity", + "changedPaths", + "failures", + "fallbackCoverage", + "generatedArtifacts", + "nonClaims", + "planState", + "privatePathExclusions", + "proofLikePaths", + "publicApiContractTouched", + "requiredCommands", + "schemaVersion", + "skippedGates", + "touchedRequirementWitnesses", + "unknownEdges" + ], + "rootKind": "object", + "variantId": "02-plan", + "when": [ + "without --agent-envelope" + ] + } + ] + }, + "canonicalDigest": "sha256:45fd178ec9319a1f2394b45508cdd7164d5a836c06064ebc3f8ea8f4ba1349af" + }, + { + "definitionId": "proofkit.self-check.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "json_value", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [], + "requiredFields": [], + "rootKind": "json_value", + "variantId": "01-json-value", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:3c842174dff5361e7f83166469b832805e05aa314b073c16234b5b64e346281e" + }, + { + "definitionId": "proofkit.self-check.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:0ea95e277ebe44cd2de42c29b47c38686ac0b6b390d8965367437b3fe138e209" + }, + { + "definitionId": "proofkit.spec-overview-claims.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "boundaryId", + "claims", + "extractionRefs", + "nonClaims", + "overviewPath", + "requirementIds", + "requirementsPath", + "schemaVersion", + "sourceId", + "specPackagePath" + ], + "requiredFields": [ + "boundaryId", + "claims", + "extractionRefs", + "nonClaims", + "overviewPath", + "requirementIds", + "requirementsPath", + "schemaVersion", + "sourceId", + "specPackagePath" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:c753b344fa6f5b4ab3f65f2e4e99cfc5adbc24b31030b3175889e2d2d34eee43" + }, + { + "definitionId": "proofkit.spec-overview-claims.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:7faf2c5a579206f187e480e6b4c12b8da0a3578a36037af602962e16a3465353" + }, + { + "definitionId": "proofkit.spec-proof-bundle-admission.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "bundleId", + "mergeRequiredReceiptIds", + "nonClaims", + "receiptAdmission", + "receiptProducerAdmission", + "requirementBindings", + "schemaVersion", + "witnessPlan" + ], + "requiredFields": [ + "bundleId", + "mergeRequiredReceiptIds", + "nonClaims", + "requirementBindings", + "schemaVersion", + "witnessPlan" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:1aa48d72b9159b42fdf51faed4efaf406901c6de7ba4180041cb439b54a8fa85" + }, + { + "definitionId": "proofkit.spec-proof-bundle-admission.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:96885d5ea11cf23867de200f29a0fe8f188e723c7bd469935086d90f42e05b33" + }, + { + "definitionId": "proofkit.stack-preset.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:338f720a24bd36f4ab34cad9b14d3bd26389119e5c713146f1c4eb019f4a0637" + }, + { + "definitionId": "proofkit.test-evidence-inventory.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "authority", + "entries", + "inventoryId", + "nonClaims", + "ownerId", + "schemaVersion", + "sourceId" + ], + "requiredFields": [ + "authority", + "entries", + "inventoryId", + "nonClaims", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-direct-inventory", + "when": [ + "without --projection; direct inventory" + ] + }, + { + "allowedFields": [ + "authority", + "discoveredTests", + "draftId", + "nonClaims", + "repository", + "runner", + "schemaVersion" + ], + "requiredFields": [ + "authority", + "discoveredTests", + "draftId", + "repository", + "runner", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "02-discovery-draft", + "when": [ + "--projection discovery-draft" + ] + }, + { + "allowedFields": [ + "commandRefPolicy", + "compactProofContract", + "inventoryId", + "nonClaims", + "requirementSource", + "schemaVersion" + ], + "requiredFields": [ + "commandRefPolicy", + "compactProofContract", + "inventoryId", + "requirementSource", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "03-proof-binding-derived", + "when": [ + "--projection proof-binding-derived" + ] + }, + { + "allowedFields": [ + "authority", + "inventoryId", + "nonClaims", + "schemaVersion", + "sourceColumns", + "sourceTexts", + "sources" + ], + "requiredFields": [ + "authority", + "inventoryId", + "nonClaims", + "schemaVersion", + "sourceColumns", + "sourceTexts", + "sources" + ], + "rootKind": "object", + "variantId": "04-source-set", + "when": [ + "without --projection; source-set inventory" + ] + }, + { + "allowedFields": [ + "inventory", + "schema" + ], + "requiredFields": [ + "inventory", + "schema" + ], + "rootKind": "object", + "variantId": "05-wrapped-inventory", + "when": [ + "without --projection; wrapped inventory" + ] + } + ] + }, + "canonicalDigest": "sha256:4006bfcaab9bd8cb781595aa14f48768cbee996f5e866757fa73be1c9938fa98" + }, + { + "definitionId": "proofkit.test-evidence-inventory.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "entrySources", + "inputPaths", + "inventory", + "nonClaims", + "normalizedInventoryId", + "normalizedKind", + "schemaVersion", + "sourceAuthority", + "sourceColumns", + "sourceCount", + "sources" + ], + "requiredFields": [ + "entrySources", + "inputPaths", + "inventory", + "nonClaims", + "normalizedInventoryId", + "normalizedKind", + "schemaVersion", + "sourceAuthority", + "sourceColumns", + "sourceCount", + "sources" + ], + "rootKind": "object", + "variantId": "01-normalized-direct", + "when": [ + "--normalized-inventory without --projection" + ] + }, + { + "allowedFields": [ + "entrySources", + "inputPaths", + "inventory", + "nonClaims", + "normalizedInventoryId", + "normalizedKind", + "projectionKind", + "projectionSummary", + "schemaVersion", + "sourceAuthority", + "sourceColumns", + "sourceCount", + "sources" + ], + "requiredFields": [ + "entrySources", + "inputPaths", + "inventory", + "nonClaims", + "normalizedInventoryId", + "normalizedKind", + "projectionKind", + "projectionSummary", + "schemaVersion", + "sourceAuthority", + "sourceColumns", + "sourceCount", + "sources" + ], + "rootKind": "object", + "variantId": "02-normalized-proof-binding", + "when": [ + "--normalized-inventory --projection proof-binding-derived" + ] + }, + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "03-report", + "when": [ + "--normalized-inventory failure report", + "--projection discovery-draft without --normalized-inventory", + "--projection proof-binding-derived without --normalized-inventory", + "without --normalized-inventory" + ] + } + ] + }, + "canonicalDigest": "sha256:aa2813ab8406c0dba4a9ac4fc215cb607e2c2003e9b0921ac1f6a68faaacff0d" + }, + { + "definitionId": "proofkit.text-policy.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "files", + "nonClaims", + "policy", + "reportId", + "schemaVersion" + ], + "requiredFields": [ + "files", + "nonClaims", + "policy", + "reportId", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:af1e6196ddee9ee4262ca14c9cca1451c89241ebeef177af4a12be866b088f9a" + }, + { + "definitionId": "proofkit.text-policy.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:a47795a1a72dcece9e3e513c8d33829fed187a959450d1ce007697a434b26df1" + }, + { + "definitionId": "proofkit.typescript-public-api-surfaces.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "entries", + "machineContract", + "schemaVersion" + ], + "requiredFields": [ + "entries", + "machineContract", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:687efe1fe441443ad7bcab69f05987b47a25672da8032e7471bd7c48ebd95a30" + }, + { + "definitionId": "proofkit.typescript-public-api-surfaces.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "entryCount", + "failures", + "inputAuthority", + "nonClaims" + ], + "requiredFields": [ + "entryCount", + "failures", + "inputAuthority", + "nonClaims" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:93da5f4d5efd4c482cb5da37bb60a80f8e47e4eab549428790ddd1a4e680009d" + }, + { + "definitionId": "proofkit.witness-plan.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "commands", + "schemaVersion", + "vocabulary" + ], + "requiredFields": [ + "commands", + "vocabulary" + ], + "rootKind": "object", + "variantId": "01-direct", + "when": [ + "without projection" + ] + }, + { + "allowedFields": [ + "projection", + "requirementProofBinding", + "schemaVersion", + "vocabulary" + ], + "requiredFields": [ + "projection", + "requirementProofBinding", + "schemaVersion", + "vocabulary" + ], + "rootKind": "object", + "variantId": "02-requirement-bindings-projection", + "when": [ + "projection=requirement-bindings" + ] + } + ] + }, + "canonicalDigest": "sha256:6670d1db21e0cc51e8811224b98e9160c6b8c52e841b18ba67e3e6426004831c" + }, + { + "definitionId": "proofkit.witness-plan.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "commands", + "parallelGroups" + ], + "requiredFields": [ + "commands", + "parallelGroups" + ], + "rootKind": "object", + "variantId": "01-plan", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:77a320105ae18dd507d1d44c51e3fe26ebe7efc8decdd436f9311c0daae57d57" + }, + { + "definitionId": "proofkit.witness-scheduler-plan.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "commands", + "nonClaims", + "policies", + "schedulerPlanId", + "schemaVersion", + "vocabulary" + ], + "requiredFields": [ + "commands", + "nonClaims", + "policies", + "schedulerPlanId", + "schemaVersion", + "vocabulary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:56f26197b5ee72e825a9c01b76c9e3683aac89241a808df657ee7d556f1806bf" + }, + { + "definitionId": "proofkit.witness-scheduler-plan.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:520c73c13a4f47e396da9a6f618a67f95ced3f18804137f11f597a4d98b81830" + }, + { + "definitionId": "proofkit.workspace-changed-package-plan.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "changedPaths", + "escalationRules", + "includeReverseDependents", + "packages", + "packagesRoot", + "schemaVersion" + ], + "requiredFields": [ + "changedPaths", + "escalationRules", + "packages", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-direct", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:34d8ad1aab208d578000ef39b9eaf7503dcbd2b97645a1444d6bb8a0f63db482" + }, + { + "definitionId": "proofkit.workspace-changed-package-plan.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "requiredFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "rootKind": "object", + "variantId": "01-agent-envelope", + "when": [ + "--agent-envelope" + ] + }, + { + "allowedFields": [ + "changedPaths", + "directRootPackageNames", + "directRoots", + "escalationReasons", + "fullWorkspace", + "rootPackageNames", + "roots", + "schemaVersion" + ], + "requiredFields": [ + "changedPaths", + "directRootPackageNames", + "directRoots", + "escalationReasons", + "fullWorkspace", + "rootPackageNames", + "roots", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "02-plan", + "when": [ + "without --agent-envelope" + ] + } + ] + }, + "canonicalDigest": "sha256:e6df6d1141707abfd3122d0e538418ee3755ab248f1e4d061f957bfbb7ee26b7" + }, + { + "definitionId": "proofkit.workspace-manifest-facts.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "dependencyFields", + "nonClaims", + "packages", + "projectionId", + "root", + "schemaVersion" + ], + "requiredFields": [ + "dependencyFields", + "nonClaims", + "packages", + "projectionId", + "root", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:af1c88e54f82c48f1fad068efeb74fdc8f74e2b8ff618081ea496f1bfe609408" + }, + { + "definitionId": "proofkit.workspace-manifest-facts.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "changedPackagePlanPackages", + "diagnostics", + "knownPackageNames", + "manifestSources", + "nonClaims", + "packageUniverse", + "packages", + "projectionId", + "reportId", + "reportKind", + "root", + "schemaVersion", + "shardPartitionPackages", + "state", + "summary" + ], + "requiredFields": [ + "changedPackagePlanPackages", + "diagnostics", + "knownPackageNames", + "manifestSources", + "nonClaims", + "packageUniverse", + "packages", + "projectionId", + "reportId", + "reportKind", + "root", + "schemaVersion", + "shardPartitionPackages", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:ec6ea756481bd02822e396e90849a9a2757c9f831f72340fef491f8fccbd53b6" + }, + { + "definitionId": "proofkit.workspace-registry.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "dependencyPolicy", + "knownPackageNames", + "lockfilePolicy", + "nonClaims", + "packages", + "root", + "schemaVersion", + "scriptPolicy" + ], + "requiredFields": [ + "dependencyPolicy", + "knownPackageNames", + "lockfilePolicy", + "packages", + "root", + "schemaVersion", + "scriptPolicy" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:55d09e1672bc094c0ef564c578c719d8f4a193636747768593d784c8addce631" + }, + { + "definitionId": "proofkit.workspace-registry.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "requiredFields": [ + "diagnostics", + "nonClaims", + "reportId", + "reportKind", + "ruleResults", + "schemaVersion", + "state", + "summary" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:36644320c351b3f278dcadc38bd27bb492f5c042fe7a2cc9a0b0756655d4e097" + }, + { + "definitionId": "proofkit.workspace-shard-partition.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "packages", + "roots", + "schemaVersion", + "shardTotal" + ], + "requiredFields": [ + "packages", + "roots", + "schemaVersion", + "shardTotal" + ], + "rootKind": "object", + "variantId": "01-direct", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:265fbbf98e23fb32cb3d9f585b7c31ed181ca5a185416dd9088fa21d30ec8eec" + }, + { + "definitionId": "proofkit.workspace-shard-partition.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "requiredFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "rootKind": "object", + "variantId": "01-agent-envelope", + "when": [ + "--agent-envelope" + ] + }, + { + "allowedFields": [ + "failures", + "packageShards", + "rootPackageCount", + "rootPackageNames", + "schemaVersion", + "shardTotal", + "shards" + ], + "requiredFields": [ + "failures", + "packageShards", + "rootPackageCount", + "rootPackageNames", + "schemaVersion", + "shardTotal", + "shards" + ], + "rootKind": "object", + "variantId": "02-partition", + "when": [ + "without --agent-envelope" + ] + } + ] + }, + "canonicalDigest": "sha256:2715c816e36923fb506233f241b5b6172ec6d0640ec27a2738f20594cae9e960" } ] } diff --git a/proofkit/receipt-producer-policy.json b/proofkit/receipt-producer-policy.json index 7868dfa..a939f8d 100644 --- a/proofkit/receipt-producer-policy.json +++ b/proofkit/receipt-producer-policy.json @@ -34,7 +34,7 @@ "local-go-python" ], "evidenceRefs": [ - "AGENTS.md" + "docs/specs/proofkit-package-boundary/requirements.v1.json" ], "nonClaim": "Local developer receipts are advisory and do not satisfy merge obligations." } diff --git a/proofkit/requirement-bindings.json b/proofkit/requirement-bindings.json index b798b95..0c4e3f4 100644 --- a/proofkit/requirement-bindings.json +++ b/proofkit/requirement-bindings.json @@ -9,7 +9,7 @@ "claimLevel": "blocking", "proofState": "witness_backed", "nonClaims": [ - "This requirement does not claim consumer adoption, registry publication, rollout approval, or production readiness." + "This requirement does not claim a complete Markdown parser, discovery of unclassified code-span paths, consumer adoption, registry publication, rollout approval, or production readiness." ] }, { @@ -19,7 +19,7 @@ "claimLevel": "blocking", "proofState": "witness_backed", "nonClaims": [ - "This requirement does not claim native witness execution, producer authentication, proof freshness, or merge approval." + "This requirement does not claim nested CLI field shape, scalar type, collection cardinality, nullability, a complete Markdown parser, native witness execution, installation of source-checkout witness files, runtime JSON Schema validation, producer authentication, proof freshness, or merge approval." ] }, { @@ -159,7 +159,7 @@ "claimLevel": "blocking", "proofState": "witness_backed", "nonClaims": [ - "This requirement does not claim repository scanning, source freshness computation from files, requirement meaning, native witness execution, committed generated HTML, merge approval, release approval, rollout approval, or production readiness." + "This requirement does not claim repository scanning, source freshness computation from files, requirement meaning, native witness execution, protection from adversarial concurrent content or namespace mutation by the same operating-system user during the operation, filesystem crash or fsync durability, committed generated HTML, merge approval, release approval, rollout approval, or production readiness." ] }, { @@ -409,7 +409,7 @@ "claimLevel": "blocking", "proofState": "witness_backed", "nonClaims": [ - "This requirement does not claim exhaustive command semantic completeness beyond the declared route inventory or byte-for-byte stability for diagnostics outside the public CLI ABI." + "This requirement does not claim nested field shape, scalar type, collection cardinality, nullability, exhaustive command semantic completeness beyond the declared route inventory, installation of source-checkout witness files, runtime JSON Schema validation, or byte-for-byte stability for diagnostics outside the public CLI ABI." ] }, { @@ -586,7 +586,7 @@ "specPath": "docs/specs/proofkit-supply-chain-quality/requirements.v1.json", "claimLevel": "blocking", "proofState": "witness_backed", - "nonClaims": ["This requirement does not claim full WCAG conformance, every branded browser, provider CI ingestion, registry publication, release approval, rollout approval, or production readiness."] + "nonClaims": ["This requirement does not claim full WCAG 2.2 conformance, branded Safari behavior, complete screen-reader interoperability, every operating-system theme, 400-percent zoom behavior, continuous screenshot evidence, guaranteed capture of the best-effort failure screenshot, provider CI ingestion, registry publication, release approval, rollout approval, or production readiness."] }, { "requirementId": "REQ-PROOFKIT-QUALITY-023", @@ -604,6 +604,14 @@ "proofState": "witness_backed", "nonClaims": ["This requirement does not infer change-record completeness from source changes, make release notes approval authority, or prove provider publication, attestation authenticity, consumer adoption, rollout approval, or production readiness."] }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-025", + "ownerId": "proofkit.supply-chain-quality", + "specPath": "docs/specs/proofkit-supply-chain-quality/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": ["This requirement does not claim external action safety, tag equivalence, provider execution, branch-protection configuration, release publication, or provider-side asset immutability."] + }, { "requirementId": "REQ-PROOFKIT-SPEC-017", "ownerId": "proofkit.spec-proof-core", @@ -630,7 +638,7 @@ "specPath": "docs/specs/proofkit-spec-proof-core/requirements.v1.json", "claimLevel": "blocking", "proofState": "witness_backed", - "nonClaims": ["This requirement does not claim ambient repository discovery, later-checkout freshness, requirement meaning, proof adequacy, native witness execution, merge approval, release approval, rollout approval, or production readiness."] + "nonClaims": ["Expected-digest coverage records only equality between caller-supplied expected digests and admitted current bytes; it does not authenticate a producer, baseline, checkout, or freshness and does not claim ambient repository discovery, requirement meaning, proof adequacy, native witness execution, merge approval, release approval, rollout approval, or production readiness."] }, { "requirementId": "REQ-PROOFKIT-SPEC-020", @@ -646,7 +654,7 @@ "specPath": "docs/specs/proofkit-spec-proof-core/requirements.v1.json", "claimLevel": "blocking", "proofState": "witness_backed", - "nonClaims": ["This requirement does not authenticate the surrounding browser profile or operating-system user and does not claim remote access, annotation persistence, agent execution, provider delivery, proof freshness, merge approval, release approval, rollout approval, branded Safari behavior, or production readiness."] + "nonClaims": ["Expected-digest coverage does not authenticate a producer, baseline, checkout, or freshness; successful launcher invocation does not prove browser rendering; this requirement also does not authenticate the surrounding browser profile or operating-system user and does not claim remote access, annotation persistence, agent execution, provider delivery, complete screen-reader interoperability, full WCAG 2.2 conformance, every operating-system theme, 400-percent zoom behavior, merge approval, release approval, rollout approval, branded Safari behavior, or production readiness."] }, { "requirementId": "REQ-PROOFKIT-SPEC-022", @@ -654,7 +662,7 @@ "specPath": "docs/specs/proofkit-spec-proof-core/requirements.v1.json", "claimLevel": "blocking", "proofState": "witness_backed", - "nonClaims": ["This requirement does not claim textual equivalence, inferred moves, product meaning, proof freshness, merge approval, release approval, rollout approval, or production readiness."] + "nonClaims": ["Expected-digest coverage does not authenticate a producer, baseline, checkout, or freshness; this requirement also does not claim textual equivalence, inferred moves, product meaning, merge approval, release approval, rollout approval, or production readiness."] }, { "requirementId": "REQ-PROOFKIT-SPEC-023", @@ -662,7 +670,7 @@ "specPath": "docs/specs/proofkit-spec-proof-core/requirements.v1.json", "claimLevel": "blocking", "proofState": "witness_backed", - "nonClaims": ["This requirement does not infer code topology, line or branch coverage, native execution, proof freshness, merge approval, release approval, rollout approval, or production readiness."] + "nonClaims": ["Expected-digest coverage does not authenticate a producer, baseline, checkout, or freshness; this requirement also does not infer code topology, line or branch coverage, native execution, merge approval, release approval, rollout approval, or production readiness."] } ], "bindings": [ @@ -671,7 +679,13 @@ "scenarioId": "proofkit.package-boundary.root-export-and-deep-import-denial", "witnessId": "proofkit.package-artifact.boundary", "witnessKind": "contract", - "witnessPath": "internal/tools/packageverify/main.go", + "witnessPath": "internal/tools/packageverify/main_test.go", + "witnessSelectors": [ + { + "selector": "TestVerifyRootPackageRejectsEachForbiddenRootEntry", + "command": "go test ./internal/tools/packageverify -run '^TestVerifyRootPackageRejectsEachForbiddenRootEntry$'" + } + ], "commandIds": [ "proofkit.go-test", "proofkit.package-artifact" @@ -687,6 +701,52 @@ "witnessId": "proofkit.packageverify.package-public-docs-no-mutable-release-facts", "witnessKind": "contract", "witnessPath": "internal/tools/packageverify/main_test.go", + "witnessSelectors": [ + { + "selector": "TestVerifyNoStalePackageDocsRejectsMutableReleaseFactsInMarkdown", + "command": "go test ./internal/tools/packageverify -run '^TestVerifyNoStalePackageDocsRejectsMutableReleaseFactsInMarkdown$'" + } + ], + "commandIds": [ + "proofkit.go-test", + "proofkit.package-artifact" + ], + "environmentClasses": [ + "local-go", + "local-go-python" + ] + }, + { + "requirementId": "REQ-PROOFKIT-PACKAGE-001", + "scenarioId": "proofkit.package-boundary.contract-map-table-shape", + "witnessId": "proofkit.contract-map.table-shape-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/app/cli_contract_test.go", + "witnessSelectors": [ + { + "selector": "TestContractMapDecisionTreeHasThreeCells", + "command": "go test ./internal/app -run '^TestContractMapDecisionTreeHasThreeCells$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, + { + "requirementId": "REQ-PROOFKIT-PACKAGE-001", + "scenarioId": "proofkit.package-boundary.package-public-reference-closure", + "witnessId": "proofkit.package-artifact.reference-closure-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/tools/packageverify/main_test.go", + "witnessSelectors": [ + { + "selector": "TestPackagePublicReferenceClosure", + "command": "go test ./internal/tools/packageverify -run '^TestPackagePublicReferenceClosure$'" + } + ], "commandIds": [ "proofkit.go-test", "proofkit.package-artifact" @@ -709,12 +769,50 @@ "local-go" ] }, + { + "requirementId": "REQ-PROOFKIT-PACKAGE-002", + "scenarioId": "proofkit.package-boundary.cli-machine-contract-closure", + "witnessId": "proofkit.cli-contract.complete-generated-closure", + "witnessKind": "contract", + "witnessPath": "internal/tools/commandcontractgen/main_test.go", + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, { "requirementId": "REQ-PROOFKIT-PACKAGE-002", "scenarioId": "proofkit.package-boundary.readiness-closeout-overclaim-grammar", "witnessId": "proofkit.readiness-closeout.overclaim-grammar", "witnessKind": "contract", "witnessPath": "internal/command/readinesscloseout/readinesscloseout_test.go", + "witnessSelectors": [ + { + "selector": "TestPhraseScanDecodesOneStrictCharacterReference", + "command": "go test ./internal/command/readinesscloseout -run '^TestPhraseScanDecodesOneStrictCharacterReference$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, + { + "requirementId": "REQ-PROOFKIT-PACKAGE-002", + "scenarioId": "proofkit.package-boundary.requirement-browser-fixed-launcher", + "witnessId": "proofkit.requirement-browser.fixed-launcher-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/command/requirementbrowser/server_test.go", + "witnessSelectors": [ + { + "selector": "TestOpenBrowserUsesFixedLauncherAndLoopbackURL", + "command": "go test ./internal/command/requirementbrowser -run '^TestOpenBrowserUsesFixedLauncherAndLoopbackURL$'" + } + ], "commandIds": [ "proofkit.go-test" ], @@ -780,6 +878,24 @@ "witnessId": "proofkit.typescript-public-api.explicit-scan-topology", "witnessKind": "contract", "witnessPath": "internal/command/publicapi/public_api_test.go", + "witnessSelectors": [ + { + "selector": "TestCanonicalSourceSnapshotRejectsChangedCrossAliasAdmission", + "command": "go test ./internal/command/publicapi -run '^TestCanonicalSourceSnapshotRejectsChangedCrossAliasAdmission$'" + }, + { + "selector": "TestScanCacheBindsBytesToFirstCanonicalIdentityAcrossSymlinkRetarget", + "command": "go test ./internal/command/publicapi -run '^TestScanCacheBindsBytesToFirstCanonicalIdentityAcrossSymlinkRetarget$'" + }, + { + "selector": "TestVerifyPinsPackageRootAcrossInRootSiblingSwap", + "command": "go test ./internal/command/publicapi -run '^TestVerifyPinsPackageRootAcrossInRootSiblingSwap$'" + }, + { + "selector": "TestVerifyRejectsDeterministicSymlinkSwap", + "command": "go test ./internal/command/publicapi -run '^TestVerifyRejectsDeterministicSymlinkSwap$'" + } + ], "commandIds": [ "proofkit.go-test" ], @@ -800,16 +916,112 @@ "local-go" ] }, + { + "requirementId": "REQ-PROOFKIT-PACKAGE-002", + "scenarioId": "proofkit.package-boundary.launcher-profile-admission", + "witnessId": "proofkit.cliexec.launcher-profile-admission-matrix", + "witnessKind": "contract", + "witnessPath": "internal/kernel/cliexec/cliexec_test.go", + "witnessSelectors": [ + { + "selector": "TestLauncherProfileAdmissionMatrix", + "command": "go test ./internal/kernel/cliexec -run '^TestLauncherProfileAdmissionMatrix$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, + { + "requirementId": "REQ-PROOFKIT-PACKAGE-002", + "scenarioId": "proofkit.package-boundary.generated-command-field-inventory", + "witnessId": "proofkit.generated-command.field-inventory", + "witnessKind": "contract", + "witnessPath": "internal/app/invocation_profile_test.go", + "witnessSelectors": [ + { + "selector": "TestGeneratedCommandInvocationProfileFieldInventory", + "command": "go test ./internal/app -run '^TestGeneratedCommandInvocationProfileFieldInventory$'" + }, + { + "selector": "TestGeneratedCommandInvocationProfileRouteClosure", + "command": "go test ./internal/app -run '^TestGeneratedCommandInvocationProfileRouteClosure$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, + { + "requirementId": "REQ-PROOFKIT-PACKAGE-002", + "scenarioId": "proofkit.package-boundary.generated-command-caller-preservation", + "witnessId": "proofkit.generated-command.caller-preservation", + "witnessKind": "contract", + "witnessPath": "internal/command/gradualadoption/gradualadoption_test.go", + "witnessSelectors": [ + { + "selector": "TestBootstrapPreservesCallerDisplayCommandInGuidancePayload", + "command": "go test ./internal/command/gradualadoption -run '^TestBootstrapPreservesCallerDisplayCommandInGuidancePayload$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, + { + "requirementId": "REQ-PROOFKIT-PACKAGE-002", + "scenarioId": "proofkit.package-boundary.cli-output-root-witnesses", + "witnessId": "proofkit.cli-output-root.exact-witnesses", + "witnessKind": "contract", + "witnessPath": "internal/app/cli_abi_test.go", + "witnessSelectors": [ + { + "selector": "TestAdoptionContractEnvelopeCLIABI", + "command": "go test ./internal/app -run '^TestAdoptionContractEnvelopeCLIABI$'" + }, + { + "selector": "TestSelfCheckOutputUsesExactRootShape", + "command": "go test ./internal/app -run '^TestSelfCheckOutputUsesExactRootShape$'" + }, + { + "selector": "TestStandaloneMultiVariantCommandsUseExactRootShapes", + "command": "go test ./internal/app -run '^TestStandaloneMultiVariantCommandsUseExactRootShapes$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, { "requirementId": "REQ-PROOFKIT-PACKAGE-003", "scenarioId": "proofkit.package-boundary.outside-consumer-artifact", "witnessId": "proofkit.package-artifact.outside-consumer", "witnessKind": "contract", - "witnessPath": "internal/tools/packageverify/main.go", + "witnessPath": "internal/tools/packageverify/main_test.go", + "witnessSelectors": [ + { + "selector": "TestExactTarballOnboardingTrace", + "command": "go test ./internal/tools/packageverify -run '^TestExactTarballOnboardingTrace$'" + } + ], "commandIds": [ + "proofkit.go-test", "proofkit.package-artifact" ], "environmentClasses": [ + "local-go", "local-go-python" ] }, @@ -818,15 +1030,42 @@ "scenarioId": "proofkit.package-boundary.ci-receipt-anchor", "witnessId": "proofkit.ci.receipt-anchor", "witnessKind": "contract", - "witnessPath": ".github/workflows/ci.yml", + "witnessPath": "scripts/validate-self-hosting-receipts_test.go", + "witnessSelectors": [ + { + "selector": "TestReceiptIDKeepsLocalAndCIIdentitiesDistinct", + "command": "go test ./scripts -run '^TestReceiptIDKeepsLocalAndCIIdentitiesDistinct$'" + } + ], "commandIds": [ "proofkit.ci-receipt-anchor", + "proofkit.go-test", "proofkit.package-gate" ], "environmentClasses": [ + "local-go", "local-go-python" ] }, + { + "requirementId": "REQ-PROOFKIT-PACKAGE-004", + "scenarioId": "proofkit.package-boundary.self-hosting-report-verdict", + "witnessId": "proofkit.self-hosting.report-verdict-falsifier", + "witnessKind": "contract", + "witnessPath": "scripts/validate-self-hosting-receipts_test.go", + "witnessSelectors": [ + { + "selector": "TestRunProofkitVerdictCases", + "command": "go test ./scripts -run '^TestRunProofkitVerdictCases$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, { "requirementId": "REQ-PROOFKIT-PACKAGE-005", "scenarioId": "proofkit.package-boundary.go-format-static-vet-and-vulnerability-clean", @@ -849,6 +1088,20 @@ "witnessId": "proofkit.source-hygiene.tracked-language-closure", "witnessKind": "contract", "witnessPath": "scripts/source_hygiene_test.go", + "witnessSelectors": [ + { + "selector": "TestSourceHygieneIgnoresTokenSubstringsInsideContentDigests", + "command": "go test ./scripts -run '^TestSourceHygieneIgnoresTokenSubstringsInsideContentDigests$'" + }, + { + "selector": "TestSourceHygieneReadsStagedBlob", + "command": "go test ./scripts -run '^TestSourceHygieneReadsStagedBlob$'" + }, + { + "selector": "TestSourceHygieneReadsTrackedWorktree", + "command": "go test ./scripts -run '^TestSourceHygieneReadsTrackedWorktree$'" + } + ], "commandIds": [ "proofkit.go-test" ], @@ -861,7 +1114,13 @@ "scenarioId": "proofkit.package-boundary.merge-critical-runtime-preconditions", "witnessId": "proofkit.ci.merge-critical-runtime-preconditions", "witnessKind": "contract", - "witnessPath": "scripts/workflow_package_gate_oracle_test.go", + "witnessPath": "scripts/workflow_runtime_preconditions_test.go", + "witnessSelectors": [ + { + "selector": "TestCISourceQualityInstallsPythonBeforeLifecycleTests", + "command": "go test ./scripts -run '^TestCISourceQualityInstallsPythonBeforeLifecycleTests$'" + } + ], "commandIds": [ "proofkit.go-test" ], @@ -874,12 +1133,42 @@ "scenarioId": "proofkit.package-boundary.python-wheel-candidate", "witnessId": "proofkit.python-package.boundary", "witnessKind": "contract", - "witnessPath": "internal/tools/pythonpackage", + "witnessPath": "scripts/validate-self-hosting-receipts_test.go", + "witnessSelectors": [ + { + "selector": "TestPythonArtifactRefsRejectEachWheelIdentityDefect", + "command": "go test ./scripts -run '^TestPythonArtifactRefsRejectEachWheelIdentityDefect$'" + } + ], "commandIds": [ + "proofkit.go-test", "proofkit.python-package", "proofkit.python-verify" ], "environmentClasses": [ + "local-go", + "local-python" + ] + }, + { + "requirementId": "REQ-PROOFKIT-PACKAGE-006", + "scenarioId": "proofkit.package-boundary.python-platform-doc-projection", + "witnessId": "proofkit.python-package.platform-doc-projection-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/tools/pythonpackage/metadata_test.go", + "witnessSelectors": [ + { + "selector": "TestREADMEPlatformAndPythonProjection", + "command": "go test ./internal/tools/pythonpackage -run '^TestREADMEPlatformAndPythonProjection$'" + } + ], + "commandIds": [ + "proofkit.go-test", + "proofkit.python-package", + "proofkit.python-verify" + ], + "environmentClasses": [ + "local-go", "local-python" ] }, @@ -896,6 +1185,32 @@ "local-go" ] }, + { + "requirementId": "REQ-PROOFKIT-PACKAGE-006", + "scenarioId": "proofkit.package-boundary.python-wheel-generated-continuation", + "witnessId": "proofkit.python-package.generated-continuation", + "witnessKind": "contract", + "witnessPath": "internal/tools/pythonpackage/continuation_test.go", + "witnessSelectors": [ + { + "selector": "TestExactDisplayedRouteOperandsRejectsWhitespaceAndExpansionMutants", + "command": "go test ./internal/tools/pythonpackage -run '^TestExactDisplayedRouteOperandsRejectsWhitespaceAndExpansionMutants$'" + }, + { + "selector": "TestInstalledWheelContinuationUsesExactPythonModuleProfileWithoutNPM", + "command": "go test ./internal/tools/pythonpackage -run '^TestInstalledWheelContinuationUsesExactPythonModuleProfileWithoutNPM$'" + } + ], + "commandIds": [ + "proofkit.go-test", + "proofkit.python-package", + "proofkit.python-verify" + ], + "environmentClasses": [ + "local-go", + "local-python" + ] + }, { "requirementId": "REQ-PROOFKIT-SPEC-001", "scenarioId": "proofkit.spec-proof-core.requirement-source-admission", @@ -909,6 +1224,43 @@ "local-go" ] }, + { + "requirementId": "REQ-PROOFKIT-SPEC-001", + "scenarioId": "proofkit.spec-proof-core.installed-readme-first-input", + "witnessId": "proofkit.packageverify.installed-readme-input-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/tools/packageverify/main_test.go", + "witnessSelectors": [ + { + "selector": "TestExactTarballOnboardingTrace", + "command": "go test ./internal/tools/packageverify -run '^TestExactTarballOnboardingTrace$'" + }, + { + "selector": "TestInstalledREADMEFirstInputRejectsAmbiguousFences", + "command": "go test ./internal/tools/packageverify -run '^TestInstalledREADMEFirstInputRejectsAmbiguousFences$'" + }, + { + "selector": "TestInstalledREADMEFirstInputUsesBoundedLiteralShellWords", + "command": "go test ./internal/tools/packageverify -run '^TestInstalledREADMEFirstInputUsesBoundedLiteralShellWords$'" + }, + { + "selector": "TestInstalledREADMEFirstInputPreservesJSONExampleBytes", + "command": "go test ./internal/tools/packageverify -run '^TestInstalledREADMEFirstInputPreservesJSONExampleBytes$'" + }, + { + "selector": "TestLiteralShellWordsConsumesLongBackslashRun", + "command": "go test ./internal/tools/packageverify -run '^TestLiteralShellWordsConsumesLongBackslashRun$'" + } + ], + "commandIds": [ + "proofkit.go-test", + "proofkit.package-artifact" + ], + "environmentClasses": [ + "local-go", + "local-go-python" + ] + }, { "requirementId": "REQ-PROOFKIT-SPEC-002", "scenarioId": "proofkit.spec-proof-core.requirement-proof-binding-reporting", @@ -1175,6 +1527,12 @@ "witnessId": "proofkit.requirement-spec-tree-view.cli-output-path-falsifier", "witnessKind": "contract", "witnessPath": "internal/app/cli_abi_test.go", + "witnessSelectors": [ + { + "selector": "TestOutputWriterRejectsDeterministicParentSwap", + "command": "go test ./internal/app -run '^TestOutputWriterRejectsDeterministicParentSwap$'" + } + ], "commandIds": [ "proofkit.go-test" ], @@ -1195,6 +1553,25 @@ "local-go" ] }, + { + "requirementId": "REQ-PROOFKIT-SPEC-009", + "scenarioId": "proofkit.spec-proof-core.requirement-browser-view-vocabulary", + "witnessId": "proofkit.requirement-browser.invalid-view-vocabulary-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/command/requirementbrowser/requirementbrowser_test.go", + "witnessSelectors": [ + { + "selector": "TestInvalidViewListsEverySupportedView", + "command": "go test ./internal/command/requirementbrowser -run '^TestInvalidViewListsEverySupportedView$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, { "requirementId": "REQ-PROOFKIT-SPEC-010", "scenarioId": "proofkit.spec-proof-core.requirement-impact-input-compose", @@ -1227,6 +1604,12 @@ "witnessId": "proofkit.adoption-contract-envelope.public-cli-abi", "witnessKind": "contract", "witnessPath": "internal/app/cli_abi_test.go", + "witnessSelectors": [ + { + "selector": "TestAdoptionContractEnvelopeCLIABI", + "command": "go test ./internal/app -run '^TestAdoptionContractEnvelopeCLIABI$'" + } + ], "commandIds": [ "proofkit.go-test" ], @@ -1565,6 +1948,20 @@ "witnessId": "proofkit.adoption-doctor.enforcement-and-envelope", "witnessKind": "contract", "witnessPath": "internal/command/adoptiondoctor/adoptiondoctor_test.go", + "witnessSelectors": [ + { + "selector": "TestBuildReportsObserveAndWarnWithoutBlockingAdvisoryGaps", + "command": "go test ./internal/command/adoptiondoctor -run '^TestBuildReportsObserveAndWarnWithoutBlockingAdvisoryGaps$'" + }, + { + "selector": "TestBuildEnforceTouchedSkipsGapsOutsideTouchedSelection", + "command": "go test ./internal/command/adoptiondoctor -run '^TestBuildEnforceTouchedSkipsGapsOutsideTouchedSelection$'" + }, + { + "selector": "TestBuildBlocksEveryModeForExternalPreconditions", + "command": "go test ./internal/command/adoptiondoctor -run '^TestBuildBlocksEveryModeForExternalPreconditions$'" + } + ], "commandIds": [ "proofkit.go-test" ], @@ -1631,6 +2028,28 @@ "witnessId": "proofkit.cli-abi.golden-corpus", "witnessKind": "contract", "witnessPath": "internal/app/cli_abi_test.go", + "witnessSelectors": [ + { + "selector": "TestAdoptionContractEnvelopeCLIABI", + "command": "go test ./internal/app -run '^TestAdoptionContractEnvelopeCLIABI$'" + }, + { + "selector": "TestRequirementBrowserOneShotCLIOutputVariants", + "command": "go test ./internal/app -run '^TestRequirementBrowserOneShotCLIOutputVariants$'" + }, + { + "selector": "TestRequiredInputCommandsRouteStructuralErrorsByMode", + "command": "go test ./internal/app -run '^TestRequiredInputCommandsRouteStructuralErrorsByMode$'" + }, + { + "selector": "TestSelfCheckOutputUsesExactRootShape", + "command": "go test ./internal/app -run '^TestSelfCheckOutputUsesExactRootShape$'" + }, + { + "selector": "TestStandaloneMultiVariantCommandsUseExactRootShapes", + "command": "go test ./internal/app -run '^TestStandaloneMultiVariantCommandsUseExactRootShapes$'" + } + ], "commandIds": [ "proofkit.go-test" ], @@ -1644,7 +2063,64 @@ "witnessId": "proofkit.cli-contract.descriptor-parity", "witnessKind": "contract", "witnessPath": "internal/app/cli_contract_test.go", + "witnessSelectors": [ + { + "selector": "TestCLIConditionModelClosesAdoptionOutputRoutes", + "command": "go test ./internal/app -run '^TestCLIConditionModelClosesAdoptionOutputRoutes$'" + }, + { + "selector": "TestCommandDescriptorContractParityRejectsMutations", + "command": "go test ./internal/app -run '^TestCommandDescriptorContractParityRejectsMutations$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-004", + "scenarioId": "proofkit.supply-chain-quality.cli-output-witness-contract", + "witnessId": "proofkit.cli-output-root.binding-closure", + "witnessKind": "contract", + "witnessPath": "internal/app/cli_output_witness_contract_test.go", + "witnessSelectors": [ + { + "selector": "TestRootDistinctOutputWitnessBindingsAreExact", + "command": "go test ./internal/app -run '^TestRootDistinctOutputWitnessBindingsAreExact$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-004", + "scenarioId": "proofkit.supply-chain-quality.cli-contract-generation", + "witnessId": "proofkit.cli-contract.generated-projections", + "witnessKind": "contract", + "witnessPath": "internal/tools/commandcontractgen/main_test.go", + "witnessSelectors": [ + { + "selector": "TestCLIFlagConditionModelRejectsAmbiguity", + "command": "go test ./internal/tools/commandcontractgen -run '^TestCLIFlagConditionModelRejectsAmbiguity$'" + }, + { + "selector": "TestRenderRejectsIncompleteAndStaleCommandContracts", + "command": "go test ./internal/tools/commandcontractgen -run '^TestRenderRejectsIncompleteAndStaleCommandContracts$'" + }, + { + "selector": "TestRunGeneratesAndChecksBothProjections", + "command": "go test ./internal/tools/commandcontractgen -run '^TestRunGeneratesAndChecksBothProjections$'" + } + ], "commandIds": [ + "proofkit.command-contract-check", "proofkit.go-test" ], "environmentClasses": [ @@ -1664,6 +2140,29 @@ "local-go" ] }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-004", + "scenarioId": "proofkit.supply-chain-quality.legacy-digest-vocabulary-confinement", + "witnessId": "proofkit.cli-contract.legacy-digest-vocabulary-confinement-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/app/requirement_context_cli_test.go", + "witnessSelectors": [ + { + "selector": "TestConditionSensitiveProofCommandsUseExactRootVariants", + "command": "go test ./internal/app -run '^TestConditionSensitiveProofCommandsUseExactRootVariants$'" + }, + { + "selector": "TestLegacyDigestVocabularyConfinedToV1AdaptersAndFixtures", + "command": "go test ./internal/app -run '^TestLegacyDigestVocabularyConfinedToV1AdaptersAndFixtures$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, { "requirementId": "REQ-PROOFKIT-QUALITY-005", "scenarioId": "proofkit.supply-chain-quality.codeql-workflow", @@ -1677,14 +2176,81 @@ "local-go" ] }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-005", + "scenarioId": "proofkit.supply-chain-quality.codeql-permission-separation", + "witnessId": "proofkit.codeql.permission-separation-falsifier", + "witnessKind": "contract", + "witnessPath": "scripts/workflow_security_scanner_oracles_test.go", + "witnessSelectors": [ + { + "selector": "TestSecurityScannerWorkflowsSeparateProviderPublicationPermissions", + "command": "go test ./scripts -run '^TestSecurityScannerWorkflowsSeparateProviderPublicationPermissions$'" + } + ], + "commandIds": [ + "proofkit.actionlint", + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, { "requirementId": "REQ-PROOFKIT-QUALITY-010", "scenarioId": "proofkit.supply-chain-quality.coverage-metrics", "witnessId": "proofkit.coverage-metrics.linkage-report", "witnessKind": "contract", - "witnessPath": "internal/tools/coveragemetrics/main.go", + "witnessPath": "internal/tools/coveragemetrics/main_test.go", + "witnessSelectors": [ + { + "selector": "TestEachCommandRouteClosureConjunctHasIndependentFalsifier", + "command": "go test ./internal/tools/coveragemetrics -run '^TestEachCommandRouteClosureConjunctHasIndependentFalsifier$'" + }, + { + "selector": "TestEachLinkageDeadZoneConjunctHasIndependentFalsifier", + "command": "go test ./internal/tools/coveragemetrics -run '^TestEachLinkageDeadZoneConjunctHasIndependentFalsifier$'" + } + ], "commandIds": [ - "proofkit.coverage-metrics" + "proofkit.coverage-metrics", + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-010", + "scenarioId": "proofkit.supply-chain-quality.binding-selector-executability", + "witnessId": "proofkit.coverage-metrics.binding-selector-executability-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/tools/coveragemetrics/main_test.go", + "witnessSelectors": [ + { + "selector": "TestBindingWitnessSelectorsAcceptUnnamedGoTestParameter", + "command": "go test ./internal/tools/coveragemetrics -run '^TestBindingWitnessSelectorsAcceptUnnamedGoTestParameter$'" + }, + { + "selector": "TestBindingWitnessSelectorsRejectInvalidGoTestSignature", + "command": "go test ./internal/tools/coveragemetrics -run '^TestBindingWitnessSelectorsRejectInvalidGoTestSignature$'" + }, + { + "selector": "TestBindingWitnessSelectorsRejectMissingSemanticOwner", + "command": "go test ./internal/tools/coveragemetrics -run '^TestBindingWitnessSelectorsRejectMissingSemanticOwner$'" + }, + { + "selector": "TestBindingWitnessSelectorsRejectNonTestAndBuildExcludedFiles", + "command": "go test ./internal/tools/coveragemetrics -run '^TestBindingWitnessSelectorsRejectNonTestAndBuildExcludedFiles$'" + }, + { + "selector": "TestBindingWitnessSelectorsRequireExactCriticalInventories", + "command": "go test ./internal/tools/coveragemetrics -run '^TestBindingWitnessSelectorsRequireExactCriticalInventories$'" + } + ], + "commandIds": [ + "proofkit.coverage-metrics", + "proofkit.go-test" ], "environmentClasses": [ "local-go" @@ -1750,12 +2316,113 @@ "local-go" ] }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-011", + "scenarioId": "proofkit.supply-chain-quality.ci-required-aggregate-exactness", + "witnessId": "proofkit.ci.required-aggregate-neutralization-falsifier", + "witnessKind": "contract", + "witnessPath": "scripts/workflow_package_gate_oracle_test.go", + "witnessSelectors": [ + { + "selector": "TestCIWorkflowDeclaresFailClosedRequiredAggregate", + "command": "go test ./scripts -run '^TestCIWorkflowDeclaresFailClosedRequiredAggregate$'" + }, + { + "selector": "TestCIRequiredAggregateRejectsNeutralizedScript", + "command": "go test ./scripts -run '^TestCIRequiredAggregateRejectsNeutralizedScript$'" + }, + { + "selector": "TestCIRequiredAggregateRejectsExecutionOverrides", + "command": "go test ./scripts -run '^TestCIRequiredAggregateRejectsExecutionOverrides$'" + }, + { + "selector": "TestCIRequiredAggregateRejectsPlatformSmokeSubstitution", + "command": "go test ./scripts -run '^TestCIRequiredAggregateRejectsPlatformSmokeSubstitution$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, { "requirementId": "REQ-PROOFKIT-QUALITY-013", "scenarioId": "proofkit.supply-chain-quality.workflow-package-gate-oracle", "witnessId": "proofkit.workflow-package-gate.typed-oracle", "witnessKind": "contract", "witnessPath": "scripts/workflow_package_gate_oracle_test.go", + "witnessSelectors": [ + { + "selector": "TestCIWorkflowDeclaresFailClosedRequiredAggregate", + "command": "go test ./scripts -run '^TestCIWorkflowDeclaresFailClosedRequiredAggregate$'" + }, + { + "selector": "TestNeedsListNormalizesStringAndList", + "command": "go test ./scripts -run '^TestNeedsListNormalizesStringAndList$'" + }, + { + "selector": "TestPackageGateWorkflowOracleAcceptsOwnerCIAndReleaseWorkflows", + "command": "go test ./scripts -run '^TestPackageGateWorkflowOracleAcceptsOwnerCIAndReleaseWorkflows$'" + }, + { + "selector": "TestPackageGateWorkflowOracleAdmitsAlwaysWithNeedSuccess", + "command": "go test ./scripts -run '^TestPackageGateWorkflowOracleAdmitsAlwaysWithNeedSuccess$'" + }, + { + "selector": "TestPackageGateWorkflowOracleAdmitsLaterAlwaysWithSuccess", + "command": "go test ./scripts -run '^TestPackageGateWorkflowOracleAdmitsLaterAlwaysWithSuccess$'" + }, + { + "selector": "TestPackageGateWorkflowOracleAdmitsPrivateAttestationBypass", + "command": "go test ./scripts -run '^TestPackageGateWorkflowOracleAdmitsPrivateAttestationBypass$'" + }, + { + "selector": "TestPackageGateWorkflowOracleRejectsAlwaysWithoutNeedSuccess", + "command": "go test ./scripts -run '^TestPackageGateWorkflowOracleRejectsAlwaysWithoutNeedSuccess$'" + }, + { + "selector": "TestPackageGateWorkflowOracleRejectsDisabledAndShadowedEvidence", + "command": "go test ./scripts -run '^TestPackageGateWorkflowOracleRejectsDisabledAndShadowedEvidence$'" + }, + { + "selector": "TestPackageGateWorkflowOracleRejectsDuplicatePriorStepName", + "command": "go test ./scripts -run '^TestPackageGateWorkflowOracleRejectsDuplicatePriorStepName$'" + }, + { + "selector": "TestPackageGateWorkflowOracleRejectsExecutionOverrides", + "command": "go test ./scripts -run '^TestPackageGateWorkflowOracleRejectsExecutionOverrides$'" + }, + { + "selector": "TestPackageGateWorkflowOracleRejectsLateRequiredPriorStep", + "command": "go test ./scripts -run '^TestPackageGateWorkflowOracleRejectsLateRequiredPriorStep$'" + }, + { + "selector": "TestPackageGateWorkflowOracleRejectsMissingWorkflowPermissionFloor", + "command": "go test ./scripts -run '^TestPackageGateWorkflowOracleRejectsMissingWorkflowPermissionFloor$'" + }, + { + "selector": "TestPackageGateWorkflowOracleRejectsNeedSuccessBypass", + "command": "go test ./scripts -run '^TestPackageGateWorkflowOracleRejectsNeedSuccessBypass$'" + }, + { + "selector": "TestPackageGateWorkflowOracleRejectsRequiredPriorExecutionOverride", + "command": "go test ./scripts -run '^TestPackageGateWorkflowOracleRejectsRequiredPriorExecutionOverride$'" + }, + { + "selector": "TestPackageGateWorkflowOracleRejectsUnusedAllowedStepEnvironment", + "command": "go test ./scripts -run '^TestPackageGateWorkflowOracleRejectsUnusedAllowedStepEnvironment$'" + }, + { + "selector": "TestPackageGateWorkflowOracleRejectsWrongPriorStepCommand", + "command": "go test ./scripts -run '^TestPackageGateWorkflowOracleRejectsWrongPriorStepCommand$'" + }, + { + "selector": "TestWorkflowGuardExpressionsRejectNeutralization", + "command": "go test ./scripts -run '^TestWorkflowGuardExpressionsRejectNeutralization$'" + } + ], "commandIds": [ "proofkit.go-test" ], @@ -1896,6 +2563,20 @@ "witnessId": "proofkit.pythonpackage.release-platform-parity", "witnessKind": "contract", "witnessPath": "internal/tools/pythonpackage/metadata_test.go", + "witnessSelectors": [ + { + "selector": "TestREADMEPlatformAndPythonProjection", + "command": "go test ./internal/tools/pythonpackage -run '^TestREADMEPlatformAndPythonProjection$'" + }, + { + "selector": "TestReleaseTargetsProjectExactPythonWheelMetadata", + "command": "go test ./internal/tools/pythonpackage -run '^TestReleaseTargetsProjectExactPythonWheelMetadata$'" + }, + { + "selector": "TestVerifyWheelContentsRequiresExactWheelMetadata", + "command": "go test ./internal/tools/pythonpackage -run '^TestVerifyWheelContentsRequiresExactWheelMetadata$'" + } + ], "commandIds": [ "proofkit.go-test", "proofkit.python-package", @@ -2060,6 +2741,32 @@ "witnessId": "proofkit.packageverify.installed-json-abi-smoke", "witnessKind": "contract", "witnessPath": "internal/tools/packageverify/main_test.go", + "witnessSelectors": [ + { + "selector": "TestExactTarballOnboardingTrace", + "command": "go test ./internal/tools/packageverify -run '^TestExactTarballOnboardingTrace$'" + }, + { + "selector": "TestInstalledInvocationRequiresAuthoredOrderAndExactCommandToken", + "command": "go test ./internal/tools/packageverify -run '^TestInstalledInvocationRequiresAuthoredOrderAndExactCommandToken$'" + }, + { + "selector": "TestOnboardingTraceCoversEveryDiscoveredPresetAndREADMEInput", + "command": "go test ./internal/tools/packageverify -run '^TestOnboardingTraceCoversEveryDiscoveredPresetAndREADMEInput$'" + }, + { + "selector": "TestInstalledREADMEFirstInputUsesBoundedLiteralShellWords", + "command": "go test ./internal/tools/packageverify -run '^TestInstalledREADMEFirstInputUsesBoundedLiteralShellWords$'" + }, + { + "selector": "TestInstalledREADMEFirstInputPreservesJSONExampleBytes", + "command": "go test ./internal/tools/packageverify -run '^TestInstalledREADMEFirstInputPreservesJSONExampleBytes$'" + }, + { + "selector": "TestLiteralShellWordsConsumesLongBackslashRun", + "command": "go test ./internal/tools/packageverify -run '^TestLiteralShellWordsConsumesLongBackslashRun$'" + } + ], "commandIds": [ "proofkit.go-test", "proofkit.package-artifact" @@ -2123,6 +2830,26 @@ "local-go" ] }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-006", + "scenarioId": "proofkit.supply-chain-quality.osv-permission-separation", + "witnessId": "proofkit.osv.permission-separation-falsifier", + "witnessKind": "contract", + "witnessPath": "scripts/workflow_security_scanner_oracles_test.go", + "witnessSelectors": [ + { + "selector": "TestSecurityScannerWorkflowsSeparateProviderPublicationPermissions", + "command": "go test ./scripts -run '^TestSecurityScannerWorkflowsSeparateProviderPublicationPermissions$'" + } + ], + "commandIds": [ + "proofkit.actionlint", + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, { "requirementId": "REQ-PROOFKIT-QUALITY-001", "scenarioId": "proofkit.supply-chain-quality.release-attestation-wiring", @@ -2141,8 +2868,23 @@ "scenarioId": "proofkit.supply-chain-quality.release-sbom", "witnessId": "proofkit.release-sbom.deterministic-inventory", "witnessKind": "contract", - "witnessPath": "internal/tools/releasesbom/main.go", + "witnessPath": "internal/tools/releasesbom/main_test.go", + "witnessSelectors": [ + { + "selector": "TestArtifactSpecificRuntimeEdgesAndExcludedInventory", + "command": "go test ./internal/tools/releasesbom -run '^TestArtifactSpecificRuntimeEdgesAndExcludedInventory$'" + }, + { + "selector": "TestReleaseFileEvidenceRejectsDeterministicIdentitySwap", + "command": "go test ./internal/tools/releasesbom -run '^TestReleaseFileEvidenceRejectsDeterministicIdentitySwap$'" + }, + { + "selector": "TestReleaseFileEvidenceRejectsDeterministicInPlaceMutation", + "command": "go test ./internal/tools/releasesbom -run '^TestReleaseFileEvidenceRejectsDeterministicInPlaceMutation$'" + } + ], "commandIds": [ + "proofkit.go-test", "proofkit.release-sbom" ], "environmentClasses": [ @@ -2162,6 +2904,30 @@ "local-go" ] }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-007", + "scenarioId": "proofkit.supply-chain-quality.scorecard-permission-and-publication-inputs", + "witnessId": "proofkit.scorecard.permission-and-publication-inputs-falsifier", + "witnessKind": "contract", + "witnessPath": "scripts/workflow_security_scanner_oracles_test.go", + "witnessSelectors": [ + { + "selector": "TestScorecardPublicPublishDeclaresRequiredOutputInputs", + "command": "go test ./scripts -run '^TestScorecardPublicPublishDeclaresRequiredOutputInputs$'" + }, + { + "selector": "TestSecurityScannerWorkflowsSeparateProviderPublicationPermissions", + "command": "go test ./scripts -run '^TestSecurityScannerWorkflowsSeparateProviderPublicationPermissions$'" + } + ], + "commandIds": [ + "proofkit.actionlint", + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, { "requirementId": "REQ-PROOFKIT-SPEC-017", "scenarioId": "proofkit.spec-proof-core.capability-map-admission-trust-modes", @@ -2181,6 +2947,16 @@ "witnessId": "proofkit.command-family-navigation.help-compatibility-falsifier", "witnessKind": "contract", "witnessPath": "internal/app/command_family_catalog_test.go", + "witnessSelectors": [ + { + "selector": "TestRootHelpDiscoversFamiliesWithoutExpandingThem", + "command": "go test ./internal/app -run '^TestRootHelpDiscoversFamiliesWithoutExpandingThem$'" + }, + { + "selector": "TestStackPresetVocabularyProjectsFromOneOwner", + "command": "go test ./internal/app -run '^TestStackPresetVocabularyProjectsFromOneOwner$'" + } + ], "commandIds": [ "proofkit.go-test" ], @@ -2252,9 +3028,39 @@ "witnessId": "proofkit.requirement-browser.proof-artifact-confinement-falsifier", "witnessKind": "contract", "witnessPath": "internal/tools/browserproofverify/artifact_paths_test.go", + "witnessSelectors": [ + { + "selector": "TestFinalizeBrowserProofRunCleansSuccessAndRetainsFailureDiagnostics", + "command": "go test ./internal/tools/browserproofverify -run '^TestFinalizeBrowserProofRunCleansSuccessAndRetainsFailureDiagnostics$'" + } + ], "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-022", + "scenarioId": "proofkit.supply-chain-quality.browser-failure-diagnostics-retention", + "witnessId": "proofkit.requirement-browser.failure-diagnostics-retention-falsifier", + "witnessKind": "contract", + "witnessPath": "scripts/workflow_browser_runtime_oracle_test.go", + "witnessSelectors": [ + { + "selector": "TestCIBrowserRuntimeRetainsFailureDiagnosticsWithoutPublishingProof", + "command": "go test ./scripts -run '^TestCIBrowserRuntimeRetainsFailureDiagnosticsWithoutPublishingProof$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-022", + "scenarioId": "proofkit.supply-chain-quality.browser-trace-observer-effect", + "witnessId": "proofkit.requirement-browser.trace-observer-effect-falsifier", + "witnessKind": "technical", + "witnessPath": "scripts/browser-proof-inputs.test.mjs", + "commandIds": ["proofkit.browser-check"], + "environmentClasses": ["local-node-browser"] + }, { "requirementId": "REQ-PROOFKIT-QUALITY-022", "scenarioId": "proofkit.supply-chain-quality.browser-terminal-concurrency", @@ -2279,6 +3085,24 @@ "witnessId": "proofkit.python-package.macho-platform-falsifier", "witnessKind": "contract", "witnessPath": "internal/tools/pythonpackage/metadata_test.go", + "witnessSelectors": [ + { + "selector": "TestMachOMinimumMacOSAcceptsLegacyVersionCommand", + "command": "go test ./internal/tools/pythonpackage -run '^TestMachOMinimumMacOSAcceptsLegacyVersionCommand$'" + }, + { + "selector": "TestMachOMinimumMacOSRejectsTruncatedBuildVersion", + "command": "go test ./internal/tools/pythonpackage -run '^TestMachOMinimumMacOSRejectsTruncatedBuildVersion$'" + }, + { + "selector": "TestVerifyWheelContentsAcceptsDarwinTagAtOrAboveMachOMinimum", + "command": "go test ./internal/tools/pythonpackage -run '^TestVerifyWheelContentsAcceptsDarwinTagAtOrAboveMachOMinimum$'" + }, + { + "selector": "TestVerifyWheelContentsRejectsDarwinTagBelowMachOMinimum", + "command": "go test ./internal/tools/pythonpackage -run '^TestVerifyWheelContentsRejectsDarwinTagBelowMachOMinimum$'" + } + ], "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] }, @@ -2297,6 +3121,20 @@ "witnessId": "proofkit.release-change.versioned-projection-falsifier", "witnessKind": "contract", "witnessPath": "internal/tools/releasechange/record_test.go", + "witnessSelectors": [ + { + "selector": "TestAdmitEnforcesVersionedChangeClass", + "command": "go test ./internal/tools/releasechange -run '^TestAdmitEnforcesVersionedChangeClass$'" + }, + { + "selector": "TestRenderStatesPreOneExactPinPolicy", + "command": "go test ./internal/tools/releasechange -run '^TestRenderStatesPreOneExactPinPolicy$'" + }, + { + "selector": "TestCurrentChangeRecordNamesReviewedSemanticChanges", + "command": "go test ./internal/tools/releasechange -run '^TestCurrentChangeRecordNamesReviewedSemanticChanges$'" + } + ], "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] }, @@ -2318,6 +3156,29 @@ "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-025", + "scenarioId": "proofkit.supply-chain-quality.workflow-source-oracles", + "witnessId": "proofkit.workflow.exact-source-oracle-falsifiers", + "witnessKind": "contract", + "witnessPath": "scripts/workflow_source_oracles_test.go", + "witnessSelectors": [ + { + "selector": "TestExistingReleasePathIsReadOnlyAndFailsOnDrift", + "command": "go test ./scripts -run '^TestExistingReleasePathIsReadOnlyAndFailsOnDrift$'" + }, + { + "selector": "TestWorkflowClosedKeyAdmission", + "command": "go test ./scripts -run '^TestWorkflowClosedKeyAdmission$'" + }, + { + "selector": "TestWorkflowExternalActionsUseFullCommitSHAs", + "command": "go test ./scripts -run '^TestWorkflowExternalActionsUseFullCommitSHAs$'" + } + ], + "commandIds": ["proofkit.actionlint", "proofkit.go-test"], + "environmentClasses": ["local-go"] + }, { "requirementId": "REQ-PROOFKIT-SPEC-019", "scenarioId": "proofkit.spec-proof-core.requirement-context-composition", @@ -2336,6 +3197,21 @@ "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] }, + { + "requirementId": "REQ-PROOFKIT-SPEC-019", + "scenarioId": "proofkit.spec-proof-core.requirement-context-digest-coverage-migration", + "witnessId": "proofkit.requirement-context.digest-coverage-adapter-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/command/requirementcontext/requirementcontext_test.go", + "witnessSelectors": [ + { + "selector": "TestV1DigestCoverageAdapters", + "command": "go test ./internal/command/requirementcontext -run '^TestV1DigestCoverageAdapters$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, { "requirementId": "REQ-PROOFKIT-SPEC-020", "scenarioId": "proofkit.spec-proof-core.requirement-context-slicing", @@ -2387,6 +3263,35 @@ "witnessId": "proofkit.requirement-browser.one-shot-cleanup-falsifier", "witnessKind": "contract", "witnessPath": "internal/command/requirementbrowser/server_test.go", + "witnessSelectors": [ + { + "selector": "TestServeOneShotDoesNotReadCompletedDoneTwice", + "command": "go test ./internal/command/requirementbrowser -run '^TestServeOneShotDoesNotReadCompletedDoneTwice$'" + }, + { + "selector": "TestServeOneShotReturnsCleanupFailuresWithoutWritingTerminalPacket", + "command": "go test ./internal/command/requirementbrowser -run '^TestServeOneShotReturnsCleanupFailuresWithoutWritingTerminalPacket$'" + }, + { + "selector": "TestServeOneShotWaitsForDoneBeforeWritingTerminalPacket", + "command": "go test ./internal/command/requirementbrowser -run '^TestServeOneShotWaitsForDoneBeforeWritingTerminalPacket$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-SPEC-021", + "scenarioId": "proofkit.spec-proof-core.requirement-browser-digest-coverage-projections", + "witnessId": "proofkit.requirement-browser.digest-coverage-projection-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/command/requirementbrowser/workspace_test.go", + "witnessSelectors": [ + { + "selector": "TestV2DigestCoverageProjections", + "command": "go test ./internal/command/requirementbrowser -run '^TestV2DigestCoverageProjections$'" + } + ], "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] }, @@ -2417,6 +3322,21 @@ "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] }, + { + "requirementId": "REQ-PROOFKIT-SPEC-022", + "scenarioId": "proofkit.spec-proof-core.requirement-semantic-diff-digest-coverage-migration", + "witnessId": "proofkit.requirement-semantic-diff.digest-coverage-adapter-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/command/requirementdiff/requirementdiff_test.go", + "witnessSelectors": [ + { + "selector": "TestDigestCoverageAdaptersPreserveSemanticDiffV2", + "command": "go test ./internal/command/requirementdiff -run '^TestDigestCoverageAdaptersPreserveSemanticDiffV2$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, { "requirementId": "REQ-PROOFKIT-SPEC-023", "scenarioId": "proofkit.spec-proof-core.requirement-traceability-evidence-planes", @@ -2444,6 +3364,21 @@ "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] }, + { + "requirementId": "REQ-PROOFKIT-SPEC-023", + "scenarioId": "proofkit.spec-proof-core.requirement-traceability-context-migration", + "witnessId": "proofkit.requirement-traceability.context-migration-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/command/requirementgraph/requirementgraph_test.go", + "witnessSelectors": [ + { + "selector": "TestBuildConsumesNormalizedV1AndV2ContextSnapshots", + "command": "go test ./internal/command/requirementgraph -run '^TestBuildConsumesNormalizedV1AndV2ContextSnapshots$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, { "requirementId": "REQ-PROOFKIT-SPEC-021", "scenarioId": "proofkit.spec-proof-core.requirement-browser-rendered-runtime", @@ -2478,6 +3413,11 @@ "command": "npm run browser:check", "environmentClass": "local-node-browser" }, + { + "commandId": "proofkit.command-contract-check", + "command": "npm run command-contract:check", + "environmentClass": "local-go" + }, { "commandId": "proofkit.ci-receipt-anchor", "command": "npm run self:receipt", diff --git a/proofkit/witness-plan.json b/proofkit/witness-plan.json index 7efdbc4..a18a44a 100644 --- a/proofkit/witness-plan.json +++ b/proofkit/witness-plan.json @@ -171,6 +171,35 @@ ] } }, + { + "schemaVersion": 1, + "id": "proofkit.command-contract-check", + "cwd": ".", + "argv": [ + "npm", + "run", + "command-contract:check" + ], + "environment": { + "inherit": "none", + "allowlist": [], + "classes": [ + "local-go" + ] + }, + "timeoutMs": 600000, + "networkPolicy": "none", + "credentialClass": "none", + "cachePolicy": "disabled", + "expectedArtifacts": [], + "parallelGroup": "local-go-static", + "exitCodePolicy": { + "kind": "zero", + "successCodes": [ + 0 + ] + } + }, { "schemaVersion": 1, "id": "proofkit.go-fmt", @@ -738,6 +767,7 @@ "internal/kernel/admit", "internal/kernel/agentenvelope", "internal/kernel/browserdoc", + "internal/kernel/cliexec", "internal/kernel/compactproofcontract", "internal/kernel/digest", "internal/kernel/markdownfmt", @@ -788,8 +818,8 @@ "timeoutMs": 600000 }, "nonClaims": [ - "Browser runtime proof does not prove registry publication, rollout, or production readiness.", - "Pinned Playwright engines are not branded browser compatibility proof." + "Browser runtime proof does not establish full WCAG 2.2 conformance, complete screen-reader interoperability, every operating-system theme, 400-percent zoom behavior, registry publication, rollout, or production readiness.", + "Pinned Chromium, Firefox, and Playwright WebKit engines do not prove branded Safari compatibility." ] }, { @@ -843,6 +873,40 @@ "Self-hosting witness policy does not prove producer authentication or merge approval." ] }, + { + "commandId": "proofkit.command-contract-check", + "inputSelectors": [ + "internal/app/command_contract_generated.go", + "internal/command/stackpreset/preset_ids_generated.go", + "internal/tools/commandcontractgen", + "proofkit/cli-contract.v2.json" + ], + "outputSelectors": [], + "resourceReads": [ + "resource.proofkit.source" + ], + "resourceWrites": [], + "exclusiveLocks": [], + "sideEffectClass": "none", + "deterministicOutput": true, + "cacheAdmissionRefs": [], + "retryPolicy": { + "kind": "none", + "maxAttempts": 1 + }, + "cancellationPolicy": { + "kind": "cooperative", + "graceMs": 5000 + }, + "timeoutPolicy": { + "kind": "bounded", + "timeoutMs": 600000 + }, + "nonClaims": [ + "Generated projection freshness does not execute native command semantics or validate package publication.", + "Self-hosting witness policy does not prove producer authentication or merge approval." + ] + }, { "commandId": "proofkit.coverage-metrics", "inputSelectors": [ diff --git a/python/agentic_proofkit/cli.py b/python/agentic_proofkit/cli.py index e60ccea..c2715ba 100644 --- a/python/agentic_proofkit/cli.py +++ b/python/agentic_proofkit/cli.py @@ -20,10 +20,14 @@ def main(argv: Sequence[str] | None = None) -> int: return 127 ensure_executable(binary) command = [str(binary), *argv] + python_executable = str(Path(sys.executable).absolute()) + environment = os.environ.copy() + environment["AGENTIC_PROOFKIT_LAUNCHER_PROFILE"] = "python_module" + environment["AGENTIC_PROOFKIT_PYTHON_EXECUTABLE"] = python_executable if os.name != "nt": - os.execv(str(binary), command) - raise AssertionError("os.execv returned unexpectedly") - return subprocess.run(command, check=False).returncode + os.execve(str(binary), command, environment) + raise AssertionError("os.execve returned unexpectedly") + return subprocess.run(command, check=False, env=environment).returncode def ensure_executable(path: Path) -> None: diff --git a/release/change-record.v1.json b/release/change-record.v1.json deleted file mode 100644 index 4385dd8..0000000 --- a/release/change-record.v1.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "schemaVersion": 1, - "version": "0.1.160", - "breakingChanges": [ - { - "changeId": "proofkit.python-wheel.macos-minimum", - "summary": "Darwin distributions now declare macOS 12.0 as their minimum supported operating-system version, matching the embedded executable." - }, - { - "changeId": "proofkit.requirement-graph.topology-id-removal", - "summary": "Requirement traceability graph inputs advance to schemaVersion 2 and reject the semantically inert codeTopology.topologyId field." - } - ], - "additions": [ - { - "changeId": "proofkit.python-wheel.license-closure", - "summary": "Python wheels carry Core Metadata 2.4 license fields and an exact copy of the repository MIT license." - }, - { - "changeId": "proofkit.release.change-record", - "summary": "Release notes are generated from this version-bound machine-readable public-contract change record." - }, - { - "changeId": "proofkit.release.retained-evidence-topology", - "summary": "Retained evidence checksums use executable artifact-relative paths owned by one builder and verifier." - }, - { - "changeId": "proofkit.requirement-graph.topology-closure", - "summary": "Traceability graph build and output admission enforce source-digest coherence, bounded materialization, and parent-to-edge topology closure." - } - ], - "migration": { - "required": true, - "steps": [ - "Set requirement-traceability-graph input schemaVersion to 2 and remove codeTopology.topologyId.", - "Run Darwin package binaries only on macOS 12.0 or later." - ] - }, - "platformRequirements": [ - "Published Darwin package binaries require macOS 12.0 or later on arm64 and x86_64." - ], - "knownLimitations": [ - "TSX source parsing remains unsupported.", - "The immutable 0.1.159 release retains its inaccurate macOS wheel tags and incomplete wheel license payload." - ], - "rollback": { - "strategy": "previous_admitted_version" - } -} diff --git a/release/change-record.v2.json b/release/change-record.v2.json new file mode 100644 index 0000000..a1f335b --- /dev/null +++ b/release/change-record.v2.json @@ -0,0 +1,130 @@ +{ + "schemaVersion": 2, + "previousVersion": "0.1.160", + "version": "0.2.0", + "changeClass": "breaking", + "breakingChanges": [ + { + "changeId": "proofkit.adoption-doctor.advisory-rule-status", + "summary": "Non-enforced adoption-doctor advisory gap rules now report skipped instead of passed, including observe-mode rules and gaps outside an enforce-touched selection; these gaps do not change the top-level outcome." + }, + { + "changeId": "proofkit.adoption-doctor.blocked-prerequisites", + "summary": "Adoption doctor now reports unresolved external prerequisites as blocked with exit code 1 in every adoption mode; observe and warn no longer relax them." + }, + { + "changeId": "proofkit.browser.native-list-keyboard-contract", + "summary": "Requirement browser navigation now uses native list and button semantics; the removed synthetic tree no longer provides ArrowUp or ArrowDown roving focus." + }, + { + "changeId": "proofkit.cli.adoption-contract-single-value-flags", + "summary": "Adoption contract envelope now rejects repeated --mode or --pilot flags and an explicitly empty --pilot value instead of changing or misreporting the selected root-shape variant." + }, + { + "changeId": "proofkit.cli.invalid-input-channels", + "summary": "Malformed ordinary command input now uses stderr while explicit agent envelopes retain machine-readable invalid-input output." + }, + { + "changeId": "proofkit.cli.pilot-admission-single-value-selector", + "summary": "Pilot admission now rejects repeated or mixed --pilot and --stack-diverse selectors instead of applying last-write-wins routing; the single --stack-diverse alias remains supported with direct or contract-envelope input." + }, + { + "changeId": "proofkit.context.digest-coverage-v2", + "summary": "Requirement context, diff, graph, and browser workspace contracts advance to version 2 with expectedDigestCoverage vocabulary." + }, + { + "changeId": "proofkit.launcher.python-executable-format-controls", + "summary": "Python-module launcher admission now rejects Unicode format characters as well as control characters in the absolute executable path before rendering display commands." + }, + { + "changeId": "proofkit.onboarding.generated-command-invocation", + "summary": "Proofkit-owned generated display commands and structured argv now use one explicit installed launcher channel across help, preset, bootstrap, project, route, workflow, and coverage surfaces: offline npm exec for npm consumers and the active absolute Python interpreter module route for wheel consumers; direct binary consumers retain caller-owned PATH resolution." + }, + { + "changeId": "proofkit.package.installed-governance-routes", + "summary": "The npm artifact no longer ships AGENTS.md or CONTRIBUTING.md; governance and contribution routes remain source-checkout-only." + }, + { + "changeId": "proofkit.readiness-closeout.character-reference-policy", + "summary": "Readiness closeout now decodes one strict semicolon-terminated CommonMark or HTML character reference pass before policy phrase matching; text that previously hid a forbidden phrase through one such reference now fails closed." + }, + { + "changeId": "proofkit.typescript.absolute-symlink", + "summary": "The TypeScript public API scanner rejects absolute symlink targets and requires confined relative in-root links." + } + ], + "additions": [ + { + "changeId": "proofkit.browser.accessibility-state-matrix", + "summary": "The requirement browser adds deterministic loading and failure states, native list semantics, bounded reflow, target-size, and contrast witnesses." + }, + { + "changeId": "proofkit.cli.contract-closure", + "summary": "The authored CLI contract now closes input and output compatibility projections, admits canonical machine-disjoint CLI variant condition models, and generates private runtime metadata." + }, + { + "changeId": "proofkit.onboarding.installed-artifact-trace", + "summary": "Canonical onboarding uses exact npm pins, copyable offline npm exec routes at every displayed help transition, exact preset commands, and a displayed installed-README first-input continuation." + }, + { + "changeId": "proofkit.package.public-reference-closure", + "summary": "The npm package excludes contributor-only governance files and closes package-public Markdown and machine references over shipped entries or explicit source-checkout evidence fields." + }, + { + "changeId": "proofkit.pilot-admission.all-envelope", + "summary": "Pilot admission contract envelopes now admit the existing all mode as one strict two-input envelope and return the ordered first and stack-diverse pilot reports." + }, + { + "changeId": "proofkit.requirement-bindings.witness-selectors", + "summary": "Requirement binding admission and output now preserve optional witnessSelectors records with exact selector and command fields." + }, + { + "changeId": "proofkit.requirement-output.confined-atomic-publication", + "summary": "Requirement view output now uses repository-confined same-parent atomic replacement after final destination-parent plus temporary-object identity, mode, and content admission." + }, + { + "changeId": "proofkit.security-workflows.permission-separation", + "summary": "Security workflow source contracts now keep advisory CodeQL, OSV, and Scorecard analysis read-only, isolate provider publication authority, and require exact Scorecard public-output inputs." + }, + { + "changeId": "proofkit.release.artifact-honest-sbom", + "summary": "CycloneDX runtime edges are emitted only from content-bound per-binary build information while source module inventory is excluded." + }, + { + "changeId": "proofkit.release.existing-release-immutability", + "summary": "Existing release verification is read-only and fails closed instead of uploading or backfilling missing assets." + }, + { + "changeId": "proofkit.release.workflow-action-pins", + "summary": "Every external GitHub Actions use is guarded by a full commit-SHA source oracle." + } + ], + "migration": { + "required": true, + "steps": [ + "Update adoption-doctor consumers that inspect non-enforced advisory rule results to accept skipped instead of passed in observe mode and outside an enforce-touched selection; these gaps leave the top-level outcome unchanged.", + "Update adoption-doctor consumers to treat unresolved external prerequisites as blocked with a nonzero exit in every adoption mode; only advisory gaps remain mode-relaxable.", + "Update requirement-browser keyboard automation to use standard Tab and Shift+Tab traversal plus Enter or Space activation instead of synthetic ArrowUp or ArrowDown tree navigation.", + "Pass --mode and --pilot at most once to adoption-contract-envelope, provide a non-empty --pilot value, and split repeated-flag invocations into one unambiguous invocation.", + "Pass at most one of --pilot or --stack-diverse to pilot-admission; omit both to retain the default first pilot, and split repeated or mixed-selector invocations into one unambiguous invocation.", + "Update context, semantic-diff, graph, and workspace consumers to schemaVersion 2 and replace baselineVerification with expectedDigestCoverage.", + "Remove Unicode control or format characters from the absolute Python executable path supplied by the Python-module launcher profile.", + "Update consumers that compare, execute, or persist Proofkit-generated display commands or structured argv to accept channel-specific installed invocation prefixes across help, preset, bootstrap, project, route, workflow, and coverage surfaces; do not rewrite caller-owned bootstrap command text.", + "Update readiness-closeout consumers and fixtures to treat one strict semicolon-terminated CommonMark or HTML character reference pass as equivalent policy text before phrase matching.", + "Replace absolute symlinks traversed by the TypeScript public API scanner, including package-manifest ancestors and source paths, with relative in-root symlinks.", + "Update consumers that read AGENTS.md or CONTRIBUTING.md from the installed npm artifact to use a source checkout or their own admitted governance policy.", + "Read ordinary malformed-input diagnostics from stderr unless an explicit agent envelope was requested." + ] + }, + "platformRequirements": [ + "Published Darwin package binaries require macOS 12.0 or later on arm64 and x86_64." + ], + "knownLimitations": [ + "TSX source parsing remains unsupported.", + "Static route coverage does not prove semantic execution coverage.", + "The immutable 0.1.160 release is not modified, republished, or backfilled." + ], + "rollback": { + "strategy": "previous_admitted_version" + } +} diff --git a/scripts/browser-proof-inputs.test.mjs b/scripts/browser-proof-inputs.test.mjs index c03ed3e..2eb114d 100644 --- a/scripts/browser-proof-inputs.test.mjs +++ b/scripts/browser-proof-inputs.test.mjs @@ -6,9 +6,378 @@ import test from "node:test"; import {createScanner, LanguageVariant, SyntaxKind} from "typescript/unstable/ast"; +import {analyzeAxe, assertAxeTestComplete, axeConfigureOptions, axeDistributionSource, axeDistributionVersion, axeRunOptions, initializeAxe} from "../tests/browser/axe-harness.mjs"; import {executeBrowserProof} from "./browser-proof-execution.mjs"; import {assertInputSnapshotUnchanged, browserProofInputManifestPath, loadBrowserProofInputResolution, materializeInputSnapshot, snapshotInputAssets} from "./browser-proof-inputs.mjs"; +test("browser accessibility harness closes direct audit topology", async () => { + const values = { + PROOFKIT_BROWSER_TEST_OUTPUT_DIR: resolve(tmpdir(), "proofkit-browser-config-output"), + PROOFKIT_BROWSER_TEST_REPORT_PATH: resolve(tmpdir(), "proofkit-browser-config-report.json"), + PROOFKIT_BROWSER_TEST_URL: "http://127.0.0.1:41001/", + }; + const previous = Object.fromEntries(Object.keys(values).map((name) => [name, process.env[name]])); + Object.assign(process.env, values); + try { + const {default: config} = await import("../playwright.config.mjs"); + assert.equal(config.retries, 0); + assert.equal(config.timeout, 30_000); + assert.equal(config.use.screenshot, "only-on-failure"); + assert.deepEqual(config.use.trace, { + mode: "retain-on-failure", + screenshots: false, + snapshots: true, + sources: true, + }); + assert.equal(axeDistributionVersion, "4.12.1"); + assert(axeDistributionSource.length > 0); + const exactConfigureOptions = { + allowedOrigins: [""], + branding: {application: "playwright"}, + }; + const exactRunOptions = { + rules: {"target-size": {enabled: true}}, + }; + assert.deepEqual(axeConfigureOptions, exactConfigureOptions); + assert.deepEqual(axeRunOptions, exactRunOptions); + assert(Object.isFrozen(axeConfigureOptions)); + assert(Object.isFrozen(axeConfigureOptions.allowedOrigins)); + assert(Object.isFrozen(axeConfigureOptions.branding)); + assert(Object.isFrozen(axeRunOptions)); + assert(Object.isFrozen(axeRunOptions.rules)); + assert(Object.isFrozen(axeRunOptions.rules["target-size"])); + + const assertClosedConfigureOptions = (actual) => { + assert.deepEqual(actual, exactConfigureOptions); + }; + for (const mutant of [ + {branding: {application: "playwright"}}, + {allowedOrigins: [""]}, + {allowedOrigins: ["https://example.invalid"], branding: {application: "playwright"}}, + {allowedOrigins: [""], branding: {}}, + {allowedOrigins: [""], branding: {application: "other"}}, + {allowedOrigins: [""], branding: {application: "playwright"}, surplus: true}, + {allowedOrigins: [""], branding: {application: "playwright", surplus: true}}, + ]) { + assert.throws(() => assertClosedConfigureOptions(mutant)); + } + const assertClosedRunOptions = (actual) => { + assert.deepEqual(actual, exactRunOptions); + }; + for (const mutant of [ + {}, + {rules: {}}, + {rules: {"target-size": {}}}, + {rules: {"target-size": {enabled: false}}}, + {rules: {"target-size": {enabled: true, surplus: true}}}, + {rules: {"target-size": {enabled: true}}, surplus: true}, + {rules: {"target-size": {enabled: true}}, runOnly: ["target-size", "button-name"]}, + {rules: {"button-name": {enabled: true}, "target-size": {enabled: true}}}, + ]) { + assert.throws(() => assertClosedRunOptions(mutant)); + } + + const initScripts = []; + const initContext = { + addInitScript: async (value) => { + initScripts.push(value); + }, + }; + const initPageA = {context: () => initContext}; + const initPageB = {context: () => initContext}; + await initializeAxe(initPageA); + await assert.rejects(initializeAxe(initPageB), /already initialized/); + const otherInitScripts = []; + await initializeAxe({ + context: () => ({ + addInitScript: async (value) => { + otherInitScripts.push(value); + }, + }), + }); + const exactInitScripts = [{content: axeDistributionSource}]; + assert.deepEqual(initScripts, exactInitScripts); + assert.deepEqual(otherInitScripts, exactInitScripts); + for (const mutant of [ + [], + [{content: "wrong"}], + [...exactInitScripts, ...exactInitScripts], + [{content: axeDistributionSource, surplus: true}], + ]) { + assert.throws(() => assert.deepEqual(mutant, exactInitScripts)); + } + + let releaseRegistration; + let registrationStarted; + const registrationBarrier = new Promise((resolve) => { + releaseRegistration = resolve; + }); + const registrationEntered = new Promise((resolve) => { + registrationStarted = resolve; + }); + const concurrentInitScripts = []; + const concurrentContext = { + addInitScript: async (value) => { + concurrentInitScripts.push(value); + registrationStarted(); + await registrationBarrier; + }, + }; + const firstInit = initializeAxe({context: () => concurrentContext}); + await registrationEntered; + const secondInit = initializeAxe({context: () => concurrentContext}); + await Promise.resolve(); + const registrationsBeforeRelease = concurrentInitScripts.length; + releaseRegistration(); + const initOutcomes = await Promise.allSettled([firstInit, secondInit]); + assert.equal(registrationsBeforeRelease, 1); + assert.equal(initOutcomes[0].status, "fulfilled"); + assert.equal(initOutcomes[1].status, "rejected"); + assert.match(initOutcomes[1].reason.message, /already initialized/); + assert.deepEqual(concurrentInitScripts, exactInitScripts); + + let rollbackAttempts = 0; + const rollbackContext = { + addInitScript: async () => { + rollbackAttempts += 1; + if (rollbackAttempts === 1) throw new Error("registration failed"); + }, + }; + const rollbackPage = {context: () => rollbackContext}; + await assert.rejects(initializeAxe(rollbackPage), /registration failed/); + await initializeAxe(rollbackPage); + await assert.rejects(initializeAxe(rollbackPage), /already initialized/); + assert.equal(rollbackAttempts, 2); + + const mainFrame = {}; + const documentSentinel = {}; + const exactTestEngine = { + name: "axe-core", + version: axeDistributionVersion, + }; + const createHarnessPage = ({ + beforeEvaluate, + configure = "callable", + frames = [mainFrame], + run = "callable", + testEngine = exactTestEngine, + version = axeDistributionVersion, + } = {}) => { + const calls = { + configure: [], + context: 0, + evaluate: [], + events: [], + run: [], + }; + const runtime = {}; + if (version !== "absent") { + Object.defineProperty(runtime, "version", { + get: () => { + calls.events.push({type: "version", value: version}); + return version; + }, + }); + } + if (configure === "callable") { + runtime.configure = (options) => { + calls.configure.push(options); + calls.events.push({options, type: "configure"}); + }; + } else if (configure !== "absent") { + runtime.configure = configure; + } + if (run === "callable") { + runtime.run = async (document, options) => { + calls.run.push({document, options}); + calls.events.push({document, options, type: "run"}); + return { + incomplete: [], + passes: [], + testEngine, + violations: [], + }; + }; + } else if (run !== "absent") { + runtime.run = run; + } + const page = { + context: () => { + calls.context += 1; + throw new Error("analysis must not access a context or temporary page"); + }, + evaluate: async (callback, args) => { + calls.evaluate.push({args}); + await beforeEvaluate?.(); + const previousAxe = globalThis.axe; + const previousDocument = globalThis.document; + globalThis.axe = runtime; + globalThis.document = documentSentinel; + try { + return await callback(args); + } finally { + if (previousAxe === undefined) delete globalThis.axe; + else globalThis.axe = previousAxe; + if (previousDocument === undefined) delete globalThis.document; + else globalThis.document = previousDocument; + } + }, + frames: () => frames, + mainFrame: () => mainFrame, + }; + return {calls, page}; + }; + + const admitted = createHarnessPage(); + const result = await analyzeAxe(admitted.page); + assert.deepEqual(result.testEngine, exactTestEngine); + assert.deepEqual(admitted.calls.configure, [exactConfigureOptions]); + assert.deepEqual(admitted.calls.run, [{ + document: documentSentinel, + options: exactRunOptions, + }]); + assert.deepEqual(admitted.calls.evaluate, [{ + args: { + configureOptions: exactConfigureOptions, + expectedVersion: axeDistributionVersion, + runOptions: exactRunOptions, + }, + }]); + assert.deepEqual(admitted.calls.events, [ + {type: "version", value: axeDistributionVersion}, + {options: exactConfigureOptions, type: "configure"}, + {document: documentSentinel, options: exactRunOptions, type: "run"}, + ]); + assert.equal(admitted.calls.context, 0); + await assert.rejects(analyzeAxe(admitted.page), /already analyzed/); + assert.equal(admitted.calls.evaluate.length, 1); + + let releasePendingRegistration; + let pendingRegistrationStarted; + const pendingRegistrationBarrier = new Promise((resolve) => { + releasePendingRegistration = resolve; + }); + const pendingRegistrationEntered = new Promise((resolve) => { + pendingRegistrationStarted = resolve; + }); + const pending = createHarnessPage(); + const pendingContext = { + addInitScript: async () => { + pendingRegistrationStarted(); + await pendingRegistrationBarrier; + }, + }; + pending.page.context = () => pendingContext; + const pendingInitialization = initializeAxe(pending.page); + await pendingRegistrationEntered; + await analyzeAxe(pending.page); + assert.throws(() => assertAxeTestComplete(pending.page), /did not initialize and analyze exactly once/); + releasePendingRegistration(); + await pendingInitialization; + assert.doesNotThrow(() => assertAxeTestComplete(pending.page)); + + let releaseEvaluation; + let evaluationStarted; + const evaluationBarrier = new Promise((resolve) => { + releaseEvaluation = resolve; + }); + const evaluationEntered = new Promise((resolve) => { + evaluationStarted = resolve; + }); + const concurrent = createHarnessPage({ + beforeEvaluate: async () => { + evaluationStarted(); + await evaluationBarrier; + }, + }); + const concurrentAuditContext = {addInitScript: async () => {}}; + concurrent.page.context = () => concurrentAuditContext; + await initializeAxe(concurrent.page); + const firstAudit = analyzeAxe(concurrent.page); + await evaluationEntered; + const secondAudit = analyzeAxe(concurrent.page); + await Promise.resolve(); + const evaluationsBeforeRelease = concurrent.calls.evaluate.length; + assert.throws(() => assertAxeTestComplete(concurrent.page), /did not initialize and analyze exactly once/); + releaseEvaluation(); + const auditOutcomes = await Promise.allSettled([firstAudit, secondAudit]); + assert.equal(evaluationsBeforeRelease, 1); + assert.equal(auditOutcomes[0].status, "fulfilled"); + assert.equal(auditOutcomes[1].status, "rejected"); + assert.match(auditOutcomes[1].reason.message, /already analyzed/); + assert.equal(concurrent.calls.evaluate.length, 1); + assert.doesNotThrow(() => assertAxeTestComplete(concurrent.page)); + + const incompleteContext = {addInitScript: async () => {}}; + const incomplete = createHarnessPage(); + incomplete.page.context = () => incompleteContext; + await initializeAxe(incomplete.page); + assert.throws(() => assertAxeTestComplete(incomplete.page), /did not initialize and analyze exactly once/); + await analyzeAxe(incomplete.page); + assert.doesNotThrow(() => assertAxeTestComplete(incomplete.page)); + const auditOnlyContext = {addInitScript: async () => {}}; + const auditOnly = createHarnessPage(); + auditOnly.page.context = () => auditOnlyContext; + await analyzeAxe(auditOnly.page); + assert.throws(() => assertAxeTestComplete(auditOnly.page), /did not initialize and analyze exactly once/); + for (const options of [ + {frames: []}, + {version: "4.12.0"}, + {testEngine: {name: "other", version: axeDistributionVersion}}, + ]) { + const failed = createHarnessPage(options); + const failedContext = {addInitScript: async () => {}}; + failed.page.context = () => failedContext; + await initializeAxe(failed.page); + await assert.rejects(analyzeAxe(failed.page)); + const evaluationsAfterFailure = failed.calls.evaluate.length; + await assert.rejects(analyzeAxe(failed.page), /already analyzed/); + assert.equal(failed.calls.evaluate.length, evaluationsAfterFailure); + assert.throws(() => assertAxeTestComplete(failed.page), /did not initialize and analyze exactly once/); + } + + for (const frames of [[], [{}], [mainFrame, {}]]) { + const rejected = createHarnessPage({frames}); + await assert.rejects(analyzeAxe(rejected.page), /exactly the main frame/); + assert.equal(rejected.calls.evaluate.length, 0); + } + for (const version of ["absent", "4.12.0"]) { + const rejected = createHarnessPage({version}); + await assert.rejects(analyzeAxe(rejected.page), /was not initialized/); + assert.equal(rejected.calls.configure.length, 0); + assert.equal(rejected.calls.run.length, 0); + } + for (const [field, value] of [ + ["configure", "absent"], + ["configure", null], + ["run", "absent"], + ["run", null], + ]) { + const rejected = createHarnessPage({[field]: value}); + await assert.rejects(analyzeAxe(rejected.page), /is incomplete/); + assert.equal(rejected.calls.configure.length, 0); + assert.equal(rejected.calls.run.length, 0); + } + for (const testEngine of [ + null, + {version: axeDistributionVersion}, + {name: "other", version: axeDistributionVersion}, + {name: "axe-core"}, + {name: "axe-core", version: "4.12.0"}, + ]) { + const rejected = createHarnessPage({testEngine}); + await assert.rejects(analyzeAxe(rejected.page), /unexpected test engine/); + assert.equal(rejected.calls.evaluate.length, 1); + assert.equal(rejected.calls.run.length, 1); + } + } finally { + for (const [name, value] of Object.entries(previous)) { + if (value === undefined) delete process.env[name]; + else process.env[name] = value; + } + } +}); + test("owner resolution closes manifest, Go dependencies, and role-owned paths", () => { const resolution = loadBrowserProofInputResolution(); assert.deepEqual(resolution.inputPaths, [...resolution.inputPaths].sort()); @@ -18,6 +387,15 @@ test("owner resolution closes manifest, Go dependencies, and role-owned paths", assert.equal(resolution.serverTarget, "./internal/tools/browsertestserver"); }); +test("workspace navigation excludes provider-falsified lifecycle waits", () => { + const source = readFileSync("tests/browser/workspace.spec.mjs", "utf8"); + const lifecycleMethods = [...source.matchAll( + /\bpage\.(goBack|goForward|goto|reload|waitForLoadState|waitForNavigation|waitForURL)\s*\(/g, + )].map((match) => match[1]); + assert.deepEqual(lifecycleMethods, ["goto"]); + assert.equal(source.match(/\bpage\.goto\("about:blank"\);/g)?.length, 1); +}); + test("every local JavaScript import is content-bound by the owner resolution", () => { const {inputPaths} = loadBrowserProofInputResolution(); const admitted = new Set(inputPaths); diff --git a/scripts/source-hygiene.mjs b/scripts/source-hygiene.mjs index 02476ee..779edae 100644 --- a/scripts/source-hygiene.mjs +++ b/scripts/source-hygiene.mjs @@ -3,6 +3,7 @@ import { execFileSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; const textExtensions = new Set([ + ".css", ".go", ".js", ".json", @@ -21,6 +22,22 @@ const bannedTokens = [ ["auto", "fleet"].join(""), ]; +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function tokenPattern(token) { + const leftBoundary = /^[a-z0-9]/.test(token) ? "(^|[^a-z0-9])" : ""; + const rightBoundary = /[a-z0-9]$/.test(token) ? "([^a-z0-9]|$)" : ""; + return new RegExp(`${leftBoundary}${escapeRegExp(token)}${rightBoundary}`, "i"); +} + +const bannedTokenPatterns = bannedTokens.map(tokenPattern); + +function containsOrganizationSpecificToken(text) { + return bannedTokenPatterns.some((pattern) => pattern.test(text)); +} + function trackedFiles() { return execFileSync("git", ["ls-files", "-z"], { encoding: "utf8" }) .split("\0") @@ -55,7 +72,7 @@ for (const entry of trackedIndexEntries()) { const lowerText = execFileSync("git", ["cat-file", "-p", entry.object], { encoding: "utf8", }).toLowerCase(); - if (bannedTokens.some((token) => lowerText.includes(token))) { + if (containsOrganizationSpecificToken(lowerText)) { organizationSpecific.add(entry.file); } } @@ -66,7 +83,7 @@ for (const file of trackedFiles()) { } const lowerText = readFileSync(file, "utf8").toLowerCase(); - if (bannedTokens.some((token) => lowerText.includes(token))) { + if (containsOrganizationSpecificToken(lowerText)) { organizationSpecific.add(file); } } diff --git a/scripts/source_hygiene_test.go b/scripts/source_hygiene_test.go index 79a1232..8944d35 100644 --- a/scripts/source_hygiene_test.go +++ b/scripts/source_hygiene_test.go @@ -4,6 +4,7 @@ import ( "os" "os/exec" "path/filepath" + "sort" "strings" "testing" ) @@ -11,7 +12,7 @@ import ( func TestSourceHygieneReadsStagedBlob(t *testing.T) { t.Parallel() - for _, file := range []string{"README.md", "proof.py"} { + for _, file := range sourceHygieneFixtureFiles(t) { file := file t.Run(file, func(t *testing.T) { t.Parallel() @@ -37,7 +38,7 @@ func TestSourceHygieneReadsStagedBlob(t *testing.T) { func TestSourceHygieneReadsTrackedWorktree(t *testing.T) { t.Parallel() - for _, file := range []string{"README.md", "proof.py"} { + for _, file := range sourceHygieneFixtureFiles(t) { file := file t.Run(file, func(t *testing.T) { t.Parallel() @@ -60,6 +61,27 @@ func TestSourceHygieneReadsTrackedWorktree(t *testing.T) { } } +func TestSourceHygieneIgnoresTokenSubstringsInsideContentDigests(t *testing.T) { + t.Parallel() + + scriptPath := sourceHygieneScriptPath(t) + tempDir := t.TempDir() + runCommand(t, tempDir, "git", "init") + + path := filepath.Join(tempDir, "contract.json") + content := `{"canonicalDigest":"sha256:0123456789abcdef0123456789abcdef0123456789afcdef0123456789abcdef"}` + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("write digest fixture: %v", err) + } + runCommand(t, tempDir, "git", "add", "contract.json") + + command := exec.Command("node", scriptPath) + command.Dir = tempDir + if output, err := command.CombinedOutput(); err != nil { + t.Fatalf("source hygiene rejected an identifier substring inside a digest: %v\n%s", err, output) + } +} + func assertSourceHygieneRejects(t *testing.T, scriptPath string, repoRoot string, file string, evidenceClass string) { t.Helper() command := exec.Command("node", scriptPath) @@ -77,11 +99,57 @@ func assertSourceHygieneRejects(t *testing.T, scriptPath string, repoRoot string func sourceHygieneScriptPath(t *testing.T) string { t.Helper() + return filepath.Join(sourceHygieneRepoRoot(t), "scripts", "source-hygiene.mjs") +} + +func sourceHygieneFixtureFiles(t *testing.T) []string { + t.Helper() + + repoRoot := sourceHygieneRepoRoot(t) + command := exec.Command( + "git", + "ls-files", + "-z", + "--", + "internal/command/requirementbrowser/assets", + ) + command.Dir = repoRoot + output, err := command.Output() + if err != nil { + t.Fatalf("list tracked browser assets: %v", err) + } + + extensions := make(map[string]struct{}) + for _, asset := range strings.Split(string(output), "\x00") { + if asset == "" { + continue + } + extension := filepath.Ext(asset) + if extension == "" { + t.Fatalf("tracked browser asset %q has no text extension", asset) + } + extensions[extension] = struct{}{} + } + if len(extensions) == 0 { + t.Fatal("tracked browser asset inventory is empty") + } + + files := []string{"README.md", "proof.py"} + for extension := range extensions { + files = append(files, "browser-asset"+extension) + } + sort.Strings(files) + return files +} + +func sourceHygieneRepoRoot(t *testing.T) string { + t.Helper() + repoRoot, err := filepath.Abs("..") if err != nil { t.Fatalf("resolve repo root: %v", err) } - return filepath.Join(repoRoot, "scripts", "source-hygiene.mjs") + return repoRoot } func runCommand(t *testing.T, dir string, name string, args ...string) { diff --git a/scripts/validate-self-hosting-receipts.go b/scripts/validate-self-hosting-receipts.go index 188de19..598b2e3 100644 --- a/scripts/validate-self-hosting-receipts.go +++ b/scripts/validate-self-hosting-receipts.go @@ -316,14 +316,21 @@ func runProofkit(command string, inputPath string, outputPath string) error { result := exec.Command(binaryPath, command, "--input", inputPath) output, err := result.CombinedOutput() if err != nil { - return fmt.Errorf("%s failed: %w\n%s", command, err, string(output)) + return proofkitVerdict(command, err, output) } if err := os.WriteFile(outputPath, output, 0o644); err != nil { return err } + return proofkitVerdict(command, nil, output) +} + +func proofkitVerdict(command string, processErr error, output []byte) error { + if processErr != nil { + return fmt.Errorf("%s failed: %w\n%s", command, processErr, string(output)) + } reportOutput, err := admission.DecodeJSON(bytes.NewReader(output), maxJSONBytes) if err != nil { - return err + return fmt.Errorf("%s emitted invalid JSON: %w", command, err) } record, ok := reportOutput.(map[string]any) if !ok || record["state"] != "passed" { @@ -439,10 +446,12 @@ func pythonArtifactRefs(version string) ([]map[string]any, error) { if _, err := os.Stat(wheelPath); err != nil { return nil, err } - if expected := stringField(record, "sha256"); expected != "" { - if actual := strings.TrimPrefix(digestFile(wheelPath), "sha256:"); actual != expected { - return nil, fmt.Errorf("python wheel sha256 mismatch for %s", filename) - } + expected := stringField(record, "sha256") + if expected == "" { + return nil, fmt.Errorf("%s package record for %s must include sha256", path, filename) + } + if actual := strings.TrimPrefix(digestFile(wheelPath), "sha256:"); actual != expected { + return nil, fmt.Errorf("python wheel sha256 mismatch for %s", filename) } refs = append(refs, map[string]any{"kind": "artifact", "path": wheelPath, "sha256": digestFile(wheelPath)}) } diff --git a/scripts/validate-self-hosting-receipts_test.go b/scripts/validate-self-hosting-receipts_test.go index 30a5bf3..2966ba6 100644 --- a/scripts/validate-self-hosting-receipts_test.go +++ b/scripts/validate-self-hosting-receipts_test.go @@ -1,6 +1,7 @@ package main import ( + "errors" "os" "path/filepath" "reflect" @@ -14,6 +15,36 @@ import ( "go.yaml.in/yaml/v3" ) +const testRootPackageName = "@research-engineering/agentic-proofkit" + +func TestRunProofkitVerdictCases(t *testing.T) { + cases := []struct { + name string + processErr error + output string + wantError string + }{ + {name: "process exit", processErr: errors.New("exit status 7"), output: `{"state":"passed"}`, wantError: "exit status 7"}, + {name: "invalid JSON", output: `{"state":`, wantError: "emitted invalid JSON"}, + {name: "wrong state", output: `{"state":"failed"}`, wantError: "did not pass"}, + {name: "passed", output: `{"state":"passed"}`}, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + err := proofkitVerdict("fixture", test.processErr, []byte(test.output)) + if test.wantError == "" { + if err != nil { + t.Fatalf("proofkitVerdict() error=%v, want nil", err) + } + return + } + if err == nil || !strings.Contains(err.Error(), test.wantError) { + t.Fatalf("proofkitVerdict() error=%v, want %q", err, test.wantError) + } + }) + } +} + func TestCurrentPlatformBinaryUsesReleasePlatformOwner(t *testing.T) { target, err := releaseplatform.CurrentTarget() if err != nil { @@ -309,6 +340,182 @@ func TestSelfHostingPackageGateReceiptKeepsAggregateEvidenceModel(t *testing.T) } } +func TestPackageArtifactRefsRejectEachPackageIdentityDefect(t *testing.T) { + cases := []struct { + name string + mutate func(t *testing.T, records []any) + want string + }{ + { + name: "name", + mutate: func(_ *testing.T, records []any) { + records[0].(map[string]any)["name"] = "@research-engineering/other" + }, + want: "unexpected package artifact", + }, + { + name: "version", + mutate: func(_ *testing.T, records []any) { + records[0].(map[string]any)["version"] = "9.9.9" + }, + want: "version must match package.json", + }, + { + name: "duplicate", + mutate: func(_ *testing.T, records []any) { + records = append(records, cloneObject(records[0].(map[string]any))) + mustWriteJSON(t, filepath.Join(packageArtifactRoot, "npm-pack.json"), records) + }, + want: "duplicate package artifact", + }, + { + name: "missing artifact", + mutate: func(t *testing.T, records []any) { + filename := records[0].(map[string]any)["filename"].(string) + if err := os.Remove(filepath.Join(packageArtifactRoot, filename)); err != nil { + t.Fatal(err) + } + }, + want: "no such file or directory", + }, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + packageJSON, records := writeArtifactRefFixture(t) + test.mutate(t, records) + if test.name != "duplicate" { + mustWriteJSON(t, filepath.Join(packageArtifactRoot, "npm-pack.json"), records) + } + _, err := packageArtifactRefs(packageJSON) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("packageArtifactRefs() error=%v, want %q", err, test.want) + } + }) + } +} + +func TestPythonArtifactRefsRejectEachWheelIdentityDefect(t *testing.T) { + cases := []struct { + name string + mutate func(t *testing.T, packageSet map[string]any) + want string + }{ + { + name: "version", + mutate: func(_ *testing.T, packageSet map[string]any) { + packageSet["packageVersion"] = "9.9.9" + }, + want: "packageVersion must match", + }, + { + name: "duplicate", + mutate: func(_ *testing.T, packageSet map[string]any) { + packages := packageSet["packages"].([]any) + packageSet["packages"] = append(packages, cloneObject(packages[0].(map[string]any))) + }, + want: "duplicate Python wheel artifact", + }, + { + name: "missing file", + mutate: func(t *testing.T, packageSet map[string]any) { + filename := packageSet["packages"].([]any)[0].(map[string]any)["filename"].(string) + if err := os.Remove(filepath.Join(pythonArtifactRoot, filename)); err != nil { + t.Fatal(err) + } + }, + want: "no such file or directory", + }, + { + name: "SHA mismatch", + mutate: func(_ *testing.T, packageSet map[string]any) { + packageSet["packages"].([]any)[0].(map[string]any)["sha256"] = strings.Repeat("0", 64) + }, + want: "sha256 mismatch", + }, + { + name: "missing SHA", + mutate: func(_ *testing.T, packageSet map[string]any) { + delete(packageSet["packages"].([]any)[0].(map[string]any), "sha256") + }, + want: "must include sha256", + }, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + packageJSON, _ := writeArtifactRefFixture(t) + packageSet := readRepoArtifactObject(t, filepath.Join(pythonArtifactRoot, "python-packages.json")) + test.mutate(t, packageSet) + mustWriteJSON(t, filepath.Join(pythonArtifactRoot, "python-packages.json"), packageSet) + _, err := pythonArtifactRefs(packageJSON["version"].(string)) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("pythonArtifactRefs() error=%v, want %q", err, test.want) + } + }) + } +} + +func TestReceiptIDKeepsLocalAndCIIdentitiesDistinct(t *testing.T) { + local := receiptID(false) + ci := receiptID(true) + if local != "receipt.local.package-artifact" || + ci != "receipt.github.actions.package-artifact" || + local == ci { + t.Fatalf("receipt identities local=%q ci=%q", local, ci) + } +} + +func writeArtifactRefFixture(t *testing.T) (map[string]any, []any) { + t.Helper() + t.Chdir(t.TempDir()) + if err := os.MkdirAll(packageArtifactRoot, 0o755); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(pythonArtifactRoot, 0o755); err != nil { + t.Fatal(err) + } + const ( + version = "1.2.3" + tarball = "agentic-proofkit-1.2.3.tgz" + wheel = "agentic_proofkit-1.2.3-py3-none-any.whl" + ) + if err := os.WriteFile(filepath.Join(packageArtifactRoot, tarball), []byte("tarball"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(pythonArtifactRoot, wheel), []byte("wheel"), 0o644); err != nil { + t.Fatal(err) + } + records := []any{map[string]any{ + "name": testRootPackageName, + "version": version, + "filename": tarball, + }} + mustWriteJSON(t, filepath.Join(packageArtifactRoot, "npm-pack.json"), records) + mustWriteJSON(t, filepath.Join(pythonArtifactRoot, "python-packages.json"), map[string]any{ + "packageVersion": version, + "packages": []any{map[string]any{ + "filename": wheel, + "sha256": strings.TrimPrefix(digestFile(filepath.Join(pythonArtifactRoot, wheel)), "sha256:"), + }}, + }) + return map[string]any{"name": testRootPackageName, "version": version}, records +} + +func mustWriteJSON(t *testing.T, path string, value any) { + t.Helper() + if err := writeJSON(path, value); err != nil { + t.Fatal(err) + } +} + +func readRepoArtifactObject(t *testing.T, path string) map[string]any { + t.Helper() + value, err := readJSONObject(path) + if err != nil { + t.Fatal(err) + } + return value +} + func receiptProducerPolicy(t *testing.T) map[string]any { t.Helper() decoded := readRepoJSON(t, "proofkit/receipt-producer-policy.json") @@ -356,19 +563,33 @@ func readRepoJSON(t *testing.T, path string) any { } func TestCIWorkflowPackageGateRemainsAdvisory(t *testing.T) { - assertPackageGateWorkflowFile(t, filepath.Join("..", ".github", "workflows", "ci.yml"), packageGateWorkflowExpectation{ - label: "ci workflow", + assertPackageGateWorkflowFile( + t, + filepath.Join("..", ".github", "workflows", "ci.yml"), + ciPackageGateWorkflowExpectation(), + ) +} + +func ciPackageGateWorkflowExpectation() packageGateWorkflowExpectation { + return packageGateWorkflowExpectation{ + label: "ci workflow", + workflowName: "ci", + workflowConcurrency: &workflowConcurrencyExpectation{ + group: "ci-${{ github.event.pull_request.number || github.ref }}", + cancelInProgress: true, + }, jobID: "source-quality", stepName: "Verify release closeout", runCommand: "npm run release:closeout", mustFollowSteps: ciSourceQualityProofSteps(), mustPrecedeStepNames: []string{"Upload package tarball artifact"}, requireReadOnlyWorkflowPermissions: true, + stepInventorySHA256: ciSourceQualityStepInventorySHA256, requiredTriggers: []workflowTriggerExpectation{ {event: "pull_request"}, {event: "push", path: []string{"branches"}, value: "main"}, }, - }) + } } func ciSourceQualityProofSteps() []workflowStepExpectation { @@ -378,6 +599,7 @@ func ciSourceQualityProofSteps() []workflowStepExpectation { {name: "Verify text policy", runCommand: "npm run text-policy"}, {name: "Verify Mermaid diagrams", runCommand: "npm run mermaid:check"}, {name: "Verify Go formatting", runCommand: "npm run go:fmt"}, + {name: "Verify generated command contracts", runCommand: "npm run command-contract:check"}, {name: "Verify generated command family catalog", runCommand: "npm run command-family:check"}, {name: "Run all Go tests", runCommand: "npm run go:test"}, {name: "Run Go vet", runCommand: "npm run go:vet"}, @@ -391,13 +613,35 @@ func ciSourceQualityProofSteps() []workflowStepExpectation { } func TestReleaseWorkflowPackageGateRemainsAdvisory(t *testing.T) { - assertPackageGateWorkflowFile(t, filepath.Join("..", ".github", "workflows", "release.yml"), packageGateWorkflowExpectation{ - label: "release workflow", - jobID: "candidate", - stepName: "Run package gate", - runCommand: "npm run check", + assertPackageGateWorkflowFile( + t, + filepath.Join("..", ".github", "workflows", "release.yml"), + releasePackageGateWorkflowExpectation(), + ) +} + +func releasePackageGateWorkflowExpectation() packageGateWorkflowExpectation { + return packageGateWorkflowExpectation{ + label: "release workflow", + workflowName: "release", + workflowConcurrency: &workflowConcurrencyExpectation{ + group: "release-${{ github.ref }}", + cancelInProgress: false, + }, + jobID: "candidate", + stepName: "Run package gate", + runCommand: "npm run check", + workflowEnv: map[string]any{"REGISTRY_URL": "https://registry.npmjs.org"}, + allowedStepEnv: map[string]map[string]any{ + "Verify source package identity": { + "EXPECTED_VERSION": "${{ inputs.expected_version }}", + "MODE": "${{ inputs.mode }}", + }, + }, mustPrecedeStepNames: []string{"Build publish dry-run evidence", "Upload release candidate evidence"}, requireReadOnlyWorkflowPermissions: true, + requireReleaseExpressionInventory: true, + stepInventorySHA256: releaseCandidateStepInventorySHA256, requiredNeeds: map[string][]string{ "publish-readiness": []string{"candidate"}, "publish": []string{"publish-readiness"}, @@ -410,7 +654,7 @@ func TestReleaseWorkflowPackageGateRemainsAdvisory(t *testing.T) { {event: "push", path: []string{"tags"}, value: "v*"}, {event: "workflow_dispatch"}, }, - }) + } } func TestReleaseWorkflowCandidateEvidenceAllowsExistingNPMByteMatch(t *testing.T) { diff --git a/scripts/workflow_browser_runtime_oracle_test.go b/scripts/workflow_browser_runtime_oracle_test.go new file mode 100644 index 0000000..7957efc --- /dev/null +++ b/scripts/workflow_browser_runtime_oracle_test.go @@ -0,0 +1,98 @@ +package main + +import ( + "fmt" + "path/filepath" + "strings" + "testing" +) + +func TestCIBrowserRuntimeInstallsEnginesBeforeProofAndRetainsOnlySuccessfulEvidence(t *testing.T) { + workflow := readWorkflowForTest(t, filepath.Join("..", ".github", "workflows", "ci.yml")) + job, ok := workflow.Jobs["browser-runtime"] + if !ok { + t.Fatal("ci workflow missing browser-runtime job") + } + installIndex, err := uniqueStepIndex(job.Steps, "Install pinned browser engines") + if err != nil { + t.Fatal(err) + } + proofIndex, err := uniqueStepIndex(job.Steps, "Run browser proof") + if err != nil { + t.Fatal(err) + } + uploadIndex, err := uniqueStepIndex(job.Steps, "Upload browser proof") + if err != nil { + t.Fatal(err) + } + if !(installIndex < proofIndex && proofIndex < uploadIndex) { + t.Fatalf("browser runtime order install=%d proof=%d upload=%d", installIndex, proofIndex, uploadIndex) + } + if job.Steps[installIndex].Run != "npx playwright install --with-deps chromium firefox webkit" || job.Steps[proofIndex].Run != "npm run browser:check" { + t.Fatalf("browser runtime commands are not exact: install=%q proof=%q", job.Steps[installIndex].Run, job.Steps[proofIndex].Run) + } + upload := job.Steps[uploadIndex] + if usesAlwaysStatusCheck(upload.If) || upload.With["if-no-files-found"] != "error" || upload.With["path"] != "artifacts/proofkit/browser-runtime-proof.json" { + t.Fatalf("browser proof upload is not fail-closed success evidence: %#v", upload) + } +} + +func TestCIBrowserRuntimeRetainsFailureDiagnosticsWithoutPublishingProof(t *testing.T) { + workflow := readWorkflowForTest(t, filepath.Join("..", ".github", "workflows", "ci.yml")) + job, ok := workflow.Jobs["browser-runtime"] + if !ok { + t.Fatal("ci workflow missing browser-runtime job") + } + proofIndex, err := uniqueStepIndex(job.Steps, "Run browser proof") + if err != nil { + t.Fatal(err) + } + diagnosticsIndex, err := uniqueStepIndex(job.Steps, "Upload browser failure diagnostics") + if err != nil { + t.Fatal(err) + } + successIndex, err := uniqueStepIndex(job.Steps, "Upload browser proof") + if err != nil { + t.Fatal(err) + } + if !(proofIndex < diagnosticsIndex && diagnosticsIndex < successIndex) { + t.Fatalf("browser runtime order proof=%d diagnostics=%d success=%d", proofIndex, diagnosticsIndex, successIndex) + } + diagnostics := job.Steps[diagnosticsIndex] + success := job.Steps[successIndex] + if normalizedExpression(diagnostics.If) != "failure()" { + t.Fatalf("browser diagnostics condition=%q, want exact failure()", diagnostics.If) + } + if strings.TrimSpace(success.If) != "" { + t.Fatalf("browser proof success upload has non-default condition %q", success.If) + } + if diagnostics.Uses == "" || diagnostics.Uses != success.Uses { + t.Fatalf("browser diagnostics action=%q, want pinned proof upload action %q", diagnostics.Uses, success.Uses) + } + wantPath := "artifacts/browser-run-*/playwright-report.json\nartifacts/browser-run-*/test-results" + if strings.TrimSpace(fmt.Sprint(diagnostics.With["path"])) != wantPath || + diagnostics.With["if-no-files-found"] != "error" || + fmt.Sprint(diagnostics.With["retention-days"]) != "14" || + diagnostics.With["name"] != "browser-runtime-diagnostics-${{ github.sha }}-${{ github.run_attempt }}" { + t.Fatalf("browser failure diagnostics upload is not exact and bounded: %#v", diagnostics) + } +} + +func TestReleaseCandidateInstallsBrowserEnginesBeforePackageGate(t *testing.T) { + workflow := readWorkflowForTest(t, filepath.Join("..", ".github", "workflows", "release.yml")) + job, ok := workflow.Jobs["candidate"] + if !ok { + t.Fatal("release workflow missing candidate job") + } + installIndex, err := uniqueStepIndex(job.Steps, "Install pinned browser engines") + if err != nil { + t.Fatal(err) + } + gateIndex, err := uniqueStepIndex(job.Steps, "Run package gate") + if err != nil { + t.Fatal(err) + } + if installIndex >= gateIndex || job.Steps[installIndex].Run != "npx playwright install --with-deps chromium firefox webkit" || job.Steps[gateIndex].Run != "npm run check" { + t.Fatalf("release browser prerequisite is not fail-closed before package gate: install=%#v gate=%#v", job.Steps[installIndex], job.Steps[gateIndex]) + } +} diff --git a/scripts/workflow_oracle_support_test.go b/scripts/workflow_oracle_support_test.go new file mode 100644 index 0000000..a7f32c5 --- /dev/null +++ b/scripts/workflow_oracle_support_test.go @@ -0,0 +1,538 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "go.yaml.in/yaml/v3" +) + +type githubWorkflow struct { + Name string `yaml:"name"` + Env map[string]any `yaml:"env"` + Concurrency *githubConcurrency `yaml:"concurrency"` + Defaults *githubDefaults `yaml:"defaults"` + Jobs map[string]githubJob `yaml:"jobs"` + On map[string]any `yaml:"on"` + Permissions any `yaml:"permissions"` +} + +type githubConcurrency struct { + Group any `yaml:"group"` + CancelInProgress any `yaml:"cancel-in-progress"` + + groupPresent bool `yaml:"-"` + cancelInProgressPresent bool `yaml:"-"` +} + +type githubDefaults struct { + Run *githubRunDefaults `yaml:"run"` +} + +type githubRunDefaults struct { + Shell any `yaml:"shell"` + WorkingDirectory any `yaml:"working-directory"` +} + +type githubJob struct { + ContinueOnError any `yaml:"continue-on-error,omitempty"` + Defaults *githubDefaults `yaml:"defaults,omitempty"` + Env map[string]any `yaml:"env"` + Environment any `yaml:"environment,omitempty"` + If string `yaml:"if,omitempty"` + Name string `yaml:"name"` + Needs any `yaml:"needs"` + Permissions any `yaml:"permissions"` + RunsOn any `yaml:"runs-on"` + Steps []githubStep `yaml:"steps"` + TimeoutMinutes any `yaml:"timeout-minutes,omitempty"` + Uses string `yaml:"uses,omitempty"` + + continueOnErrorPresent bool `yaml:"-"` + defaultsPresent bool `yaml:"-"` + environmentPresent bool `yaml:"-"` + ifPresent bool `yaml:"-"` + timeoutMinutesPresent bool `yaml:"-"` + usesPresent bool `yaml:"-"` +} + +type githubStep struct { + ContinueOnError any `yaml:"continue-on-error,omitempty"` + Env map[string]any `yaml:"env"` + ID string `yaml:"id,omitempty"` + If string `yaml:"if,omitempty"` + Name string `yaml:"name"` + Run string `yaml:"run,omitempty"` + Shell any `yaml:"shell,omitempty"` + TimeoutMinutes any `yaml:"timeout-minutes,omitempty"` + Uses string `yaml:"uses,omitempty"` + With map[string]any `yaml:"with,omitempty"` + WorkingDirectory any `yaml:"working-directory,omitempty"` + + continueOnErrorPresent bool `yaml:"-"` + idPresent bool `yaml:"-"` + shellPresent bool `yaml:"-"` + timeoutMinutesPresent bool `yaml:"-"` + workingDirectoryPresent bool `yaml:"-"` + ifPresent bool `yaml:"-"` + runPresent bool `yaml:"-"` + usesPresent bool `yaml:"-"` + withPresent bool `yaml:"-"` +} + +type plainGithubJob githubJob + +type plainGithubConcurrency githubConcurrency + +func (concurrency *githubConcurrency) UnmarshalYAML(node *yaml.Node) error { + var decoded plainGithubConcurrency + if err := node.Decode(&decoded); err != nil { + return err + } + *concurrency = githubConcurrency(decoded) + concurrency.groupPresent = yamlMappingHasKey(node, "group") + concurrency.cancelInProgressPresent = yamlMappingHasKey(node, "cancel-in-progress") + return nil +} + +func (job *githubJob) UnmarshalYAML(node *yaml.Node) error { + var decoded plainGithubJob + if err := node.Decode(&decoded); err != nil { + return err + } + *job = githubJob(decoded) + job.continueOnErrorPresent = yamlMappingHasKey(node, "continue-on-error") + job.defaultsPresent = yamlMappingHasKey(node, "defaults") + job.environmentPresent = yamlMappingHasKey(node, "environment") + job.ifPresent = yamlMappingHasKey(node, "if") + job.timeoutMinutesPresent = yamlMappingHasKey(node, "timeout-minutes") + job.usesPresent = yamlMappingHasKey(node, "uses") + return nil +} + +type plainGithubStep githubStep + +func (step *githubStep) UnmarshalYAML(node *yaml.Node) error { + var decoded plainGithubStep + if err := node.Decode(&decoded); err != nil { + return err + } + *step = githubStep(decoded) + step.continueOnErrorPresent = yamlMappingHasKey(node, "continue-on-error") + step.idPresent = yamlMappingHasKey(node, "id") + step.ifPresent = yamlMappingHasKey(node, "if") + step.runPresent = yamlMappingHasKey(node, "run") + step.shellPresent = yamlMappingHasKey(node, "shell") + step.timeoutMinutesPresent = yamlMappingHasKey(node, "timeout-minutes") + step.usesPresent = yamlMappingHasKey(node, "uses") + step.withPresent = yamlMappingHasKey(node, "with") + step.workingDirectoryPresent = yamlMappingHasKey(node, "working-directory") + return nil +} + +func yamlMappingHasKey(node *yaml.Node, key string) bool { + if node.Kind != yaml.MappingNode { + return false + } + for index := 0; index+1 < len(node.Content); index += 2 { + if node.Content[index].Value == key { + return true + } + } + return false +} + +func permissionWrites(raw any) bool { + switch value := raw.(type) { + case nil: + return false + case string: + return strings.Contains(strings.ToLower(value), "write") + case map[string]any: + for _, permission := range value { + if text, ok := permission.(string); ok && strings.EqualFold(strings.TrimSpace(text), "write") { + return true + } + } + return false + default: + return false + } +} + +func permissionDeclaredReadOnly(raw any) bool { + switch value := raw.(type) { + case string: + normalized := strings.ToLower(strings.TrimSpace(value)) + return normalized == "read-all" || normalized == "read" + case map[string]any: + if len(value) == 0 { + return false + } + for _, permission := range value { + text, ok := permission.(string) + if !ok { + return false + } + normalized := strings.ToLower(strings.TrimSpace(text)) + if normalized != "read" && normalized != "none" { + return false + } + } + return true + default: + return false + } +} + +func readWorkflowForTest(t *testing.T, path string) githubWorkflow { + t.Helper() + raw, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read workflow %s: %v", path, err) + } + if err := validateWorkflowClosedKeys(path, raw); err != nil { + t.Fatalf("validate closed workflow %s: %v", path, err) + } + var workflow githubWorkflow + if err := yaml.Unmarshal(raw, &workflow); err != nil { + t.Fatalf("parse workflow %s: %v", path, err) + } + return workflow +} + +func validateWorkflowClosedKeys(path string, raw []byte) error { + var document yaml.Node + if err := yaml.Unmarshal(raw, &document); err != nil { + return fmt.Errorf("parse workflow YAML: %w", err) + } + if document.Kind != yaml.DocumentNode || len(document.Content) != 1 { + return fmt.Errorf("workflow YAML must contain exactly one document mapping") + } + workflow := document.Content[0] + if err := validateClosedYAMLMapping( + fmt.Sprintf("workflow %q", path), + workflow, + "name", + "on", + "permissions", + "concurrency", + "defaults", + "env", + "jobs", + ); err != nil { + return err + } + if err := validateOptionalNestedMapping( + fmt.Sprintf("workflow %q concurrency", path), + yamlMappingValue(workflow, "concurrency"), + "group", + "cancel-in-progress", + ); err != nil { + return err + } + if err := validateDefaultsMapping( + fmt.Sprintf("workflow %q defaults", path), + yamlMappingValue(workflow, "defaults"), + ); err != nil { + return err + } + + jobs := yamlMappingValue(workflow, "jobs") + if jobs == nil { + return fmt.Errorf("workflow %q must declare jobs", path) + } + if jobs.Kind != yaml.MappingNode { + return fmt.Errorf("workflow %q jobs must be a mapping", path) + } + if err := validateDynamicYAMLMappingKeys(fmt.Sprintf("workflow %q jobs", path), jobs); err != nil { + return err + } + seenReleaseEnvironments := make(map[string]struct{}) + for index := 0; index+1 < len(jobs.Content); index += 2 { + jobID := jobs.Content[index].Value + job := jobs.Content[index+1] + if err := validateClosedYAMLMapping( + fmt.Sprintf("workflow %q job %q", path, jobID), + job, + "name", + "needs", + "if", + "runs-on", + "timeout-minutes", + "permissions", + "env", + "steps", + "environment", + ); err != nil { + return err + } + if environment := yamlMappingValue(job, "environment"); environment != nil { + if err := validateOwnedReleaseEnvironment(path, jobID, environment); err != nil { + return err + } + seenReleaseEnvironments[jobID] = struct{}{} + } + if yamlMappingValue(job, "environment") == nil && workflowAllowsReleaseEnvironments(path) && + (jobID == "publish" || jobID == "publish-pypi") { + return fmt.Errorf("workflow %q job %q must declare its exact owner environment", path, jobID) + } + if job.Kind != yaml.MappingNode { + return fmt.Errorf("workflow %q job %q must be a mapping", path, jobID) + } + if yamlMappingValue(job, "environment") != nil && + !workflowAllowsReleaseEnvironments(path) { + return fmt.Errorf("workflow %q job %q contains unowned key %q", path, jobID, "environment") + } + steps := yamlMappingValue(job, "steps") + if steps == nil { + return fmt.Errorf("workflow %q job %q must declare steps", path, jobID) + } + if steps.Kind != yaml.SequenceNode { + return fmt.Errorf("workflow %q job %q steps must be a sequence", path, jobID) + } + for stepIndex, step := range steps.Content { + if err := validateClosedYAMLMapping( + fmt.Sprintf("workflow %q job %q step %d", path, jobID, stepIndex), + step, + "name", + "id", + "if", + "run", + "uses", + "with", + "env", + ); err != nil { + return err + } + } + } + if workflowAllowsReleaseEnvironments(path) && len(seenReleaseEnvironments) != 2 { + return fmt.Errorf( + "workflow %q release environment inventory=%v, want exact publish and publish-pypi", + path, + seenReleaseEnvironments, + ) + } + return nil +} + +func workflowAllowsReleaseEnvironments(path string) bool { + actual, err := filepath.Abs(path) + if err != nil { + return false + } + expected, err := filepath.Abs(filepath.Join("..", ".github", "workflows", "release.yml")) + return err == nil && actual == expected +} + +func validateOwnedReleaseEnvironment(path string, jobID string, environment *yaml.Node) error { + if !workflowAllowsReleaseEnvironments(path) { + return fmt.Errorf("workflow %q job %q contains unowned key %q", path, jobID, "environment") + } + switch jobID { + case "publish": + if environment.Kind != yaml.ScalarNode || + environment.Tag != "!!str" || + environment.Value != "npm-production" { + return fmt.Errorf( + "workflow %q job %q environment must equal exact scalar npm-production", + path, + jobID, + ) + } + case "publish-pypi": + if err := validateClosedYAMLMapping( + fmt.Sprintf("workflow %q job %q environment", path, jobID), + environment, + "name", + "url", + ); err != nil { + return err + } + name := yamlMappingValue(environment, "name") + url := yamlMappingValue(environment, "url") + if name == nil || name.Kind != yaml.ScalarNode || name.Tag != "!!str" || name.Value != "pypi" || + url == nil || url.Kind != yaml.ScalarNode || url.Tag != "!!str" || + url.Value != "https://pypi.org/p/agentic-proofkit" { + return fmt.Errorf( + "workflow %q job %q environment must equal exact pypi owner mapping", + path, + jobID, + ) + } + default: + return fmt.Errorf("workflow %q job %q contains unowned key %q", path, jobID, "environment") + } + return nil +} + +func validateDefaultsMapping(label string, defaults *yaml.Node) error { + if defaults == nil || defaults.Kind == yaml.ScalarNode && defaults.Tag == "!!null" { + return nil + } + if err := validateClosedYAMLMapping(label, defaults, "run"); err != nil { + return err + } + return validateOptionalNestedMapping( + label+" run", + yamlMappingValue(defaults, "run"), + "shell", + "working-directory", + ) +} + +func validateOptionalNestedMapping(label string, node *yaml.Node, allowed ...string) error { + if node == nil || node.Kind == yaml.ScalarNode && node.Tag == "!!null" { + return nil + } + return validateClosedYAMLMapping(label, node, allowed...) +} + +func validateClosedYAMLMapping(label string, node *yaml.Node, allowed ...string) error { + if node.Kind != yaml.MappingNode { + return fmt.Errorf("%s must be a mapping", label) + } + allowedKeys := make(map[string]struct{}, len(allowed)) + for _, key := range allowed { + allowedKeys[key] = struct{}{} + } + seen := make(map[string]struct{}, len(node.Content)/2) + for index := 0; index+1 < len(node.Content); index += 2 { + keyNode := node.Content[index] + if keyNode.Kind != yaml.ScalarNode || keyNode.Tag != "!!str" { + return fmt.Errorf("%s contains a non-string key", label) + } + key := keyNode.Value + if _, duplicate := seen[key]; duplicate { + return fmt.Errorf("%s contains duplicate key %q", label, key) + } + seen[key] = struct{}{} + if _, admitted := allowedKeys[key]; !admitted { + return fmt.Errorf("%s contains unowned key %q", label, key) + } + } + return nil +} + +func validateDynamicYAMLMappingKeys(label string, node *yaml.Node) error { + if node.Kind != yaml.MappingNode { + return fmt.Errorf("%s must be a mapping", label) + } + seen := make(map[string]struct{}, len(node.Content)/2) + for index := 0; index+1 < len(node.Content); index += 2 { + keyNode := node.Content[index] + if keyNode.Kind != yaml.ScalarNode || keyNode.Tag != "!!str" { + return fmt.Errorf("%s contains a non-string key", label) + } + key := keyNode.Value + if _, duplicate := seen[key]; duplicate { + return fmt.Errorf("%s contains duplicate key %q", label, key) + } + seen[key] = struct{}{} + } + return nil +} + +func yamlMappingValue(node *yaml.Node, key string) *yaml.Node { + if node == nil || node.Kind != yaml.MappingNode { + return nil + } + for index := 0; index+1 < len(node.Content); index += 2 { + if node.Content[index].Value == key { + return node.Content[index+1] + } + } + return nil +} + +func cloneWorkflow(t *testing.T, workflow githubWorkflow) githubWorkflow { + t.Helper() + content, err := yaml.Marshal(workflow) + if err != nil { + t.Fatal(err) + } + var cloned githubWorkflow + if err := yaml.Unmarshal(content, &cloned); err != nil { + t.Fatal(err) + } + return cloned +} + +func withString(values map[string]any, key string) string { + if values == nil { + return "" + } + value, ok := values[key].(string) + if !ok { + return "" + } + return value +} + +func normalizedExpression(value string) string { + normalized := strings.ToLower(strings.TrimSpace(value)) + normalized = strings.TrimPrefix(normalized, "${{") + normalized = strings.TrimSuffix(normalized, "}}") + return strings.TrimSpace(normalized) +} + +func canonicalWorkflowExpression(value string) string { + value = strings.TrimSpace(value) + if strings.HasPrefix(value, "${{") && strings.HasSuffix(value, "}}") { + value = strings.TrimSpace(value[3 : len(value)-2]) + } + var output strings.Builder + quote := rune(0) + escaped := false + for _, character := range value { + if quote != 0 { + output.WriteRune(character) + if escaped { + escaped = false + continue + } + if character == '\\' && quote == '"' { + escaped = true + continue + } + if character == quote { + quote = 0 + } + continue + } + if character == '\'' || character == '"' { + quote = character + output.WriteRune(character) + continue + } + if character == ' ' || character == '\t' || character == '\n' || character == '\r' { + continue + } + if character >= 'A' && character <= 'Z' { + character += 'a' - 'A' + } + output.WriteRune(character) + } + if quote != 0 { + return "" + } + return output.String() +} + +func uniqueStepIndex(steps []githubStep, name string) (int, error) { + found := -1 + for index, step := range steps { + if step.Name != name { + continue + } + if found >= 0 { + return -1, fmt.Errorf("step %q must be unique", name) + } + found = index + } + return found, nil +} diff --git a/scripts/workflow_package_gate_oracle_test.go b/scripts/workflow_package_gate_oracle_test.go index 26473da..65e8433 100644 --- a/scripts/workflow_package_gate_oracle_test.go +++ b/scripts/workflow_package_gate_oracle_test.go @@ -1,6 +1,9 @@ package main import ( + "crypto/sha256" + "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -14,16 +17,33 @@ import ( const mergeSatisfyingProducerEnv = "PROOFKIT_MERGE_SATISFYING_PRODUCER" +const requiredPlatformSmokeOwnerCommand = "go run ./internal/tools/packagebuild current && ./dist/agentic-proofkit --help >/dev/null && go run ./internal/tools/pythonpackage build-current && go run ./internal/tools/pythonpackage verify-current" +const setupVerifiedNPMActionSHA256 = "ead7e280f6430a9e83a544d5200217efaa36bf7aaedc879f417141fddfb20e8e" +const ciSourceQualityStepInventorySHA256 = "90143666d13b499059937564e0829ecb1799946edb2975f187759cf5ef246da0" +const ciBrowserRuntimeStepInventorySHA256 = "4880405e46ad4daad339117e76174a579e77cdfb70dde9c18d4afc7873f30aa4" +const releaseCandidateStepInventorySHA256 = "a7a1f3216ab9dd0700957ad23ca557df446f5ceb917525aec1e3ead293de0585" + type packageGateWorkflowExpectation struct { label string + workflowName string + workflowConcurrency *workflowConcurrencyExpectation jobID string stepName string runCommand string + workflowEnv map[string]any + allowedStepEnv map[string]map[string]any mustFollowSteps []workflowStepExpectation mustPrecedeStepNames []string requireReadOnlyWorkflowPermissions bool requiredNeeds map[string][]string requiredTriggers []workflowTriggerExpectation + requireReleaseExpressionInventory bool + stepInventorySHA256 string +} + +type workflowConcurrencyExpectation struct { + group string + cancelInProgress bool } type workflowStepExpectation struct { @@ -37,39 +57,15 @@ type workflowTriggerExpectation struct { value string } -type githubWorkflow struct { - Env map[string]any `yaml:"env"` - Jobs map[string]githubJob `yaml:"jobs"` - On map[string]any `yaml:"on"` - Permissions any `yaml:"permissions"` -} - -type githubJob struct { - ContinueOnError any `yaml:"continue-on-error"` - Env map[string]any `yaml:"env"` - If string `yaml:"if"` - Needs any `yaml:"needs"` - Permissions any `yaml:"permissions"` - RunsOn any `yaml:"runs-on"` - Steps []githubStep `yaml:"steps"` -} - -type githubStep struct { - ContinueOnError any `yaml:"continue-on-error"` - Env map[string]any `yaml:"env"` - If string `yaml:"if"` - Name string `yaml:"name"` - Run string `yaml:"run"` - Uses string `yaml:"uses"` - With map[string]any `yaml:"with"` -} - func assertPackageGateWorkflowFile(t *testing.T, path string, expectation packageGateWorkflowExpectation) { t.Helper() raw, err := os.ReadFile(path) if err != nil { t.Fatalf("read %s: %v", path, err) } + if err := validateWorkflowClosedKeys(path, raw); err != nil { + t.Fatalf("%s closed workflow grammar: %v", expectation.label, err) + } if err := validatePackageGateWorkflow(raw, expectation); err != nil { t.Fatalf("%s package gate workflow: %v", expectation.label, err) } @@ -80,15 +76,42 @@ func validatePackageGateWorkflow(raw []byte, expectation packageGateWorkflowExpe if err := yaml.Unmarshal(raw, &workflow); err != nil { return fmt.Errorf("parse workflow YAML: %w", err) } + if expectation.workflowName != "" && workflow.Name != expectation.workflowName { + return fmt.Errorf("workflow name=%q, want exact %q", workflow.Name, expectation.workflowName) + } + if expectation.workflowConcurrency != nil { + if err := validateExactWorkflowConcurrency( + expectation.label, + workflow.Concurrency, + *expectation.workflowConcurrency, + ); err != nil { + return err + } + } if len(workflow.Jobs) == 0 { return fmt.Errorf("workflow must declare jobs") } + if err := validateOptionalWorkflowRunDefaults(workflow.Defaults); err != nil { + return err + } + if !equalStringAnyMaps(workflow.Env, expectation.workflowEnv) { + return fmt.Errorf("workflow env=%v, want exact %v", workflow.Env, expectation.workflowEnv) + } if err := validateTriggers(workflow.On, expectation.requiredTriggers); err != nil { return err } - if err := validateRequiredNeeds(workflow.Jobs, expectation.requiredNeeds); err != nil { + if err := validateRequiredNeeds( + workflow.Jobs, + expectation.requiredNeeds, + expectation.requireReleaseExpressionInventory, + ); err != nil { return err } + if expectation.requireReleaseExpressionInventory { + if err := validateReleaseWorkflowExpressions(workflow); err != nil { + return err + } + } if hasForbiddenProducerOptIn(workflow) { return fmt.Errorf("workflow must not set %s in env or run commands", mergeSatisfyingProducerEnv) } @@ -102,11 +125,28 @@ func validatePackageGateWorkflow(raw []byte, expectation packageGateWorkflowExpe if !ok { return fmt.Errorf("job %q not found", expectation.jobID) } + if err := validateJobExecutionControls(expectation.jobID, job, expectation.allowedStepEnv); err != nil { + return err + } + if expectation.stepInventorySHA256 != "" { + if err := validateExactStepInventory(expectation.label, job.Steps, expectation.stepInventorySHA256); err != nil { + return err + } + } if disabledExpression(job.If) { return fmt.Errorf("job %q is disabled by if expression", expectation.jobID) } - if truthy(job.ContinueOnError) { - return fmt.Errorf("job %q must not continue on package-gate errors", expectation.jobID) + if !expectation.requireReleaseExpressionInventory { + condition := canonicalWorkflowExpression(job.If) + _, ownsNeeds := expectation.requiredNeeds[expectation.jobID] + if (condition == "" && job.ifPresent) || + (condition != "" && (!ownsNeeds || !statusGuardExpressionAdmitted(condition))) { + return fmt.Errorf( + "job %q if=%q, want absent or exact owner status expression", + expectation.jobID, + condition, + ) + } } if needs := needsList(job.Needs); len(needs) > 0 { for _, need := range needs { @@ -130,9 +170,6 @@ func validatePackageGateWorkflow(raw []byte, expectation packageGateWorkflowExpe if disabledExpression(step.If) { return fmt.Errorf("step %q is disabled by if expression", expectation.stepName) } - if truthy(step.ContinueOnError) { - return fmt.Errorf("step %q must not continue on package-gate errors", expectation.stepName) - } if step.Uses != "" { return fmt.Errorf("step %q must be a run step, got uses=%q", expectation.stepName, step.Uses) } @@ -159,9 +196,6 @@ func validatePackageGateWorkflow(raw []byte, expectation packageGateWorkflowExpe if disabledExpression(prior.If) { return fmt.Errorf("required prior step %q is disabled by if expression", priorStep) } - if truthy(prior.ContinueOnError) { - return fmt.Errorf("required prior step %q must not continue on package-gate errors", priorStep) - } if priorExpectation.runCommand != "" { if prior.Uses != "" { return fmt.Errorf("required prior step %q must be a run step, got uses=%q", priorStep, prior.Uses) @@ -186,13 +220,79 @@ func validatePackageGateWorkflow(raw []byte, expectation packageGateWorkflowExpe if disabledExpression(later.If) { return fmt.Errorf("required later step %q is disabled by if expression", laterStep) } - if truthy(later.ContinueOnError) { - return fmt.Errorf("required later step %q must not continue on package-gate errors", laterStep) - } if usesAlwaysStatusCheck(later.If) && !requiresPriorStepSuccess(later.If) { return fmt.Errorf("required later step %q uses always() and must explicitly require success()", laterStep) } } + if !expectation.requireReleaseExpressionInventory { + allowedLaterConditions := make(map[string]struct{}, len(expectation.mustPrecedeStepNames)) + for _, name := range expectation.mustPrecedeStepNames { + allowedLaterConditions[name] = struct{}{} + } + for _, step := range job.Steps { + condition := canonicalWorkflowExpression(step.If) + if condition == "" { + if step.ifPresent { + return fmt.Errorf( + "job %q step %q has an explicit empty condition, want absent exact condition", + expectation.jobID, + step.Name, + ) + } + continue + } + _, later := allowedLaterConditions[step.Name] + if later && condition == admittedStatusExpressions()[0] { + continue + } + return fmt.Errorf( + "job %q step %q if=%q, want absent exact condition", + expectation.jobID, + step.Name, + condition, + ) + } + } + return nil +} + +func validateExactStepInventory(label string, steps []githubStep, expectedSHA256 string) error { + records := make([]map[string]any, 0, len(steps)) + for _, step := range steps { + records = append(records, map[string]any{ + "continueOnError": step.ContinueOnError, + "env": step.Env, + "id": step.ID, + "if": step.If, + "name": step.Name, + "presence": map[string]bool{ + "continueOnError": step.continueOnErrorPresent, + "id": step.idPresent, + "if": step.ifPresent, + "run": step.runPresent, + "shell": step.shellPresent, + "timeoutMinutes": step.timeoutMinutesPresent, + "uses": step.usesPresent, + "with": step.withPresent, + "workingDirectory": step.workingDirectoryPresent, + }, + "run": step.Run, + "shell": step.Shell, + "timeoutMinutes": step.TimeoutMinutes, + "uses": step.Uses, + "with": step.With, + "workingDirectory": step.WorkingDirectory, + }) + } + encoded, err := json.Marshal(records) + if err != nil { + return fmt.Errorf("%s exact step inventory encoding: %w", label, err) + } + sum := sha256.Sum256(encoded) + actual := fmt.Sprintf("%x", sum[:]) + if actual != expectedSHA256 { + return fmt.Errorf("%s exact step inventory sha256=%s, want %s", label, actual, expectedSHA256) + } return nil } @@ -220,8 +320,15 @@ func validateTriggers(on map[string]any, expectations []workflowTriggerExpectati return nil } -func validateRequiredNeeds(jobs map[string]githubJob, required map[string][]string) error { +func validateRequiredNeeds( + jobs map[string]githubJob, + required map[string][]string, + conditionsOwnedByExactInventory bool, +) error { for jobID, expectedNeeds := range required { + if len(expectedNeeds) == 0 { + return fmt.Errorf("required workflow job %q has an empty exact needs inventory", jobID) + } job, ok := jobs[jobID] if !ok { return fmt.Errorf("required workflow job %q not found", jobID) @@ -229,6 +336,17 @@ func validateRequiredNeeds(jobs map[string]githubJob, required map[string][]stri if disabledExpression(job.If) { return fmt.Errorf("required workflow job %q is disabled by if expression", jobID) } + condition := canonicalWorkflowExpression(job.If) + if !conditionsOwnedByExactInventory && + ((condition == "" && job.ifPresent) || + (condition != "" && !statusGuardExpressionAdmitted(condition))) { + return fmt.Errorf( + "required workflow job %q if=%q must use an exact owner expression and explicitly require needs.%s.result == 'success'", + jobID, + condition, + expectedNeeds[0], + ) + } actualNeeds := needsList(job.Needs) for _, expectedNeed := range expectedNeeds { if !containsString(actualNeeds, expectedNeed) { @@ -280,40 +398,236 @@ func disabledExpression(value string) bool { } func usesAlwaysStatusCheck(value string) bool { - return strings.Contains(removeExpressionWhitespace(normalizedExpression(value)), "always()") + return strings.Contains(unquotedExpression(canonicalWorkflowExpression(value)), "always()") } func requiresSuccessfulNeed(value string, need string) bool { - normalized := removeExpressionWhitespace(normalizedExpression(value)) + normalized := canonicalWorkflowExpression(value) token := "needs." + need + ".result=='success'" return statusGuardExpressionAdmitted(normalized) && (hasConjunct(normalized, token) || hasAllowedOptionalNeedGuard(normalized, need, token)) } func requiresPriorStepSuccess(value string) bool { - normalized := removeExpressionWhitespace(normalizedExpression(value)) + normalized := canonicalWorkflowExpression(value) return statusGuardExpressionAdmitted(normalized) && hasConjunct(normalized, "success()") } func statusGuardExpressionAdmitted(normalized string) bool { - withoutAllowedDisjunctions := normalized - for _, guard := range allowedStatusDisjunctions() { - withoutAllowedDisjunctions = strings.ReplaceAll(withoutAllowedDisjunctions, guard, "") - } - if strings.Contains(withoutAllowedDisjunctions, "||") || - strings.Contains(normalized, "failure()") || - strings.Contains(normalized, "cancelled()") { - return false + for _, admitted := range admittedStatusExpressions() { + if normalized == admitted { + return true + } } - return true + return false } -func allowedStatusDisjunctions() []string { +func admittedStatusExpressions() []string { return []string{ - "(github.event_name=='push'||inputs.mode=='publish')", - "(vars.proofkit_enable_pypi_publish!='true'||needs.publish-pypi.result=='success')", - "(vars.proofkit_enable_github_attestations!='true'||github.event.repository.private==true||needs.release-attestations.result=='success')", + "always()&&success()", + "always()&&needs.candidate.result=='success'", + "always()&&needs.candidate.result=='success'&&(vars.proofkit_enable_github_attestations!='true'||github.event.repository.private==true||needs.release-attestations.result=='success')", + "always()&&(github.event_name=='push'||inputs.mode=='publish')&&needs.candidate.result=='success'&&needs.publish.result=='success'&&(vars.proofkit_enable_pypi_publish!='true'||needs.publish-pypi.result=='success')", + "always()&&(github.event_name=='push'||inputs.mode=='publish')&&vars.proofkit_enable_github_attestations=='true'&&github.event.repository.private==false&&needs.candidate.result=='success'&&needs.publish.result=='success'&&needs.release-metadata.result=='success'&&(vars.proofkit_enable_pypi_publish!='true'||needs.publish-pypi.result=='success')", + "always()&&(github.event_name=='push'||inputs.mode=='publish')&&needs.candidate.result=='success'&&needs.publish.result=='success'&&needs.release-metadata.result=='success'&&(vars.proofkit_enable_pypi_publish!='true'||needs.publish-pypi.result=='success')&&(vars.proofkit_enable_github_attestations!='true'||github.event.repository.private==true||needs.release-attestations.result=='success')", + } +} + +func validateReleaseWorkflowExpressions(workflow githubWorkflow) error { + if err := validateExactWorkflowRuntimeControls( + "release workflow", + workflow, + map[string]jobRuntimeControlsExpectation{ + "candidate": {timeoutMinutes: 30}, + "publish-readiness": {timeoutMinutes: 10}, + "publish": { + timeoutMinutes: 20, + environment: "npm-production", + environmentPresent: true, + }, + "publish-pypi": { + timeoutMinutes: 20, + environment: map[string]any{ + "name": "pypi", + "url": "https://pypi.org/p/agentic-proofkit", + }, + environmentPresent: true, + }, + "release-metadata": {timeoutMinutes: 10}, + "release-attestations": {timeoutMinutes: 10}, + "release-assets": {timeoutMinutes: 15}, + }, + ); err != nil { + return err + } + jobExpressions := map[string]string{ + "candidate": "", + "publish-readiness": "github.event_name=='push'||inputs.mode=='publish'", + "publish": "github.event_name=='push'||inputs.mode=='publish'", + "publish-pypi": "(github.event_name=='push'||inputs.mode=='publish')&&vars.proofkit_enable_pypi_publish=='true'", + "release-metadata": admittedStatusExpressions()[3], + "release-attestations": admittedStatusExpressions()[4], + "release-assets": admittedStatusExpressions()[5], + } + stepExpressions := map[string]map[string]string{ + "publish-pypi": { + "Publish to PyPI": "steps.pypi-preflight.outputs.skip_publish!='true'", + }, + "release-metadata": { + "Download PyPI registry evidence": "vars.proofkit_enable_pypi_publish=='true'", + }, + "release-assets": { + "Download PyPI registry evidence": "vars.proofkit_enable_pypi_publish=='true'", + "Download attestation evidence": "vars.proofkit_enable_github_attestations=='true'&&github.event.repository.private==false", + "Upload PyPI release evidence": "vars.proofkit_enable_pypi_publish=='true'", + }, + } + return validateExactWorkflowExpressions( + "release workflow", + workflow, + jobExpressions, + stepExpressions, + ) +} + +type jobRuntimeControlsExpectation struct { + timeoutMinutes int + environment any + environmentPresent bool +} + +func validateExactWorkflowRuntimeControls( + label string, + workflow githubWorkflow, + expected map[string]jobRuntimeControlsExpectation, +) error { + if len(workflow.Jobs) != len(expected) { + return fmt.Errorf("%s runtime-control job count=%d, want exact %d", label, len(workflow.Jobs), len(expected)) + } + for jobID, want := range expected { + job, ok := workflow.Jobs[jobID] + if !ok { + return fmt.Errorf("%s runtime-control job %q is missing", label, jobID) + } + if !job.timeoutMinutesPresent || !reflect.DeepEqual(job.TimeoutMinutes, want.timeoutMinutes) { + return fmt.Errorf( + "%s job %q timeout-minutes=%v, want exact %d", + label, + jobID, + job.TimeoutMinutes, + want.timeoutMinutes, + ) + } + if job.environmentPresent != want.environmentPresent || + !reflect.DeepEqual(job.Environment, want.environment) { + return fmt.Errorf( + "%s job %q environment=%v presence=%t, want exact %v presence=%t", + label, + jobID, + job.Environment, + job.environmentPresent, + want.environment, + want.environmentPresent, + ) + } + if job.ContinueOnError != nil || job.continueOnErrorPresent || + job.Defaults != nil || job.defaultsPresent || + len(job.Env) != 0 || + job.Uses != "" || job.usesPresent { + return fmt.Errorf("%s job %q contains an unowned execution control", label, jobID) + } + for _, step := range job.Steps { + if step.ContinueOnError != nil || step.continueOnErrorPresent || + step.Shell != nil || step.shellPresent || + step.TimeoutMinutes != nil || step.timeoutMinutesPresent || + step.WorkingDirectory != nil || step.workingDirectoryPresent { + return fmt.Errorf( + "%s job %q step %q contains an unowned execution control", + label, + jobID, + step.Name, + ) + } + } } + return nil +} + +func validateExactWorkflowExpressions( + label string, + workflow githubWorkflow, + jobExpressions map[string]string, + stepExpressions map[string]map[string]string, +) error { + if len(workflow.Jobs) != len(jobExpressions) { + return fmt.Errorf( + "%s job inventory count=%d, want exact %d", + label, + len(workflow.Jobs), + len(jobExpressions), + ) + } + for jobID, job := range workflow.Jobs { + expectedJobExpression, owned := jobExpressions[jobID] + if !owned { + return fmt.Errorf("%s has unowned job %s", label, jobID) + } + if actual := canonicalWorkflowExpression(job.If); actual != expectedJobExpression { + return fmt.Errorf( + "%s job %s if=%q, want exact owner expression %q", + label, + jobID, + actual, + expectedJobExpression, + ) + } + if expectedJobExpression == "" && job.ifPresent { + return fmt.Errorf( + "%s job %s has an explicit empty condition, want absent condition", + label, + jobID, + ) + } + expectedStepExpressions := stepExpressions[jobID] + seen := make(map[string]int, len(expectedStepExpressions)) + for _, step := range job.Steps { + expected := "" + if ownerExpression, ok := expectedStepExpressions[step.Name]; ok { + expected = ownerExpression + seen[step.Name]++ + } + if actual := canonicalWorkflowExpression(step.If); actual != expected { + return fmt.Errorf( + "%s job %s step %s if=%q, want exact owner expression %q", + label, + jobID, + step.Name, + actual, + expected, + ) + } + if expected == "" && step.ifPresent { + return fmt.Errorf( + "%s job %s step %s has an explicit empty condition, want absent condition", + label, + jobID, + step.Name, + ) + } + } + for stepName := range expectedStepExpressions { + if seen[stepName] != 1 { + return fmt.Errorf( + "%s job %s expression step %s count=%d, want exactly one", + label, + jobID, + stepName, + seen[stepName], + ) + } + } + } + return nil } func hasAllowedOptionalNeedGuard(normalized string, need string, token string) bool { @@ -334,10 +648,6 @@ func hasConjunct(normalized string, token string) bool { strings.HasSuffix(normalized, "&&"+token) } -func removeExpressionWhitespace(value string) string { - return strings.Join(strings.Fields(value), "") -} - func needsList(raw any) []string { switch value := raw.(type) { case nil: @@ -399,416 +709,1009 @@ func containsString(values []string, want string) bool { return false } -func permissionWrites(raw any) bool { - switch value := raw.(type) { - case nil: - return false - case string: - return strings.Contains(strings.ToLower(value), "write") - case map[string]any: - for _, permission := range value { - if text, ok := permission.(string); ok && strings.EqualFold(strings.TrimSpace(text), "write") { - return true - } - } - return false - default: - return false +func TestCIWorkflowDeclaresFailClosedRequiredAggregate(t *testing.T) { + workflow := readWorkflowForTest(t, filepath.Join("..", ".github", "workflows", "ci.yml")) + if err := validateCIRequiredAggregate(workflow); err != nil { + t.Fatal(err) } -} - -func permissionHas(raw any, key string, want string) bool { - record, ok := raw.(map[string]any) - if !ok { - return false + if err := validatePlatformSmokeOwnerScript(readPackageManifestForTest(t)); err != nil { + t.Fatal(err) + } + if err := validateSetupVerifiedNPMAction(readSetupVerifiedNPMActionForTest(t)); err != nil { + t.Fatal(err) } - value, ok := record[key] + gate, ok := workflow.Jobs["ci-required-gate"] if !ok { - return false + t.Fatalf("ci workflow missing ci-required-gate job") } - text, ok := value.(string) - return ok && strings.EqualFold(strings.TrimSpace(text), want) -} - -func permissionDeclaredReadOnly(raw any) bool { - switch value := raw.(type) { - case string: - normalized := strings.ToLower(strings.TrimSpace(value)) - return normalized == "read-all" || normalized == "read" - case map[string]any: - if len(value) == 0 { - return false - } - for _, permission := range value { - text, ok := permission.(string) - if !ok { - return false - } - normalized := strings.ToLower(strings.TrimSpace(text)) - if normalized != "read" && normalized != "none" { - return false - } + if gate.ContinueOnError != nil { + t.Fatalf("ci-required-gate must omit continue-on-error") + } + if permissionWrites(gate.Permissions) { + t.Fatalf("ci-required-gate permissions grant write scopes: %#v", gate.Permissions) + } + if !usesAlwaysStatusCheck(gate.If) { + t.Fatalf("ci-required-gate if=%q, want always() so failed or skipped needs are inspected", gate.If) + } + wantNeeds := []string{"browser-runtime", "platform-smoke", "source-quality"} + if got := needsList(gate.Needs); !reflect.DeepEqual(got, wantNeeds) { + t.Fatalf("ci-required-gate needs=%#v, want %#v", got, wantNeeds) + } + if len(gate.Steps) != 1 { + t.Fatalf("ci-required-gate steps=%d, want one aggregate assertion step", len(gate.Steps)) + } + run := gate.Steps[0].Run + for _, need := range wantNeeds { + want := fmt.Sprintf(`test "${{ needs.%s.result }}" = "success"`, need) + if !strings.Contains(run, want) { + t.Fatalf("ci-required-gate run must require %s success, run=%q", need, run) } - return true - default: - return false } } -func TestSecurityScannerWorkflowsSeparateProviderPublicationPermissions(t *testing.T) { +func TestCIRequiredAggregateRejectsNeutralizedScript(t *testing.T) { + base := readWorkflowForTest(t, filepath.Join("..", ".github", "workflows", "ci.yml")) + if err := validateCIRequiredAggregate(base); err != nil { + t.Fatalf("owner CI workflow: %v", err) + } cases := []struct { - name string - path string - advisoryJobs []string - providerJobs map[string]map[string]string + name string + mutate func(*githubJob) }{ { - name: "codeql", - path: filepath.Join("..", ".github", "workflows", "codeql.yml"), - advisoryJobs: []string{"analyze"}, - providerJobs: map[string]map[string]string{ - "upload-sarif": {"security-events": "write"}, + name: "dead branch", + mutate: func(job *githubJob) { + job.Steps[0].Run = "if false; then\n" + job.Steps[0].Run + "fi\n" + }, + }, + { + name: "or true", + mutate: func(job *githubJob) { + job.Steps[0].Run = strings.Replace(job.Steps[0].Run, `"success"`, `"success" || true`, 1) + }, + }, + { + name: "early exit", + mutate: func(job *githubJob) { + job.Steps[0].Run = "exit 0\n" + job.Steps[0].Run + }, + }, + { + name: "background assertion", + mutate: func(job *githubJob) { + job.Steps[0].Run = strings.Replace(job.Steps[0].Run, `"success"`, `"success" &`, 1) + }, + }, + { + name: "step bypass", + mutate: func(job *githubJob) { + job.Steps[0].ContinueOnError = true + }, + }, + { + name: "job bypass", + mutate: func(job *githubJob) { + job.ContinueOnError = true }, }, { - name: "osv", - path: filepath.Join("..", ".github", "workflows", "osv-scanner.yml"), - advisoryJobs: []string{"scan"}, - providerJobs: map[string]map[string]string{ - "upload-sarif": {"security-events": "write"}, + name: "job expression bypass", + mutate: func(job *githubJob) { + job.ContinueOnError = "${{ 1 == 1 }}" }, }, { - name: "scorecard", - path: filepath.Join("..", ".github", "workflows", "scorecard.yml"), - advisoryJobs: []string{"scorecard"}, - providerJobs: map[string]map[string]string{ - "upload-sarif": {"security-events": "write"}, + name: "job bypass field present but false", + mutate: func(job *githubJob) { + job.ContinueOnError = false + }, + }, + { + name: "guard neutralized", + mutate: func(job *githubJob) { + job.If = "${{ always() || true }}" }, }, } - for _, item := range cases { - t.Run(item.name, func(t *testing.T) { - workflow := readWorkflowForTest(t, item.path) - if permissionWrites(workflow.Permissions) { - t.Fatalf("%s workflow-level permissions grant write scopes", item.path) + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + workflow := cloneWorkflow(t, base) + job := workflow.Jobs["ci-required-gate"] + test.mutate(&job) + workflow.Jobs["ci-required-gate"] = job + if err := validateCIRequiredAggregate(workflow); err == nil { + t.Fatal("neutralized required aggregate was admitted") } - for _, jobID := range item.advisoryJobs { - job, ok := workflow.Jobs[jobID] - if !ok { - t.Fatalf("%s missing advisory job %q", item.path, jobID) + }) + } +} + +func TestCIRequiredAggregateRejectsExecutionOverrides(t *testing.T) { + base := readWorkflowForTest(t, filepath.Join("..", ".github", "workflows", "ci.yml")) + if err := validateCIRequiredAggregate(base); err != nil { + t.Fatalf("owner CI workflow: %v", err) + } + cases := []struct { + name string + mutate func(*githubWorkflow) + }{ + { + name: "workflow name", + mutate: func(workflow *githubWorkflow) { + workflow.Name = "shadow" + }, + }, + { + name: "workflow concurrency group", + mutate: func(workflow *githubWorkflow) { + workflow.Concurrency.Group = "shadow" + }, + }, + { + name: "workflow concurrency cancellation", + mutate: func(workflow *githubWorkflow) { + workflow.Concurrency.CancelInProgress = false + }, + }, + { + name: "workflow shell", + mutate: func(workflow *githubWorkflow) { + workflow.Defaults.Run.Shell = "bash {0} || true" + }, + }, + { + name: "workflow working directory", + mutate: func(workflow *githubWorkflow) { + workflow.Defaults.Run.WorkingDirectory = "shadow" + }, + }, + { + name: "workflow environment", + mutate: func(workflow *githubWorkflow) { + workflow.Env = map[string]any{"BASH_ENV": "./scripts/bypass.sh"} + }, + }, + { + name: "job shell defaults", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["ci-required-gate"] + job.Defaults = &githubDefaults{ + Run: &githubRunDefaults{Shell: "bash {0} || true"}, } - if permissionWrites(job.Permissions) { - t.Fatalf("%s advisory job %q grants write scopes: %#v", item.path, jobID, job.Permissions) + workflow.Jobs["ci-required-gate"] = job + }, + }, + { + name: "required leaf job continue on error", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["browser-runtime"] + job.ContinueOnError = "${{ 1 == 1 }}" + workflow.Jobs["browser-runtime"] = job + }, + }, + { + name: "required leaf job null continue on error", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["browser-runtime"] + job.continueOnErrorPresent = true + workflow.Jobs["browser-runtime"] = job + }, + }, + { + name: "required leaf job shell defaults", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["platform-smoke"] + job.Defaults = &githubDefaults{ + Run: &githubRunDefaults{Shell: "bash {0} || true"}, } - } - for jobID, permissions := range item.providerJobs { - job, ok := workflow.Jobs[jobID] - if !ok { - t.Fatalf("%s missing provider job %q", item.path, jobID) + workflow.Jobs["platform-smoke"] = job + }, + }, + { + name: "required leaf job null defaults", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["platform-smoke"] + job.defaultsPresent = true + workflow.Jobs["platform-smoke"] = job + }, + }, + { + name: "required leaf job environment", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["platform-smoke"] + job.Env = map[string]any{"PATH": "./shadow"} + workflow.Jobs["platform-smoke"] = job + }, + }, + { + name: "required leaf job timeout", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["source-quality"] + job.TimeoutMinutes = 1 + workflow.Jobs["source-quality"] = job + }, + }, + { + name: "required leaf job dynamic false condition", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["source-quality"] + job.If = "${{ github.event_name == 'never' }}" + workflow.Jobs["source-quality"] = job + }, + }, + { + name: "required leaf job explicit null condition", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["source-quality"] + job.If = "" + job.ifPresent = true + workflow.Jobs["source-quality"] = job + }, + }, + { + name: "required leaf step dynamic false condition", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["source-quality"] + for index := range job.Steps { + if job.Steps[index].Name == "Run all Go tests" { + job.Steps[index].If = "${{ github.event_name == 'never' }}" + } } - if item.name == "codeql" || item.name == "osv" { - if !providerUploadDisabledOnPullRequest(job.If) { - t.Fatalf("%s provider job %q must not upload provider evidence on pull_request: if=%q", item.path, jobID, job.If) + workflow.Jobs["source-quality"] = job + }, + }, + { + name: "required leaf step explicit null condition", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["source-quality"] + for index := range job.Steps { + if job.Steps[index].Name == "Run all Go tests" { + job.Steps[index].If = "" + job.Steps[index].ifPresent = true } } - for scope, want := range permissions { - if !permissionHas(job.Permissions, scope, want) { - t.Fatalf("%s provider job %q permission %s=%#v, want %q", item.path, jobID, scope, job.Permissions, want) + workflow.Jobs["source-quality"] = job + }, + }, + { + name: "required leaf step shell", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["source-quality"] + for index := range job.Steps { + if job.Steps[index].Name == "Verify browser source types" { + job.Steps[index].Shell = "bash {0} || true" } } + workflow.Jobs["source-quality"] = job + }, + }, + { + name: "required leaf step environment", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["source-quality"] + for index := range job.Steps { + if job.Steps[index].Name == "Verify browser source types" { + job.Steps[index].Env = map[string]any{"NODE_OPTIONS": "--require ./scripts/bypass.cjs"} + } + } + workflow.Jobs["source-quality"] = job + }, + }, + { + name: "source quality semantic shadow step", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["source-quality"] + index, err := uniqueStepIndex(job.Steps, "Run all Go tests") + if err != nil || index < 0 { + panic("owner source-quality workflow lost Run all Go tests") + } + shadow := githubStep{ + Name: "Shadow Go tests", + Run: "npm pkg set scripts.go:test=true", + runPresent: true, + } + job.Steps = append(job.Steps[:index], append([]githubStep{shadow}, job.Steps[index:]...)...) + workflow.Jobs["source-quality"] = job + }, + }, + { + name: "source quality step id", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["source-quality"] + index, err := uniqueStepIndex(job.Steps, "Run all Go tests") + if err != nil || index < 0 { + panic("owner source-quality workflow lost Run all Go tests") + } + job.Steps[index].ID = "shadow-go-tests" + job.Steps[index].idPresent = true + workflow.Jobs["source-quality"] = job + }, + }, + { + name: "source quality timeout minutes", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["source-quality"] + index, err := uniqueStepIndex(job.Steps, "Run all Go tests") + if err != nil || index < 0 { + panic("owner source-quality workflow lost Run all Go tests") + } + job.Steps[index].TimeoutMinutes = 1 + job.Steps[index].timeoutMinutesPresent = true + workflow.Jobs["source-quality"] = job + }, + }, + { + name: "browser runtime semantic shadow step", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["browser-runtime"] + index, err := uniqueStepIndex(job.Steps, "Run browser proof") + if err != nil || index < 0 { + panic("owner browser-runtime workflow lost Run browser proof") + } + shadow := githubStep{ + Name: "Shadow browser proof", + Run: "npm pkg set scripts.browser:check=true", + runPresent: true, + } + job.Steps = append(job.Steps[:index], append([]githubStep{shadow}, job.Steps[index:]...)...) + workflow.Jobs["browser-runtime"] = job + }, + }, + { + name: "browser runtime step id", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["browser-runtime"] + index, err := uniqueStepIndex(job.Steps, "Run browser proof") + if err != nil || index < 0 { + panic("owner browser-runtime workflow lost Run browser proof") + } + job.Steps[index].ID = "shadow-browser-proof" + job.Steps[index].idPresent = true + workflow.Jobs["browser-runtime"] = job + }, + }, + { + name: "browser runtime timeout minutes", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["browser-runtime"] + index, err := uniqueStepIndex(job.Steps, "Run browser proof") + if err != nil || index < 0 { + panic("owner browser-runtime workflow lost Run browser proof") + } + job.Steps[index].TimeoutMinutes = 1 + job.Steps[index].timeoutMinutesPresent = true + workflow.Jobs["browser-runtime"] = job + }, + }, + { + name: "step shell", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["ci-required-gate"] + job.Steps[0].Shell = "bash {0} || true" + workflow.Jobs["ci-required-gate"] = job + }, + }, + { + name: "step null shell", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["ci-required-gate"] + job.Steps[0].shellPresent = true + workflow.Jobs["ci-required-gate"] = job + }, + }, + { + name: "step working directory", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["ci-required-gate"] + job.Steps[0].WorkingDirectory = "shadow" + workflow.Jobs["ci-required-gate"] = job + }, + }, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + workflow := cloneWorkflow(t, base) + test.mutate(&workflow) + if err := validateCIRequiredAggregate(workflow); err == nil { + t.Fatal("execution override on required aggregate was admitted") } }) } } -func providerUploadDisabledOnPullRequest(expression string) bool { - normalized := normalizedExpression(expression) - return strings.Contains(normalized, "github.event_name != 'pull_request'") || - strings.Contains(normalized, `github.event_name != "pull_request"`) -} - -func TestScorecardPublicPublishDeclaresRequiredOutputInputs(t *testing.T) { - workflow := readWorkflowForTest(t, filepath.Join("..", ".github", "workflows", "scorecard-publish.yml")) - if permissionWrites(workflow.Permissions) { - t.Fatalf("scorecard publish workflow-level permissions grant write scopes") +func TestCIRequiredAggregateRejectsPlatformSmokeSubstitution(t *testing.T) { + base := readWorkflowForTest(t, filepath.Join("..", ".github", "workflows", "ci.yml")) + if err := validateCIRequiredAggregate(base); err != nil { + t.Fatalf("owner CI workflow: %v", err) } - if len(workflow.Jobs) != 1 { - t.Fatalf("scorecard publish workflow must contain exactly one job, got %d", len(workflow.Jobs)) + packageManifest := readPackageManifestForTest(t) + if err := validatePlatformSmokeOwnerScript(packageManifest); err != nil { + t.Fatalf("owner platform-smoke package script: %v", err) } - job, ok := workflow.Jobs["scorecard"] - if !ok { - t.Fatalf("scorecard workflow missing public publish job") - } - if !permissionHas(job.Permissions, "id-token", "write") { - t.Fatalf("scorecard public publish job must declare id-token=write, got %#v", job.Permissions) + setupAction := readSetupVerifiedNPMActionForTest(t) + if err := validateSetupVerifiedNPMAction(setupAction); err != nil { + t.Fatalf("owner setup-verified-npm action: %v", err) } - stepIndex, err := uniqueStepIndex(job.Steps, "Publish Scorecard results") - if err != nil { - t.Fatal(err) - } - if stepIndex < 0 { - t.Fatalf("scorecard public publish job missing Scorecard action step") - } - step := job.Steps[stepIndex] - if !strings.HasPrefix(step.Uses, "ossf/scorecard-action@") { - t.Fatalf("public publish step uses %q, want ossf/scorecard-action", step.Uses) - } - if !withBool(step.With, "publish_results") { - t.Fatalf("public publish step must set publish_results=true") - } - if got := withString(step.With, "results_file"); got != "scorecard-public-results.json" { - t.Fatalf("public publish results_file=%q, want scorecard-public-results.json", got) + cases := []struct { + name string + mutate func(*githubWorkflow) + }{ + { + name: "wrong platform runner", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["platform-smoke"] + job.RunsOn = "ubuntu-24.04" + workflow.Jobs["platform-smoke"] = job + }, + }, + { + name: "no-op replaces platform smoke", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["platform-smoke"] + job.Steps = []githubStep{{ + Name: "No-op", + Run: "true", + }} + workflow.Jobs["platform-smoke"] = job + }, + }, + { + name: "platform smoke command neutralized", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["platform-smoke"] + index, err := uniqueStepIndex(job.Steps, "Run platform smoke") + if err != nil || index < 0 { + t.Fatalf("locate platform smoke: index=%d err=%v", index, err) + } + job.Steps[index].Run = "true" + workflow.Jobs["platform-smoke"] = job + }, + }, + { + name: "reusable job substitution", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["platform-smoke"] + job.Steps = nil + job.Uses = "example/ci/.github/workflows/no-op.yml@0123456789012345678901234567890123456789" + workflow.Jobs["platform-smoke"] = job + }, + }, + { + name: "provider check name drift", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["platform-smoke"] + job.Name = "quality / platform smoke" + workflow.Jobs["platform-smoke"] = job + }, + }, + { + name: "non-string runner label", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["platform-smoke"] + job.RunsOn = []any{"macos-15", false} + workflow.Jobs["platform-smoke"] = job + }, + }, + { + name: "inserted semantic shadow step", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["platform-smoke"] + job.Steps = append([]githubStep{{ + Name: "Shadow platform smoke", + Run: "npm pkg set scripts.platform:smoke=true", + }}, job.Steps...) + workflow.Jobs["platform-smoke"] = job + }, + }, + { + name: "dual uses and whitespace run", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["platform-smoke"] + job.Steps[0].Run = " " + job.Steps[0].runPresent = true + workflow.Jobs["platform-smoke"] = job + }, + }, + { + name: "explicit null run on uses step", + mutate: func(workflow *githubWorkflow) { + job := workflow.Jobs["platform-smoke"] + job.Steps[0].runPresent = true + workflow.Jobs["platform-smoke"] = job + }, + }, } - if got := withString(step.With, "results_format"); got != "json" { - t.Fatalf("public publish results_format=%q, want json", got) + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + workflow := cloneWorkflow(t, base) + test.mutate(&workflow) + if err := validateCIRequiredAggregate(workflow); err == nil { + t.Fatal("platform-smoke substitution was admitted") + } + }) } + t.Run("package script owner neutralized", func(t *testing.T) { + var manifest map[string]any + if err := json.Unmarshal(packageManifest, &manifest); err != nil { + t.Fatal(err) + } + scripts, ok := manifest["scripts"].(map[string]any) + if !ok { + t.Fatal("package scripts are not an object") + } + scripts["platform:smoke"] = "true" + mutated, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + if err := validatePlatformSmokeOwnerScript(mutated); err == nil { + t.Fatal("neutralized platform-smoke package script was admitted") + } + }) + t.Run("local setup action shadows package script", func(t *testing.T) { + owner := string(setupAction) + mutated := strings.Replace( + owner, + ` echo "$bin_dir" >> "$GITHUB_PATH"`+"\n", + ` echo "$bin_dir" >> "$GITHUB_PATH"`+"\n\n"+ + " - name: Shadow platform smoke\n"+ + " shell: bash\n"+ + " run: npm pkg set scripts.platform:smoke=true\n", + 1, + ) + if mutated == owner { + t.Fatal("local setup action mutation did not change the owner") + } + var parsedAction any + if err := yaml.Unmarshal([]byte(mutated), &parsedAction); err != nil { + t.Fatalf("semantic-shadow action mutant is not valid YAML: %v", err) + } + if err := validateSetupVerifiedNPMAction([]byte(mutated)); err == nil { + t.Fatal("mutated setup-verified-npm action was admitted") + } + }) } -func readWorkflowForTest(t *testing.T, path string) githubWorkflow { - t.Helper() - raw, err := os.ReadFile(path) - if err != nil { - t.Fatalf("read workflow %s: %v", path, err) - } - var workflow githubWorkflow - if err := yaml.Unmarshal(raw, &workflow); err != nil { - t.Fatalf("parse workflow %s: %v", path, err) - } - return workflow +func TestPackageGateWorkflowOracleAcceptsOwnerCIAndReleaseWorkflows(t *testing.T) { + t.Run("ci", func(t *testing.T) { + assertPackageGateWorkflowFile( + t, + filepath.Join("..", ".github", "workflows", "ci.yml"), + ciPackageGateWorkflowExpectation(), + ) + }) + t.Run("release", func(t *testing.T) { + assertPackageGateWorkflowFile( + t, + filepath.Join("..", ".github", "workflows", "release.yml"), + releasePackageGateWorkflowExpectation(), + ) + }) } -func TestWorkflowsUseExplicitHostedRunnerLabels(t *testing.T) { - for _, path := range workflowPathsForTest(t) { - t.Run(filepath.Base(path), func(t *testing.T) { - workflow := readWorkflowForTest(t, path) - for jobID, job := range workflow.Jobs { - for _, label := range runnerLabels(job.RunsOn) { - if strings.HasSuffix(label, "-latest") { - t.Fatalf("%s job %q uses floating hosted runner label %q", path, jobID, label) - } - } - } - }) +func TestWorkflowGuardExpressionsRejectNeutralization(t *testing.T) { + workflow := readWorkflowForTest(t, filepath.Join("..", ".github", "workflows", "release.yml")) + if err := validateReleaseWorkflowExpressions(workflow); err != nil { + t.Fatalf("owner release workflow expressions: %v", err) + } + expected := workflow.Jobs["release-metadata"].If + if !statusGuardExpressionAdmitted(canonicalWorkflowExpression(expected)) { + t.Fatalf("owner-reviewed release-metadata expression was rejected: %q", expected) + } + cases := []string{ + expected + " || true", + "false && (" + expected + ")", + `'` + expected + `' == '` + expected + `'`, + strings.Replace(expected, "'push'", "'p ush'", 1), + "", + } + for _, expression := range cases { + if statusGuardExpressionAdmitted(canonicalWorkflowExpression(expression)) { + t.Fatalf("neutralized or non-owner expression was admitted: %q", expression) + } } + t.Run("candidate job dynamic false", func(t *testing.T) { + mutated := cloneWorkflow(t, workflow) + job := mutated.Jobs["candidate"] + job.If = "${{ github.event_name == 'never' }}" + mutated.Jobs["candidate"] = job + if err := validateReleaseWorkflowExpressions(mutated); err == nil { + t.Fatal("dynamically false release candidate job condition was admitted") + } + }) + t.Run("candidate job explicit null", func(t *testing.T) { + mutated := cloneWorkflow(t, workflow) + job := mutated.Jobs["candidate"] + job.If = "" + job.ifPresent = true + mutated.Jobs["candidate"] = job + if err := validateReleaseWorkflowExpressions(mutated); err == nil { + t.Fatal("explicit null release candidate job condition was admitted") + } + }) + t.Run("package gate step dynamic false", func(t *testing.T) { + mutated := cloneWorkflow(t, workflow) + job := mutated.Jobs["candidate"] + index, err := uniqueStepIndex(job.Steps, "Run package gate") + if err != nil || index < 0 { + t.Fatalf("locate package gate: index=%d err=%v", index, err) + } + job.Steps[index].If = "${{ github.event_name == 'never' }}" + mutated.Jobs["candidate"] = job + if err := validateReleaseWorkflowExpressions(mutated); err == nil { + t.Fatal("dynamically false release package-gate step condition was admitted") + } + }) } -func workflowPathsForTest(t *testing.T) []string { - t.Helper() - patterns := []string{ - filepath.Join("..", ".github", "workflows", "*.yml"), - filepath.Join("..", ".github", "workflows", "*.yaml"), +func validateOptionalWorkflowRunDefaults(defaults *githubDefaults) error { + if defaults == nil { + return nil } - var paths []string - for _, pattern := range patterns { - matches, err := filepath.Glob(pattern) - if err != nil { - t.Fatalf("glob workflows with %s: %v", pattern, err) - } - paths = append(paths, matches...) + if defaults.Run == nil { + return errors.New("workflow defaults must declare only exact run defaults") } - sort.Strings(paths) - if len(paths) == 0 { - t.Fatalf("no workflow files found") + shell, ok := defaults.Run.Shell.(string) + if !ok || shell != "bash" { + return fmt.Errorf("workflow defaults run shell=%v, want exact bash", defaults.Run.Shell) } - return paths + if defaults.Run.WorkingDirectory != nil { + return errors.New("workflow defaults must not override run working-directory") + } + return nil } -func TestCIWorkflowDeclaresFailClosedRequiredAggregate(t *testing.T) { - workflow := readWorkflowForTest(t, filepath.Join("..", ".github", "workflows", "ci.yml")) - gate, ok := workflow.Jobs["ci-required-gate"] - if !ok { - t.Fatalf("ci workflow missing ci-required-gate job") +func validateJobExecutionControls(jobID string, job githubJob, allowedStepEnv map[string]map[string]any) error { + if job.ContinueOnError != nil || job.continueOnErrorPresent { + return fmt.Errorf("job %q must omit continue-on-error", jobID) } - if truthy(gate.ContinueOnError) { - t.Fatalf("ci-required-gate must not continue on errors") + if job.Defaults != nil || job.defaultsPresent { + return fmt.Errorf("job %q must not override workflow run defaults", jobID) } - if permissionWrites(gate.Permissions) { - t.Fatalf("ci-required-gate permissions grant write scopes: %#v", gate.Permissions) + if len(job.Env) != 0 { + return fmt.Errorf("job %q must not override workflow environment", jobID) } - if !usesAlwaysStatusCheck(gate.If) { - t.Fatalf("ci-required-gate if=%q, want always() so failed or skipped needs are inspected", gate.If) + if job.Environment != nil || job.environmentPresent { + return fmt.Errorf("job %q must not select a deployment environment", jobID) } - wantNeeds := []string{"browser-runtime", "platform-smoke", "source-quality"} - if got := needsList(gate.Needs); !reflect.DeepEqual(got, wantNeeds) { - t.Fatalf("ci-required-gate needs=%#v, want %#v", got, wantNeeds) + if job.Uses != "" || job.usesPresent { + return fmt.Errorf("job %q must not substitute a reusable workflow", jobID) } - if len(gate.Steps) != 1 { - t.Fatalf("ci-required-gate steps=%d, want one aggregate assertion step", len(gate.Steps)) + seenAllowedStepEnv := make(map[string]int, len(allowedStepEnv)) + for _, step := range job.Steps { + if step.ContinueOnError != nil || step.continueOnErrorPresent { + return fmt.Errorf("job %q step %q must omit continue-on-error", jobID, step.Name) + } + if step.Shell != nil || step.shellPresent || + step.TimeoutMinutes != nil || step.timeoutMinutesPresent || + step.WorkingDirectory != nil || step.workingDirectoryPresent { + return fmt.Errorf( + "job %q step %q must not override shell or working-directory; timeout-minutes is also forbidden", + jobID, + step.Name, + ) + } + expectedEnv, allowed := allowedStepEnv[step.Name] + if !equalStringAnyMaps(step.Env, expectedEnv) { + return fmt.Errorf("job %q step %q env=%v, want exact %v", jobID, step.Name, step.Env, expectedEnv) + } + if allowed { + seenAllowedStepEnv[step.Name]++ + } } - run := gate.Steps[0].Run - for _, need := range wantNeeds { - want := fmt.Sprintf(`test "${{ needs.%s.result }}" = "success"`, need) - if !strings.Contains(run, want) { - t.Fatalf("ci-required-gate run must require %s success, run=%q", need, run) + for name := range allowedStepEnv { + if seenAllowedStepEnv[name] != 1 { + return fmt.Errorf("job %q allowed env step %q count=%d, want exactly one", jobID, name, seenAllowedStepEnv[name]) } } + return nil } -func TestCIBrowserRuntimeInstallsEnginesBeforeProofAndRetainsOnlySuccessfulEvidence(t *testing.T) { - workflow := readWorkflowForTest(t, filepath.Join("..", ".github", "workflows", "ci.yml")) - job, ok := workflow.Jobs["browser-runtime"] - if !ok { - t.Fatal("ci workflow missing browser-runtime job") +func equalStringAnyMaps(left, right map[string]any) bool { + if len(left) == 0 && len(right) == 0 { + return true } - installIndex, err := uniqueStepIndex(job.Steps, "Install pinned browser engines") - if err != nil { - t.Fatal(err) + return reflect.DeepEqual(left, right) +} + +func unquotedExpression(value string) string { + var output strings.Builder + quote := rune(0) + escaped := false + for _, character := range value { + if quote != 0 { + if escaped { + escaped = false + continue + } + if character == '\\' && quote == '"' { + escaped = true + continue + } + if character == quote { + quote = 0 + } + continue + } + if character == '\'' || character == '"' { + quote = character + continue + } + output.WriteRune(character) } - proofIndex, err := uniqueStepIndex(job.Steps, "Run browser proof") - if err != nil { - t.Fatal(err) + return output.String() +} + +func singleLineRun(value string) string { + trimmed := strings.TrimSpace(value) + if strings.ContainsAny(trimmed, "\n\r") { + return trimmed } - uploadIndex, err := uniqueStepIndex(job.Steps, "Upload browser proof") + return trimmed +} + +const requiredAggregateShell = `set -euo pipefail +test "${{ needs.browser-runtime.result }}" = "success" +test "${{ needs.source-quality.result }}" = "success" +test "${{ needs.platform-smoke.result }}" = "success" +printf 'OK: all required quality checks passed\n'` + +const requiredPlatformSmokeShell = `set -euo pipefail +npm run npm:version +npm run platform:smoke +` + +func readPackageManifestForTest(t *testing.T) []byte { + t.Helper() + raw, err := os.ReadFile(filepath.Join("..", "package.json")) if err != nil { - t.Fatal(err) + t.Fatalf("read package manifest: %v", err) } - if !(installIndex < proofIndex && proofIndex < uploadIndex) { - t.Fatalf("browser runtime order install=%d proof=%d upload=%d", installIndex, proofIndex, uploadIndex) + return raw +} + +func validatePlatformSmokeOwnerScript(raw []byte) error { + var manifest struct { + Scripts map[string]string `json:"scripts"` } - if job.Steps[installIndex].Run != "npx playwright install --with-deps chromium firefox webkit" || job.Steps[proofIndex].Run != "npm run browser:check" { - t.Fatalf("browser runtime commands are not exact: install=%q proof=%q", job.Steps[installIndex].Run, job.Steps[proofIndex].Run) + if err := json.Unmarshal(raw, &manifest); err != nil { + return fmt.Errorf("parse package manifest: %w", err) } - upload := job.Steps[uploadIndex] - if usesAlwaysStatusCheck(upload.If) || upload.With["if-no-files-found"] != "error" || upload.With["path"] != "artifacts/proofkit/browser-runtime-proof.json" { - t.Fatalf("browser proof upload is not fail-closed success evidence: %#v", upload) + if actual := manifest.Scripts["platform:smoke"]; actual != requiredPlatformSmokeOwnerCommand { + return fmt.Errorf( + "package platform:smoke=%q, want exact owner command %q", + actual, + requiredPlatformSmokeOwnerCommand, + ) } + return nil } -func TestReleaseCandidateInstallsBrowserEnginesBeforePackageGate(t *testing.T) { - workflow := readWorkflowForTest(t, filepath.Join("..", ".github", "workflows", "release.yml")) - job, ok := workflow.Jobs["candidate"] - if !ok { - t.Fatal("release workflow missing candidate job") - } - installIndex, err := uniqueStepIndex(job.Steps, "Install pinned browser engines") +func readSetupVerifiedNPMActionForTest(t *testing.T) []byte { + t.Helper() + raw, err := os.ReadFile(filepath.Join("..", ".github", "actions", "setup-verified-npm", "action.yml")) if err != nil { - t.Fatal(err) + t.Fatalf("read setup-verified-npm action: %v", err) + } + return raw +} + +func validateSetupVerifiedNPMAction(raw []byte) error { + sum := sha256.Sum256(raw) + actual := fmt.Sprintf("%x", sum[:]) + if actual != setupVerifiedNPMActionSHA256 { + return fmt.Errorf( + "setup-verified-npm action digest=%s, want exact owner digest %s", + actual, + setupVerifiedNPMActionSHA256, + ) } - gateIndex, err := uniqueStepIndex(job.Steps, "Run package gate") - if err != nil { - t.Fatal(err) + return nil +} + +func validateExactPlatformSmokeSteps(job githubJob) error { + expected := []githubStep{ + { + Name: "Checkout", + Uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0", + With: map[string]any{"persist-credentials": false}, + }, + { + Name: "Setup Node", + Uses: "actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e", + With: map[string]any{ + "node-version": "24.18.0", + "package-manager-cache": false, + }, + }, + { + Name: "Install verified npm", + Uses: "./.github/actions/setup-verified-npm", + }, + { + Name: "Setup Go", + Uses: "actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e", + With: map[string]any{ + "go-version-file": "go.mod", + "cache": true, + }, + }, + { + Name: "Run platform smoke", + Run: requiredPlatformSmokeShell, + }, } - if installIndex >= gateIndex || job.Steps[installIndex].Run != "npx playwright install --with-deps chromium firefox webkit" || job.Steps[gateIndex].Run != "npm run check" { - t.Fatalf("release browser prerequisite is not fail-closed before package gate: install=%#v gate=%#v", job.Steps[installIndex], job.Steps[gateIndex]) + if len(job.Steps) != len(expected) { + return fmt.Errorf( + "ci workflow platform-smoke step count=%d, want exact %d", + len(job.Steps), + len(expected), + ) + } + for index, want := range expected { + actual := job.Steps[index] + if actual.Name != want.Name || + actual.Uses != want.Uses || + actual.Run != want.Run || + !reflect.DeepEqual(actual.With, want.With) || + actual.runPresent != (want.Run != "") || + actual.usesPresent != (want.Uses != "") || + actual.withPresent != (want.With != nil) { + return fmt.Errorf( + "ci workflow platform-smoke step %d differs from exact owner step %q", + index, + want.Name, + ) + } } + return nil } -func TestCISourceQualityInstallsPythonBeforeLifecycleTests(t *testing.T) { - workflow := readWorkflowForTest(t, filepath.Join("..", ".github", "workflows", "ci.yml")) - job, ok := workflow.Jobs["source-quality"] - if !ok { - t.Fatal("ci workflow missing source-quality job") +func validateCIRequiredAggregate(workflow githubWorkflow) error { + if workflow.Name != "ci" { + return fmt.Errorf("ci workflow name=%q, want exact ci", workflow.Name) + } + if err := validateExactWorkflowConcurrency( + "ci workflow", + workflow.Concurrency, + workflowConcurrencyExpectation{ + group: "ci-${{ github.event.pull_request.number || github.ref }}", + cancelInProgress: true, + }, + ); err != nil { + return err } - setupIndex, err := uniqueStepIndex(job.Steps, "Setup Python") - if err != nil { - t.Fatal(err) + if len(workflow.Env) != 0 { + return errors.New("ci workflow must not override workflow environment") } - if setupIndex < 0 { - t.Fatal("source-quality job missing Setup Python") + if workflow.Defaults == nil { + return errors.New("ci workflow must declare exact bash run defaults") } - testIndex, err := uniqueStepIndex(job.Steps, "Run all Go tests") - if err != nil { - t.Fatal(err) + if err := validateOptionalWorkflowRunDefaults(workflow.Defaults); err != nil { + return fmt.Errorf("ci workflow run defaults: %w", err) } - if testIndex < 0 { - t.Fatal("source-quality job missing Run all Go tests") + wantNeeds := []string{"browser-runtime", "platform-smoke", "source-quality"} + for _, jobID := range append(append([]string{}, wantNeeds...), "ci-required-gate") { + job, ok := workflow.Jobs[jobID] + if !ok { + return fmt.Errorf("ci workflow missing required job %q", jobID) + } + if err := validateJobExecutionControls(jobID, job, nil); err != nil { + return err + } } - if setupIndex >= testIndex { - t.Fatalf("Setup Python index=%d must precede Go tests index=%d", setupIndex, testIndex) + if err := validateExactWorkflowExpressions( + "ci workflow", + workflow, + map[string]string{ + "browser-runtime": "", + "platform-smoke": "", + "source-quality": "", + "ci-required-gate": "always()", + }, + map[string]map[string]string{ + "browser-runtime": { + "Upload browser failure diagnostics": "failure()", + }, + }, + ); err != nil { + return err } - setup := job.Steps[setupIndex] - if setup.Uses != "actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1" { - t.Fatalf("Setup Python uses=%q, want pinned actions/setup-python v6.3.0", setup.Uses) + exactJobTopology := map[string]struct { + name string + runner string + timeoutMinutes int + }{ + "source-quality": {name: "quality / source", runner: "ubuntu-24.04", timeoutMinutes: 15}, + "platform-smoke": {name: "quality / platform smoke / macos-15", runner: "macos-15", timeoutMinutes: 12}, + "browser-runtime": {name: "quality / browser runtime", runner: "ubuntu-24.04", timeoutMinutes: 20}, + "ci-required-gate": {name: "quality / required aggregate", runner: "ubuntu-24.04", timeoutMinutes: 5}, + } + for jobID, expected := range exactJobTopology { + job := workflow.Jobs[jobID] + if job.Name != expected.name { + return fmt.Errorf("ci workflow job %s name=%q, want exact %q", jobID, job.Name, expected.name) + } + actualRunner, stringRunner := job.RunsOn.(string) + if !stringRunner || actualRunner != expected.runner { + return fmt.Errorf("ci workflow job %s runs-on=%v, want exact scalar %q", jobID, job.RunsOn, expected.runner) + } + if !job.timeoutMinutesPresent || !reflect.DeepEqual(job.TimeoutMinutes, expected.timeoutMinutes) { + return fmt.Errorf( + "ci workflow job %s timeout-minutes=%v, want exact %d", + jobID, + job.TimeoutMinutes, + expected.timeoutMinutes, + ) + } } - if got := withString(setup.With, "python-version"); got != "3.14.6" { - t.Fatalf("Setup Python python-version=%q, want 3.14.6", got) + platformSmoke := workflow.Jobs["platform-smoke"] + if err := validateExactPlatformSmokeSteps(platformSmoke); err != nil { + return err } -} - -func withString(values map[string]any, key string) string { - if values == nil { - return "" + if err := validateExactStepInventory( + "ci workflow source-quality", + workflow.Jobs["source-quality"].Steps, + ciSourceQualityStepInventorySHA256, + ); err != nil { + return err } - value, ok := values[key].(string) - if !ok { - return "" + if err := validateExactStepInventory( + "ci workflow browser-runtime", + workflow.Jobs["browser-runtime"].Steps, + ciBrowserRuntimeStepInventorySHA256, + ); err != nil { + return err } - return value -} - -func runnerLabels(raw any) []string { - switch value := raw.(type) { - case nil: - return nil - case string: - return []string{value} - case []any: - result := make([]string, 0, len(value)) - for _, item := range value { - if text, ok := item.(string); ok { - result = append(result, text) - } - } - sort.Strings(result) - return result - default: - return nil + gate, ok := workflow.Jobs["ci-required-gate"] + if !ok { + return errors.New("ci workflow missing ci-required-gate") } -} - -func withBool(values map[string]any, key string) bool { - if values == nil { - return false + if canonicalWorkflowExpression(gate.If) != "always()" { + return fmt.Errorf("ci-required-gate if=%q, want exact always()", gate.If) } - switch value := values[key].(type) { - case bool: - return value - case string: - return normalizedExpression(value) == "true" - default: - return false + if gate.ContinueOnError != nil || gate.Defaults != nil || gate.Uses != "" { + return errors.New("ci-required-gate has a bypass field") } -} - -func truthy(raw any) bool { - switch value := raw.(type) { - case bool: - return value - case string: - return normalizedExpression(value) == "true" - default: - return false + if got := needsList(gate.Needs); !reflect.DeepEqual(got, wantNeeds) { + return fmt.Errorf("ci-required-gate needs=%v, want %v", got, wantNeeds) } -} - -func normalizedExpression(value string) string { - normalized := strings.ToLower(strings.TrimSpace(value)) - normalized = strings.TrimPrefix(normalized, "${{") - normalized = strings.TrimSuffix(normalized, "}}") - return strings.TrimSpace(normalized) -} - -func singleLineRun(value string) string { - trimmed := strings.TrimSpace(value) - if strings.ContainsAny(trimmed, "\n\r") { - return trimmed + if len(gate.Steps) != 1 { + return fmt.Errorf("ci-required-gate steps=%d, want exactly one", len(gate.Steps)) + } + step := gate.Steps[0] + if step.Name != "Verify required quality results" || + step.If != "" || + step.ContinueOnError != nil || + step.Shell != nil || + step.Uses != "" || + step.WorkingDirectory != nil || + len(step.Env) != 0 || + strings.TrimSpace(step.Run) != requiredAggregateShell { + return errors.New("ci-required-gate aggregate step differs from the exact owner-reviewed shell block") } - return trimmed + return nil } -func uniqueStepIndex(steps []githubStep, name string) (int, error) { - found := -1 - for index, step := range steps { - if step.Name != name { - continue - } - if found >= 0 { - return -1, fmt.Errorf("step %q must be unique", name) - } - found = index +func validateExactWorkflowConcurrency( + label string, + actual *githubConcurrency, + expected workflowConcurrencyExpectation, +) error { + if actual == nil || + !actual.groupPresent || + !actual.cancelInProgressPresent || + !reflect.DeepEqual(actual.Group, expected.group) || + !reflect.DeepEqual(actual.CancelInProgress, expected.cancelInProgress) { + return fmt.Errorf( + "%s concurrency=%v, want exact group=%q cancel-in-progress=%t", + label, + actual, + expected.group, + expected.cancelInProgress, + ) } - return found, nil + return nil } func TestPackageGateWorkflowOracleRejectsDisabledAndShadowedEvidence(t *testing.T) { @@ -856,6 +1759,38 @@ jobs: `, wantErr: "job \"gate\" is disabled", }, + { + name: "dynamically false job", + yaml: ` +on: + pull_request: +jobs: + gate: + if: ${{ github.event_name == 'never' }} + steps: + - name: Run package gate + run: npm run check + - name: Upload evidence + run: echo upload +`, + wantErr: "want absent or exact owner status expression", + }, + { + name: "explicit null job condition", + yaml: ` +on: + pull_request: +jobs: + gate: + if: null + steps: + - name: Run package gate + run: npm run check + - name: Upload evidence + run: echo upload +`, + wantErr: "want absent or exact owner status expression", + }, { name: "continue on error job", yaml: ` @@ -870,7 +1805,7 @@ jobs: - name: Upload evidence run: echo upload `, - wantErr: "must not continue", + wantErr: "must omit continue-on-error", }, { name: "disabled step", @@ -888,6 +1823,38 @@ jobs: `, wantErr: "step \"Run package gate\" is disabled", }, + { + name: "dynamically false gate step", + yaml: ` +on: + pull_request: +jobs: + gate: + steps: + - name: Run package gate + if: ${{ github.event_name == 'never' }} + run: npm run check + - name: Upload evidence + run: echo upload +`, + wantErr: "want absent exact condition", + }, + { + name: "explicit null gate step condition", + yaml: ` +on: + pull_request: +jobs: + gate: + steps: + - name: Run package gate + if: null + run: npm run check + - name: Upload evidence + run: echo upload +`, + wantErr: "want absent exact condition", + }, { name: "continue on error step", yaml: ` @@ -902,7 +1869,7 @@ jobs: - name: Upload evidence run: echo upload `, - wantErr: "must not continue", + wantErr: "must omit continue-on-error", }, { name: "continue on error expression variant", @@ -918,7 +1885,7 @@ jobs: - name: Upload evidence run: echo upload `, - wantErr: "must not continue", + wantErr: "must omit continue-on-error", }, { name: "wrong command contains package gate text", @@ -1032,7 +1999,7 @@ jobs: continue-on-error: true run: echo upload `, - wantErr: "required later step \"Upload evidence\" must not continue", + wantErr: "step \"Upload evidence\" must omit continue-on-error", }, { name: "later evidence always without success", @@ -1098,6 +2065,22 @@ jobs: `, wantErr: "required later step \"Upload evidence\" is disabled", }, + { + name: "dynamically false later evidence", + yaml: ` +on: + pull_request: +jobs: + gate: + steps: + - name: Run package gate + run: npm run check + - name: Upload evidence + if: ${{ github.event_name == 'never' }} + run: echo upload +`, + wantErr: "want absent exact condition", + }, { name: "unknown need", yaml: ` @@ -1140,6 +2123,155 @@ jobs: } }) } + raw, err := os.ReadFile(filepath.Join("..", ".github", "workflows", "release.yml")) + if err != nil { + t.Fatal(err) + } + for _, mutant := range []struct { + name string + replacement string + wantErr string + }{ + { + name: "release candidate semantic shadow step", + replacement: " - name: Shadow package gate\n" + + " run: npm pkg set scripts.check=true\n\n" + + " - name: Run package gate\n", + wantErr: "exact step inventory sha256", + }, + { + name: "release candidate step id", + replacement: " - name: Run package gate\n id: shadow-package-gate\n", + wantErr: "exact step inventory sha256", + }, + { + name: "release candidate timeout minutes", + replacement: " - name: Run package gate\n timeout-minutes: 1\n", + wantErr: "contains an unowned execution control", + }, + } { + t.Run(mutant.name, func(t *testing.T) { + mutated := strings.Replace( + string(raw), + " - name: Run package gate\n", + mutant.replacement, + 1, + ) + if mutated == string(raw) { + t.Fatal("release candidate mutation did not change the workflow") + } + err := validatePackageGateWorkflow([]byte(mutated), releasePackageGateWorkflowExpectation()) + if err == nil || !strings.Contains(err.Error(), mutant.wantErr) { + t.Fatalf("release candidate mutation error=%v, want %q rejection", err, mutant.wantErr) + } + }) + } + for _, mutant := range []struct { + name string + old string + replacement string + wantErr string + }{ + { + name: "release workflow name", + old: "name: release\n", + replacement: "name: shadow\n", + wantErr: "workflow name=", + }, + { + name: "release concurrency group", + old: " group: release-${{ github.ref }}\n", + replacement: " group: shadow\n", + wantErr: "concurrency=", + }, + { + name: "release concurrency cancellation", + old: " cancel-in-progress: false\n", + replacement: " cancel-in-progress: true\n", + wantErr: "concurrency=", + }, + { + name: "release candidate job timeout", + old: " timeout-minutes: 30\n", + replacement: " timeout-minutes: 1\n", + wantErr: "timeout-minutes=1", + }, + } { + t.Run(mutant.name, func(t *testing.T) { + mutated := strings.Replace(string(raw), mutant.old, mutant.replacement, 1) + if mutated == string(raw) { + t.Fatal("release owner-value mutation did not change the workflow") + } + err := validatePackageGateWorkflow([]byte(mutated), releasePackageGateWorkflowExpectation()) + if err == nil || !strings.Contains(err.Error(), mutant.wantErr) { + t.Fatalf("release owner-value mutation error=%v, want %q", err, mutant.wantErr) + } + }) + } +} + +func TestPackageGateWorkflowOracleRejectsExecutionOverrides(t *testing.T) { + expectation := packageGateWorkflowExpectation{ + label: "fixture", + jobID: "gate", + stepName: "Run package gate", + runCommand: "npm run check", + mustPrecedeStepNames: []string{"Upload evidence"}, + requiredTriggers: []workflowTriggerExpectation{{event: "pull_request"}}, + } + const base = ` +on: + pull_request: +defaults: + run: + shell: bash +jobs: + gate: + steps: + - name: Run package gate + run: npm run check + - name: Upload evidence + run: echo upload +` + cases := []struct { + name string + old string + replacement string + wantErr string + }{ + {name: "workflow shell", old: " shell: bash\n", replacement: " shell: bash {0} || true\n", wantErr: "want exact bash"}, + {name: "workflow working directory", old: " shell: bash\n", replacement: " shell: bash\n working-directory: shadow\n", wantErr: "must not override run working-directory"}, + {name: "workflow environment", old: "defaults:\n", replacement: "env:\n BASH_ENV: ./scripts/bypass.sh\ndefaults:\n", wantErr: "workflow env="}, + {name: "job expression continue on error", old: " gate:\n", replacement: " gate:\n continue-on-error: ${{ 1 == 1 }}\n", wantErr: "must omit continue-on-error"}, + {name: "job false continue on error", old: " gate:\n", replacement: " gate:\n continue-on-error: false\n", wantErr: "must omit continue-on-error"}, + {name: "job null continue on error", old: " gate:\n", replacement: " gate:\n continue-on-error: null\n", wantErr: "must omit continue-on-error"}, + {name: "job defaults", old: " gate:\n", replacement: " gate:\n defaults:\n run:\n shell: bash {0} || true\n", wantErr: "must not override workflow run defaults"}, + {name: "job null defaults", old: " gate:\n", replacement: " gate:\n defaults: null\n", wantErr: "must not override workflow run defaults"}, + {name: "job environment", old: " gate:\n", replacement: " gate:\n env:\n PATH: ./shadow\n", wantErr: "must not override workflow environment"}, + {name: "job deployment environment", old: " gate:\n", replacement: " gate:\n environment: shadow\n", wantErr: "must not select a deployment environment"}, + {name: "step expression continue on error", old: " - name: Run package gate\n", replacement: " - name: Run package gate\n continue-on-error: ${{ 1 == 1 }}\n", wantErr: "must omit continue-on-error"}, + {name: "step false continue on error", old: " - name: Run package gate\n", replacement: " - name: Run package gate\n continue-on-error: false\n", wantErr: "must omit continue-on-error"}, + {name: "step null continue on error", old: " - name: Run package gate\n", replacement: " - name: Run package gate\n continue-on-error: null\n", wantErr: "must omit continue-on-error"}, + {name: "step shell", old: " - name: Run package gate\n", replacement: " - name: Run package gate\n shell: bash {0} || true\n", wantErr: "must not override shell or working-directory"}, + {name: "step null shell", old: " - name: Run package gate\n", replacement: " - name: Run package gate\n shell: null\n", wantErr: "must not override shell or working-directory"}, + {name: "step working directory", old: " - name: Run package gate\n", replacement: " - name: Run package gate\n working-directory: shadow\n", wantErr: "must not override shell or working-directory"}, + {name: "step null working directory", old: " - name: Run package gate\n", replacement: " - name: Run package gate\n working-directory: null\n", wantErr: "must not override shell or working-directory"}, + {name: "step timeout", old: " - name: Run package gate\n", replacement: " - name: Run package gate\n timeout-minutes: 1\n", wantErr: "timeout-minutes is also forbidden"}, + {name: "step environment", old: " - name: Run package gate\n", replacement: " - name: Run package gate\n env:\n NODE_OPTIONS: --require ./scripts/bypass.cjs\n", wantErr: "step \"Run package gate\" env="}, + {name: "later step shell", old: " - name: Upload evidence\n", replacement: " - name: Upload evidence\n shell: bash {0} || true\n", wantErr: "step \"Upload evidence\" must not override shell"}, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + raw := strings.Replace(base, test.old, test.replacement, 1) + if raw == base { + t.Fatal("test mutation did not change the workflow") + } + err := validatePackageGateWorkflow([]byte(raw), expectation) + if err == nil || !strings.Contains(err.Error(), test.wantErr) { + t.Fatalf("validatePackageGateWorkflow() error=%v, want %q", err, test.wantErr) + } + }) + } } func TestPackageGateWorkflowOracleAdmitsLaterAlwaysWithSuccess(t *testing.T) { @@ -1168,6 +2300,31 @@ jobs: } } +func TestPackageGateWorkflowOracleRejectsUnusedAllowedStepEnvironment(t *testing.T) { + expectation := packageGateWorkflowExpectation{ + label: "fixture", + jobID: "gate", + stepName: "Run package gate", + runCommand: "npm run check", + allowedStepEnv: map[string]map[string]any{ + "Missing owner step": {"MODE": "candidate"}, + }, + requiredTriggers: []workflowTriggerExpectation{{event: "pull_request"}}, + } + err := validatePackageGateWorkflow([]byte(` +on: + pull_request: +jobs: + gate: + steps: + - name: Run package gate + run: npm run check +`), expectation) + if err == nil || !strings.Contains(err.Error(), "allowed env step \"Missing owner step\" count=0") { + t.Fatalf("validatePackageGateWorkflow() error=%v, want unused allowed-step environment failure", err) + } +} + func TestPackageGateWorkflowOracleRejectsMissingWorkflowPermissionFloor(t *testing.T) { expectation := packageGateWorkflowExpectation{ label: "fixture", @@ -1248,6 +2405,34 @@ jobs: } } +func TestPackageGateWorkflowOracleRejectsRequiredPriorExecutionOverride(t *testing.T) { + expectation := packageGateWorkflowExpectation{ + label: "fixture", + jobID: "gate", + stepName: "Verify release closeout", + runCommand: "npm run release:closeout", + mustFollowSteps: []workflowStepExpectation{ + {name: "Run semantic tests", runCommand: "go test ./internal/command/..."}, + }, + requiredTriggers: []workflowTriggerExpectation{{event: "pull_request"}}, + } + err := validatePackageGateWorkflow([]byte(` +on: + pull_request: +jobs: + gate: + steps: + - name: Run semantic tests + shell: bash {0} || true + run: go test ./internal/command/... + - name: Verify release closeout + run: npm run release:closeout +`), expectation) + if err == nil || !strings.Contains(err.Error(), "step \"Run semantic tests\" must not override shell") { + t.Fatalf("validatePackageGateWorkflow() error=%v, want prior-step execution override failure", err) + } +} + func TestPackageGateWorkflowOracleRejectsDuplicatePriorStepName(t *testing.T) { expectation := packageGateWorkflowExpectation{ label: "fixture", diff --git a/scripts/workflow_runtime_preconditions_test.go b/scripts/workflow_runtime_preconditions_test.go new file mode 100644 index 0000000..ded69ce --- /dev/null +++ b/scripts/workflow_runtime_preconditions_test.go @@ -0,0 +1,38 @@ +package main + +import ( + "path/filepath" + "testing" +) + +func TestCISourceQualityInstallsPythonBeforeLifecycleTests(t *testing.T) { + workflow := readWorkflowForTest(t, filepath.Join("..", ".github", "workflows", "ci.yml")) + job, ok := workflow.Jobs["source-quality"] + if !ok { + t.Fatal("ci workflow missing source-quality job") + } + setupIndex, err := uniqueStepIndex(job.Steps, "Setup Python") + if err != nil { + t.Fatal(err) + } + if setupIndex < 0 { + t.Fatal("source-quality job missing Setup Python") + } + testIndex, err := uniqueStepIndex(job.Steps, "Run all Go tests") + if err != nil { + t.Fatal(err) + } + if testIndex < 0 { + t.Fatal("source-quality job missing Run all Go tests") + } + if setupIndex >= testIndex { + t.Fatalf("Setup Python index=%d must precede Go tests index=%d", setupIndex, testIndex) + } + setup := job.Steps[setupIndex] + if setup.Uses != "actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1" { + t.Fatalf("Setup Python uses=%q, want pinned actions/setup-python v6.3.0", setup.Uses) + } + if got := withString(setup.With, "python-version"); got != "3.14.6" { + t.Fatalf("Setup Python python-version=%q, want 3.14.6", got) + } +} diff --git a/scripts/workflow_security_scanner_oracles_test.go b/scripts/workflow_security_scanner_oracles_test.go new file mode 100644 index 0000000..7b8dd6d --- /dev/null +++ b/scripts/workflow_security_scanner_oracles_test.go @@ -0,0 +1,346 @@ +package main + +import ( + "fmt" + "path/filepath" + "strings" + "testing" +) + +type securityScannerPermissionExpectation struct { + name string + path string + workflowPermissions map[string]string + advisoryJobs map[string]map[string]string + providerJobs map[string]map[string]string +} + +func TestSecurityScannerWorkflowsSeparateProviderPublicationPermissions(t *testing.T) { + cases := []securityScannerPermissionExpectation{ + { + name: "codeql", + path: filepath.Join("..", ".github", "workflows", "codeql.yml"), + workflowPermissions: map[string]string{"actions": "read", "contents": "read"}, + advisoryJobs: map[string]map[string]string{"analyze": nil}, + providerJobs: map[string]map[string]string{ + "upload-sarif": {"actions": "read", "contents": "read", "security-events": "write"}, + }, + }, + { + name: "osv", + path: filepath.Join("..", ".github", "workflows", "osv-scanner.yml"), + workflowPermissions: map[string]string{"actions": "read", "contents": "read"}, + advisoryJobs: map[string]map[string]string{"scan": nil}, + providerJobs: map[string]map[string]string{ + "upload-sarif": {"actions": "read", "contents": "read", "security-events": "write"}, + }, + }, + { + name: "scorecard", + path: filepath.Join("..", ".github", "workflows", "scorecard.yml"), + workflowPermissions: map[string]string{}, + advisoryJobs: map[string]map[string]string{ + "scorecard": {"checks": "read", "contents": "read", "issues": "read", "pull-requests": "read"}, + }, + providerJobs: map[string]map[string]string{ + "upload-sarif": {"actions": "read", "contents": "read", "security-events": "write"}, + }, + }, + } + for _, item := range cases { + t.Run(item.name, func(t *testing.T) { + workflow := readWorkflowForTest(t, item.path) + if err := validateSecurityScannerPermissionSeparation(workflow, item); err != nil { + t.Fatalf("owner scanner permissions: %v", err) + } + + missingWorkflowPermissions := cloneWorkflow(t, workflow) + missingWorkflowPermissions.Permissions = nil + if err := validateSecurityScannerPermissionSeparation(missingWorkflowPermissions, item); err == nil { + t.Fatal("scanner permission oracle admitted missing workflow permission floor") + } + + advisoryWrite := cloneWorkflow(t, workflow) + for jobID := range item.advisoryJobs { + job := advisoryWrite.Jobs[jobID] + job.Permissions = map[string]any{"contents": "write"} + advisoryWrite.Jobs[jobID] = job + break + } + if err := validateSecurityScannerPermissionSeparation(advisoryWrite, item); err == nil { + t.Fatal("scanner permission oracle admitted advisory write authority") + } + + providerSurplus := cloneWorkflow(t, workflow) + for jobID := range item.providerJobs { + job := providerSurplus.Jobs[jobID] + permissions := job.Permissions.(map[string]any) + permissions["contents"] = "write" + job.Permissions = permissions + providerSurplus.Jobs[jobID] = job + break + } + if err := validateSecurityScannerPermissionSeparation(providerSurplus, item); err == nil { + t.Fatal("scanner permission oracle admitted surplus provider write authority") + } + + unclassifiedWriteJob := cloneWorkflow(t, workflow) + unclassifiedWriteJob.Jobs["unclassified-write"] = githubJob{ + Permissions: map[string]any{"contents": "write"}, + } + if err := validateSecurityScannerPermissionSeparation(unclassifiedWriteJob, item); err == nil { + t.Fatal("scanner permission oracle admitted an unclassified job") + } + }) + } +} + +func validateSecurityScannerPermissionSeparation( + workflow githubWorkflow, + expectation securityScannerPermissionExpectation, +) error { + if !permissionSetEquals(workflow.Permissions, expectation.workflowPermissions) { + return fmt.Errorf( + "%s workflow permissions=%#v, want exact %#v", + expectation.path, + workflow.Permissions, + expectation.workflowPermissions, + ) + } + expectedJobCount := len(expectation.advisoryJobs) + len(expectation.providerJobs) + if len(workflow.Jobs) != expectedJobCount { + return fmt.Errorf( + "%s jobs=%d, want exact advisory/provider inventory of %d", + expectation.path, + len(workflow.Jobs), + expectedJobCount, + ) + } + for jobID, permissions := range expectation.advisoryJobs { + job, ok := workflow.Jobs[jobID] + if !ok { + return fmt.Errorf("%s missing advisory job %q", expectation.path, jobID) + } + if permissions == nil { + if job.Permissions != nil { + return fmt.Errorf("%s advisory job %q must inherit the exact workflow permission floor", expectation.path, jobID) + } + continue + } + if !permissionSetEquals(job.Permissions, permissions) { + return fmt.Errorf( + "%s advisory job %q permissions=%#v, want exact %#v", + expectation.path, + jobID, + job.Permissions, + permissions, + ) + } + } + for jobID, permissions := range expectation.providerJobs { + job, ok := workflow.Jobs[jobID] + if !ok { + return fmt.Errorf("%s missing provider job %q", expectation.path, jobID) + } + if expectation.name == "codeql" || expectation.name == "osv" { + if !providerUploadDisabledOnPullRequest(job.If) { + return fmt.Errorf( + "%s provider job %q must not upload provider evidence on pull_request: if=%q", + expectation.path, + jobID, + job.If, + ) + } + } + if !permissionSetEquals(job.Permissions, permissions) { + return fmt.Errorf( + "%s provider job %q permissions=%#v, want exact %#v", + expectation.path, + jobID, + job.Permissions, + permissions, + ) + } + } + return nil +} + +func permissionSetEquals(raw any, want map[string]string) bool { + record, ok := raw.(map[string]any) + if !ok || len(record) != len(want) { + return false + } + for key, value := range want { + actual, ok := record[key].(string) + if !ok || !strings.EqualFold(strings.TrimSpace(actual), value) { + return false + } + } + return true +} + +func providerUploadDisabledOnPullRequest(expression string) bool { + return canonicalWorkflowExpression(expression) == + "github.event_name!='pull_request'&&(github.event.repository.private==false||vars.enable_code_scanning_upload=='true')" +} + +func TestScorecardPublicPublishDeclaresRequiredOutputInputs(t *testing.T) { + workflow := readWorkflowForTest(t, filepath.Join("..", ".github", "workflows", "scorecard-publish.yml")) + if !permissionSetEquals(workflow.Permissions, map[string]string{}) { + t.Fatalf("scorecard publish workflow permissions=%#v, want exact empty map", workflow.Permissions) + } + if len(workflow.Jobs) != 1 { + t.Fatalf("scorecard publish workflow must contain exactly one job, got %d", len(workflow.Jobs)) + } + job, ok := workflow.Jobs["scorecard"] + if !ok { + t.Fatalf("scorecard workflow missing public publish job") + } + wantPermissions := map[string]string{ + "checks": "read", + "contents": "read", + "id-token": "write", + "issues": "read", + "pull-requests": "read", + } + if !permissionSetEquals(job.Permissions, wantPermissions) { + t.Fatalf("scorecard public publish permissions=%#v, want exact %#v", job.Permissions, wantPermissions) + } + stepIndex, err := uniqueStepIndex(job.Steps, "Publish Scorecard results") + if err != nil { + t.Fatal(err) + } + if stepIndex < 0 { + t.Fatalf("scorecard public publish job missing Scorecard action step") + } + step := job.Steps[stepIndex] + if !isScorecardActionReference(step.Uses) { + t.Fatalf("public publish step uses %q, want ossf/scorecard-action", step.Uses) + } + if !scorecardActionBoundaryIsExact(job.Steps, stepIndex) { + t.Fatalf("scorecard public publish boundary is not exact at step %d", stepIndex) + } + + missingWorkflowPermissions := cloneWorkflow(t, workflow) + missingWorkflowPermissions.Permissions = nil + if permissionSetEquals(missingWorkflowPermissions.Permissions, map[string]string{}) { + t.Fatal("scorecard publish permission oracle admitted a missing workflow permission floor") + } + surplusProviderPermission := cloneWorkflow(t, workflow) + surplusJob := surplusProviderPermission.Jobs["scorecard"] + surplusPermissions := surplusJob.Permissions.(map[string]any) + surplusPermissions["actions"] = "write" + surplusJob.Permissions = surplusPermissions + surplusProviderPermission.Jobs["scorecard"] = surplusJob + if permissionSetEquals(surplusJob.Permissions, wantPermissions) { + t.Fatal("scorecard publish permission oracle admitted surplus provider write authority") + } + surplusOutputInput := cloneWorkflow(t, workflow) + surplusOutputInputJob := surplusOutputInput.Jobs["scorecard"] + surplusOutputInputStep := surplusOutputInputJob.Steps[stepIndex] + surplusOutputInputStep.With["repo_token"] = "${{ github.token }}" + surplusOutputInputJob.Steps[stepIndex] = surplusOutputInputStep + surplusOutputInput.Jobs["scorecard"] = surplusOutputInputJob + if scorecardActionBoundaryIsExact(surplusOutputInputJob.Steps, stepIndex) { + t.Fatal("scorecard publish input oracle admitted a surplus authority-bearing input") + } + substitutedOutputValue := cloneWorkflow(t, workflow) + substitutedOutputValueJob := substitutedOutputValue.Jobs["scorecard"] + substitutedOutputValueStep := substitutedOutputValueJob.Steps[stepIndex] + substitutedOutputValueStep.With["publish_results"] = "true }}" + substitutedOutputValueJob.Steps[stepIndex] = substitutedOutputValueStep + substitutedOutputValue.Jobs["scorecard"] = substitutedOutputValueJob + if scorecardActionBoundaryIsExact(substitutedOutputValueJob.Steps, stepIndex) { + t.Fatal("scorecard publish input oracle admitted a substituted boolean value") + } + secondScorecardAction := cloneWorkflow(t, workflow) + secondScorecardActionJob := secondScorecardAction.Jobs["scorecard"] + secondScorecardActionJob.Steps = append(secondScorecardActionJob.Steps, githubStep{ + Name: "Unexpected second Scorecard action", + Uses: step.Uses, + With: map[string]any{ + "publish_results": true, + "repo_token": "${{ github.token }}", + "results_file": "scorecard-public-results.json", + "results_format": "json", + }, + }) + secondScorecardAction.Jobs["scorecard"] = secondScorecardActionJob + if scorecardActionBoundaryIsExact(secondScorecardActionJob.Steps, stepIndex) { + t.Fatal("scorecard publish oracle admitted a second Scorecard action") + } + _, scorecardRef, _ := strings.Cut(step.Uses, "@") + caseVariantScorecardAction := cloneWorkflow(t, workflow) + caseVariantScorecardActionJob := caseVariantScorecardAction.Jobs["scorecard"] + caseVariantScorecardActionJob.Steps = append(caseVariantScorecardActionJob.Steps, githubStep{ + Name: "Unexpected case-variant Scorecard action", + Uses: "OSSF/Scorecard-Action@" + scorecardRef, + With: map[string]any{ + "publish_results": true, + "repo_token": "${{ github.token }}", + "results_file": "scorecard-public-results.json", + "results_format": "json", + }, + }) + caseVariantScorecardAction.Jobs["scorecard"] = caseVariantScorecardActionJob + if scorecardActionBoundaryIsExact(caseVariantScorecardActionJob.Steps, stepIndex) { + t.Fatal("scorecard publish oracle admitted a case-variant second Scorecard action") + } + for _, otherAction := range []string{ + "ossf/scorecard-action-extra@" + scorecardRef, + "ossf/scorecard-action/subpath@" + scorecardRef, + "ossf/scorecard-action", + "ossf/scorecard-action@", + "ossf/scorecard-action@@" + scorecardRef, + "o\u017f\u017ff/\u017fcorecard-action@" + scorecardRef, + } { + if isScorecardActionReference(otherAction) { + t.Fatalf("scorecard action classifier admitted distinct or malformed reference %q", otherAction) + } + } +} + +func scorecardActionBoundaryIsExact(steps []githubStep, selectedIndex int) bool { + if selectedIndex < 0 || selectedIndex >= len(steps) { + return false + } + scorecardActionCount := 0 + scorecardActionIndex := -1 + for index, step := range steps { + if isScorecardActionReference(step.Uses) { + scorecardActionCount++ + scorecardActionIndex = index + } + } + return scorecardActionCount == 1 && + scorecardActionIndex == selectedIndex && + scorecardOutputInputsEqual(steps[selectedIndex].With) +} + +func isScorecardActionReference(value string) bool { + repository, ref, found := strings.Cut(value, "@") + return found && + repository != "" && + ref != "" && + !strings.Contains(ref, "@") && + isASCII(repository) && + strings.EqualFold(repository, "ossf/scorecard-action") +} + +func isASCII(value string) bool { + for index := 0; index < len(value); index++ { + if value[index] > 0x7f { + return false + } + } + return true +} + +func scorecardOutputInputsEqual(values map[string]any) bool { + publishResults, publishResultsIsBool := values["publish_results"].(bool) + return len(values) == 3 && + publishResultsIsBool && + publishResults && + withString(values, "results_file") == "scorecard-public-results.json" && + withString(values, "results_format") == "json" +} diff --git a/scripts/workflow_source_oracles_test.go b/scripts/workflow_source_oracles_test.go new file mode 100644 index 0000000..ac9adca --- /dev/null +++ b/scripts/workflow_source_oracles_test.go @@ -0,0 +1,437 @@ +package main + +import ( + "crypto/sha256" + "errors" + "fmt" + "os" + "path/filepath" + "reflect" + "regexp" + "sort" + "strings" + "testing" + + "go.yaml.in/yaml/v3" +) + +func TestWorkflowsUseExplicitHostedRunnerLabels(t *testing.T) { + for _, path := range workflowPathsForTest(t) { + t.Run(filepath.Base(path), func(t *testing.T) { + workflow := readWorkflowForTest(t, path) + for jobID, job := range workflow.Jobs { + for _, label := range runnerLabels(job.RunsOn) { + if strings.HasSuffix(label, "-latest") { + t.Fatalf("%s job %q uses floating hosted runner label %q", path, jobID, label) + } + } + } + }) + } +} + +func TestWorkflowClosedKeyAdmission(t *testing.T) { + paths := workflowPathsForTest(t) + actualPaths := make([]string, 0, len(paths)) + for _, path := range paths { + relative := strings.TrimPrefix(filepath.ToSlash(path), "../") + actualPaths = append(actualPaths, relative) + raw, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + if err := validateWorkflowClosedKeys(path, raw); err != nil { + t.Fatalf("owner workflow %s: %v", path, err) + } + } + if !reflect.DeepEqual(actualPaths, admittedWorkflowPaths) { + t.Fatalf("closed-key workflow inventory=%v, want exact %v", actualPaths, admittedWorkflowPaths) + } + + ciPath := filepath.Join("..", ".github", "workflows", "ci.yml") + ciRaw := readWorkflowBytesForTest(t, ciPath) + for _, mutant := range []struct { + name string + anchor string + insertion string + }{ + {name: "workflow run-name", anchor: "name: ci\n", insertion: "run-name: shadow\n"}, + {name: "workflow unknown key", anchor: "name: ci\n", insertion: "proofkit-shadow: true\n"}, + {name: "workflow duplicate key", anchor: "name: ci\n", insertion: "name: shadow\n"}, + {name: "workflow merge key", anchor: "name: ci\n", insertion: "<<: {}\n"}, + {name: "jobs duplicate key", anchor: " source-quality:\n", insertion: " steps: []\n source-quality:\n"}, + {name: "jobs merge key", anchor: "jobs:\n", insertion: " <<: {}\n"}, + {name: "jobs non-string key", anchor: "jobs:\n", insertion: " ? [shadow]\n : {steps: []}\n"}, + } { + t.Run(mutant.name, func(t *testing.T) { + assertClosedWorkflowMutationRejected(t, ciPath, ciRaw, mutant.anchor, mutant.anchor+mutant.insertion) + }) + } + + jobMutants := []struct { + name string + insertion string + }{ + {name: "container", insertion: " container: attacker.example/proofkit-shadow:latest\n"}, + {name: "strategy", insertion: " strategy:\n fail-fast: false\n"}, + {name: "services", insertion: " services: {}\n"}, + {name: "outputs", insertion: " outputs: {}\n"}, + {name: "concurrency", insertion: " concurrency: shadow\n"}, + {name: "environment", insertion: " environment: shadow\n"}, + {name: "defaults", insertion: " defaults:\n run:\n shell: bash\n"}, + {name: "continue-on-error", insertion: " continue-on-error: false\n"}, + {name: "reusable uses", insertion: " uses: ./.github/workflows/shadow.yml\n"}, + {name: "reusable with", insertion: " with: {}\n"}, + {name: "reusable secrets", insertion: " secrets: inherit\n"}, + {name: "duplicate", insertion: " name: shadow\n"}, + {name: "merge", insertion: " <<: {}\n"}, + {name: "unknown", insertion: " proofkit-shadow: true\n"}, + } + for _, job := range []struct { + name string + anchor string + }{ + {name: "producer", anchor: " source-quality:\n"}, + {name: "aggregate", anchor: " ci-required-gate:\n"}, + } { + for _, mutant := range jobMutants { + t.Run(job.name+" "+mutant.name, func(t *testing.T) { + assertClosedWorkflowMutationRejected( + t, + ciPath, + ciRaw, + job.anchor, + job.anchor+mutant.insertion, + ) + }) + } + } + + stepMutants := []struct { + name string + insertion string + }{ + {name: "continue-on-error", insertion: " continue-on-error: false\n"}, + {name: "shell", insertion: " shell: bash {0} || true\n"}, + {name: "timeout-minutes", insertion: " timeout-minutes: 1\n"}, + {name: "working-directory", insertion: " working-directory: shadow\n"}, + {name: "duplicate", insertion: " name: shadow\n"}, + {name: "merge", insertion: " <<: {}\n"}, + {name: "unknown", insertion: " proofkit-shadow: true\n"}, + } + for _, step := range []struct { + name string + anchor string + }{ + {name: "producer", anchor: " - name: Checkout\n"}, + {name: "aggregate", anchor: " - name: Verify required quality results\n"}, + } { + for _, mutant := range stepMutants { + t.Run(step.name+" step "+mutant.name, func(t *testing.T) { + assertClosedWorkflowMutationRejected( + t, + ciPath, + ciRaw, + step.anchor, + step.anchor+mutant.insertion, + ) + }) + } + } + + releasePath := filepath.Join("..", ".github", "workflows", "release.yml") + releaseRaw := readWorkflowBytesForTest(t, releasePath) + for _, mutant := range []struct { + name string + old string + replacement string + }{ + { + name: "environment on unowned job", + old: " candidate:\n", + replacement: " candidate:\n environment: shadow\n", + }, + { + name: "publish environment wrong scalar", + old: " environment: npm-production\n", + replacement: " environment: shadow\n", + }, + { + name: "publish environment wrong kind", + old: " environment: npm-production\n", + replacement: " environment:\n name: npm-production\n", + }, + { + name: "pypi environment wrong name", + old: " name: pypi\n", + replacement: " name: shadow\n", + }, + { + name: "pypi environment wrong url", + old: " url: https://pypi.org/p/agentic-proofkit\n", + replacement: " url: https://example.invalid/shadow\n", + }, + { + name: "pypi environment surplus", + old: " url: https://pypi.org/p/agentic-proofkit\n", + replacement: " url: https://pypi.org/p/agentic-proofkit\n proofkit-shadow: true\n", + }, + { + name: "publish environment missing", + old: " environment: npm-production\n", + replacement: "", + }, + } { + t.Run(mutant.name, func(t *testing.T) { + assertClosedWorkflowMutationRejected(t, releasePath, releaseRaw, mutant.old, mutant.replacement) + }) + } +} + +func readWorkflowBytesForTest(t *testing.T, path string) string { + t.Helper() + raw, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + return string(raw) +} + +func assertClosedWorkflowMutationRejected( + t *testing.T, + path string, + owner string, + old string, + replacement string, +) { + t.Helper() + mutated := strings.Replace(owner, old, replacement, 1) + if mutated == owner { + t.Fatalf("workflow mutation did not change %s", path) + } + if err := validateWorkflowClosedKeys(path, []byte(mutated)); err == nil { + t.Fatalf("closed-key workflow oracle admitted mutation in %s", path) + } +} + +func workflowPathsForTest(t *testing.T) []string { + t.Helper() + patterns := []string{ + filepath.Join("..", ".github", "workflows", "*.yml"), + filepath.Join("..", ".github", "workflows", "*.yaml"), + } + var paths []string + for _, pattern := range patterns { + matches, err := filepath.Glob(pattern) + if err != nil { + t.Fatalf("glob workflows with %s: %v", pattern, err) + } + paths = append(paths, matches...) + } + sort.Strings(paths) + if len(paths) == 0 { + t.Fatalf("no workflow files found") + } + return paths +} + +func TestWorkflowExternalActionsUseFullCommitSHAs(t *testing.T) { + if err := validateWorkflowActionPins(filepath.Join("..")); err != nil { + t.Fatal(err) + } + base := readWorkflowForTest(t, filepath.Join("..", ".github", "workflows", "ci.yml")) + for _, reference := range []string{"actions/checkout@v7", "actions/checkout@9c091bb"} { + t.Run(reference, func(t *testing.T) { + workflow := cloneWorkflow(t, base) + job := workflow.Jobs["source-quality"] + job.Steps[0].Uses = reference + workflow.Jobs["source-quality"] = job + if err := validateWorkflowActionReferences(workflow); err == nil { + t.Fatalf("unpinned action reference %q was admitted", reference) + } + }) + } + for _, reference := range []string{ + "./scripts/shadow-action", + "./.github/actions/../shadow-action", + `.\\.github\\actions\\shadow-action`, + } { + t.Run(reference, func(t *testing.T) { + if err := validateActionReference(reference); err == nil { + t.Fatalf("escaping local action reference %q was admitted", reference) + } + }) + } +} + +func TestExistingReleasePathIsReadOnlyAndFailsOnDrift(t *testing.T) { + workflow := readWorkflowForTest(t, filepath.Join("..", ".github", "workflows", "release.yml")) + job := workflow.Jobs["release-assets"] + index, err := uniqueStepIndex(job.Steps, "Create GitHub Release") + if err != nil || index < 0 { + t.Fatalf("locate release step: index=%d err=%v", index, err) + } + run := job.Steps[index].Run + if err := validateExistingReleasePath(run); err != nil { + t.Fatalf("owner existing-release path: %v", err) + } + cases := []struct { + name string + mutate func(string) string + }{ + {name: "upload", mutate: func(value string) string { + return strings.Replace(value, "gh release download", "gh release upload", 1) + }}, + {name: "edit", mutate: func(value string) string { return strings.Replace(value, "gh release view", "gh release edit", 1) }}, + {name: "delete", mutate: func(value string) string { return strings.Replace(value, "gh release view", "gh release delete", 1) }}, + {name: "api", mutate: func(value string) string { return strings.Replace(value, "gh release view", "gh api", 1) }}, + {name: "curl", mutate: func(value string) string { + return strings.Replace(value, "gh release view", "curl https://example.invalid", 1) + }}, + {name: "indirection", mutate: func(value string) string { + return strings.Replace(value, "gh release view", `release_client=\"gh release view\"; $release_client`, 1) + }}, + {name: "extra command", mutate: func(value string) string { + return strings.Replace(value, "existing_dir=", "echo mutate\nexisting_dir=", 1) + }}, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + if err := validateExistingReleasePath(test.mutate(run)); err == nil { + t.Fatal("mutated existing-release path was admitted") + } + }) + } +} + +func runnerLabels(raw any) []string { + switch value := raw.(type) { + case nil: + return nil + case string: + return []string{value} + case []any: + result := make([]string, 0, len(value)) + for _, item := range value { + if text, ok := item.(string); ok { + result = append(result, text) + } + } + sort.Strings(result) + return result + default: + return nil + } +} + +var externalActionReference = regexp.MustCompile(`^[^/@[:space:]]+/[^@[:space:]]+@[0-9a-f]{40}$`) + +var admittedWorkflowPaths = []string{ + ".github/workflows/ci.yml", + ".github/workflows/codeql.yml", + ".github/workflows/osv-scanner.yml", + ".github/workflows/release.yml", + ".github/workflows/scorecard-publish.yml", + ".github/workflows/scorecard.yml", + ".github/workflows/semantic-diff.yml", +} + +func validateWorkflowActionPins(root string) error { + directory := filepath.Join(root, ".github", "workflows") + entries, err := os.ReadDir(directory) + if err != nil { + return err + } + actual := make([]string, 0, len(entries)) + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yml") { + return fmt.Errorf("unadmitted workflow entry %s", entry.Name()) + } + actual = append(actual, filepath.ToSlash(filepath.Join(".github", "workflows", entry.Name()))) + } + sort.Strings(actual) + if !reflect.DeepEqual(actual, admittedWorkflowPaths) { + return fmt.Errorf("workflow inventory=%v, want exact %v", actual, admittedWorkflowPaths) + } + for _, relative := range actual { + content, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(relative))) + if err != nil { + return err + } + var workflow githubWorkflow + if err := yaml.Unmarshal(content, &workflow); err != nil { + return fmt.Errorf("parse %s: %w", relative, err) + } + if err := validateWorkflowActionReferences(workflow); err != nil { + return fmt.Errorf("%s: %w", relative, err) + } + } + return nil +} + +func validateWorkflowActionReferences(workflow githubWorkflow) error { + for jobID, job := range workflow.Jobs { + if job.Uses != "" { + if err := validateActionReference(job.Uses); err != nil { + return fmt.Errorf("job %s: %w", jobID, err) + } + } + for index, step := range job.Steps { + if step.Uses == "" { + continue + } + if err := validateActionReference(step.Uses); err != nil { + return fmt.Errorf("job %s step %d: %w", jobID, index, err) + } + } + } + return nil +} + +func validateActionReference(reference string) error { + if strings.HasPrefix(reference, "./") { + relative := strings.TrimPrefix(reference, "./") + clean := filepath.ToSlash(filepath.Clean(filepath.FromSlash(relative))) + if clean != relative || !strings.HasPrefix(relative, ".github/actions/") || strings.Contains(reference, `\`) { + return fmt.Errorf("local action reference %q is not repository-confined", reference) + } + return nil + } + if !externalActionReference.MatchString(reference) { + return fmt.Errorf("external action reference %q must use a full lowercase 40-hex commit", reference) + } + return nil +} + +const existingReleaseReadOnlyBlockSHA256 = "8ca22124fd06580cc8d111bee06eee87cda0d4d57bdc67838e738114e3247885" + +func validateExistingReleasePath(run string) error { + startMarker := `if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then` + endMarker := "\nfi\ngh release create" + start := strings.Index(run, startMarker) + if start < 0 { + return errors.New("existing-release branch is missing") + } + endRelative := strings.Index(run[start:], endMarker) + if endRelative < 0 { + return errors.New("existing-release branch terminator is missing") + } + block := strings.TrimSpace(run[start : start+endRelative+len("\nfi")]) + for _, forbidden := range []string{ + "gh release upload", + "gh release edit", + "gh release delete", + "gh api", + "curl ", + } { + if strings.Contains(block, forbidden) { + return fmt.Errorf("existing-release branch contains mutating or alternate provider command %q", forbidden) + } + } + sum := sha256.Sum256([]byte(block)) + actual := fmt.Sprintf("%x", sum[:]) + if actual != existingReleaseReadOnlyBlockSHA256 { + return fmt.Errorf("existing-release branch digest=%s, want exact owner digest %s", actual, existingReleaseReadOnlyBlockSHA256) + } + return nil +} diff --git a/tests/browser/axe-harness.mjs b/tests/browser/axe-harness.mjs new file mode 100644 index 0000000..4edf4d4 --- /dev/null +++ b/tests/browser/axe-harness.mjs @@ -0,0 +1,75 @@ +import axeMin from "axe-core/axe.min.js"; + +export const axeDistributionSource = axeMin.source; +export const axeDistributionVersion = axeMin.version; +export const axeConfigureOptions = Object.freeze({ + allowedOrigins: Object.freeze([""]), + branding: Object.freeze({application: "playwright"}), +}); +export const axeRunOptions = Object.freeze({ + rules: Object.freeze({ + "target-size": Object.freeze({enabled: true}), + }), +}); +const attemptedContexts = new WeakSet(); +const initializedContexts = new WeakSet(); +const attemptedPages = new WeakSet(); +const completedPages = new WeakSet(); + +export async function initializeAxe(page) { + const context = page.context(); + if (attemptedContexts.has(context)) { + throw new Error("axe was already initialized for this browser context"); + } + attemptedContexts.add(context); + try { + await context.addInitScript({content: axeDistributionSource}); + } catch (error) { + attemptedContexts.delete(context); + throw error; + } + initializedContexts.add(context); +} + +export async function analyzeAxe(page) { + if (attemptedPages.has(page)) { + throw new Error("axe already analyzed this page"); + } + attemptedPages.add(page); + const frames = page.frames(); + if (frames.length !== 1 || frames[0] !== page.mainFrame()) { + throw new Error("axe analysis requires exactly the main frame"); + } + const result = await page.evaluate( + async ({configureOptions, expectedVersion, runOptions}) => { + const axe = globalThis.axe; + if (axe?.version !== expectedVersion) { + throw new Error("the pinned axe distribution was not initialized"); + } + if (typeof axe.configure !== "function" || typeof axe.run !== "function") { + throw new Error("the pinned axe distribution is incomplete"); + } + axe.configure(configureOptions); + return axe.run(globalThis.document, runOptions); + }, + { + configureOptions: axeConfigureOptions, + expectedVersion: axeDistributionVersion, + runOptions: axeRunOptions, + }, + ); + if ( + result?.testEngine?.name !== "axe-core" || + result.testEngine.version !== axeDistributionVersion + ) { + throw new Error("axe returned an unexpected test engine"); + } + completedPages.add(page); + return result; +} + +export function assertAxeTestComplete(page) { + if (!initializedContexts.has(page.context()) || !completedPages.has(page)) { + throw new Error("axe test did not initialize and analyze exactly once"); + } +} diff --git a/tests/browser/workspace.spec.mjs b/tests/browser/workspace.spec.mjs index 99a2404..522733c 100644 --- a/tests/browser/workspace.spec.mjs +++ b/tests/browser/workspace.spec.mjs @@ -1,6 +1,185 @@ -import AxeBuilder from "@axe-core/playwright"; import {expect, test} from "@playwright/test"; +import {analyzeAxe, assertAxeTestComplete, initializeAxe} from "./axe-harness.mjs"; + +const workspaceNavigationToken = "proofkit.workspace-navigation.scheduled"; + +function admittedWorkspaceURL(baseURL) { + if (typeof baseURL !== "string") throw new Error("Workspace base URL is unavailable"); + const url = new URL(baseURL); + if ( + url.protocol !== "http:" + || url.hostname !== "127.0.0.1" + || url.port === "" + || url.username !== "" + || url.password !== "" + || url.pathname !== "/" + || url.search !== "" + || url.hash !== "" + ) throw new Error("Workspace base URL is outside the admitted local origin"); + return url.href; +} + +function isWorkspaceNavigationResponse(candidate, workspaceURL, mainFrame) { + const request = candidate.request(); + return candidate.url() === workspaceURL + && request.isNavigationRequest() + && request.frame() === mainFrame; +} + +async function navigateWorkspace(page, workspaceURL, trigger, responseError) { + const controller = new AbortController(); + const mainFrame = page.mainFrame(); + const responsePromise = page.waitForResponse( + (candidate) => isWorkspaceNavigationResponse(candidate, workspaceURL, mainFrame), + {signal: controller.signal}, + ); + try { + const token = await trigger(workspaceNavigationToken); + if (token !== workspaceNavigationToken) { + throw new Error("Workspace navigation trigger token is invalid"); + } + const response = await responsePromise; + if (!response.ok()) throw new Error(responseError); + await expect( + page.getByRole("heading", {name: "browser.fixture.workspace", exact: true}), + ).toBeVisible(); + } catch (error) { + controller.abort(); + await responsePromise.catch(() => undefined); + throw error; + } +} + +async function openWorkspace(page, baseURL) { + const workspaceURL = admittedWorkspaceURL(baseURL); + await navigateWorkspace( + page, + workspaceURL, + (token) => page.evaluate(({target, value}) => { + window.setTimeout(() => window.location.assign(target), 0); + return value; + }, {target: workspaceURL, value: token}), + "Workspace navigation did not return a successful response", + ); +} + +async function reloadWorkspace(page, baseURL) { + const workspaceURL = admittedWorkspaceURL(baseURL); + await navigateWorkspace( + page, + workspaceURL, + (token) => page.evaluate((value) => { + window.setTimeout(() => window.location.reload(), 0); + return value; + }, token), + "Workspace reload did not return a successful response", + ); +} + +async function expectIdentityOrder(rows, expected) { + await expect(rows).toHaveCount(expected.length); + for (let index = 0; index < expected.length; index += 1) { + await expect(rows.nth(index)).toHaveAttribute("data-identity", expected[index]); + } +} + +function deepFreeze(value) { + if (value && typeof value === "object" && !Object.isFrozen(value)) { + Object.freeze(value); + for (const child of Object.values(value)) deepFreeze(child); + } + return value; +} + +async function executeAssertionPlan(plan, sink) { + for (const entry of plan) await sink(entry); +} + +async function expectCSS(locator, properties) { + const plan = Object.freeze(Object.entries(properties).map(([property, value]) => + Object.freeze({property, value}))); + const completed = []; + await executeAssertionPlan(plan, async ({property, value}) => { + await expect(locator).toHaveCSS(property, value); + completed.push({property, value}); + }); + expect(completed).toEqual(plan); +} + +function assertAssertionPlanFalsifiers() { + const first = Object.freeze({property: "opacity", value: "1"}); + const second = Object.freeze({property: "filter", value: "none"}); + const plan = Object.freeze([first, second]); + for (const mutant of [ + [first], + [first, second, first], + [second, first], + [first, second, Object.freeze({property: "mask-image", value: "none"})], + ]) expect(mutant).not.toEqual(plan); +} + +async function expectVisibleTable(table, caption, headers, rows) { + const viewport = table.locator(".."); + await expect(table).toBeVisible(); + const commonStyles = { + opacity: "1", + filter: "none", + "clip-path": "none", + "mask-image": "none", + "content-visibility": "visible", + zoom: "1", + "animation-name": "none", + "transition-duration": "0s", + "transition-delay": "0s", + }; + for (const element of [ + viewport, + table, + table.locator(":scope > thead"), + table.locator(":scope > thead > tr"), + table.locator(":scope > tbody"), + ]) { + await expect(element).toBeVisible(); + await expectCSS(element, commonStyles); + } + const captionLocator = table.locator(":scope > caption"); + await expect(captionLocator).toHaveText(caption); + const headerCells = table.locator(":scope > thead > tr > th"); + await expect(headerCells).toHaveText(headers); + const bodyRows = table.locator(":scope > tbody > tr"); + await expect(bodyRows).toHaveCount(rows.length); + await expect(table.locator(":scope > tbody > tr:visible")).toHaveCount(rows.length); + for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) { + const row = bodyRows.nth(rowIndex); + await expect(row).toBeVisible(); + await expectCSS(row, commonStyles); + await expect(row.locator(":scope > td")).toHaveText(rows[rowIndex]); + } + const textElements = table.locator("caption, th, td"); + const textCount = 1 + headers.length + rows.reduce((count, row) => count + row.length, 0); + await expect(textElements).toHaveCount(textCount); + await expect(table.locator("caption:visible, th:visible, td:visible")).toHaveCount(textCount); + for (let index = 0; index < textCount; index += 1) { + await expectCSS(textElements.nth(index), { + opacity: "1", + color: "rgb(23, 32, 51)", + "font-size": "16px", + "font-size-adjust": "none", + "-webkit-text-security": "none", + "text-transform": "none", + filter: "none", + "clip-path": "none", + "mask-image": "none", + "content-visibility": "visible", + zoom: "1", + "animation-name": "none", + "transition-duration": "0s", + "transition-delay": "0s", + }); + } +} + test.beforeEach(async ({browser, browserName, channel, connectOptions, launchOptions}, testInfo) => { testInfo.annotations.push({type: "proofkit.browser-engine", description: browserName}); testInfo.annotations.push({type: "proofkit.browser-version", description: browser.version()}); @@ -11,18 +190,480 @@ test.beforeEach(async ({browser, browserName, channel, connectOptions, launchOpt expect(launchOptions.executablePath).toBeUndefined(); }); -test("workspace renders admitted views and creates a keyboard-authorized handoff", async ({page}) => { +const axeTest = test.extend({ + axePage: async ({page}, use) => { + await initializeAxe(page); + try { + await use(page); + } finally { + assertAxeTestComplete(page); + } + }, +}); + +const workspaceStateMatrix = [ + { + name: "bootstrap loading", + state: "bootstrap-loading", + heading: "Loading workspace", + setup: async (page, baseURL) => { + let releaseManifest; + const barrier = new Promise((resolve) => { releaseManifest = resolve; }); + await page.route("**/api/v1/manifest", async (route) => { + await barrier; + try { + await route.continue(); + } catch { + // Releasing the barrier during page teardown may close the request. + } + }); + await openWorkspace(page, baseURL); + return () => releaseManifest(); + }, + packetState: "empty", + }, + { + name: "bootstrap failed", + state: "bootstrap-failed", + heading: "Workspace unavailable", + setup: async (page, baseURL) => { + await page.route("**/api/v1/manifest", (route) => route.fulfill({status: 503, contentType: "application/json", body: '{"error":"secret-internal-detail"}'})); + await openWorkspace(page, baseURL); + await expect(page.getByRole("alert")).toContainText("The admitted workspace could not be loaded."); + await expect(page.getByRole("alert")).not.toContainText("secret-internal-detail"); + }, + packetState: "empty", + }, + { + name: "specifications loading", + state: "specifications-loading", + heading: "Specifications", + setup: async (page, baseURL) => { + const release = await holdRoute(page, "**/api/v1/requirements"); + await openWorkspace(page, baseURL); + await expect(page.locator("body")).toHaveAttribute("data-state", "specifications-loading"); + return release; + }, + packetState: "empty", + }, + { + name: "specifications", + state: "specifications", + heading: "Specifications", + setup: async (page, baseURL) => { + await openWorkspace(page, baseURL); + }, + packetState: "empty", + }, + { + name: "specifications no match", + state: "specifications", + contentState: "no-match", + heading: "Specifications", + setup: async (page, baseURL) => { + await page.route("**/api/v1/requirements", async (route) => { + const response = await route.fetch(); + const body = await response.json(); + await route.fulfill({ + response, + json: { + ...body, + projection: { + ...body.projection, + availableRequirementCount: 0, + omittedRequirementCount: 0, + requirements: [], + selectedRequirementCount: 0, + }, + }, + }); + }); + await openWorkspace(page, baseURL); + await expect(page.locator("#workspace-content [role=status]")).toHaveText("No requirements matched the admitted query."); + }, + packetState: "empty", + }, + { + name: "diff loading", + state: "diff-loading", + heading: "Semantic diff", + setup: async (page, baseURL) => { + const release = await holdRoute(page, "**/api/v1/diff"); + await openWorkspace(page, baseURL); + await page.getByRole("button", {name: "Diff"}).click(); + await expect(page.locator("body")).toHaveAttribute("data-state", "diff-loading"); + return release; + }, + packetState: "empty", + }, + { + name: "diff", + state: "diff", + heading: "Semantic diff", + setup: async (page, baseURL) => { + await openWorkspace(page, baseURL); + await page.getByRole("button", {name: "Diff"}).click(); + }, + packetState: "empty", + }, + { + name: "graph loading", + state: "graph-loading", + heading: "Traceability graph", + setup: async (page, baseURL) => { + const release = await holdRoute(page, "**/api/v1/graph"); + await openWorkspace(page, baseURL); + await page.getByRole("button", {name: "Traceability"}).click(); + await expect(page.locator("body")).toHaveAttribute("data-state", "graph-loading"); + return release; + }, + packetState: "empty", + }, + { + name: "graph", + state: "graph", + heading: "Traceability graph", + setup: async (page, baseURL) => { + await openWorkspace(page, baseURL); + await page.getByRole("button", {name: "Traceability"}).click(); + }, + packetState: "empty", + }, + { + name: "diff unavailable", + state: "diff-unavailable", + heading: "Semantic diff", + setup: async (page, baseURL) => { + await disableOptionalViews(page); + await openWorkspace(page, baseURL); + await page.getByRole("button", {name: "Diff"}).click(); + }, + packetState: "empty", + }, + { + name: "graph unavailable", + state: "graph-unavailable", + heading: "Traceability graph", + setup: async (page, baseURL) => { + await disableOptionalViews(page); + await openWorkspace(page, baseURL); + await page.getByRole("button", {name: "Traceability"}).click(); + }, + packetState: "empty", + }, + { + name: "view request failed", + state: "view-failed", + heading: "Specifications", + setup: async (page, baseURL) => { + await page.route("**/api/v1/requirements", (route) => route.fulfill({status: 500, contentType: "application/json", body: '{"error":"secret-view-detail"}'})); + await openWorkspace(page, baseURL); + await expect(page.getByRole("alert")).toContainText("The admitted workspace view could not be loaded."); + await expect(page.getByRole("alert")).not.toContainText("secret-view-detail"); + }, + packetState: "empty", + }, + { + name: "handoff result", + state: "handoff-result", + heading: "Handoff packet", + setup: async (page, baseURL) => { + await openWorkspace(page, baseURL); + await createHandoff(page); + }, + packetState: "result", + }, + { + name: "handoff failed", + state: "handoff-failed", + heading: "Handoff packet", + setup: async (page, baseURL) => { + await page.route("**/api/v1/handoff", (route) => route.fulfill({status: 500, contentType: "application/json", body: '{"error":"secret-handoff-detail"}'})); + await openWorkspace(page, baseURL); + await createHandoff(page); + await expect(page.getByRole("alert")).toContainText("The handoff packet could not be created."); + await expect(page.getByRole("alert")).not.toContainText("secret-handoff-detail"); + }, + packetState: "failed", + }, +]; + +for (const row of workspaceStateMatrix) { + axeTest(`workspace state matrix: ${row.name}`, async ({axePage: page, baseURL}) => { + const release = await row.setup(page, baseURL); + try { + await assertAxe(page, row); + await assertReflow(page, row); + await assertRenderedContrast(page, row); + const packetRegion = page.getByRole("region", {name: "Handoff packet"}); + await expect(packetRegion).toBeVisible(); + if (row.packetState === "result") { + await expect(packetRegion.locator("pre")).toContainText('"state": "submitted"'); + } else { + await expect(packetRegion.locator("pre")).toBeEmpty(); + } + await assertHandoffPacketTabOrder(page, packetRegion); + } finally { + release?.(); + } + }); +} + +test("workspace open rejects an unsuccessful main-resource response", async ({baseURL, page}) => { + await page.route((url) => url.pathname === "/", async (route) => { + const response = await route.fetch(); + await route.fulfill({response, status: 503}); + }); + await expect(openWorkspace(page, baseURL)).rejects.toThrow( + "Workspace navigation did not return a successful response", + ); +}); + +test("workspace reload rejects an unsuccessful main-resource response", async ({baseURL, page}) => { + await openWorkspace(page, baseURL); + await page.route((url) => url.pathname === "/", async (route) => { + const response = await route.fetch(); + await route.fulfill({response, status: 503}); + }); + await expect(reloadWorkspace(page, baseURL)).rejects.toThrow( + "Workspace reload did not return a successful response", + ); +}); + +test("workspace open rejects accessible-name drift in the server-owned heading", async ({baseURL, page}) => { + const heading = "

      browser.fixture.workspace

      "; + let mutantDelivered = false; + await page.route((url) => url.pathname === "/", async (route) => { + const response = await route.fetch(); + expect(response.status()).toBe(200); + const body = await response.text(); + expect(body).toContain(heading); + const mutatedBody = body.replace(heading, "

      browser.fixture.workspace drift

      "); + expect(mutatedBody).not.toBe(body); + await route.fulfill({ + response, + body: mutatedBody, + }); + mutantDelivered = true; + }); + await expect(openWorkspace(page, baseURL)).rejects.toThrow(); + expect(mutantDelivered).toBe(true); +}); + +test("workspace navigation admits the exact base and ignores response decoys", async ({baseURL, page}) => { + const configuredURL = new URL(baseURL); + const localAuthorityError = "Workspace base URL is outside the admitted local origin"; + expect(admittedWorkspaceURL(baseURL)).toBe(configuredURL.href); + expect(() => admittedWorkspaceURL(undefined)).toThrow("Workspace base URL is unavailable"); + for (const rejectedURL of [ + `https://127.0.0.1:${configuredURL.port}/`, + `http://localhost:${configuredURL.port}/`, + "http://127.0.0.1/", + `http://user@127.0.0.1:${configuredURL.port}/`, + `http://:secret@127.0.0.1:${configuredURL.port}/`, + `http://127.0.0.1:${configuredURL.port}/workspace`, + `http://127.0.0.1:${configuredURL.port}/?query=1`, + `http://127.0.0.1:${configuredURL.port}/#fragment`, + ]) { + expect(() => admittedWorkspaceURL(rejectedURL)).toThrow(localAuthorityError); + } + await openWorkspace(page, baseURL); + const workspaceURL = admittedWorkspaceURL(baseURL); + const mainFrame = page.mainFrame(); + const childFrame = {}; + const candidate = (url, navigation, frame) => ({ + url: () => url, + request: () => ({ + isNavigationRequest: () => navigation, + frame: () => frame, + }), + }); + expect([ + isWorkspaceNavigationResponse(candidate(workspaceURL, true, mainFrame), workspaceURL, mainFrame), + isWorkspaceNavigationResponse(candidate(workspaceURL, false, mainFrame), workspaceURL, mainFrame), + isWorkspaceNavigationResponse(candidate(workspaceURL, true, childFrame), workspaceURL, mainFrame), + isWorkspaceNavigationResponse(candidate(`${workspaceURL}foreign`, true, mainFrame), workspaceURL, mainFrame), + ]).toEqual([true, false, false, false]); + let decoyDelivered = false; + await page.route((url) => url.href === workspaceURL, async (route) => { + const response = await route.fetch(); + if (route.request().isNavigationRequest()) { + await route.fulfill({response}); + return; + } + await route.fulfill({response, status: 503}); + decoyDelivered = true; + }); + await navigateWorkspace( + page, + workspaceURL, + (token) => page.evaluate(({target, value}) => { + window.setTimeout(async () => { + await window.fetch(target); + window.location.reload(); + }, 0); + return value; + }, {target: workspaceURL, value: token}), + "Workspace reload did not return a successful response", + ); + expect(decoyDelivered).toBe(true); + + const cleanupFrame = {}; + let cleanupAbortObserved = false; + let cleanupConsumptionObserved = false; + const cleanupEvents = []; + const cleanupPage = { + mainFrame: () => cleanupFrame, + waitForResponse: (_predicate, {signal}) => { + cleanupEvents.push("waiter-armed"); + let fallback; + const pending = new Promise((resolve, reject) => { + fallback = setTimeout(() => resolve({ok: () => false}), 25); + signal.addEventListener("abort", () => { + clearTimeout(fallback); + cleanupAbortObserved = true; + cleanupEvents.push("waiter-aborted"); + reject(new Error("Workspace navigation waiter aborted")); + }, {once: true}); + }); + const consume = pending.catch.bind(pending); + pending.catch = (...args) => { + cleanupConsumptionObserved = true; + cleanupEvents.push("waiter-consumed"); + return consume(...args); + }; + return pending; + }, + }; + await expect(navigateWorkspace( + cleanupPage, + workspaceURL, + async () => { + cleanupEvents.push("trigger-called"); + return "proofkit.workspace-navigation.wrong"; + }, + "Workspace navigation fallback response was admitted", + )).rejects.toThrow("Workspace navigation trigger token is invalid"); + expect(cleanupAbortObserved).toBe(true); + expect(cleanupConsumptionObserved).toBe(true); + expect(cleanupEvents).toEqual([ + "waiter-armed", + "trigger-called", + "waiter-aborted", + "waiter-consumed", + ]); +}); + +axeTest("combined axe negative control proves default and target-size sensitivity", async ({axePage: page}) => { + await page.goto("about:blank"); + await page.setContent(` +
      + +
      + `); + const result = await analyzeAxe(page); + const expectedTargetSize = { + incomplete: [], + passes: [["#unnamed"]], + violations: [["#undersized"]], + }; + const expectedButtonName = { + incomplete: [], + passes: [["#undersized"]], + violations: [["#unnamed"]], + }; + expect(axeRuleProjection(result, "target-size")).toEqual(expectedTargetSize); + expect(axeRuleProjection(result, "button-name")).toEqual(expectedButtonName); + for (const mutant of [ + [withAxeOutcome(result, "violations", "target-size", "#unnamed"), "target-size", expectedTargetSize], + [withAxeOutcome(result, "incomplete", "target-size", "#unnamed"), "target-size", expectedTargetSize], + [withAxeOutcome(result, "violations", "button-name", "#undersized"), "button-name", expectedButtonName], + [withAxeOutcome(result, "incomplete", "button-name", "#undersized"), "button-name", expectedButtonName], + ]) { + expect(axeRuleProjection(mutant[0], mutant[1])).not.toEqual(mutant[2]); + } +}); + +test("focus contrast negative control rejects an invisible focus indicator", async ({baseURL, page}) => { + await openWorkspace(page, baseURL); + await page.locator("#submit-question, [data-view]:not([disabled])").evaluateAll((controls) => { + for (const control of controls) control.style.setProperty("outline", "none", "important"); + }); + await expect(assertRenderedContrast(page, workspaceStateMatrix.find((row) => row.name === "specifications"))).rejects.toThrow(/visible positive-width outline/); +}); + +for (const mismatch of [ + {field: "requestId", value: "browser.specifications.foreign"}, + {field: "snapshotId", value: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}, +]) { + test(`a malformed current view response with mismatched ${mismatch.field} fails closed`, async ({baseURL, page}) => { + await page.route("**/api/v1/requirements", async (route) => { + const response = await route.fetch(); + const body = await response.json(); + await route.fulfill({response, json: {...body, [mismatch.field]: mismatch.value}}); + }); + await openWorkspace(page, baseURL); + await expect(page.locator("body")).toHaveAttribute("data-state", "view-failed"); + await expect(page.locator("#workspace-content")).toHaveAttribute("aria-busy", "false"); + await expect(page.getByRole("alert")).toHaveText("The admitted workspace view could not be loaded."); + await expect(page.getByRole("alert")).not.toContainText(mismatch.value); + }); +} + +test("handoff packet output never creates a zero-value keyboard stop", async ({baseURL, page}) => { + let handoffFails = false; + await page.route("**/api/v1/handoff", (route) => route.fulfill(handoffFails ? { + status: 500, + contentType: "application/json", + body: "{}", + } : { + status: 200, + contentType: "application/json", + body: '{"state":"submitted"}', + })); + const packet = page.locator("#handoff-packet"); + const expectNotFocusable = async () => { + await expect(packet).not.toHaveAttribute("tabindex", /.+/); + expect(await packet.evaluate((element) => { + element.focus(); + return element === document.activeElement; + })).toBe(false); + }; + + await openWorkspace(page, baseURL); + await expect(packet).toBeEmpty(); + await expectNotFocusable(); + + await page.getByRole("button", {name: "Select invariant"}).first().click(); + await page.getByRole("textbox", {name: "Question"}).fill("Is the handoff output still readable?"); + await page.getByRole("button", {name: "Create handoff packet"}).click(); + await expect(page.locator("body")).toHaveAttribute("data-state", "handoff-result"); + await expect(packet).toContainText('"state": "submitted"'); + await expectNotFocusable(); + + handoffFails = true; + await reloadWorkspace(page, baseURL); + await page.getByRole("button", {name: "Select invariant"}).first().click(); + await page.getByRole("textbox", {name: "Question"}).fill("Does failure add a keyboard stop?"); + await page.getByRole("button", {name: "Create handoff packet"}).click(); + await expect(page.locator("body")).toHaveAttribute("data-state", "handoff-failed"); + await expect(packet).toBeEmpty(); + await expectNotFocusable(); +}); + +test("workspace renders admitted views and creates a keyboard-authorized handoff", async ({baseURL, browserName, page}) => { + assertAssertionPlanFalsifiers(); const consoleErrors = []; page.on("console", (message) => { if (message.type() === "error") consoleErrors.push(message.text()); }); - await page.goto("/"); + await openWorkspace(page, baseURL); await expect(page.getByRole("heading", {name: "browser.fixture.workspace"})).toBeVisible(); await expect(page.locator('meta[name="proofkit-browser-capability"]')).toHaveCount(0); const workspaceAuthority = page.getByLabel("Authority boundary"); await expect(workspaceAuthority).toContainText("presentation_adapter"); - await expect(workspaceAuthority).toContainText("Baseline: unverified"); + await expect(workspaceAuthority).toContainText("Expected-digest coverage: none"); await expect(workspaceAuthority).toContainText("do not prove receipt freshness"); const requirementBoundary = page.getByLabel("Boundary for REQ-CONSUMER-001"); @@ -32,33 +673,356 @@ test("workspace renders admitted views and creates a keyboard-authorized handoff await expect(requirementBoundary).toContainText("This requirement does not approve merge"); const selectInvariant = page.getByRole("button", {name: "Select invariant"}); + const specificationsView = page.getByRole("button", {name: "Specifications"}); + const diffView = page.getByRole("button", {name: "Diff"}); + await expect(specificationsView).toHaveAttribute("aria-current", "page"); + await specificationsView.focus(); + await page.keyboard.press("Tab"); + if (browserName === "webkit") { + const question = page.getByRole("textbox", {name: "Question"}); + if (await question.evaluate((element) => element === document.activeElement)) { + // Playwright WebKit follows the macOS preference that Tab visits + // text-entry controls while Option-Tab includes all native controls. + await page.keyboard.press("Shift+Tab"); + await expect(question).not.toBeFocused(); + await specificationsView.focus(); + await page.keyboard.press("Alt+Tab"); + await expect(diffView).toBeFocused(); + await page.keyboard.press("Alt+Shift+Tab"); + await expect(specificationsView).toBeFocused(); + } else { + await expect(diffView).toBeFocused(); + await page.keyboard.press("Shift+Tab"); + await expect(specificationsView).toBeFocused(); + } + } else { + await expect(diffView).toBeFocused(); + await page.keyboard.press("Shift+Tab"); + await expect(specificationsView).toBeFocused(); + } await selectInvariant.focus(); await page.keyboard.press("Enter"); await expect(page.getByRole("status")).toContainText("1 source-bound target"); + await page.getByRole("button", {name: "Clear selection"}).click(); + await expect(page.getByRole("status")).toContainText("No source-bound text selected"); + await selectInvariant.focus(); + await page.keyboard.press("Space"); + await expect(page.getByRole("status")).toContainText("1 source-bound target"); await page.getByRole("textbox", {name: "Question"}).fill("Does retry preserve the same contract?"); await page.getByRole("button", {name: "Create handoff packet"}).click(); await expect(page.getByRole("status")).toContainText("Handoff packet created"); - await expect(page.getByLabel("Handoff packet")).toContainText('"state": "submitted"'); - await expect(page.getByLabel("Handoff packet")).toContainText("retry \u{1F680}"); + const packetRegion = page.getByRole("region", {name: "Handoff packet"}); + await expect(packetRegion).toContainText('"state": "submitted"'); + await expect(packetRegion).toContainText("retry \u{1F680}"); await page.getByRole("button", {name: "Diff"}).click(); + await expect(diffView).toHaveAttribute("aria-current", "page"); + await expect(specificationsView).not.toHaveAttribute("aria-current", "page"); await expect(page.getByRole("heading", {name: /scalar_changed/})).toBeVisible(); await expect(page.getByText(/Source digests: sha256:/)).toBeVisible(); const diffBoundary = page.locator(".projection-boundary"); await expect(diffBoundary).toContainText("lookup_fragment_only"); - await expect(diffBoundary).toContainText(/Base snapshot: sha256:.*\(unverified\)/); - await expect(diffBoundary).toContainText(/Current snapshot: sha256:.*\(unverified\)/); + await expect(diffBoundary).toContainText(/Base snapshot: sha256:.*\(expected-digest coverage: none\)/); + await expect(diffBoundary).toContainText(/Current snapshot: sha256:.*\(expected-digest coverage: none\)/); await expect(diffBoundary).toContainText("does not own requirement meaning"); - await page.getByRole("button", {name: "Traceability"}).click(); - const graph = page.getByRole("img", {name: /traceability nodes and edges/}); - await expect(graph).toBeVisible(); - const nodeIDs = (await graph.getAttribute("data-node-ids")).split(" ").filter(Boolean); - const edgeIDs = (await graph.getAttribute("data-edge-ids")).split(" ").filter(Boolean); - const tableNodeIDs = await page.locator('table[data-identity-kind="node"] tbody tr').evaluateAll((rows) => rows.map((row) => row.dataset.identity)); - const tableEdgeIDs = await page.locator('table[data-identity-kind="edge"] tbody tr').evaluateAll((rows) => rows.map((row) => row.dataset.identity)); - expect(tableNodeIDs).toEqual(nodeIDs); - expect(tableEdgeIDs).toEqual(edgeIDs); + const graphURL = new URL("/api/v1/graph", page.url()).href; + const graphRequests = []; + const graphResponses = []; + const graphInterceptions = []; + const graphCallbacks = new Set(); + const graphCallbackErrors = []; + let graphAttemptState = "armed"; + const sentinel = "c116-intercepted-node"; + const isGraphRequest = (request) => request.method() === "POST" && request.url() === graphURL; + const requestHandler = (request) => { + if (isGraphRequest(request)) graphRequests.push(request); + }; + const responseHandler = (response) => { + if (!isGraphRequest(response.request())) return; + const callback = response.json() + .then((body) => graphResponses.push({body, response})) + .catch((error) => graphCallbackErrors.push(error)); + graphCallbacks.add(callback); + }; + const routeHandler = async (route) => { + const callback = (async () => { + try { + expect(graphAttemptState).toBe("armed"); + graphAttemptState = "intercepted"; + expect(route.request().url()).toBe(graphURL); + const upstream = await route.fetch({timeout: 5000, maxRedirects: 0, maxRetries: 0}); + expect(upstream.ok()).toBe(true); + expect(upstream.url()).toBe(graphURL); + const body = await upstream.json(); + expect(JSON.stringify(body)).not.toContain(sentinel); + expect(body.projection.nodes.length).toBeGreaterThan(0); + body.projection.nodes[0] = { + ...body.projection.nodes[0], + label: `${sentinel} ${body.projection.nodes[0].label}`, + }; + graphInterceptions.push(body); + await route.fulfill({response: upstream, json: body}); + graphAttemptState = "admitted"; + } catch (error) { + try { + await route.abort("failed"); + } catch { + // The route may already have reached a terminal state. + } + throw error; + } + })(); + graphCallbacks.add(callback); + await callback; + }; + page.on("request", requestHandler); + page.on("response", responseHandler); + await page.route(graphURL, routeHandler); + try { + await page.getByRole("button", {name: "Traceability"}).click(); + await expect(page.locator("body")).toHaveAttribute("data-state", "graph"); + await expect.poll(() => graphRequests.length).toBe(1); + await expect.poll(() => graphInterceptions.length).toBe(1); + await expect.poll(() => graphResponses.length).toBe(1); + expect(graphResponses[0].response.request()).toBe(graphRequests[0]); + expect(graphResponses[0].body).toEqual(graphInterceptions[0]); + const projection = deepFreeze(structuredClone(graphResponses[0].body.projection)); + expect(Object.isFrozen(projection.nodes)).toBe(true); + const nodes = projection.nodes; + const edges = projection.edges; + const nodeIDs = nodes.map((node) => node.nodeId); + const edgeIDs = edges.map((edge) => edge.edgeId); + const graph = page.getByRole("img", {name: /traceability nodes and edges/}); + const graphViewport = page.getByRole("region", {name: "Traceability graph viewport"}); + await expect(graphViewport).toBeVisible(); + await expect(graph).toBeVisible(); + await expect(graph).toHaveAttribute("data-node-ids", nodeIDs.join(" ")); + await expect(graph).toHaveAttribute("data-edge-ids", edgeIDs.join(" ")); + const expectedHeight = Math.max(180, Math.ceil(nodes.length / 2) * 76 + 40); + await expect(graph).toHaveAttribute("viewBox", `0 0 800 ${expectedHeight}`); + await expectCSS(graphViewport, { + display: "block", + visibility: "visible", + opacity: "1", + "overflow-x": "auto", + "overflow-y": "auto", + transform: "none", + translate: "none", + rotate: "none", + scale: "none", + "offset-path": "none", + zoom: "1", + "content-visibility": "visible", + filter: "none", + "clip-path": "none", + "mask-image": "none", + "animation-name": "none", + "transition-duration": "0s", + "transition-delay": "0s", + }); + await expectCSS(graph, { + display: "block", + visibility: "visible", + opacity: "1", + "min-width": "800px", + height: /^(?:1[89]\d|[2-9]\d{2,})(?:\.\d+)?px$/, + transform: "none", + translate: "none", + rotate: "none", + scale: "none", + "offset-path": "none", + zoom: "1", + "content-visibility": "visible", + filter: "none", + "clip-path": "none", + "mask-image": "none", + "animation-name": "none", + "transition-duration": "0s", + "transition-delay": "0s", + }); + for (const ancestor of [ + page.locator("html"), + page.locator("body"), + page.locator("body > main"), + page.locator("#workspace-content"), + ]) { + await expectCSS(ancestor, { + "animation-name": "none", + "transition-duration": "0s", + "transition-delay": "0s", + }); + } + await expect(page.locator("animate, animateColor, animateMotion, animateTransform, discard, set")).toHaveCount(0); + const positions = new Map(nodes.map((node, index) => [ + node.nodeId, + {x: 28 + (index % 2) * 390, y: 28 + Math.floor(index / 2) * 76}, + ])); + const rootChildren = graph.locator(":scope > *"); + await expect(rootChildren).toHaveCount(edges.length + nodes.length); + for (let index = 0; index < edges.length; index += 1) { + const edge = edges[index]; + const from = positions.get(edge.fromNodeId); + const to = positions.get(edge.toNodeId); + expect(from).toBeDefined(); + expect(to).toBeDefined(); + const line = graph.locator(`:scope > line:nth-child(${index + 1})`); + await expect(line).toHaveAttribute("data-edge-id", edge.edgeId); + const coordinates = { + x1: String(from.x + 180), + y1: String(from.y + 24), + x2: String(to.x + 180), + y2: String(to.y + 24), + }; + expect(coordinates.x1 !== coordinates.x2 || coordinates.y1 !== coordinates.y2).toBe(true); + for (const [name, value] of Object.entries(coordinates)) { + await expect(line).toHaveAttribute(name, value); + } + await expect(line.locator(":scope > *")).toHaveCount(0); + await expectCSS(line, { + display: /^(?:inline|block)$/, + visibility: "visible", + opacity: "1", + stroke: "rgb(23, 32, 51)", + "stroke-opacity": "1", + "stroke-width": "1.5px", + "stroke-dasharray": "none", + transform: "none", + translate: "none", + rotate: "none", + scale: "none", + "offset-path": "none", + zoom: "1", + "content-visibility": "visible", + filter: "none", + "clip-path": "none", + "mask-image": "none", + "animation-name": "none", + "transition-duration": "0s", + "transition-delay": "0s", + }); + } + for (let index = 0; index < nodes.length; index += 1) { + const node = nodes[index]; + const position = positions.get(node.nodeId); + const group = graph.locator(`:scope > g:nth-child(${edges.length + index + 1})`); + await expect(group).toHaveAttribute("data-node-id", node.nodeId); + await expect(group.locator(":scope > *")).toHaveCount(3); + const title = group.locator(":scope > title:nth-child(1)"); + const rect = group.locator(":scope > rect:nth-child(2)"); + const text = group.locator(":scope > text:nth-child(3)"); + const fullLabel = `${node.evidencePlane}: ${node.label}`; + const visibleLabel = [...fullLabel].length > 48 ? `${[...fullLabel].slice(0, 47).join("")}...` : fullLabel; + await expect(title).toHaveText(fullLabel); + await expect(text).toHaveText(visibleLabel); + await expect(title.locator(":scope > *")).toHaveCount(0); + await expect(text.locator(":scope > *")).toHaveCount(0); + for (const [name, value] of Object.entries({ + x: String(position.x), + y: String(position.y), + width: "350", + height: "48", + rx: "4", + })) await expect(rect).toHaveAttribute(name, value); + await expect(rect.locator(":scope > *")).toHaveCount(0); + await expect(text).toHaveAttribute("x", String(position.x + 10)); + await expect(text).toHaveAttribute("y", String(position.y + 29)); + for (const name of ["dx", "dy", "textLength", "lengthAdjust", "rotate"]) { + await expect(text).not.toHaveAttribute(name, /(?:)/); + } + await expectCSS(rect, { + display: /^(?:inline|block)$/, + visibility: "visible", + opacity: "1", + fill: "rgb(242, 244, 247)", + "fill-opacity": "1", + stroke: "rgb(102, 112, 133)", + "stroke-opacity": "1", + x: `${position.x}px`, + y: `${position.y}px`, + width: "350px", + height: "48px", + rx: "4px", + ry: "auto", + }); + await expectCSS(text, { + display: /^(?:inline|block)$/, + visibility: "visible", + opacity: "1", + fill: "rgb(23, 32, 51)", + "fill-opacity": "1", + "font-size": "13px", + "font-size-adjust": "none", + "text-anchor": "start", + direction: "ltr", + "writing-mode": "horizontal-tb", + "dominant-baseline": "auto", + "letter-spacing": "normal", + "word-spacing": "0px", + "text-transform": "none", + "-webkit-text-security": "none", + }); + for (const element of [group, rect, text]) { + await expectCSS(element, { + display: /^(?:inline|block)$/, + visibility: "visible", + opacity: "1", + transform: "none", + translate: "none", + rotate: "none", + scale: "none", + "offset-path": "none", + zoom: "1", + "content-visibility": "visible", + filter: "none", + "clip-path": "none", + "mask-image": "none", + "animation-name": "none", + "transition-duration": "0s", + "transition-delay": "0s", + }); + } + } + const nodeRows = nodes.map((node) => [ + node.nodeId, node.kind, node.evidencePlane, node.sourceId, node.authorityClass, + node.currentnessState, node.rangeVerification, node.state, node.producerId, + ].map((value) => value ?? "")); + const edgeRows = edges.map((edge) => [ + edge.edgeId, edge.edgeKind, edge.fromNodeId, edge.toNodeId, + edge.authorityClass, edge.currentnessState, + Array.isArray(edge.evidenceRefs) ? edge.evidenceRefs.join(", ") : (edge.evidenceRefs ?? ""), + ].map((value) => value ?? "")); + const nodeTable = page.locator('table[data-identity-kind="node"]'); + const edgeTable = page.locator('table[data-identity-kind="edge"]'); + await expectVisibleTable(nodeTable, "Admitted traceability nodes", + ["Node", "Kind", "Evidence plane", "Source", "Authority", "Currentness", "Verification", "State", "Producer"], nodeRows); + await expectVisibleTable(edgeTable, "Admitted traceability edges", + ["Edge", "Kind", "From", "To", "Authority", "Currentness", "Evidence"], edgeRows); + await expectIdentityOrder(nodeTable.locator("tbody tr"), nodeIDs); + await expectIdentityOrder(edgeTable.locator("tbody tr"), edgeIDs); + await expect(graph.locator(`:scope > g:nth-child(${edges.length + 1}) > title`)).toContainText(sentinel); + } finally { + const stateBeforeCleanup = graphAttemptState; + graphAttemptState = "closing"; + page.off("request", requestHandler); + page.off("response", responseHandler); + let detachError; + try { + await page.unroute(graphURL, routeHandler); + } catch (error) { + detachError = error; + } + const callbackResults = await Promise.allSettled([...graphCallbacks]); + graphAttemptState = "detached"; + expect(stateBeforeCleanup).toBe("admitted"); + expect(detachError).toBeUndefined(); + expect(callbackResults.every((result) => result.status === "fulfilled")).toBe(true); + expect(graphCallbackErrors).toEqual([]); + expect(graphRequests).toHaveLength(1); + expect(graphInterceptions).toHaveLength(1); + expect(graphResponses).toHaveLength(1); + expect(graphAttemptState).toBe("detached"); + } const repositoryRow = page.locator('table[data-identity-kind="node"] tbody tr[data-identity="code:code.repository"]'); await expect(repositoryRow).toContainText("stale"); const rangeRow = page.locator('table[data-identity-kind="node"] tbody tr[data-identity="code:code.retry"]'); @@ -76,55 +1040,61 @@ test("workspace renders admitted views and creates a keyboard-authorized handoff await expect(traceEdgeRow).toContainText("owner_admitted"); await expect(traceEdgeRow).toContainText("current"); await expect(page.locator(".projection-boundary")).toContainText("does not infer code topology"); - await expect(graph.locator("title").filter({hasText: /deliberately long traceability label/})).toHaveCount(1); - const graphBox = await graph.boundingBox(); - expect(graphBox?.width).toBeGreaterThan(100); - expect(graphBox?.height).toBeGreaterThan(100); + await expect(page.getByRole("img", {name: /traceability nodes and edges/}).locator("title").filter({hasText: /deliberately long traceability label/})).toHaveCount(1); expect(consoleErrors).toEqual([]); - const firstEdge = graph.locator("line").first(); - await expect(firstEdge).toHaveCount(1); - expect(await firstEdge.evaluate((line) => getComputedStyle(line).stroke)).not.toBe("none"); - expect((await graph.screenshot({animations: "allow", caret: "initial"})).byteLength).toBeGreaterThan(1000); - const accessibility = await new AxeBuilder({page}).analyze(); - expect(accessibility.violations).toEqual([]); }); -test("collapsed text selection cannot retain hidden handoff authority", async ({page}) => { - await page.goto("/"); +test("collapsed text selection cannot retain hidden handoff authority", async ({baseURL, page}) => { + await openWorkspace(page, baseURL); const invariant = page.locator("[data-anchor-id]").first(); const selectionButton = page.locator("[data-select-anchor]").first(); await expect(invariant).toBeVisible(); await selectionButton.click(); await expect(selectionButton).toHaveAttribute("aria-pressed", "true"); - await setSelection(page, invariant, 0, 0); + await invariant.click(); await expect(selectionButton).toHaveAttribute("aria-pressed", "true"); - await setSelection(page, invariant, 0, 3); + await invariant.selectText(); await expect(page.getByRole("status")).toContainText("1 source-bound target"); await expect(selectionButton).toHaveAttribute("aria-pressed", "false"); - await page.evaluate(() => window.getSelection()?.collapseToStart()); + await invariant.click(); await expect(page.getByRole("status")).toContainText("No source-bound text selected"); await expect(selectionButton).toHaveAttribute("aria-pressed", "false"); await page.getByRole("textbox", {name: "Question"}).fill("Can a hidden selection be submitted?"); await page.getByRole("button", {name: "Create handoff packet"}).click(); await expect(page.getByRole("status")).toContainText("Select invariant text"); - await expect(page.getByLabel("Handoff packet")).toBeEmpty(); + await expect(page.getByRole("region", {name: "Handoff packet"}).locator("pre")).toBeEmpty(); }); -test("text selection projects Unicode code-point coordinates", async ({page}) => { - await page.goto("/"); +test("text selection projects Unicode code-point coordinates", async ({baseURL, page}) => { + await openWorkspace(page, baseURL); const invariant = page.locator("[data-anchor-id]").filter({hasText: "retry \u{1F680}"}).first(); await expect(invariant).toBeVisible(); - const offsets = await invariant.evaluate((element) => { - const text = element.firstChild?.textContent ?? ""; - const domStart = text.indexOf("\u{1F680}"); - if (domStart < 0) throw new Error("Unicode fixture is unavailable"); - return { - codePointStart: Array.from(text.slice(0, domStart)).length, - domEnd: domStart + "\u{1F680}".length, - domStart, - }; - }); - await setSelection(page, invariant, offsets.domStart, offsets.domEnd); + const invariantText = await invariant.textContent(); + if (invariantText === null) throw new Error("Unicode fixture text is unavailable"); + const domStart = invariantText.indexOf("\u{1F680}"); + if (domStart < 0) throw new Error("Unicode fixture is unavailable"); + const domEnd = domStart + "\u{1F680}".length; + const expectedStartCodePoint = Array.from(invariantText.slice(0, domStart)).length; + const expectedEndCodePoint = Array.from(invariantText.slice(0, domEnd)).length; + expect(domEnd - domStart).toBe(2); + expect(expectedStartCodePoint).toBeGreaterThan(0); + expect(expectedEndCodePoint - expectedStartCodePoint).toBe(1); + await invariant.evaluate((element, bounds) => { + const text = element.firstChild; + const selection = window.getSelection(); + if (!(text instanceof Text) || text.textContent !== bounds.expectedText || !selection) { + throw new Error("Exact Unicode selection fixture is unavailable"); + } + if (!Number.isInteger(bounds.start) || !Number.isInteger(bounds.end) || + bounds.start < 0 || bounds.end > text.length || bounds.start >= bounds.end) { + throw new Error("Exact Unicode selection bounds are invalid"); + } + const range = document.createRange(); + range.setStart(text, bounds.start); + range.setEnd(text, bounds.end); + selection.removeAllRanges(); + selection.addRange(range); + }, {end: domEnd, expectedText: invariantText, start: domStart}); await expect(page.getByRole("status")).toContainText("1 source-bound target"); await expect(page.getByLabel("Selected source text").getByRole("listitem")).toHaveText("\u{1F680}"); await page.getByRole("textbox", {name: "Question"}).fill("Does the Unicode coordinate remain source-bound?"); @@ -132,11 +1102,11 @@ test("text selection projects Unicode code-point coordinates", async ({page}) => await page.getByRole("button", {name: "Create handoff packet"}).click(); await expect(page.getByRole("status")).toContainText("Handoff packet created"); - const packet = JSON.parse(await page.getByLabel("Handoff packet").textContent()); + const packet = JSON.parse(await page.getByRole("region", {name: "Handoff packet"}).locator("pre").textContent()); const annotation = packet.annotations[0]; expect(annotation.exactQuote).toBe("\u{1F680}"); - expect(annotation.startCodePoint).toBe(offsets.codePointStart); - expect(annotation.endCodePoint).toBe(offsets.codePointStart + 1); + expect(annotation.startCodePoint).toBe(expectedStartCodePoint); + expect(annotation.endCodePoint).toBe(expectedEndCodePoint); await page.getByRole("button", {name: "Clear selection"}).click(); await expect(page.getByLabel("Selected source text").getByRole("listitem")).toHaveCount(0); await expect(page.getByRole("status")).toContainText("No source-bound text selected"); @@ -144,28 +1114,7 @@ test("text selection projects Unicode code-point coordinates", async ({page}) => await expect(page.getByRole("status")).toContainText("Select invariant text"); }); -/** @param {import("@playwright/test").Page} page @param {import("@playwright/test").Locator} locator @param {number} start @param {number} end */ -async function setSelection(page, locator, start, end) { - const anchorID = await locator.getAttribute("data-anchor-id"); - if (!anchorID) throw new Error("Invariant anchor identity is unavailable"); - await page.waitForFunction((expectedID) => [...document.querySelectorAll("[data-anchor-id]")].some((element) => element.getAttribute("data-anchor-id") === expectedID), anchorID); - await page.evaluate((bounds) => { - const element = [...document.querySelectorAll("[data-anchor-id]")].find((candidate) => candidate.getAttribute("data-anchor-id") === bounds.anchorID); - if (!element) throw new Error("Invariant anchor is unavailable"); - const text = element.firstChild; - const selection = window.getSelection(); - if (!text || !selection) throw new Error("Text selection is unavailable"); - const limit = text.textContent?.length ?? 0; - const range = document.createRange(); - range.setStart(text, Math.min(bounds.start, limit)); - range.setEnd(text, Math.min(bounds.end, limit)); - selection.removeAllRanges(); - selection.addRange(range); - document.dispatchEvent(new Event("selectionchange")); - }, {anchorID, end, start}); -} - -test("a view transition cooperatively aborts the superseded request", async ({page}) => { +test("a view transition cooperatively aborts the superseded request", async ({baseURL, page}) => { await disableOptionalViews(page); await page.addInitScript(() => { const nativeFetch = globalThis.fetch.bind(globalThis); @@ -190,7 +1139,7 @@ test("a view transition cooperatively aborts the superseded request", async ({pa } }); - await page.goto("/"); + await openWorkspace(page, baseURL); await page.waitForFunction(() => globalThis.__proofkitAbortProbe?.started === true); await page.getByRole("button", {name: "Diff"}).click(); await page.waitForFunction(() => globalThis.__proofkitAbortProbe?.aborted === true); @@ -202,7 +1151,7 @@ for (const unavailableView of [ {button: "Diff", heading: "Semantic diff"}, {button: "Traceability", heading: "Traceability graph"}, ]) { - test(`request generation rejects a non-cooperative late response after opening ${unavailableView.button}`, async ({page}) => { + test(`request generation rejects a non-cooperative late response after opening ${unavailableView.button}`, async ({baseURL, page}) => { await disableOptionalViews(page); await page.addInitScript(() => { const nativeFetch = globalThis.fetch.bind(globalThis); @@ -230,7 +1179,7 @@ for (const unavailableView of [ }; }); - await page.goto("/"); + await openWorkspace(page, baseURL); await page.waitForFunction(() => globalThis.__proofkitLateResponse?.started === true); await page.getByRole("button", {name: unavailableView.button}).click(); await expect(page.getByRole("heading", {name: unavailableView.heading})).toBeVisible(); @@ -238,8 +1187,224 @@ for (const unavailableView of [ await page.evaluate(() => globalThis.__proofkitLateResponse.release()); await page.waitForFunction(() => globalThis.__proofkitLateResponse?.consumed === true); await expect(page.getByRole("heading", {name: unavailableView.heading})).toBeVisible(); - await expect(page.locator('[role="tree"]')).toHaveCount(0); + await expect(page.getByRole("list", {name: "Specification requirements"})).toHaveCount(0); + }); +} + +for (const handoffOutcome of [ + {name: "successful", expectedStatus: "Handoff packet created."}, + {name: "failed", expectedStatus: "The handoff packet could not be created."}, +]) { + test(`a late ${handoffOutcome.name} handoff cannot replace a newer view state`, async ({baseURL, page}) => { + let releaseHandoff; + let markHandoffStarted; + const handoffReleased = new Promise((resolve) => { releaseHandoff = resolve; }); + const handoffStarted = new Promise((resolve) => { markHandoffStarted = resolve; }); + await page.route("**/api/v1/handoff", async (route) => { + const response = handoffOutcome.name === "successful" + ? await route.fetch({timeout: 5000, maxRedirects: 0, maxRetries: 0}) + : null; + markHandoffStarted(); + await handoffReleased; + if (response) { + await route.fulfill({response}); + } else { + await route.fulfill({status: 503, contentType: "application/json", body: "{}"}); + } + }); + + await openWorkspace(page, baseURL); + await page.getByRole("button", {name: "Select invariant"}).first().click(); + await page.getByRole("textbox", {name: "Question"}).fill("Does a newer view retain its state?"); + await page.getByRole("button", {name: "Create handoff packet"}).click(); + await handoffStarted; + await page.getByRole("button", {name: "Diff"}).click(); + await expect(page.locator("body")).toHaveAttribute("data-state", "diff"); + await expect(page.getByRole("button", {name: "Diff"})).toHaveAttribute("aria-current", "page"); + + releaseHandoff(); + await expect(page.getByText(handoffOutcome.expectedStatus, {exact: true})).toBeVisible(); + if (handoffOutcome.name === "successful") { + await expect(page.getByRole("region", {name: "Handoff packet"}).locator("pre")).toContainText('"state": "submitted"'); + } + await expect(page.locator("body")).toHaveAttribute("data-state", "diff"); + await expect(page.getByRole("button", {name: "Diff"})).toHaveAttribute("aria-current", "page"); + }); +} + +async function createHandoff(page) { + await page.getByRole("button", {name: "Select invariant"}).first().click(); + await page.getByRole("textbox", {name: "Question"}).fill("Does the admitted contract remain source-bound?"); + await page.getByRole("button", {name: "Create handoff packet"}).click(); +} + +async function holdRoute(page, pattern) { + let release; + const barrier = new Promise((resolve) => { release = resolve; }); + await page.route(pattern, async (route) => { + await barrier; + try { + await route.continue(); + } catch { + // Releasing the barrier during page teardown may close the request. + } + }); + return () => release(); +} + +async function assertHandoffPacketTabOrder(page, packetRegion) { + const packet = packetRegion.locator("pre"); + expect(await packet.getAttribute("tabindex")).toBeNull(); + expect(await packet.evaluate((element) => element.tabIndex)).toBe(-1); + const submit = page.getByRole("button", {name: "Create handoff packet"}); + await submit.focus(); + await page.keyboard.press("Tab"); + await expect(packet).not.toBeFocused(); + await submit.focus(); + await page.keyboard.press("Shift+Tab"); + await expect(packet).not.toBeFocused(); +} + +async function assertWorkspaceState(page, row) { + await expect(page.locator("body")).toHaveAttribute("data-state", row.state); + await expect(page.getByRole("heading", {name: row.heading, exact: true})).toBeVisible(); + if (row.contentState) { + await expect(page.locator("#workspace-content [role=status]")).toHaveAttribute("data-state", row.contentState); + } +} + +async function assertAxe(page, row) { + await assertWorkspaceState(page, row); + const result = await analyzeAxe(page); + const applicable = [...result.passes, ...result.violations, ...result.incomplete].filter((entry) => entry.id === "target-size"); + expect(result.violations).toEqual([]); + expect(applicable.length).toBeGreaterThan(0); + expect(result.incomplete.filter((entry) => entry.id === "target-size")).toEqual([]); +} + +function axeRuleProjection(result, ruleId) { + return { + incomplete: result.incomplete.filter((entry) => entry.id === ruleId).flatMap((entry) => entry.nodes.map((node) => node.target)), + passes: result.passes.filter((entry) => entry.id === ruleId).flatMap((entry) => entry.nodes.map((node) => node.target)), + violations: result.violations.filter((entry) => entry.id === ruleId).flatMap((entry) => entry.nodes.map((node) => node.target)), + }; +} + +function withAxeOutcome(result, outcome, ruleId, target) { + return { + ...result, + [outcome]: [...result[outcome], {id: ruleId, nodes: [{target: [target]}]}], + }; +} + +async function assertReflow(page, row) { + await page.setViewportSize({width: 320, height: 800}); + await assertWorkspaceState(page, row); + const result = await page.evaluate(() => { + const documentOverflow = document.documentElement.scrollWidth - document.documentElement.clientWidth; + const internal = [...document.querySelectorAll("*")] + .filter((element) => element.clientWidth > 0 && element.scrollWidth - element.clientWidth > 1) + .map((element) => ({ + clientWidth: element.clientWidth, + className: element.className, + id: element.id, + label: element.getAttribute("aria-label"), + role: element.getAttribute("role"), + scrollWidth: element.scrollWidth, + tagName: element.tagName, + })); + return {documentOverflow, internal}; }); + expect(result.documentOverflow).toBeLessThanOrEqual(1); + const unlabelledOverflow = result.internal.filter((viewport) => + !["graph-viewport", "table-viewport"].includes(viewport.className) || + viewport.role !== "region" || + !viewport.label); + expect(unlabelledOverflow).toEqual([]); +} + +async function assertRenderedContrast(page, row) { + for (const colorScheme of ["light", "dark"]) { + await page.emulateMedia({colorScheme}); + await assertWorkspaceState(page, row); + await page.keyboard.press("Tab"); + const samples = await page.evaluate(() => { + const parse = (value) => { + if (value === "transparent") return [0, 0, 0, 0]; + const match = value.match(/^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:\s*[,/]\s*([\d.]+))?\s*\)$/); + if (!match) throw new Error(`Unsupported rendered color: ${value}`); + return [Number(match[1]), Number(match[2]), Number(match[3]), match[4] === undefined ? 1 : Number(match[4])]; + }; + const composite = (foreground, background) => { + const alpha = foreground[3] + background[3] * (1 - foreground[3]); + if (alpha === 0) return [0, 0, 0, 0]; + return [ + (foreground[0] * foreground[3] + background[0] * background[3] * (1 - foreground[3])) / alpha, + (foreground[1] * foreground[3] + background[1] * background[3] * (1 - foreground[3])) / alpha, + (foreground[2] * foreground[3] + background[2] * background[3] * (1 - foreground[3])) / alpha, + alpha, + ]; + }; + const background = (element) => { + if (!(element instanceof Element)) return [255, 255, 255, 1]; + const parent = background(element.parentElement); + const color = parse(getComputedStyle(element).backgroundColor); + color[3] *= Number(getComputedStyle(element).opacity); + return composite(color, parent); + }; + const luminance = (color) => { + const channels = color.slice(0, 3).map((value) => { + const normalized = value / 255; + return normalized <= 0.04045 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4; + }); + return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722; + }; + const ratio = (left, right) => { + const first = luminance(left); + const second = luminance(right); + return (Math.max(first, second) + 0.05) / (Math.min(first, second) + 0.05); + }; + const controls = [document.querySelector("#submit-question"), document.querySelector("[data-view]:not([disabled])")] + .filter((element, index, values) => element instanceof HTMLElement && values.indexOf(element) === index); + const values = []; + for (const control of controls) { + const style = getComputedStyle(control); + const parentBackground = background(control.parentElement); + const controlBackground = background(control); + const opacity = Number(style.opacity); + const text = parse(style.color); + text[3] *= opacity; + const border = parse(style.borderTopColor); + border[3] *= opacity; + control.focus(); + const focused = getComputedStyle(control); + const outlineStyle = focused.outlineStyle; + const outlineWidth = Number.parseFloat(focused.outlineWidth); + if (["hidden", "none"].includes(outlineStyle) || !Number.isFinite(outlineWidth) || outlineWidth <= 0) { + throw new Error("Rendered focus indicator must use a visible positive-width outline"); + } + const outline = parse(focused.outlineColor); + outline[3] *= Number(focused.opacity); + values.push({ + border: ratio(composite(border, parentBackground), parentBackground), + focus: ratio(composite(outline, parentBackground), parentBackground), + text: ratio(composite(text, controlBackground), controlBackground), + }); + } + const heading = document.querySelector("h1"); + if (!(heading instanceof HTMLElement)) throw new Error("Workspace heading is unavailable"); + const headingBackground = background(heading); + const headingColor = parse(getComputedStyle(heading).color); + values.push({border: null, focus: null, text: ratio(composite(headingColor, headingBackground), headingBackground)}); + return values; + }); + expect(samples.length).toBeGreaterThan(1); + for (const sample of samples) { + expect(sample.text).toBeGreaterThanOrEqual(4.5); + if (sample.border !== null) expect(sample.border).toBeGreaterThanOrEqual(3); + if (sample.focus !== null) expect(sample.focus).toBeGreaterThanOrEqual(3); + } + } } async function disableOptionalViews(page) {