From 85e5656c5adf85e838f47aab06df8a376062c8b8 Mon Sep 17 00:00:00 2001 From: iperev Date: Sat, 5 Sep 2026 05:51:51 +0200 Subject: [PATCH 1/5] feat: add bounded project status workflow --- .github/workflows/ci.yml | 3 + docs/proofkit-contract-map.md | 8 +- .../specs/proofkit-agent-workflow/overview.md | 32 +- .../requirements.v1.json | 55 ++ .../proofkit-spec-proof-core/overview.md | 4 + .../requirements.v1.json | 13 + ...ption_materialization_version_edge_test.go | 125 ++- internal/app/agent_workflow_args.go | 11 +- internal/app/agent_workflow_command_test.go | 90 +- internal/app/app.go | 2 + internal/app/cli_contract_test.go | 21 +- .../app/cli_output_witness_contract_test.go | 36 + internal/app/command_contract_generated.go | 18 +- internal/app/command_coverage_routes.go | 9 + internal/app/command_coverage_test.go | 2 + internal/app/command_descriptors.go | 4 + .../app/command_family_catalog_generated.go | 3 +- .../project_navigation_version_edge_test.go | 460 ++++++++++ internal/app/project_status_command.go | 136 +++ internal/app/project_status_command_test.go | 304 +++++++ .../compact-current-production-consumers.json | 11 +- .../v0.8.0/preserved-command-contracts.json | 26 + .../v0.8.0/release/change-record.v2.json | 36 + .../app/testdata/v0.9-wire-observations.json | 69 ++ .../command/adoptionmaterialization/build.go | 22 +- .../project_closure.go | 196 +++++ .../project_closure_test.go | 276 ++++++ .../command/projectstatus/dependency_test.go | 52 ++ internal/command/projectstatus/evaluate.go | 179 ++++ internal/command/projectstatus/filesystem.go | 107 +++ internal/command/projectstatus/inspect.go | 242 ++++++ .../command/projectstatus/inspect_test.go | 568 +++++++++++++ internal/command/projectstatus/model.go | 292 +++++++ internal/command/projectstatus/output.go | 61 ++ .../command/projectstatus/output_admission.go | 321 +++++++ .../projectstatus/projectstatus_test.go | 364 ++++++++ internal/command/projectstatus/text.go | 77 ++ .../stackpreset/preset_ids_generated.go | 2 +- internal/kernel/commandroute/route.go | 26 +- internal/kernel/commandroute/route_test.go | 34 + .../control_inspection.go | 186 ++++ .../control_inspection_test.go | 783 +++++++++++++++++ .../control_observation.go | 232 +++++ .../repositorytransaction/control_state.go | 11 +- .../repositorytransaction/filesystem.go | 99 +-- .../repositorytransaction/inspection_lease.go | 204 +++++ internal/kernel/repositorytransaction/lock.go | 13 +- internal/kernel/rootpath/exact.go | 77 ++ internal/kernel/rootpath/exact_test.go | 196 +++++ internal/kernel/rootpath/open_other.go | 14 + internal/kernel/rootpath/open_unix.go | 141 ++++ internal/tools/commandcontractgen/main.go | 11 +- .../tools/commandcontractgen/main_test.go | 18 +- internal/tools/coveragemetrics/main.go | 574 +------------ internal/tools/coveragemetrics/main_test.go | 28 + .../coveragemetrics/required_inventory.go | 794 ++++++++++++++++++ .../tools/installedclicontract/contract.go | 22 +- .../installedclicontract/contract_test.go | 3 +- internal/tools/packageverify/main_test.go | 11 +- .../tools/pythonpackage/continuation_test.go | 2 +- internal/tools/pythonpackage/metadata_test.go | 2 +- internal/tools/releasechange/record_test.go | 27 +- .../workflowsmoke/project_navigation_smoke.go | 90 ++ .../tools/workflowsmoke/workflow_smoke.go | 26 +- .../workflowsmoke/workflow_smoke_test.go | 30 +- package-lock.json | 4 +- package.json | 2 +- proofkit/cli-contract.v2.json | 310 ++++++- proofkit/command-families.v1.json | 9 + proofkit/requirement-bindings.json | 390 +++++++++ release/change-record.v2.json | 33 +- scripts/workflow_package_gate_oracle_test.go | 4 + 72 files changed, 7770 insertions(+), 873 deletions(-) create mode 100644 internal/app/project_navigation_version_edge_test.go create mode 100644 internal/app/project_status_command.go create mode 100644 internal/app/project_status_command_test.go create mode 100644 internal/app/testdata/releases/v0.8.0/preserved-command-contracts.json create mode 100644 internal/app/testdata/releases/v0.8.0/release/change-record.v2.json create mode 100644 internal/app/testdata/v0.9-wire-observations.json create mode 100644 internal/command/adoptionmaterialization/project_closure.go create mode 100644 internal/command/adoptionmaterialization/project_closure_test.go create mode 100644 internal/command/projectstatus/dependency_test.go create mode 100644 internal/command/projectstatus/evaluate.go create mode 100644 internal/command/projectstatus/filesystem.go create mode 100644 internal/command/projectstatus/inspect.go create mode 100644 internal/command/projectstatus/inspect_test.go create mode 100644 internal/command/projectstatus/model.go create mode 100644 internal/command/projectstatus/output.go create mode 100644 internal/command/projectstatus/output_admission.go create mode 100644 internal/command/projectstatus/projectstatus_test.go create mode 100644 internal/command/projectstatus/text.go create mode 100644 internal/kernel/repositorytransaction/control_inspection.go create mode 100644 internal/kernel/repositorytransaction/control_inspection_test.go create mode 100644 internal/kernel/repositorytransaction/control_observation.go create mode 100644 internal/kernel/repositorytransaction/inspection_lease.go create mode 100644 internal/kernel/rootpath/exact.go create mode 100644 internal/kernel/rootpath/exact_test.go create mode 100644 internal/kernel/rootpath/open_other.go create mode 100644 internal/kernel/rootpath/open_unix.go create mode 100644 internal/tools/coveragemetrics/required_inventory.go create mode 100644 internal/tools/workflowsmoke/project_navigation_smoke.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e07815..58f7995 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -151,6 +151,9 @@ jobs: go-version-file: go.mod cache: true + - name: Run Darwin filesystem invariants + run: go test ./internal/kernel/rootpath ./internal/kernel/repositorytransaction ./internal/command/projectstatus -count=1 + - name: Run platform smoke run: | set -euo pipefail diff --git a/docs/proofkit-contract-map.md b/docs/proofkit-contract-map.md index ef9b59b..0599589 100644 --- a/docs/proofkit-contract-map.md +++ b/docs/proofkit-contract-map.md @@ -10,6 +10,8 @@ This map helps consuming repositories choose the smallest Proofkit CLI command or JSON contract without loading the full README or source tree. It is not an exhaustive schema reference. The canonical command inventory is `proofkit/cli-contract.v2.json`. +Its `commandRouteGrammar.omittedRoutePolicy` field owns how consumers expand a +command record that omits an explicit `route`. Formal rule: @@ -39,7 +41,8 @@ owner boundaries. It is not a second command-family inventory. | Family | Main commands | Caller provides | Proofkit owns | Consumer owns | Output authority | |---|---|---|---|---|---| -| Agent workflow planning | `change-workflow-plan`, `native-evidence-guidance` | explicit checkpoint, completed stage ids, bounded context refs, governing authority ref, and required context ref ids | optional built-in `proofkit.reviewed-change.v1` checkpoint relation, reference-closed next-stage context, deterministic agent prompts, bounded text/JSON/envelope projections, and repository-neutral native-evidence guidance with closed applicability classes | custom workflow topology, repository state discovery, stage execution, native witness semantics, evidence collection, review conclusions, merge, release, deployment, and rollout authority | next-action plan, terminal workflow report, bounded agent envelope, or guidance catalog | +| Project state navigation | `status`, `next` | explicit repository root | bounded transaction-first materialized-project inspection, normalized observation identity, deterministic project-state classification, and one non-executable next action; admitted in-bound records bind exact content digests, while unread out-of-bound records intentionally identify only their invalid class | repository policy, byte identity for unread out-of-bound records, witness execution, receipt trust/currentness/scope, merge, release, deployment, rollout, and production readiness | project-status report, next-action packet, or bounded text projection | +| Agent workflow planning | `change plan`, `native-evidence-guidance` | explicit checkpoint, completed stage ids, bounded context refs, governing authority ref, and required context ref ids | optional built-in `proofkit.reviewed-change.v1` checkpoint relation, reference-closed next-stage context, deterministic agent prompts, bounded text/JSON/envelope projections, and repository-neutral native-evidence guidance with closed applicability classes | custom workflow topology, repository state discovery, stage execution, native witness semantics, evidence collection, review conclusions, merge, release, deployment, and rollout authority | next-action plan, terminal workflow report, bounded agent envelope, or guidance catalog | | Adoption and scaffolding | `adopt plan`, `adopt materialize plan`, `adopt materialize apply`, `adopt materialize recover`, `repository-inventory`, `adoption-contract-envelope`, `adoption-workflow-plan`, `adoption-checklist`, `adoption-doctor`, `gradual-adoption`, `gradual-adoption-bootstrap`, `gradual-adoption-guidance`, `capability-map-admission`, `pilot-admission`, `scaffold-profile-plan`, `scaffold-project-structure`, `stack-preset` | explicit repository root, explicit fresh/code-baseline/audit-from-code intent, optional stack hint, owner-reviewed candidate packet, expected transaction and desired-state identities, recovery action, aggregate adoption contract envelope, checklist facts, target paths, owner routes, caller-extracted stale authority vocabulary facts, explicit pre-spec capability observations, and pilot records | bounded fixed-catalog root inventory, candidate-only front-door tasks, owner-closed read-only materialization plans, confined transactional apply and recovery receipts, aggregate contract-envelope admission, deterministic starter plans, checklist/report admission, bounded guidance envelopes, dry-run manifests, pre-spec trust-mode admission, adoption gap and stale-authority classification, and pilot shape admission | stack selection, arbitrary source inspection, candidate review, final requirement meaning, proof adequacy, rollout policy, text extraction, code observation extraction, and pilot truth | inventory, candidate-only plan, transaction-bound materialization plan or receipt, selected child output, report, seed packet, or agent envelope | | Requirement source | `capability-map-admission`, `requirement-authoring-plan`, `requirement-source-admission`, `requirement-source-transition`, `spec-overview-claims`, `requirement-spec-tree`, `requirement-spec-tree-view`, `requirement-source-view`, `requirement-browser-server` | `requirements.v1.json`, caller-owned capability maps, caller-owned authoring facts, overview claim extraction, explicit spec hierarchy, view options | candidate seed admission, candidate-only authoring packets, source-shape admission, lifecycle checks, explicit tree topology/source-ref admission, shared safe renderer fragments, presentation-only views | requirement meaning, extraction completeness, Markdown extraction completeness, hierarchy ownership, proof adequacy, file materialization | capability map report, authoring packet, source report, spec-tree report, rendered view, or browser presentation | | Requirement proof binding | `requirement-bindings`, `binding-partition`, `proof-slice`, `evidence-graph`, `requirement-proof-resolver`, `requirement-proof-source-set`, `requirement-proof-view`, `spec-proof-bundle-admission` | requirement records, bindings, witness commands, source-set facts, receipt reports, partition policy | graph validation, binding partition projection, compact slices, declaration-only compact route projection with full binding identity and role-qualified witness routes, resolver projection, bundle linkage checks | selector resolution, oracle quality, witness execution, mutation adequacy, finding completeness, proof freshness, trust, assurance, merge policy | proof report, partition report, slice, declaration lookup graph, or view | @@ -128,8 +131,9 @@ Semantic context routes are `requirement-context-compose`, | State or goal | Next Proofkit route | Stop or escalation condition | |---|---|---| +| The repository may already contain a materialized Proofkit project. | `status --repo-root ` for the full bounded classification or `next --repo-root ` for its single action projection. | Treat `verification_required` as a request to run repository-owned verification, never as completion or approval. Stop on blocked or recovery-required states; the packet does not execute its route or own policy. | | The agent does not know where to start. | `adopt plan --mode --repo-root `, where mode is `fresh`, `code-baseline`, or `audit-from-code` | Choose the trust intent explicitly. Treat the fixed-catalog inventory and tasks as a read-only candidate plan; stop before arbitrary source inspection, writing files, or making requirements authoritative. | -| An agent needs a bounded, deterministic stage transition for an engineering change. | `change-workflow-plan` selects the optional built-in `proofkit.reviewed-change.v1` profile; use `--agent-envelope` for the compact work packet and `native-evidence-guidance` when the consuming repository has not yet materialized repository-specific evidence instructions. | Supply only explicit current checkpoint, completed stages, and admitted context references. Apply conditional guidance slots only when their applicability class matches a declared consumer mechanism. Stop before treating the profile, plan, or guidance as repository policy or as proof that a stage ran, evidence exists, review passed, or merge/release is authorized. | +| An agent needs a bounded, deterministic stage transition for an engineering change. | `change plan` selects the optional built-in `proofkit.reviewed-change.v1` profile; use `--agent-envelope` for the compact work packet and `native-evidence-guidance` when the consuming repository has not yet materialized repository-specific evidence instructions. | Supply only explicit current checkpoint, completed stages, and admitted context references. Apply conditional guidance slots only when their applicability class matches a declared consumer mechanism. Stop before treating the profile, plan, or guidance as repository policy or as proof that a stage ran, evidence exists, review passed, or merge/release is authorized. | | 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. | Start with `adopt plan --mode fresh --repo-root `; use `scaffold-project-structure`, `adoption-workflow-plan`, or `stack-preset` only as later specialist routes when an owner has selected them. | Treat front-door tasks as candidate-only. Stop before writing files; the consumer owns materialization, overwrite policy, and final requirement text. | | Owner-reviewed candidate requirement sources, proof bindings, and test inventory are ready for repository materialization. | Use command ID `adopt-materialize-plan` through route `adopt materialize plan --input --repo-root `, review the exact transaction and desired-state identities, then use command ID `adopt-materialize-apply` through route `adopt materialize apply` with both expected identities. Use `adopt materialize recover` only for the exact observed transaction and state-compatible `resume` or `rollback` action. | Stop on stale state, unknown ownership, path-role collision, pending transaction, identity mismatch, or recovery-required output. A plan or receipt does not prove requirement meaning, witness truth, proof adequacy, merge approval, rollout, or production readiness. | diff --git a/docs/specs/proofkit-agent-workflow/overview.md b/docs/specs/proofkit-agent-workflow/overview.md index 5f9dee5..3bcdee1 100644 --- a/docs/specs/proofkit-agent-workflow/overview.md +++ b/docs/specs/proofkit-agent-workflow/overview.md @@ -7,7 +7,7 @@ witnesses, merge, release, rollout, or production-readiness decisions. The public capability is deliberately small: -1. `change-workflow-plan` admits explicit JSON and projects one next action for +1. `change plan` admits explicit JSON and projects one next action for the optional built-in profile `proofkit.reviewed-change.v1`, whose ordered stages are `architecture`, `design`, `implementation_plan`, `implementation`, `verification`, `pull_request`, and `closeout`. @@ -19,12 +19,19 @@ The public capability is deliberately small: the consuming witness declares the named mechanism. 3. Existing descriptors, dispatch, command families, root-shape CLI contracts, agent envelopes, and package gates provide public-surface closure. +4. `status --repo-root` classifies only a bounded normalized + materialized-project observation, and `next --repo-root` projects one + bounded action. Admitted in-bound records bind exact content digests; + unread out-of-bound records bind only their invalid class. Neither command + claims native execution or proof completion. -Neither workflow command scans a repository. Both command cores are stateless -pure projections with no filesystem, Git, process, environment, clock, random, -network, container, or provider dependency. Neither workflow command adds a -setup facade, hidden route policy, external prompt resource, persisted -experiment state, or second source codec. Agent-route brief and full +The change planner and evidence-guidance cores are stateless pure projections. +Project status reads only an explicit repository root, the conventional routing +manifest, its declared children, and transaction control state through bounded +owner-admitted transport. No workflow command executes Git, native witnesses, +network, containers, or providers. No workflow command adds a setup facade, +hidden route policy, external prompt resource, persisted experiment state, +generic report interpreter, or second source codec. Agent-route brief and full projections remain independently owned by the spec-proof-core package. ## Requirements @@ -69,6 +76,19 @@ projections remain independently owned by the spec-proof-core package. descriptor/dispatcher/family/help/root-contract/witness/generated/package surfaces, with npm-only non-runtime specification docs and cross-channel runtime behavior proof. +- `REQ-PROOFKIT-WORKFLOW-012`: one truthful project-state owner, exhaustive + precedence, existing child and cross-record closure owners, and no promotion + of source declarations or caller status into execution evidence. +- `REQ-PROOFKIT-WORKFLOW-013`: one root-bound inspection lease, cooperative + writer exclusion, descriptor-relative exact-path traversal, bounded + content-cohort validation, fail-closed partial control observations, one + bounded retry, and a portable non-disclosing normalized-observation identity. +- `REQ-PROOFKIT-WORKFLOW-014`: one total state-to-action table, one bounded + next action, explicit owner decisions, and no embedded route universe. +- `REQ-PROOFKIT-WORKFLOW-015`: status/next CLI channel and exit semantics, + pre-emission failure discipline, one bounded stdout write without claiming + atomicity from a failing external sink, and a versioned breaking replacement + of the flat change route by `change plan` across source and installed carriers. Shared stable-JSON/diagnostic hardening is owned by the supply-chain-quality spec. Typed local-reference closure is owned by the existing agent-envelope diff --git a/docs/specs/proofkit-agent-workflow/requirements.v1.json b/docs/specs/proofkit-agent-workflow/requirements.v1.json index 323c86c..510a523 100644 --- a/docs/specs/proofkit-agent-workflow/requirements.v1.json +++ b/docs/specs/proofkit-agent-workflow/requirements.v1.json @@ -147,6 +147,61 @@ "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, "deferral": null, "updatePolicy": {"reviewOwnerId": "proofkit.agent-workflow", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-012", + "ownerId": "proofkit.agent-workflow", + "invariant": "Project status consumes only one explicit repository root, the conventional digest-routed project manifest, its exact manifest-declared canonical children, and one repository-transaction control projection; it delegates child admission and cross-record closure to the adoption-materialization owner, publishes exactly one state from uninitialized, recovery_required, blocked, stale, and verification_required through one exhaustive precedence table, and never promotes source declarations, binding commands, caller-owned passed labels, or caller-declared workflow completion into native execution, receipt currentness, trust, merge, release, rollout, deployment, or production-readiness evidence.", + "claimLevel": "blocking", + "riskClass": "high", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-WORKFLOW-012"], + "nonClaims": ["Project status classifies the observed materialized-project snapshot and can become stale immediately after emission; it does not prove native witness execution or complete any declared proof scope."], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.agent-workflow", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-013", + "ownerId": "proofkit.agent-workflow", + "invariant": "Project inspection is explicit, root-confined, non-mutating, symlink-denying, byte-bounded, and normalized-observation-bound: one inspection lease pins one repository root for the whole attempt, exports only exact read-only file capabilities, and, when the transaction control namespace exists, holds its native cooperative writer lock; transaction control is projected by its native owner as clean, recoverable with an exact transaction identity, or invalid with a portable content-bound observation epoch only after the entire namespace is observed within canonical entry, file, aggregate, depth, and file-type bounds, while overflow or unsupported shape fails without a partial packet; every manifest and child route is traversed component by component from the pinned root with exact-name admission, no symlink following, descriptor identity checks, regular-file admission, per-file one-MiB and per-pass aggregate eight-MiB limits before semantic decoding; the root identity and transaction observation are equal before and after the read and a second bounded digest pass over the manifest and every routed child equals the admitted first-pass cohort, or the operation performs at most one complete retry and then fails as concurrent change; every public state receives a snapshot identity over one total tagged observation whose admitted in-bound records bind exact content digests, whose unread out-of-bound records intentionally bind only their invalid class, whose project is unknown or admitted, whose manifest is absent, invalid with an optional bounded content digest, or admitted with identity and content digest, whose transaction carries its tagged state and epoch, whose children are ordered kind/state/expected/observed digest records, and whose cross-record closure state is explicit, without exposing raw bytes, repository paths, or caller text.", + "claimLevel": "blocking", + "riskClass": "high", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-WORKFLOW-013"], + "nonClaims": ["A coherent read snapshot does not prevent later filesystem mutation, authenticate repository ownership, provide power-loss or multi-reader atomicity, exclude a same-user writer that bypasses the repository transaction owner, or identify unread out-of-bound record bytes beyond their normalized invalid class."], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.agent-workflow", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-014", + "ownerId": "proofkit.agent-workflow", + "invariant": "Project next-action output is a deterministic bounded projection of the admitted project status through one total state-to-action table: blocked maps to non-executable repair, recovery_required maps to a non-executable recovery-direction decision bound to the transaction identity, uninitialized maps to a non-executable source-trust-mode decision, stale maps to rematerialization, and verification_required maps to repository-owned native verification; it emits exactly one action, at most sixteen stable non-disclosing issue codes, no route universe, no inferred trust mode, recovery direction, command success, or owner decision, and stable JSON within 32768 bytes plus text within 4096 UTF-8 bytes and sixteen lines that preserves exactly projectState, actionClass, executable, commandRoute, contextRef, requiredDecision, and issueCodes while intentionally omitting JSON-only packetId, snapshotId, statusRef, and nonClaims.", + "claimLevel": "blocking", + "riskClass": "high", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-WORKFLOW-014"], + "nonClaims": ["A next-action packet is derived guidance; it does not execute, authorize, or prove the proposed action."], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.agent-workflow", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-015", + "ownerId": "proofkit.agent-workflow", + "invariant": "The public status and next commands require exactly one explicit --repo-root, default to stable ANSI-free JSON, admit text and terminal-only color through the shared presentation contract, return exit zero for every successfully classified project state, including bounded project-record decoding or admission failure, and return exit one without beginning output emission for invocation, confinement, cancellation, inspection-bound, concurrent-change, cleanup, or serialization failure; successful serialization reaches stdout through one bounded write, and a transport failure returns exit one without claiming atomic behavior from a writer that accepted a prefix before failing; the same versioned breaking public edge adds both commands, replaces the flat change-workflow-plan route with change plan without adding a route-alias registry or second implementation, rejects the retired route, admits the hierarchical route, and closes descriptor, help, CLI contract, command family, contract map, ABI, source witness, installed npm, and installed wheel surfaces.", + "claimLevel": "blocking", + "riskClass": "high", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-WORKFLOW-015"], + "nonClaims": [ + "A caller-provided stdout writer that accepts a prefix and then fails does not provide an atomic sink, so Proofkit does not claim that such a transport leaves stdout empty.", + "CLI surface closure does not prove registry publication, provider ingestion, consumer migration, native witness truth, rollout, or production readiness." + ], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.agent-workflow", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} } ], "nonClaims": [ diff --git a/docs/specs/proofkit-spec-proof-core/overview.md b/docs/specs/proofkit-spec-proof-core/overview.md index a1a8042..d137801 100644 --- a/docs/specs/proofkit-spec-proof-core/overview.md +++ b/docs/specs/proofkit-spec-proof-core/overview.md @@ -200,6 +200,10 @@ execution receipts, and merge policy. materialization public version edge binds all three transactional materialization routes and their exact public contracts to a compatible release record without reinterpreting the frozen prior edge. +- `REQ-PROOFKIT-SPEC-035`: the project-state public version edge binds status, + next, and the change-plan route replacement to exact ABI and command-contract + identities plus one breaking release record without reinterpreting the + frozen prior edge. ## 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 295064b..4b35b47 100644 --- a/docs/specs/proofkit-spec-proof-core/requirements.v1.json +++ b/docs/specs/proofkit-spec-proof-core/requirements.v1.json @@ -698,6 +698,19 @@ "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, "deferral": null, "updatePolicy": {"reviewOwnerId": "proofkit.spec-proof-core", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-SPEC-035", + "ownerId": "proofkit.spec-proof-core", + "invariant": "The 0.8.0-to-0.9.0 public version edge binds the exact previous and current public ABI digests; the exact addition of status and next with their public routes and output contract identities and digests; the exact replacement of the flat change-workflow-plan route by change plan while preserving its single internal implementation and input/output contract identities and digests; and the complete ordered breaking, additive, and migration inventories to one digest-bound current release change record. The edge rejects the retired route, admits the hierarchical route, and cannot mutate or reinterpret the byte-frozen 0.7.0-to-0.8.0 edge.", + "claimLevel": "blocking", + "riskClass": "high", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-SPEC-035"], + "nonClaims": ["A source-bound version edge does not authenticate registry publication, provider ingestion, consumer migration, native witness truth, rollout, or production readiness."], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.spec-proof-core", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} } ], "nonClaims": [ diff --git a/internal/app/adoption_materialization_version_edge_test.go b/internal/app/adoption_materialization_version_edge_test.go index 1cf358e..9d3c9cf 100644 --- a/internal/app/adoption_materialization_version_edge_test.go +++ b/internal/app/adoption_materialization_version_edge_test.go @@ -16,29 +16,30 @@ import ( ) const adoptionMaterializationVersionEdgePath = "internal/app/testdata/v0.8-wire-observations.json" +const archivedAdoptionMaterializationReleaseRoot = "internal/app/testdata/releases/v0.8.0" const frozenAdoptionFrontDoorVersionEdgePath = "internal/app/testdata/v0.7-wire-observations.json" const frozenAdoptionFrontDoorVersionEdgeSHA256 = "3f3916ff3413aed42539cfd122d0796b636f6819512459a13f4143443bd2a14e" type adoptionMaterializationVersionEdge struct { - AddedCommandContracts []materializationCommandContract `json:"addedCommandContracts"` - AdditionChangeIDs []string `json:"additionChangeIds"` - BreakingChangeIDs []string `json:"breakingChangeIds"` - ChangeClass string `json:"changeClass"` - ChangeRecordRef string `json:"changeRecordRef"` - ChangeRecordSHA256 string `json:"changeRecordSha256"` - CommandContractSelection string `json:"commandContractSelection"` - CurrentPublicABISHA256 string `json:"currentPublicAbiSha256"` - EdgeID string `json:"edgeId"` - EvidenceClass string `json:"evidenceClass"` - NonClaims []string `json:"nonClaims"` - PreviousPublicABISHA256 string `json:"previousPublicAbiSha256"` - PreviousVersion string `json:"previousVersion"` - SchemaVersion int `json:"schemaVersion"` - Version string `json:"version"` + AddedCommandContracts []versionEdgeCommandContract `json:"addedCommandContracts"` + AdditionChangeIDs []string `json:"additionChangeIds"` + BreakingChangeIDs []string `json:"breakingChangeIds"` + ChangeClass string `json:"changeClass"` + ChangeRecordRef string `json:"changeRecordRef"` + ChangeRecordSHA256 string `json:"changeRecordSha256"` + CommandContractSelection string `json:"commandContractSelection"` + CurrentPublicABISHA256 string `json:"currentPublicAbiSha256"` + EdgeID string `json:"edgeId"` + EvidenceClass string `json:"evidenceClass"` + NonClaims []string `json:"nonClaims"` + PreviousPublicABISHA256 string `json:"previousPublicAbiSha256"` + PreviousVersion string `json:"previousVersion"` + SchemaVersion int `json:"schemaVersion"` + Version string `json:"version"` } -type materializationCommandContract struct { +type versionEdgeCommandContract struct { Command string `json:"command"` InputContract *versionEdgeContractIdentity `json:"inputContract,omitempty"` OutputContract versionEdgeContractIdentity `json:"outputContract"` @@ -52,12 +53,7 @@ type versionEdgeContractIdentity struct { func TestAdoptionMaterializationVersionEdgeClosesPublicCommands(t *testing.T) { record := readAdoptionMaterializationVersionEdge(t) - currentABI := "sha256:" + currentCLIContractPublicABISHA256(t) - currentCommands, err := currentMaterializationCommandContracts(repoRoot(t)) - if err != nil { - t.Fatal(err) - } - if err := validateAdoptionMaterializationVersionEdge(record, repoRoot(t), currentABI, currentCommands); err != nil { + if err := validateAdoptionMaterializationVersionEdge(record, archivedAdoptionMaterializationRoot(t)); err != nil { t.Fatal(err) } @@ -93,7 +89,7 @@ func TestAdoptionMaterializationVersionEdgeClosesPublicCommands(t *testing.T) { t.Run(fmt.Sprintf("mutant-%d", index), func(t *testing.T) { value := cloneAdoptionMaterializationVersionEdge(record) mutate(&value) - if err := validateAdoptionMaterializationVersionEdge(value, repoRoot(t), currentABI, currentCommands); err == nil { + if err := validateAdoptionMaterializationVersionEdge(value, archivedAdoptionMaterializationRoot(t)); err == nil { t.Fatal("materialization version-edge mutant was admitted") } }) @@ -113,12 +109,7 @@ func TestAdoptionMaterializationVersionEdgePreservesFrozenPredecessor(t *testing func TestAdoptionMaterializationVersionEdgeRejectsCoordinatedChangeRecordDrift(t *testing.T) { record := readAdoptionMaterializationVersionEdge(t) - currentABI := "sha256:" + currentCLIContractPublicABISHA256(t) - currentCommands, err := currentMaterializationCommandContracts(repoRoot(t)) - if err != nil { - t.Fatal(err) - } - content, err := os.ReadFile(filepath.Join(repoRoot(t), record.ChangeRecordRef)) + content, err := os.ReadFile(filepath.Join(archivedAdoptionMaterializationRoot(t), record.ChangeRecordRef)) if err != nil { t.Fatal(err) } @@ -144,7 +135,7 @@ func TestAdoptionMaterializationVersionEdgeRejectsCoordinatedChangeRecordDrift(t mutant := cloneAdoptionMaterializationVersionEdge(record) digest := sha256.Sum256(mutantContent) mutant.ChangeRecordSHA256 = fmt.Sprintf("sha256:%x", digest) - if err := validateAdoptionMaterializationVersionEdge(mutant, mutantRoot, currentABI, currentCommands); err == nil || !strings.Contains(err.Error(), "contradicts") { + if err := validateAdoptionMaterializationVersionEdge(mutant, mutantRoot); err == nil || !strings.Contains(err.Error(), "contradicts") { t.Fatalf("coordinated change-record mutant error=%v, want inventory contradiction", err) } } @@ -198,7 +189,7 @@ func readAdoptionMaterializationVersionEdge(t *testing.T) adoptionMaterializatio return record } -func validateAdoptionMaterializationVersionEdge(record adoptionMaterializationVersionEdge, changeRecordRoot, currentPublicABI string, currentCommands []materializationCommandContract) error { +func validateAdoptionMaterializationVersionEdge(record adoptionMaterializationVersionEdge, changeRecordRoot string) error { if record.SchemaVersion != 1 || record.EdgeID != "proofkit.public-wire.0.7.0-to-0.8.0" || record.EvidenceClass != "owner_authored_current_version_edge_observation" { return fmt.Errorf("adoption materialization version-edge identity is invalid") } @@ -208,10 +199,10 @@ func validateAdoptionMaterializationVersionEdge(record adoptionMaterializationVe if record.CommandContractSelection != "added_public_commands" { return fmt.Errorf("adoption materialization command-contract selection policy is invalid") } - if record.PreviousPublicABISHA256 != "sha256:c3b7219fccd7d400b182beb53715f69758e02a4fef6f9465ba0c80a866abd1c7" || record.CurrentPublicABISHA256 != currentPublicABI || record.PreviousPublicABISHA256 == record.CurrentPublicABISHA256 { - return fmt.Errorf("adoption materialization version-edge ABI identity is invalid: previous=%s current=%s wantCurrent=%s", record.PreviousPublicABISHA256, record.CurrentPublicABISHA256, currentPublicABI) + if record.PreviousPublicABISHA256 != "sha256:c3b7219fccd7d400b182beb53715f69758e02a4fef6f9465ba0c80a866abd1c7" || record.CurrentPublicABISHA256 != "sha256:b5ea707ee5851cea6b75442e4faf20e93879371faf3636e96a98ccd23b527463" || record.PreviousPublicABISHA256 == record.CurrentPublicABISHA256 { + return fmt.Errorf("adoption materialization version-edge ABI identity is invalid") } - if !slices.EqualFunc(record.AddedCommandContracts, currentCommands, equalMaterializationCommandContract) { + if !slices.EqualFunc(record.AddedCommandContracts, frozenMaterializationCommandContracts(), equalVersionEdgeCommandContract) { return fmt.Errorf("adoption materialization added command contracts are not exact") } if !slices.Equal(record.BreakingChangeIDs, []string{}) || !slices.Equal(record.AdditionChangeIDs, []string{"proofkit.adoption.transactional-materialization", "proofkit.repository.transaction-protocol"}) { @@ -245,48 +236,28 @@ func validateAdoptionMaterializationVersionEdge(record adoptionMaterializationVe return nil } -func currentMaterializationCommandContracts(root string) ([]materializationCommandContract, error) { - content, err := os.ReadFile(filepath.Join(root, "proofkit", "cli-contract.v2.json")) - if err != nil { - return nil, fmt.Errorf("read current CLI contract: %w", err) - } - contract, err := admission.DecodeTypedJSON[cliContract](bytes.NewReader(content), int64(len(content))) - if err != nil { - return nil, fmt.Errorf("admit current CLI contract: %w", err) - } - result := make([]materializationCommandContract, 0, 3) - for _, name := range []string{"adopt-materialize-apply", "adopt-materialize-plan", "adopt-materialize-recover"} { - var command *cliContractCommand - for index := range contract.Commands { - if contract.Commands[index].Command == name { - command = &contract.Commands[index] - break - } - } - if command == nil { - return nil, fmt.Errorf("current CLI contract is missing %s", name) - } - metadata := generatedCommandContractMetadataByName[name] - if metadata.OutputContractSHA256 == "" || (command.InputContract != nil && metadata.InputContractSHA256 == "") { - return nil, fmt.Errorf("generated command contract metadata is incomplete for %s", name) - } - item := materializationCommandContract{ - Command: name, - OutputContract: versionEdgeContractIdentity{ - ContractID: contractIDFromRaw(command.OutputContract), - ContractSHA256: metadata.OutputContractSHA256, - }, - Route: effectiveContractRoute(*command), - } - if command.InputContract != nil { - item.InputContract = &versionEdgeContractIdentity{ - ContractID: contractIDFromRaw(command.InputContract), - ContractSHA256: metadata.InputContractSHA256, - } - } - result = append(result, item) +func archivedAdoptionMaterializationRoot(t *testing.T) string { + t.Helper() + return filepath.Join(repoRoot(t), archivedAdoptionMaterializationReleaseRoot) +} + +func frozenMaterializationCommandContracts() []versionEdgeCommandContract { + return []versionEdgeCommandContract{ + { + Command: "adopt-materialize-apply", Route: []string{"adopt", "materialize", "apply"}, + InputContract: &versionEdgeContractIdentity{ContractID: "proofkit.adopt-materialize-apply.input.v1", ContractSHA256: "sha256:073bc4b983038d593e6a655104af51bb2a105974946ef5e2c9b857c68db6577f"}, + OutputContract: versionEdgeContractIdentity{ContractID: "proofkit.adopt-materialize-apply.output.v1", ContractSHA256: "sha256:a3ff3ab6f2835ce6eb6bd69351adec171d722663d7d33aabaeade33e52f411af"}, + }, + { + Command: "adopt-materialize-plan", Route: []string{"adopt", "materialize", "plan"}, + InputContract: &versionEdgeContractIdentity{ContractID: "proofkit.adopt-materialize-plan.input.v1", ContractSHA256: "sha256:e7f2b1339f8ab11f577872d409b4fec66c79a79febe8cef7cfd15b969c0c4de4"}, + OutputContract: versionEdgeContractIdentity{ContractID: "proofkit.adopt-materialize-plan.output.v1", ContractSHA256: "sha256:02fc198c3a8eb0d03505f008c27e51c103b7b608eb3870e1757de529f697909a"}, + }, + { + Command: "adopt-materialize-recover", Route: []string{"adopt", "materialize", "recover"}, + OutputContract: versionEdgeContractIdentity{ContractID: "proofkit.adopt-materialize-recover.output.v1", ContractSHA256: "sha256:a24c3c8710e000e83f068a3d98bc33f0239076687c04d1d72d2457d8a389bef3"}, + }, } - return result, nil } func contractIDFromRaw(raw any) string { @@ -295,7 +266,7 @@ func contractIDFromRaw(raw any) string { return contractID } -func equalMaterializationCommandContract(left, right materializationCommandContract) bool { +func equalVersionEdgeCommandContract(left, right versionEdgeCommandContract) bool { return left.Command == right.Command && slices.Equal(left.Route, right.Route) && equalOptionalContractIdentity(left.InputContract, right.InputContract) && left.OutputContract == right.OutputContract } @@ -307,7 +278,7 @@ func equalOptionalContractIdentity(left, right *versionEdgeContractIdentity) boo } func cloneAdoptionMaterializationVersionEdge(record adoptionMaterializationVersionEdge) adoptionMaterializationVersionEdge { - record.AddedCommandContracts = append([]materializationCommandContract(nil), record.AddedCommandContracts...) + record.AddedCommandContracts = append([]versionEdgeCommandContract(nil), record.AddedCommandContracts...) for index := range record.AddedCommandContracts { record.AddedCommandContracts[index].Route = append([]string(nil), record.AddedCommandContracts[index].Route...) if record.AddedCommandContracts[index].InputContract != nil { diff --git a/internal/app/agent_workflow_args.go b/internal/app/agent_workflow_args.go index e8f4f19..8ba7ec5 100644 --- a/internal/app/agent_workflow_args.go +++ b/internal/app/agent_workflow_args.go @@ -18,13 +18,14 @@ type agentWorkflowArgs struct { func parseAgentWorkflowArgs(command string, args []string) (agentWorkflowArgs, error) { options := agentWorkflowArgs{format: "json", color: "never"} + diagnosticRoute := commandRouteForDiagnostic(command) seen := map[string]bool{} for index := 0; index < len(args); index++ { argument := args[index] switch argument { case "--agent-envelope": if command != "change-workflow-plan" { - return agentWorkflowArgs{}, fmt.Errorf("unsupported argument for %s: %s", command, argument) + return agentWorkflowArgs{}, fmt.Errorf("unsupported argument for %s: %s", diagnosticRoute, argument) } if seen[argument] { return agentWorkflowArgs{}, fmt.Errorf("%s may be specified only once", argument) @@ -33,13 +34,13 @@ func parseAgentWorkflowArgs(command string, args []string) (agentWorkflowArgs, e options.agentEnvelope = true case "--color", "--format", "--input", "--input-pointer": if command == "native-evidence-guidance" && (argument == "--input" || argument == "--input-pointer") { - return agentWorkflowArgs{}, fmt.Errorf("unsupported argument for %s: %s", command, argument) + return agentWorkflowArgs{}, fmt.Errorf("unsupported argument for %s: %s", diagnosticRoute, argument) } if seen[argument] { return agentWorkflowArgs{}, fmt.Errorf("%s may be specified only once", argument) } if index+1 >= len(args) || args[index+1] == "" && argument != "--input-pointer" { - return agentWorkflowArgs{}, missingAgentWorkflowValue(command, argument) + return agentWorkflowArgs{}, missingAgentWorkflowValue(diagnosticRoute, argument) } seen[argument] = true value := args[index+1] @@ -67,11 +68,11 @@ func parseAgentWorkflowArgs(command string, args []string) (agentWorkflowArgs, e options.pointerPresent = true } default: - return agentWorkflowArgs{}, fmt.Errorf("unsupported argument for %s: %s", command, argument) + return agentWorkflowArgs{}, fmt.Errorf("unsupported argument for %s: %s", diagnosticRoute, argument) } } if command == "change-workflow-plan" && options.inputPath == "" { - return agentWorkflowArgs{}, fmt.Errorf("change-workflow-plan requires --input ") + return agentWorkflowArgs{}, fmt.Errorf("%s requires --input ", diagnosticRoute) } if options.colorExplicit && options.format != "text" { return agentWorkflowArgs{}, fmt.Errorf("--color is valid only with --format text") diff --git a/internal/app/agent_workflow_command_test.go b/internal/app/agent_workflow_command_test.go index 02e8b82..58fa829 100644 --- a/internal/app/agent_workflow_command_test.go +++ b/internal/app/agent_workflow_command_test.go @@ -24,11 +24,11 @@ func TestAgentWorkflowCLITruthTable(t *testing.T) { semanticOutputClasses = 24 envelopeTransitionClasses = 8 frozenRejectionClasses = 45 - extraRejectionCases = 2 + extraRejectionCases = 3 helpClasses = 10 colorClasses = 8 ) - if got, want := semanticOutputClasses+envelopeTransitionClasses+frozenRejectionClasses+extraRejectionCases+helpClasses+colorClasses, 97; got != want { + if got, want := semanticOutputClasses+envelopeTransitionClasses+frozenRejectionClasses+extraRejectionCases+helpClasses+colorClasses, 98; got != want { t.Fatalf("agent workflow CLI truth-table cardinality = %d, want %d", got, want) } t.Run("semantic output classes", testAgentWorkflowSemanticOutputClasses) @@ -36,6 +36,12 @@ func TestAgentWorkflowCLITruthTable(t *testing.T) { t.Run("usage errors precede input", testAgentWorkflowUsageErrorsPrecedeInput) t.Run("exclusive help classes", testAgentWorkflowHelpClasses) t.Run("terminal capability product", testAgentWorkflowTerminalCapabilityProduct) + t.Run("retired flat route", func(t *testing.T) { + status, stdout, stderr := executeAgentWorkflowCLI(t, []string{"change-workflow-plan", "--input", "-"}, panicReader{}, PresentationCapabilities{}) + if status != 1 || stdout != "" || !strings.Contains(stderr, "unsupported command: change-workflow-plan") { + t.Fatalf("status=%d stdout=%q stderr=%q", status, stdout, stderr) + } + }) } func testAgentWorkflowEnvelopeTransitionClasses(t *testing.T) { @@ -79,7 +85,7 @@ func testAgentWorkflowEnvelopeTransitionClasses(t *testing.T) { if err != nil { t.Fatal(err) } - status, stdout, stderr := executeAgentWorkflowCLI(t, []string{"change-workflow-plan", "--input", "-", "--agent-envelope"}, bytes.NewReader(payload), PresentationCapabilities{}) + status, stdout, stderr := executeAgentWorkflowCLI(t, []string{"change", "plan", "--input", "-", "--agent-envelope"}, bytes.NewReader(payload), PresentationCapabilities{}) if status != 0 || stderr != "" { t.Fatalf("status=%d stderr=%q", status, stderr) } @@ -129,7 +135,7 @@ func testAgentWorkflowEnvelopeTransitionClasses(t *testing.T) { if err != nil { t.Fatal(err) } - status, stdout, stderr := executeAgentWorkflowCLI(t, []string{"change-workflow-plan", "--input", "-", "--agent-envelope"}, bytes.NewReader(payload), PresentationCapabilities{}) + status, stdout, stderr := executeAgentWorkflowCLI(t, []string{"change", "plan", "--input", "-", "--agent-envelope"}, bytes.NewReader(payload), PresentationCapabilities{}) if status != 0 || stderr != "" { t.Fatalf("status=%d stderr=%q", status, stderr) } @@ -163,7 +169,7 @@ func testAgentWorkflowSemanticOutputClasses(t *testing.T) { for _, envelope := range []bool{false, true} { name := strings.Join([]string{"planner", formatClass(explicitFormat), layout, envelopeClass(envelope)}, "/") t.Run(name, func(t *testing.T) { - args := []string{"change-workflow-plan", "--input", "-"} + args := []string{"change", "plan", "--input", "-"} if explicitFormat { args = append(args, "--format", "json") } @@ -191,9 +197,9 @@ func testAgentWorkflowSemanticOutputClasses(t *testing.T) { } for _, args := range [][]string{ - {"change-workflow-plan", "--input", "-", "--format", "text"}, - {"change-workflow-plan", "--input", "-", "--format", "text", "--color", "never"}, - {"change-workflow-plan", "--input", "-", "--format", "text", "--color", "auto"}, + {"change", "plan", "--input", "-", "--format", "text"}, + {"change", "plan", "--input", "-", "--format", "text", "--color", "never"}, + {"change", "plan", "--input", "-", "--format", "text", "--color", "auto"}, } { status, stdout, stderr := executeAgentWorkflowCLI(t, args, strings.NewReader(validChangeWorkflowInput), PresentationCapabilities{}) if status != 0 || stderr != "" || stdout != wantText || strings.Contains(stdout, "\x1b[") { @@ -235,30 +241,30 @@ func testAgentWorkflowSemanticOutputClasses(t *testing.T) { func testAgentWorkflowUsageErrorsPrecedeInput(t *testing.T) { cases := map[string][]string{ - "planner/json auto": {"change-workflow-plan", "--input", "-", "--color", "auto"}, - "planner/json auto envelope": {"change-workflow-plan", "--input", "-", "--color", "auto", "--agent-envelope"}, - "planner/json explicit never": {"change-workflow-plan", "--input", "-", "--color", "never"}, - "planner/text envelope never": {"change-workflow-plan", "--input", "-", "--format", "text", "--color", "never", "--agent-envelope"}, - "planner/text envelope auto": {"change-workflow-plan", "--input", "-", "--format", "text", "--color", "auto", "--agent-envelope"}, - "planner/text layout pretty": {"--json-layout", "pretty", "change-workflow-plan", "--input", "-", "--format", "text"}, - "planner/text layout compact": {"--json-layout", "compact", "change-workflow-plan", "--input", "-", "--format", "text"}, - "planner/missing input": {"change-workflow-plan"}, - "planner/output": {"change-workflow-plan", "--input", "-", "--output", "out.json"}, - "planner/duplicate input": {"change-workflow-plan", "--input", "-", "--input", "second.json"}, - "planner/duplicate pointer": {"change-workflow-plan", "--input", "-", "--input-pointer", "", "--input-pointer", "/other"}, - "planner/duplicate format": {"change-workflow-plan", "--input", "-", "--format", "json", "--format", "text"}, - "planner/duplicate color": {"change-workflow-plan", "--input", "-", "--color", "never", "--color", "auto"}, - "planner/unknown flag": {"change-workflow-plan", "--input", "-", "--unknown"}, - "planner/missing input value": {"change-workflow-plan", "--input"}, - "planner/missing pointer value": {"change-workflow-plan", "--input", "-", "--input-pointer"}, - "planner/missing format value": {"change-workflow-plan", "--input", "-", "--format"}, - "planner/missing color value": {"change-workflow-plan", "--input", "-", "--color"}, - "planner/bad pointer": {"change-workflow-plan", "--input", "-", "--input-pointer", "workflow"}, - "planner/malformed UTF-8 pointer": {"change-workflow-plan", "--input", "-", "--input-pointer", string([]byte{'/', 0xff})}, - "planner/bad format": {"change-workflow-plan", "--input", "-", "--format", "yaml"}, - "planner/bad color": {"change-workflow-plan", "--input", "-", "--format", "text", "--color", "always"}, - "planner/post-command layout": {"change-workflow-plan", "--json-layout", "compact", "--input", "-"}, - "planner/surplus operand": {"change-workflow-plan", "--input", "-", "surplus"}, + "planner/json auto": {"change", "plan", "--input", "-", "--color", "auto"}, + "planner/json auto envelope": {"change", "plan", "--input", "-", "--color", "auto", "--agent-envelope"}, + "planner/json explicit never": {"change", "plan", "--input", "-", "--color", "never"}, + "planner/text envelope never": {"change", "plan", "--input", "-", "--format", "text", "--color", "never", "--agent-envelope"}, + "planner/text envelope auto": {"change", "plan", "--input", "-", "--format", "text", "--color", "auto", "--agent-envelope"}, + "planner/text layout pretty": {"--json-layout", "pretty", "change", "plan", "--input", "-", "--format", "text"}, + "planner/text layout compact": {"--json-layout", "compact", "change", "plan", "--input", "-", "--format", "text"}, + "planner/missing input": {"change", "plan"}, + "planner/output": {"change", "plan", "--input", "-", "--output", "out.json"}, + "planner/duplicate input": {"change", "plan", "--input", "-", "--input", "second.json"}, + "planner/duplicate pointer": {"change", "plan", "--input", "-", "--input-pointer", "", "--input-pointer", "/other"}, + "planner/duplicate format": {"change", "plan", "--input", "-", "--format", "json", "--format", "text"}, + "planner/duplicate color": {"change", "plan", "--input", "-", "--color", "never", "--color", "auto"}, + "planner/unknown flag": {"change", "plan", "--input", "-", "--unknown"}, + "planner/missing input value": {"change", "plan", "--input"}, + "planner/missing pointer value": {"change", "plan", "--input", "-", "--input-pointer"}, + "planner/missing format value": {"change", "plan", "--input", "-", "--format"}, + "planner/missing color value": {"change", "plan", "--input", "-", "--color"}, + "planner/bad pointer": {"change", "plan", "--input", "-", "--input-pointer", "workflow"}, + "planner/malformed UTF-8 pointer": {"change", "plan", "--input", "-", "--input-pointer", string([]byte{'/', 0xff})}, + "planner/bad format": {"change", "plan", "--input", "-", "--format", "yaml"}, + "planner/bad color": {"change", "plan", "--input", "-", "--format", "text", "--color", "always"}, + "planner/post-command layout": {"change", "plan", "--json-layout", "compact", "--input", "-"}, + "planner/surplus operand": {"change", "plan", "--input", "-", "surplus"}, "guidance/input": {"native-evidence-guidance", "--input", "-"}, "guidance/pointer": {"native-evidence-guidance", "--input-pointer", "/workflow"}, "guidance/output": {"native-evidence-guidance", "--output", "out.json"}, @@ -276,11 +282,11 @@ func testAgentWorkflowUsageErrorsPrecedeInput(t *testing.T) { "guidance/bad color": {"native-evidence-guidance", "--format", "text", "--color", "always"}, "guidance/post-command layout": {"native-evidence-guidance", "--json-layout", "compact"}, "guidance/surplus operand": {"native-evidence-guidance", "surplus"}, - "global/missing layout value planner": {"--json-layout", "change-workflow-plan", "--input", "-"}, + "global/missing layout value planner": {"--json-layout", "change", "plan", "--input", "-"}, "global/missing layout value guidance": {"--json-layout", "native-evidence-guidance"}, - "global/bad layout planner": {"--json-layout", "dense", "change-workflow-plan", "--input", "-"}, + "global/bad layout planner": {"--json-layout", "dense", "change", "plan", "--input", "-"}, "global/bad layout guidance": {"--json-layout", "dense", "native-evidence-guidance"}, - "global/duplicate layout planner": {"--json-layout", "pretty", "--json-layout", "compact", "change-workflow-plan", "--input", "-"}, + "global/duplicate layout planner": {"--json-layout", "pretty", "--json-layout", "compact", "change", "plan", "--input", "-"}, "global/duplicate layout guidance": {"--json-layout", "pretty", "--json-layout", "compact", "native-evidence-guidance"}, } if got, want := len(cases)-2, 45; got != want { @@ -299,8 +305,8 @@ func testAgentWorkflowUsageErrorsPrecedeInput(t *testing.T) { func testAgentWorkflowHelpClasses(t *testing.T) { valid := [][]string{ - {"change-workflow-plan", "--help"}, - {"change-workflow-plan", "-h"}, + {"change", "plan", "--help"}, + {"change", "plan", "-h"}, {"native-evidence-guidance", "--help"}, {"native-evidence-guidance", "-h"}, } @@ -314,10 +320,10 @@ func testAgentWorkflowHelpClasses(t *testing.T) { args []string want string }{ - {args: []string{"--json-layout", "compact", "change-workflow-plan", "--help"}, want: "--json-layout is valid only for JSON command output"}, + {args: []string{"--json-layout", "compact", "change", "plan", "--help"}, want: "--json-layout is valid only for JSON command output"}, {args: []string{"--json-layout", "compact", "native-evidence-guidance", "--help"}, want: "--json-layout is valid only for JSON command output"}, - {args: []string{"change-workflow-plan", "--input", "-", "--help"}, want: "help accepts no additional arguments"}, - {args: []string{"change-workflow-plan", "-h", "--input", "-"}, want: "help accepts no additional arguments"}, + {args: []string{"change", "plan", "--input", "-", "--help"}, want: "help accepts no additional arguments"}, + {args: []string{"change", "plan", "-h", "--input", "-"}, want: "help accepts no additional arguments"}, {args: []string{"native-evidence-guidance", "--format", "text", "--help"}, want: "help accepts no additional arguments"}, {args: []string{"native-evidence-guidance", "-h", "--format", "text"}, want: "help accepts no additional arguments"}, } @@ -330,7 +336,7 @@ func testAgentWorkflowHelpClasses(t *testing.T) { t.Run("help_like_input_paths_are_values", func(t *testing.T) { for _, path := range []string{"--help", "-h"} { - status, stdout, stderr := executeAgentWorkflowCLI(t, []string{"change-workflow-plan", "--input", path}, panicReader{}, PresentationCapabilities{StdoutIsTTY: true}) + status, stdout, stderr := executeAgentWorkflowCLI(t, []string{"change", "plan", "--input", path}, panicReader{}, PresentationCapabilities{StdoutIsTTY: true}) if status != 1 || stdout != "" || stderr == "" || strings.Contains(stderr, "help accepts no additional arguments") { t.Fatalf("path=%q status=%d stdout=%q stderr=%q", path, status, stdout, stderr) } @@ -357,7 +363,7 @@ func testAgentWorkflowTerminalCapabilityProduct(t *testing.T) { stdin func() io.Reader plain string }{ - {name: "change-workflow-plan", args: []string{"change-workflow-plan", "--input", "-", "--format", "text", "--color", "auto"}, stdin: func() io.Reader { return strings.NewReader(validChangeWorkflowInput) }, plain: wantWorkflowText}, + {name: "change plan", args: []string{"change", "plan", "--input", "-", "--format", "text", "--color", "auto"}, stdin: func() io.Reader { return strings.NewReader(validChangeWorkflowInput) }, plain: wantWorkflowText}, {name: "native-evidence-guidance", args: []string{"native-evidence-guidance", "--format", "text", "--color", "auto"}, stdin: func() io.Reader { return strings.NewReader("unread") }, plain: wantGuidanceText}, } for _, command := range commands { diff --git a/internal/app/app.go b/internal/app/app.go index 2e21bae..df1b8be 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -158,6 +158,8 @@ func RunWithRendererAndCapabilities(ctx context.Context, args []string, stdin io return writeJSON(output, 0, err, stdout, stderr) case commandRunnerPilotAdmission: return runPilotAdmission(args[1:], stdin, stdout, stderr) + case commandRunnerProjectStatus: + return runProjectStatus(ctx, args[0], args[1:], stdout, stderr, capabilities) case commandRunnerProjectStructure: return runProjectStructure(args[1:], stdin, stdout, stderr, renderer) case commandRunnerTypeScriptPublicAPISurfaces: diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index fdcfe85..ca36cd8 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -24,7 +24,7 @@ import ( ) const ( - cliContractPublicABISHA256 = "b5ea707ee5851cea6b75442e4faf20e93879371faf3636e96a98ccd23b527463" + cliContractPublicABISHA256 = "17b7f185adb80bcdeb6bc5e6a08cf0b95e6b6f8766d2cba634d08f59b7821e0b" maxAggregateFileReadBytesForContractTest = 64 << 20 maxPackageManifestBytesForContractTest = 256 << 10 maxSourceFileBytesForContractTest = 8 << 20 @@ -792,8 +792,9 @@ func TestProofkitContractMapRoutesRequiredInputCommands(t *testing.T) { if command.Input != "required" { continue } - if !strings.Contains(document, "`"+command.Command+"`") { - t.Fatalf("docs/proofkit-contract-map.md does not route required-input command %s", command.Command) + route := strings.Join(effectiveContractRoute(command), " ") + if !strings.Contains(document, "`"+route+"`") { + t.Fatalf("docs/proofkit-contract-map.md does not route required-input command %s through %s", command.Command, route) } } } @@ -1525,11 +1526,13 @@ func TestDescriptorFlagConstraintsAreRenderedTruthfully(t *testing.T) { "adoption-contract-envelope": "agentic-proofkit adoption-contract-envelope --input [--agent-envelope] [--checked-scope ] [--guidance-mode ] [--materialization-manifest] --mode [--pilot ] [--touched-rule-id ]", "conformance-profile": "agentic-proofkit conformance-profile --input [--format ] [--input-pointer ] (--list | --profile | --verify)", "json-report-cli-adapter-source": "agentic-proofkit json-report-cli-adapter-source [--format ] --language ", + "next": "agentic-proofkit next [--color ] [--format ] --repo-root ", "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 ] [--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 )", "repository-inventory": "agentic-proofkit repository-inventory --repo-root ", "stack-preset": "agentic-proofkit stack-preset --preset ", + "status": "agentic-proofkit status [--color ] [--format ] --repo-root ", "typescript-public-api-surfaces": "agentic-proofkit typescript-public-api-surfaces --input [--input-pointer ] --repo-root ", } constrainedCount := 0 @@ -1728,12 +1731,13 @@ func assertCLIContractSchema(t *testing.T) { if err := json.Unmarshal(processContract["commandRouteGrammar"], &routeGrammar); err != nil { t.Fatalf("decode command route grammar: %v", err) } - assertKeys(t, "CLI command route grammar", keysAny(routeGrammar), []string{"ambiguityPolicy", "maximumTokens", "minimumTokens", "separator", "tokenPattern"}) + assertKeys(t, "CLI command route grammar", keysAny(routeGrammar), []string{"ambiguityPolicy", "maximumTokens", "minimumTokens", "omittedRoutePolicy", "separator", "tokenPattern"}) if routeGrammar["minimumTokens"] != float64(commandroute.MinimumTokens) || routeGrammar["maximumTokens"] != float64(commandroute.MaximumTokens) || routeGrammar["separator"] != commandroute.Separator || routeGrammar["tokenPattern"] != commandroute.TokenPattern || - routeGrammar["ambiguityPolicy"] != commandroute.AmbiguityPolicy { + routeGrammar["ambiguityPolicy"] != commandroute.AmbiguityPolicy || + routeGrammar["omittedRoutePolicy"] != commandroute.OmittedRoutePolicy { t.Fatalf("CLI command route grammar does not match runtime owner: %#v", routeGrammar) } var globalOptions map[string]any @@ -1896,10 +1900,11 @@ func assertCLIContractSchema(t *testing.T) { } func effectiveContractRoute(command cliContractCommand) []string { - if len(command.Route) == 0 { - return []string{command.Command} + route, ok := commandroute.Resolve(command.Command, command.Route) + if !ok { + return nil } - return append([]string(nil), command.Route...) + return route } func commandByContractID(commands []cliContractCommand, commandID string) cliContractCommand { diff --git a/internal/app/cli_output_witness_contract_test.go b/internal/app/cli_output_witness_contract_test.go index fc3a777..429c487 100644 --- a/internal/app/cli_output_witness_contract_test.go +++ b/internal/app/cli_output_witness_contract_test.go @@ -402,6 +402,14 @@ func rootDistinctOutputContractExpectations() []rootDistinctOutputContractExpect SelectorTest: "TestAgentRouteEnvelopeModesUseExactRootShapes", ExecutableCommand: "go test ./internal/app -run '^TestAgentRouteEnvelopeModesUseExactRootShapes$'", }, + { + Command: "next", + NativeSourceForm: "nativeSource", + NativeSourcePaths: []string{"internal/command/projectstatus"}, + SelectorPath: "internal/app/project_status_command_test.go", + SelectorTest: "TestNextOutputUsesExactRootShape", + ExecutableCommand: "go test ./internal/app -run '^TestNextOutputUsesExactRootShape$'", + }, { Command: "pilot-admission", NativeSourceForm: "nativeSources", @@ -418,6 +426,14 @@ func rootDistinctOutputContractExpectations() []rootDistinctOutputContractExpect SelectorTest: "TestRequirementAuthoringPlanOutputUsesVersionedRootShape", ExecutableCommand: "go test ./internal/app -run '^TestRequirementAuthoringPlanOutputUsesVersionedRootShape$'", }, + { + Command: "status", + NativeSourceForm: "nativeSource", + NativeSourcePaths: []string{"internal/command/projectstatus"}, + SelectorPath: "internal/app/project_status_command_test.go", + SelectorTest: "TestStatusOutputUsesExactRootShape", + ExecutableCommand: "go test ./internal/app -run '^TestStatusOutputUsesExactRootShape$'", + }, { Command: "self-check", NativeSourceForm: "nativeSource", @@ -471,6 +487,16 @@ func rootDistinctOutputBindingMappings() []rootDistinctOutputBindingMapping { ScenarioID: "proofkit.package-boundary.cli-output-root-witnesses", SelectorTest: "TestStandaloneMultiVariantCommandsUseExactRootShapes", }, + { + RequirementID: "REQ-PROOFKIT-PACKAGE-002", + ScenarioID: "proofkit.package-boundary.project-status-output-root-witnesses", + SelectorTest: "TestNextOutputUsesExactRootShape", + }, + { + RequirementID: "REQ-PROOFKIT-PACKAGE-002", + ScenarioID: "proofkit.package-boundary.project-status-output-root-witnesses", + SelectorTest: "TestStatusOutputUsesExactRootShape", + }, { RequirementID: "REQ-PROOFKIT-QUALITY-004", ScenarioID: "proofkit.supply-chain-quality.adoption-materialization-cli-abi", @@ -511,6 +537,16 @@ func rootDistinctOutputBindingMappings() []rootDistinctOutputBindingMapping { ScenarioID: "proofkit.supply-chain-quality.cli-abi-golden", SelectorTest: "TestStandaloneMultiVariantCommandsUseExactRootShapes", }, + { + RequirementID: "REQ-PROOFKIT-QUALITY-004", + ScenarioID: "proofkit.supply-chain-quality.project-status-cli-abi", + SelectorTest: "TestNextOutputUsesExactRootShape", + }, + { + RequirementID: "REQ-PROOFKIT-QUALITY-004", + ScenarioID: "proofkit.supply-chain-quality.project-status-cli-abi", + SelectorTest: "TestStatusOutputUsesExactRootShape", + }, { RequirementID: "REQ-PROOFKIT-SPEC-011", ScenarioID: "proofkit.spec-proof-core.adoption-contract-envelope-cli-abi", diff --git a/internal/app/command_contract_generated.go b/internal/app/command_contract_generated.go index 9945217..57c3cac 100644 --- a/internal/app/command_contract_generated.go +++ b/internal/app/command_contract_generated.go @@ -1,7 +1,7 @@ // Code generated by internal/tools/commandcontractgen; DO NOT EDIT. package app -const commandContractSourceSHA256 = "ea2fbade9c0651e7742b852a3f11433afee58a5a26d997a2cb00400db777c488" +const commandContractSourceSHA256 = "5306b7223c5c0671871272f9790195fa1064de85c638f276493c110a6c3c51ed" type generatedCommandContractMetadata struct { InputContractSHA256 string @@ -12,19 +12,19 @@ type generatedCommandContractMetadata struct { } var generatedCommandContractMetadataByName = map[string]generatedCommandContractMetadata{ - "adopt-materialize-apply": {InputContractSHA256: "sha256:073bc4b983038d593e6a655104af51bb2a105974946ef5e2c9b857c68db6577f", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.apply-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:a3ff3ab6f2835ce6eb6bd69351adec171d722663d7d33aabaeade33e52f411af", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "apply"}}, - "adopt-materialize-plan": {InputContractSHA256: "sha256:e7f2b1339f8ab11f577872d409b4fec66c79a79febe8cef7cfd15b969c0c4de4", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.plan-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:02fc198c3a8eb0d03505f008c27e51c103b7b608eb3870e1757de529f697909a", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "plan"}}, - "adopt-materialize-recover": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:a24c3c8710e000e83f068a3d98bc33f0239076687c04d1d72d2457d8a389bef3", FlagChoices: map[string][]string{"--action": []string{"resume", "rollback"}, "--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "recover"}}, + "adopt-materialize-apply": {InputContractSHA256: "sha256:98539b75bf6d4caebc75d724e9201925a1235290791adcebc865c1b3c74976ee", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.apply-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:0d3c0d8ed58376ab4d7e55321016f547261fffd16a6a68c80fce79319b4388ac", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "apply"}}, + "adopt-materialize-plan": {InputContractSHA256: "sha256:65925b56d0332349c5048e9b1450ff0b7a3381224342df1a630128e3f4a2cde2", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.plan-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:e9df1aea8f422b097a28cfc5b395535637d905c7771b11bea805e32de976d11c", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "plan"}}, + "adopt-materialize-recover": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:89f7d0b152f092f2e14ad1b80b847fee5ffc8bffe12471899fcb2f05694d0eae", FlagChoices: map[string][]string{"--action": []string{"resume", "rollback"}, "--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "recover"}}, "adopt-plan": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:13e3392d9005c27fed3003a1d036123fe12a341dbe16a97c0cd9f8272fb83320", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}, "--mode": []string{"audit-from-code", "code-baseline", "fresh"}, "--stack": []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"}}, RouteTokens: []string{"adopt", "plan"}}, "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{}, RouteTokens: []string{"adoption-checklist"}}, "adoption-contract-envelope": {InputContractSHA256: "sha256:c310214676ff4b6f536a5bc9d687f681a7e71f73d7a03ac932707d8cd3905cdf", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.adoption-contract-envelope.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3efb2c5161fee16fd8ac6a40dcb6d9c41fbc23e468f60621436ae9e8076e0950", FlagChoices: map[string][]string{}, RouteTokens: []string{"adoption-contract-envelope"}}, "adoption-doctor": {InputContractSHA256: "sha256:efa9acfe32bff07f56d9dc9902530df2979794289bc2f7f547f7a108a7dd0f35", 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:8fdfc6608f197e633f042f20031ae1014872a90aa3daa66885ffcaddca994766", FlagChoices: map[string][]string{}, RouteTokens: []string{"adoption-doctor"}}, "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{}, RouteTokens: []string{"adoption-workflow-plan"}}, - "agent-route": {InputContractSHA256: "sha256:c00e832b4e9eac6b858eec46e810431c0a5c9f56c5c50f055f39ee024f50014c", InputSchemaSummary: []string{"availableInputs", "browserMode", "goal", "knownChangedPaths", "mode", "nonClaims", "observedReports", "openBrowser", "routeId", "schemaVersion", "root-shape-only definition proofkit.agent-route.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:a06b5434109090d8d949a900f61206274db54afdd8602e09b509b0f81e6e5a65", FlagChoices: map[string][]string{}, RouteTokens: []string{"agent-route"}}, + "agent-route": {InputContractSHA256: "sha256:c00e832b4e9eac6b858eec46e810431c0a5c9f56c5c50f055f39ee024f50014c", InputSchemaSummary: []string{"availableInputs", "browserMode", "goal", "knownChangedPaths", "mode", "nonClaims", "observedReports", "openBrowser", "routeId", "schemaVersion", "root-shape-only definition proofkit.agent-route.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:9589fd4f944365160fe15d83b9ef70e51d2e8335204857a284dde9d83061833d", FlagChoices: map[string][]string{}, RouteTokens: []string{"agent-route"}}, "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{}, RouteTokens: []string{"binding-partition"}}, "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{}, RouteTokens: []string{"branch-authority"}}, "capability-map-admission": {InputContractSHA256: "sha256:e49433f295c43c34d5d660ac9d656b117ed87208406b57723d25165ffec5d486", 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:bfa35fe1be210ab98f3620694ab63b52a9724f7b92cd9dcbfd7b01b2c6a3555e", FlagChoices: map[string][]string{}, RouteTokens: []string{"capability-map-admission"}}, - "change-workflow-plan": {InputContractSHA256: "sha256:e3124fc636b7f66b24daf8e1435cea11da15a741abeabe0cc3d3890b13c71625", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.change-workflow-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:cd035e9b71d83c341b1a937a18699fd727cb4b0d694983d715b064292ae4d8bd", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"change-workflow-plan"}}, + "change-workflow-plan": {InputContractSHA256: "sha256:e3124fc636b7f66b24daf8e1435cea11da15a741abeabe0cc3d3890b13c71625", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.change-workflow-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:cd035e9b71d83c341b1a937a18699fd727cb4b0d694983d715b064292ae4d8bd", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"change", "plan"}}, "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{}, RouteTokens: []string{"changed-path-set"}}, "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{}, RouteTokens: []string{"completion-criteria"}}, "conformance-profile": {InputContractSHA256: "sha256:10857de4cea06702bb4d35580046275d4b1f88821d287a4c57dabc187bda954e", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.conformance-profile.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:4654015b8b9055080c1d5528773462fab3fe81d40c7f3b9870e5dbec4dc98caf", FlagChoices: map[string][]string{}, RouteTokens: []string{"conformance-profile"}}, @@ -42,9 +42,10 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "migration-parity-admission": {InputContractSHA256: "sha256:0b36c0e68da3b857dac4b13e7b3bd523052459106133aa8c908a4352682e6c05", 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:8e0f8af2b205817f018b0fe133fe789661caa29007695e036bfcab63c1830f47", FlagChoices: map[string][]string{}, RouteTokens: []string{"migration-parity-admission"}}, "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{}, RouteTokens: []string{"migration-plan"}}, "native-evidence-guidance": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:c9306d800668ecea18aaced6a21334036a935570f267baed356e6a4888025c8d", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"native-evidence-guidance"}}, + "next": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:9dfa71617f9d727949a988bf726bcf0070460faa59565a8adc468994a36594c6", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"next"}}, "obligation-decision": {InputContractSHA256: "sha256:1dea2ed5c5066451d6d49b815cea99df2cdae2ef05d42fed16c8aeb45eb7f445", 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:96dc074f611bcc12e511bc803c548e4df623e2de869d3add29a3ea6386e04330", FlagChoices: map[string][]string{}, RouteTokens: []string{"obligation-decision"}}, "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{}, RouteTokens: []string{"package-runtime-dependency-admission"}}, - "pilot-admission": {InputContractSHA256: "sha256:a1d9116ce619f7d705349ff4ae44c0f4399a281ebaa9e7d62ea304ac57af59ba", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.pilot-admission.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:a4bb2558c381ba413ceaf203e669893275fc0bf99e5da843354792510d22ed8c", FlagChoices: map[string][]string{}, RouteTokens: []string{"pilot-admission"}}, + "pilot-admission": {InputContractSHA256: "sha256:a1d9116ce619f7d705349ff4ae44c0f4399a281ebaa9e7d62ea304ac57af59ba", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.pilot-admission.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:9880584ed1fce4a8cea4bbbe4d1fa7f1a69a060ba2ffecb9a44c9ecb18a450cd", FlagChoices: map[string][]string{}, RouteTokens: []string{"pilot-admission"}}, "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{}, RouteTokens: []string{"producer-policy-self-proof"}}, "proof-obligation-algebra": {InputContractSHA256: "sha256:4f176b6bc9bdbd0d96d65c071d66447d246665bda7a23269e7927f1d0b80b043", 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:f9ee9e56b349756c55856a2dab198e1ad85db70a468c38e3aeca73cfe2ed66f6", FlagChoices: map[string][]string{}, RouteTokens: []string{"proof-obligation-algebra"}}, "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{}, RouteTokens: []string{"proof-receipt-admission"}}, @@ -83,10 +84,11 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "selective-gate-evidence": {InputContractSHA256: "sha256:8aa178ab7ca7c475c23707bc4e15fd3f9f8d57acf6f6dcf279677e7769a45586", 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:723569262bb85d9674b2a78d3bcb6e9f4cab229b71e8c784ff1b804a7fcade71", FlagChoices: map[string][]string{}, RouteTokens: []string{"selective-gate-evidence"}}, "selective-gate-obligation-decision-input": {InputContractSHA256: "sha256:85761fcbc0ea94239d55bf379d0592a6ca814e6612a2d609a651f6cdaf8ca10a", 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:ab9dddabe975238d7019266c43350afa2df1a61d4c2eb7bc23afd520b588a2da", FlagChoices: map[string][]string{}, RouteTokens: []string{"selective-gate-obligation-decision-input"}}, "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{}, RouteTokens: []string{"selective-gate-plan"}}, - "self-check": {InputContractSHA256: "sha256:e58bd8b13055b4430f8fb8b9db07ee2bc2101749955754d55240956ebc19cb9c", 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:da85234836879ca800ae37239a3b542dd56fd21fe53df0524ea480e420fd77fc", FlagChoices: map[string][]string{}, RouteTokens: []string{"self-check"}}, + "self-check": {InputContractSHA256: "sha256:b614b659a5aec20214cf25158d5c7e360d43ee024b5ceaa3308995f259b32bb4", 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:757cc5ebaa9c4dd8f634f8462840f1cfccf62ec3dd55c495b13fc331fc33eb48", FlagChoices: map[string][]string{}, RouteTokens: []string{"self-check"}}, "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{}, RouteTokens: []string{"spec-overview-claims"}}, "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{}, RouteTokens: []string{"spec-proof-bundle-admission"}}, "stack-preset": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:ef5920f363a4a96dcac308ea8412260a06e64ba4876460a369aefb8983130a9d", FlagChoices: map[string][]string{"--preset": []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"}}, RouteTokens: []string{"stack-preset"}}, + "status": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:29cf593bf349fd5ec7276f28ce75817a44a8b12fc7aea5b4c055cfaf4188924c", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"status"}}, "test-evidence-inventory": {InputContractSHA256: "sha256:c2e26a02e127eec069275c9afa902a20cb9e062f4008db35e042d8fe0e3fdd60", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.test-evidence-inventory.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:06fd4be6a34e73708f8539fed04ec41b2a4c9c79cf0e0dcc311496c0e6aafcbf", FlagChoices: map[string][]string{}, RouteTokens: []string{"test-evidence-inventory"}}, "text-policy": {InputContractSHA256: "sha256:afc366b0bcdcb33d7b85d3347f832cb647de68cb737cfa81a52e55f5b4901038", 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:94031bd5ee75115e80ebe10cb8c3c7b77806dc349721ba550cacaa99ff8184e4", FlagChoices: map[string][]string{}, RouteTokens: []string{"text-policy"}}, "typescript-public-api-surfaces": {InputContractSHA256: "sha256:11ddef5ae86c8f5ff9fcb8cfac540c8ea680cd61233e54e9ba74563b28020e2a", 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:f26372435db16de436cd5483089d06d3723496574b28d80de97ee36ccddf1587", FlagChoices: map[string][]string{}, RouteTokens: []string{"typescript-public-api-surfaces"}}, diff --git a/internal/app/command_coverage_routes.go b/internal/app/command_coverage_routes.go index a2f5716..f75385c 100644 --- a/internal/app/command_coverage_routes.go +++ b/internal/app/command_coverage_routes.go @@ -100,6 +100,10 @@ var commandCoverageRoutes = map[string][]commandCoverageRoute{ packageFalsifierRoute("internal/command/nativeevidenceguidance/guidance_test.go", "TestGuidanceSlotPredicates", semanticRouteProof("nativeevidenceguidance.guidance_slot_predicates"), "Native evidence guidance must expose the complete fixed 22-slot repository-neutral evidence vocabulary in canonical order."), packageFalsifierRoute("internal/command/nativeevidenceguidance/guidance_test.go", "TestGuidancePurityPredicates", semanticRouteProof("nativeevidenceguidance.guidance_purity_predicates"), "Native evidence guidance must remain deterministic and return fresh caller-owned projections without ambient authority."), }, + "next": { + directCLIRoute("internal/app/project_status_command_test.go", "TestProjectStatusCLI", semanticRouteProof("project_status_command.next_whole_cli"), "Project next must preserve one owner-admitted non-executable action across JSON, text, color, and argument-admission paths."), + packageFalsifierRoute("internal/command/projectstatus/projectstatus_test.go", "TestEvaluateTotalStateActionTable", semanticRouteProof("projectstatus.evaluate_total_state_action_table"), "Project next must remain a total one-action projection for every admitted project state without claiming execution or completion."), + }, "obligation-decision": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/obligationdecision/obligationdecision_test.go", "TestBuildAdmitsSatisfiedBlockingObligationsAndRejectsMissingReceipt", semanticRouteProof("obligationdecision.build_admits_satisfied_blocking_obligations_and_rejects_missing_receipt"), "Obligation decision must fail blocking obligations that lack satisfying evidence states.")}, "package-runtime-dependency-admission": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/packageruntimedependency/package_runtime_dependency_test.go", "TestBuildAdmitsExternalRuntimeDependencyAndRejectsWorkspaceResolution", semanticRouteProof("package_runtime_dependency.build_admits_external_runtime_dependency_and_rejects_workspace_resolution"), "Package runtime dependency admission must reject local workspace resolution.")}, "pilot-admission": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/pilotadmission/pilotadmission_test.go", "TestBuildRejectsUnknownPilotContractField", semanticRouteProof("pilotadmission.build_rejects_unknown_pilot_contract_field"), "Pilot admission must reject malformed pilot contract records instead of silently accepting unknown policy fields.")}, @@ -150,6 +154,11 @@ var commandCoverageRoutes = map[string][]commandCoverageRoute{ "spec-overview-claims": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/specoverviewclaims/specoverviewclaims_test.go", "TestBuildRejectsInvalidOverviewClaimBoundaryFacts", semanticRouteProof("specoverviewclaims.build_rejects_invalid_overview_claim_boundary_facts"), "Spec overview claim admission must reject invalid path, extraction, digest, marker, rationale, and non-claim boundary facts."), packageFalsifierRoute("internal/command/specoverviewclaims/specoverviewclaims_test.go", "TestBuildRejectsNonDurableRequirementCitationsForEveryNonDurableKind", semanticRouteProof("specoverviewclaims.build_rejects_non_durable_requirement_citations_for_every_non_durable_kind"), "Spec overview claim admission must reject every non-durable claim kind when it carries requirement citations.")}, "spec-proof-bundle-admission": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/specproofbundleadmission/specproofbundleadmission_test.go", "TestBuildRejectsForgedReceiptAdmissionChild", semanticRouteProof("specproofbundleadmission.build_rejects_forged_receipt_admission_child"), "Spec proof bundle admission must reject forged child receipt admission reports.")}, "stack-preset": {directCLIRoute("internal/app/command_coverage_test.go", "TestNoInputCommandsHaveCommandSpecificBehavior", semanticRouteProof("command_coverage.no_input_commands_have_command_specific_behavior"), "Stack preset CLI route must emit JSON and reject unknown preset flags."), packageFalsifierRoute("internal/command/stackpreset/stackpreset_test.go", "TestPresetInventoryIsCompleteDeterministicAndDefensivelyCopied", semanticRouteProof("stackpreset.preset_inventory_is_complete_deterministic_and_defensively_copied"), "Stack preset inventory must keep preset ids aligned with complete non-empty profile records and defensive copies."), packageFalsifierRoute("internal/command/stackpreset/stackpreset_test.go", "TestUnknownPresetIsRejected", semanticRouteProof("stackpreset.unknown_preset_is_rejected"), "Stack preset package API must reject unknown preset ids.")}, + "status": { + directCLIRoute("internal/app/project_status_command_test.go", "TestProjectStatusCLI", semanticRouteProof("project_status_command.status_whole_cli"), "Project status must preserve owner-admitted bounded classification across JSON, text, color, and pre-I/O argument admission paths."), + packageFalsifierRoute("internal/command/projectstatus/inspect_test.go", "TestInspectClassifiesMaterializedProjectWithoutMutation", semanticRouteProof("projectstatus.inspect_classifies_materialized_project_without_mutation"), "Project status must classify absent, admitted, and stale materialized projects without mutating transaction state or disclosing repository paths."), + packageFalsifierRoute("internal/command/projectstatus/inspect_test.go", "TestInspectCohortValidationClosesCleanEpochABA", semanticRouteProof("projectstatus.inspect_cohort_validation_closes_clean_epoch_aba"), "Project status must reject a clean-state ABA when manifest or child content changes between its bounded observation passes."), + }, "test-evidence-inventory": { requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/testevidenceinventory/testevidenceinventory_test.go", "TestBuildRejectsIncompleteDeclaredOracleMetadataAndDuplicateFalsifier", semanticRouteProof("testevidenceinventory.build_rejects_incomplete_declared_oracle_metadata_and_duplicate_falsifier"), "Test evidence inventory must reject incomplete caller-declared oracle metadata and duplicate falsifier equivalence claims."), diff --git a/internal/app/command_coverage_test.go b/internal/app/command_coverage_test.go index d7b54c2..581bb30 100644 --- a/internal/app/command_coverage_test.go +++ b/internal/app/command_coverage_test.go @@ -532,6 +532,8 @@ func noInputRuntimeSmokeArgs(t *testing.T, descriptor commandDescriptor) ([]stri return []string{"json-report-cli-adapter-source", "--language", "typescript"}, true case "native-evidence-guidance": return []string{"native-evidence-guidance"}, true + case "next", "status": + return append(cloneStrings(descriptor.routeTokens), "--repo-root", t.TempDir()), true case "repository-inventory": return append(cloneStrings(descriptor.routeTokens), "--repo-root", t.TempDir()), true case "stack-preset": diff --git a/internal/app/command_descriptors.go b/internal/app/command_descriptors.go index 231a190..86b62ea 100644 --- a/internal/app/command_descriptors.go +++ b/internal/app/command_descriptors.go @@ -34,6 +34,7 @@ const ( commandRunnerJSONReportCLIAdapterSource commandRunner = "json_report_cli_adapter_source" commandRunnerPilotAdmission commandRunner = "pilot_admission" commandRunnerPlanning commandRunner = "planning" + commandRunnerProjectStatus commandRunner = "project_status" commandRunnerProjectStructure commandRunner = "project_structure" commandRunnerRequirementBrowserServer commandRunner = "requirement_browser_server" commandRunnerRequirementContextCompose commandRunner = "requirement_context_compose" @@ -124,6 +125,7 @@ var commandDescriptors = []commandDescriptor{ 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("native-evidence-guidance", commandInputNone, flags("--color", "--format"), modes("json", "text"), ownerDirs("nativeevidenceguidance"), withRunner(commandRunnerAgentWorkflow), withSemanticAppTests("TestAgentWorkflowCLITruthTable"), withFlagChoices("--color", "auto", "never"), withFlagChoices("--format", "json", "text"), withSingleOccurrenceFlags("--color")), + command("next", commandInputNone, flags("--color", "--format", "--repo-root"), modes("json", "text"), ownerDirs("projectstatus"), withRunner(commandRunnerProjectStatus), withSemanticAppTests("TestProjectStatusCLI"), withScopeClass(commandScopeExplicitFileSystemScan), withRequiredFlags("--repo-root"), withFlagChoices("--color", "auto", "never"), withFlagChoices("--format", "json", "text"), withFlagPresenceAndRequiredValue("--color", "--format", "text"), withSingleOccurrenceFlags("--color", "--repo-root")), 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")), command("pilot-admission", commandInputRequired, flags("--contract-envelope", "--input", "--input-pointer", "--pilot", "--stack-diverse"), modes("json"), ownerDirs("pilotadmission"), withRunner(commandRunnerPilotAdmission), withContractEnvelope(), withFlagValueRequirement("--pilot", "all", "--contract-envelope")), @@ -169,6 +171,7 @@ var commandDescriptors = []commandDescriptor{ command("spec-overview-claims", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("specoverviewclaims")), command("spec-proof-bundle-admission", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("specproofbundleadmission")), command("stack-preset", commandInputNone, flags("--preset"), modes("json"), ownerDirs("stackpreset"), withRunner(commandRunnerStackPreset), withSemanticAppTests("TestNoInputCommandsHaveCommandSpecificBehavior"), withRequiredFlags("--preset")), + command("status", commandInputNone, flags("--color", "--format", "--repo-root"), modes("json", "text"), ownerDirs("projectstatus"), withRunner(commandRunnerProjectStatus), withSemanticAppTests("TestProjectStatusCLI"), withScopeClass(commandScopeExplicitFileSystemScan), withRequiredFlags("--repo-root"), withFlagChoices("--color", "auto", "never"), withFlagChoices("--format", "json", "text"), withFlagPresenceAndRequiredValue("--color", "--format", "text"), withSingleOccurrenceFlags("--color", "--repo-root")), 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")), @@ -197,6 +200,7 @@ var knownCommandRunners = map[commandRunner]struct{}{ commandRunnerJSONReportCLIAdapterSource: {}, commandRunnerPilotAdmission: {}, commandRunnerPlanning: {}, + commandRunnerProjectStatus: {}, commandRunnerProjectStructure: {}, commandRunnerRequirementBrowserServer: {}, commandRunnerRequirementContextCompose: {}, diff --git a/internal/app/command_family_catalog_generated.go b/internal/app/command_family_catalog_generated.go index 2c54238..783a9a3 100644 --- a/internal/app/command_family_catalog_generated.go +++ b/internal/app/command_family_catalog_generated.go @@ -1,7 +1,7 @@ // Code generated by internal/tools/commandfamilygen; DO NOT EDIT. package app -const commandFamilyCatalogSourceSHA256 = "43f2fd15005e84d9e999de262d6a5f6ec2534f2fb0b94975fc2fb4f78ed9b82f" +const commandFamilyCatalogSourceSHA256 = "9f9103412437d96896976af0429bf7ed2bd21fdd878010f13eda22cb05827f53" func generatedCommandFamilyCatalog() commandFamilyCatalog { return commandFamilyCatalog{ @@ -13,6 +13,7 @@ func generatedCommandFamilyCatalog() commandFamilyCatalog { {ID: "cli-metadata-and-conformance", Label: "CLI metadata and conformance", Purpose: "Expose, route, generate, and self-check CLI contract surfaces.", Commands: []string{"agent-route", "conformance-profile", "help", "json-report-cli-adapter-source", "self-check"}}, {ID: "deployment-and-readiness", Label: "Deployment and readiness", Purpose: "Admit deployment evidence and bounded closeout decisions.", Commands: []string{"branch-authority", "completion-criteria", "deployment-evidence-admission", "readiness-closeout"}}, {ID: "migration-and-retirement", Label: "Migration and retirement", Purpose: "Prove parity and plan retirement of consumer-local infrastructure.", Commands: []string{"migration-parity-admission", "migration-plan"}}, + {ID: "project-state-navigation", Label: "Project state navigation", Purpose: "Classify a materialized project and expose one bounded next action.", Commands: []string{"next", "status"}}, {ID: "proof-artifact-governance", Label: "Proof artifact governance", Purpose: "Govern custom rules, document lifecycle, freshness, and witness plans.", Commands: []string{"custom-rule-boundary", "document-lifecycle-boundary", "rendered-artifact-freshness", "witness-plan", "witness-scheduler-plan"}}, {ID: "proof-binding-topology", Label: "Proof binding topology", Purpose: "Admit, resolve, partition, and inspect requirement proof topology.", Commands: []string{"binding-partition", "evidence-graph", "proof-slice", "requirement-bindings", "requirement-proof-resolver", "requirement-proof-source-set", "spec-proof-bundle-admission"}}, {ID: "receipt-authority", Label: "Receipt authority", Purpose: "Admit receipts, producer compatibility, currentness, and trust classes.", Commands: []string{"producer-policy-self-proof", "proof-receipt-admission", "receipt-currentness-scope", "receipt-producer-admission", "receipt-trust-class"}}, diff --git a/internal/app/project_navigation_version_edge_test.go b/internal/app/project_navigation_version_edge_test.go new file mode 100644 index 0000000..429cd77 --- /dev/null +++ b/internal/app/project_navigation_version_edge_test.go @@ -0,0 +1,460 @@ +package app + +import ( + "bytes" + "crypto/sha256" + "encoding/json" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/commandroute" + "github.com/research-engineering/agentic-proofkit/internal/tools/releasechange" +) + +const projectNavigationVersionEdgePath = "internal/app/testdata/v0.9-wire-observations.json" +const frozenProjectNavigationPredecessorPath = "internal/app/testdata/v0.8-wire-observations.json" +const frozenProjectNavigationPredecessorSHA256 = "ed0651c53c015c00d8ed7a0db681a213e9df6248302c5f12fc898e4b6a82c5ab" +const frozenProjectNavigationCommandContractPath = "internal/app/testdata/releases/v0.8.0/preserved-command-contracts.json" +const frozenProjectNavigationCommandContractSHA256 = "907259153bb1e45e982295ec6081b40eb9f02b219c25af6004eb8c29a12c328a" + +type projectNavigationVersionEdge struct { + AddedCommandContracts []versionEdgeCommandContract `json:"addedCommandContracts"` + AdditionChangeIDs []string `json:"additionChangeIds"` + BreakingChangeIDs []string `json:"breakingChangeIds"` + ChangeClass string `json:"changeClass"` + ChangeRecordRef string `json:"changeRecordRef"` + ChangeRecordSHA256 string `json:"changeRecordSha256"` + ChangedCommandRoutes []versionEdgeRouteReplacement `json:"changedCommandRoutes"` + CommandContractSelection string `json:"commandContractSelection"` + CurrentPublicABISHA256 string `json:"currentPublicAbiSha256"` + EdgeID string `json:"edgeId"` + EvidenceClass string `json:"evidenceClass"` + MigrationSteps []string `json:"migrationSteps"` + NonClaims []string `json:"nonClaims"` + ProcessContractChanges []versionEdgeProcessChange `json:"processContractChanges"` + PreviousPublicABISHA256 string `json:"previousPublicAbiSha256"` + PreviousVersion string `json:"previousVersion"` + SchemaVersion int `json:"schemaVersion"` + Version string `json:"version"` +} + +type versionEdgeProcessChange struct { + ChangeID string `json:"changeId"` + CurrentValue string `json:"currentValue"` + JSONPointer string `json:"jsonPointer"` + PreviousState string `json:"previousState"` +} + +type versionEdgeRouteReplacement struct { + Command string `json:"command"` + CurrentRoute []string `json:"currentRoute"` + PreservedInputContract versionEdgeContractIdentity `json:"preservedInputContract"` + PreservedOutputContract versionEdgeContractIdentity `json:"preservedOutputContract"` + PreviousRoute []string `json:"previousRoute"` +} + +type frozenProjectNavigationCommandContract struct { + Command string `json:"command"` + CommandRouteGrammar frozenCommandRouteGrammar `json:"commandRouteGrammar"` + InputContract versionEdgeContractIdentity `json:"inputContract"` + NonClaims []string `json:"nonClaims"` + ObservationKind string `json:"observationKind"` + OutputContract versionEdgeContractIdentity `json:"outputContract"` + PublicABISHA256 string `json:"publicAbiSha256"` + ReleaseVersion string `json:"releaseVersion"` + Route []string `json:"route"` + SchemaVersion int `json:"schemaVersion"` +} + +type frozenCommandRouteGrammar struct { + AmbiguityPolicy string `json:"ambiguityPolicy"` + MaximumTokens int `json:"maximumTokens"` + MinimumTokens int `json:"minimumTokens"` + Separator string `json:"separator"` + TokenPattern string `json:"tokenPattern"` +} + +func TestProjectNavigationVersionEdgeClosesPublicRoutes(t *testing.T) { + record := readProjectNavigationVersionEdge(t) + root := repoRoot(t) + if err := validateProjectNavigationVersionEdge(record, root, root, currentCLIContractPublicABISHA256(t)); err != nil { + t.Fatal(err) + } + assertProjectNavigationRouteCutover(t) + + mutants := []func(*projectNavigationVersionEdge){ + func(value *projectNavigationVersionEdge) { value.CurrentPublicABISHA256 += "0" }, + func(value *projectNavigationVersionEdge) { + value.PreviousPublicABISHA256 = value.CurrentPublicABISHA256 + }, + func(value *projectNavigationVersionEdge) { value.ChangeClass = "compatible" }, + func(value *projectNavigationVersionEdge) { + value.AddedCommandContracts = value.AddedCommandContracts[1:] + }, + func(value *projectNavigationVersionEdge) { + value.AddedCommandContracts[0].Route = []string{"project-next"} + }, + func(value *projectNavigationVersionEdge) { + value.AddedCommandContracts[1].OutputContract.ContractSHA256 += "0" + }, + func(value *projectNavigationVersionEdge) { + value.ChangedCommandRoutes[0].PreviousRoute = []string{"change", "plan"} + }, + func(value *projectNavigationVersionEdge) { + value.ChangedCommandRoutes[0].CurrentRoute = []string{"change-workflow-plan"} + }, + func(value *projectNavigationVersionEdge) { + value.ChangedCommandRoutes[0].PreservedInputContract.ContractID += ".drift" + }, + func(value *projectNavigationVersionEdge) { + value.ChangedCommandRoutes[0].Command = "different-command" + }, + func(value *projectNavigationVersionEdge) { + value.ChangedCommandRoutes[0].PreservedInputContract.ContractSHA256 += "0" + }, + func(value *projectNavigationVersionEdge) { + value.ChangedCommandRoutes[0].PreservedOutputContract.ContractID += ".drift" + }, + func(value *projectNavigationVersionEdge) { + value.ChangedCommandRoutes[0].PreservedOutputContract.ContractSHA256 += "0" + }, + func(value *projectNavigationVersionEdge) { value.ChangedCommandRoutes = nil }, + func(value *projectNavigationVersionEdge) { value.BreakingChangeIDs = nil }, + func(value *projectNavigationVersionEdge) { value.AdditionChangeIDs = value.AdditionChangeIDs[1:] }, + func(value *projectNavigationVersionEdge) { value.MigrationSteps = nil }, + func(value *projectNavigationVersionEdge) { value.ChangeRecordSHA256 += "0" }, + func(value *projectNavigationVersionEdge) { value.CommandContractSelection = "all_digest_changes" }, + func(value *projectNavigationVersionEdge) { value.ProcessContractChanges = nil }, + func(value *projectNavigationVersionEdge) { value.ProcessContractChanges[0].CurrentValue = "different" }, + func(value *projectNavigationVersionEdge) { value.NonClaims = nil }, + } + for index, mutate := range mutants { + t.Run(fmt.Sprintf("mutant-%d", index), func(t *testing.T) { + value := cloneProjectNavigationVersionEdge(record) + mutate(&value) + if err := validateProjectNavigationVersionEdge(value, root, root, currentCLIContractPublicABISHA256(t)); err == nil { + t.Fatal("project navigation version-edge mutant was admitted") + } + }) + } +} + +func TestProjectNavigationVersionEdgeRejectsCoordinatedChangeRecordDrift(t *testing.T) { + record := readProjectNavigationVersionEdge(t) + content, err := os.ReadFile(filepath.Join(repoRoot(t), record.ChangeRecordRef)) + if err != nil { + t.Fatal(err) + } + value, err := admission.DecodeJSON(bytes.NewReader(content), int64(len(content))) + if err != nil { + t.Fatal(err) + } + root := value.(map[string]any) + root["breakingChanges"].([]any)[0].(map[string]any)["changeId"] = "proofkit.agent-workflow.change-plan-route.drift" + mutantContent, err := json.MarshalIndent(root, "", " ") + if err != nil { + t.Fatal(err) + } + mutantContent = append(mutantContent, '\n') + mutantRoot := t.TempDir() + path := filepath.Join(mutantRoot, filepath.FromSlash(record.ChangeRecordRef)) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, mutantContent, 0o600); err != nil { + t.Fatal(err) + } + mutant := cloneProjectNavigationVersionEdge(record) + digest := sha256.Sum256(mutantContent) + mutant.ChangeRecordSHA256 = fmt.Sprintf("sha256:%x", digest) + if err := validateProjectNavigationVersionEdge(mutant, repoRoot(t), mutantRoot, currentCLIContractPublicABISHA256(t)); err == nil || !strings.Contains(err.Error(), "contradicts") { + t.Fatalf("coordinated change-record mutant error=%v, want inventory contradiction", err) + } +} + +func TestProjectNavigationVersionEdgePreservesFrozenPredecessor(t *testing.T) { + content, err := os.ReadFile(filepath.Join(repoRoot(t), frozenProjectNavigationPredecessorPath)) + if err != nil { + t.Fatal(err) + } + digest := sha256.Sum256(content) + if got := fmt.Sprintf("%x", digest); got != frozenProjectNavigationPredecessorSHA256 { + t.Fatalf("frozen predecessor digest=%s, want %s", got, frozenProjectNavigationPredecessorSHA256) + } + frozen := readFrozenProjectNavigationCommandContract(t) + record := readProjectNavigationVersionEdge(t) + replacement := record.ChangedCommandRoutes[0] + if record.PreviousVersion != frozen.ReleaseVersion || record.PreviousPublicABISHA256 != frozen.PublicABISHA256 || replacement.Command != frozen.Command || !slices.Equal(replacement.PreviousRoute, frozen.Route) || replacement.PreservedInputContract != frozen.InputContract || replacement.PreservedOutputContract != frozen.OutputContract { + t.Fatalf("version edge does not preserve the frozen predecessor contract: edge=%#v frozen=%#v", replacement, frozen) + } +} + +func readFrozenProjectNavigationCommandContract(t *testing.T) frozenProjectNavigationCommandContract { + t.Helper() + content, err := os.ReadFile(filepath.Join(repoRoot(t), frozenProjectNavigationCommandContractPath)) + if err != nil { + t.Fatal(err) + } + digest := sha256.Sum256(content) + if got := fmt.Sprintf("%x", digest); got != frozenProjectNavigationCommandContractSHA256 { + t.Fatalf("frozen command contract digest=%s, want %s", got, frozenProjectNavigationCommandContractSHA256) + } + value, err := admission.DecodeJSON(bytes.NewReader(content), int64(len(content))) + if err != nil { + t.Fatal(err) + } + root, ok := value.(map[string]any) + if !ok { + t.Fatal("frozen command contract observation must be an object") + } + assertExactObjectKeys(t, root, []string{"command", "commandRouteGrammar", "inputContract", "nonClaims", "observationKind", "outputContract", "publicAbiSha256", "releaseVersion", "route", "schemaVersion"}, "frozen command contract observation") + assertExactObjectKeys(t, root["commandRouteGrammar"].(map[string]any), []string{"ambiguityPolicy", "maximumTokens", "minimumTokens", "separator", "tokenPattern"}, "frozen command route grammar") + assertExactObjectKeys(t, root["inputContract"].(map[string]any), []string{"contractId", "contractSha256"}, "frozen input contract") + assertExactObjectKeys(t, root["outputContract"].(map[string]any), []string{"contractId", "contractSha256"}, "frozen output contract") + var record frozenProjectNavigationCommandContract + if err := json.Unmarshal(content, &record); err != nil { + t.Fatal(err) + } + wantGrammar := frozenCommandRouteGrammar{ + AmbiguityPolicy: "no_route_is_prefix_of_another", + MaximumTokens: 4, + MinimumTokens: 1, + Separator: " ", + TokenPattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$", + } + if record.SchemaVersion != 1 || record.ObservationKind != "proofkit.frozen-command-contract-observation" || record.ReleaseVersion != "0.8.0" || record.PublicABISHA256 != "sha256:b5ea707ee5851cea6b75442e4faf20e93879371faf3636e96a98ccd23b527463" || record.Command != "change-workflow-plan" || record.CommandRouteGrammar != wantGrammar || !slices.Equal(record.Route, []string{"change-workflow-plan"}) || !slices.Equal(record.NonClaims, []string{"This frozen source observation does not authenticate registry publication, provider state, consumer migration, or runtime compatibility."}) { + t.Fatalf("frozen command contract observation is invalid: %#v", record) + } + return record +} + +func readProjectNavigationVersionEdge(t *testing.T) projectNavigationVersionEdge { + t.Helper() + content, err := os.ReadFile(filepath.Join(repoRoot(t), projectNavigationVersionEdgePath)) + if err != nil { + t.Fatal(err) + } + value, err := admission.DecodeJSON(bytes.NewReader(content), int64(len(content))) + if err != nil { + t.Fatal(err) + } + root, ok := value.(map[string]any) + if !ok { + t.Fatal("project navigation version edge must be an object") + } + assertExactObjectKeys(t, root, []string{"addedCommandContracts", "additionChangeIds", "breakingChangeIds", "changeClass", "changeRecordRef", "changeRecordSha256", "changedCommandRoutes", "commandContractSelection", "currentPublicAbiSha256", "edgeId", "evidenceClass", "migrationSteps", "nonClaims", "previousPublicAbiSha256", "previousVersion", "processContractChanges", "schemaVersion", "version"}, "project navigation version edge") + for index, raw := range root["addedCommandContracts"].([]any) { + item := raw.(map[string]any) + assertExactObjectKeys(t, item, []string{"command", "outputContract", "route"}, fmt.Sprintf("added command contract %d", index)) + assertExactObjectKeys(t, item["outputContract"].(map[string]any), []string{"contractId", "contractSha256"}, fmt.Sprintf("added command contract %d output", index)) + } + for index, raw := range root["changedCommandRoutes"].([]any) { + item := raw.(map[string]any) + assertExactObjectKeys(t, item, []string{"command", "currentRoute", "preservedInputContract", "preservedOutputContract", "previousRoute"}, fmt.Sprintf("changed command route %d", index)) + for _, field := range []string{"preservedInputContract", "preservedOutputContract"} { + assertExactObjectKeys(t, item[field].(map[string]any), []string{"contractId", "contractSha256"}, fmt.Sprintf("changed command route %d %s", index, field)) + } + } + for index, raw := range root["processContractChanges"].([]any) { + assertExactObjectKeys(t, raw.(map[string]any), []string{"changeId", "currentValue", "jsonPointer", "previousState"}, fmt.Sprintf("process contract change %d", index)) + } + var record projectNavigationVersionEdge + if err := json.Unmarshal(content, &record); err != nil { + t.Fatal(err) + } + return record +} + +func validateProjectNavigationVersionEdge(record projectNavigationVersionEdge, contractRoot, changeRecordRoot, currentABI string) error { + if record.SchemaVersion != 1 || record.EdgeID != "proofkit.public-wire.0.8.0-to-0.9.0" || record.EvidenceClass != "owner_authored_current_version_edge_observation" { + return fmt.Errorf("project navigation version-edge identity is invalid") + } + if record.PreviousVersion != "0.8.0" || record.Version != "0.9.0" || record.ChangeClass != "breaking" { + return fmt.Errorf("project navigation version-edge release identity is invalid") + } + if record.CommandContractSelection != "added_commands_changed_routes_and_process_contract" { + return fmt.Errorf("project navigation command-contract selection policy is invalid") + } + if record.PreviousPublicABISHA256 != "sha256:b5ea707ee5851cea6b75442e4faf20e93879371faf3636e96a98ccd23b527463" || record.CurrentPublicABISHA256 != "sha256:"+currentABI || record.PreviousPublicABISHA256 == record.CurrentPublicABISHA256 { + return fmt.Errorf("project navigation version-edge ABI identity is invalid") + } + currentAdded, err := currentVersionEdgeCommandContracts(contractRoot, []string{"next", "status"}) + if err != nil { + return err + } + if !slices.EqualFunc(record.AddedCommandContracts, currentAdded, equalVersionEdgeCommandContract) { + return fmt.Errorf("project navigation added command contracts are not exact") + } + currentRoute, err := currentVersionEdgeRouteReplacement(contractRoot) + if err != nil { + return err + } + if !slices.EqualFunc(record.ChangedCommandRoutes, []versionEdgeRouteReplacement{currentRoute}, equalVersionEdgeRouteReplacement) { + return fmt.Errorf("project navigation route replacement is not exact") + } + processChanges := []versionEdgeProcessChange{{ + ChangeID: "proofkit.cli-contract.omitted-route-policy", CurrentValue: commandroute.OmittedRoutePolicy, + JSONPointer: "/processContract/commandRouteGrammar/omittedRoutePolicy", PreviousState: "absent", + }} + if !slices.Equal(record.ProcessContractChanges, processChanges) || currentOmittedRoutePolicy(contractRoot) != commandroute.OmittedRoutePolicy { + return fmt.Errorf("project navigation process-contract change is not exact") + } + if !slices.Equal(record.BreakingChangeIDs, []string{"proofkit.agent-workflow.change-plan-route", "proofkit.cli-contract.omitted-route-policy"}) || !slices.Equal(record.AdditionChangeIDs, []string{"proofkit.project-state.next-action", "proofkit.project-state.status"}) { + return fmt.Errorf("project navigation change inventory is not exact") + } + if record.ChangeRecordRef != releasechange.RecordPath { + return fmt.Errorf("project navigation change record reference is not exact") + } + changeRecordPath := filepath.Join(changeRecordRoot, filepath.FromSlash(record.ChangeRecordRef)) + changeRecordContent, err := os.ReadFile(changeRecordPath) + if err != nil { + return fmt.Errorf("read project navigation change record: %w", err) + } + digest := sha256.Sum256(changeRecordContent) + if record.ChangeRecordSHA256 != fmt.Sprintf("sha256:%x", digest) { + return fmt.Errorf("project navigation change record digest is not exact") + } + changeRecord, err := releasechange.Read(changeRecordPath) + if err != nil { + return fmt.Errorf("admit project navigation change record: %w", err) + } + if changeRecord.PreviousVersion != record.PreviousVersion || changeRecord.Version != record.Version || changeRecord.ChangeClass != record.ChangeClass || !changeRecord.Migration.Required || !slices.Equal(record.MigrationSteps, changeRecord.Migration.Steps) { + return fmt.Errorf("project navigation change record identity is inconsistent") + } + if !slices.Equal(record.BreakingChangeIDs, releaseChangeIDs(changeRecord.BreakingChanges)) || !slices.Equal(record.AdditionChangeIDs, releaseChangeIDs(changeRecord.Additions)) { + return fmt.Errorf("project navigation change inventory contradicts the bound change record") + } + if !slices.Equal(record.NonClaims, []string{"This owner-authored version-edge observation binds source and contract identities; it does not authenticate registry publication, provider ingestion, consumer migration, native witness truth, rollout, or production readiness."}) { + return fmt.Errorf("project navigation version-edge non-claims are not exact") + } + return nil +} + +func currentVersionEdgeCommandContracts(root string, names []string) ([]versionEdgeCommandContract, error) { + contract, err := readVersionEdgeCLIContract(root) + if err != nil { + return nil, err + } + result := make([]versionEdgeCommandContract, 0, len(names)) + for _, name := range names { + command, err := findVersionEdgeCommand(contract, name) + if err != nil { + return nil, err + } + metadata := generatedCommandContractMetadataByName[name] + if command.InputContract != nil || metadata.InputContractSHA256 != "" || metadata.OutputContractSHA256 == "" { + return nil, fmt.Errorf("current CLI contract has unexpected input or incomplete output metadata for %s", name) + } + result = append(result, versionEdgeCommandContract{ + Command: name, + OutputContract: versionEdgeContractIdentity{ + ContractID: contractIDFromRaw(command.OutputContract), + ContractSHA256: metadata.OutputContractSHA256, + }, + Route: effectiveContractRoute(command), + }) + } + return result, nil +} + +func currentVersionEdgeRouteReplacement(root string) (versionEdgeRouteReplacement, error) { + contract, err := readVersionEdgeCLIContract(root) + if err != nil { + return versionEdgeRouteReplacement{}, err + } + command, err := findVersionEdgeCommand(contract, "change-workflow-plan") + if err != nil { + return versionEdgeRouteReplacement{}, err + } + metadata := generatedCommandContractMetadataByName[command.Command] + if command.InputContract == nil || metadata.InputContractSHA256 == "" || metadata.OutputContractSHA256 == "" { + return versionEdgeRouteReplacement{}, fmt.Errorf("current change plan contract metadata is incomplete") + } + return versionEdgeRouteReplacement{ + Command: command.Command, + PreviousRoute: []string{"change-workflow-plan"}, + CurrentRoute: effectiveContractRoute(command), + PreservedInputContract: versionEdgeContractIdentity{ + ContractID: contractIDFromRaw(command.InputContract), ContractSHA256: metadata.InputContractSHA256, + }, + PreservedOutputContract: versionEdgeContractIdentity{ + ContractID: contractIDFromRaw(command.OutputContract), ContractSHA256: metadata.OutputContractSHA256, + }, + }, nil +} + +func readVersionEdgeCLIContract(root string) (cliContract, error) { + content, err := os.ReadFile(filepath.Join(root, "proofkit", "cli-contract.v2.json")) + if err != nil { + return cliContract{}, fmt.Errorf("read current CLI contract: %w", err) + } + contract, err := admission.DecodeTypedJSON[cliContract](bytes.NewReader(content), int64(len(content))) + if err != nil { + return cliContract{}, fmt.Errorf("admit current CLI contract: %w", err) + } + return contract, nil +} + +func findVersionEdgeCommand(contract cliContract, name string) (cliContractCommand, error) { + for _, command := range contract.Commands { + if command.Command == name { + return command, nil + } + } + return cliContractCommand{}, fmt.Errorf("current CLI contract is missing %s", name) +} + +func equalVersionEdgeRouteReplacement(left, right versionEdgeRouteReplacement) bool { + return left.Command == right.Command && slices.Equal(left.PreviousRoute, right.PreviousRoute) && slices.Equal(left.CurrentRoute, right.CurrentRoute) && left.PreservedInputContract == right.PreservedInputContract && left.PreservedOutputContract == right.PreservedOutputContract +} + +func cloneProjectNavigationVersionEdge(record projectNavigationVersionEdge) projectNavigationVersionEdge { + record.AddedCommandContracts = append([]versionEdgeCommandContract(nil), record.AddedCommandContracts...) + for index := range record.AddedCommandContracts { + record.AddedCommandContracts[index].Route = append([]string(nil), record.AddedCommandContracts[index].Route...) + } + record.ChangedCommandRoutes = append([]versionEdgeRouteReplacement(nil), record.ChangedCommandRoutes...) + for index := range record.ChangedCommandRoutes { + record.ChangedCommandRoutes[index].PreviousRoute = append([]string(nil), record.ChangedCommandRoutes[index].PreviousRoute...) + record.ChangedCommandRoutes[index].CurrentRoute = append([]string(nil), record.ChangedCommandRoutes[index].CurrentRoute...) + } + record.ProcessContractChanges = append([]versionEdgeProcessChange(nil), record.ProcessContractChanges...) + record.AdditionChangeIDs = append([]string(nil), record.AdditionChangeIDs...) + record.BreakingChangeIDs = append([]string(nil), record.BreakingChangeIDs...) + record.MigrationSteps = append([]string(nil), record.MigrationSteps...) + record.NonClaims = append([]string(nil), record.NonClaims...) + return record +} + +func currentOmittedRoutePolicy(root string) string { + content, err := os.ReadFile(filepath.Join(root, "proofkit", "cli-contract.v2.json")) + if err != nil { + return "" + } + value, err := admission.DecodeJSON(bytes.NewReader(content), int64(len(content))) + if err != nil { + return "" + } + record, _ := value.(map[string]any) + process, _ := record["processContract"].(map[string]any) + grammar, _ := process["commandRouteGrammar"].(map[string]any) + policy, _ := grammar["omittedRoutePolicy"].(string) + return policy +} + +func assertProjectNavigationRouteCutover(t *testing.T) { + t.Helper() + status, stdout, stderr := executeAgentWorkflowCLI(t, []string{"change-workflow-plan", "--input", "-"}, panicReader{}, PresentationCapabilities{}) + if status != 1 || stdout != "" || !strings.Contains(stderr, "unsupported command: change-workflow-plan") { + t.Fatalf("retired route status=%d stdout=%q stderr=%q", status, stdout, stderr) + } + status, stdout, stderr = executeAgentWorkflowCLI(t, []string{"change", "plan", "--input", "-"}, bytes.NewBufferString(validChangeWorkflowInput), PresentationCapabilities{}) + if status != 0 || stdout == "" || stderr != "" { + t.Fatalf("current route status=%d stdout=%q stderr=%q", status, stdout, stderr) + } +} diff --git a/internal/app/project_status_command.go b/internal/app/project_status_command.go new file mode 100644 index 0000000..aabc932 --- /dev/null +++ b/internal/app/project_status_command.go @@ -0,0 +1,136 @@ +package app + +import ( + "context" + "fmt" + "io" + + "github.com/research-engineering/agentic-proofkit/internal/command/projectstatus" +) + +type projectStatusArgs struct { + color string + format string + repositoryRoot string +} + +func runProjectStatus(ctx context.Context, command string, args []string, stdout io.Writer, stderr io.Writer, capabilities PresentationCapabilities) int { + options, err := parseProjectStatusArgs(command, args) + if err != nil { + writeDiagnostic(stderr, err) + return 1 + } + status, err := projectstatus.Inspect(ctx, options.repositoryRoot) + if err != nil { + writeDiagnostic(stderr, err) + return 1 + } + return projectStatusResult(command, options, status, stdout, stderr, capabilities) +} + +func projectStatusResult(command string, options projectStatusArgs, status projectstatus.Status, stdout io.Writer, stderr io.Writer, capabilities PresentationCapabilities) int { + if command == "status" { + if options.format == "json" { + return writeJSON(status.JSONValue(), 0, nil, stdout, stderr) + } + lines, err := projectstatus.StatusText(status) + return writeProjectStatusText(lines, options.color, capabilities, stdout, stderr, err) + } + if command != "next" { + writeDiagnosticf(stderr, "unsupported project status command") + return 1 + } + next, err := projectstatus.NextFromStatus(status) + if err != nil { + writeDiagnostic(stderr, err) + return 1 + } + if options.format == "json" { + return writeJSON(next.JSONValue(), 0, nil, stdout, stderr) + } + lines, err := projectstatus.NextText(next) + return writeProjectStatusText(lines, options.color, capabilities, stdout, stderr, err) +} + +func parseProjectStatusArgs(command string, args []string) (projectStatusArgs, error) { + options := projectStatusArgs{color: "never", format: "json"} + seen := map[string]bool{} + for index := 0; index < len(args); index++ { + flag := args[index] + if flag != "--color" && flag != "--format" && flag != "--repo-root" { + return projectStatusArgs{}, fmt.Errorf("unsupported argument for %s", command) + } + if seen[flag] { + return projectStatusArgs{}, fmt.Errorf("%s may be specified only once", flag) + } + seen[flag] = true + if index+1 >= len(args) || args[index+1] == "" { + return projectStatusArgs{}, missingProjectStatusValue(flag) + } + value := args[index+1] + index++ + switch flag { + case "--color": + if value != "auto" && value != "never" { + return projectStatusArgs{}, fmt.Errorf("--color requires one of: auto, never") + } + options.color = value + case "--format": + if value != "json" && value != "text" { + return projectStatusArgs{}, fmt.Errorf("--format requires one of: json, text") + } + options.format = value + case "--repo-root": + options.repositoryRoot = value + } + } + if options.repositoryRoot == "" { + return projectStatusArgs{}, fmt.Errorf("%s requires --repo-root ", command) + } + if seen["--color"] && options.format != "text" { + return projectStatusArgs{}, fmt.Errorf("--color is valid only with --format text") + } + return options, nil +} + +func missingProjectStatusValue(flag string) error { + switch flag { + case "--color": + return fmt.Errorf("--color requires one of: auto, never") + case "--format": + return fmt.Errorf("--format requires one of: json, text") + case "--repo-root": + return fmt.Errorf("--repo-root requires a path") + default: + return fmt.Errorf("unsupported project status argument") + } +} + +func writeProjectStatusText(lines []projectstatus.TextLine, color string, capabilities PresentationCapabilities, stdout io.Writer, stderr io.Writer, lineErr error) int { + if lineErr != nil { + return writeText("", 1, lineErr, stdout, stderr) + } + plain, err := projectstatus.RenderText(lines) + if err != nil { + return writeText("", 1, err, stdout, stderr) + } + view := projectStatusTerminalText(lines) + output, err := renderTerminalText(view, color, capabilities) + if err == nil && color == "never" && output != plain { + err = fmt.Errorf("project status text projection drifted") + } + return writeText(output, 0, err, stdout, stderr) +} + +func projectStatusTerminalText(lines []projectstatus.TextLine) terminalText { + tokens := make([]terminalTextToken, 0, len(lines)*2) + for _, line := range lines { + tokens = append(tokens, terminalTextToken{kind: terminalTokenLabel, text: line.Label}) + if line.Value == "" { + tokens = append(tokens, terminalTextToken{kind: terminalTokenPlain, text: "\n"}) + continue + } + tokens = append(tokens, terminalTextToken{kind: terminalTokenPlain, text: ": " + line.Value + "\n"}) + } + return newTerminalText(tokens...) +} diff --git a/internal/app/project_status_command_test.go b/internal/app/project_status_command_test.go new file mode 100644 index 0000000..dc39ec9 --- /dev/null +++ b/internal/app/project_status_command_test.go @@ -0,0 +1,304 @@ +package app + +import ( + "bytes" + "errors" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/command/projectstatus" + "github.com/research-engineering/agentic-proofkit/internal/kernel/commandroute" + "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" + "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" +) + +func TestStatusOutputUsesExactRootShape(t *testing.T) { + root := t.TempDir() + code, output, diagnostic := executeAgentWorkflowCLI(t, []string{"status", "--repo-root", root}, panicReader{}, PresentationCapabilities{}) + if code != 0 || diagnostic != "" { + t.Fatalf("status exit=%d stderr=%q stdout=%q", code, diagnostic, output) + } + value, ok := decodeCLIJSON(t, output).(map[string]any) + if !ok { + t.Fatal("status output must be an object") + } + assertExactObjectKeys(t, value, []string{"issueCodes", "manifestId", "nextAction", "nonClaims", "projectId", "projectState", "reportKind", "schemaVersion", "snapshotId", "statusId"}, "status output") +} + +func TestNextOutputUsesExactRootShape(t *testing.T) { + root := t.TempDir() + code, output, diagnostic := executeAgentWorkflowCLI(t, []string{"next", "--repo-root", root}, panicReader{}, PresentationCapabilities{}) + if code != 0 || diagnostic != "" { + t.Fatalf("next exit=%d stderr=%q stdout=%q", code, diagnostic, output) + } + value, ok := decodeCLIJSON(t, output).(map[string]any) + if !ok { + t.Fatal("next output must be an object") + } + assertExactObjectKeys(t, value, []string{"action", "issueCodes", "nonClaims", "packetId", "packetKind", "projectState", "schemaVersion", "snapshotId", "statusRef"}, "next output") +} + +func TestProjectStatusOutputMatrix(t *testing.T) { + statusKeys := []string{"issueCodes", "manifestId", "nextAction", "nonClaims", "projectId", "projectState", "reportKind", "schemaVersion", "snapshotId", "statusId"} + nextKeys := []string{"action", "issueCodes", "nonClaims", "packetId", "packetKind", "projectState", "schemaVersion", "snapshotId", "statusRef"} + states := []projectstatus.ProjectState{ + projectstatus.StateBlocked, + projectstatus.StateRecoveryRequired, + projectstatus.StateStale, + projectstatus.StateUninitialized, + projectstatus.StateVerificationRequired, + } + for _, state := range states { + status := admittedProjectStatusFixture(t, state) + if route := status.NextAction.CommandRoute; len(route) > 0 { + if _, ok := commandDescriptorByRoute[commandroute.Key(route)]; !ok { + t.Fatalf("project state %s emits route %v without a public command descriptor", state, route) + } + } + for _, command := range []string{"next", "status"} { + t.Run(string(state)+"/"+command, func(t *testing.T) { + var stdout bytes.Buffer + var stderr bytes.Buffer + code := projectStatusResult(command, projectStatusArgs{color: "never", format: "json", repositoryRoot: "unused"}, status, &stdout, &stderr, PresentationCapabilities{}) + if code != 0 || stderr.Len() != 0 { + t.Fatalf("JSON exit=%d stderr=%q stdout=%q", code, stderr.String(), stdout.String()) + } + value, ok := decodeCLIJSON(t, stdout.String()).(map[string]any) + if !ok { + t.Fatal("project navigation JSON must be an object") + } + keys := statusKeys + if command == "next" { + keys = nextKeys + } + assertExactObjectKeys(t, value, keys, command+" output") + if value["projectState"] != string(state) { + t.Fatalf("projectState=%v, want %s", value["projectState"], state) + } + + stdout.Reset() + code = projectStatusResult(command, projectStatusArgs{color: "never", format: "text", repositoryRoot: "unused"}, status, &stdout, &stderr, PresentationCapabilities{}) + if code != 0 || stderr.Len() != 0 || stdout.Len() == 0 || strings.Contains(stdout.String(), "\x1b[") || !strings.Contains(stdout.String(), string(state)) { + t.Fatalf("text exit=%d stderr=%q stdout=%q", code, stderr.String(), stdout.String()) + } + }) + } + } +} + +func TestProjectStatusCLI(t *testing.T) { + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.082990774938213415032196768034286988848197095097772338307445528941413312751637") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.016575929375243753473232212796372621717203348455914493779019079597618731316529") + repositoryRoot := t.TempDir() + + t.Run("status and next preserve owner output", func(t *testing.T) { + statusCode, statusOutput, statusDiagnostic := executeAgentWorkflowCLI(t, []string{"status", "--repo-root", repositoryRoot}, panicReader{}, PresentationCapabilities{}) + if statusCode != 0 || statusDiagnostic != "" { + t.Fatalf("status exit=%d stderr=%q stdout=%q", statusCode, statusDiagnostic, statusOutput) + } + status, err := projectstatus.AdmitStatusOutput(decodeCLIJSON(t, statusOutput)) + if err != nil || status.ProjectState != projectstatus.StateUninitialized { + t.Fatalf("status admission=%v state=%s", err, status.ProjectState) + } + + nextCode, nextOutput, nextDiagnostic := executeAgentWorkflowCLI(t, []string{"next", "--repo-root", repositoryRoot}, panicReader{}, PresentationCapabilities{}) + if nextCode != 0 || nextDiagnostic != "" { + t.Fatalf("next exit=%d stderr=%q stdout=%q", nextCode, nextDiagnostic, nextOutput) + } + next, err := projectstatus.AdmitNextOutput(decodeCLIJSON(t, nextOutput)) + if err != nil || next.StatusRef != status.StatusID || next.Action.ActionClass != projectstatus.ActionChooseAdoptionMode { + t.Fatalf("next admission=%v packet=%#v", err, next) + } + if strings.Contains(statusOutput+nextOutput, repositoryRoot) { + t.Fatal("project status output disclosed repository root") + } + }) + + t.Run("compact JSON preserves value", func(t *testing.T) { + prettyCode, pretty, prettyDiagnostic := executeAgentWorkflowCLI(t, []string{"status", "--repo-root", repositoryRoot}, panicReader{}, PresentationCapabilities{}) + compactCode, compact, compactDiagnostic := executeAgentWorkflowCLI(t, []string{"--json-layout", "compact", "status", "--repo-root", repositoryRoot}, panicReader{}, PresentationCapabilities{}) + if prettyCode != 0 || compactCode != 0 || prettyDiagnostic != "" || compactDiagnostic != "" { + t.Fatalf("pretty=%d/%q compact=%d/%q", prettyCode, prettyDiagnostic, compactCode, compactDiagnostic) + } + if !equalCLIJSON(t, decodeCLIJSON(t, pretty), decodeCLIJSON(t, compact)) || strings.Contains(compact, "\n ") { + t.Fatal("JSON layout changed project status value or retained indentation") + } + }) + + t.Run("materialized project and drift remain visible through the public route", func(t *testing.T) { + materializedRoot := t.TempDir() + payload, transactionID, desiredStateID := adoptionMaterializationPlanFixture(t, materializedRoot) + applyArgs := []string{ + "adopt", "materialize", "apply", + "--input", "-", + "--repo-root", materializedRoot, + "--expect-transaction", transactionID, + "--expect-desired-state", desiredStateID, + } + if code, output, diagnostic := executeAgentWorkflowCLI(t, applyArgs, bytes.NewReader(payload), PresentationCapabilities{}); code != 0 || diagnostic != "" { + t.Fatalf("materialize exit=%d stderr=%q stdout=%q", code, diagnostic, output) + } + + statusCode, statusOutput, statusDiagnostic := executeAgentWorkflowCLI(t, []string{"status", "--repo-root", materializedRoot}, panicReader{}, PresentationCapabilities{}) + if statusCode != 0 || statusDiagnostic != "" { + t.Fatalf("materialized status exit=%d stderr=%q stdout=%q", statusCode, statusDiagnostic, statusOutput) + } + status, err := projectstatus.AdmitStatusOutput(decodeCLIJSON(t, statusOutput)) + if err != nil || status.ProjectState != projectstatus.StateVerificationRequired { + t.Fatalf("materialized status admission=%v state=%s", err, status.ProjectState) + } + nextCode, nextOutput, nextDiagnostic := executeAgentWorkflowCLI(t, []string{"next", "--repo-root", materializedRoot}, panicReader{}, PresentationCapabilities{}) + if nextCode != 0 || nextDiagnostic != "" { + t.Fatalf("materialized next exit=%d stderr=%q stdout=%q", nextCode, nextDiagnostic, nextOutput) + } + next, err := projectstatus.AdmitNextOutput(decodeCLIJSON(t, nextOutput)) + if err != nil || next.StatusRef != status.StatusID || next.Action.ActionClass != projectstatus.ActionRunRepositoryVerification { + t.Fatalf("materialized next admission=%v packet=%#v", err, next) + } + + requirementPath := filepath.Join(materializedRoot, "docs", "specs", "pilot", "requirements.v1.json") + content, err := os.ReadFile(requirementPath) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(requirementPath, append(content, '\n'), 0o600); err != nil { + t.Fatal(err) + } + staleCode, staleOutput, staleDiagnostic := executeAgentWorkflowCLI(t, []string{"status", "--repo-root", materializedRoot}, panicReader{}, PresentationCapabilities{}) + if staleCode != 0 || staleDiagnostic != "" { + t.Fatalf("stale status exit=%d stderr=%q stdout=%q", staleCode, staleDiagnostic, staleOutput) + } + stale, err := projectstatus.AdmitStatusOutput(decodeCLIJSON(t, staleOutput)) + if err != nil || stale.ProjectState != projectstatus.StateStale || stale.NextAction.ActionClass != projectstatus.ActionRematerializeProject { + t.Fatalf("stale status admission=%v packet=%#v", err, stale) + } + }) + + t.Run("text and terminal color are derived", func(t *testing.T) { + args := []string{"next", "--repo-root", repositoryRoot, "--format", "text"} + code, plain, diagnostic := executeAgentWorkflowCLI(t, args, panicReader{}, PresentationCapabilities{StdoutIsTTY: true}) + if code != 0 || diagnostic != "" || strings.Contains(plain, "\x1b[") { + t.Fatalf("plain exit=%d stderr=%q stdout=%q", code, diagnostic, plain) + } + colorArgs := append(cloneStrings(args), "--color", "auto") + code, colored, diagnostic := executeAgentWorkflowCLI(t, colorArgs, panicReader{}, PresentationCapabilities{StdoutIsTTY: true}) + if code != 0 || diagnostic != "" || !strings.Contains(colored, "\x1b[") { + t.Fatalf("colored exit=%d stderr=%q stdout=%q", code, diagnostic, colored) + } + code, noColor, diagnostic := executeAgentWorkflowCLI(t, colorArgs, panicReader{}, PresentationCapabilities{StdoutIsTTY: true, NoColorPresent: true}) + if code != 0 || diagnostic != "" || noColor != plain { + t.Fatalf("NO_COLOR exit=%d stderr=%q stdout=%q", code, diagnostic, noColor) + } + code, redirected, diagnostic := executeAgentWorkflowCLI(t, colorArgs, panicReader{}, PresentationCapabilities{}) + if code != 0 || diagnostic != "" || redirected != plain || strings.Contains(redirected, "\x1b[") { + t.Fatalf("non-TTY exit=%d stderr=%q stdout=%q want=%q", code, diagnostic, redirected, plain) + } + }) + + t.Run("argument admission precedes repository reads", func(t *testing.T) { + missingRoot := filepath.Join(t.TempDir(), "missing") + cases := []struct { + args []string + want string + }{ + {args: []string{"status", "--repo-root", missingRoot, "--format", "yaml"}, want: "--format"}, + {args: []string{"next", "--repo-root", missingRoot, "--format", "json", "--color", "auto"}, want: "--color"}, + {args: []string{"status", "--repo-root", missingRoot, "--repo-root", repositoryRoot}, want: "only once"}, + {args: []string{"next", "--repo-root"}, want: "requires a path"}, + {args: []string{"status", "--repo-root", missingRoot, "--unknown", "value"}, want: "unsupported argument"}, + } + for _, item := range cases { + code, output, diagnostic := executeAgentWorkflowCLI(t, item.args, panicReader{}, PresentationCapabilities{}) + if code != 1 || output != "" || !strings.Contains(diagnostic, item.want) || strings.Contains(diagnostic, missingRoot) { + t.Fatalf("args=%v exit=%d stdout=%q stderr=%q", item.args, code, output, diagnostic) + } + } + code, output, diagnostic := executeAgentWorkflowCLI(t, []string{"status", "--repo-root", missingRoot}, panicReader{}, PresentationCapabilities{}) + if code != 1 || output != "" || diagnostic == "" || strings.Contains(diagnostic, missingRoot) { + t.Fatalf("inspection failure exit=%d stdout=%q stderr=%q", code, output, diagnostic) + } + secretArgument := "--sk-proj-caller-secret-sentinel" + code, output, diagnostic = executeAgentWorkflowCLI(t, []string{"status", "--repo-root", repositoryRoot, secretArgument}, panicReader{}, PresentationCapabilities{}) + if code != 1 || output != "" || !strings.Contains(diagnostic, "unsupported argument") || strings.Contains(diagnostic, secretArgument) { + t.Fatalf("secret-shaped argument exit=%d stdout=%q stderr=%q", code, output, diagnostic) + } + }) +} + +func TestProjectStatusTransportFailureUsesOneBoundedWriteWithoutAtomicSinkClaim(t *testing.T) { + status := admittedProjectStatusFixture(t, projectstatus.StateUninitialized) + stdout := &prefixThenErrorWriter{maximum: 7} + var stderr bytes.Buffer + code := projectStatusResult("status", projectStatusArgs{color: "never", format: "json", repositoryRoot: "unused"}, status, stdout, &stderr, PresentationCapabilities{}) + if code != 1 || stdout.calls != 1 || stdout.Len() != stdout.maximum || !strings.Contains(stderr.String(), "write output") { + t.Fatalf("transport failure exit=%d calls=%d stdout=%q stderr=%q", code, stdout.calls, stdout.String(), stderr.String()) + } +} + +type prefixThenErrorWriter struct { + bytes.Buffer + calls int + maximum int +} + +func (writer *prefixThenErrorWriter) Write(value []byte) (int, error) { + writer.calls++ + count := min(writer.maximum, len(value)) + _, _ = writer.Buffer.Write(value[:count]) + return count, errors.New("injected transport failure") +} + +func admittedProjectStatusFixture(t *testing.T, state projectstatus.ProjectState) projectstatus.Status { + t.Helper() + manifestID := digest.SHA256TextRef("project manifest") + status := projectstatus.Status{ + ProjectState: state, + SnapshotID: digest.SHA256TextRef("project snapshot " + string(state)), + } + switch state { + case projectstatus.StateBlocked: + status.IssueCodes = []string{projectstatus.IssueManifestInvalid} + status.NextAction.ActionClass = projectstatus.ActionRepairProjectRecords + case projectstatus.StateRecoveryRequired: + status.IssueCodes = []string{projectstatus.IssueTransactionRecoveryRequired} + status.NextAction.ActionClass = projectstatus.ActionChooseRecovery + status.NextAction.CommandRoute = []string{"adopt", "materialize", "recover"} + status.NextAction.ContextRef = digest.SHA256TextRef("active transaction") + status.NextAction.RequiredDecision = "resume_or_rollback" + case projectstatus.StateStale: + status.IssueCodes = []string{projectstatus.IssueChildMissing} + status.ProjectID = "pilot.project" + status.ManifestID = manifestID + status.NextAction.ActionClass = projectstatus.ActionRematerializeProject + status.NextAction.CommandRoute = []string{"adopt", "materialize", "plan"} + status.NextAction.ContextRef = manifestID + case projectstatus.StateUninitialized: + status.IssueCodes = []string{projectstatus.IssueManifestMissing} + status.NextAction.ActionClass = projectstatus.ActionChooseAdoptionMode + status.NextAction.CommandRoute = []string{"adopt", "plan"} + status.NextAction.RequiredDecision = "adoption_mode" + case projectstatus.StateVerificationRequired: + status.ProjectID = "pilot.project" + status.ManifestID = manifestID + status.NextAction.ActionClass = projectstatus.ActionRunRepositoryVerification + status.NextAction.ContextRef = manifestID + default: + t.Fatalf("unsupported fixture state %s", state) + } + status.NextAction.CommandRoute = append([]string{}, status.NextAction.CommandRoute...) + status.NextAction.ActionID = "proofkit.project-status.action." + status.NextAction.ActionClass + identity := status.JSONValue() + delete(identity, "statusId") + statusID, err := digest.StableJSONSHA256Ref(identity) + if err != nil { + t.Fatal(err) + } + status.StatusID = statusID + admitted, err := projectstatus.AdmitStatusOutput(status.JSONValue()) + if err != nil { + t.Fatalf("admit fixture state %s: %v", state, err) + } + return admitted +} diff --git a/internal/app/testdata/compact-current-production-consumers.json b/internal/app/testdata/compact-current-production-consumers.json index 3236d93..b30ec85 100644 --- a/internal/app/testdata/compact-current-production-consumers.json +++ b/internal/app/testdata/compact-current-production-consumers.json @@ -9,6 +9,7 @@ "internal/app/app.go", "internal/app/command_registry.go", "internal/app/conformance_command.go", + "internal/app/project_status_command.go", "internal/app/requirement_browser_command.go", "internal/app/requirement_commands.go", "internal/app/requirement_context_command.go", @@ -17,9 +18,11 @@ "internal/command/adoptioncontract/adoptioncontract.go", "internal/command/adoptionmaterialization/admission.go", "internal/command/adoptionmaterialization/build.go", + "internal/command/adoptionmaterialization/project_closure.go", "internal/command/conformanceprofile/conformanceprofile.go", "internal/command/impact/impact.go", "internal/command/pilotadmission/pilotadmission.go", + "internal/command/projectstatus/inspect.go", "internal/command/proofbindingtestinventory/proofbindingtestinventory.go", "internal/command/requirementbinding/requirementbinding.go", "internal/command/requirementbrowser/requirementbrowser.go", @@ -48,7 +51,13 @@ "internal/command/testevidenceinventory/source_set.go", "internal/command/testevidenceinventory/testevidenceinventory.go", "internal/tools/browsertestserver/main.go", - "internal/tools/coveragemetrics/main.go" + "internal/tools/coveragemetrics/main.go", + "internal/tools/packageverify/main.go", + "internal/tools/pythonpackage/main.go", + "internal/tools/pythonpackage/verify.go", + "internal/tools/workflowsmoke/process.go", + "internal/tools/workflowsmoke/project_navigation_smoke.go", + "internal/tools/workflowsmoke/workflow_smoke.go" ], "nonClaims": [ "Candidate inclusion proves a bounded static dependency or schema signal only; it does not prove runtime invocation, semantic ownership, or an absence of consumers that violate the declared static signal policy." diff --git a/internal/app/testdata/releases/v0.8.0/preserved-command-contracts.json b/internal/app/testdata/releases/v0.8.0/preserved-command-contracts.json new file mode 100644 index 0000000..163e251 --- /dev/null +++ b/internal/app/testdata/releases/v0.8.0/preserved-command-contracts.json @@ -0,0 +1,26 @@ +{ + "schemaVersion": 1, + "observationKind": "proofkit.frozen-command-contract-observation", + "releaseVersion": "0.8.0", + "publicAbiSha256": "sha256:b5ea707ee5851cea6b75442e4faf20e93879371faf3636e96a98ccd23b527463", + "commandRouteGrammar": { + "minimumTokens": 1, + "maximumTokens": 4, + "separator": " ", + "tokenPattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", + "ambiguityPolicy": "no_route_is_prefix_of_another" + }, + "command": "change-workflow-plan", + "route": ["change-workflow-plan"], + "inputContract": { + "contractId": "proofkit.change-workflow-plan.input.v1", + "contractSha256": "sha256:e3124fc636b7f66b24daf8e1435cea11da15a741abeabe0cc3d3890b13c71625" + }, + "outputContract": { + "contractId": "proofkit.change-workflow-plan.output.v1", + "contractSha256": "sha256:cd035e9b71d83c341b1a937a18699fd727cb4b0d694983d715b064292ae4d8bd" + }, + "nonClaims": [ + "This frozen source observation does not authenticate registry publication, provider state, consumer migration, or runtime compatibility." + ] +} diff --git a/internal/app/testdata/releases/v0.8.0/release/change-record.v2.json b/internal/app/testdata/releases/v0.8.0/release/change-record.v2.json new file mode 100644 index 0000000..4125ae2 --- /dev/null +++ b/internal/app/testdata/releases/v0.8.0/release/change-record.v2.json @@ -0,0 +1,36 @@ +{ + "schemaVersion": 2, + "previousVersion": "0.7.0", + "version": "0.8.0", + "changeClass": "compatible", + "breakingChanges": [], + "additions": [ + { + "changeId": "proofkit.adoption.transactional-materialization", + "summary": "Add separate read-only plan, compare-and-swap apply, and state-bound recovery routes that compile owner-admitted adoption candidates into canonical repository artifacts." + }, + { + "changeId": "proofkit.repository.transaction-protocol", + "summary": "Add a bounded repository-confined transaction owner with immutable journals, exact before-state checks, deterministic resume, and byte-identical rollback for cooperative writers." + } + ], + "migration": { + "required": false, + "steps": [] + }, + "platformRequirements": [ + "Published Darwin package binaries require macOS 13.0 or later on arm64 and x86_64." + ], + "knownLimitations": [ + "Adopt plan inventories only a fixed root-file catalog; it does not infer stack identity, inspect arbitrary source semantics, generate requirements, write files, or execute native evidence.", + "Transactional materialization writes only owner-admitted candidate artifacts under one explicit repository root; it does not infer requirement meaning, execute native evidence, approve merge or release, provide filesystem-wide atomic visibility to concurrent readers, or protect its private namespace from a hostile same-user process.", + "Agent workflow plans, prompts, text, and envelopes are derived guidance and do not execute agents, repository mutations, native witnesses, CI, release, rollout, or production operations.", + "Brief agent-route packets cap pretty JSON at 3072 bytes and may defer oversized argv to explicit full detail; the bound does not claim tokenizer-specific token counts.", + "Complete nested public structural contracts remain blocked under SCHEMA-01; current CLI contracts own exact root variants only.", + "The selected requirement-source v2 codec remains internal; current requirement sources are not migrated and no source cutover is claimed.", + "TSX source parsing remains unsupported." + ], + "rollback": { + "strategy": "previous_admitted_version" + } +} diff --git a/internal/app/testdata/v0.9-wire-observations.json b/internal/app/testdata/v0.9-wire-observations.json new file mode 100644 index 0000000..0d711a9 --- /dev/null +++ b/internal/app/testdata/v0.9-wire-observations.json @@ -0,0 +1,69 @@ +{ + "schemaVersion": 1, + "edgeId": "proofkit.public-wire.0.8.0-to-0.9.0", + "previousVersion": "0.8.0", + "version": "0.9.0", + "evidenceClass": "owner_authored_current_version_edge_observation", + "changeClass": "breaking", + "commandContractSelection": "added_commands_changed_routes_and_process_contract", + "changeRecordRef": "release/change-record.v2.json", + "changeRecordSha256": "sha256:6aaa914c454d6f135ece86b632b8618911b71d6b5da08bc2c25657c09228f4f3", + "previousPublicAbiSha256": "sha256:b5ea707ee5851cea6b75442e4faf20e93879371faf3636e96a98ccd23b527463", + "currentPublicAbiSha256": "sha256:17b7f185adb80bcdeb6bc5e6a08cf0b95e6b6f8766d2cba634d08f59b7821e0b", + "addedCommandContracts": [ + { + "command": "next", + "route": ["next"], + "outputContract": { + "contractId": "proofkit.next.output.v1", + "contractSha256": "sha256:9dfa71617f9d727949a988bf726bcf0070460faa59565a8adc468994a36594c6" + } + }, + { + "command": "status", + "route": ["status"], + "outputContract": { + "contractId": "proofkit.status.output.v1", + "contractSha256": "sha256:29cf593bf349fd5ec7276f28ce75817a44a8b12fc7aea5b4c055cfaf4188924c" + } + } + ], + "changedCommandRoutes": [ + { + "command": "change-workflow-plan", + "previousRoute": ["change-workflow-plan"], + "currentRoute": ["change", "plan"], + "preservedInputContract": { + "contractId": "proofkit.change-workflow-plan.input.v1", + "contractSha256": "sha256:e3124fc636b7f66b24daf8e1435cea11da15a741abeabe0cc3d3890b13c71625" + }, + "preservedOutputContract": { + "contractId": "proofkit.change-workflow-plan.output.v1", + "contractSha256": "sha256:cd035e9b71d83c341b1a937a18699fd727cb4b0d694983d715b064292ae4d8bd" + } + } + ], + "processContractChanges": [ + { + "changeId": "proofkit.cli-contract.omitted-route-policy", + "jsonPointer": "/processContract/commandRouteGrammar/omittedRoutePolicy", + "previousState": "absent", + "currentValue": "command_id" + } + ], + "breakingChangeIds": [ + "proofkit.agent-workflow.change-plan-route", + "proofkit.cli-contract.omitted-route-policy" + ], + "additionChangeIds": [ + "proofkit.project-state.next-action", + "proofkit.project-state.status" + ], + "migrationSteps": [ + "Replace agentic-proofkit change-workflow-plan invocations with agentic-proofkit change plan; input and output JSON contracts are unchanged.", + "Update CLI-contract consumers to require commandRouteGrammar.omittedRoutePolicy=command_id; commands without an explicit route continue to resolve to their stable command ID." + ], + "nonClaims": [ + "This owner-authored version-edge observation binds source and contract identities; it does not authenticate registry publication, provider ingestion, consumer migration, native witness truth, rollout, or production readiness." + ] +} diff --git a/internal/command/adoptionmaterialization/build.go b/internal/command/adoptionmaterialization/build.go index 47ae109..f97a5b9 100644 --- a/internal/command/adoptionmaterialization/build.go +++ b/internal/command/adoptionmaterialization/build.go @@ -248,26 +248,18 @@ func validateExistingArtifacts(transaction repositorytransaction.Plan, artifacts } func admitExistingArtifact(content []byte, desired artifact, projectID string) error { + if desired.Kind != ArtifactProjectManifest { + identity, admitted := admitProjectChildRecord(content, Route{ArtifactKind: desired.Kind, Path: desired.Path}, &admittedProjectChildren{}) + if !admitted || identity != desired.ID { + return fmt.Errorf("adoption materialization existing artifact has incompatible ownership") + } + return nil + } raw, err := admission.DecodeJSON(bytes.NewReader(content), repositorytransaction.MaximumFileBytes) if err != nil { return fmt.Errorf("adoption materialization refuses to replace an unknown existing artifact") } switch desired.Kind { - case ArtifactRequirementSource: - result, err := requirementsourceadmission.Evaluate(raw) - if err != nil || result.ExitCode != 0 || result.Source.SourceID != desired.ID { - return fmt.Errorf("adoption materialization existing requirement source has incompatible ownership") - } - case ArtifactRequirementBinding: - result, err := requirementbinding.Build(raw) - if err != nil || result.Record.State != "passed" || result.Input.BindingID != desired.ID { - return fmt.Errorf("adoption materialization existing requirement binding has incompatible ownership") - } - case ArtifactTestInventory: - result, err := testevidenceinventory.EvaluateDirect(raw) - if err != nil || result.ExitCode != 0 || result.Inventory.InventoryID != desired.ID { - return fmt.Errorf("adoption materialization existing test inventory has incompatible ownership") - } case ArtifactProjectManifest: manifest, err := AdmitManifest(raw) if err != nil || manifest.ProjectID != projectID { diff --git a/internal/command/adoptionmaterialization/project_closure.go b/internal/command/adoptionmaterialization/project_closure.go new file mode 100644 index 0000000..0dabec3 --- /dev/null +++ b/internal/command/adoptionmaterialization/project_closure.go @@ -0,0 +1,196 @@ +package adoptionmaterialization + +import ( + "bytes" + "fmt" + "slices" + + "github.com/research-engineering/agentic-proofkit/internal/command/requirementbinding" + "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/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" + "github.com/research-engineering/agentic-proofkit/internal/kernel/repositorytransaction" +) + +// RoutedProjectRecord is one manifest-routed record observed by a read-only +// caller. Content remains untrusted until AdmitMaterializedProject returns. +type RoutedProjectRecord struct { + Content []byte + Path string +} + +// RoutedProjectRecordAdmission is the canonical child-owner result for one +// supplied manifest route. +type RoutedProjectRecordAdmission struct { + Admitted bool + DigestMatches bool + Path string +} + +// MaterializedProjectAdmission separates child admission from cross-record +// closure. Closure is evaluated only for a complete, digest-matched, admitted +// route set. +type MaterializedProjectAdmission struct { + ClosureAdmitted bool + ClosureEvaluated bool + Records []RoutedProjectRecordAdmission +} + +type materializedProjectSnapshot struct { + Binding requirementbinding.Input + BindingPath string + Inventory testevidenceinventory.Inventory + InventoryPath string + Manifest Manifest + Sources []requirementsourceadmission.Source +} + +type admittedProjectChildren struct { + binding requirementbinding.Input + bindingPath string + inventory testevidenceinventory.Inventory + inventoryPath string + sources []requirementsourceadmission.Source +} + +// AdmitMaterializedProject routes child bytes through their semantic owners and +// evaluates the adoption-materialization closure without reading a repository +// or establishing freshness or execution. +func AdmitMaterializedProject(manifest Manifest, records []RoutedProjectRecord) (MaterializedProjectAdmission, error) { + admittedManifest, err := AdmitManifest(manifest.JSONValue()) + if err != nil || !sameManifest(manifest, admittedManifest) { + return MaterializedProjectAdmission{}, fmt.Errorf("materialized project manifest is not admitted") + } + manifest = admittedManifest + records = snapshotRoutedProjectRecords(records) + if len(records) > len(manifest.Routes) { + return MaterializedProjectAdmission{}, fmt.Errorf("materialized project has more records than manifest routes") + } + + byPath := make(map[string][]byte, len(records)) + routesByPath := make(map[string]Route, len(manifest.Routes)) + for _, route := range manifest.Routes { + routesByPath[route.Path] = route + } + for _, record := range records { + if _, exists := routesByPath[record.Path]; !exists { + return MaterializedProjectAdmission{}, fmt.Errorf("materialized project record is outside the manifest") + } + if _, duplicate := byPath[record.Path]; duplicate { + return MaterializedProjectAdmission{}, fmt.Errorf("materialized project record paths must be unique") + } + byPath[record.Path] = record.Content + } + + children := admittedProjectChildren{} + result := MaterializedProjectAdmission{Records: make([]RoutedProjectRecordAdmission, 0, len(records))} + complete := len(records) == len(manifest.Routes) + for _, route := range manifest.Routes { + content, present := byPath[route.Path] + if !present { + complete = false + continue + } + item := RoutedProjectRecordAdmission{Path: route.Path} + if len(content) <= repositorytransaction.MaximumFileBytes && digest.SHA256BytesRef(content) == route.ArtifactID { + item.DigestMatches = true + _, item.Admitted = admitProjectChildRecord(content, route, &children) + } + complete = complete && item.DigestMatches && item.Admitted + result.Records = append(result.Records, item) + } + if !complete { + return result, nil + } + + result.ClosureEvaluated = true + snapshot := materializedProjectSnapshot{ + Binding: children.binding, BindingPath: children.bindingPath, + Inventory: children.inventory, InventoryPath: children.inventoryPath, + Manifest: manifest, Sources: children.sources, + } + result.ClosureAdmitted = validateMaterializedProjectSnapshot(snapshot) == nil + return result, nil +} + +func snapshotRoutedProjectRecords(records []RoutedProjectRecord) []RoutedProjectRecord { + result := make([]RoutedProjectRecord, len(records)) + for index, record := range records { + result[index] = RoutedProjectRecord{ + Content: append([]byte(nil), record.Content...), + Path: record.Path, + } + } + return result +} + +func admitProjectChildRecord(content []byte, route Route, children *admittedProjectChildren) (string, bool) { + raw, err := admission.DecodeJSON(bytes.NewReader(content), repositorytransaction.MaximumFileBytes) + if err != nil { + return "", false + } + switch route.ArtifactKind { + case ArtifactRequirementSource: + result, err := requirementsourceadmission.Evaluate(raw) + if err != nil || result.ExitCode != 0 { + return "", false + } + children.sources = append(children.sources, result.Source) + return result.Source.SourceID, true + case ArtifactRequirementBinding: + result, err := requirementbinding.Build(raw) + if err != nil || result.Record.State != "passed" { + return "", false + } + children.binding = result.Input + children.bindingPath = route.Path + return result.Input.BindingID, true + case ArtifactTestInventory: + result, err := testevidenceinventory.EvaluateDirect(raw) + if err != nil || result.ExitCode != 0 { + return "", false + } + children.inventory = result.Inventory + children.inventoryPath = route.Path + return result.Inventory.InventoryID, true + default: + return "", false + } +} + +func validateMaterializedProjectSnapshot(snapshot materializedProjectSnapshot) error { + request := Request{ + Binding: snapshot.Binding, + BindingPath: snapshot.BindingPath, + Inventory: snapshot.Inventory, + InventoryPath: snapshot.InventoryPath, + ProjectID: snapshot.Manifest.ProjectID, + RequestID: snapshot.Manifest.MaterializationRequestID, + SourcePlanID: snapshot.Manifest.SourcePlanID, + Sources: snapshot.Sources, + } + if err := validateClosure(request); err != nil { + return err + } + children, err := childArtifacts(request) + if err != nil { + return err + } + expected, err := buildManifest(request, children) + if err != nil { + return err + } + if !sameManifest(snapshot.Manifest, expected) { + return fmt.Errorf("project routing manifest does not exactly match its materialized children") + } + return nil +} + +func sameManifest(left, right Manifest) bool { + return left.ManifestID == right.ManifestID && + left.MaterializationRequestID == right.MaterializationRequestID && + left.ProjectID == right.ProjectID && + left.SourcePlanID == right.SourcePlanID && + slices.Equal(left.Routes, right.Routes) +} diff --git a/internal/command/adoptionmaterialization/project_closure_test.go b/internal/command/adoptionmaterialization/project_closure_test.go new file mode 100644 index 0000000..32b364e --- /dev/null +++ b/internal/command/adoptionmaterialization/project_closure_test.go @@ -0,0 +1,276 @@ +package adoptionmaterialization + +import ( + "bytes" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/command/requirementbinding" + "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/digest" +) + +func TestMaterializedProjectClosureIsDeterministic(t *testing.T) { + snapshot := validMaterializedProjectSnapshot(t) + for iteration := 0; iteration < 2; iteration++ { + if err := validateMaterializedProjectSnapshot(snapshot); err != nil { + t.Fatalf("validateMaterializedProjectSnapshot() iteration %d error = %v", iteration, err) + } + } +} + +func TestMaterializedProjectRecordSnapshotDoesNotAliasCallerInput(t *testing.T) { + content := []byte("owner bytes") + records := []RoutedProjectRecord{{Content: content, Path: "docs/specs/owner/requirements.v1.json"}} + snapshot := snapshotRoutedProjectRecords(records) + + content[0] = 'X' + records[0].Content[1] = 'Y' + records[0].Path = "changed" + if got := string(snapshot[0].Content); got != "owner bytes" || snapshot[0].Path != "docs/specs/owner/requirements.v1.json" { + t.Fatalf("snapshot aliases caller-owned records: %#v", snapshot[0]) + } +} + +func TestMaterializedProjectClosureRejectsCrossInconsistentChildren(t *testing.T) { + t.Run("binding projection", func(t *testing.T) { + snapshot := validMaterializedProjectSnapshot(t) + raw := requirementbinding.InputValue(snapshot.Binding) + raw["requirements"].([]any)[0].(map[string]any)["ownerId"] = "different.owner" + result, err := requirementbinding.Build(raw) + if err != nil || result.Record.State != "passed" { + t.Fatalf("independent binding admission failed: result=%#v error=%v", result, err) + } + snapshot.Binding = result.Input + if err := validateMaterializedProjectSnapshot(snapshot); err == nil { + t.Fatal("validateMaterializedProjectSnapshot() admitted a binding that contradicted its source owner") + } + }) + + t.Run("inventory route", func(t *testing.T) { + snapshot := validMaterializedProjectSnapshot(t) + snapshot.Inventory.Entries[0].RequirementRefs = []string{"REQ-OTHER-001"} + result, err := testevidenceinventory.EvaluateDirect(testevidenceinventory.InventoryValue(snapshot.Inventory)) + if err != nil || result.ExitCode != 0 { + t.Fatalf("independent inventory admission failed: result=%#v error=%v", result, err) + } + if err := validateMaterializedProjectSnapshot(snapshot); err == nil { + t.Fatal("validateMaterializedProjectSnapshot() admitted an inventory reference outside its binding") + } + }) +} + +func TestMaterializedProjectClosureRejectsManifestRouteDrift(t *testing.T) { + tests := []struct { + name string + mutate func(*Manifest) + }{ + { + name: "path", + mutate: func(manifest *Manifest) { + for index := range manifest.Routes { + if manifest.Routes[index].ArtifactKind == ArtifactRequirementBinding { + manifest.Routes[index].Path = "proofkit/alternate-bindings.json" + } + } + }, + }, + { + name: "kind", + mutate: func(manifest *Manifest) { + bindingIndex, inventoryIndex := -1, -1 + for index, route := range manifest.Routes { + switch route.ArtifactKind { + case ArtifactRequirementBinding: + bindingIndex = index + case ArtifactTestInventory: + inventoryIndex = index + } + } + manifest.Routes[bindingIndex].ArtifactKind, manifest.Routes[inventoryIndex].ArtifactKind = + manifest.Routes[inventoryIndex].ArtifactKind, manifest.Routes[bindingIndex].ArtifactKind + }, + }, + { + name: "artifact identity", + mutate: func(manifest *Manifest) { + for index := range manifest.Routes { + if manifest.Routes[index].ArtifactKind == ArtifactRequirementBinding { + manifest.Routes[index].ArtifactID = digest.SHA256BytesRef([]byte("different binding bytes")) + } + } + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + snapshot := validMaterializedProjectSnapshot(t) + snapshot.Manifest = readmitManifest(t, snapshot.Manifest, test.mutate) + if err := validateMaterializedProjectSnapshot(snapshot); err == nil { + t.Fatal("validateMaterializedProjectSnapshot() admitted manifest route drift") + } + }) + } +} + +func TestMaterializedProjectClosureRejectsMissingAndSurplusSources(t *testing.T) { + t.Run("missing", func(t *testing.T) { + snapshot := validMaterializedProjectSnapshot(t) + snapshot.Sources = nil + if err := validateMaterializedProjectSnapshot(snapshot); err == nil { + t.Fatal("validateMaterializedProjectSnapshot() admitted a missing requirement source") + } + }) + + t.Run("surplus", func(t *testing.T) { + snapshot := validMaterializedProjectSnapshot(t) + raw := requirementsourceadmission.SourceValue(snapshot.Sources[0]) + raw["sourceId"] = "surplus.requirements" + raw["specPackagePath"] = "docs/specs/surplus" + raw["overviewPath"] = "docs/specs/surplus/overview.md" + raw["requirementsPath"] = "docs/specs/surplus/requirements.v1.json" + requirement := raw["requirements"].([]any)[0].(map[string]any) + requirement["requirementId"] = "REQ-SURPLUS-001" + result, err := requirementsourceadmission.Evaluate(raw) + if err != nil || result.ExitCode != 0 { + t.Fatalf("independent source admission failed: result=%#v error=%v", result, err) + } + snapshot.Sources = append(snapshot.Sources, result.Source) + if err := validateMaterializedProjectSnapshot(snapshot); err == nil { + t.Fatal("validateMaterializedProjectSnapshot() admitted a source absent from the manifest") + } + }) +} + +func TestAdmitMaterializedProjectRejectsConstructedManifestIdentityDrift(t *testing.T) { + snapshot := validMaterializedProjectSnapshot(t) + manifest := snapshot.Manifest + manifest.ProjectID = "different.project" + if _, err := AdmitMaterializedProject(manifest, nil); err == nil { + t.Fatal("AdmitMaterializedProject() admitted caller-constructed manifest identity drift") + } +} + +func TestAdmitMaterializedProjectRoutesEveryManifestArtifactKindThroughItsOwner(t *testing.T) { + request, err := admitRequest(validRequest(t, t.TempDir())) + if err != nil { + t.Fatal(err) + } + artifacts, err := childArtifacts(request) + if err != nil { + t.Fatal(err) + } + manifest, err := buildManifest(request, artifacts) + if err != nil { + t.Fatal(err) + } + records := make([]RoutedProjectRecord, 0, len(artifacts)) + for _, artifact := range artifacts { + records = append(records, RoutedProjectRecord{Content: artifact.Content, Path: artifact.Path}) + } + result, err := AdmitMaterializedProject(manifest, records) + if err != nil || !result.ClosureEvaluated || !result.ClosureAdmitted || len(result.Records) != len(manifest.Routes) { + t.Fatalf("AdmitMaterializedProject()=%#v, %v", result, err) + } + for _, item := range result.Records { + if !item.DigestMatches || !item.Admitted { + t.Fatalf("route %s was not owner-admitted: %#v", item.Path, item) + } + } + + for index := range records { + kind := "unknown" + for _, route := range manifest.Routes { + if route.Path == records[index].Path { + kind = route.ArtifactKind + } + } + t.Run(kind+"/digest_mismatch", func(t *testing.T) { + mutant := append([]RoutedProjectRecord(nil), records...) + mutant[index].Content = bytes.Repeat([]byte{'x'}, len(mutant[index].Content)) + got, err := AdmitMaterializedProject(manifest, mutant) + var routeAdmission RoutedProjectRecordAdmission + for _, item := range got.Records { + if item.Path == mutant[index].Path { + routeAdmission = item + } + } + if err != nil || got.ClosureEvaluated || got.ClosureAdmitted || routeAdmission.DigestMatches || routeAdmission.Admitted { + t.Fatalf("mutated route admission=%#v, %v", got, err) + } + }) + + t.Run(kind+"/semantic_owner_rejection", func(t *testing.T) { + invalidContent := []byte("{}") + mutant := append([]RoutedProjectRecord(nil), records...) + mutant[index].Content = invalidContent + mutantManifest := readmitManifest(t, manifest, func(candidate *Manifest) { + for routeIndex := range candidate.Routes { + if candidate.Routes[routeIndex].Path == mutant[index].Path { + candidate.Routes[routeIndex].ArtifactID = digest.SHA256BytesRef(invalidContent) + } + } + }) + got, err := AdmitMaterializedProject(mutantManifest, mutant) + var routeAdmission RoutedProjectRecordAdmission + for _, item := range got.Records { + if item.Path == mutant[index].Path { + routeAdmission = item + } + } + if err != nil || got.ClosureEvaluated || got.ClosureAdmitted || !routeAdmission.DigestMatches || routeAdmission.Admitted { + t.Fatalf("semantically invalid route admission=%#v, %v", got, err) + } + }) + } +} + +func TestAdmitMaterializedProjectRejectsSurplusAndDuplicateRoutes(t *testing.T) { + snapshot := validMaterializedProjectSnapshot(t) + record := RoutedProjectRecord{Path: snapshot.Manifest.Routes[0].Path} + if _, err := AdmitMaterializedProject(snapshot.Manifest, []RoutedProjectRecord{record, record}); err == nil { + t.Fatal("AdmitMaterializedProject() admitted duplicate record paths") + } + record.Path = "proofkit/unrouted.json" + if _, err := AdmitMaterializedProject(snapshot.Manifest, []RoutedProjectRecord{record}); err == nil { + t.Fatal("AdmitMaterializedProject() admitted an unmanifested record path") + } +} + +func validMaterializedProjectSnapshot(t *testing.T) materializedProjectSnapshot { + t.Helper() + request, err := admitRequest(validRequest(t, t.TempDir())) + if err != nil { + t.Fatal(err) + } + children, err := childArtifacts(request) + if err != nil { + t.Fatal(err) + } + manifest, err := buildManifest(request, children) + if err != nil { + t.Fatal(err) + } + return materializedProjectSnapshot{ + Binding: request.Binding, BindingPath: request.BindingPath, + Inventory: request.Inventory, InventoryPath: request.InventoryPath, + Manifest: manifest, Sources: request.Sources, + } +} + +func readmitManifest(t *testing.T, manifest Manifest, mutate func(*Manifest)) Manifest { + t.Helper() + manifest.Routes = append([]Route(nil), manifest.Routes...) + mutate(&manifest) + manifest.ManifestID = "" + manifestID, err := digest.StableJSONSHA256Ref(manifest.identityValue()) + if err != nil { + t.Fatal(err) + } + manifest.ManifestID = manifestID + admitted, err := AdmitManifest(manifest.JSONValue()) + if err != nil { + t.Fatalf("independent manifest admission failed: %v", err) + } + return admitted +} diff --git a/internal/command/projectstatus/dependency_test.go b/internal/command/projectstatus/dependency_test.go new file mode 100644 index 0000000..b2a42e8 --- /dev/null +++ b/internal/command/projectstatus/dependency_test.go @@ -0,0 +1,52 @@ +package projectstatus + +import ( + "go/parser" + "go/token" + "os" + "strings" + "testing" +) + +func TestProjectStatusDelegatesChildAdmissionToMaterializationOwner(t *testing.T) { + for _, entry := range mustProductionGoFiles(t) { + content, err := os.ReadFile(entry) + if err != nil { + t.Fatal(err) + } + parsed, err := parser.ParseFile(token.NewFileSet(), entry, content, parser.ImportsOnly) + if err != nil { + t.Fatal(err) + } + for _, imported := range parsed.Imports { + path := strings.Trim(imported.Path.Value, "\"") + for _, forbidden := range []string{ + "internal/command/requirementbinding", + "internal/command/requirementsourceadmission", + "internal/command/testevidenceinventory", + } { + if strings.HasSuffix(path, forbidden) { + t.Fatalf("%s imports child semantic owner %s directly", entry, path) + } + } + } + } +} + +func mustProductionGoFiles(t *testing.T) []string { + t.Helper() + entries, err := os.ReadDir(".") + if err != nil { + t.Fatal(err) + } + files := []string{} + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".go") && !strings.HasSuffix(entry.Name(), "_test.go") { + files = append(files, entry.Name()) + } + } + if len(files) == 0 { + t.Fatal("project status package has no production files") + } + return files +} diff --git a/internal/command/projectstatus/evaluate.go b/internal/command/projectstatus/evaluate.go new file mode 100644 index 0000000..4d90719 --- /dev/null +++ b/internal/command/projectstatus/evaluate.go @@ -0,0 +1,179 @@ +package projectstatus + +import ( + "fmt" + "reflect" + "sort" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" +) + +func evaluate(snapshot inspectionSnapshot) (Status, error) { + if err := validateSnapshot(snapshot); err != nil { + return Status{}, err + } + id, err := snapshotID(snapshot) + if err != nil { + return Status{}, fmt.Errorf("derive project status snapshot identity") + } + state, issues := classify(snapshot) + action, err := canonicalAction(state, issues, actionContext(state, snapshot)) + if err != nil { + return Status{}, fmt.Errorf("derive project status next action: %w", err) + } + status := Status{ + IssueCodes: issues, + ManifestID: snapshot.Manifest.ManifestID, + NextAction: action, + ProjectID: snapshot.ProjectID, + ProjectState: state, + SnapshotID: id, + } + statusID, err := digest.StableJSONSHA256Ref(status.identityValue()) + if err != nil { + return Status{}, fmt.Errorf("derive project status identity") + } + status.StatusID = statusID + if _, err := AdmitStatusOutput(status.JSONValue()); err != nil { + return Status{}, fmt.Errorf("admit generated project status: %w", err) + } + return status, nil +} + +func NextFromStatus(status Status) (Next, error) { + admitted, err := AdmitStatusOutput(status.JSONValue()) + if err != nil { + return Next{}, err + } + next := Next{ + Action: admitted.NextAction, + IssueCodes: append([]string{}, admitted.IssueCodes...), + ProjectState: admitted.ProjectState, + SnapshotID: admitted.SnapshotID, + StatusRef: admitted.StatusID, + } + id, err := digest.StableJSONSHA256Ref(next.identityValue()) + if err != nil { + return Next{}, fmt.Errorf("derive project next-action identity") + } + next.PacketID = id + if _, err := AdmitNextOutput(next.JSONValue()); err != nil { + return Next{}, fmt.Errorf("admit generated project next action: %w", err) + } + return next, nil +} + +func classify(snapshot inspectionSnapshot) (ProjectState, []string) { + if snapshot.Transaction.State == TransactionInvalid { + return StateBlocked, []string{IssueTransactionInvalid} + } + if snapshot.Transaction.State == TransactionRecoverable { + return StateRecoveryRequired, []string{IssueTransactionRecoveryRequired} + } + if snapshot.Manifest.State == ManifestAbsent { + return StateUninitialized, []string{IssueManifestMissing} + } + if snapshot.Manifest.State == ManifestInvalid { + return StateBlocked, []string{IssueManifestInvalid} + } + issues := make([]string, 0, len(snapshot.Children)+1) + invalid := false + stale := false + for _, child := range snapshot.Children { + switch child.State { + case ChildMissing: + issues = append(issues, IssueChildMissing) + stale = true + case ChildDigestMismatch: + issues = append(issues, IssueChildDigestMismatch) + stale = true + case ChildInvalid: + issues = append(issues, IssueChildInvalid) + invalid = true + } + } + if snapshot.ClosureState == ClosureInvalid { + issues = append(issues, IssueClosureInvalid) + invalid = true + } + issues = sortedUnique(issues) + if invalid { + return StateBlocked, issues + } + if stale { + return StateStale, issues + } + return StateVerificationRequired, issues +} + +func actionContext(state ProjectState, snapshot inspectionSnapshot) string { + switch state { + case StateRecoveryRequired: + return snapshot.Transaction.TransactionID + case StateStale, StateVerificationRequired: + return snapshot.Manifest.ManifestID + default: + return "" + } +} + +func canonicalAction(state ProjectState, issues []string, contextRef string) (NextAction, error) { + action := NextAction{CommandRoute: []string{}, Executable: false} + switch state { + case StateBlocked: + if contextRef != "" || len(issues) == 0 { + return NextAction{}, fmt.Errorf("project blocked issue set is invalid") + } + if reflect.DeepEqual(issues, []string{IssueTransactionInvalid}) { + action.ActionClass = ActionRepairControlState + } else if reflect.DeepEqual(issues, []string{IssueManifestInvalid}) || reflect.DeepEqual(issues, []string{IssueClosureInvalid}) || + allIssuesIn(issues, IssueChildInvalid, IssueChildMissing, IssueChildDigestMismatch) && containsAnyIssue(issues, IssueChildInvalid) { + action.ActionClass = ActionRepairProjectRecords + } else { + return NextAction{}, fmt.Errorf("project blocked issue set is invalid") + } + case StateRecoveryRequired: + if !reflect.DeepEqual(issues, []string{IssueTransactionRecoveryRequired}) || contextRef == "" { + return NextAction{}, fmt.Errorf("project recovery-required projection is invalid") + } + action.ActionClass = ActionChooseRecovery + action.CommandRoute = []string{"adopt", "materialize", "recover"} + action.ContextRef = contextRef + action.RequiredDecision = "resume_or_rollback" + case StateUninitialized: + if !reflect.DeepEqual(issues, []string{IssueManifestMissing}) || contextRef != "" { + return NextAction{}, fmt.Errorf("project uninitialized issue set is invalid") + } + action.ActionClass = ActionChooseAdoptionMode + action.CommandRoute = []string{"adopt", "plan"} + action.RequiredDecision = "adoption_mode" + case StateStale: + if len(issues) == 0 || !allIssuesIn(issues, IssueChildMissing, IssueChildDigestMismatch) || contextRef == "" { + return NextAction{}, fmt.Errorf("project stale projection is invalid") + } + action.ActionClass = ActionRematerializeProject + action.CommandRoute = []string{"adopt", "materialize", "plan"} + action.ContextRef = contextRef + case StateVerificationRequired: + if len(issues) != 0 || contextRef == "" { + return NextAction{}, fmt.Errorf("project verification-required projection is invalid") + } + action.ActionClass = ActionRunRepositoryVerification + action.ContextRef = contextRef + default: + return NextAction{}, fmt.Errorf("project state is unsupported") + } + action.ActionID = "proofkit.project-status.action." + action.ActionClass + return action, nil +} + +func sortedUnique(values []string) []string { + sort.Strings(values) + result := values[:0] + for _, value := range values { + if len(result) == 0 || result[len(result)-1] != value { + result = append(result, value) + } + } + return append([]string{}, result...) +} diff --git a/internal/command/projectstatus/filesystem.go b/internal/command/projectstatus/filesystem.go new file mode 100644 index 0000000..50e94cf --- /dev/null +++ b/internal/command/projectstatus/filesystem.go @@ -0,0 +1,107 @@ +package projectstatus + +import ( + "context" + "errors" + "fmt" + "io" + "io/fs" + "os" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" + "github.com/research-engineering/agentic-proofkit/internal/kernel/repositorytransaction" +) + +var errSnapshotChanged = errors.New("project status snapshot changed during inspection") + +type fileState string + +const ( + fileMissing fileState = "missing" + fileInvalid fileState = "invalid" + fileRead fileState = "read" +) + +type fileObservation struct { + content []byte + digest string + state fileState +} + +type readBudget struct { + remaining int64 +} + +func readProjectFile(ctx context.Context, lease *repositorytransaction.InspectionLease, relativePath string, budget *readBudget) (fileObservation, error) { + return readProjectFileWithHook(ctx, lease, relativePath, budget, nil) +} + +func readProjectFileWithHook(ctx context.Context, lease *repositorytransaction.InspectionLease, relativePath string, budget *readBudget, beforeRouteReopen func()) (observation fileObservation, returnErr error) { + if err := ctx.Err(); err != nil { + return fileObservation{}, err + } + file, err := lease.OpenExactRegularFile(relativePath) + if errors.Is(err, fs.ErrNotExist) { + return fileObservation{state: fileMissing}, nil + } + if errors.Is(err, repositorytransaction.ErrUnsafeInspectionRoute) { + return fileObservation{state: fileInvalid}, nil + } + if errors.Is(err, repositorytransaction.ErrInspectionRouteChanged) { + return fileObservation{}, errSnapshotChanged + } + if err != nil { + return fileObservation{}, fmt.Errorf("open project status record route") + } + defer func() { + if closeErr := file.Close(); closeErr != nil { + observation = fileObservation{} + returnErr = fmt.Errorf("close project status record: %w", closeErr) + } + }() + opened, err := file.Stat() + if err != nil || !opened.Mode().IsRegular() || opened.Mode()&^fs.ModePerm != 0 { + return fileObservation{}, fmt.Errorf("inspect project status record") + } + if opened.Size() < 0 || opened.Size() > MaximumFileBytes || opened.Size() > budget.remaining { + return fileObservation{state: fileInvalid}, nil + } + content, err := io.ReadAll(io.LimitReader(file, opened.Size()+1)) + if err != nil { + return fileObservation{}, fmt.Errorf("read project status record") + } + if int64(len(content)) != opened.Size() { + return fileObservation{}, errSnapshotChanged + } + afterHandle, err := file.Stat() + if err != nil || !os.SameFile(opened, afterHandle) || opened.Size() != afterHandle.Size() || !opened.ModTime().Equal(afterHandle.ModTime()) { + return fileObservation{}, errSnapshotChanged + } + if beforeRouteReopen != nil { + beforeRouteReopen() + } + current, err := lease.OpenExactRegularFile(relativePath) + if errors.Is(err, fs.ErrNotExist) || errors.Is(err, repositorytransaction.ErrUnsafeInspectionRoute) || errors.Is(err, repositorytransaction.ErrInspectionRouteChanged) { + return fileObservation{}, errSnapshotChanged + } + if err != nil { + return fileObservation{}, fmt.Errorf("reopen project status record route") + } + if err := verifyReopenedProjectFile(current, opened, int64(len(content))); err != nil { + return fileObservation{}, err + } + budget.remaining -= int64(len(content)) + return fileObservation{content: append([]byte{}, content...), digest: digest.SHA256BytesRef(content), state: fileRead}, nil +} + +func verifyReopenedProjectFile(current repositorytransaction.InspectionFile, opened fs.FileInfo, contentSize int64) error { + currentInfo, statErr := current.Stat() + closeErr := current.Close() + if closeErr != nil { + return fmt.Errorf("close rechecked project status record: %w", closeErr) + } + if statErr != nil || !os.SameFile(opened, currentInfo) || currentInfo.Size() != contentSize { + return errSnapshotChanged + } + return nil +} diff --git a/internal/command/projectstatus/inspect.go b/internal/command/projectstatus/inspect.go new file mode 100644 index 0000000..57475d6 --- /dev/null +++ b/internal/command/projectstatus/inspect.go @@ -0,0 +1,242 @@ +package projectstatus + +import ( + "bytes" + "context" + "errors" + "fmt" + + "github.com/research-engineering/agentic-proofkit/internal/command/adoptionmaterialization" + "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/repositorytransaction" +) + +type controlInspector func(context.Context, *repositorytransaction.InspectionLease) (repositorytransaction.ControlInspection, error) +type projectFileReader func(context.Context, *repositorytransaction.InspectionLease, string, *readBudget) (fileObservation, error) + +type inspectionDependencies struct { + inspectControl controlInspector + readFile projectFileReader + closeLease func(*repositorytransaction.InspectionLease) error +} + +type cohortEntry struct { + digest string + path string + state fileState +} + +var defaultInspectionDependencies = inspectionDependencies{ + inspectControl: func(ctx context.Context, lease *repositorytransaction.InspectionLease) (repositorytransaction.ControlInspection, error) { + return lease.InspectControlState(ctx) + }, + readFile: readProjectFile, + closeLease: func(lease *repositorytransaction.InspectionLease) error { + return lease.Close() + }, +} + +func Inspect(ctx context.Context, repositoryRoot string) (Status, error) { + return inspectWithDependencies(ctx, repositoryRoot, defaultInspectionDependencies) +} + +func inspectWithDependencies(ctx context.Context, repositoryRoot string, dependencies inspectionDependencies) (Status, error) { + if dependencies.inspectControl == nil || dependencies.readFile == nil { + return Status{}, fmt.Errorf("project status inspection dependencies are incomplete") + } + for attempt := 0; attempt < 2; attempt++ { + status, err := inspectAttempt(ctx, repositoryRoot, dependencies) + if err == nil { + return status, nil + } + if !errors.Is(err, errSnapshotChanged) && !errors.Is(err, repositorytransaction.ErrControlStateChanged) { + return Status{}, err + } + } + return Status{}, fmt.Errorf("project status repository changed during both bounded inspection attempts") +} + +func inspectAttempt(ctx context.Context, repositoryRoot string, dependencies inspectionDependencies) (status Status, returnErr error) { + lease, err := repositorytransaction.OpenInspectionLease(ctx, repositoryRoot) + if err != nil { + return Status{}, err + } + closeLease := dependencies.closeLease + if closeLease == nil { + closeLease = defaultInspectionDependencies.closeLease + } + defer func() { + if closeErr := closeLease(lease); closeErr != nil { + status = Status{} + returnErr = fmt.Errorf("close project status inspection: %w", closeErr) + } + }() + before, err := dependencies.inspectControl(ctx, lease) + if err != nil { + return Status{}, err + } + if err := lease.VerifyRootIdentity(); err != nil { + return Status{}, err + } + transaction, err := observeTransaction(before) + if err != nil { + return Status{}, err + } + snapshot := inspectionSnapshot{ + ClosureState: ClosureNotEvaluated, + Manifest: manifestObservation{State: ManifestAbsent}, + Transaction: transaction, + } + var cohort []cohortEntry + if transaction.State == TransactionClean { + snapshot, cohort, err = inspectProjectFiles(ctx, lease, transaction, dependencies.readFile) + if err != nil { + return Status{}, err + } + if err := verifyCohort(ctx, lease, cohort, dependencies.readFile); err != nil { + return Status{}, err + } + } + after, err := dependencies.inspectControl(ctx, lease) + if err != nil { + return Status{}, err + } + if before != after { + return Status{}, errSnapshotChanged + } + if err := lease.VerifyRootIdentity(); err != nil { + return Status{}, err + } + return evaluate(snapshot) +} + +func observeTransaction(value repositorytransaction.ControlInspection) (transactionObservation, error) { + result := transactionObservation{Epoch: value.EpochID, TransactionID: value.TransactionID} + switch value.State { + case repositorytransaction.ControlStateClean: + result.State = TransactionClean + case repositorytransaction.ControlStateRecoverable: + result.State = TransactionRecoverable + case repositorytransaction.ControlStateInvalid: + result.State = TransactionInvalid + default: + return transactionObservation{}, fmt.Errorf("repository transaction owner returned an unsupported control state") + } + return result, nil +} + +func inspectProjectFiles(ctx context.Context, lease *repositorytransaction.InspectionLease, transaction transactionObservation, readFile projectFileReader) (inspectionSnapshot, []cohortEntry, error) { + snapshot := inspectionSnapshot{ + ClosureState: ClosureNotEvaluated, + Manifest: manifestObservation{State: ManifestAbsent}, + Transaction: transaction, + } + budget := &readBudget{remaining: MaximumAggregateBytes} + manifestFile, err := readFile(ctx, lease, adoptionmaterialization.ProjectManifestPath, budget) + if err != nil { + return inspectionSnapshot{}, nil, err + } + cohort := []cohortEntry{{digest: manifestFile.digest, path: adoptionmaterialization.ProjectManifestPath, state: manifestFile.state}} + switch manifestFile.state { + case fileMissing: + return snapshot, cohort, nil + case fileInvalid: + snapshot.Manifest.State = ManifestInvalid + return snapshot, cohort, nil + case fileRead: + snapshot.Manifest.ContentDigest = manifestFile.digest + default: + return inspectionSnapshot{}, nil, fmt.Errorf("project status file owner returned an unsupported state") + } + rawManifest, err := admission.DecodeJSON(bytes.NewReader(manifestFile.content), MaximumFileBytes) + if err != nil { + snapshot.Manifest.State = ManifestInvalid + return snapshot, cohort, nil + } + manifest, err := adoptionmaterialization.AdmitManifest(rawManifest) + if err != nil { + snapshot.Manifest.State = ManifestInvalid + return snapshot, cohort, nil + } + snapshot.Manifest = manifestObservation{ContentDigest: manifestFile.digest, ManifestID: manifest.ManifestID, State: ManifestAdmitted} + snapshot.ProjectID = manifest.ProjectID + children, closure, childCohort, err := inspectChildren(ctx, lease, manifest, budget, readFile) + if err != nil { + return inspectionSnapshot{}, nil, err + } + cohort = append(cohort, childCohort...) + snapshot.Children = children + snapshot.ClosureState = closure + return snapshot, cohort, nil +} + +func inspectChildren(ctx context.Context, lease *repositorytransaction.InspectionLease, manifest adoptionmaterialization.Manifest, budget *readBudget, readFile projectFileReader) ([]childObservation, ClosureState, []cohortEntry, error) { + observations := make(map[string]fileObservation, len(manifest.Routes)) + records := make([]adoptionmaterialization.RoutedProjectRecord, 0, len(manifest.Routes)) + cohort := make([]cohortEntry, 0, len(manifest.Routes)) + for _, route := range manifest.Routes { + file, err := readFile(ctx, lease, route.Path, budget) + if err != nil { + return nil, ClosureNotEvaluated, nil, err + } + observations[route.Path] = file + cohort = append(cohort, cohortEntry{digest: file.digest, path: route.Path, state: file.state}) + if file.state == fileRead { + records = append(records, adoptionmaterialization.RoutedProjectRecord{Content: file.content, Path: route.Path}) + } + } + admissionResult, err := adoptionmaterialization.AdmitMaterializedProject(manifest, records) + if err != nil { + return nil, ClosureNotEvaluated, nil, err + } + admissions := make(map[string]adoptionmaterialization.RoutedProjectRecordAdmission, len(admissionResult.Records)) + for _, item := range admissionResult.Records { + admissions[item.Path] = item + } + children := make([]childObservation, 0, len(manifest.Routes)) + for _, route := range manifest.Routes { + file := observations[route.Path] + child := childObservation{ArtifactKind: route.ArtifactKind, ExpectedDigest: route.ArtifactID, ObservedDigest: file.digest} + switch file.state { + case fileMissing: + child.State = ChildMissing + case fileInvalid: + child.State = ChildInvalid + case fileRead: + admitted := admissions[route.Path] + switch { + case !admitted.DigestMatches: + child.State = ChildDigestMismatch + case !admitted.Admitted: + child.State = ChildInvalid + default: + child.State = ChildAdmitted + } + default: + return nil, ClosureNotEvaluated, nil, fmt.Errorf("project status file owner returned an unsupported state") + } + children = append(children, child) + } + closure := ClosureNotEvaluated + if admissionResult.ClosureEvaluated { + closure = ClosureInvalid + if admissionResult.ClosureAdmitted { + closure = ClosureAdmitted + } + } + return children, closure, cohort, nil +} + +func verifyCohort(ctx context.Context, lease *repositorytransaction.InspectionLease, cohort []cohortEntry, readFile projectFileReader) error { + budget := &readBudget{remaining: MaximumAggregateBytes} + for _, expected := range cohort { + observed, err := readFile(ctx, lease, expected.path, budget) + if err != nil { + return err + } + if observed.state != expected.state || observed.digest != expected.digest { + return errSnapshotChanged + } + } + return nil +} diff --git a/internal/command/projectstatus/inspect_test.go b/internal/command/projectstatus/inspect_test.go new file mode 100644 index 0000000..a749cc1 --- /dev/null +++ b/internal/command/projectstatus/inspect_test.go @@ -0,0 +1,568 @@ +package projectstatus + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/command/adoptionmaterialization" + "github.com/research-engineering/agentic-proofkit/internal/command/adoptionplan" + "github.com/research-engineering/agentic-proofkit/internal/command/repositoryinventory" + "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" + "github.com/research-engineering/agentic-proofkit/internal/kernel/repositorytransaction" + "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" + "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" +) + +func TestInspectClassifiesMaterializedProjectWithoutMutation(t *testing.T) { + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.086999612230625810638279009641652637656804117475637022915480714642144225105240") + root := t.TempDir() + status, err := Inspect(context.Background(), root) + if err != nil { + t.Fatal(err) + } + if status.ProjectState != StateUninitialized || status.NextAction.ActionClass != ActionChooseAdoptionMode { + t.Fatalf("Inspect() = %#v", status) + } + if _, err := os.Stat(filepath.Join(root, repositorytransaction.ControlRoot)); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("Inspect() created transaction state: %v", err) + } + + materializeTestProject(t, root) + status, err = Inspect(context.Background(), root) + if err != nil { + t.Fatal(err) + } + if status.ProjectState != StateVerificationRequired || status.ProjectID != "pilot.project" || status.ManifestID == "" { + t.Fatalf("Inspect() = %#v", status) + } + + sourcePath := filepath.Join(root, "docs", "specs", "pilot", "requirements.v1.json") + if err := os.WriteFile(sourcePath, []byte("{}\n"), 0o644); err != nil { + t.Fatal(err) + } + status, err = Inspect(context.Background(), root) + if err != nil { + t.Fatal(err) + } + if status.ProjectState != StateStale || !reflectIssue(status.IssueCodes, IssueChildDigestMismatch) { + t.Fatalf("Inspect() after drift = %#v", status) + } +} + +func TestInspectRejectsAdmittedChildrenWithInvalidCrossRecordClosure(t *testing.T) { + root := t.TempDir() + materializeTestProject(t, root) + breakMaterializedProjectClosure(t, root) + status, err := Inspect(context.Background(), root) + if err != nil { + t.Fatal(err) + } + if status.ProjectState != StateBlocked || !reflectIssue(status.IssueCodes, IssueClosureInvalid) { + t.Fatalf("Inspect() = %#v, want blocked closure-invalid status", status) + } +} + +func TestInspectFailsClosedOnSymlinksAndBoundsWithoutDisclosure(t *testing.T) { + t.Run("manifest symlink", func(t *testing.T) { + root := t.TempDir() + if err := os.MkdirAll(filepath.Join(root, "proofkit"), 0o755); err != nil { + t.Fatal(err) + } + target := filepath.Join(root, "private.json") + if err := os.WriteFile(target, []byte("caller-private-value"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.Symlink(target, filepath.Join(root, filepath.FromSlash(adoptionmaterialization.ProjectManifestPath))); err != nil { + t.Fatal(err) + } + status, err := Inspect(context.Background(), root) + if err != nil { + t.Fatal(err) + } + if status.ProjectState != StateBlocked || strings.Contains(string(mustStatusJSON(t, status)), "caller-private-value") || strings.Contains(string(mustStatusJSON(t, status)), target) { + t.Fatalf("Inspect() disclosed symlink data: %#v", status) + } + }) + + t.Run("oversize manifest", func(t *testing.T) { + root := t.TempDir() + manifestPath := filepath.Join(root, filepath.FromSlash(adoptionmaterialization.ProjectManifestPath)) + if err := os.MkdirAll(filepath.Dir(manifestPath), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(manifestPath, make([]byte, MaximumFileBytes+1), 0o644); err != nil { + t.Fatal(err) + } + status, err := Inspect(context.Background(), root) + if err != nil || status.ProjectState != StateBlocked { + t.Fatalf("Inspect() status=%#v error=%v", status, err) + } + }) + + t.Run("root symlink", func(t *testing.T) { + realRoot := t.TempDir() + alias := filepath.Join(t.TempDir(), "repository") + if err := os.Symlink(realRoot, alias); err != nil { + t.Fatal(err) + } + if _, err := Inspect(context.Background(), alias); err == nil || !strings.Contains(err.Error(), "non-symlink") { + t.Fatalf("Inspect() error = %v", err) + } + }) +} + +func TestInspectRejectsCaseAliasedCanonicalRoute(t *testing.T) { + root := t.TempDir() + materializeTestProject(t, root) + if err := os.Rename(filepath.Join(root, "docs"), filepath.Join(root, "Docs")); err != nil { + t.Fatal(err) + } + if _, err := Inspect(context.Background(), root); err == nil || !strings.Contains(err.Error(), "record route") || strings.Contains(err.Error(), root) { + t.Fatalf("Inspect() error = %v, want non-disclosing confinement failure", err) + } +} + +func TestInspectCohortValidationClosesCleanEpochABA(t *testing.T) { + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.069681249039502790550676865624759525498194884189057668377576360433547116631868") + root := t.TempDir() + control := repositorytransaction.ControlInspection{ + EpochID: digest.SHA256TextRef("unchanged clean epoch"), + State: repositorytransaction.ControlStateClean, + } + reads := 0 + dependencies := inspectionDependencies{ + inspectControl: func(context.Context, *repositorytransaction.InspectionLease) (repositorytransaction.ControlInspection, error) { + return control, nil + }, + readFile: func(context.Context, *repositorytransaction.InspectionLease, string, *readBudget) (fileObservation, error) { + reads++ + if reads%2 == 1 { + return fileObservation{state: fileMissing}, nil + } + return fileObservation{state: fileInvalid}, nil + }, + } + if _, err := inspectWithDependencies(context.Background(), root, dependencies); err == nil || !strings.Contains(err.Error(), "both bounded inspection attempts") { + t.Fatalf("inspectWithDependencies() error = %v", err) + } + if reads != 4 { + t.Fatalf("read count = %d, want two complete two-pass attempts", reads) + } +} + +func TestInspectCleanupFailureDominatesRetryableSnapshotChange(t *testing.T) { + controlReads := 0 + closeCalls := 0 + dependencies := inspectionDependencies{ + inspectControl: func(context.Context, *repositorytransaction.InspectionLease) (repositorytransaction.ControlInspection, error) { + controlReads++ + return repositorytransaction.ControlInspection{ + EpochID: digest.SHA256TextRef(fmt.Sprintf("control epoch %d", controlReads)), + State: repositorytransaction.ControlStateClean, + }, nil + }, + readFile: func(context.Context, *repositorytransaction.InspectionLease, string, *readBudget) (fileObservation, error) { + return fileObservation{state: fileMissing}, nil + }, + closeLease: func(lease *repositorytransaction.InspectionLease) error { + closeCalls++ + if err := lease.Close(); err != nil { + return err + } + return errors.New("injected inspection cleanup failure") + }, + } + if _, err := inspectWithDependencies(context.Background(), t.TempDir(), dependencies); err == nil || !strings.Contains(err.Error(), "cleanup failure") || errors.Is(err, errSnapshotChanged) { + t.Fatalf("inspectWithDependencies() error=%v, want terminal cleanup failure", err) + } + if controlReads != 2 || closeCalls != 1 { + t.Fatalf("control reads=%d close calls=%d, want no retry after cleanup failure", controlReads, closeCalls) + } +} + +func TestReopenedProjectFileCleanupFailureDominatesSnapshotClassification(t *testing.T) { + path := filepath.Join(t.TempDir(), "record.json") + if err := os.WriteFile(path, []byte("{}\n"), 0o644); err != nil { + t.Fatal(err) + } + info, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + file := &cleanupFailingInspectionFile{info: info} + err = verifyReopenedProjectFile(file, info, info.Size()) + if err == nil || errors.Is(err, errSnapshotChanged) || !strings.Contains(err.Error(), "injected close failure") { + t.Fatalf("verifyReopenedProjectFile() error=%v, want terminal cleanup failure", err) + } +} + +type cleanupFailingInspectionFile struct { + info os.FileInfo +} + +func (*cleanupFailingInspectionFile) Read([]byte) (int, error) { + return 0, nil +} + +func (file *cleanupFailingInspectionFile) Stat() (os.FileInfo, error) { + return file.info, nil +} + +func (*cleanupFailingInspectionFile) Close() error { + return errors.New("injected close failure") +} + +func TestInspectMapsRecoverableControlState(t *testing.T) { + transactionID := digest.SHA256TextRef("recoverable transaction") + control := repositorytransaction.ControlInspection{ + EpochID: digest.SHA256TextRef("recoverable epoch"), + State: repositorytransaction.ControlStateRecoverable, + TransactionID: transactionID, + } + dependencies := inspectionDependencies{ + inspectControl: func(context.Context, *repositorytransaction.InspectionLease) (repositorytransaction.ControlInspection, error) { + return control, nil + }, + readFile: func(context.Context, *repositorytransaction.InspectionLease, string, *readBudget) (fileObservation, error) { + t.Fatal("recoverable control state must dominate project-file reads") + return fileObservation{}, nil + }, + } + status, err := inspectWithDependencies(context.Background(), t.TempDir(), dependencies) + if err != nil { + t.Fatal(err) + } + if status.ProjectState != StateRecoveryRequired || status.NextAction.ActionClass != ActionChooseRecovery || status.NextAction.ContextRef != transactionID { + t.Fatalf("inspectWithDependencies() = %#v", status) + } +} + +func TestInspectMapsInvalidControlState(t *testing.T) { + root := t.TempDir() + controlDirectory := filepath.Join(root, filepath.FromSlash(repositorytransaction.ControlDirectory)) + if err := os.MkdirAll(controlDirectory, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(controlDirectory, "unknown"), []byte("opaque"), 0o600); err != nil { + t.Fatal(err) + } + status, err := Inspect(context.Background(), root) + if err != nil { + t.Fatal(err) + } + if status.ProjectState != StateBlocked || status.NextAction.ActionClass != ActionRepairControlState || !reflectIssue(status.IssueCodes, IssueTransactionInvalid) { + t.Fatalf("Inspect()=%#v, want invalid transaction classification", status) + } +} + +func TestInspectAttemptRejectsFinalRepositoryRootReplacement(t *testing.T) { + parent := t.TempDir() + root := filepath.Join(parent, "repository") + if err := os.Mkdir(root, 0o755); err != nil { + t.Fatal(err) + } + control := repositorytransaction.ControlInspection{EpochID: digest.SHA256TextRef("stable epoch"), State: repositorytransaction.ControlStateClean} + controlReads := 0 + dependencies := inspectionDependencies{ + inspectControl: func(context.Context, *repositorytransaction.InspectionLease) (repositorytransaction.ControlInspection, error) { + controlReads++ + if controlReads == 2 { + if err := os.Rename(root, root+"-original"); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(root, 0o755); err != nil { + t.Fatal(err) + } + } + return control, nil + }, + readFile: func(context.Context, *repositorytransaction.InspectionLease, string, *readBudget) (fileObservation, error) { + return fileObservation{state: fileMissing}, nil + }, + } + if _, err := inspectAttempt(context.Background(), root, dependencies); !errors.Is(err, repositorytransaction.ErrControlStateChanged) { + t.Fatalf("inspectAttempt() error=%v, want repository-root change", err) + } +} + +func TestInspectRejectsChangingControlEpochAcrossBothAttempts(t *testing.T) { + controlReads := 0 + dependencies := inspectionDependencies{ + inspectControl: func(context.Context, *repositorytransaction.InspectionLease) (repositorytransaction.ControlInspection, error) { + controlReads++ + return repositorytransaction.ControlInspection{ + EpochID: digest.SHA256TextRef(fmt.Sprintf("control epoch %d", controlReads%2)), + State: repositorytransaction.ControlStateClean, + }, nil + }, + readFile: func(context.Context, *repositorytransaction.InspectionLease, string, *readBudget) (fileObservation, error) { + return fileObservation{state: fileMissing}, nil + }, + } + if _, err := inspectWithDependencies(context.Background(), t.TempDir(), dependencies); err == nil || !strings.Contains(err.Error(), "both bounded inspection attempts") { + t.Fatalf("inspectWithDependencies() error = %v", err) + } + if controlReads != 4 { + t.Fatalf("control read count = %d, want two reads per bounded attempt", controlReads) + } +} + +func TestReadProjectFileEnforcesAggregateBoundBeforeRead(t *testing.T) { + rootPath := t.TempDir() + lease, err := repositorytransaction.OpenInspectionLease(context.Background(), rootPath) + if err != nil { + t.Fatal(err) + } + defer lease.Close() + content := bytes.Repeat([]byte{'a'}, MaximumFileBytes) + for index := 0; index < 8; index++ { + name := fmt.Sprintf("record-%d.json", index) + if err := os.WriteFile(filepath.Join(rootPath, name), content, 0o644); err != nil { + t.Fatal(err) + } + } + if err := os.WriteFile(filepath.Join(rootPath, "overflow.json"), []byte{'x'}, 0o644); err != nil { + t.Fatal(err) + } + budget := &readBudget{remaining: MaximumAggregateBytes} + for index := 0; index < 8; index++ { + observation, err := readProjectFile(context.Background(), lease, fmt.Sprintf("record-%d.json", index), budget) + if err != nil || observation.state != fileRead { + t.Fatalf("read %d state=%s error=%v", index, observation.state, err) + } + } + if budget.remaining != 0 { + t.Fatalf("remaining aggregate budget = %d", budget.remaining) + } + overflow, err := readProjectFile(context.Background(), lease, "overflow.json", budget) + if err != nil || overflow.state != fileInvalid || len(overflow.content) != 0 || budget.remaining != 0 { + t.Fatalf("overflow=%#v remaining=%d error=%v", overflow, budget.remaining, err) + } +} + +func TestReadProjectFileRejectsSameByteRouteReplacement(t *testing.T) { + root := t.TempDir() + path := filepath.Join(root, "record.json") + content := []byte("{\"state\":\"same\"}\n") + if err := os.WriteFile(path, content, 0o600); err != nil { + t.Fatal(err) + } + lease, err := repositorytransaction.OpenInspectionLease(context.Background(), root) + if err != nil { + t.Fatal(err) + } + defer lease.Close() + _, err = readProjectFileWithHook(context.Background(), lease, "record.json", &readBudget{remaining: MaximumAggregateBytes}, func() { + if renameErr := os.Rename(path, path+".original"); renameErr != nil { + t.Fatal(renameErr) + } + if writeErr := os.WriteFile(path, content, 0o600); writeErr != nil { + t.Fatal(writeErr) + } + }) + if !errors.Is(err, errSnapshotChanged) { + t.Fatalf("readProjectFileWithHook() error=%v, want snapshot change", err) + } +} + +func TestInspectDeduplicatesRepeatedIssueCodes(t *testing.T) { + root := t.TempDir() + materializeTestProject(t, root) + manifestContent, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(adoptionmaterialization.ProjectManifestPath))) + if err != nil { + t.Fatal(err) + } + raw, err := admission.DecodeJSON(bytes.NewReader(manifestContent), MaximumFileBytes) + if err != nil { + t.Fatal(err) + } + manifest, err := adoptionmaterialization.AdmitManifest(raw) + if err != nil { + t.Fatal(err) + } + for _, route := range manifest.Routes[:2] { + if err := os.Remove(filepath.Join(root, filepath.FromSlash(route.Path))); err != nil { + t.Fatal(err) + } + } + status, err := Inspect(context.Background(), root) + if err != nil { + t.Fatal(err) + } + if status.ProjectState != StateStale || len(status.IssueCodes) != 1 || status.IssueCodes[0] != IssueChildMissing { + t.Fatalf("Inspect() issues=%v state=%s, want one missing-record issue", status.IssueCodes, status.ProjectState) + } +} + +func TestOutOfBoundManifestIdentityIsAClassificationNotByteIdentity(t *testing.T) { + statuses := make([]Status, 0, 2) + for _, fill := range []byte{'a', 'b'} { + root := t.TempDir() + manifestPath := filepath.Join(root, filepath.FromSlash(adoptionmaterialization.ProjectManifestPath)) + if err := os.MkdirAll(filepath.Dir(manifestPath), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(manifestPath, bytes.Repeat([]byte{fill}, MaximumFileBytes+1), 0o600); err != nil { + t.Fatal(err) + } + status, err := Inspect(context.Background(), root) + if err != nil { + t.Fatal(err) + } + statuses = append(statuses, status) + } + if statuses[0].ProjectState != StateBlocked || statuses[0].SnapshotID != statuses[1].SnapshotID || statuses[0].StatusID != statuses[1].StatusID { + t.Fatalf("out-of-bound classifications differ: %#v %#v", statuses[0], statuses[1]) + } +} + +func TestInspectHonorsCancellationBeforeReads(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + if _, err := Inspect(ctx, t.TempDir()); !errors.Is(err, context.Canceled) { + t.Fatalf("Inspect() error = %v", err) + } +} + +func materializeTestProject(t *testing.T, root string) { + t.Helper() + if err := os.WriteFile(filepath.Join(root, "README.md"), []byte("# Pilot\n"), 0o644); err != nil { + t.Fatal(err) + } + inventory, err := repositoryinventory.Scan(context.Background(), root) + if err != nil { + t.Fatal(err) + } + sourcePlan, err := adoptionplan.Build(adoptionplan.IntentFresh, inventory, "") + if err != nil { + t.Fatal(err) + } + nonClaims := []any{"Pilot requirement fixture does not prove rollout."} + request := map[string]any{ + "schemaVersion": json.Number("1"), "requestKind": adoptionmaterialization.RequestKind, + "requestId": "pilot.materialization.request", "projectId": "pilot.project", "sourcePlan": sourcePlan.JSONValue(), + "requirementSources": []any{map[string]any{ + "schemaVersion": json.Number("1"), "sourceId": "pilot.requirements", "specPackagePath": "docs/specs/pilot", + "overviewPath": "docs/specs/pilot/overview.md", "requirementsPath": "docs/specs/pilot/requirements.v1.json", + "nonClaims": []any{"Pilot source fixture does not prove production readiness."}, + "requirements": []any{map[string]any{ + "claimLevel": "blocking", "deferral": nil, "invariant": "Pilot materialization preserves admitted requirement meaning.", + "lifecycle": map[string]any{"evidenceRefs": []any{}, "replacementRequirementIds": []any{}, "state": "active"}, + "nonClaimRefs": []any{}, "nonClaims": nonClaims, "ownerId": "pilot.owner", + "proofBindingRefs": []any{"proofkit/requirement-bindings.json"}, "requirementId": "REQ-PILOT-001", "riskClass": "high", + "updatePolicy": map[string]any{"requiresImpactDeclaration": true, "requiresProofBindingReview": true, "reviewOwnerId": "pilot.owner"}, + }}, + }}, + "requirementProofBinding": map[string]any{ + "path": "proofkit/requirement-bindings.json", + "record": map[string]any{ + "schemaVersion": json.Number("1"), "bindingId": "pilot.bindings", + "requirements": []any{map[string]any{"claimLevel": "blocking", "nonClaims": nonClaims, "ownerId": "pilot.owner", "proofState": "witness_backed", "requirementId": "REQ-PILOT-001", "specPath": "docs/specs/pilot/requirements.v1.json"}}, + "bindings": []any{map[string]any{"commandIds": []any{"pilot.command.test"}, "environmentClasses": []any{"local-go"}, "requirementId": "REQ-PILOT-001", "scenarioId": "pilot.scenario.materialization", "witnessId": "pilot.witness.materialization", "witnessKind": "contract", "witnessPath": "internal/pilot/materialization_test.go"}}, + "witnessCommands": []any{map[string]any{"command": "go test ./internal/pilot", "commandId": "pilot.command.test", "environmentClasses": []any{"local-go"}}}, + "selection": map[string]any{"changedPaths": []any{}, "ownerIds": []any{}, "requirementIds": []any{}}, + "nonClaims": []any{"Pilot binding fixture does not execute witnesses."}, + }, + }, + "testEvidenceInventory": map[string]any{ + "path": "proofkit/test-evidence-inventory.json", + "record": map[string]any{ + "schemaVersion": json.Number("1"), "inventoryId": "pilot.inventory", "authority": "caller_owned_inventory", + "entries": []any{map[string]any{ + "testId": "pilot.test.materialization", "selector": "go test ./internal/pilot -run TestMaterialization", "sourcePath": "internal/pilot/materialization_test.go", "ownerId": "pilot.owner", + "evidenceClass": "declared_semantic_falsifier_route", "requirementRefs": []any{"REQ-PILOT-001"}, "ownerInvariantRefs": []any{}, "commandRefs": []any{"pilot.command.test"}, "witnessRefs": []any{"pilot.witness.materialization"}, + "falsifier": map[string]any{"falsifierId": "pilot.falsifier.materialization", "negativeCaseId": "pilot.case.materialization", "wrongImplementationClassId": "pilot.wrong.materialization", "dominanceGroup": "pilot.materialization", "supersedes": []any{}}, + "oracle": map[string]any{"oracleId": "pilot.oracle.materialization", "oracleKind": "negative_exit_and_diagnostic", "expectedPublicOutcome": "invalid materialization fails closed", "assertionSummary": "A contradictory materialization request is rejected before mutation."}, + "nonClaims": []any{}, + }}, + "nonClaims": []any{"Pilot inventory fixture does not execute native tests."}, + }, + }, + "nonClaims": []any{"Pilot materialization request is test-only."}, + } + plan, err := adoptionmaterialization.BuildPlan(context.Background(), request, root) + if err != nil { + t.Fatal(err) + } + if _, exitCode, err := adoptionmaterialization.Apply(context.Background(), request, root, plan.Transaction.TransactionID, plan.Transaction.DesiredStateID); err != nil || exitCode != 0 { + t.Fatalf("Apply() exit=%d error=%v", exitCode, err) + } +} + +func reflectIssue(values []string, expected string) bool { + for _, value := range values { + if value == expected { + return true + } + } + return false +} + +func mustStatusJSON(t *testing.T, status Status) []byte { + t.Helper() + content, err := json.Marshal(status.JSONValue()) + if err != nil { + t.Fatal(err) + } + return content +} + +func breakMaterializedProjectClosure(t *testing.T, root string) { + t.Helper() + inventoryPath := filepath.Join(root, "proofkit", "test-evidence-inventory.json") + inventoryContent, err := os.ReadFile(inventoryPath) + if err != nil { + t.Fatal(err) + } + inventoryRaw, err := admission.DecodeJSON(bytes.NewReader(inventoryContent), MaximumFileBytes) + if err != nil { + t.Fatal(err) + } + inventory := inventoryRaw.(map[string]any) + inventory["entries"].([]any)[0].(map[string]any)["requirementRefs"] = []any{"REQ-PILOT-999"} + inventoryContent, err = stablejson.Marshal(inventory) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(inventoryPath, inventoryContent, 0o600); err != nil { + t.Fatal(err) + } + + manifestPath := filepath.Join(root, filepath.FromSlash(adoptionmaterialization.ProjectManifestPath)) + manifestContent, err := os.ReadFile(manifestPath) + if err != nil { + t.Fatal(err) + } + manifestRaw, err := admission.DecodeJSON(bytes.NewReader(manifestContent), MaximumFileBytes) + if err != nil { + t.Fatal(err) + } + manifest := manifestRaw.(map[string]any) + for _, raw := range manifest["routes"].([]any) { + route := raw.(map[string]any) + if route["path"] == "proofkit/test-evidence-inventory.json" { + route["artifactId"] = digest.SHA256BytesRef(inventoryContent) + } + } + delete(manifest, "manifestId") + manifestID, err := digest.StableJSONSHA256Ref(manifest) + if err != nil { + t.Fatal(err) + } + manifest["manifestId"] = manifestID + manifestContent, err = stablejson.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(manifestPath, manifestContent, 0o600); err != nil { + t.Fatal(err) + } +} diff --git a/internal/command/projectstatus/model.go b/internal/command/projectstatus/model.go new file mode 100644 index 0000000..09d07d5 --- /dev/null +++ b/internal/command/projectstatus/model.go @@ -0,0 +1,292 @@ +// Package projectstatus owns bounded, read-only classification of a +// materialized Proofkit project and its single next-action projection. +package projectstatus + +import ( + "encoding/json" + "fmt" + + "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/repositorytransaction" +) + +const ( + SchemaVersion = 1 + StatusKind = "proofkit.project-status" + NextKind = "proofkit.project-next-action" + + MaximumFileBytes = repositorytransaction.MaximumFileBytes + MaximumAggregateBytes = repositorytransaction.MaximumAggregateBytes + MaximumOutputBytes = 32 << 10 + MaximumTextBytes = 4 << 10 + MaximumTextLines = 16 + MaximumIssueCodes = 16 +) + +type ProjectState string + +const ( + StateUninitialized ProjectState = "uninitialized" + StateRecoveryRequired ProjectState = "recovery_required" + StateBlocked ProjectState = "blocked" + StateStale ProjectState = "stale" + StateVerificationRequired ProjectState = "verification_required" +) + +type TransactionState string + +const ( + TransactionClean TransactionState = "clean" + TransactionRecoverable TransactionState = "recoverable" + TransactionInvalid TransactionState = "invalid" +) + +type ManifestState string + +const ( + ManifestAbsent ManifestState = "absent" + ManifestInvalid ManifestState = "invalid" + ManifestAdmitted ManifestState = "admitted" +) + +type ChildState string + +const ( + ChildMissing ChildState = "missing" + ChildDigestMismatch ChildState = "digest_mismatch" + ChildInvalid ChildState = "invalid" + ChildAdmitted ChildState = "admitted" +) + +type ClosureState string + +const ( + ClosureNotEvaluated ClosureState = "not_evaluated" + ClosureInvalid ClosureState = "invalid" + ClosureAdmitted ClosureState = "admitted" +) + +const ( + IssueTransactionInvalid = "transaction_control_invalid" + IssueTransactionRecoveryRequired = "transaction_recovery_required" + IssueManifestMissing = "project_manifest_missing" + IssueManifestInvalid = "project_manifest_invalid" + IssueChildMissing = "project_record_missing" + IssueChildDigestMismatch = "project_record_digest_mismatch" + IssueChildInvalid = "project_record_invalid" + IssueClosureInvalid = "project_cross_record_closure_invalid" +) + +const ( + ActionRepairControlState = "repair_control_state" + ActionChooseRecovery = "choose_recovery" + ActionChooseAdoptionMode = "choose_adoption_mode" + ActionRepairProjectRecords = "repair_project_records" + ActionRematerializeProject = "rematerialize_project" + ActionRunRepositoryVerification = "run_repository_verification" +) + +var boundaryNonClaims = []string{ + "Project next actions are derived non-executable guidance; repository owners retain execution and policy authority.", + "Project status does not approve merge, release, rollout, deployment, or production readiness.", + "Project status does not execute native witnesses or verify receipt trust, currentness, or scope.", +} + +type transactionObservation struct { + Epoch string + State TransactionState + TransactionID string +} + +type manifestObservation struct { + ContentDigest string + ManifestID string + State ManifestState +} + +type childObservation struct { + ArtifactKind string + ExpectedDigest string + ObservedDigest string + State ChildState +} + +type inspectionSnapshot struct { + Children []childObservation + ClosureState ClosureState + Manifest manifestObservation + ProjectID string + Transaction transactionObservation +} + +type NextAction struct { + ActionClass string + ActionID string + CommandRoute []string + ContextRef string + Executable bool + RequiredDecision string +} + +type Status struct { + IssueCodes []string + ManifestID string + NextAction NextAction + ProjectID string + ProjectState ProjectState + SnapshotID string + StatusID string +} + +type Next struct { + Action NextAction + IssueCodes []string + PacketID string + ProjectState ProjectState + SnapshotID string + StatusRef string +} + +func (snapshot inspectionSnapshot) identityValue() map[string]any { + children := make([]any, 0, len(snapshot.Children)) + for _, child := range snapshot.Children { + children = append(children, map[string]any{ + "artifactKind": child.ArtifactKind, + "expectedDigest": child.ExpectedDigest, + "observedDigest": nullable(child.ObservedDigest), + "state": string(child.State), + }) + } + project := map[string]any{"state": "unknown"} + if snapshot.ProjectID != "" { + project = map[string]any{"projectId": snapshot.ProjectID, "state": "admitted"} + } + return map[string]any{ + "children": children, + "closureState": string(snapshot.ClosureState), + "manifest": map[string]any{ + "contentDigest": nullable(snapshot.Manifest.ContentDigest), + "manifestId": nullable(snapshot.Manifest.ManifestID), + "state": string(snapshot.Manifest.State), + }, + "project": project, + "schemaVersion": json.Number("1"), + "transaction": map[string]any{ + "epoch": snapshot.Transaction.Epoch, + "state": string(snapshot.Transaction.State), + "transactionId": nullable(snapshot.Transaction.TransactionID), + }, + } +} + +func validateSnapshot(snapshot inspectionSnapshot) error { + if _, err := admit.SHA256Ref(snapshot.Transaction.Epoch, "project status transaction epoch"); err != nil { + return err + } + switch snapshot.Transaction.State { + case TransactionClean, TransactionInvalid: + if snapshot.Transaction.TransactionID != "" { + return fmt.Errorf("project status transaction identity is invalid for its state") + } + case TransactionRecoverable: + if _, err := admit.SHA256Ref(snapshot.Transaction.TransactionID, "project status recoverable transaction"); err != nil { + return err + } + default: + return fmt.Errorf("project status transaction state is invalid") + } + if snapshot.Transaction.State != TransactionClean { + if snapshot.Manifest.State != ManifestAbsent || snapshot.ProjectID != "" || len(snapshot.Children) != 0 || snapshot.ClosureState != ClosureNotEvaluated { + return fmt.Errorf("project status transaction-first snapshot contains later observations") + } + return nil + } + switch snapshot.Manifest.State { + case ManifestAbsent: + if snapshot.Manifest.ContentDigest != "" || snapshot.Manifest.ManifestID != "" || snapshot.ProjectID != "" || len(snapshot.Children) != 0 || snapshot.ClosureState != ClosureNotEvaluated { + return fmt.Errorf("project status absent manifest observation is inconsistent") + } + case ManifestInvalid: + if snapshot.Manifest.ContentDigest != "" { + if _, err := admit.SHA256Ref(snapshot.Manifest.ContentDigest, "project status invalid manifest digest"); err != nil { + return err + } + } + if snapshot.Manifest.ManifestID != "" || snapshot.ProjectID != "" || len(snapshot.Children) != 0 || snapshot.ClosureState != ClosureNotEvaluated { + return fmt.Errorf("project status invalid manifest observation is inconsistent") + } + case ManifestAdmitted: + if _, err := admit.SHA256Ref(snapshot.Manifest.ContentDigest, "project status manifest content digest"); err != nil { + return err + } + if _, err := admit.SHA256Ref(snapshot.Manifest.ManifestID, "project status manifest identity"); err != nil { + return err + } + if _, err := admit.RuleID(snapshot.ProjectID, "project status project identity"); err != nil { + return err + } + if len(snapshot.Children) < 3 { + return fmt.Errorf("project status admitted manifest must observe every routed child") + } + allAdmitted := true + for _, child := range snapshot.Children { + if _, err := admit.RuleID(child.ArtifactKind, "project status child artifact kind"); err != nil { + return err + } + if _, err := admit.SHA256Ref(child.ExpectedDigest, "project status child expected digest"); err != nil { + return err + } + switch child.State { + case ChildMissing: + if child.ObservedDigest != "" { + return fmt.Errorf("project status missing child has an observed digest") + } + case ChildDigestMismatch: + if _, err := admit.SHA256Ref(child.ObservedDigest, "project status child observed digest"); err != nil { + return err + } + if child.ObservedDigest == child.ExpectedDigest { + return fmt.Errorf("project status mismatched child digests are equal") + } + case ChildInvalid: + if child.ObservedDigest != "" { + if _, err := admit.SHA256Ref(child.ObservedDigest, "project status invalid child digest"); err != nil { + return err + } + if child.ObservedDigest != child.ExpectedDigest { + return fmt.Errorf("project status invalid child bypassed digest currentness") + } + } + case ChildAdmitted: + if child.ObservedDigest != child.ExpectedDigest { + return fmt.Errorf("project status admitted child digest does not match its route") + } + default: + return fmt.Errorf("project status child state is invalid") + } + allAdmitted = allAdmitted && child.State == ChildAdmitted + } + if allAdmitted { + if snapshot.ClosureState != ClosureAdmitted && snapshot.ClosureState != ClosureInvalid { + return fmt.Errorf("project status closure state was not evaluated") + } + } else if snapshot.ClosureState != ClosureNotEvaluated { + return fmt.Errorf("project status closure was evaluated before child admission completed") + } + default: + return fmt.Errorf("project status manifest state is invalid") + } + return nil +} + +func nullable(value string) any { + if value == "" { + return nil + } + return value +} + +func snapshotID(snapshot inspectionSnapshot) (string, error) { + return digest.StableJSONSHA256Ref(snapshot.identityValue()) +} diff --git a/internal/command/projectstatus/output.go b/internal/command/projectstatus/output.go new file mode 100644 index 0000000..b186649 --- /dev/null +++ b/internal/command/projectstatus/output.go @@ -0,0 +1,61 @@ +package projectstatus + +import ( + "encoding/json" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" +) + +func (status Status) JSONValue() map[string]any { + value := status.identityValue() + value["statusId"] = status.StatusID + return value +} + +func (status Status) identityValue() map[string]any { + return map[string]any{ + "issueCodes": admit.StringSliceToAny(status.IssueCodes), + "manifestId": nullable(status.ManifestID), + "nextAction": status.NextAction.JSONValue(), + "nonClaims": admit.StringSliceToAny(boundaryNonClaims), + "projectId": nullable(status.ProjectID), + "projectState": string(status.ProjectState), + "reportKind": StatusKind, + "schemaVersion": json.Number("1"), + "snapshotId": status.SnapshotID, + } +} + +func (next Next) JSONValue() map[string]any { + value := next.identityValue() + value["packetId"] = next.PacketID + return value +} + +func (next Next) identityValue() map[string]any { + return map[string]any{ + "action": next.Action.JSONValue(), + "issueCodes": admit.StringSliceToAny(next.IssueCodes), + "nonClaims": admit.StringSliceToAny(boundaryNonClaims), + "packetKind": NextKind, + "projectState": string(next.ProjectState), + "schemaVersion": json.Number("1"), + "snapshotId": next.SnapshotID, + "statusRef": next.StatusRef, + } +} + +func (action NextAction) JSONValue() map[string]any { + route := make([]any, len(action.CommandRoute)) + for index, token := range action.CommandRoute { + route[index] = token + } + return map[string]any{ + "actionClass": action.ActionClass, + "actionId": action.ActionID, + "commandRoute": route, + "contextRef": nullable(action.ContextRef), + "executable": action.Executable, + "requiredDecision": nullable(action.RequiredDecision), + } +} diff --git a/internal/command/projectstatus/output_admission.go b/internal/command/projectstatus/output_admission.go new file mode 100644 index 0000000..ca2f44f --- /dev/null +++ b/internal/command/projectstatus/output_admission.go @@ -0,0 +1,321 @@ +package projectstatus + +import ( + "fmt" + "reflect" + + "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" +) + +var projectStateSet = map[string]struct{}{ + string(StateUninitialized): {}, + string(StateRecoveryRequired): {}, + string(StateBlocked): {}, + string(StateStale): {}, + string(StateVerificationRequired): {}, +} + +var issueCodeSet = map[string]struct{}{ + IssueTransactionInvalid: {}, + IssueTransactionRecoveryRequired: {}, + IssueManifestMissing: {}, + IssueManifestInvalid: {}, + IssueChildMissing: {}, + IssueChildDigestMismatch: {}, + IssueChildInvalid: {}, + IssueClosureInvalid: {}, +} + +func AdmitStatusOutput(raw any) (Status, error) { + record, ok := raw.(map[string]any) + if !ok { + return Status{}, fmt.Errorf("project status output must be an object") + } + keys := []string{"issueCodes", "manifestId", "nextAction", "nonClaims", "projectId", "projectState", "reportKind", "schemaVersion", "snapshotId", "statusId"} + if err := admit.KnownKeys(record, keys, "project status output"); err != nil { + return Status{}, err + } + if !admit.JSONNumberEquals(record["schemaVersion"], SchemaVersion) || record["reportKind"] != StatusKind { + return Status{}, fmt.Errorf("project status output identity is invalid") + } + statusID, err := admit.SHA256Ref(record["statusId"], "project status statusId") + if err != nil { + return Status{}, err + } + snapshotID, err := admit.SHA256Ref(record["snapshotId"], "project status snapshotId") + if err != nil { + return Status{}, err + } + stateText, err := admit.Enum(record["projectState"], projectStateSet, "project status projectState") + if err != nil { + return Status{}, err + } + projectID, err := nullableRuleID(record["projectId"], "project status projectId") + if err != nil { + return Status{}, err + } + manifestID, err := nullableSHA256Ref(record["manifestId"], "project status manifestId") + if err != nil { + return Status{}, err + } + issues, err := admitIssueCodes(record["issueCodes"], "project status issueCodes") + if err != nil { + return Status{}, err + } + if err := admitBoundaryNonClaims(record["nonClaims"]); err != nil { + return Status{}, err + } + action, err := admitAction(record["nextAction"]) + if err != nil { + return Status{}, err + } + status := Status{ + IssueCodes: issues, ManifestID: manifestID, NextAction: action, + ProjectID: projectID, ProjectState: ProjectState(stateText), + SnapshotID: snapshotID, StatusID: statusID, + } + if err := validateStatusRelations(status); err != nil { + return Status{}, err + } + wantID, err := digest.StableJSONSHA256Ref(status.identityValue()) + if err != nil || wantID != status.StatusID { + return Status{}, fmt.Errorf("project status identity does not match its content") + } + if err := validateOutputBytes(status.JSONValue()); err != nil { + return Status{}, err + } + return status, nil +} + +func AdmitNextOutput(raw any) (Next, error) { + record, ok := raw.(map[string]any) + if !ok { + return Next{}, fmt.Errorf("project next-action output must be an object") + } + keys := []string{"action", "issueCodes", "nonClaims", "packetId", "packetKind", "projectState", "schemaVersion", "snapshotId", "statusRef"} + if err := admit.KnownKeys(record, keys, "project next-action output"); err != nil { + return Next{}, err + } + if !admit.JSONNumberEquals(record["schemaVersion"], SchemaVersion) || record["packetKind"] != NextKind { + return Next{}, fmt.Errorf("project next-action output identity is invalid") + } + packetID, err := admit.SHA256Ref(record["packetId"], "project next-action packetId") + if err != nil { + return Next{}, err + } + snapshotID, err := admit.SHA256Ref(record["snapshotId"], "project next-action snapshotId") + if err != nil { + return Next{}, err + } + statusRef, err := admit.SHA256Ref(record["statusRef"], "project next-action statusRef") + if err != nil { + return Next{}, err + } + stateText, err := admit.Enum(record["projectState"], projectStateSet, "project next-action projectState") + if err != nil { + return Next{}, err + } + issues, err := admitIssueCodes(record["issueCodes"], "project next-action issueCodes") + if err != nil { + return Next{}, err + } + if err := admitBoundaryNonClaims(record["nonClaims"]); err != nil { + return Next{}, err + } + action, err := admitAction(record["action"]) + if err != nil { + return Next{}, err + } + next := Next{ + Action: action, IssueCodes: issues, PacketID: packetID, + ProjectState: ProjectState(stateText), SnapshotID: snapshotID, StatusRef: statusRef, + } + if err := validateStateAction(next.ProjectState, next.Action, next.IssueCodes); err != nil { + return Next{}, err + } + wantID, err := digest.StableJSONSHA256Ref(next.identityValue()) + if err != nil || wantID != next.PacketID { + return Next{}, fmt.Errorf("project next-action identity does not match its content") + } + if err := validateOutputBytes(next.JSONValue()); err != nil { + return Next{}, err + } + return next, nil +} + +func admitAction(raw any) (NextAction, error) { + record, ok := raw.(map[string]any) + if !ok { + return NextAction{}, fmt.Errorf("project next action must be an object") + } + keys := []string{"actionClass", "actionId", "commandRoute", "contextRef", "executable", "requiredDecision"} + if err := admit.KnownKeys(record, keys, "project next action"); err != nil { + return NextAction{}, err + } + actionClass, err := admit.RuleID(record["actionClass"], "project next action actionClass") + if err != nil { + return NextAction{}, err + } + actionID, err := admit.RuleID(record["actionId"], "project next action actionId") + if err != nil { + return NextAction{}, err + } + if actionID != "proofkit.project-status.action."+actionClass { + return NextAction{}, fmt.Errorf("project next action identity is invalid") + } + executable, ok := record["executable"].(bool) + if !ok || executable { + return NextAction{}, fmt.Errorf("project next action must be non-executable") + } + route, err := admitRoute(record["commandRoute"]) + if err != nil { + return NextAction{}, err + } + contextRef, err := nullableSHA256Ref(record["contextRef"], "project next action contextRef") + if err != nil { + return NextAction{}, err + } + requiredDecision, err := nullableRuleID(record["requiredDecision"], "project next action requiredDecision") + if err != nil { + return NextAction{}, err + } + return NextAction{ + ActionClass: actionClass, ActionID: actionID, CommandRoute: route, + ContextRef: contextRef, Executable: false, RequiredDecision: requiredDecision, + }, nil +} + +func admitRoute(raw any) ([]string, error) { + values, ok := raw.([]any) + if !ok || len(values) > 4 { + return nil, fmt.Errorf("project next action commandRoute is invalid") + } + result := make([]string, 0, len(values)) + for _, value := range values { + token, err := admit.RuleID(value, "project next action commandRoute token") + if err != nil { + return nil, err + } + result = append(result, token) + } + return result, nil +} + +func validateStatusRelations(status Status) error { + if status.ProjectID == "" && status.ManifestID != "" { + return fmt.Errorf("project status manifest requires project identity") + } + if status.ProjectID != "" && status.ManifestID == "" { + return fmt.Errorf("project status project identity requires manifest identity") + } + if err := validateStateAction(status.ProjectState, status.NextAction, status.IssueCodes); err != nil { + return err + } + switch status.ProjectState { + case StateUninitialized, StateRecoveryRequired: + if status.ProjectID != "" || status.ManifestID != "" { + return fmt.Errorf("project status identity is inconsistent with its state") + } + case StateBlocked: + manifestOnly := reflect.DeepEqual(status.IssueCodes, []string{IssueManifestInvalid}) || reflect.DeepEqual(status.IssueCodes, []string{IssueTransactionInvalid}) + if manifestOnly != (status.ProjectID == "" && status.ManifestID == "") { + return fmt.Errorf("project blocked identity is inconsistent with its issues") + } + case StateStale, StateVerificationRequired: + if status.ProjectID == "" || status.ManifestID == "" || status.NextAction.ContextRef != status.ManifestID { + return fmt.Errorf("project status manifest context is inconsistent with its state") + } + } + return nil +} + +func validateStateAction(state ProjectState, action NextAction, issues []string) error { + want, err := canonicalAction(state, issues, action.ContextRef) + if err != nil { + return err + } + if !reflect.DeepEqual(action, want) { + return fmt.Errorf("project state and next action do not match") + } + return nil +} + +func admitIssueCodes(raw any, context string) ([]string, error) { + values, ok := raw.([]any) + if !ok || len(values) > MaximumIssueCodes { + return nil, fmt.Errorf("%s are invalid", context) + } + result := make([]string, 0, len(values)) + previous := "" + for _, rawValue := range values { + value, err := admit.RuleID(rawValue, context) + if err != nil { + return nil, err + } + if _, ok := issueCodeSet[value]; !ok || previous != "" && previous >= value { + return nil, fmt.Errorf("%s must be sorted, unique, and supported", context) + } + previous = value + result = append(result, value) + } + return result, nil +} + +func allIssuesIn(values []string, admitted ...string) bool { + set := map[string]struct{}{} + for _, value := range admitted { + set[value] = struct{}{} + } + for _, value := range values { + if _, ok := set[value]; !ok { + return false + } + } + return true +} + +func containsAnyIssue(values []string, candidates ...string) bool { + for _, value := range values { + for _, candidate := range candidates { + if value == candidate { + return true + } + } + } + return false +} + +func admitBoundaryNonClaims(raw any) error { + values, err := admit.PreserveSortedTextArray(raw, "project status nonClaims", false) + if err != nil || !reflect.DeepEqual(values, boundaryNonClaims) { + return fmt.Errorf("project status nonClaims are invalid") + } + return nil +} + +func nullableRuleID(raw any, context string) (string, error) { + if raw == nil { + return "", nil + } + return admit.RuleID(raw, context) +} + +func nullableSHA256Ref(raw any, context string) (string, error) { + if raw == nil { + return "", nil + } + return admit.SHA256Ref(raw, context) +} + +func validateOutputBytes(value map[string]any) error { + content, err := stablejson.Marshal(value) + if err != nil { + return err + } + if len(content) > MaximumOutputBytes { + return fmt.Errorf("project status output exceeds its byte limit") + } + return nil +} diff --git a/internal/command/projectstatus/projectstatus_test.go b/internal/command/projectstatus/projectstatus_test.go new file mode 100644 index 0000000..a66fdee --- /dev/null +++ b/internal/command/projectstatus/projectstatus_test.go @@ -0,0 +1,364 @@ +package projectstatus + +import ( + "reflect" + "strings" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" + "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" +) + +func TestEvaluateTotalStateActionTable(t *testing.T) { + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.060322937390720972859694282639537757818712419857577877895991208334865304069513") + tests := []struct { + name string + snapshot inspectionSnapshot + wantState ProjectState + wantAction string + wantIssues []string + }{ + {name: "invalid transaction", snapshot: transactionSnapshot(TransactionInvalid, ""), wantState: StateBlocked, wantAction: ActionRepairControlState, wantIssues: []string{IssueTransactionInvalid}}, + {name: "recoverable transaction", snapshot: transactionSnapshot(TransactionRecoverable, digest.SHA256TextRef("transaction")), wantState: StateRecoveryRequired, wantAction: ActionChooseRecovery, wantIssues: []string{IssueTransactionRecoveryRequired}}, + {name: "missing manifest", snapshot: transactionSnapshot(TransactionClean, ""), wantState: StateUninitialized, wantAction: ActionChooseAdoptionMode, wantIssues: []string{IssueManifestMissing}}, + {name: "invalid manifest", snapshot: invalidManifestSnapshot(), wantState: StateBlocked, wantAction: ActionRepairProjectRecords, wantIssues: []string{IssueManifestInvalid}}, + {name: "missing child", snapshot: childStateSnapshot(ChildMissing), wantState: StateStale, wantAction: ActionRematerializeProject, wantIssues: []string{IssueChildMissing}}, + {name: "mismatched child", snapshot: childStateSnapshot(ChildDigestMismatch), wantState: StateStale, wantAction: ActionRematerializeProject, wantIssues: []string{IssueChildDigestMismatch}}, + {name: "invalid child dominates missing", snapshot: mixedInvalidSnapshot(), wantState: StateBlocked, wantAction: ActionRepairProjectRecords, wantIssues: []string{IssueChildInvalid, IssueChildMissing}}, + {name: "invalid closure", snapshot: closureSnapshot(ClosureInvalid), wantState: StateBlocked, wantAction: ActionRepairProjectRecords, wantIssues: []string{IssueClosureInvalid}}, + {name: "admitted project", snapshot: closureSnapshot(ClosureAdmitted), wantState: StateVerificationRequired, wantAction: ActionRunRepositoryVerification, wantIssues: []string{}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + status, err := evaluate(test.snapshot) + if err != nil { + t.Fatalf("evaluate() error = %v", err) + } + if status.ProjectState != test.wantState || status.NextAction.ActionClass != test.wantAction || !reflect.DeepEqual(status.IssueCodes, test.wantIssues) { + t.Fatalf("evaluate() = state %q action %q issues %v", status.ProjectState, status.NextAction.ActionClass, status.IssueCodes) + } + if status.NextAction.Executable { + t.Fatal("evaluate() emitted executable next action") + } + next, err := NextFromStatus(status) + if err != nil { + t.Fatalf("NextFromStatus() error = %v", err) + } + if next.StatusRef != status.StatusID || !reflect.DeepEqual(next.Action, status.NextAction) { + t.Fatal("NextFromStatus() lost the status-owned action") + } + }) + } +} + +func TestSnapshotIdentityBindsEveryDecisionOperand(t *testing.T) { + base := closureSnapshot(ClosureAdmitted) + baseID, err := snapshotID(base) + if err != nil { + t.Fatal(err) + } + variants := map[string]inspectionSnapshot{ + "transaction epoch": func() inspectionSnapshot { + value := base + value.Transaction.Epoch = digest.SHA256TextRef("other epoch") + return value + }(), + "transaction state": func() inspectionSnapshot { + value := base + value.Transaction.State = TransactionInvalid + return value + }(), + "transaction identity": func() inspectionSnapshot { + value := base + value.Transaction.TransactionID = digest.SHA256TextRef("transaction") + return value + }(), + "manifest digest": func() inspectionSnapshot { + value := base + value.Manifest.ContentDigest = digest.SHA256TextRef("other manifest") + return value + }(), + "manifest identity": func() inspectionSnapshot { + value := base + value.Manifest.ManifestID = digest.SHA256TextRef("other manifest identity") + return value + }(), + "manifest state": func() inspectionSnapshot { + value := base + value.Manifest.State = ManifestInvalid + return value + }(), + "project id": func() inspectionSnapshot { value := base; value.ProjectID = "project.other"; return value }(), + "child kind": func() inspectionSnapshot { + value := base + value.Children = cloneChildren(base.Children) + value.Children[0].ArtifactKind = "other_source" + return value + }(), + "child expected digest": func() inspectionSnapshot { + value := base + value.Children = cloneChildren(base.Children) + value.Children[0].ExpectedDigest = digest.SHA256TextRef("other child") + value.Children[0].ObservedDigest = value.Children[0].ExpectedDigest + return value + }(), + "child observed digest": func() inspectionSnapshot { + value := base + value.Children = cloneChildren(base.Children) + value.Children[0].ObservedDigest = digest.SHA256TextRef("other observed child") + return value + }(), + "child state": func() inspectionSnapshot { + value := base + value.Children = cloneChildren(base.Children) + value.Children[0].State = ChildMissing + return value + }(), + "child cardinality": func() inspectionSnapshot { + value := base + value.Children = cloneChildren(base.Children[1:]) + return value + }(), + "child order": func() inspectionSnapshot { + value := base + value.Children = cloneChildren(base.Children) + value.Children[0], value.Children[1] = value.Children[1], value.Children[0] + return value + }(), + "closure": closureSnapshot(ClosureInvalid), + } + for name, variant := range variants { + t.Run(name, func(t *testing.T) { + variantID, err := snapshotID(variant) + if err != nil { + t.Fatal(err) + } + if variantID == baseID { + t.Fatal("decision operand did not change snapshot identity") + } + }) + } +} + +func TestOutputAdmissionRejectsReidentifiedStateActionMismatch(t *testing.T) { + status, err := evaluate(closureSnapshot(ClosureAdmitted)) + if err != nil { + t.Fatal(err) + } + status.NextAction.ActionClass = ActionRepairProjectRecords + status.NextAction.ActionID = "proofkit.project-status.action." + status.NextAction.ActionClass + status.NextAction.ContextRef = "" + status.StatusID, err = digest.StableJSONSHA256Ref(status.identityValue()) + if err != nil { + t.Fatal(err) + } + if _, err := AdmitStatusOutput(status.JSONValue()); err == nil { + t.Fatalf("AdmitStatusOutput() error = %v", err) + } +} + +func TestOutputAdmissionRejectsUnreachableClosureCombination(t *testing.T) { + status, err := evaluate(closureSnapshot(ClosureInvalid)) + if err != nil { + t.Fatal(err) + } + status.IssueCodes = []string{IssueClosureInvalid, IssueChildMissing} + status.StatusID, err = digest.StableJSONSHA256Ref(status.identityValue()) + if err != nil { + t.Fatal(err) + } + if _, err := AdmitStatusOutput(status.JSONValue()); err == nil { + t.Fatal("AdmitStatusOutput() admitted closure evaluation before child admission") + } +} + +func TestSnapshotValidationRejectsPrematureClosureAndDigestDrift(t *testing.T) { + premature := childStateSnapshot(ChildMissing) + premature.ClosureState = ClosureAdmitted + if _, err := evaluate(premature); err == nil || !strings.Contains(err.Error(), "before child admission") { + t.Fatalf("evaluate() error = %v", err) + } + drifted := closureSnapshot(ClosureAdmitted) + drifted.Children[0].ObservedDigest = digest.SHA256TextRef("drifted") + if _, err := evaluate(drifted); err == nil || !strings.Contains(err.Error(), "does not match") { + t.Fatalf("evaluate() error = %v", err) + } +} + +func TestTextProjectionIsBoundedAndSemanticallyDerived(t *testing.T) { + for _, snapshot := range []inspectionSnapshot{ + transactionSnapshot(TransactionInvalid, ""), + transactionSnapshot(TransactionRecoverable, digest.SHA256TextRef("transaction")), + transactionSnapshot(TransactionClean, ""), + invalidManifestSnapshot(), + childStateSnapshot(ChildMissing), + closureSnapshot(ClosureAdmitted), + } { + status, err := evaluate(snapshot) + if err != nil { + t.Fatal(err) + } + statusLines, err := StatusText(status) + if err != nil { + t.Fatal(err) + } + wantStatusLines := []TextLine{ + {Label: "Project status"}, + {Label: "State", Value: string(status.ProjectState)}, + {Label: "snapshot", Value: status.SnapshotID}, + {Label: "Next", Value: status.NextAction.ActionClass}, + } + if len(status.IssueCodes) > 0 { + wantStatusLines = append(wantStatusLines, TextLine{Label: "Issues", Value: strings.Join(status.IssueCodes, ", ")}) + } + if !reflect.DeepEqual(statusLines, wantStatusLines) { + t.Fatalf("StatusText() = %#v, want %#v", statusLines, wantStatusLines) + } + statusText, err := RenderText(statusLines) + if err != nil { + t.Fatal(err) + } + if len(statusText) > MaximumTextBytes || strings.Count(statusText, "\n") > MaximumTextLines || !strings.Contains(statusText, string(status.ProjectState)) { + t.Fatalf("RenderText(status) = %q", statusText) + } + + next, err := NextFromStatus(status) + if err != nil { + t.Fatal(err) + } + nextLines, err := NextText(next) + if err != nil { + t.Fatal(err) + } + wantNextLines := []TextLine{ + {Label: "Project next action"}, + {Label: "State", Value: string(next.ProjectState)}, + {Label: "Action", Value: next.Action.ActionClass}, + {Label: "Executable", Value: "false"}, + } + if len(next.Action.CommandRoute) > 0 { + wantNextLines = append(wantNextLines, TextLine{Label: "Route", Value: strings.Join(next.Action.CommandRoute, " ")}) + } + if next.Action.ContextRef != "" { + wantNextLines = append(wantNextLines, TextLine{Label: "Context", Value: next.Action.ContextRef}) + } + if next.Action.RequiredDecision != "" { + wantNextLines = append(wantNextLines, TextLine{Label: "Decision", Value: next.Action.RequiredDecision}) + } + if len(next.IssueCodes) > 0 { + wantNextLines = append(wantNextLines, TextLine{Label: "Issues", Value: strings.Join(next.IssueCodes, ", ")}) + } + if !reflect.DeepEqual(nextLines, wantNextLines) { + t.Fatalf("NextText() = %#v, want %#v", nextLines, wantNextLines) + } + nextText, err := RenderText(nextLines) + if err != nil { + t.Fatal(err) + } + if len(nextText) > MaximumTextBytes || strings.Count(nextText, "\n") > MaximumTextLines || !strings.Contains(nextText, next.Action.ActionClass) || !strings.Contains(nextText, next.Action.ContextRef) { + t.Fatalf("RenderText(next) = %q", nextText) + } + } +} + +func TestNextTextEquivalenceIntentionallyExcludesJSONIdentity(t *testing.T) { + firstSnapshot := transactionSnapshot(TransactionClean, "") + secondSnapshot := transactionSnapshot(TransactionClean, "") + secondSnapshot.Transaction.Epoch = digest.SHA256TextRef("different clean control epoch") + firstStatus, err := evaluate(firstSnapshot) + if err != nil { + t.Fatal(err) + } + secondStatus, err := evaluate(secondSnapshot) + if err != nil { + t.Fatal(err) + } + firstNext, err := NextFromStatus(firstStatus) + if err != nil { + t.Fatal(err) + } + secondNext, err := NextFromStatus(secondStatus) + if err != nil { + t.Fatal(err) + } + if firstNext.PacketID == secondNext.PacketID || firstNext.SnapshotID == secondNext.SnapshotID || firstNext.StatusRef == secondNext.StatusRef { + t.Fatal("distinct snapshots did not produce distinct JSON identity coordinates") + } + firstLines, err := NextText(firstNext) + if err != nil { + t.Fatal(err) + } + secondLines, err := NextText(secondNext) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(firstLines, secondLines) { + t.Fatalf("JSON-only identity changed text projection: first=%#v second=%#v", firstLines, secondLines) + } +} + +func transactionSnapshot(state TransactionState, transactionID string) inspectionSnapshot { + return inspectionSnapshot{ + ClosureState: ClosureNotEvaluated, + Manifest: manifestObservation{State: ManifestAbsent}, + Transaction: transactionObservation{ + Epoch: digest.SHA256TextRef("control epoch"), State: state, TransactionID: transactionID, + }, + } +} + +func invalidManifestSnapshot() inspectionSnapshot { + snapshot := transactionSnapshot(TransactionClean, "") + snapshot.Manifest = manifestObservation{State: ManifestInvalid, ContentDigest: digest.SHA256TextRef("invalid manifest")} + return snapshot +} + +func closureSnapshot(state ClosureState) inspectionSnapshot { + manifestDigest := digest.SHA256TextRef("manifest") + return inspectionSnapshot{ + Children: []childObservation{ + admittedChild("requirement_source", "source"), + admittedChild("requirement_proof_binding", "binding"), + admittedChild("test_evidence_inventory", "inventory"), + }, + ClosureState: state, + Manifest: manifestObservation{ + ContentDigest: manifestDigest, ManifestID: digest.SHA256TextRef("manifest identity"), State: ManifestAdmitted, + }, + ProjectID: "project.test", + Transaction: transactionObservation{Epoch: digest.SHA256TextRef("control epoch"), State: TransactionClean}, + } +} + +func childStateSnapshot(state ChildState) inspectionSnapshot { + snapshot := closureSnapshot(ClosureAdmitted) + snapshot.ClosureState = ClosureNotEvaluated + snapshot.Children = cloneChildren(snapshot.Children) + child := &snapshot.Children[0] + child.State = state + switch state { + case ChildMissing: + child.ObservedDigest = "" + case ChildDigestMismatch: + child.ObservedDigest = digest.SHA256TextRef("changed source") + case ChildInvalid: + child.ObservedDigest = child.ExpectedDigest + } + return snapshot +} + +func mixedInvalidSnapshot() inspectionSnapshot { + snapshot := childStateSnapshot(ChildInvalid) + snapshot.Children[1].State = ChildMissing + snapshot.Children[1].ObservedDigest = "" + return snapshot +} + +func admittedChild(kind, content string) childObservation { + id := digest.SHA256TextRef(content) + return childObservation{ArtifactKind: kind, ExpectedDigest: id, ObservedDigest: id, State: ChildAdmitted} +} + +func cloneChildren(values []childObservation) []childObservation { + return append([]childObservation{}, values...) +} diff --git a/internal/command/projectstatus/text.go b/internal/command/projectstatus/text.go new file mode 100644 index 0000000..1157bad --- /dev/null +++ b/internal/command/projectstatus/text.go @@ -0,0 +1,77 @@ +package projectstatus + +import ( + "fmt" + "strings" +) + +type TextLine struct { + Label string + Value string +} + +func StatusText(status Status) ([]TextLine, error) { + admitted, err := AdmitStatusOutput(status.JSONValue()) + if err != nil { + return nil, err + } + status = admitted + lines := []TextLine{ + {Label: "Project status"}, + {Label: "State", Value: string(status.ProjectState)}, + {Label: "snapshot", Value: status.SnapshotID}, + {Label: "Next", Value: status.NextAction.ActionClass}, + } + if len(status.IssueCodes) > 0 { + lines = append(lines, TextLine{Label: "Issues", Value: strings.Join(status.IssueCodes, ", ")}) + } + return lines, nil +} + +func NextText(next Next) ([]TextLine, error) { + admitted, err := AdmitNextOutput(next.JSONValue()) + if err != nil { + return nil, err + } + next = admitted + lines := []TextLine{ + {Label: "Project next action"}, + {Label: "State", Value: string(next.ProjectState)}, + {Label: "Action", Value: next.Action.ActionClass}, + {Label: "Executable", Value: "false"}, + } + if len(next.Action.CommandRoute) > 0 { + lines = append(lines, TextLine{Label: "Route", Value: strings.Join(next.Action.CommandRoute, " ")}) + } + if next.Action.ContextRef != "" { + lines = append(lines, TextLine{Label: "Context", Value: next.Action.ContextRef}) + } + if next.Action.RequiredDecision != "" { + lines = append(lines, TextLine{Label: "Decision", Value: next.Action.RequiredDecision}) + } + if len(next.IssueCodes) > 0 { + lines = append(lines, TextLine{Label: "Issues", Value: strings.Join(next.IssueCodes, ", ")}) + } + return lines, nil +} + +func RenderText(lines []TextLine) (string, error) { + if len(lines) == 0 || len(lines) > MaximumTextLines { + return "", fmt.Errorf("project status text exceeds its line limit") + } + plain := make([]string, len(lines)) + for index, line := range lines { + if line.Label == "" || strings.ContainsAny(line.Label, "\r\n") || strings.ContainsAny(line.Value, "\r\n") { + return "", fmt.Errorf("project status text coordinate is invalid") + } + plain[index] = line.Label + if line.Value != "" { + plain[index] += ": " + line.Value + } + } + text := strings.Join(plain, "\n") + "\n" + if len(text) > MaximumTextBytes { + return "", fmt.Errorf("project status text exceeds its byte limit") + } + return text, nil +} diff --git a/internal/command/stackpreset/preset_ids_generated.go b/internal/command/stackpreset/preset_ids_generated.go index 6b16584..32fe5fc 100644 --- a/internal/command/stackpreset/preset_ids_generated.go +++ b/internal/command/stackpreset/preset_ids_generated.go @@ -1,6 +1,6 @@ // Code generated by internal/tools/commandcontractgen; DO NOT EDIT. package stackpreset -const presetContractSourceSHA256 = "ea2fbade9c0651e7742b852a3f11433afee58a5a26d997a2cb00400db777c488" +const presetContractSourceSHA256 = "5306b7223c5c0671871272f9790195fa1064de85c638f276493c110a6c3c51ed" var presetIDs = []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"} diff --git a/internal/kernel/commandroute/route.go b/internal/kernel/commandroute/route.go index 18e5f1d..dbe2b64 100644 --- a/internal/kernel/commandroute/route.go +++ b/internal/kernel/commandroute/route.go @@ -9,11 +9,12 @@ import ( var tokenPattern = regexp.MustCompile(TokenPattern) const ( - MinimumTokens = 1 - MaximumTokens = 4 - Separator = " " - TokenPattern = `^[a-z0-9]+(?:-[a-z0-9]+)*$` - AmbiguityPolicy = "no_route_is_prefix_of_another" + MinimumTokens = 1 + MaximumTokens = 4 + Separator = " " + TokenPattern = `^[a-z0-9]+(?:-[a-z0-9]+)*$` + AmbiguityPolicy = "no_route_is_prefix_of_another" + OmittedRoutePolicy = "command_id" ) func Valid(tokens []string) bool { @@ -32,6 +33,21 @@ func ValidToken(token string) bool { return tokenPattern.MatchString(token) } +// Resolve returns the effective route. A nil route is omitted and therefore +// resolves to the stable command identity; an explicitly empty route is invalid. +func Resolve(commandID string, route []string) ([]string, bool) { + if !ValidToken(commandID) { + return nil, false + } + if route == nil { + return []string{commandID}, true + } + if !Valid(route) { + return nil, false + } + return slices.Clone(route), true +} + func Parse(text string) ([]string, bool) { tokens := strings.Split(text, Separator) if !Valid(tokens) { diff --git a/internal/kernel/commandroute/route_test.go b/internal/kernel/commandroute/route_test.go index 42f16b7..6331aa0 100644 --- a/internal/kernel/commandroute/route_test.go +++ b/internal/kernel/commandroute/route_test.go @@ -6,6 +6,9 @@ import ( ) func TestGrammarBoundariesAreExact(t *testing.T) { + if OmittedRoutePolicy != "command_id" { + t.Fatalf("omitted route policy=%q", OmittedRoutePolicy) + } valid := []string{"one", "two", "three", "four"} if !Valid(valid[:MinimumTokens]) || !Valid(valid[:MaximumTokens]) { t.Fatal("exact command-route token bounds were rejected") @@ -46,3 +49,34 @@ func TestPrefixIsStrict(t *testing.T) { t.Fatal("non-prefix route was recognized") } } + +func TestOmittedRoutePolicyUsesStableCommandIdentity(t *testing.T) { + if OmittedRoutePolicy != "command_id" { + t.Fatalf("omitted route policy=%q, want command_id", OmittedRoutePolicy) + } + omitted, ok := Resolve("stable-command", nil) + if !ok || !slices.Equal(omitted, []string{"stable-command"}) { + t.Fatalf("omitted route resolved to %v, ok=%v", omitted, ok) + } + explicitInput := []string{"stable", "route"} + explicit, ok := Resolve("stable-command", explicitInput) + if !ok || !slices.Equal(explicit, explicitInput) { + t.Fatalf("explicit route resolved to %v, ok=%v", explicit, ok) + } + explicit[0] = "changed" + if explicitInput[0] != "stable" { + t.Fatal("resolved route aliases caller-owned input") + } + for _, test := range []struct { + commandID string + route []string + }{ + {commandID: "invalid command", route: nil}, + {commandID: "stable-command", route: []string{}}, + {commandID: "stable-command", route: []string{"Invalid"}}, + } { + if resolved, valid := Resolve(test.commandID, test.route); valid || resolved != nil { + t.Fatalf("Resolve(%q, %v)=%v, %v; want nil, false", test.commandID, test.route, resolved, valid) + } + } +} diff --git a/internal/kernel/repositorytransaction/control_inspection.go b/internal/kernel/repositorytransaction/control_inspection.go new file mode 100644 index 0000000..2d0b3fd --- /dev/null +++ b/internal/kernel/repositorytransaction/control_inspection.go @@ -0,0 +1,186 @@ +package repositorytransaction + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" + "github.com/research-engineering/agentic-proofkit/internal/kernel/rootpath" +) + +var ErrControlStateChanged = errors.New("repository transaction control state changed during inspection") + +const ( + ControlStateClean = "clean" + ControlStateRecoverable = "recoverable" + ControlStateInvalid = "invalid" +) + +// ControlInspection is a read-only projection of the repository transaction +// namespace. TransactionID is present only when recovery identity is known. +type ControlInspection struct { + EpochID string + State string + TransactionID string +} + +// InspectControlState returns a content-bound transaction-control epoch +// without creating, removing, or rewriting repository state. +func InspectControlState(ctx context.Context, rootPath string) (inspection ControlInspection, returnErr error) { + lease, err := OpenInspectionLease(ctx, rootPath) + if err != nil { + return ControlInspection{}, err + } + defer func() { + if closeErr := lease.Close(); closeErr != nil { + inspection = ControlInspection{} + returnErr = fmt.Errorf("close repository transaction inspection: %w", closeErr) + } + }() + return lease.InspectControlState(ctx) +} + +func emptyControlObservationID() (string, error) { + value, err := digest.StableJSONSHA256Ref(map[string]any{ + "controlObservationKind": "proofkit.repository-control-observation", + "entries": []any{}, + "schemaVersion": json.Number("1"), + }) + if err != nil { + return "", fmt.Errorf("derive empty repository transaction control observation: %w", err) + } + return value, nil +} + +func newControlInspection(state, transactionID, observationID string) (ControlInspection, error) { + epochID, err := digest.StableJSONSHA256Ref(map[string]any{ + "controlEpochKind": "proofkit.repository-control-epoch", + "observationId": observationID, + "schemaVersion": json.Number("1"), + "state": state, + "transactionId": nullableText(transactionID), + }) + if err != nil { + return ControlInspection{}, fmt.Errorf("derive repository transaction control epoch: %w", err) + } + return ControlInspection{EpochID: epochID, State: state, TransactionID: transactionID}, nil +} + +func classifyControlState(root *os.Root, rootID string, observation controlObservation) (string, string, error) { + if observation.Invalid { + return ControlStateInvalid, "", nil + } + entries := observation.Entries + if len(entries) == 0 { + return ControlStateClean, "", nil + } + terminal, terminalFound, err := findTerminalControlEntry(entries) + if err != nil { + return ControlStateInvalid, "", nil + } + active := false + for _, entry := range entries { + if entry.Name() == "active" && entry.IsDir() && entry.Type()&os.ModeSymlink == 0 { + active = true + } + } + if !active { + if len(entries) != 1 || !terminalFound { + return ControlStateInvalid, "", nil + } + valid, validationErr := validTerminalControlState(root, terminal) + if operationalErr := controlInspectionOperationalError(validationErr); operationalErr != nil { + return "", "", operationalErr + } + if validationErr != nil || !valid { + return ControlStateInvalid, "", nil + } + return ControlStateClean, "", nil + } + if len(entries) > 2 { + return ControlStateInvalid, "", nil + } + if terminalFound { + valid, validationErr := validTerminalControlState(root, terminal) + if operationalErr := controlInspectionOperationalError(validationErr); operationalErr != nil { + return "", "", operationalErr + } + if validationErr != nil || !valid { + return ControlStateInvalid, "", nil + } + } + if err := validatePrivateDirectory(root, activeDirectory, 0o700); err != nil { + if operationalErr := controlInspectionOperationalError(err); operationalErr != nil { + return "", "", operationalErr + } + return ControlStateInvalid, "", nil + } + plan, err := loadJournal(root) + if err != nil { + if operationalErr := controlInspectionOperationalError(err); operationalErr != nil { + return "", "", operationalErr + } + var admitted bool + plan, admitted, err = loadPreparingJournal(root) + if operationalErr := controlInspectionOperationalError(err); operationalErr != nil { + return "", "", operationalErr + } + if err != nil || !admitted { + return ControlStateInvalid, "", nil + } + } + if plan.RootID != rootID { + return ControlStateInvalid, "", nil + } + if err := validateActiveState(root, plan); err != nil { + if operationalErr := controlInspectionOperationalError(err); operationalErr != nil { + return "", "", operationalErr + } + return ControlStateInvalid, "", nil + } + committed, committedErr := markerExists(root, committedMarker) + rolledBack, rolledBackErr := markerExists(root, rolledBackMarker) + if operationalErr := controlInspectionOperationalError(errors.Join(committedErr, rolledBackErr)); operationalErr != nil { + return "", "", operationalErr + } + if committedErr != nil || rolledBackErr != nil || (committed && rolledBack) { + return ControlStateInvalid, "", nil + } + selected, selectedExists, err := readRecoveryAction(root) + if operationalErr := controlInspectionOperationalError(err); operationalErr != nil { + return "", "", operationalErr + } + if err != nil || (selectedExists && selected.TransactionID != plan.TransactionID) { + return ControlStateInvalid, "", nil + } + return ControlStateRecoverable, plan.TransactionID, nil +} + +func validTerminalControlState(root *os.Root, terminal terminalControlIdentity) (bool, error) { + path := ControlDirectory + "/" + terminal.Entry.Name() + children, err := transactionEntries(root, path) + if err != nil { + return false, err + } + if terminal.Retired && len(children) == 0 { + return true, nil + } + if len(children) != 1 || children[0].Name() != terminalReceiptName { + return false, nil + } + receipt, err := loadTerminalReceipt(root, path) + if err != nil { + return false, err + } + return receipt.TransactionID == terminal.TransactionID && receipt.State == terminal.State, nil +} + +func controlInspectionOperationalError(err error) error { + if errors.Is(err, ErrReadCleanup) || errors.Is(err, rootpath.ErrTraversalCleanup) { + return err + } + return nil +} diff --git a/internal/kernel/repositorytransaction/control_inspection_test.go b/internal/kernel/repositorytransaction/control_inspection_test.go new file mode 100644 index 0000000..4379294 --- /dev/null +++ b/internal/kernel/repositorytransaction/control_inspection_test.go @@ -0,0 +1,783 @@ +package repositorytransaction + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "reflect" + "sort" + "strings" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" + "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" +) + +func TestInspectControlStateClassifiesOwnerStates(t *testing.T) { + t.Run("absent is clean", func(t *testing.T) { + rootPath := t.TempDir() + inspection, err := InspectControlState(context.Background(), rootPath) + assertControlInspection(t, inspection, err, ControlStateClean, "") + if _, err := os.Stat(filepath.Join(rootPath, ControlRoot)); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("read-only inspection created control state: %v", err) + } + }) + + t.Run("empty control namespace is clean", func(t *testing.T) { + rootPath := t.TempDir() + root, _, err := openRepository(rootPath) + if err != nil { + t.Fatal(err) + } + if err := ensureDirectory(root, ControlDirectory, 0o700); err != nil { + root.Close() + t.Fatal(err) + } + if err := root.Close(); err != nil { + t.Fatal(err) + } + inspection, err := InspectControlState(context.Background(), rootPath) + assertControlInspection(t, inspection, err, ControlStateClean, "") + }) + + for _, terminalState := range []string{StateApplied, StateRolledBack} { + t.Run("valid terminal "+terminalState+" is clean", func(t *testing.T) { + rootPath := t.TempDir() + plan, err := BuildPlan(context.Background(), rootPath, []Target{{Path: "proofkit/a.json", Content: []byte("a\n"), Mode: 0o644}}) + if err != nil { + t.Fatal(err) + } + if terminalState == StateApplied { + if result, err := Apply(context.Background(), rootPath, plan); err != nil || result.State != StateApplied { + t.Fatalf("Apply()=%#v, %v", result, err) + } + } else { + leaveInterruptedPrefix(t, rootPath, plan, 0) + if result, err := Recover(context.Background(), rootPath, plan.TransactionID, RecoveryRollback); err != nil || result.State != StateRolledBack { + t.Fatalf("Recover()=%#v, %v", result, err) + } + } + inspection, err := InspectControlState(context.Background(), rootPath) + assertControlInspection(t, inspection, err, ControlStateClean, "") + }) + } + + t.Run("active canonical journal is recoverable", func(t *testing.T) { + rootPath := t.TempDir() + plan, err := BuildPlan(context.Background(), rootPath, []Target{{Path: "proofkit/a.json", Content: []byte("a\n"), Mode: 0o644}}) + if err != nil { + t.Fatal(err) + } + leaveInterruptedPrefix(t, rootPath, plan, 0) + inspection, err := InspectControlState(context.Background(), rootPath) + assertControlInspection(t, inspection, err, ControlStateRecoverable, plan.TransactionID) + }) + + t.Run("canonical preparing journal is recoverable", func(t *testing.T) { + rootPath := t.TempDir() + plan, err := BuildPlan(context.Background(), rootPath, []Target{{Path: "proofkit/a.json", Content: []byte("a\n"), Mode: 0o644}}) + if err != nil { + t.Fatal(err) + } + root, _, err := openRepository(rootPath) + if err != nil { + t.Fatal(err) + } + if err := ensureDirectory(root, activeDirectory, 0o700); err != nil { + root.Close() + t.Fatal(err) + } + content, err := stablejson.Marshal(journalValue(plan)) + if err != nil { + root.Close() + t.Fatal(err) + } + if err := writeOwnedFile(root, journalTemp, content, 0o600); err != nil { + root.Close() + t.Fatal(err) + } + if err := root.Close(); err != nil { + t.Fatal(err) + } + inspection, err := InspectControlState(context.Background(), rootPath) + assertControlInspection(t, inspection, err, ControlStateRecoverable, plan.TransactionID) + }) +} + +func TestControlInspectionOnlyPromotesOperationalCleanupFailures(t *testing.T) { + cleanupErr := closeReadResource(errorCloser{}, "test resource") + if !errors.Is(cleanupErr, ErrReadCleanup) || !errors.Is(controlInspectionOperationalError(cleanupErr), ErrReadCleanup) { + t.Fatalf("cleanup error=%v promoted=%v", cleanupErr, controlInspectionOperationalError(cleanupErr)) + } + if promoted := controlInspectionOperationalError(errors.New("malformed caller state")); promoted != nil { + t.Fatalf("semantic admission error promoted as operational: %v", promoted) + } +} + +type errorCloser struct{} + +func (errorCloser) Close() error { + return errors.New("injected close failure") +} + +func TestInspectControlStateRejectsMalformedAndConflictingState(t *testing.T) { + tests := []struct { + name string + setup func(*testing.T, string) + }{ + {name: "malformed journal has unknown identity", setup: setupMalformedInspectionJournal}, + {name: "conflicting terminal routes", setup: setupConflictingInspectionTerminals}, + {name: "conflicting terminal markers", setup: setupConflictingInspectionMarkers}, + {name: "malformed terminal receipt", setup: setupMalformedInspectionTerminalReceipt}, + {name: "unknown caller-named control entry", setup: setupUnknownInspectionEntry}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + rootPath := t.TempDir() + test.setup(t, rootPath) + inspection, err := InspectControlState(context.Background(), rootPath) + assertControlInspection(t, inspection, err, ControlStateInvalid, "") + if strings.Contains(fmt.Sprintf("%#v", inspection), "caller-private-label") { + t.Fatal("control inspection disclosed a caller-owned entry name") + } + }) + } +} + +func setupMalformedInspectionJournal(t *testing.T, rootPath string) { + t.Helper() + root, _, err := openRepository(rootPath) + if err != nil { + t.Fatal(err) + } + defer root.Close() + if err := ensureDirectory(root, activeDirectory, 0o700); err != nil { + t.Fatal(err) + } + if err := writeOwnedFile(root, journalTemp, []byte("{"), 0o600); err != nil { + t.Fatal(err) + } +} + +func setupConflictingInspectionTerminals(t *testing.T, rootPath string) { + t.Helper() + root, _, err := openRepository(rootPath) + if err != nil { + t.Fatal(err) + } + defer root.Close() + if err := ensureDirectory(root, ControlDirectory, 0o700); err != nil { + t.Fatal(err) + } + for _, digit := range []string{"1", "2"} { + transactionID := "sha256:" + strings.Repeat(digit, 64) + if err := root.Mkdir(filepath.FromSlash(terminalTombstonePath(transactionID, StateApplied)), 0o700); err != nil { + t.Fatal(err) + } + } +} + +func setupConflictingInspectionMarkers(t *testing.T, rootPath string) { + t.Helper() + plan, err := BuildPlan(context.Background(), rootPath, []Target{{Path: "proofkit/a.json", Content: []byte("a\n"), Mode: 0o644}}) + if err != nil { + t.Fatal(err) + } + leaveInterruptedPrefix(t, rootPath, plan, 0) + root, _, err := openRepository(rootPath) + if err != nil { + t.Fatal(err) + } + defer root.Close() + if err := writeMarker(root, committedMarker); err != nil { + t.Fatal(err) + } + if err := writeMarker(root, rolledBackMarker); err != nil { + t.Fatal(err) + } +} + +func setupMalformedInspectionTerminalReceipt(t *testing.T, rootPath string) { + t.Helper() + plan, err := BuildPlan(context.Background(), rootPath, []Target{{Path: "proofkit/a.json", Content: []byte("a\n"), Mode: 0o644}}) + if err != nil { + t.Fatal(err) + } + if result, err := Apply(context.Background(), rootPath, plan); err != nil || result.State != StateApplied { + t.Fatalf("Apply()=%#v, %v", result, err) + } + path := filepath.Join(rootPath, filepath.FromSlash(terminalTombstonePath(plan.TransactionID, StateApplied)), terminalReceiptName) + if err := os.WriteFile(path, []byte("{"), 0o600); err != nil { + t.Fatal(err) + } +} + +func setupUnknownInspectionEntry(t *testing.T, rootPath string) { + t.Helper() + root, _, err := openRepository(rootPath) + if err != nil { + t.Fatal(err) + } + defer root.Close() + if err := ensureDirectory(root, ControlDirectory, 0o700); err != nil { + t.Fatal(err) + } + if err := writeOwnedFile(root, ControlDirectory+"/caller-private-label", []byte("opaque"), 0o600); err != nil { + t.Fatal(err) + } +} + +func TestInspectControlStateReturnsBusyAndRespectsContext(t *testing.T) { + rootPath := t.TempDir() + root, _, err := openRepository(rootPath) + if err != nil { + t.Fatal(err) + } + defer root.Close() + lock, err := acquireTransactionLock(root) + if err != nil { + t.Fatal(err) + } + defer lock.release() + if _, err := InspectControlState(context.Background(), rootPath); !errors.Is(err, ErrBusy) { + t.Fatalf("InspectControlState() error=%v, want ErrBusy", err) + } + + cancelled, cancel := context.WithCancel(context.Background()) + cancel() + if _, err := InspectControlState(cancelled, t.TempDir()); !errors.Is(err, context.Canceled) { + t.Fatalf("InspectControlState(cancelled) error=%v, want context.Canceled", err) + } +} + +func TestInspectionLeasePinsRootAndExcludesCooperativeWriter(t *testing.T) { + rootPath := t.TempDir() + initial, err := BuildPlan(context.Background(), rootPath, []Target{{Path: "proofkit/a.json", Content: []byte("a\n"), Mode: 0o644}}) + if err != nil { + t.Fatal(err) + } + if result, err := Apply(context.Background(), rootPath, initial); err != nil || result.State != StateApplied { + t.Fatalf("initial Apply()=%#v, %v", result, err) + } + next, err := BuildPlan(context.Background(), rootPath, []Target{{Path: "proofkit/b.json", Content: []byte("b\n"), Mode: 0o644}}) + if err != nil { + t.Fatal(err) + } + lease, err := OpenInspectionLease(context.Background(), rootPath) + if err != nil { + t.Fatal(err) + } + if inspection, err := lease.InspectControlState(context.Background()); err != nil || inspection.State != ControlStateClean { + lease.Close() + t.Fatalf("lease inspection=%#v, %v", inspection, err) + } + if _, err := Apply(context.Background(), rootPath, next); !errors.Is(err, ErrBusy) { + lease.Close() + t.Fatalf("Apply() while inspection lease held error=%v, want ErrBusy", err) + } + if err := lease.Close(); err != nil { + t.Fatal(err) + } + if result, err := Apply(context.Background(), rootPath, next); err != nil || result.State != StateApplied { + t.Fatalf("Apply() after inspection lease=%#v, %v", result, err) + } +} + +func TestInspectionLeaseDoesNotResolveAReplacementRoot(t *testing.T) { + rootPath := filepath.Join(t.TempDir(), "repository") + if err := os.Mkdir(rootPath, 0o755); err != nil { + t.Fatal(err) + } + lease, err := OpenInspectionLease(context.Background(), rootPath) + if err != nil { + t.Fatal(err) + } + savedPath := rootPath + "-saved" + replacementPath := rootPath + "-replacement" + if err := os.Mkdir(replacementPath, 0o755); err != nil { + lease.Close() + t.Fatal(err) + } + setupUnknownInspectionEntry(t, replacementPath) + if err := os.Rename(rootPath, savedPath); err != nil { + lease.Close() + t.Fatal(err) + } + if err := os.Rename(replacementPath, rootPath); err != nil { + lease.Close() + t.Fatal(err) + } + + inspection, inspectErr := lease.InspectControlState(context.Background()) + if inspectErr != nil || inspection.State != ControlStateClean { + lease.Close() + t.Fatalf("pinned inspection=%#v, %v, want original clean root", inspection, inspectErr) + } + if err := lease.VerifyRootIdentity(); !errors.Is(err, ErrControlStateChanged) { + lease.Close() + t.Fatalf("VerifyRootIdentity() error=%v, want ErrControlStateChanged", err) + } + if err := lease.Close(); err != nil { + t.Fatal(err) + } +} + +func TestInspectionLeaseRejectsControlNamespaceCreatedAfterOpen(t *testing.T) { + rootPath := t.TempDir() + lease, err := OpenInspectionLease(context.Background(), rootPath) + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(rootPath, filepath.FromSlash(ControlDirectory)), 0o700); err != nil { + lease.Close() + t.Fatal(err) + } + if inspection, err := lease.InspectControlState(context.Background()); !errors.Is(err, ErrControlStateChanged) || inspection != (ControlInspection{}) { + lease.Close() + t.Fatalf("InspectControlState()=%#v, %v, want control-state change", inspection, err) + } + if err := lease.Close(); err != nil { + t.Fatal(err) + } +} + +func TestInspectControlFileRejectsGrowthAfterRoutePreflight(t *testing.T) { + rootPath := t.TempDir() + root, _, err := openRepository(rootPath) + if err != nil { + t.Fatal(err) + } + defer root.Close() + if err := ensureDirectory(root, activeDirectory, 0o700); err != nil { + t.Fatal(err) + } + const relativePath = activeDirectory + "/growth.bin" + if err := writeOwnedFile(root, relativePath, []byte("a"), 0o600); err != nil { + t.Fatal(err) + } + before, err := root.Lstat(filepath.FromSlash(relativePath)) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(rootPath, filepath.FromSlash(relativePath)), bytes.Repeat([]byte{'b'}, 32), 0o600); err != nil { + t.Fatal(err) + } + if _, err := inspectControlFileDigest(context.Background(), root, relativePath, before, 16); !errors.Is(err, ErrControlStateChanged) { + t.Fatalf("inspectControlFileDigest() error = %v, want ErrControlStateChanged", err) + } +} + +func TestInspectControlFileRejectsSameByteRouteReplacement(t *testing.T) { + rootPath := t.TempDir() + root, _, err := openRepository(rootPath) + if err != nil { + t.Fatal(err) + } + defer root.Close() + if err := ensureDirectory(root, activeDirectory, 0o700); err != nil { + t.Fatal(err) + } + const relativePath = activeDirectory + "/record.json" + content := []byte("same bytes\n") + if err := writeOwnedFile(root, relativePath, content, 0o600); err != nil { + t.Fatal(err) + } + before, err := root.Lstat(filepath.FromSlash(relativePath)) + if err != nil { + t.Fatal(err) + } + _, err = inspectControlFileDigestWithHook(context.Background(), root, relativePath, before, MaximumFileBytes, func() { + if renameErr := root.Rename(filepath.FromSlash(relativePath), filepath.FromSlash(relativePath+".original")); renameErr != nil { + t.Fatal(renameErr) + } + if writeErr := writeOwnedFile(root, relativePath, content, 0o600); writeErr != nil { + t.Fatal(writeErr) + } + }) + if !errors.Is(err, ErrControlStateChanged) { + t.Fatalf("inspectControlFileDigestWithHook() error=%v, want control-state change", err) + } +} + +func TestInspectionLeaseExportsOnlyReadOnlyFileCapability(t *testing.T) { + leaseType := reflect.TypeOf((*InspectionLease)(nil)) + if _, exists := leaseType.MethodByName("Root"); exists { + t.Fatal("InspectionLease exports its mutation-capable confined root") + } + osRootType := reflect.TypeOf((*os.Root)(nil)) + for index := 0; index < leaseType.NumMethod(); index++ { + method := leaseType.Method(index) + for output := 0; output < method.Type.NumOut(); output++ { + if method.Type.Out(output) == osRootType { + t.Fatalf("InspectionLease.%s exports *os.Root", method.Name) + } + } + } +} + +func TestInspectionLeaseFileCannotBeReassertedAsMutable(t *testing.T) { + rootPath := t.TempDir() + if err := os.WriteFile(filepath.Join(rootPath, "record.json"), []byte("record\n"), 0o644); err != nil { + t.Fatal(err) + } + lease, err := OpenInspectionLease(context.Background(), rootPath) + if err != nil { + t.Fatal(err) + } + file, err := lease.OpenExactRegularFile("record.json") + if err != nil { + lease.Close() + t.Fatal(err) + } + type byteWriter interface { + Write([]byte) (int, error) + } + if _, mutable := file.(byteWriter); mutable { + file.Close() + lease.Close() + t.Fatal("inspection file exposes a mutation method through its dynamic type") + } + if _, rawFile := file.(*os.File); rawFile { + file.Close() + lease.Close() + t.Fatal("inspection file exposes its mutation-capable operating-system descriptor") + } + if err := file.Close(); err != nil { + lease.Close() + t.Fatal(err) + } + if err := lease.Close(); err != nil { + t.Fatal(err) + } +} + +func TestInspectControlStateEpochIsDeterministicAndContentBound(t *testing.T) { + rootPath := t.TempDir() + plan, err := BuildPlan(context.Background(), rootPath, []Target{{Path: "proofkit/a.json", Content: []byte("a\n"), Mode: 0o644}}) + if err != nil { + t.Fatal(err) + } + root, _, err := openRepository(rootPath) + if err != nil { + t.Fatal(err) + } + if err := ensureDirectory(root, activeDirectory, 0o700); err != nil { + root.Close() + t.Fatal(err) + } + content, err := stablejson.Marshal(journalValue(plan)) + if err != nil { + root.Close() + t.Fatal(err) + } + if err := writeOwnedFile(root, journalTemp, content, 0o600); err != nil { + root.Close() + t.Fatal(err) + } + if err := root.Close(); err != nil { + t.Fatal(err) + } + + first, err := InspectControlState(context.Background(), rootPath) + assertControlInspection(t, first, err, ControlStateRecoverable, plan.TransactionID) + second, err := InspectControlState(context.Background(), rootPath) + assertControlInspection(t, second, err, ControlStateRecoverable, plan.TransactionID) + if first != second { + t.Fatalf("deterministic inspection changed: first=%#v second=%#v", first, second) + } + + root, _, err = openRepository(rootPath) + if err != nil { + t.Fatal(err) + } + if err := root.Rename(filepath.FromSlash(journalTemp), filepath.FromSlash(journalPath)); err != nil { + root.Close() + t.Fatal(err) + } + if err := root.Close(); err != nil { + t.Fatal(err) + } + third, err := InspectControlState(context.Background(), rootPath) + assertControlInspection(t, third, err, ControlStateRecoverable, plan.TransactionID) + if third.EpochID == first.EpochID { + t.Fatal("control epoch did not change when admitted control content changed") + } +} + +func TestInspectControlStateRejectsPartialControlObservations(t *testing.T) { + t.Run("entry overflow", func(t *testing.T) { + rootPath := t.TempDir() + root, _, err := openRepository(rootPath) + if err != nil { + t.Fatal(err) + } + if err := ensureDirectory(root, ControlDirectory, 0o700); err != nil { + root.Close() + t.Fatal(err) + } + for index := 0; index < 4; index++ { + if err := writeOwnedFile(root, fmt.Sprintf("%s/unknown-%d", ControlDirectory, index), []byte("opaque"), 0o600); err != nil { + root.Close() + t.Fatal(err) + } + } + if err := root.Close(); err != nil { + t.Fatal(err) + } + + inspection, err := InspectControlState(context.Background(), rootPath) + if !errors.Is(err, errControlObservationBound) || inspection != (ControlInspection{}) { + t.Fatalf("InspectControlState()=%#v, %v, want bounded failure", inspection, err) + } + }) + + t.Run("nested directory", func(t *testing.T) { + rootPath := t.TempDir() + root, _, err := openRepository(rootPath) + if err != nil { + t.Fatal(err) + } + if err := ensureDirectory(root, activeDirectory+"/nested", 0o700); err != nil { + root.Close() + t.Fatal(err) + } + if err := root.Close(); err != nil { + t.Fatal(err) + } + + inspection, err := InspectControlState(context.Background(), rootPath) + if !errors.Is(err, errControlObservationShape) || inspection != (ControlInspection{}) { + t.Fatalf("InspectControlState()=%#v, %v, want unsupported-shape failure", inspection, err) + } + }) +} + +func TestInspectControlStateHashesSymlinkTargetsWithoutDisclosingThem(t *testing.T) { + rootPath := t.TempDir() + controlPath := filepath.Join(rootPath, filepath.FromSlash(ControlDirectory)) + if err := os.MkdirAll(controlPath, 0o700); err != nil { + t.Fatal(err) + } + linkPath := filepath.Join(controlPath, "unknown") + if err := os.Symlink("alpha", linkPath); err != nil { + t.Fatal(err) + } + first, err := InspectControlState(context.Background(), rootPath) + assertControlInspection(t, first, err, ControlStateInvalid, "") + + if err := os.Remove(linkPath); err != nil { + t.Fatal(err) + } + if err := os.Symlink("omega", linkPath); err != nil { + t.Fatal(err) + } + second, err := InspectControlState(context.Background(), rootPath) + assertControlInspection(t, second, err, ControlStateInvalid, "") + if first.EpochID == second.EpochID { + t.Fatal("same-length symlink target change did not change the control epoch") + } + if strings.Contains(fmt.Sprintf("%#v %#v", first, second), "alpha") || strings.Contains(fmt.Sprintf("%#v %#v", first, second), "omega") { + t.Fatal("control inspection disclosed a symlink target") + } +} + +func TestInspectControlStateUsesPortableObservationFields(t *testing.T) { + rootPath := t.TempDir() + root, _, err := openRepository(rootPath) + if err != nil { + t.Fatal(err) + } + const directoryName = "unknown" + const fileName = "payload" + directoryPath := ControlDirectory + "/" + directoryName + if err := ensureDirectory(root, directoryPath, 0o700); err != nil { + root.Close() + t.Fatal(err) + } + if err := writeOwnedFile(root, directoryPath+"/"+fileName, []byte("alpha"), 0o600); err != nil { + root.Close() + t.Fatal(err) + } + if err := root.Close(); err != nil { + t.Fatal(err) + } + + observationID, err := digest.StableJSONSHA256Ref(map[string]any{ + "controlObservationKind": "proofkit.repository-control-observation", + "entries": []any{map[string]any{ + "entries": []any{map[string]any{ + "contentId": digest.SHA256TextRef("alpha"), + "kind": "regular", + "mode": json.Number("384"), + "nameId": digest.SHA256TextRef(fileName), + "size": json.Number("5"), + }}, + "kind": "directory", + "mode": json.Number("448"), + "nameId": digest.SHA256TextRef(directoryName), + }}, + "schemaVersion": json.Number("1"), + }) + if err != nil { + t.Fatal(err) + } + want, err := newControlInspection(ControlStateInvalid, "", observationID) + if err != nil { + t.Fatal(err) + } + got, err := InspectControlState(context.Background(), rootPath) + if err != nil || got != want { + t.Fatalf("InspectControlState()=%#v, %v, want %#v", got, err, want) + } +} + +func TestInspectControlStateNormalizesPortableEntryNames(t *testing.T) { + for _, pair := range [][2]string{ + {"caf\u00e9", "cafe\u0301"}, + {"Custom", "custom"}, + } { + left := invalidControlEpochForName(t, pair[0]) + right := invalidControlEpochForName(t, pair[1]) + if left != right { + t.Fatalf("portable-equivalent names %q and %q produced different epochs", pair[0], pair[1]) + } + } +} + +func TestInspectControlStateRejectsClassificationChangeHiddenByPortableNameIdentity(t *testing.T) { + rootPath := t.TempDir() + plan, err := BuildPlan(context.Background(), rootPath, []Target{{Path: "proofkit/a.json", Content: []byte("a\n"), Mode: 0o644}}) + if err != nil { + t.Fatal(err) + } + leaveInterruptedPrefix(t, rootPath, plan, 0) + lease, err := OpenInspectionLease(context.Background(), rootPath) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := lease.Close(); err != nil { + t.Fatal(err) + } + }() + + _, err = lease.inspectControlState(context.Background(), func() { + from := filepath.Join(rootPath, filepath.FromSlash(activeDirectory)) + to := filepath.Join(rootPath, filepath.FromSlash(ControlDirectory), "ACTIVE") + if renameErr := os.Rename(from, to); renameErr != nil { + t.Fatal(renameErr) + } + }) + if !errors.Is(err, ErrControlStateChanged) { + t.Fatalf("InspectControlState() error=%v, want control-state change", err) + } +} + +func TestControlObservationRejectsPortableEntryAliasCollision(t *testing.T) { + entries := []fs.DirEntry{testNamedDirEntry("caf\u00e9"), testNamedDirEntry("cafe\u0301")} + if err := sortInspectionEntries(entries); !errors.Is(err, errControlObservationShape) { + t.Fatalf("sortInspectionEntries() error=%v, want unsupported shape", err) + } +} + +func invalidControlEpochForName(t *testing.T, name string) string { + t.Helper() + rootPath := t.TempDir() + controlPath := filepath.Join(rootPath, filepath.FromSlash(ControlDirectory)) + if err := os.MkdirAll(controlPath, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(controlPath, name), []byte("portable\n"), 0o600); err != nil { + t.Fatal(err) + } + inspection, err := InspectControlState(context.Background(), rootPath) + if err != nil || inspection.State != ControlStateInvalid { + t.Fatalf("InspectControlState()=%#v, %v, want invalid", inspection, err) + } + return inspection.EpochID +} + +type testNamedDirEntry string + +func (entry testNamedDirEntry) Name() string { return string(entry) } +func (testNamedDirEntry) IsDir() bool { return false } +func (testNamedDirEntry) Type() fs.FileMode { return 0 } +func (testNamedDirEntry) Info() (fs.FileInfo, error) { return nil, nil } + +func TestInspectControlStateDoesNotMutateExistingNamespace(t *testing.T) { + rootPath := t.TempDir() + plan, err := BuildPlan(context.Background(), rootPath, []Target{{Path: "proofkit/a.json", Content: []byte("a\n"), Mode: 0o644}}) + if err != nil { + t.Fatal(err) + } + leaveInterruptedPrefix(t, rootPath, plan, 0) + before := snapshotTestTree(t, rootPath) + inspection, err := InspectControlState(context.Background(), rootPath) + assertControlInspection(t, inspection, err, ControlStateRecoverable, plan.TransactionID) + after := snapshotTestTree(t, rootPath) + if !reflect.DeepEqual(before, after) { + t.Fatalf("read-only inspection mutated repository:\nbefore=%#v\nafter=%#v", before, after) + } +} + +type testTreeEntry struct { + Content []byte + Mode fs.FileMode + Path string +} + +func snapshotTestTree(t *testing.T, rootPath string) []testTreeEntry { + t.Helper() + entries := []testTreeEntry{} + err := filepath.WalkDir(rootPath, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + if path == rootPath { + return nil + } + relative, err := filepath.Rel(rootPath, path) + if err != nil { + return err + } + info, err := entry.Info() + if err != nil { + return err + } + item := testTreeEntry{Mode: info.Mode(), Path: filepath.ToSlash(relative)} + if info.Mode().IsRegular() { + item.Content, err = os.ReadFile(path) + if err != nil { + return err + } + } + entries = append(entries, item) + return nil + }) + if err != nil { + t.Fatal(err) + } + sort.Slice(entries, func(left, right int) bool { return entries[left].Path < entries[right].Path }) + for index := range entries { + entries[index].Content = bytes.Clone(entries[index].Content) + } + return entries +} + +func assertControlInspection(t *testing.T, got ControlInspection, err error, state, transactionID string) { + t.Helper() + if err != nil { + t.Fatalf("InspectControlState() error=%v", err) + } + if got.State != state || got.TransactionID != transactionID { + t.Fatalf("InspectControlState()=%#v, want state=%q transactionId=%q", got, state, transactionID) + } + if !strings.HasPrefix(got.EpochID, "sha256:") || len(got.EpochID) != len("sha256:")+64 { + t.Fatalf("InspectControlState() epochId=%q", got.EpochID) + } +} diff --git a/internal/kernel/repositorytransaction/control_observation.go b/internal/kernel/repositorytransaction/control_observation.go new file mode 100644 index 0000000..46c3265 --- /dev/null +++ b/internal/kernel/repositorytransaction/control_observation.go @@ -0,0 +1,232 @@ +package repositorytransaction + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "sort" + "strconv" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" + "github.com/research-engineering/agentic-proofkit/internal/kernel/pathidentity" +) + +const maximumControlObservationBytes = MaximumAggregateBytes * 4 + +var ( + errControlObservationBound = errors.New("repository transaction control observation exceeds its bound") + errControlObservationShape = errors.New("repository transaction control observation contains an unsupported shape") +) + +type controlObservation struct { + Digest string + Entries []fs.DirEntry + Invalid bool +} + +func observeControlNamespace(ctx context.Context, root *os.Root) (controlObservation, error) { + entries, err := readInspectionEntries(root, ControlDirectory, 3) + if err != nil { + return controlObservation{}, err + } + remaining := int64(maximumControlObservationBytes) + values := make([]any, 0, len(entries)) + invalid := false + for _, entry := range entries { + if err := ctx.Err(); err != nil { + return controlObservation{}, fmt.Errorf("inspect repository transaction control state cancelled: %w", err) + } + value, entryInvalid, err := observeControlEntry(ctx, root, ControlDirectory, entry, true, &remaining) + if err != nil { + return controlObservation{}, err + } + invalid = invalid || entryInvalid + values = append(values, value) + } + value := map[string]any{ + "controlObservationKind": "proofkit.repository-control-observation", + "entries": values, + "schemaVersion": json.Number("1"), + } + observationID, err := digest.StableJSONSHA256Ref(value) + if err != nil { + return controlObservation{}, fmt.Errorf("derive repository transaction control observation: %w", err) + } + return controlObservation{Digest: observationID, Entries: entries, Invalid: invalid}, nil +} + +func observeControlEntry(ctx context.Context, root *os.Root, directory string, entry fs.DirEntry, descend bool, remaining *int64) (map[string]any, bool, error) { + relativePath := directory + "/" + entry.Name() + nameKey, err := pathidentity.Key(entry.Name()) + if err != nil { + return nil, false, errControlObservationShape + } + info, err := root.Lstat(filepath.FromSlash(relativePath)) + if err != nil { + return nil, false, fmt.Errorf("inspect repository transaction control entry") + } + entryKind := "other" + switch { + case info.Mode()&os.ModeSymlink != 0: + entryKind = "symlink" + case info.IsDir(): + entryKind = "directory" + case info.Mode().IsRegular(): + entryKind = "regular" + } + value := map[string]any{ + "kind": entryKind, + "nameId": digest.SHA256TextRef(nameKey), + } + invalid := false + switch entryKind { + case "regular": + value["mode"] = json.Number(strconv.FormatUint(uint64(info.Mode().Perm()), 10)) + value["size"] = json.Number(strconv.FormatInt(info.Size(), 10)) + if info.Size() < 0 || info.Size() > MaximumFileBytes || info.Size() > *remaining { + return nil, false, errControlObservationBound + } + contentID, err := inspectControlFileDigest(ctx, root, relativePath, info, min(MaximumFileBytes, *remaining)) + if err != nil { + return nil, false, err + } + *remaining -= info.Size() + value["contentId"] = contentID + case "directory": + if !descend { + return nil, false, errControlObservationShape + } + value["mode"] = json.Number(strconv.FormatUint(uint64(info.Mode().Perm()), 10)) + limit := MaximumOperations*2 + MaximumOperations*pathidentity.MaximumComponents + 10 + children, err := readInspectionEntries(root, relativePath, limit) + if err != nil { + return nil, false, err + } + childValues := make([]any, 0, len(children)) + for _, child := range children { + childValue, childInvalid, err := observeControlEntry(ctx, root, relativePath, child, false, remaining) + if err != nil { + return nil, false, err + } + invalid = invalid || childInvalid + childValues = append(childValues, childValue) + } + value["entries"] = childValues + case "symlink": + target, err := root.Readlink(filepath.FromSlash(relativePath)) + if err != nil { + return nil, false, ErrControlStateChanged + } + current, err := root.Lstat(filepath.FromSlash(relativePath)) + if err != nil || current.Mode()&os.ModeSymlink == 0 || !os.SameFile(info, current) || current.Size() != info.Size() { + return nil, false, ErrControlStateChanged + } + value["targetId"] = digest.SHA256TextRef(target) + invalid = true + default: + return nil, false, errControlObservationShape + } + return value, invalid, nil +} + +func readInspectionEntries(root *os.Root, relativePath string, maximum int) (entries []fs.DirEntry, returnErr error) { + directory, err := root.Open(filepath.FromSlash(relativePath)) + if err != nil { + return nil, fmt.Errorf("open repository transaction control directory") + } + defer func() { + if closeErr := closeReadResource(directory, "control directory"); closeErr != nil { + entries = nil + returnErr = closeErr + } + }() + entries, err = directory.ReadDir(maximum + 1) + if err != nil && !errors.Is(err, io.EOF) { + return nil, fmt.Errorf("read repository transaction control directory") + } + if len(entries) > maximum { + return nil, errControlObservationBound + } + if err := sortInspectionEntries(entries); err != nil { + return nil, err + } + return entries, nil +} + +func sortInspectionEntries(entries []fs.DirEntry) error { + keys := make(map[string]string, len(entries)) + for _, entry := range entries { + key, err := pathidentity.Key(entry.Name()) + if err != nil { + return errControlObservationShape + } + if _, exists := keys[key]; exists { + return errControlObservationShape + } + keys[key] = entry.Name() + } + sort.Slice(entries, func(left, right int) bool { return keys[entries[left].Name()] < keys[entries[right].Name()] }) + return nil +} + +func inspectControlFileDigest(ctx context.Context, root *os.Root, relativePath string, routeInfo fs.FileInfo, maximum int64) (string, error) { + return inspectControlFileDigestWithHook(ctx, root, relativePath, routeInfo, maximum, nil) +} + +func inspectControlFileDigestWithHook(ctx context.Context, root *os.Root, relativePath string, routeInfo fs.FileInfo, maximum int64, beforeRouteRecheck func()) (contentID string, returnErr error) { + if maximum < 0 || routeInfo.Size() < 0 || routeInfo.Size() > maximum { + return "", fmt.Errorf("repository transaction control file exceeds its read bound") + } + file, err := openNoFollow(root, filepath.FromSlash(relativePath)) + if err != nil { + return "", fmt.Errorf("open repository transaction control file") + } + defer func() { + if closeErr := closeReadResource(file, "control file"); closeErr != nil { + contentID = "" + returnErr = closeErr + } + }() + opened, err := file.Stat() + if err != nil || !opened.Mode().IsRegular() || !os.SameFile(routeInfo, opened) || opened.Size() != routeInfo.Size() || opened.Size() > maximum { + return "", ErrControlStateChanged + } + content := make([]byte, 0, opened.Size()) + buffer := make([]byte, 32<<10) + limited := io.LimitReader(file, maximum+1) + for int64(len(content)) <= maximum { + if err := ctx.Err(); err != nil { + return "", fmt.Errorf("inspect repository transaction control state cancelled: %w", err) + } + count, readErr := limited.Read(buffer) + if count > 0 { + content = append(content, buffer[:count]...) + } + if errors.Is(readErr, io.EOF) { + break + } + if readErr != nil { + return "", fmt.Errorf("read repository transaction control file") + } + } + if int64(len(content)) > maximum { + return "", ErrControlStateChanged + } + after, err := file.Stat() + if err != nil || !os.SameFile(opened, after) || after.Size() != int64(len(content)) || opened.Size() != after.Size() || !opened.ModTime().Equal(after.ModTime()) { + return "", ErrControlStateChanged + } + if beforeRouteRecheck != nil { + beforeRouteRecheck() + } + current, err := root.Lstat(filepath.FromSlash(relativePath)) + if err != nil || current.Mode()&os.ModeSymlink != 0 || !os.SameFile(opened, current) || current.Size() != int64(len(content)) { + return "", ErrControlStateChanged + } + return digest.SHA256BytesRef(content), nil +} diff --git a/internal/kernel/repositorytransaction/control_state.go b/internal/kernel/repositorytransaction/control_state.go index 3d6b3e9..e63ddc7 100644 --- a/internal/kernel/repositorytransaction/control_state.go +++ b/internal/kernel/repositorytransaction/control_state.go @@ -83,7 +83,7 @@ func activeEntries(root *os.Root) ([]fs.DirEntry, error) { return transactionEntries(root, activeDirectory) } -func transactionEntries(root *os.Root, relativePath string) ([]fs.DirEntry, error) { +func transactionEntries(root *os.Root, relativePath string) (entries []fs.DirEntry, returnErr error) { if err := validatePrivateDirectory(root, relativePath, 0o700); err != nil { return nil, err } @@ -91,9 +91,14 @@ func transactionEntries(root *os.Root, relativePath string) ([]fs.DirEntry, erro if err != nil { return nil, fmt.Errorf("open repository transaction state") } - defer directory.Close() + defer func() { + if closeErr := closeReadResource(directory, "transaction state directory"); closeErr != nil { + entries = nil + returnErr = closeErr + } + }() entryLimit := MaximumOperations*2 + MaximumOperations*pathidentity.MaximumComponents + 10 - entries, err := directory.ReadDir(entryLimit + 1) + entries, err = directory.ReadDir(entryLimit + 1) if err != nil && !errors.Is(err, io.EOF) { return nil, fmt.Errorf("read repository transaction state") } diff --git a/internal/kernel/repositorytransaction/filesystem.go b/internal/kernel/repositorytransaction/filesystem.go index c86ce90..5275c23 100644 --- a/internal/kernel/repositorytransaction/filesystem.go +++ b/internal/kernel/repositorytransaction/filesystem.go @@ -2,7 +2,6 @@ package repositorytransaction import ( "bytes" - "errors" "fmt" "io" "io/fs" @@ -13,12 +12,11 @@ import ( "strings" "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" - "github.com/research-engineering/agentic-proofkit/internal/kernel/pathidentity" + "github.com/research-engineering/agentic-proofkit/internal/kernel/rootpath" ) const ( - activeDirectory = ControlDirectory + "/active" - maximumDirectoryEntries = 16 << 10 + activeDirectory = ControlDirectory + "/active" ) func openRepository(rootPath string) (*os.Root, string, error) { @@ -53,40 +51,6 @@ func openRepository(rootPath string) (*os.Root, string, error) { return root, digest.SHA256TextRef(filepath.Clean(absolute) + "\x00" + identity), nil } -func exactEntryExists(root *os.Root, directory, component string) (bool, error) { - wantedKey, err := pathidentity.Key(component) - if err != nil { - return false, fmt.Errorf("repository transaction path component is invalid") - } - if directory == "" { - directory = "." - } - handle, err := root.Open(filepath.FromSlash(directory)) - if err != nil { - return false, fmt.Errorf("open repository transaction parent directory") - } - defer handle.Close() - entries, err := handle.ReadDir(maximumDirectoryEntries + 1) - if err != nil && !errors.Is(err, io.EOF) { - return false, fmt.Errorf("read repository transaction parent directory") - } - if len(entries) > maximumDirectoryEntries { - return false, fmt.Errorf("repository transaction parent directory exceeds its entry limit") - } - exact := false - for _, entry := range entries { - entryKey, keyErr := pathidentity.Key(entry.Name()) - if keyErr != nil || entryKey != wantedKey { - continue - } - if entry.Name() != component || exact { - return false, fmt.Errorf("repository transaction path has an ambiguous portable filesystem identity") - } - exact = true - } - return exact, nil -} - func exactRouteExists(root *os.Root, relativePath string) (bool, error) { current := "" components := strings.Split(relativePath, "/") @@ -95,7 +59,7 @@ func exactRouteExists(root *os.Root, relativePath string) (bool, error) { if parent == "" { parent = "." } - exists, err := exactEntryExists(root, parent, component) + exists, err := rootpath.ExactEntryExists(root, parent, component) if err != nil || !exists { return false, err } @@ -133,7 +97,7 @@ func inspectParentDirectories(root *os.Root, directory string) ([]string, error) continue } parent := path.Dir(current) - exists, err := exactEntryExists(root, parent, component) + exists, err := rootpath.ExactEntryExists(root, parent, component) if err != nil { return nil, err } @@ -153,7 +117,7 @@ func inspectParentDirectories(root *os.Root, directory string) ([]string, error) return missing, nil } -func inspectTarget(root *os.Root, relativePath string, maximum int64) (Snapshot, []byte, error) { +func inspectTarget(root *os.Root, relativePath string, maximum int64) (snapshot Snapshot, content []byte, returnErr error) { missing, err := inspectParentDirectories(root, path.Dir(relativePath)) if err != nil { return Snapshot{}, nil, err @@ -161,7 +125,7 @@ func inspectTarget(root *os.Root, relativePath string, maximum int64) (Snapshot, if len(missing) > 0 { return Snapshot{}, nil, nil } - targetExists, err := exactEntryExists(root, path.Dir(relativePath), path.Base(relativePath)) + targetExists, err := rootpath.ExactEntryExists(root, path.Dir(relativePath), path.Base(relativePath)) if err != nil { return Snapshot{}, nil, err } @@ -183,12 +147,18 @@ func inspectTarget(root *os.Root, relativePath string, maximum int64) (Snapshot, if err != nil { return Snapshot{}, nil, fmt.Errorf("open repository transaction target") } - defer file.Close() + defer func() { + if closeErr := closeReadResource(file, "transaction target"); closeErr != nil { + snapshot = Snapshot{} + content = nil + returnErr = closeErr + } + }() opened, err := file.Stat() if err != nil || !opened.Mode().IsRegular() || !os.SameFile(routeInfo, opened) { return Snapshot{}, nil, fmt.Errorf("repository transaction target changed during admission") } - content, err := io.ReadAll(io.LimitReader(file, maximum+1)) + content, err = io.ReadAll(io.LimitReader(file, maximum+1)) if err != nil || int64(len(content)) > maximum { return Snapshot{}, nil, fmt.Errorf("read repository transaction target") } @@ -200,7 +170,7 @@ func inspectTarget(root *os.Root, relativePath string, maximum int64) (Snapshot, if err != nil || current.Mode()&os.ModeSymlink != 0 || !os.SameFile(opened, current) { return Snapshot{}, nil, fmt.Errorf("repository transaction target route changed during read") } - snapshot := snapshotForContent(content, opened.Mode().Perm()) + snapshot = snapshotForContent(content, opened.Mode().Perm()) return snapshot, append([]byte(nil), content...), nil } @@ -228,7 +198,7 @@ func ensureDirectory(root *os.Root, relativePath string, mode fs.FileMode) error current += "/" + component } parent := path.Dir(current) - exists, err := exactEntryExists(root, parent, component) + exists, err := rootpath.ExactEntryExists(root, parent, component) if err != nil { return err } @@ -250,9 +220,12 @@ func ensureDirectory(root *os.Root, relativePath string, mode fs.FileMode) error return nil } -func validatePrivateDirectory(root *os.Root, relativePath string, mode fs.FileMode) error { +func validatePrivateDirectory(root *os.Root, relativePath string, mode fs.FileMode) (returnErr error) { exact, err := exactRouteExists(root, relativePath) - if err != nil || !exact { + if err != nil { + return err + } + if !exact { return fmt.Errorf("repository transaction directory route is invalid") } native := filepath.FromSlash(relativePath) @@ -268,7 +241,11 @@ func validatePrivateDirectory(root *os.Root, relativePath string, mode fs.FileMo if err != nil { return fmt.Errorf("open repository transaction directory") } - defer directory.Close() + defer func() { + if closeErr := closeReadResource(directory, "private transaction directory"); closeErr != nil { + returnErr = closeErr + } + }() handleInfo, err := directory.Stat() if err != nil || !os.SameFile(routeInfo, handleInfo) { return fmt.Errorf("repository transaction directory changed during admission") @@ -298,7 +275,7 @@ func controlNamespaceExists(root *os.Root) (bool, error) { return true, nil } -func syncDirectory(root *os.Root, relativePath string) error { +func syncDirectory(root *os.Root, relativePath string) (returnErr error) { if relativePath == "" { relativePath = "." } @@ -306,7 +283,11 @@ func syncDirectory(root *os.Root, relativePath string) error { if err != nil { return fmt.Errorf("open repository directory for sync") } - defer directory.Close() + defer func() { + if closeErr := closeReadResource(directory, "synced transaction directory"); closeErr != nil { + returnErr = closeErr + } + }() if err := directory.Sync(); err != nil { return fmt.Errorf("sync repository directory") } @@ -383,16 +364,24 @@ func discardOwnedTemporaryFile(root *os.Root, relativePath string) error { return syncDirectory(root, path.Dir(relativePath)) } -func readOwnedFile(root *os.Root, relativePath string, maximum int64) ([]byte, error) { +func readOwnedFile(root *os.Root, relativePath string, maximum int64) (content []byte, returnErr error) { exists, err := exactRouteExists(root, relativePath) - if err != nil || !exists { + if err != nil { + return nil, err + } + if !exists { return nil, fmt.Errorf("repository transaction file route is invalid") } file, err := openNoFollow(root, filepath.FromSlash(relativePath)) if err != nil { return nil, fmt.Errorf("open repository transaction file") } - defer file.Close() + defer func() { + if closeErr := closeReadResource(file, "private transaction file"); closeErr != nil { + content = nil + returnErr = closeErr + } + }() info, err := file.Stat() if err != nil || !info.Mode().IsRegular() || info.Mode()&^fs.ModePerm != 0 || info.Mode().Perm() != 0o600 || info.Size() > maximum { return nil, fmt.Errorf("repository transaction file is invalid") @@ -401,7 +390,7 @@ func readOwnedFile(root *os.Root, relativePath string, maximum int64) ([]byte, e if err != nil || !owned { return nil, fmt.Errorf("repository transaction file is not privately owned") } - content, err := io.ReadAll(io.LimitReader(file, maximum+1)) + content, err = io.ReadAll(io.LimitReader(file, maximum+1)) if err != nil || int64(len(content)) > maximum { return nil, fmt.Errorf("read repository transaction file") } diff --git a/internal/kernel/repositorytransaction/inspection_lease.go b/internal/kernel/repositorytransaction/inspection_lease.go new file mode 100644 index 0000000..7634b7a --- /dev/null +++ b/internal/kernel/repositorytransaction/inspection_lease.go @@ -0,0 +1,204 @@ +package repositorytransaction + +import ( + "context" + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/rootpath" +) + +// InspectionFile is the read-only capability returned by an inspection lease. +// Callers must close the file before closing the lease; the interface exposes +// no mutation methods. +type InspectionFile interface { + io.Reader + io.Closer + Stat() (fs.FileInfo, error) +} + +type inspectionFile struct { + file *os.File +} + +func (file *inspectionFile) Read(buffer []byte) (int, error) { + if file == nil || file.file == nil { + return 0, os.ErrClosed + } + return file.file.Read(buffer) +} + +func (file *inspectionFile) Stat() (fs.FileInfo, error) { + if file == nil || file.file == nil { + return nil, os.ErrClosed + } + return file.file.Stat() +} + +func (file *inspectionFile) Close() error { + if file == nil || file.file == nil { + return os.ErrClosed + } + underlying := file.file + file.file = nil + return underlying.Close() +} + +var ( + ErrInspectionRouteChanged = errors.New("repository inspection route changed") + ErrReadCleanup = errors.New("repository read cleanup failed") + ErrUnsafeInspectionRoute = errors.New("repository inspection route is unsafe") +) + +func closeReadResource(resource io.Closer, label string) error { + if err := resource.Close(); err != nil { + return fmt.Errorf("%w: %s", ErrReadCleanup, label) + } + return nil +} + +// InspectionLease pins one repository root and, when the transaction control +// namespace exists, holds its cooperative writer lock for the full read. +type InspectionLease struct { + absolute string + controlNamespace bool + identity os.FileInfo + lock *transactionLock + root *os.Root + rootID string +} + +func OpenInspectionLease(ctx context.Context, rootPath string) (*InspectionLease, error) { + if err := ctx.Err(); err != nil { + return nil, fmt.Errorf("open repository inspection lease cancelled: %w", err) + } + root, rootID, err := openRepository(rootPath) + if err != nil { + return nil, err + } + absolute, err := filepath.Abs(rootPath) + if err != nil { + root.Close() + return nil, fmt.Errorf("resolve repository inspection root") + } + identity, err := root.Stat(".") + if err != nil { + root.Close() + return nil, fmt.Errorf("inspect repository inspection root") + } + lock, exists, err := acquireExistingTransactionLock(root) + if err != nil { + root.Close() + return nil, err + } + return &InspectionLease{ + absolute: absolute, controlNamespace: exists, identity: identity, + lock: lock, root: root, rootID: rootID, + }, nil +} + +// OpenExactRegularFile opens one exact repository-relative regular file +// without exposing the mutation-capable confined root. +func (lease *InspectionLease) OpenExactRegularFile(relativePath string) (InspectionFile, error) { + if lease == nil || lease.root == nil { + return nil, fmt.Errorf("repository inspection lease is closed") + } + file, err := rootpath.OpenExactRegularFile(lease.root, relativePath) + switch { + case errors.Is(err, rootpath.ErrRouteChanged): + return nil, ErrInspectionRouteChanged + case errors.Is(err, rootpath.ErrUnsafeRoute): + return nil, ErrUnsafeInspectionRoute + case err == nil: + return &inspectionFile{file: file}, nil + default: + return nil, err + } +} + +func (lease *InspectionLease) VerifyRootIdentity() error { + if lease == nil || lease.root == nil { + return fmt.Errorf("repository inspection lease is closed") + } + handleInfo, err := lease.root.Stat(".") + if err != nil || !os.SameFile(lease.identity, handleInfo) { + return ErrControlStateChanged + } + routeInfo, err := os.Lstat(lease.absolute) + if err != nil || routeInfo.Mode()&os.ModeSymlink != 0 || !routeInfo.IsDir() || !os.SameFile(lease.identity, routeInfo) { + return ErrControlStateChanged + } + return nil +} + +func (lease *InspectionLease) InspectControlState(ctx context.Context) (ControlInspection, error) { + return lease.inspectControlState(ctx, nil) +} + +func (lease *InspectionLease) inspectControlState(ctx context.Context, beforeReobserve func()) (ControlInspection, error) { + if lease == nil || lease.root == nil { + return ControlInspection{}, fmt.Errorf("repository inspection lease is closed") + } + if err := ctx.Err(); err != nil { + return ControlInspection{}, fmt.Errorf("inspect repository transaction control state cancelled: %w", err) + } + if !lease.controlNamespace { + exists, err := controlNamespaceExists(lease.root) + if err != nil { + return ControlInspection{}, err + } + if exists { + return ControlInspection{}, ErrControlStateChanged + } + emptyObservationID, err := emptyControlObservationID() + if err != nil { + return ControlInspection{}, err + } + return newControlInspection(ControlStateClean, "", emptyObservationID) + } + + before, err := observeControlNamespace(ctx, lease.root) + if err != nil { + return ControlInspection{}, err + } + state, transactionID, err := classifyControlState(lease.root, lease.rootID, before) + if err != nil { + return ControlInspection{}, err + } + if err := ctx.Err(); err != nil { + return ControlInspection{}, fmt.Errorf("inspect repository transaction control state cancelled: %w", err) + } + if beforeReobserve != nil { + beforeReobserve() + } + after, err := observeControlNamespace(ctx, lease.root) + if err != nil { + return ControlInspection{}, err + } + afterState, afterTransactionID, err := classifyControlState(lease.root, lease.rootID, after) + if err != nil { + return ControlInspection{}, err + } + if before.Digest != after.Digest || state != afterState || transactionID != afterTransactionID { + return ControlInspection{}, ErrControlStateChanged + } + return newControlInspection(state, transactionID, after.Digest) +} + +func (lease *InspectionLease) Close() error { + if lease == nil || lease.root == nil { + return nil + } + lockErr := lease.lock.releaseChecked() + rootErr := lease.root.Close() + lease.lock = nil + lease.root = nil + if err := errors.Join(lockErr, rootErr); err != nil { + return fmt.Errorf("%w: inspection lease", ErrReadCleanup) + } + return nil +} diff --git a/internal/kernel/repositorytransaction/lock.go b/internal/kernel/repositorytransaction/lock.go index 17cd3f9..4f6e590 100644 --- a/internal/kernel/repositorytransaction/lock.go +++ b/internal/kernel/repositorytransaction/lock.go @@ -1,6 +1,7 @@ package repositorytransaction import ( + "errors" "fmt" "os" "path/filepath" @@ -47,9 +48,15 @@ func lockTransactionDirectory(root *os.Root) (*transactionLock, error) { } func (lock *transactionLock) release() { + _ = lock.releaseChecked() +} + +func (lock *transactionLock) releaseChecked() error { if lock == nil || lock.directory == nil { - return + return nil } - _ = unlockDirectory(lock.directory) - _ = lock.directory.Close() + unlockErr := unlockDirectory(lock.directory) + closeErr := lock.directory.Close() + lock.directory = nil + return errors.Join(unlockErr, closeErr) } diff --git a/internal/kernel/rootpath/exact.go b/internal/kernel/rootpath/exact.go new file mode 100644 index 0000000..df0012f --- /dev/null +++ b/internal/kernel/rootpath/exact.go @@ -0,0 +1,77 @@ +// Package rootpath owns exact, platform-portable filesystem route lookup +// beneath an already confined os.Root. +package rootpath + +import ( + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/pathidentity" +) + +const maximumDirectoryEntries = 16 << 10 + +var ( + ErrAmbiguousRoute = errors.New("exact root path has an ambiguous portable filesystem identity") + ErrRouteChanged = errors.New("exact root path changed during traversal") + ErrTraversalCleanup = errors.New("exact root path traversal cleanup failed") + ErrUnsafeRoute = errors.New("exact root path traverses a symlink or non-regular entry") +) + +// ExactEntryExists reports whether component exists with the exact spelling +// supplied by the caller. A portable-equivalent alias is rejected instead of +// being treated as the canonical route on a case-insensitive filesystem. +func ExactEntryExists(root *os.Root, directory, component string) (bool, error) { + return exactEntryExistsWithClose(root, directory, component, func(file *os.File) error { return file.Close() }) +} + +func exactEntryExistsWithClose(root *os.Root, directory, component string, closeFile func(*os.File) error) (bool, error) { + if root == nil { + return false, fmt.Errorf("exact root path lookup requires a root") + } + if closeFile == nil { + return false, fmt.Errorf("exact root path lookup requires a closer") + } + if directory == "" { + directory = "." + } + handle, err := root.Open(filepath.FromSlash(directory)) + if err != nil { + return false, fmt.Errorf("open exact root path parent directory") + } + _, exists, err := exactDirectoryEntry(handle, component) + if closeErr := closeFile(handle); closeErr != nil { + return false, fmt.Errorf("%w: close parent directory", ErrTraversalCleanup) + } + return exists, err +} + +func exactDirectoryEntry(handle *os.File, component string) (fs.DirEntry, bool, error) { + wantedKey, err := pathidentity.Key(component) + if err != nil || filepath.Base(component) != component { + return nil, false, fmt.Errorf("exact root path component is invalid") + } + entries, err := handle.ReadDir(maximumDirectoryEntries + 1) + if err != nil && !errors.Is(err, io.EOF) { + return nil, false, fmt.Errorf("read exact root path parent directory") + } + if len(entries) > maximumDirectoryEntries { + return nil, false, fmt.Errorf("exact root path parent directory exceeds its entry limit") + } + var exact fs.DirEntry + for _, entry := range entries { + entryKey, keyErr := pathidentity.Key(entry.Name()) + if keyErr != nil || entryKey != wantedKey { + continue + } + if entry.Name() != component || exact != nil { + return nil, false, ErrAmbiguousRoute + } + exact = entry + } + return exact, exact != nil, nil +} diff --git a/internal/kernel/rootpath/exact_test.go b/internal/kernel/rootpath/exact_test.go new file mode 100644 index 0000000..563fec2 --- /dev/null +++ b/internal/kernel/rootpath/exact_test.go @@ -0,0 +1,196 @@ +package rootpath + +import ( + "errors" + "io/fs" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestExactEntryExistsRejectsPortableAlias(t *testing.T) { + rootPath := t.TempDir() + if err := os.Mkdir(filepath.Join(rootPath, "Docs"), 0o755); err != nil { + t.Fatal(err) + } + root, err := os.OpenRoot(rootPath) + if err != nil { + t.Fatal(err) + } + defer root.Close() + + if exists, err := ExactEntryExists(root, ".", "Docs"); err != nil || !exists { + t.Fatalf("ExactEntryExists(exact) = %v, %v", exists, err) + } + if exists, err := ExactEntryExists(root, ".", "docs"); err == nil || exists || !strings.Contains(err.Error(), "ambiguous portable") { + t.Fatalf("ExactEntryExists(alias) = %v, %v", exists, err) + } +} + +func TestExactEntryExistsPropagatesCleanupFailure(t *testing.T) { + root, err := os.OpenRoot(t.TempDir()) + if err != nil { + t.Fatal(err) + } + defer root.Close() + _, err = exactEntryExistsWithClose(root, ".", "missing", func(file *os.File) error { + _ = file.Close() + return errors.New("injected close failure") + }) + if !errors.Is(err, ErrTraversalCleanup) || errors.Is(err, fs.ErrNotExist) { + t.Fatalf("exactEntryExistsWithClose() error=%v, want cleanup failure", err) + } +} + +func TestOpenExactRegularFileDoesNotNormalizeCleanupFailureAsMissing(t *testing.T) { + root, err := os.OpenRoot(t.TempDir()) + if err != nil { + t.Fatal(err) + } + defer root.Close() + operations := nativeTraversalOperations() + operations.closeFile = func(file *os.File) error { + _ = file.Close() + return errors.New("injected close failure") + } + _, err = openExactRegularFileWithOperations(root, "missing.json", nil, operations) + if !errors.Is(err, ErrTraversalCleanup) || errors.Is(err, fs.ErrNotExist) { + t.Fatalf("openExactRegularFileWithOperations() error=%v, want cleanup failure", err) + } +} + +func TestOpenExactRegularFilePinsEveryRouteComponent(t *testing.T) { + rootPath := t.TempDir() + if err := os.Mkdir(filepath.Join(rootPath, "docs"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(rootPath, "docs", "record.json"), []byte("canonical\n"), 0o644); err != nil { + t.Fatal(err) + } + root, err := os.OpenRoot(rootPath) + if err != nil { + t.Fatal(err) + } + defer root.Close() + + file, err := OpenExactRegularFile(root, "docs/record.json") + if err != nil { + t.Fatal(err) + } + content := make([]byte, len("canonical\n")) + if _, err := file.Read(content); err != nil { + file.Close() + t.Fatal(err) + } + if err := file.Close(); err != nil || string(content) != "canonical\n" { + t.Fatalf("close=%v content=%q", err, content) + } + + if _, err := OpenExactRegularFile(root, "docs/missing.json"); !errors.Is(err, fs.ErrNotExist) { + t.Fatalf("missing error=%v", err) + } +} + +func TestOpenExactRegularFileRejectsParentSymlinkABA(t *testing.T) { + rootPath := t.TempDir() + for _, directory := range []string{"docs", "other"} { + if err := os.Mkdir(filepath.Join(rootPath, directory), 0o755); err != nil { + t.Fatal(err) + } + } + if err := os.WriteFile(filepath.Join(rootPath, "other", "record.json"), []byte("other\n"), 0o644); err != nil { + t.Fatal(err) + } + root, err := os.OpenRoot(rootPath) + if err != nil { + t.Fatal(err) + } + defer root.Close() + + mutated := false + _, err = openExactRegularFile(root, "docs/record.json", func(componentIndex int) { + if componentIndex != 0 || mutated { + return + } + mutated = true + if renameErr := os.Rename(filepath.Join(rootPath, "docs"), filepath.Join(rootPath, "saved")); renameErr != nil { + t.Fatal(renameErr) + } + if symlinkErr := os.Symlink("other", filepath.Join(rootPath, "docs")); symlinkErr != nil { + t.Fatal(symlinkErr) + } + }) + if !errors.Is(err, ErrRouteChanged) { + t.Fatalf("parent symlink ABA error=%v, want ErrRouteChanged", err) + } +} + +func TestOpenExactRegularFileRejectsFinalComponentABA(t *testing.T) { + tests := []struct { + name string + replace func(*testing.T, string) + }{ + { + name: "regular replacement", + replace: func(t *testing.T, directory string) { + t.Helper() + if err := os.WriteFile(filepath.Join(directory, "record.json"), []byte("replacement\n"), 0o644); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "symlink replacement", + replace: func(t *testing.T, directory string) { + t.Helper() + if err := os.Symlink("saved.json", filepath.Join(directory, "record.json")); err != nil { + t.Fatal(err) + } + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + rootPath := t.TempDir() + directory := filepath.Join(rootPath, "docs") + if err := os.Mkdir(directory, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(directory, "record.json"), []byte("canonical\n"), 0o644); err != nil { + t.Fatal(err) + } + root, err := os.OpenRoot(rootPath) + if err != nil { + t.Fatal(err) + } + defer root.Close() + + mutated := false + _, err = openExactRegularFile(root, "docs/record.json", func(componentIndex int) { + if componentIndex != 1 || mutated { + return + } + mutated = true + if renameErr := os.Rename(filepath.Join(directory, "record.json"), filepath.Join(directory, "saved.json")); renameErr != nil { + t.Fatal(renameErr) + } + test.replace(t, directory) + }) + if !errors.Is(err, ErrRouteChanged) { + t.Fatalf("final-component ABA error=%v, want ErrRouteChanged", err) + } + }) + } +} + +func TestExactEntryExistsDistinguishesMissingEntry(t *testing.T) { + root, err := os.OpenRoot(t.TempDir()) + if err != nil { + t.Fatal(err) + } + defer root.Close() + if exists, err := ExactEntryExists(root, ".", "missing"); err != nil || exists { + t.Fatalf("ExactEntryExists(missing) = %v, %v", exists, err) + } +} diff --git a/internal/kernel/rootpath/open_other.go b/internal/kernel/rootpath/open_other.go new file mode 100644 index 0000000..e00e03c --- /dev/null +++ b/internal/kernel/rootpath/open_other.go @@ -0,0 +1,14 @@ +//go:build !darwin && !linux + +package rootpath + +import ( + "fmt" + "os" +) + +// OpenExactRegularFile reports that descriptor-relative traversal is not +// available outside the package's supported runtime platforms. +func OpenExactRegularFile(*os.Root, string) (*os.File, error) { + return nil, fmt.Errorf("exact root file traversal requires darwin or linux") +} diff --git a/internal/kernel/rootpath/open_unix.go b/internal/kernel/rootpath/open_unix.go new file mode 100644 index 0000000..7cdb774 --- /dev/null +++ b/internal/kernel/rootpath/open_unix.go @@ -0,0 +1,141 @@ +//go:build darwin || linux + +package rootpath + +import ( + "errors" + "fmt" + "io/fs" + "os" + "path" + "strings" + + "golang.org/x/sys/unix" +) + +type traversalHook func(componentIndex int) + +type traversalOperations struct { + closeFD func(int) error + closeFile func(*os.File) error +} + +func nativeTraversalOperations() traversalOperations { + return traversalOperations{ + closeFD: unix.Close, + closeFile: func(file *os.File) error { return file.Close() }, + } +} + +// OpenExactRegularFile opens a repository-relative regular file without +// following a symlink in any route component. +func OpenExactRegularFile(root *os.Root, relativePath string) (*os.File, error) { + return openExactRegularFile(root, relativePath, nil) +} + +func openExactRegularFile(root *os.Root, relativePath string, hook traversalHook) (*os.File, error) { + return openExactRegularFileWithOperations(root, relativePath, hook, nativeTraversalOperations()) +} + +func openExactRegularFileWithOperations(root *os.Root, relativePath string, hook traversalHook, operations traversalOperations) (*os.File, error) { + if root == nil || relativePath == "" || path.IsAbs(relativePath) || path.Clean(relativePath) != relativePath { + return nil, fmt.Errorf("exact root file route is invalid") + } + if operations.closeFD == nil || operations.closeFile == nil { + return nil, fmt.Errorf("exact root file traversal operations are incomplete") + } + components := strings.Split(relativePath, "/") + for _, component := range components { + if component == "" || component == "." || component == ".." { + return nil, fmt.Errorf("exact root file route is invalid") + } + } + + current, err := root.Open(".") + if err != nil { + return nil, fmt.Errorf("open exact root file base") + } + for index, component := range components { + _, exists, err := exactDirectoryEntry(current, component) + if err != nil { + if closeErr := closeTraversalFile(operations, current); closeErr != nil { + return nil, closeErr + } + return nil, err + } + if !exists { + if closeErr := closeTraversalFile(operations, current); closeErr != nil { + return nil, closeErr + } + return nil, fs.ErrNotExist + } + var expected unix.Stat_t + if err := unix.Fstatat(int(current.Fd()), component, &expected, unix.AT_SYMLINK_NOFOLLOW); err != nil { + if closeErr := closeTraversalFile(operations, current); closeErr != nil { + return nil, closeErr + } + if errors.Is(err, unix.ENOENT) { + return nil, ErrRouteChanged + } + return nil, fmt.Errorf("inspect exact root file route") + } + last := index == len(components)-1 + kind := expected.Mode & unix.S_IFMT + if kind == unix.S_IFLNK || (!last && kind != unix.S_IFDIR) || (last && kind != unix.S_IFREG) { + if closeErr := closeTraversalFile(operations, current); closeErr != nil { + return nil, closeErr + } + return nil, ErrUnsafeRoute + } + if hook != nil { + hook(index) + } + flags := unix.O_RDONLY | unix.O_CLOEXEC | unix.O_NOFOLLOW | unix.O_NONBLOCK + if !last { + flags |= unix.O_DIRECTORY + } + fd, openErr := unix.Openat(int(current.Fd()), component, flags, 0) + if openErr != nil { + if closeErr := closeTraversalFile(operations, current); closeErr != nil { + return nil, closeErr + } + if errors.Is(openErr, unix.ENOENT) || errors.Is(openErr, unix.ELOOP) || errors.Is(openErr, unix.ENOTDIR) { + return nil, ErrRouteChanged + } + return nil, fmt.Errorf("open exact root file route") + } + var observed unix.Stat_t + if statErr := unix.Fstat(fd, &observed); statErr != nil || expected.Dev != observed.Dev || expected.Ino != observed.Ino { + if closeErr := errors.Join(closeTraversalFD(operations, fd), closeTraversalFile(operations, current)); closeErr != nil { + return nil, closeErr + } + return nil, ErrRouteChanged + } + next := os.NewFile(uintptr(fd), "exact-root-entry") + if next == nil { + if closeErr := errors.Join(closeTraversalFD(operations, fd), closeTraversalFile(operations, current)); closeErr != nil { + return nil, closeErr + } + return nil, fmt.Errorf("adopt exact root file descriptor") + } + if closeErr := closeTraversalFile(operations, current); closeErr != nil { + return nil, errors.Join(closeErr, closeTraversalFile(operations, next)) + } + current = next + } + return current, nil +} + +func closeTraversalFile(operations traversalOperations, file *os.File) error { + if err := operations.closeFile(file); err != nil { + return fmt.Errorf("%w: file descriptor", ErrTraversalCleanup) + } + return nil +} + +func closeTraversalFD(operations traversalOperations, descriptor int) error { + if err := operations.closeFD(descriptor); err != nil { + return fmt.Errorf("%w: raw descriptor", ErrTraversalCleanup) + } + return nil +} diff --git a/internal/tools/commandcontractgen/main.go b/internal/tools/commandcontractgen/main.go index e7150fd..53af9a6 100644 --- a/internal/tools/commandcontractgen/main.go +++ b/internal/tools/commandcontractgen/main.go @@ -169,7 +169,7 @@ func admitCommandRouteGrammar(raw any) error { if !ok { return errors.New("CLI processContract commandRouteGrammar must be an object") } - if err := rejectUnknownKeys(grammar, []string{"ambiguityPolicy", "maximumTokens", "minimumTokens", "separator", "tokenPattern"}, "CLI command route grammar"); err != nil { + if err := rejectUnknownKeys(grammar, []string{"ambiguityPolicy", "maximumTokens", "minimumTokens", "omittedRoutePolicy", "separator", "tokenPattern"}, "CLI command route grammar"); err != nil { return err } minimum, minimumOK := positiveJSONInteger(grammar["minimumTokens"]) @@ -177,7 +177,7 @@ func admitCommandRouteGrammar(raw any) error { if !minimumOK || !maximumOK || minimum != commandroute.MinimumTokens || maximum != commandroute.MaximumTokens { return fmt.Errorf("CLI command route grammar token bounds must be %d through %d", commandroute.MinimumTokens, commandroute.MaximumTokens) } - if grammar["separator"] != commandroute.Separator || grammar["tokenPattern"] != commandroute.TokenPattern || grammar["ambiguityPolicy"] != commandroute.AmbiguityPolicy { + if grammar["separator"] != commandroute.Separator || grammar["tokenPattern"] != commandroute.TokenPattern || grammar["ambiguityPolicy"] != commandroute.AmbiguityPolicy || grammar["omittedRoutePolicy"] != commandroute.OmittedRoutePolicy { return errors.New("CLI command route grammar does not match the native route owner") } return nil @@ -459,7 +459,7 @@ func admitCommands(root string, contract map[string]any, definitions map[string] return nil, nil, errors.New("CLI commands must be sorted and unique") } previous = name - route := []string{name} + var route []string if rawRoute, present := command["route"]; present { var err error route, err = stringList(rawRoute, "command "+name+" route") @@ -467,6 +467,11 @@ func admitCommands(root string, contract map[string]any, definitions map[string] return nil, nil, err } } + var routeOK bool + route, routeOK = commandroute.Resolve(name, route) + if !routeOK { + return nil, nil, fmt.Errorf("command %s has an invalid route", name) + } if err := admitCommandRoute(name, route, routes); err != nil { return nil, nil, err } diff --git a/internal/tools/commandcontractgen/main_test.go b/internal/tools/commandcontractgen/main_test.go index 5c8872c..ecaa4b3 100644 --- a/internal/tools/commandcontractgen/main_test.go +++ b/internal/tools/commandcontractgen/main_test.go @@ -209,6 +209,13 @@ func TestRenderRejectsIncompleteAndStaleCommandContracts(t *testing.T) { }, want: "does not match the native route owner", }, + { + name: "command route omission policy drift", + mutate: func(contract map[string]any) { + contract["processContract"].(map[string]any)["commandRouteGrammar"].(map[string]any)["omittedRoutePolicy"] = "unknown" + }, + want: "does not match the native route owner", + }, { name: "required input contract missing", mutate: func(contract map[string]any) { @@ -645,11 +652,12 @@ func writeFixture(t *testing.T) string { "packageName": "@research-engineering/agentic-proofkit", "processContract": map[string]any{ "commandRouteGrammar": map[string]any{ - "minimumTokens": 1, - "maximumTokens": 4, - "separator": " ", - "tokenPattern": `^[a-z0-9]+(?:-[a-z0-9]+)*$`, - "ambiguityPolicy": "no_route_is_prefix_of_another", + "minimumTokens": 1, + "maximumTokens": 4, + "separator": " ", + "tokenPattern": `^[a-z0-9]+(?:-[a-z0-9]+)*$`, + "ambiguityPolicy": "no_route_is_prefix_of_another", + "omittedRoutePolicy": "command_id", }, }, "contractDefinitions": definitions, diff --git a/internal/tools/coveragemetrics/main.go b/internal/tools/coveragemetrics/main.go index 4ffaeb0..b3e9f92 100644 --- a/internal/tools/coveragemetrics/main.go +++ b/internal/tools/coveragemetrics/main.go @@ -65,12 +65,13 @@ type bindingRequirement struct { } type bindingScenario struct { - CommandIDs []string `json:"commandIds"` - RequirementID string `json:"requirementId"` - ScenarioID string `json:"scenarioId"` - WitnessID string `json:"witnessId"` - WitnessPath string `json:"witnessPath"` - WitnessSelectors []witnessSelector `json:"witnessSelectors"` + CommandIDs []string `json:"commandIds"` + EnvironmentClasses []string `json:"environmentClasses"` + RequirementID string `json:"requirementId"` + ScenarioID string `json:"scenarioId"` + WitnessID string `json:"witnessId"` + WitnessPath string `json:"witnessPath"` + WitnessSelectors []witnessSelector `json:"witnessSelectors"` } type witnessSelector struct { @@ -252,547 +253,7 @@ func validateBindingWitnessSelectorsAtRoot(root string, bindings bindingFile) er } func validateRequiredBindingWitnessSelectors(bindings bindingFile) error { - type inventoryKey struct { - requirementID string - scenarioID string - } - required := map[inventoryKey][]string{ - {"REQ-PROOFKIT-WORKFLOW-001", "proofkit.agent-workflow.pure-single-admission-owner"}: {"TestWorkflowPurityPredicates"}, - {"REQ-PROOFKIT-WORKFLOW-002", "proofkit.agent-workflow.stage-prefix-and-terminal-relation"}: {"TestWorkflowStatePredicates"}, - {"REQ-PROOFKIT-WORKFLOW-003", "proofkit.agent-workflow.total-checkpoint-successor-relation"}: {"TestWorkflowCheckpointPredicates"}, - {"REQ-PROOFKIT-WORKFLOW-004", "proofkit.agent-workflow.review-identity-closure"}: {"TestWorkflowIdentityPredicates"}, - {"REQ-PROOFKIT-WORKFLOW-005", "proofkit.agent-workflow.reference-closed-bounded-context"}: {"TestWorkflowClosurePredicates"}, - {"REQ-PROOFKIT-WORKFLOW-006", "proofkit.agent-workflow.no-ambient-authority"}: {"TestWorkflowAmbientAuthorityPredicates"}, - {"REQ-PROOFKIT-WORKFLOW-007", "proofkit.agent-workflow.native-evidence-guidance-purity"}: {"TestGuidanceNoAmbientDependencyPredicates"}, - {"REQ-PROOFKIT-WORKFLOW-007", "proofkit.agent-workflow.native-evidence-guidance-slot-closure"}: {"TestGuidanceReferenceIsCompactAndOwnerBound", "TestGuidanceSlotPredicates"}, - {"REQ-PROOFKIT-WORKFLOW-008", "proofkit.agent-workflow.bounded-safe-text"}: {"TestWorkflowTerminalTextIsOperationallyComplete", "TestWorkflowTextPredicates"}, - {"REQ-PROOFKIT-WORKFLOW-008", "proofkit.agent-workflow.prompt-coordinate-and-escalation-closure"}: {"TestWorkflowPromptPredicates"}, - {"REQ-PROOFKIT-WORKFLOW-009", "proofkit.agent-workflow.cli-presentation-capability-product"}: {"TestAgentWorkflowCLITruthTable"}, - {"REQ-PROOFKIT-WORKFLOW-009", "proofkit.agent-workflow.style-strip-parity"}: {"TestWorkflowTextProjectionParity"}, - {"REQ-PROOFKIT-WORKFLOW-010", "proofkit.agent-workflow.catalog-prerequisite-causality"}: {"TestWorkflowStatePredicates"}, - {"REQ-PROOFKIT-WORKFLOW-010", "proofkit.agent-workflow.semantic-owner-minimality"}: {"TestGuidancePurityPredicates"}, - {"REQ-PROOFKIT-WORKFLOW-010", "proofkit.agent-workflow.semantic-owner-topology"}: {"TestAgentWorkflowSemanticOwnerTopology"}, - {"REQ-PROOFKIT-WORKFLOW-011", "proofkit.agent-workflow.installed-carrier-smoke-closure"}: { - "TestRunProcessCustomOutputLimitsAreExact", - "TestRunProcessRejectsInvalidCustomOutputLimitsBeforeStart", - "TestVerifyAcceptsApplicationCLI", - "TestVerifyRejectsCarrierContractMutations", - }, - {"REQ-PROOFKIT-WORKFLOW-011", "proofkit.agent-workflow.public-cli-relation-closure"}: {"TestAgentWorkflowCLITruthTable"}, - {"REQ-PROOFKIT-WORKFLOW-011", "proofkit.agent-workflow.version-edge-wire-observation"}: {"TestAgentWorkflowVersionEdgeClosesPublicWireAdditions"}, - {"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", - "TestAgentRouteEnvelopeModesUseExactRootShapes", - "TestRequirementAuthoringPlanOutputUsesVersionedRootShape", - "TestSelfCheckOutputUsesExactRootShape", - "TestStandaloneMultiVariantCommandsUseExactRootShapes", - }, - {"REQ-PROOFKIT-PACKAGE-002", "proofkit.package-boundary.adoption-materialization-output-root-witnesses"}: { - "TestAdoptMaterializeApplyOutputUsesExactRootShape", - "TestAdoptMaterializePlanOutputUsesExactRootShape", - "TestAdoptMaterializeRecoverOutputUsesExactRootShape", - }, - {"REQ-PROOFKIT-PACKAGE-003", "proofkit.package-boundary.outside-consumer-artifact"}: { - "TestExactTarballOnboardingTrace", - "TestInstalledCommandRouteBijectionBindsCommandIdentity", - "TestInstalledNPMCarrierIsExactRegularTarballProjection", - "TestVerifyPackedOwnerRecordsRejectsSourceArtifactContentDrift", - }, - {"REQ-PROOFKIT-PACKAGE-004", "proofkit.package-boundary.ci-receipt-anchor"}: { - "TestReceiptIDKeepsLocalAndCIIdentitiesDistinct", - "TestRunInvokesEveryRequiredSelfHostingAdmissionBoundary", - }, - {"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", - "TestInstalledPythonCarrierRejectsContractReplacementRemovalAndSymlink", - "TestInstalledPythonCommandRoutesRequireExactContractBijection", - "TestInstalledWheelContinuationUsesExactPythonModuleProfileWithoutNPM", - "TestPipInstallArgumentsAreIsolatedAndOffline", - "TestPythonVerificationEnvironmentRemovesAmbientImportControls", - }, - {"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", - "TestAgentRouteEnvelopeModesUseExactRootShapes", - "TestRequiredInputCommandsRouteStructuralErrorsByMode", - "TestRequirementAuthoringPlanOutputUsesVersionedRootShape", - "TestRequirementBrowserOneShotCLIOutputVariants", - "TestSelfCheckOutputUsesExactRootShape", - "TestStandaloneMultiVariantCommandsUseExactRootShapes", - }, - {"REQ-PROOFKIT-QUALITY-004", "proofkit.supply-chain-quality.adoption-materialization-cli-abi"}: { - "TestAdoptMaterializeApplyOutputUsesExactRootShape", - "TestAdoptMaterializePlanOutputUsesExactRootShape", - "TestAdoptMaterializeRecoverOutputUsesExactRootShape", - }, - {"REQ-PROOFKIT-QUALITY-001", "proofkit.supply-chain-quality.release-attestation-wiring"}: { - "TestReleaseWorkflowRetainsReleaseAssetAndPostCreateEvidenceClosure", - }, - {"REQ-PROOFKIT-QUALITY-001", "proofkit.supply-chain-quality.retained-evidence-manifest"}: { - "TestManifestRejectsUnboundAttestationAndSymlink", - "TestManifestUsesDownloadableArtifactPaths", - }, - {"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-004", "proofkit.supply-chain-quality.cli-output-schema-evolution"}: { - "TestRequirementCoverageViewBreakingRootUsesVersionedOutputContract", - }, - {"REQ-PROOFKIT-QUALITY-005", "proofkit.supply-chain-quality.codeql-permission-separation"}: { - "TestSecurityScannerWorkflowsSeparateProviderPublicationPermissions", - }, - {"REQ-PROOFKIT-QUALITY-006", "proofkit.supply-chain-quality.osv-permission-separation"}: { - "TestOSVSourceScanFailsForEveryNonzeroScannerStatus", - "TestSecurityScannerWorkflowsSeparateProviderPublicationPermissions", - }, - {"REQ-PROOFKIT-QUALITY-007", "proofkit.supply-chain-quality.scorecard-permission-and-publication-inputs"}: { - "TestScorecardPublicPublishDeclaresRequiredOutputInputs", - "TestSecurityScannerWorkflowsSeparateProviderPublicationPermissions", - }, - {"REQ-PROOFKIT-QUALITY-010", "proofkit.supply-chain-quality.artifact-file-boundary"}: { - "TestOperationsRejectFinalSymlinkWithoutTargetMutation", - "TestOperationsRejectSymlinkComponentsWithoutOutsideMutation", - "TestReadBoundedRejectsUnrepresentableLimit", - "TestWriteReadAndRemoveRoundTrip", - }, - {"REQ-PROOFKIT-QUALITY-010", "proofkit.supply-chain-quality.artifact-file-nonblocking-open"}: { - "TestReadBoundedRejectsFIFOWithoutBlocking", - }, - {"REQ-PROOFKIT-QUALITY-010", "proofkit.supply-chain-quality.coverage-metrics"}: { - "TestEachCommandRouteClosureConjunctHasIndependentFalsifier", - "TestEachLinkageDeadZoneConjunctHasIndependentFalsifier", - "TestInvalidateMetricsFileRejectsSymlinkParentWithoutDeletingOutsideFile", - "TestWriteMetricsFileRejectsSymlinkEscapeWithoutMutation", - }, - {"REQ-PROOFKIT-QUALITY-010", "proofkit.supply-chain-quality.command-oracle-execution-ledger"}: { - "TestExecuteBindsMaterializedSourceCandidatesAndRuntimeEvents", - "TestRunGoTestCommandTerminatesImmediatelyWhenStderrExceedsBound", - "TestRunGoTestsDoesNotExecuteCrossPackageNameMatches", - "TestRunGoTestsTerminatesOnContextDeadline", - "TestValidateCurrentRejectsProducerUnreachableCandidateProjection", - }, - {"REQ-PROOFKIT-QUALITY-010", "proofkit.supply-chain-quality.command-oracle-counterfeit-corpus"}: { - "TestCounterfeitCorpusClosesRequiredAxes", - "TestCounterfeitCorpusClosureRejectsMissingRequiredAxes", - "TestEachCounterfeitCaseProducesItsCheckedInDecision", - }, - {"REQ-PROOFKIT-QUALITY-010", "proofkit.supply-chain-quality.command-oracle-source-snapshot"}: { - "TestCaptureContextTerminatesCanceledGitProcessGroup", - "TestCaptureContextTerminatesGitProcessGroupOnOutputOverflow", - "TestCaptureRejectsSuccessfulGitDiagnosticsWithoutEcho", - "TestMaterializeBindsCopiedBytesAndRejectsLiveMutation", - "TestMaterializeRejectsSymlinkAndNonEmptyDestination", - "TestMaterializeRejectsSymlinkedDestinationInsideSource", - "TestValidRevisionAdmitsOnlyGitObjectIdentityAndOptionalSnapshotDigest", - "TestValidateMaterializedRejectsSurplusFile", - }, - {"REQ-PROOFKIT-QUALITY-010", "proofkit.supply-chain-quality.binding-selector-executability"}: { - "TestBindingWitnessSelectorsAcceptUnnamedGoTestParameter", - "TestBindingWitnessSelectorsRejectInvalidGoTestSignature", - "TestBindingWitnessSelectorsRejectMissingSemanticOwner", - "TestBindingWitnessSelectorsRejectNonTestAndBuildExcludedFiles", - "TestBindingWitnessSelectorsRejectVacuousTestBody", - "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", - "TestInstalledNPMCarrierIsExactRegularTarballProjection", - "TestInstalledREADMEFirstInputPreservesJSONExampleBytes", - "TestInstalledREADMEFirstInputUsesBoundedLiteralShellWords", - "TestLiteralShellWordsConsumesLongBackslashRun", - "TestOnboardingTraceCoversEveryDiscoveredPresetAndREADMEInput", - "TestVerifyPackedOwnerRecordsRejectsSourceArtifactContentDrift", - }, - {"REQ-PROOFKIT-QUALITY-023", "proofkit.supply-chain-quality.release-closeout-npm-byte-admission"}: { - "TestBuildInputFailsClosedForEachBlockingEvidenceClass", - "TestPackRecordBytesMatchEnforcesByteLimit", - }, - {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.release-manifest-json-abi-registry-evidence"}: { - "TestNPMRegistryAuthorityFlowsFromAdmittedFileToPublishedChannel", - "TestNPMRegistryPublicationRequiresTypedAuthorityEvidence", - }, - {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.npm-registry-authority-producer"}: { - "TestRunBuildsCanonicalTypedRegistryEvidence", - "TestRunRejectsRegistryPackageSetSubstitution", - }, - {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.npm-registry-workflow-delegation"}: { - "TestReleaseWorkflowDelegatesNPMRegistryEvidenceToRepositoryOwner", - }, - {"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-023", "proofkit.supply-chain-quality.python-wheel-resource-bounds"}: { - "TestVerifyWheelContentsRejectsOversizedCompressedEntryBeforeDecompression", - "TestVerifyWheelContentsRejectsOversizedEntryBeforeDecompression", - }, - {"REQ-PROOFKIT-QUALITY-023", "proofkit.supply-chain-quality.wrapper-platform-bijection"}: { - "TestWrapperScriptRoutesEveryReleasePlatformTarget", - }, - {"REQ-PROOFKIT-QUALITY-015", "proofkit.supply-chain-quality.release-closeout-completion-criteria"}: { - "TestBuildInputFailsClosedForEachBlockingEvidenceClass", - "TestSelfEvidenceInvokesCurrentCommandOracleOwner", - "TestSelfEvidenceRejectsProducerUnreachableCommandOracleRef", - }, - {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.release-change-record-projection"}: { - "TestAdmitEnforcesVersionedChangeClass", - "TestCurrentChangeRecordNamesReviewedSemanticChanges", - "TestRenderStatesPreOneExactPinPolicy", - }, - {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.retained-evidence-artifact-topology"}: { - "TestVerifyRejectsManifestAddressDrift", - }, - {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.release-closeout-change-record"}: { - "TestBuildInputFailsClosedForEachBlockingEvidenceClass", - }, - {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.release-predecessor-lineage"}: { - "TestRunNPMLineageUsesAdmittedRecordAndProviderIdentity", - "TestValidateNPMReleaseLineage", - }, - {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.release-predecessor-lineage-workflow"}: { - "TestReleaseWorkflowCandidateEvidenceAllowsExistingNPMByteMatch", - }, - {"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-007", "proofkit.spec-proof-core.canonical-command-input-admission"}: { - "TestRequiredInputCommandsRejectMalformedCallerRecords", - }, - {"REQ-PROOFKIT-SPEC-007", "proofkit.spec-proof-core.canonical-input-admission"}: { - "TestDecodeTypedJSONUsesStrictAdmission", - }, - {"REQ-PROOFKIT-SPEC-013", "proofkit.spec-proof-core.receipt-trust-status-vocabulary-admission"}: { - "TestBuildRejectsHigherRankThatWeakensMinimumTrustSemantics", - }, - {"REQ-PROOFKIT-SPEC-021", "proofkit.spec-proof-core.requirement-browser-one-shot-cleanup"}: { - "TestServeOneShotDoesNotReadCompletedDoneTwice", - "TestServeOneShotReturnsCleanupFailuresWithoutWritingTerminalPacket", - "TestServeOneShotWaitsForDoneBeforeWritingTerminalPacket", - }, - {"REQ-PROOFKIT-SPEC-006", "proofkit.spec-proof-core.test-inventory-and-coverage-view"}: { - "TestAdmitOutputRejectsCompactProjectionDrift", - "TestAdmitOutputRejectsMissingInverseParentProjection", - "TestAdmitOutputRejectsNonCanonicalWireProjectionText", - "TestAdmitOutputRejectsRemovedValidUnmappedInventoryEntry", - "TestAdmitOutputReplaysFailedInventoryQualitySemantics", - "TestAdmitOutputReplaysFullRepositorySourceOwnerScopeFailures", - "TestAdmitOutputReplaysOwnerScopeFailures", - "TestAdmitOutputRequiresEveryCoverageBasisField", - "TestAdmitOutputRequiresEveryDeclaredRootField", - "TestAdmitOutputRetainsFailedInventoryEntriesWithoutProjectedParents", - "TestAdmitOutputValidatesEveryCoverageRowMetadataField", - }, - {"REQ-PROOFKIT-SPEC-006", "proofkit.spec-proof-core.declared-route-mapping-without-assurance"}: { - "TestBuildJSONMissingSelectorRemainsMappingOnly", - }, - {"REQ-PROOFKIT-SPEC-012", "proofkit.spec-proof-core.requirement-authoring-ref-provenance"}: { - "TestBuildPreservesDigestBoundAuthoringRefIdentity", - }, - {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-brief-cli-abi"}: { - "TestAgentRouteEnvelopeModesUseExactRootShapes", - }, - {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-brief-projection"}: { - "TestAgentBriefBindsLauncherContextThatAffectsReportDigest", - "TestAgentBriefClosesEverySelectedCommandInputReference", - "TestAgentBriefCompactsAtDeclaredByteBoundary", - "TestAgentBriefIsBoundedAndFullEnvelopeRemainsAvailable", - "TestAgentBriefNamesCompleteInputBundleBlocker", - "TestAgentBriefPreservesBlockedRouteOmissionsAndUnknownReportBlockers", - "TestBriefBlockerBoundDominatesMapMaterialization", - "TestBuildEnvelopeCapsBlockersAndCountsOmittedDetails", - "TestBuildEnvelopeCompactsOversizedArgvWithoutLosingActionIdentity", - }, - {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-flag-pre-read-admission"}: { - "TestAgentRouteModeAdmissionPrecedesInputRead", - }, - {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-report-contract-closure"}: { - "TestAgentRouteOutputContractPreservesReportSemantics", - }, - {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-brief-version-edge"}: { - "TestAgentRouteVersionEdgeClosesBriefDefaultMigration", - }, - {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-materialized-ref-admission"}: { - "TestBuildRejectsStdinTransportSentinelAsArtifactReference", - }, - {"REQ-PROOFKIT-SPEC-028", "proofkit.spec-proof-core.adoption-inventory-boundary"}: { - "TestCatalogRolePolicyIsExact", - "TestInventoryIdentityBindsEverySemanticOperand", - "TestInventoryOutputByteLimitIsExact", - "TestReadRootInventoryClassifiesPartialBatchesWithoutRetainingUnknownNames", - "TestScanDoesNotFollowUnknownSymlink", - "TestScanEnforcesPreflightBoundsAndExplicitOmissions", - "TestScanPolicyBoundariesAreExact", - "TestScanProducesBoundedClosedInventory", - "TestScanRejectsRecognizedSymlinkWithoutReadingTarget", - "TestUnsupportedPlatformFailsBeforeOpeningRepositoryRoot", - }, - {"REQ-PROOFKIT-SPEC-028", "proofkit.spec-proof-core.adoption-inventory-nonblocking-open"}: { - "TestScanRejectsFIFOReplacementWithoutBlocking", - }, - {"REQ-PROOFKIT-SPEC-029", "proofkit.spec-proof-core.adoption-plan-authority-closure"}: { - "TestBuildRejectsUnknownIntentPresetAndForgedInventory", - "TestBuildSeparatesAdoptionIntentFromCandidateAuthority", - "TestBuildStackHintCannotChangeIntentTrustOrTasks", - "TestPlanIdentityBindsIntentAndInventory", - "TestPlanWireAdmissionIsDeterministicAndOwnerClosed", - }, - {"REQ-PROOFKIT-SPEC-029", "proofkit.spec-proof-core.adoption-plan-observational-stack"}: { - "TestPlanKeepsRepositoryClassesObservationalAndStackNeutral", - }, - {"REQ-PROOFKIT-SPEC-029", "proofkit.spec-proof-core.adoption-guidance-reference-closure"}: { - "TestGuidanceReferenceIsCompactAndOwnerBound", - }, - {"REQ-PROOFKIT-SPEC-030", "proofkit.spec-proof-core.adoption-plan-presentation-closure"}: { - "TestAdoptionPlanOutputAndTextBoundsAreExact", - "TestTextProjectionPreservesJSONPlanSemantics", - }, - {"REQ-PROOFKIT-SPEC-027", "proofkit.spec-proof-core.adoption-front-door-whole-cli"}: { - "TestAdoptionFrontDoorCLI", - }, - {"REQ-PROOFKIT-SPEC-018", "proofkit.spec-proof-core.command-route-contract-closure"}: { - "TestCommandRoutesAreBoundedSafeAndUnambiguous", - "TestRenderRejectsIncompleteAndStaleCommandContracts", - }, - {"REQ-PROOFKIT-SPEC-018", "proofkit.spec-proof-core.command-route-generated-adapter"}: { - "TestGeneratedSourceAdmitsBoundedCanonicalCommandRoutes", - }, - {"REQ-PROOFKIT-SPEC-018", "proofkit.spec-proof-core.command-route-installed-contract"}: { - "TestAdmitCommandRouteTokenBoundariesAreExact", - "TestAdmitRequiresExactCommandRouteGrammarProjection", - }, - {"REQ-PROOFKIT-SPEC-018", "proofkit.spec-proof-core.command-route-kernel-owner"}: { - "TestGrammarBoundariesAreExact", - "TestParseRequiresCanonicalSeparatorAndRoundTrip", - }, - {"REQ-PROOFKIT-SPEC-031", "proofkit.spec-proof-core.adoption-version-edge-closure"}: { - "TestAdoptionFrontDoorVersionEdgeClosesInitRetirement", - "TestAdoptionFrontDoorVersionEdgeRejectsDigestBoundInventoryContradiction", - "TestRetiredInitRouteHasNoPublicDispatcher", - }, - {"REQ-PROOFKIT-SPEC-032", "proofkit.spec-proof-core.adoption-materialization-owner-closure"}: { - "TestApplyBlocksStaleMutationButAcceptsLostAcknowledgementRetry", - "TestMaterializationOutputAdmissionRejectsCrossOwnerMutants", - "TestMaterializationRejectsCrossRecordDriftAndManifestMutation", - "TestMaterializationWholeChainIsCanonicalAndOwnerClosed", - "TestReceiptAdmissionRejectsOperationAttributionMutants", - }, - {"REQ-PROOFKIT-SPEC-032", "proofkit.spec-proof-core.adoption-materialization-reference-closure"}: { - "TestInventoryReferencesMustResolveThroughBindingEdges", - "TestManifestAdmissionEqualsProducerImage", - "TestPathRoleLedgerRejectsWriteReferenceCollisions", - "TestRequirementProjectionRequiresClaimLevelParity", - }, - {"REQ-PROOFKIT-SPEC-032", "proofkit.spec-proof-core.adoption-materialization-whole-cli"}: { - "TestAdoptionMaterializationCLI", - }, - {"REQ-PROOFKIT-SPEC-033", "proofkit.spec-proof-core.repository-transaction-fault-recovery"}: { - "TestApplyAlreadySatisfiedRejectsConcurrentCooperativeWriter", - "TestApplyFaultAfterFirstPublishRestoresExactBeforeState", - "TestApplyRejectsConcurrentCooperativeWriter", - "TestProcessInterruptionAtEveryMutationBoundaryIsRecoverable", - "TestRecoverDoesNotInventIdentityForPartialPreparingJournal", - "TestTransactionLockIsInterprocess", - }, - {"REQ-PROOFKIT-SPEC-033", "proofkit.spec-proof-core.repository-transaction-output-relations"}: { - "TestPlanAndResultOutputAdmissionRejectSemanticMutants", - }, - {"REQ-PROOFKIT-SPEC-033", "proofkit.spec-proof-core.repository-transaction-cleanup-state-matrix"}: { - "TestCleanupDurabilityFailureDoesNotClaimRecoverableState", - }, - {"REQ-PROOFKIT-SPEC-033", "proofkit.spec-proof-core.repository-transaction-filesystem-portable-path-identity"}: { - "TestBuildPlanRejectsFilesystemPortableAliases", - }, - {"REQ-PROOFKIT-SPEC-033", "proofkit.spec-proof-core.repository-transaction-portable-path-identity"}: { - "TestPortableEquivalenceAndContainment", - }, - {"REQ-PROOFKIT-SPEC-033", "proofkit.spec-proof-core.repository-transaction-terminal-state"}: { - "TestAppliedTerminalReceiptReplaysCompleteResult", - "TestApplyExecutesFrozenPlan", - "TestCommittedRecoveryRejectsRollback", - "TestMalformedRecoveryActionBlocksMutation", - "TestPreparingFailureCannotClaimRollbackAfterTargetDivergence", - "TestPreparingRecoveryRejectsResumeBeforeActionSelection", - "TestPreparingRollbackAtomicallyReplacesPreviousTerminalReceipt", - "TestReadyReplacementRetiresPreviousReceiptAndPreservesCompleteResult", - "TestRecoveryActionAndTerminalReceiptAreStable", - "TestRecoveryActionIsDurableBeforeDirectionalMutation", - "TestUnknownRecoveryStateDoesNotAdoptExpectedIdentity", - }, - {"REQ-PROOFKIT-SPEC-034", "proofkit.spec-proof-core.adoption-materialization-version-edge"}: { - "TestAdoptionMaterializationVersionEdgeClosesPublicCommands", - "TestAdoptionMaterializationVersionEdgePreservesFrozenPredecessor", - "TestAdoptionMaterializationVersionEdgeRejectsCoordinatedChangeRecordDrift", - }, - {"REQ-PROOFKIT-RETIRE-006", "proofkit.consumer-infra-retirement.migration-parity-admission"}: { - "TestBuildProjectsEveryCallerDeclaredStatusAndSummaryField", - }, - } - requiredPaths := map[inventoryKey]string{ - {"REQ-PROOFKIT-WORKFLOW-001", "proofkit.agent-workflow.pure-single-admission-owner"}: "internal/command/changeworkflowplan/change_workflow_plan_test.go", - {"REQ-PROOFKIT-WORKFLOW-002", "proofkit.agent-workflow.stage-prefix-and-terminal-relation"}: "internal/command/changeworkflowplan/state_test.go", - {"REQ-PROOFKIT-WORKFLOW-003", "proofkit.agent-workflow.total-checkpoint-successor-relation"}: "internal/command/changeworkflowplan/state_test.go", - {"REQ-PROOFKIT-WORKFLOW-004", "proofkit.agent-workflow.review-identity-closure"}: "internal/command/changeworkflowplan/admission_test.go", - {"REQ-PROOFKIT-WORKFLOW-005", "proofkit.agent-workflow.reference-closed-bounded-context"}: "internal/command/changeworkflowplan/context_closure_test.go", - {"REQ-PROOFKIT-WORKFLOW-006", "proofkit.agent-workflow.no-ambient-authority"}: "internal/command/changeworkflowplan/dependency_test.go", - {"REQ-PROOFKIT-WORKFLOW-007", "proofkit.agent-workflow.native-evidence-guidance-purity"}: "internal/command/nativeevidenceguidance/dependency_test.go", - {"REQ-PROOFKIT-WORKFLOW-007", "proofkit.agent-workflow.native-evidence-guidance-slot-closure"}: "internal/command/nativeevidenceguidance/guidance_test.go", - {"REQ-PROOFKIT-WORKFLOW-008", "proofkit.agent-workflow.bounded-safe-text"}: "internal/command/changeworkflowplan/text_test.go", - {"REQ-PROOFKIT-WORKFLOW-008", "proofkit.agent-workflow.prompt-coordinate-and-escalation-closure"}: "internal/command/changeworkflowplan/prompt_test.go", - {"REQ-PROOFKIT-WORKFLOW-009", "proofkit.agent-workflow.cli-presentation-capability-product"}: "internal/app/agent_workflow_command_test.go", - {"REQ-PROOFKIT-WORKFLOW-009", "proofkit.agent-workflow.style-strip-parity"}: "internal/command/changeworkflowplan/text_test.go", - {"REQ-PROOFKIT-WORKFLOW-010", "proofkit.agent-workflow.catalog-prerequisite-causality"}: "internal/command/changeworkflowplan/state_test.go", - {"REQ-PROOFKIT-WORKFLOW-010", "proofkit.agent-workflow.semantic-owner-minimality"}: "internal/command/nativeevidenceguidance/guidance_test.go", - {"REQ-PROOFKIT-WORKFLOW-010", "proofkit.agent-workflow.semantic-owner-topology"}: "internal/app/agent_workflow_topology_test.go", - {"REQ-PROOFKIT-WORKFLOW-011", "proofkit.agent-workflow.installed-carrier-smoke-closure"}: "internal/tools/workflowsmoke/workflow_smoke_test.go", - {"REQ-PROOFKIT-WORKFLOW-011", "proofkit.agent-workflow.public-cli-relation-closure"}: "internal/app/agent_workflow_command_test.go", - {"REQ-PROOFKIT-WORKFLOW-011", "proofkit.agent-workflow.version-edge-wire-observation"}: "internal/app/agent_workflow_version_edge_test.go", - {"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-002", "proofkit.package-boundary.adoption-materialization-output-root-witnesses"}: "internal/app/adoption_materialization_command_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-001", "proofkit.supply-chain-quality.release-attestation-wiring"}: "scripts/validate-self-hosting-receipts_test.go", - {"REQ-PROOFKIT-QUALITY-001", "proofkit.supply-chain-quality.retained-evidence-manifest"}: "internal/tools/retainedevidence/manifest_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.adoption-materialization-cli-abi"}: "internal/app/adoption_materialization_command_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-004", "proofkit.supply-chain-quality.cli-output-schema-evolution"}: "internal/app/cli_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.artifact-file-boundary"}: "internal/tools/artifactfile/file_test.go", - {"REQ-PROOFKIT-QUALITY-010", "proofkit.supply-chain-quality.artifact-file-nonblocking-open"}: "internal/tools/artifactfile/file_unix_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-010", "proofkit.supply-chain-quality.command-oracle-execution-ledger"}: "internal/tools/commandoracle/execute_test.go", - {"REQ-PROOFKIT-QUALITY-010", "proofkit.supply-chain-quality.command-oracle-counterfeit-corpus"}: "internal/tools/commandoracle/corpus_test.go", - {"REQ-PROOFKIT-QUALITY-010", "proofkit.supply-chain-quality.command-oracle-source-snapshot"}: "internal/tools/repositorysnapshot/snapshot_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-023", "proofkit.supply-chain-quality.release-closeout-npm-byte-admission"}: "internal/tools/releasecloseoutinput/main_test.go", - {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.release-manifest-json-abi-registry-evidence"}: "internal/tools/releasemanifest/main_test.go", - {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.npm-registry-authority-producer"}: "internal/tools/npmregistry/main_test.go", - {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.npm-registry-workflow-delegation"}: "scripts/validate-self-hosting-receipts_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-023", "proofkit.supply-chain-quality.python-wheel-resource-bounds"}: "internal/tools/pythonpackage/metadata_test.go", - {"REQ-PROOFKIT-QUALITY-023", "proofkit.supply-chain-quality.wrapper-platform-bijection"}: "internal/tools/packagebuild/main_test.go", - {"REQ-PROOFKIT-QUALITY-015", "proofkit.supply-chain-quality.release-closeout-completion-criteria"}: "internal/tools/releasecloseoutinput/main_test.go", - {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.release-change-record-projection"}: "internal/tools/releasechange/record_test.go", - {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.retained-evidence-artifact-topology"}: "internal/tools/retainedevidence/manifest_test.go", - {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.release-closeout-change-record"}: "internal/tools/releasecloseoutinput/main_test.go", - {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.release-predecessor-lineage"}: "internal/tools/releasepreflight/main_test.go", - {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.release-predecessor-lineage-workflow"}: "scripts/validate-self-hosting-receipts_test.go", - {"REQ-PROOFKIT-QUALITY-025", "proofkit.supply-chain-quality.workflow-source-oracles"}: "scripts/workflow_source_oracles_test.go", - {"REQ-PROOFKIT-RETIRE-006", "proofkit.consumer-infra-retirement.migration-parity-admission"}: "internal/command/migrationparityadmission/migrationparityadmission_test.go", - {"REQ-PROOFKIT-SPEC-011", "proofkit.spec-proof-core.adoption-contract-envelope-cli-abi"}: "internal/app/cli_abi_test.go", - {"REQ-PROOFKIT-SPEC-007", "proofkit.spec-proof-core.canonical-command-input-admission"}: "internal/app/command_coverage_test.go", - {"REQ-PROOFKIT-SPEC-007", "proofkit.spec-proof-core.canonical-input-admission"}: "internal/kernel/admission/json_test.go", - {"REQ-PROOFKIT-SPEC-013", "proofkit.spec-proof-core.receipt-trust-status-vocabulary-admission"}: "internal/command/receipttrustclass/receipt_trust_class_test.go", - {"REQ-PROOFKIT-SPEC-021", "proofkit.spec-proof-core.requirement-browser-one-shot-cleanup"}: "internal/command/requirementbrowser/server_test.go", - {"REQ-PROOFKIT-SPEC-006", "proofkit.spec-proof-core.test-inventory-and-coverage-view"}: "internal/command/requirementcoverageview/output_closure_test.go", - {"REQ-PROOFKIT-SPEC-006", "proofkit.spec-proof-core.declared-route-mapping-without-assurance"}: "internal/command/requirementcoverageview/requirementcoverageview_test.go", - {"REQ-PROOFKIT-SPEC-012", "proofkit.spec-proof-core.requirement-authoring-ref-provenance"}: "internal/command/requirementauthoringplan/requirement_authoring_plan_test.go", - {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-brief-cli-abi"}: "internal/app/cli_abi_test.go", - {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-brief-projection"}: "internal/command/agentroute/brief_test.go", - {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-brief-version-edge"}: "internal/app/agent_route_version_edge_test.go", - {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-flag-pre-read-admission"}: "internal/app/app_test.go", - {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-materialized-ref-admission"}: "internal/command/agentroute/agentroute_test.go", - {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-report-contract-closure"}: "internal/app/cli_contract_test.go", - {"REQ-PROOFKIT-SPEC-028", "proofkit.spec-proof-core.adoption-inventory-boundary"}: "internal/command/repositoryinventory/repositoryinventory_test.go", - {"REQ-PROOFKIT-SPEC-028", "proofkit.spec-proof-core.adoption-inventory-nonblocking-open"}: "internal/command/repositoryinventory/fifo_unix_test.go", - {"REQ-PROOFKIT-SPEC-029", "proofkit.spec-proof-core.adoption-plan-authority-closure"}: "internal/command/adoptionplan/adoptionplan_test.go", - {"REQ-PROOFKIT-SPEC-029", "proofkit.spec-proof-core.adoption-plan-observational-stack"}: "internal/command/adoptionplan/repository_classes_test.go", - {"REQ-PROOFKIT-SPEC-029", "proofkit.spec-proof-core.adoption-guidance-reference-closure"}: "internal/command/nativeevidenceguidance/guidance_test.go", - {"REQ-PROOFKIT-SPEC-030", "proofkit.spec-proof-core.adoption-plan-presentation-closure"}: "internal/command/adoptionplan/adoptionplan_test.go", - {"REQ-PROOFKIT-SPEC-027", "proofkit.spec-proof-core.adoption-front-door-whole-cli"}: "internal/app/adoption_front_door_command_test.go", - {"REQ-PROOFKIT-SPEC-018", "proofkit.spec-proof-core.command-route-contract-closure"}: "internal/tools/commandcontractgen/main_test.go", - {"REQ-PROOFKIT-SPEC-018", "proofkit.spec-proof-core.command-route-generated-adapter"}: "internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source_test.go", - {"REQ-PROOFKIT-SPEC-018", "proofkit.spec-proof-core.command-route-installed-contract"}: "internal/tools/installedclicontract/contract_test.go", - {"REQ-PROOFKIT-SPEC-018", "proofkit.spec-proof-core.command-route-kernel-owner"}: "internal/kernel/commandroute/route_test.go", - {"REQ-PROOFKIT-SPEC-031", "proofkit.spec-proof-core.adoption-version-edge-closure"}: "internal/app/adoption_front_door_version_edge_test.go", - {"REQ-PROOFKIT-SPEC-032", "proofkit.spec-proof-core.adoption-materialization-owner-closure"}: "internal/command/adoptionmaterialization/adoptionmaterialization_test.go", - {"REQ-PROOFKIT-SPEC-032", "proofkit.spec-proof-core.adoption-materialization-reference-closure"}: "internal/command/adoptionmaterialization/closure_test.go", - {"REQ-PROOFKIT-SPEC-032", "proofkit.spec-proof-core.adoption-materialization-whole-cli"}: "internal/app/adoption_materialization_command_test.go", - {"REQ-PROOFKIT-SPEC-033", "proofkit.spec-proof-core.repository-transaction-fault-recovery"}: "internal/kernel/repositorytransaction/transaction_test.go", - {"REQ-PROOFKIT-SPEC-033", "proofkit.spec-proof-core.repository-transaction-output-relations"}: "internal/kernel/repositorytransaction/output_admission_test.go", - {"REQ-PROOFKIT-SPEC-033", "proofkit.spec-proof-core.repository-transaction-cleanup-state-matrix"}: "internal/kernel/repositorytransaction/state_machine_test.go", - {"REQ-PROOFKIT-SPEC-033", "proofkit.spec-proof-core.repository-transaction-filesystem-portable-path-identity"}: "internal/kernel/repositorytransaction/plan_test.go", - {"REQ-PROOFKIT-SPEC-033", "proofkit.spec-proof-core.repository-transaction-portable-path-identity"}: "internal/kernel/pathidentity/pathidentity_test.go", - {"REQ-PROOFKIT-SPEC-033", "proofkit.spec-proof-core.repository-transaction-terminal-state"}: "internal/kernel/repositorytransaction/invariant_test.go", - {"REQ-PROOFKIT-SPEC-034", "proofkit.spec-proof-core.adoption-materialization-version-edge"}: "internal/app/adoption_materialization_version_edge_test.go", - } - if len(requiredPaths) != len(required) { - return fmt.Errorf("required selector path inventory=%d, selector inventory=%d", len(requiredPaths), len(required)) - } + required := requiredBindingWitnessInventory() seenRequired := map[inventoryKey]struct{}{} for _, binding := range bindings.Bindings { key := inventoryKey{requirementID: binding.RequirementID, scenarioID: binding.ScenarioID} @@ -806,12 +267,14 @@ func validateRequiredBindingWitnessSelectors(bindings bindingFile) error { if _, duplicate := seenRequired[key]; duplicate { return fmt.Errorf("required independent-falsifier binding is duplicated: %s/%s", binding.RequirementID, binding.ScenarioID) } - wantPath, hasRequiredPath := requiredPaths[key] - if !hasRequiredPath { - return fmt.Errorf("binding %s has selectors but no exact witness path inventory", binding.ScenarioID) + if binding.WitnessPath != want.witnessPath { + return fmt.Errorf("binding %s witness path=%q, want exact %q", binding.ScenarioID, binding.WitnessPath, want.witnessPath) } - if binding.WitnessPath != wantPath { - return fmt.Errorf("binding %s witness path=%q, want exact %q", binding.ScenarioID, binding.WitnessPath, wantPath) + if want.commandIDs != nil && !equalStrings(binding.CommandIDs, want.commandIDs) { + return fmt.Errorf("binding %s commandIds=%v, want exact %v", binding.ScenarioID, binding.CommandIDs, want.commandIDs) + } + if want.environmentClasses != nil && !equalStrings(binding.EnvironmentClasses, want.environmentClasses) { + return fmt.Errorf("binding %s environmentClasses=%v, want exact %v", binding.ScenarioID, binding.EnvironmentClasses, want.environmentClasses) } seenRequired[key] = struct{}{} got := make([]string, 0, len(binding.WitnessSelectors)) @@ -819,14 +282,11 @@ func validateRequiredBindingWitnessSelectors(bindings bindingFile) error { 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) + if !equalStrings(got, want.selectors) { + return fmt.Errorf("binding %s witness selectors=%v, want %v", binding.ScenarioID, got, want.selectors) } } 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) } diff --git a/internal/tools/coveragemetrics/main_test.go b/internal/tools/coveragemetrics/main_test.go index 108ffcd..f2b7c86 100644 --- a/internal/tools/coveragemetrics/main_test.go +++ b/internal/tools/coveragemetrics/main_test.go @@ -597,6 +597,15 @@ func TestBindingWitnessSelectorsRequireExactCriticalInventories(t *testing.T) { "proofkit.agent-workflow.native-evidence-guidance-slot-closure", "proofkit.agent-workflow.no-ambient-authority", "proofkit.agent-workflow.prompt-coordinate-and-escalation-closure", + "proofkit.agent-workflow.project-navigation-installed-carriers", + "proofkit.agent-workflow.project-navigation-public-cli", + "proofkit.agent-workflow.project-navigation-version-edge", + "proofkit.agent-workflow.project-next-action-output-closure", + "proofkit.agent-workflow.project-state-bounded-inspection", + "proofkit.agent-workflow.project-state-child-owner-delegation", + "proofkit.agent-workflow.project-state-control-file-coherence", + "proofkit.agent-workflow.project-state-exact-route-traversal", + "proofkit.agent-workflow.project-state-total-classification", "proofkit.agent-workflow.public-cli-relation-closure", "proofkit.agent-workflow.pure-single-admission-owner", "proofkit.agent-workflow.reference-closed-bounded-context", @@ -720,6 +729,24 @@ func TestBindingWitnessSelectorsRequireExactCriticalInventories(t *testing.T) { t.Fatalf("command-drift error=%v", err) } }) + if scenarioID == "proofkit.agent-workflow.project-navigation-installed-carriers" { + t.Run(scenarioID+"/execution-command-class-drift", func(t *testing.T) { + mutated := cloneBindingFile(bindings) + mutated.Bindings[index].CommandIDs = []string{"proofkit.go-test"} + err := validateBindingWitnessSelectorsAtRoot(root, mutated) + if err == nil || !strings.Contains(err.Error(), "commandIds=") { + t.Fatalf("command-id drift error=%v", err) + } + }) + t.Run(scenarioID+"/environment-class-drift", func(t *testing.T) { + mutated := cloneBindingFile(bindings) + mutated.Bindings[index].EnvironmentClasses = []string{"local-go"} + err := validateBindingWitnessSelectorsAtRoot(root, mutated) + if err == nil || !strings.Contains(err.Error(), "environmentClasses=") { + t.Fatalf("environment-class drift error=%v", err) + } + }) + } } t.Run("workflow/surplus-binding", func(t *testing.T) { @@ -843,6 +870,7 @@ func cloneBindingFile(source bindingFile) bindingFile { } for index := range clone.Bindings { clone.Bindings[index].CommandIDs = append([]string(nil), source.Bindings[index].CommandIDs...) + clone.Bindings[index].EnvironmentClasses = append([]string(nil), source.Bindings[index].EnvironmentClasses...) clone.Bindings[index].WitnessSelectors = append([]witnessSelector(nil), source.Bindings[index].WitnessSelectors...) } return clone diff --git a/internal/tools/coveragemetrics/required_inventory.go b/internal/tools/coveragemetrics/required_inventory.go new file mode 100644 index 0000000..4d25d70 --- /dev/null +++ b/internal/tools/coveragemetrics/required_inventory.go @@ -0,0 +1,794 @@ +package main + +type inventoryKey struct { + requirementID string + scenarioID string +} + +type requiredInventoryEntry struct { + commandIDs []string + environmentClasses []string + selectors []string + witnessPath string +} + +func requiredBindingWitnessInventory() map[inventoryKey]requiredInventoryEntry { + return map[inventoryKey]requiredInventoryEntry{ + {"REQ-PROOFKIT-WORKFLOW-001", "proofkit.agent-workflow.pure-single-admission-owner"}: { + witnessPath: "internal/command/changeworkflowplan/change_workflow_plan_test.go", + selectors: []string{"TestWorkflowPurityPredicates"}, + }, + {"REQ-PROOFKIT-WORKFLOW-002", "proofkit.agent-workflow.stage-prefix-and-terminal-relation"}: { + witnessPath: "internal/command/changeworkflowplan/state_test.go", + selectors: []string{"TestWorkflowStatePredicates"}, + }, + {"REQ-PROOFKIT-WORKFLOW-003", "proofkit.agent-workflow.total-checkpoint-successor-relation"}: { + witnessPath: "internal/command/changeworkflowplan/state_test.go", + selectors: []string{"TestWorkflowCheckpointPredicates"}, + }, + {"REQ-PROOFKIT-WORKFLOW-004", "proofkit.agent-workflow.review-identity-closure"}: { + witnessPath: "internal/command/changeworkflowplan/admission_test.go", + selectors: []string{"TestWorkflowIdentityPredicates"}, + }, + {"REQ-PROOFKIT-WORKFLOW-005", "proofkit.agent-workflow.reference-closed-bounded-context"}: { + witnessPath: "internal/command/changeworkflowplan/context_closure_test.go", + selectors: []string{"TestWorkflowClosurePredicates"}, + }, + {"REQ-PROOFKIT-WORKFLOW-006", "proofkit.agent-workflow.no-ambient-authority"}: { + witnessPath: "internal/command/changeworkflowplan/dependency_test.go", + selectors: []string{"TestWorkflowAmbientAuthorityPredicates"}, + }, + {"REQ-PROOFKIT-WORKFLOW-007", "proofkit.agent-workflow.native-evidence-guidance-purity"}: { + witnessPath: "internal/command/nativeevidenceguidance/dependency_test.go", + selectors: []string{"TestGuidanceNoAmbientDependencyPredicates"}, + }, + {"REQ-PROOFKIT-WORKFLOW-007", "proofkit.agent-workflow.native-evidence-guidance-slot-closure"}: { + witnessPath: "internal/command/nativeevidenceguidance/guidance_test.go", + selectors: []string{"TestGuidanceReferenceIsCompactAndOwnerBound", "TestGuidanceSlotPredicates"}, + }, + {"REQ-PROOFKIT-WORKFLOW-008", "proofkit.agent-workflow.bounded-safe-text"}: { + witnessPath: "internal/command/changeworkflowplan/text_test.go", + selectors: []string{"TestWorkflowTerminalTextIsOperationallyComplete", "TestWorkflowTextPredicates"}, + }, + {"REQ-PROOFKIT-WORKFLOW-008", "proofkit.agent-workflow.prompt-coordinate-and-escalation-closure"}: { + witnessPath: "internal/command/changeworkflowplan/prompt_test.go", + selectors: []string{"TestWorkflowPromptPredicates"}, + }, + {"REQ-PROOFKIT-WORKFLOW-009", "proofkit.agent-workflow.cli-presentation-capability-product"}: { + witnessPath: "internal/app/agent_workflow_command_test.go", + selectors: []string{"TestAgentWorkflowCLITruthTable"}, + }, + {"REQ-PROOFKIT-WORKFLOW-009", "proofkit.agent-workflow.style-strip-parity"}: { + witnessPath: "internal/command/changeworkflowplan/text_test.go", + selectors: []string{"TestWorkflowTextProjectionParity"}, + }, + {"REQ-PROOFKIT-WORKFLOW-010", "proofkit.agent-workflow.catalog-prerequisite-causality"}: { + witnessPath: "internal/command/changeworkflowplan/state_test.go", + selectors: []string{"TestWorkflowStatePredicates"}, + }, + {"REQ-PROOFKIT-WORKFLOW-010", "proofkit.agent-workflow.semantic-owner-minimality"}: { + witnessPath: "internal/command/nativeevidenceguidance/guidance_test.go", + selectors: []string{"TestGuidancePurityPredicates"}, + }, + {"REQ-PROOFKIT-WORKFLOW-010", "proofkit.agent-workflow.semantic-owner-topology"}: { + witnessPath: "internal/app/agent_workflow_topology_test.go", + selectors: []string{"TestAgentWorkflowSemanticOwnerTopology"}, + }, + {"REQ-PROOFKIT-WORKFLOW-011", "proofkit.agent-workflow.installed-carrier-smoke-closure"}: { + witnessPath: "internal/tools/workflowsmoke/workflow_smoke_test.go", + selectors: []string{ + "TestRunProcessCustomOutputLimitsAreExact", + "TestRunProcessRejectsInvalidCustomOutputLimitsBeforeStart", + "TestVerifyAcceptsApplicationCLI", + "TestVerifyRejectsCarrierContractMutations", + }, + }, + {"REQ-PROOFKIT-WORKFLOW-011", "proofkit.agent-workflow.public-cli-relation-closure"}: { + witnessPath: "internal/app/agent_workflow_command_test.go", + selectors: []string{"TestAgentWorkflowCLITruthTable"}, + }, + {"REQ-PROOFKIT-WORKFLOW-011", "proofkit.agent-workflow.version-edge-wire-observation"}: { + witnessPath: "internal/app/agent_workflow_version_edge_test.go", + selectors: []string{"TestAgentWorkflowVersionEdgeClosesPublicWireAdditions"}, + }, + {"REQ-PROOFKIT-WORKFLOW-012", "proofkit.agent-workflow.project-state-total-classification"}: { + witnessPath: "internal/command/projectstatus/projectstatus_test.go", + selectors: []string{ + "TestEvaluateTotalStateActionTable", + "TestOutputAdmissionRejectsUnreachableClosureCombination", + }, + }, + {"REQ-PROOFKIT-WORKFLOW-012", "proofkit.agent-workflow.project-state-child-admission-owner"}: { + witnessPath: "internal/command/projectstatus/dependency_test.go", + selectors: []string{"TestProjectStatusDelegatesChildAdmissionToMaterializationOwner"}, + }, + {"REQ-PROOFKIT-WORKFLOW-012", "proofkit.agent-workflow.project-state-child-owner-delegation"}: { + witnessPath: "internal/command/adoptionmaterialization/project_closure_test.go", + selectors: []string{ + "TestAdmitMaterializedProjectRoutesEveryManifestArtifactKindThroughItsOwner", + "TestMaterializedProjectRecordSnapshotDoesNotAliasCallerInput", + }, + }, + {"REQ-PROOFKIT-WORKFLOW-013", "proofkit.agent-workflow.project-state-bounded-inspection"}: { + witnessPath: "internal/command/projectstatus/inspect_test.go", + selectors: []string{ + "TestInspectAttemptRejectsFinalRepositoryRootReplacement", + "TestInspectClassifiesMaterializedProjectWithoutMutation", + "TestInspectCleanupFailureDominatesRetryableSnapshotChange", + "TestInspectCohortValidationClosesCleanEpochABA", + "TestInspectDeduplicatesRepeatedIssueCodes", + "TestInspectFailsClosedOnSymlinksAndBoundsWithoutDisclosure", + "TestInspectMapsInvalidControlState", + "TestInspectMapsRecoverableControlState", + "TestInspectRejectsAdmittedChildrenWithInvalidCrossRecordClosure", + "TestInspectRejectsCaseAliasedCanonicalRoute", + "TestInspectRejectsChangingControlEpochAcrossBothAttempts", + "TestOutOfBoundManifestIdentityIsAClassificationNotByteIdentity", + "TestReadProjectFileEnforcesAggregateBoundBeforeRead", + "TestReadProjectFileRejectsSameByteRouteReplacement", + }, + }, + {"REQ-PROOFKIT-WORKFLOW-013", "proofkit.agent-workflow.project-state-control-file-coherence"}: { + witnessPath: "internal/kernel/repositorytransaction/control_inspection_test.go", + selectors: []string{ + "TestInspectControlFileRejectsGrowthAfterRoutePreflight", + "TestInspectControlFileRejectsSameByteRouteReplacement", + "TestInspectControlStateHashesSymlinkTargetsWithoutDisclosingThem", + "TestInspectControlStateRejectsClassificationChangeHiddenByPortableNameIdentity", + "TestInspectControlStateRejectsPartialControlObservations", + "TestInspectControlStateUsesPortableObservationFields", + "TestInspectionLeaseDoesNotResolveAReplacementRoot", + "TestInspectionLeaseExportsOnlyReadOnlyFileCapability", + "TestInspectionLeaseFileCannotBeReassertedAsMutable", + "TestInspectionLeasePinsRootAndExcludesCooperativeWriter", + "TestInspectionLeaseRejectsControlNamespaceCreatedAfterOpen", + }, + }, + {"REQ-PROOFKIT-WORKFLOW-013", "proofkit.agent-workflow.project-state-exact-route-traversal"}: { + witnessPath: "internal/kernel/rootpath/exact_test.go", + selectors: []string{"TestOpenExactRegularFileRejectsFinalComponentABA", "TestOpenExactRegularFileRejectsParentSymlinkABA"}, + }, + {"REQ-PROOFKIT-WORKFLOW-014", "proofkit.agent-workflow.project-next-action-output-closure"}: { + witnessPath: "internal/command/projectstatus/projectstatus_test.go", + selectors: []string{ + "TestEvaluateTotalStateActionTable", + "TestOutputAdmissionRejectsReidentifiedStateActionMismatch", + "TestTextProjectionIsBoundedAndSemanticallyDerived", + }, + }, + {"REQ-PROOFKIT-WORKFLOW-015", "proofkit.agent-workflow.project-navigation-installed-carriers"}: { + commandIDs: []string{"proofkit.go-test", "proofkit.package-artifact"}, + environmentClasses: []string{"local-go", "local-go-python"}, + witnessPath: "internal/tools/workflowsmoke/workflow_smoke_test.go", + selectors: []string{"TestVerifyAcceptsApplicationCLI", "TestVerifyRejectsCarrierContractMutations"}, + }, + {"REQ-PROOFKIT-WORKFLOW-015", "proofkit.agent-workflow.project-navigation-public-cli"}: { + witnessPath: "internal/app/project_status_command_test.go", + selectors: []string{ + "TestProjectStatusCLI", + "TestProjectStatusOutputMatrix", + "TestProjectStatusTransportFailureUsesOneBoundedWriteWithoutAtomicSinkClaim", + }, + }, + {"REQ-PROOFKIT-WORKFLOW-015", "proofkit.agent-workflow.project-navigation-version-edge"}: { + witnessPath: "internal/app/project_navigation_version_edge_test.go", + selectors: []string{"TestProjectNavigationVersionEdgeClosesPublicRoutes"}, + }, + {"REQ-PROOFKIT-PACKAGE-001", "proofkit.package-boundary.root-export-and-deep-import-denial"}: { + witnessPath: "internal/tools/packageverify/main_test.go", + selectors: []string{"TestVerifyRootPackageRejectsEachForbiddenRootEntry"}, + }, + {"REQ-PROOFKIT-PACKAGE-002", "proofkit.package-boundary.launcher-profile-admission"}: { + witnessPath: "internal/kernel/cliexec/cliexec_test.go", + selectors: []string{"TestLauncherProfileAdmissionMatrix"}, + }, + {"REQ-PROOFKIT-PACKAGE-002", "proofkit.package-boundary.generated-command-field-inventory"}: { + witnessPath: "internal/app/invocation_profile_test.go", + selectors: []string{ + "TestGeneratedCommandInvocationProfileFieldInventory", + "TestGeneratedCommandInvocationProfileRouteClosure", + }, + }, + {"REQ-PROOFKIT-PACKAGE-002", "proofkit.package-boundary.generated-command-caller-preservation"}: { + witnessPath: "internal/command/gradualadoption/gradualadoption_test.go", + selectors: []string{"TestBootstrapPreservesCallerDisplayCommandInGuidancePayload"}, + }, + {"REQ-PROOFKIT-PACKAGE-002", "proofkit.package-boundary.cli-output-root-witnesses"}: { + witnessPath: "internal/app/cli_abi_test.go", + selectors: []string{ + "TestAdoptionContractEnvelopeCLIABI", + "TestAgentRouteEnvelopeModesUseExactRootShapes", + "TestRequirementAuthoringPlanOutputUsesVersionedRootShape", + "TestSelfCheckOutputUsesExactRootShape", + "TestStandaloneMultiVariantCommandsUseExactRootShapes", + }, + }, + {"REQ-PROOFKIT-PACKAGE-002", "proofkit.package-boundary.project-status-output-root-witnesses"}: { + witnessPath: "internal/app/project_status_command_test.go", + selectors: []string{ + "TestNextOutputUsesExactRootShape", + "TestStatusOutputUsesExactRootShape", + }, + }, + {"REQ-PROOFKIT-PACKAGE-002", "proofkit.package-boundary.adoption-materialization-output-root-witnesses"}: { + witnessPath: "internal/app/adoption_materialization_command_test.go", + selectors: []string{ + "TestAdoptMaterializeApplyOutputUsesExactRootShape", + "TestAdoptMaterializePlanOutputUsesExactRootShape", + "TestAdoptMaterializeRecoverOutputUsesExactRootShape", + }, + }, + {"REQ-PROOFKIT-PACKAGE-003", "proofkit.package-boundary.outside-consumer-artifact"}: { + witnessPath: "internal/tools/packageverify/main_test.go", + selectors: []string{ + "TestExactTarballOnboardingTrace", + "TestInstalledCommandRouteBijectionBindsCommandIdentity", + "TestInstalledNPMCarrierIsExactRegularTarballProjection", + "TestVerifyPackedOwnerRecordsRejectsSourceArtifactContentDrift", + }, + }, + {"REQ-PROOFKIT-PACKAGE-004", "proofkit.package-boundary.ci-receipt-anchor"}: { + witnessPath: "scripts/validate-self-hosting-receipts_test.go", + selectors: []string{ + "TestReceiptIDKeepsLocalAndCIIdentitiesDistinct", + "TestRunInvokesEveryRequiredSelfHostingAdmissionBoundary", + }, + }, + {"REQ-PROOFKIT-PACKAGE-004", "proofkit.package-boundary.self-hosting-report-verdict"}: { + witnessPath: "scripts/validate-self-hosting-receipts_test.go", + selectors: []string{"TestRunProofkitVerdictCases"}, + }, + {"REQ-PROOFKIT-PACKAGE-005", "proofkit.package-boundary.merge-critical-runtime-preconditions"}: { + witnessPath: "scripts/workflow_runtime_preconditions_test.go", + selectors: []string{"TestCISourceQualityInstallsPythonBeforeLifecycleTests"}, + }, + {"REQ-PROOFKIT-PACKAGE-006", "proofkit.package-boundary.python-wheel-candidate"}: { + witnessPath: "scripts/validate-self-hosting-receipts_test.go", + selectors: []string{"TestPythonArtifactRefsRejectEachWheelIdentityDefect"}, + }, + {"REQ-PROOFKIT-PACKAGE-006", "proofkit.package-boundary.python-wheel-generated-continuation"}: { + witnessPath: "internal/tools/pythonpackage/continuation_test.go", + selectors: []string{ + "TestExactDisplayedRouteOperandsRejectsWhitespaceAndExpansionMutants", + "TestInstalledPythonCarrierRejectsContractReplacementRemovalAndSymlink", + "TestInstalledPythonCommandRoutesRequireExactContractBijection", + "TestInstalledWheelContinuationUsesExactPythonModuleProfileWithoutNPM", + "TestPipInstallArgumentsAreIsolatedAndOffline", + "TestPythonVerificationEnvironmentRemovesAmbientImportControls", + }, + }, + {"REQ-PROOFKIT-PACKAGE-007", "proofkit.package-boundary.package-public-docs-no-mutable-release-facts"}: { + witnessPath: "internal/tools/packageverify/main_test.go", + selectors: []string{"TestVerifyNoStalePackageDocsRejectsMutableReleaseFactsInMarkdown"}, + }, + {"REQ-PROOFKIT-QUALITY-004", "proofkit.supply-chain-quality.cli-abi-golden"}: { + witnessPath: "internal/app/cli_abi_test.go", + selectors: []string{ + "TestAdoptionContractEnvelopeCLIABI", + "TestAgentRouteEnvelopeModesUseExactRootShapes", + "TestRequiredInputCommandsRouteStructuralErrorsByMode", + "TestRequirementAuthoringPlanOutputUsesVersionedRootShape", + "TestRequirementBrowserOneShotCLIOutputVariants", + "TestSelfCheckOutputUsesExactRootShape", + "TestStandaloneMultiVariantCommandsUseExactRootShapes", + }, + }, + {"REQ-PROOFKIT-QUALITY-004", "proofkit.supply-chain-quality.project-status-cli-abi"}: { + witnessPath: "internal/app/project_status_command_test.go", + selectors: []string{ + "TestNextOutputUsesExactRootShape", + "TestStatusOutputUsesExactRootShape", + }, + }, + {"REQ-PROOFKIT-QUALITY-004", "proofkit.supply-chain-quality.adoption-materialization-cli-abi"}: { + witnessPath: "internal/app/adoption_materialization_command_test.go", + selectors: []string{ + "TestAdoptMaterializeApplyOutputUsesExactRootShape", + "TestAdoptMaterializePlanOutputUsesExactRootShape", + "TestAdoptMaterializeRecoverOutputUsesExactRootShape", + }, + }, + {"REQ-PROOFKIT-QUALITY-001", "proofkit.supply-chain-quality.release-attestation-wiring"}: { + witnessPath: "scripts/validate-self-hosting-receipts_test.go", + selectors: []string{"TestReleaseWorkflowRetainsReleaseAssetAndPostCreateEvidenceClosure"}, + }, + {"REQ-PROOFKIT-QUALITY-001", "proofkit.supply-chain-quality.retained-evidence-manifest"}: { + witnessPath: "internal/tools/retainedevidence/manifest_test.go", + selectors: []string{ + "TestManifestRejectsUnboundAttestationAndSymlink", + "TestManifestUsesDownloadableArtifactPaths", + }, + }, + {"REQ-PROOFKIT-QUALITY-004", "proofkit.supply-chain-quality.cli-contract-topology"}: { + witnessPath: "internal/app/cli_contract_test.go", + selectors: []string{ + "TestCLIConditionModelClosesAdoptionOutputRoutes", + "TestCommandDescriptorContractParityRejectsMutations", + }, + }, + {"REQ-PROOFKIT-QUALITY-004", "proofkit.supply-chain-quality.cli-output-witness-contract"}: { + witnessPath: "internal/app/cli_output_witness_contract_test.go", + selectors: []string{"TestRootDistinctOutputWitnessBindingsAreExact"}, + }, + {"REQ-PROOFKIT-QUALITY-004", "proofkit.supply-chain-quality.cli-output-schema-evolution"}: { + witnessPath: "internal/app/cli_contract_test.go", + selectors: []string{"TestRequirementCoverageViewBreakingRootUsesVersionedOutputContract"}, + }, + {"REQ-PROOFKIT-QUALITY-005", "proofkit.supply-chain-quality.codeql-permission-separation"}: { + witnessPath: "scripts/workflow_security_scanner_oracles_test.go", + selectors: []string{"TestSecurityScannerWorkflowsSeparateProviderPublicationPermissions"}, + }, + {"REQ-PROOFKIT-QUALITY-006", "proofkit.supply-chain-quality.osv-permission-separation"}: { + witnessPath: "scripts/workflow_security_scanner_oracles_test.go", + selectors: []string{ + "TestOSVSourceScanFailsForEveryNonzeroScannerStatus", + "TestSecurityScannerWorkflowsSeparateProviderPublicationPermissions", + }, + }, + {"REQ-PROOFKIT-QUALITY-007", "proofkit.supply-chain-quality.scorecard-permission-and-publication-inputs"}: { + witnessPath: "scripts/workflow_security_scanner_oracles_test.go", + selectors: []string{ + "TestScorecardPublicPublishDeclaresRequiredOutputInputs", + "TestSecurityScannerWorkflowsSeparateProviderPublicationPermissions", + }, + }, + {"REQ-PROOFKIT-QUALITY-010", "proofkit.supply-chain-quality.artifact-file-boundary"}: { + witnessPath: "internal/tools/artifactfile/file_test.go", + selectors: []string{ + "TestOperationsRejectFinalSymlinkWithoutTargetMutation", + "TestOperationsRejectSymlinkComponentsWithoutOutsideMutation", + "TestReadBoundedRejectsUnrepresentableLimit", + "TestWriteReadAndRemoveRoundTrip", + }, + }, + {"REQ-PROOFKIT-QUALITY-010", "proofkit.supply-chain-quality.artifact-file-nonblocking-open"}: { + witnessPath: "internal/tools/artifactfile/file_unix_test.go", + selectors: []string{"TestReadBoundedRejectsFIFOWithoutBlocking"}, + }, + {"REQ-PROOFKIT-QUALITY-010", "proofkit.supply-chain-quality.coverage-metrics"}: { + witnessPath: "internal/tools/coveragemetrics/main_test.go", + selectors: []string{ + "TestEachCommandRouteClosureConjunctHasIndependentFalsifier", + "TestEachLinkageDeadZoneConjunctHasIndependentFalsifier", + "TestInvalidateMetricsFileRejectsSymlinkParentWithoutDeletingOutsideFile", + "TestWriteMetricsFileRejectsSymlinkEscapeWithoutMutation", + }, + }, + {"REQ-PROOFKIT-QUALITY-010", "proofkit.supply-chain-quality.command-oracle-execution-ledger"}: { + witnessPath: "internal/tools/commandoracle/execute_test.go", + selectors: []string{ + "TestExecuteBindsMaterializedSourceCandidatesAndRuntimeEvents", + "TestRunGoTestCommandTerminatesImmediatelyWhenStderrExceedsBound", + "TestRunGoTestsDoesNotExecuteCrossPackageNameMatches", + "TestRunGoTestsTerminatesOnContextDeadline", + "TestValidateCurrentRejectsProducerUnreachableCandidateProjection", + }, + }, + {"REQ-PROOFKIT-QUALITY-010", "proofkit.supply-chain-quality.command-oracle-counterfeit-corpus"}: { + witnessPath: "internal/tools/commandoracle/corpus_test.go", + selectors: []string{ + "TestCounterfeitCorpusClosesRequiredAxes", + "TestCounterfeitCorpusClosureRejectsMissingRequiredAxes", + "TestEachCounterfeitCaseProducesItsCheckedInDecision", + }, + }, + {"REQ-PROOFKIT-QUALITY-010", "proofkit.supply-chain-quality.command-oracle-source-snapshot"}: { + witnessPath: "internal/tools/repositorysnapshot/snapshot_test.go", + selectors: []string{ + "TestCaptureContextTerminatesCanceledGitProcessGroup", + "TestCaptureContextTerminatesGitProcessGroupOnOutputOverflow", + "TestCaptureRejectsSuccessfulGitDiagnosticsWithoutEcho", + "TestMaterializeBindsCopiedBytesAndRejectsLiveMutation", + "TestMaterializeRejectsSymlinkAndNonEmptyDestination", + "TestMaterializeRejectsSymlinkedDestinationInsideSource", + "TestValidRevisionAdmitsOnlyGitObjectIdentityAndOptionalSnapshotDigest", + "TestValidateMaterializedRejectsSurplusFile", + }, + }, + {"REQ-PROOFKIT-QUALITY-010", "proofkit.supply-chain-quality.binding-selector-executability"}: { + witnessPath: "internal/tools/coveragemetrics/main_test.go", + selectors: []string{ + "TestBindingWitnessSelectorsAcceptUnnamedGoTestParameter", + "TestBindingWitnessSelectorsRejectInvalidGoTestSignature", + "TestBindingWitnessSelectorsRejectMissingSemanticOwner", + "TestBindingWitnessSelectorsRejectNonTestAndBuildExcludedFiles", + "TestBindingWitnessSelectorsRejectVacuousTestBody", + "TestBindingWitnessSelectorsRequireExactCriticalInventories", + }, + }, + {"REQ-PROOFKIT-QUALITY-011", "proofkit.supply-chain-quality.ci-required-aggregate-exactness"}: { + witnessPath: "scripts/workflow_package_gate_oracle_test.go", + selectors: []string{ + "TestCIRequiredAggregateRejectsExecutionOverrides", + "TestCIRequiredAggregateRejectsNeutralizedScript", + "TestCIRequiredAggregateRejectsPlatformSmokeSubstitution", + "TestCIWorkflowDeclaresFailClosedRequiredAggregate", + }, + }, + {"REQ-PROOFKIT-QUALITY-013", "proofkit.supply-chain-quality.workflow-package-gate-oracle"}: { + witnessPath: "scripts/workflow_package_gate_oracle_test.go", + selectors: []string{ + "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"}: { + witnessPath: "internal/tools/pythonpackage/metadata_test.go", + selectors: []string{ + "TestREADMEPlatformAndPythonProjection", + "TestReleaseTargetsProjectExactPythonWheelMetadata", + "TestVerifyWheelContentsRequiresExactWheelMetadata", + }, + }, + {"REQ-PROOFKIT-QUALITY-019", "proofkit.supply-chain-quality.installed-package-json-abi-smoke"}: { + witnessPath: "internal/tools/packageverify/main_test.go", + selectors: []string{ + "TestExactTarballOnboardingTrace", + "TestInstalledInvocationRequiresAuthoredOrderAndExactCommandToken", + "TestInstalledNPMCarrierIsExactRegularTarballProjection", + "TestInstalledREADMEFirstInputPreservesJSONExampleBytes", + "TestInstalledREADMEFirstInputUsesBoundedLiteralShellWords", + "TestLiteralShellWordsConsumesLongBackslashRun", + "TestOnboardingTraceCoversEveryDiscoveredPresetAndREADMEInput", + "TestVerifyPackedOwnerRecordsRejectsSourceArtifactContentDrift", + }, + }, + {"REQ-PROOFKIT-QUALITY-023", "proofkit.supply-chain-quality.release-closeout-npm-byte-admission"}: { + witnessPath: "internal/tools/releasecloseoutinput/main_test.go", + selectors: []string{ + "TestBuildInputFailsClosedForEachBlockingEvidenceClass", + "TestPackRecordBytesMatchEnforcesByteLimit", + }, + }, + {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.release-manifest-json-abi-registry-evidence"}: { + witnessPath: "internal/tools/releasemanifest/main_test.go", + selectors: []string{ + "TestNPMRegistryAuthorityFlowsFromAdmittedFileToPublishedChannel", + "TestNPMRegistryPublicationRequiresTypedAuthorityEvidence", + }, + }, + {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.npm-registry-authority-producer"}: { + witnessPath: "internal/tools/npmregistry/main_test.go", + selectors: []string{ + "TestRunBuildsCanonicalTypedRegistryEvidence", + "TestRunRejectsRegistryPackageSetSubstitution", + }, + }, + {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.npm-registry-workflow-delegation"}: { + witnessPath: "scripts/validate-self-hosting-receipts_test.go", + selectors: []string{"TestReleaseWorkflowDelegatesNPMRegistryEvidenceToRepositoryOwner"}, + }, + {"REQ-PROOFKIT-QUALITY-022", "proofkit.supply-chain-quality.browser-failure-diagnostics-retention"}: { + witnessPath: "scripts/workflow_browser_runtime_oracle_test.go", + selectors: []string{"TestCIBrowserRuntimeRetainsFailureDiagnosticsWithoutPublishingProof"}, + }, + {"REQ-PROOFKIT-QUALITY-023", "proofkit.supply-chain-quality.python-wheel-platform-byte-compatibility"}: { + witnessPath: "internal/tools/pythonpackage/metadata_test.go", + selectors: []string{ + "TestMachOMinimumMacOSAcceptsLegacyVersionCommand", + "TestMachOMinimumMacOSRejectsTruncatedBuildVersion", + "TestVerifyWheelContentsAcceptsDarwinTagAtOrAboveMachOMinimum", + "TestVerifyWheelContentsRejectsDarwinTagBelowMachOMinimum", + }, + }, + {"REQ-PROOFKIT-QUALITY-023", "proofkit.supply-chain-quality.python-wheel-resource-bounds"}: { + witnessPath: "internal/tools/pythonpackage/metadata_test.go", + selectors: []string{ + "TestVerifyWheelContentsRejectsOversizedCompressedEntryBeforeDecompression", + "TestVerifyWheelContentsRejectsOversizedEntryBeforeDecompression", + }, + }, + {"REQ-PROOFKIT-QUALITY-023", "proofkit.supply-chain-quality.wrapper-platform-bijection"}: { + witnessPath: "internal/tools/packagebuild/main_test.go", + selectors: []string{"TestWrapperScriptRoutesEveryReleasePlatformTarget"}, + }, + {"REQ-PROOFKIT-QUALITY-015", "proofkit.supply-chain-quality.release-closeout-completion-criteria"}: { + witnessPath: "internal/tools/releasecloseoutinput/main_test.go", + selectors: []string{ + "TestBuildInputFailsClosedForEachBlockingEvidenceClass", + "TestSelfEvidenceInvokesCurrentCommandOracleOwner", + "TestSelfEvidenceRejectsProducerUnreachableCommandOracleRef", + }, + }, + {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.release-change-record-projection"}: { + witnessPath: "internal/tools/releasechange/record_test.go", + selectors: []string{ + "TestAdmitEnforcesVersionedChangeClass", + "TestCurrentChangeRecordNamesReviewedSemanticChanges", + "TestRenderStatesPreOneExactPinPolicy", + }, + }, + {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.retained-evidence-artifact-topology"}: { + witnessPath: "internal/tools/retainedevidence/manifest_test.go", + selectors: []string{"TestVerifyRejectsManifestAddressDrift"}, + }, + {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.release-closeout-change-record"}: { + witnessPath: "internal/tools/releasecloseoutinput/main_test.go", + selectors: []string{"TestBuildInputFailsClosedForEachBlockingEvidenceClass"}, + }, + {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.release-predecessor-lineage"}: { + witnessPath: "internal/tools/releasepreflight/main_test.go", + selectors: []string{ + "TestRunNPMLineageUsesAdmittedRecordAndProviderIdentity", + "TestValidateNPMReleaseLineage", + }, + }, + {"REQ-PROOFKIT-QUALITY-024", "proofkit.supply-chain-quality.release-predecessor-lineage-workflow"}: { + witnessPath: "scripts/validate-self-hosting-receipts_test.go", + selectors: []string{"TestReleaseWorkflowCandidateEvidenceAllowsExistingNPMByteMatch"}, + }, + {"REQ-PROOFKIT-QUALITY-025", "proofkit.supply-chain-quality.workflow-source-oracles"}: { + witnessPath: "scripts/workflow_source_oracles_test.go", + selectors: []string{ + "TestExistingReleasePathIsReadOnlyAndFailsOnDrift", + "TestWorkflowClosedKeyAdmission", + "TestWorkflowExternalActionsUseFullCommitSHAs", + }, + }, + {"REQ-PROOFKIT-SPEC-011", "proofkit.spec-proof-core.adoption-contract-envelope-cli-abi"}: { + witnessPath: "internal/app/cli_abi_test.go", + selectors: []string{"TestAdoptionContractEnvelopeCLIABI"}, + }, + {"REQ-PROOFKIT-SPEC-007", "proofkit.spec-proof-core.canonical-command-input-admission"}: { + witnessPath: "internal/app/command_coverage_test.go", + selectors: []string{"TestRequiredInputCommandsRejectMalformedCallerRecords"}, + }, + {"REQ-PROOFKIT-SPEC-007", "proofkit.spec-proof-core.canonical-input-admission"}: { + witnessPath: "internal/kernel/admission/json_test.go", + selectors: []string{"TestDecodeTypedJSONUsesStrictAdmission"}, + }, + {"REQ-PROOFKIT-SPEC-013", "proofkit.spec-proof-core.receipt-trust-status-vocabulary-admission"}: { + witnessPath: "internal/command/receipttrustclass/receipt_trust_class_test.go", + selectors: []string{"TestBuildRejectsHigherRankThatWeakensMinimumTrustSemantics"}, + }, + {"REQ-PROOFKIT-SPEC-021", "proofkit.spec-proof-core.requirement-browser-one-shot-cleanup"}: { + witnessPath: "internal/command/requirementbrowser/server_test.go", + selectors: []string{ + "TestServeOneShotDoesNotReadCompletedDoneTwice", + "TestServeOneShotReturnsCleanupFailuresWithoutWritingTerminalPacket", + "TestServeOneShotWaitsForDoneBeforeWritingTerminalPacket", + }, + }, + {"REQ-PROOFKIT-SPEC-006", "proofkit.spec-proof-core.test-inventory-and-coverage-view"}: { + witnessPath: "internal/command/requirementcoverageview/output_closure_test.go", + selectors: []string{ + "TestAdmitOutputRejectsCompactProjectionDrift", + "TestAdmitOutputRejectsMissingInverseParentProjection", + "TestAdmitOutputRejectsNonCanonicalWireProjectionText", + "TestAdmitOutputRejectsRemovedValidUnmappedInventoryEntry", + "TestAdmitOutputReplaysFailedInventoryQualitySemantics", + "TestAdmitOutputReplaysFullRepositorySourceOwnerScopeFailures", + "TestAdmitOutputReplaysOwnerScopeFailures", + "TestAdmitOutputRequiresEveryCoverageBasisField", + "TestAdmitOutputRequiresEveryDeclaredRootField", + "TestAdmitOutputRetainsFailedInventoryEntriesWithoutProjectedParents", + "TestAdmitOutputValidatesEveryCoverageRowMetadataField", + }, + }, + {"REQ-PROOFKIT-SPEC-006", "proofkit.spec-proof-core.declared-route-mapping-without-assurance"}: { + witnessPath: "internal/command/requirementcoverageview/requirementcoverageview_test.go", + selectors: []string{"TestBuildJSONMissingSelectorRemainsMappingOnly"}, + }, + {"REQ-PROOFKIT-SPEC-012", "proofkit.spec-proof-core.requirement-authoring-ref-provenance"}: { + witnessPath: "internal/command/requirementauthoringplan/requirement_authoring_plan_test.go", + selectors: []string{"TestBuildPreservesDigestBoundAuthoringRefIdentity"}, + }, + {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-brief-cli-abi"}: { + witnessPath: "internal/app/cli_abi_test.go", + selectors: []string{"TestAgentRouteEnvelopeModesUseExactRootShapes"}, + }, + {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-brief-projection"}: { + witnessPath: "internal/command/agentroute/brief_test.go", + selectors: []string{ + "TestAgentBriefBindsLauncherContextThatAffectsReportDigest", + "TestAgentBriefClosesEverySelectedCommandInputReference", + "TestAgentBriefCompactsAtDeclaredByteBoundary", + "TestAgentBriefIsBoundedAndFullEnvelopeRemainsAvailable", + "TestAgentBriefNamesCompleteInputBundleBlocker", + "TestAgentBriefPreservesBlockedRouteOmissionsAndUnknownReportBlockers", + "TestBriefBlockerBoundDominatesMapMaterialization", + "TestBuildEnvelopeCapsBlockersAndCountsOmittedDetails", + "TestBuildEnvelopeCompactsOversizedArgvWithoutLosingActionIdentity", + }, + }, + {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-flag-pre-read-admission"}: { + witnessPath: "internal/app/app_test.go", + selectors: []string{"TestAgentRouteModeAdmissionPrecedesInputRead"}, + }, + {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-report-contract-closure"}: { + witnessPath: "internal/app/cli_contract_test.go", + selectors: []string{"TestAgentRouteOutputContractPreservesReportSemantics"}, + }, + {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-brief-version-edge"}: { + witnessPath: "internal/app/agent_route_version_edge_test.go", + selectors: []string{"TestAgentRouteVersionEdgeClosesBriefDefaultMigration"}, + }, + {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-materialized-ref-admission"}: { + witnessPath: "internal/command/agentroute/agentroute_test.go", + selectors: []string{"TestBuildRejectsStdinTransportSentinelAsArtifactReference"}, + }, + {"REQ-PROOFKIT-SPEC-028", "proofkit.spec-proof-core.adoption-inventory-boundary"}: { + witnessPath: "internal/command/repositoryinventory/repositoryinventory_test.go", + selectors: []string{ + "TestCatalogRolePolicyIsExact", + "TestInventoryIdentityBindsEverySemanticOperand", + "TestInventoryOutputByteLimitIsExact", + "TestReadRootInventoryClassifiesPartialBatchesWithoutRetainingUnknownNames", + "TestScanDoesNotFollowUnknownSymlink", + "TestScanEnforcesPreflightBoundsAndExplicitOmissions", + "TestScanPolicyBoundariesAreExact", + "TestScanProducesBoundedClosedInventory", + "TestScanRejectsRecognizedSymlinkWithoutReadingTarget", + "TestUnsupportedPlatformFailsBeforeOpeningRepositoryRoot", + }, + }, + {"REQ-PROOFKIT-SPEC-028", "proofkit.spec-proof-core.adoption-inventory-nonblocking-open"}: { + witnessPath: "internal/command/repositoryinventory/fifo_unix_test.go", + selectors: []string{"TestScanRejectsFIFOReplacementWithoutBlocking"}, + }, + {"REQ-PROOFKIT-SPEC-029", "proofkit.spec-proof-core.adoption-plan-authority-closure"}: { + witnessPath: "internal/command/adoptionplan/adoptionplan_test.go", + selectors: []string{ + "TestBuildRejectsUnknownIntentPresetAndForgedInventory", + "TestBuildSeparatesAdoptionIntentFromCandidateAuthority", + "TestBuildStackHintCannotChangeIntentTrustOrTasks", + "TestPlanIdentityBindsIntentAndInventory", + "TestPlanWireAdmissionIsDeterministicAndOwnerClosed", + }, + }, + {"REQ-PROOFKIT-SPEC-029", "proofkit.spec-proof-core.adoption-plan-observational-stack"}: { + witnessPath: "internal/command/adoptionplan/repository_classes_test.go", + selectors: []string{"TestPlanKeepsRepositoryClassesObservationalAndStackNeutral"}, + }, + {"REQ-PROOFKIT-SPEC-029", "proofkit.spec-proof-core.adoption-guidance-reference-closure"}: { + witnessPath: "internal/command/nativeevidenceguidance/guidance_test.go", + selectors: []string{"TestGuidanceReferenceIsCompactAndOwnerBound"}, + }, + {"REQ-PROOFKIT-SPEC-030", "proofkit.spec-proof-core.adoption-plan-presentation-closure"}: { + witnessPath: "internal/command/adoptionplan/adoptionplan_test.go", + selectors: []string{ + "TestAdoptionPlanOutputAndTextBoundsAreExact", + "TestTextProjectionPreservesJSONPlanSemantics", + }, + }, + {"REQ-PROOFKIT-SPEC-027", "proofkit.spec-proof-core.adoption-front-door-whole-cli"}: { + witnessPath: "internal/app/adoption_front_door_command_test.go", + selectors: []string{"TestAdoptionFrontDoorCLI"}, + }, + {"REQ-PROOFKIT-SPEC-018", "proofkit.spec-proof-core.command-route-contract-closure"}: { + witnessPath: "internal/tools/commandcontractgen/main_test.go", + selectors: []string{ + "TestCommandRoutesAreBoundedSafeAndUnambiguous", + "TestRenderRejectsIncompleteAndStaleCommandContracts", + }, + }, + {"REQ-PROOFKIT-SPEC-018", "proofkit.spec-proof-core.command-route-generated-adapter"}: { + witnessPath: "internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source_test.go", + selectors: []string{"TestGeneratedSourceAdmitsBoundedCanonicalCommandRoutes"}, + }, + {"REQ-PROOFKIT-SPEC-018", "proofkit.spec-proof-core.command-route-installed-contract"}: { + witnessPath: "internal/tools/installedclicontract/contract_test.go", + selectors: []string{ + "TestAdmitCommandRouteTokenBoundariesAreExact", + "TestAdmitRequiresExactCommandRouteGrammarProjection", + }, + }, + {"REQ-PROOFKIT-SPEC-018", "proofkit.spec-proof-core.command-route-kernel-owner"}: { + witnessPath: "internal/kernel/commandroute/route_test.go", + selectors: []string{ + "TestGrammarBoundariesAreExact", + "TestOmittedRoutePolicyUsesStableCommandIdentity", + "TestParseRequiresCanonicalSeparatorAndRoundTrip", + }, + }, + {"REQ-PROOFKIT-SPEC-031", "proofkit.spec-proof-core.adoption-version-edge-closure"}: { + witnessPath: "internal/app/adoption_front_door_version_edge_test.go", + selectors: []string{ + "TestAdoptionFrontDoorVersionEdgeClosesInitRetirement", + "TestAdoptionFrontDoorVersionEdgeRejectsDigestBoundInventoryContradiction", + "TestRetiredInitRouteHasNoPublicDispatcher", + }, + }, + {"REQ-PROOFKIT-SPEC-032", "proofkit.spec-proof-core.adoption-materialization-owner-closure"}: { + witnessPath: "internal/command/adoptionmaterialization/adoptionmaterialization_test.go", + selectors: []string{ + "TestApplyBlocksStaleMutationButAcceptsLostAcknowledgementRetry", + "TestMaterializationOutputAdmissionRejectsCrossOwnerMutants", + "TestMaterializationRejectsCrossRecordDriftAndManifestMutation", + "TestMaterializationWholeChainIsCanonicalAndOwnerClosed", + "TestReceiptAdmissionRejectsOperationAttributionMutants", + }, + }, + {"REQ-PROOFKIT-SPEC-032", "proofkit.spec-proof-core.adoption-materialization-reference-closure"}: { + witnessPath: "internal/command/adoptionmaterialization/closure_test.go", + selectors: []string{ + "TestInventoryReferencesMustResolveThroughBindingEdges", + "TestManifestAdmissionEqualsProducerImage", + "TestPathRoleLedgerRejectsWriteReferenceCollisions", + "TestRequirementProjectionRequiresClaimLevelParity", + }, + }, + {"REQ-PROOFKIT-SPEC-032", "proofkit.spec-proof-core.adoption-materialization-whole-cli"}: { + witnessPath: "internal/app/adoption_materialization_command_test.go", + selectors: []string{"TestAdoptionMaterializationCLI"}, + }, + {"REQ-PROOFKIT-SPEC-033", "proofkit.spec-proof-core.repository-transaction-fault-recovery"}: { + witnessPath: "internal/kernel/repositorytransaction/transaction_test.go", + selectors: []string{ + "TestApplyAlreadySatisfiedRejectsConcurrentCooperativeWriter", + "TestApplyFaultAfterFirstPublishRestoresExactBeforeState", + "TestApplyRejectsConcurrentCooperativeWriter", + "TestProcessInterruptionAtEveryMutationBoundaryIsRecoverable", + "TestRecoverDoesNotInventIdentityForPartialPreparingJournal", + "TestTransactionLockIsInterprocess", + }, + }, + {"REQ-PROOFKIT-SPEC-033", "proofkit.spec-proof-core.repository-transaction-output-relations"}: { + witnessPath: "internal/kernel/repositorytransaction/output_admission_test.go", + selectors: []string{"TestPlanAndResultOutputAdmissionRejectSemanticMutants"}, + }, + {"REQ-PROOFKIT-SPEC-033", "proofkit.spec-proof-core.repository-transaction-cleanup-state-matrix"}: { + witnessPath: "internal/kernel/repositorytransaction/state_machine_test.go", + selectors: []string{"TestCleanupDurabilityFailureDoesNotClaimRecoverableState"}, + }, + {"REQ-PROOFKIT-SPEC-033", "proofkit.spec-proof-core.repository-transaction-filesystem-portable-path-identity"}: { + witnessPath: "internal/kernel/repositorytransaction/plan_test.go", + selectors: []string{"TestBuildPlanRejectsFilesystemPortableAliases"}, + }, + {"REQ-PROOFKIT-SPEC-033", "proofkit.spec-proof-core.repository-transaction-portable-path-identity"}: { + witnessPath: "internal/kernel/pathidentity/pathidentity_test.go", + selectors: []string{"TestPortableEquivalenceAndContainment"}, + }, + {"REQ-PROOFKIT-SPEC-033", "proofkit.spec-proof-core.repository-transaction-terminal-state"}: { + witnessPath: "internal/kernel/repositorytransaction/invariant_test.go", + selectors: []string{ + "TestAppliedTerminalReceiptReplaysCompleteResult", + "TestApplyExecutesFrozenPlan", + "TestCommittedRecoveryRejectsRollback", + "TestMalformedRecoveryActionBlocksMutation", + "TestPreparingFailureCannotClaimRollbackAfterTargetDivergence", + "TestPreparingRecoveryRejectsResumeBeforeActionSelection", + "TestPreparingRollbackAtomicallyReplacesPreviousTerminalReceipt", + "TestReadyReplacementRetiresPreviousReceiptAndPreservesCompleteResult", + "TestRecoveryActionAndTerminalReceiptAreStable", + "TestRecoveryActionIsDurableBeforeDirectionalMutation", + "TestUnknownRecoveryStateDoesNotAdoptExpectedIdentity", + }, + }, + {"REQ-PROOFKIT-SPEC-034", "proofkit.spec-proof-core.adoption-materialization-version-edge"}: { + witnessPath: "internal/app/adoption_materialization_version_edge_test.go", + selectors: []string{ + "TestAdoptionMaterializationVersionEdgeClosesPublicCommands", + "TestAdoptionMaterializationVersionEdgePreservesFrozenPredecessor", + "TestAdoptionMaterializationVersionEdgeRejectsCoordinatedChangeRecordDrift", + }, + }, + {"REQ-PROOFKIT-SPEC-035", "proofkit.spec-proof-core.project-navigation-version-edge"}: { + witnessPath: "internal/app/project_navigation_version_edge_test.go", + selectors: []string{ + "TestProjectNavigationVersionEdgeClosesPublicRoutes", + "TestProjectNavigationVersionEdgePreservesFrozenPredecessor", + "TestProjectNavigationVersionEdgeRejectsCoordinatedChangeRecordDrift", + }, + }, + {"REQ-PROOFKIT-RETIRE-006", "proofkit.consumer-infra-retirement.migration-parity-admission"}: { + witnessPath: "internal/command/migrationparityadmission/migrationparityadmission_test.go", + selectors: []string{"TestBuildProjectsEveryCallerDeclaredStatusAndSummaryField"}, + }, + } +} diff --git a/internal/tools/installedclicontract/contract.go b/internal/tools/installedclicontract/contract.go index e79564c..877d216 100644 --- a/internal/tools/installedclicontract/contract.go +++ b/internal/tools/installedclicontract/contract.go @@ -70,21 +70,25 @@ func Admit(content []byte) (Contract, error) { } seenCommandIDs[commandID] = struct{}{} - routeTokens := []string{commandID} + var explicitRoute []string if rawRoute, exists := command["route"]; exists { route, ok := rawRoute.([]any) - if !ok || len(route) < commandroute.MinimumTokens || len(route) > commandroute.MaximumTokens { + if !ok { return Contract{}, fmt.Errorf("installed CLI contract command %d has an invalid route", index) } - routeTokens = make([]string, 0, len(route)) + explicitRoute = make([]string, 0, len(route)) for _, rawToken := range route { token, ok := rawToken.(string) - if !ok || !commandroute.ValidToken(token) { + if !ok { return Contract{}, fmt.Errorf("installed CLI contract command %d has an invalid route token", index) } - routeTokens = append(routeTokens, token) + explicitRoute = append(explicitRoute, token) } } + routeTokens, ok := commandroute.Resolve(commandID, explicitRoute) + if !ok { + return Contract{}, fmt.Errorf("installed CLI contract command %d has an invalid route", index) + } routeText := commandroute.Text(routeTokens) if _, exists := commandIDsByRoute[routeText]; exists { return Contract{}, fmt.Errorf("installed CLI contract duplicates a command route") @@ -118,10 +122,10 @@ func admitCommandRouteGrammar(contract map[string]any) error { return fmt.Errorf("installed CLI contract processContract must be an object") } grammar, ok := processContract["commandRouteGrammar"].(map[string]any) - if !ok || len(grammar) != 5 { - return fmt.Errorf("installed CLI contract commandRouteGrammar must contain exactly five fields") + if !ok || len(grammar) != 6 { + return fmt.Errorf("installed CLI contract commandRouteGrammar must contain exactly six fields") } - for _, key := range []string{"ambiguityPolicy", "maximumTokens", "minimumTokens", "separator", "tokenPattern"} { + for _, key := range []string{"ambiguityPolicy", "maximumTokens", "minimumTokens", "omittedRoutePolicy", "separator", "tokenPattern"} { if _, exists := grammar[key]; !exists { return fmt.Errorf("installed CLI contract commandRouteGrammar is missing %s", key) } @@ -130,7 +134,7 @@ func admitCommandRouteGrammar(contract map[string]any) error { maximum, maximumOK := exactPositiveInteger(grammar["maximumTokens"]) if !minimumOK || !maximumOK || minimum != commandroute.MinimumTokens || maximum != commandroute.MaximumTokens || grammar["separator"] != commandroute.Separator || grammar["tokenPattern"] != commandroute.TokenPattern || - grammar["ambiguityPolicy"] != commandroute.AmbiguityPolicy { + grammar["ambiguityPolicy"] != commandroute.AmbiguityPolicy || grammar["omittedRoutePolicy"] != commandroute.OmittedRoutePolicy { return fmt.Errorf("installed CLI contract commandRouteGrammar differs from the supported process grammar") } return nil diff --git a/internal/tools/installedclicontract/contract_test.go b/internal/tools/installedclicontract/contract_test.go index 6530d3c..4c1af61 100644 --- a/internal/tools/installedclicontract/contract_test.go +++ b/internal/tools/installedclicontract/contract_test.go @@ -153,6 +153,7 @@ func TestAdmitRequiresExactCommandRouteGrammarProjection(t *testing.T) { strings.Replace(base, `"separator":" "`, `"separator":"/"`, 1), strings.Replace(base, `"tokenPattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"`, `"tokenPattern":"^[a-z]+$"`, 1), strings.Replace(base, `"ambiguityPolicy":"no_route_is_prefix_of_another"`, `"ambiguityPolicy":"allow_prefixes"`, 1), + strings.Replace(base, `"omittedRoutePolicy":"command_id"`, `"omittedRoutePolicy":"unknown"`, 1), } for index, mutant := range mutants { if _, err := Admit([]byte(mutant)); err == nil { @@ -162,5 +163,5 @@ func TestAdmitRequiresExactCommandRouteGrammarProjection(t *testing.T) { } func contractFixture(commands string) []byte { - return []byte(`{"processContract":{"commandRouteGrammar":{"minimumTokens":1,"maximumTokens":4,"separator":" ","tokenPattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","ambiguityPolicy":"no_route_is_prefix_of_another"}},"commands":[` + commands + `]}`) + return []byte(`{"processContract":{"commandRouteGrammar":{"minimumTokens":1,"maximumTokens":4,"separator":" ","tokenPattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","ambiguityPolicy":"no_route_is_prefix_of_another","omittedRoutePolicy":"command_id"}},"commands":[` + commands + `]}`) } diff --git a/internal/tools/packageverify/main_test.go b/internal/tools/packageverify/main_test.go index c6e62d2..37e5337 100644 --- a/internal/tools/packageverify/main_test.go +++ b/internal/tools/packageverify/main_test.go @@ -1371,11 +1371,12 @@ func installedContractFixture(commands string) []byte { func testCommandRouteGrammar() map[string]any { return map[string]any{ - "ambiguityPolicy": commandroute.AmbiguityPolicy, - "maximumTokens": commandroute.MaximumTokens, - "minimumTokens": commandroute.MinimumTokens, - "separator": commandroute.Separator, - "tokenPattern": commandroute.TokenPattern, + "ambiguityPolicy": commandroute.AmbiguityPolicy, + "maximumTokens": commandroute.MaximumTokens, + "minimumTokens": commandroute.MinimumTokens, + "omittedRoutePolicy": commandroute.OmittedRoutePolicy, + "separator": commandroute.Separator, + "tokenPattern": commandroute.TokenPattern, } } diff --git a/internal/tools/pythonpackage/continuation_test.go b/internal/tools/pythonpackage/continuation_test.go index 1dcfa0c..586652b 100644 --- a/internal/tools/pythonpackage/continuation_test.go +++ b/internal/tools/pythonpackage/continuation_test.go @@ -69,7 +69,7 @@ func TestExactDisplayedCommandRoutesAdmitBoundedMultiTokenRoutes(t *testing.T) { func testInstalledCLIContract(t *testing.T) installedclicontract.Contract { t.Helper() - contract, err := installedclicontract.Admit([]byte(`{"processContract":{"commandRouteGrammar":{"minimumTokens":1,"maximumTokens":4,"separator":" ","tokenPattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","ambiguityPolicy":"no_route_is_prefix_of_another"}},"commands":[{"command":"sample","route":["adopt","plan"]}]}`)) + contract, err := installedclicontract.Admit([]byte(`{"processContract":{"commandRouteGrammar":{"minimumTokens":1,"maximumTokens":4,"separator":" ","tokenPattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","ambiguityPolicy":"no_route_is_prefix_of_another","omittedRoutePolicy":"command_id"}},"commands":[{"command":"sample","route":["adopt","plan"]}]}`)) if err != nil { t.Fatal(err) } diff --git a/internal/tools/pythonpackage/metadata_test.go b/internal/tools/pythonpackage/metadata_test.go index 63656ce..9309cd2 100644 --- a/internal/tools/pythonpackage/metadata_test.go +++ b/internal/tools/pythonpackage/metadata_test.go @@ -18,7 +18,7 @@ import ( const ( testLicenseContent = "MIT License\n" - testCLIContractContent = `{"processContract":{"commandRouteGrammar":{"minimumTokens":1,"maximumTokens":4,"separator":" ","tokenPattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","ambiguityPolicy":"no_route_is_prefix_of_another"}},"commands":[{"command":"stack-preset","outputContract":{"flagChoices":{"--preset":["go_cli_repo"]}}}]}` + testCLIContractContent = `{"processContract":{"commandRouteGrammar":{"minimumTokens":1,"maximumTokens":4,"separator":" ","tokenPattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","ambiguityPolicy":"no_route_is_prefix_of_another","omittedRoutePolicy":"command_id"}},"commands":[{"command":"stack-preset","outputContract":{"flagChoices":{"--preset":["go_cli_repo"]}}}]}` ) func TestPythonPackageReadersRejectAmbiguousJSON(t *testing.T) { diff --git a/internal/tools/releasechange/record_test.go b/internal/tools/releasechange/record_test.go index d8f19be..7c1a40c 100644 --- a/internal/tools/releasechange/record_test.go +++ b/internal/tools/releasechange/record_test.go @@ -194,14 +194,20 @@ func TestCurrentChangeRecordNamesReviewedSemanticChanges(t *testing.T) { assertCurrentChangeRecordNotesRejected(t, "appended duplicate change section", record, notes+"## Breaking Contract Changes\n\n- `proofkit.surplus.section`: Surplus section.\n") } -var currentBreakingChanges = []Change{} +var currentBreakingChanges = []Change{ + {ChangeID: "proofkit.agent-workflow.change-plan-route", Summary: "Replace the flat change-workflow-plan CLI route with the hierarchical change plan route while preserving one internal command implementation and its input and output contracts."}, + {ChangeID: "proofkit.cli-contract.omitted-route-policy", Summary: "Make the command-id fallback for an omitted command route an explicit required CLI-contract grammar field; Proofkit source and installed-carrier validators reject contracts that omit or alter this policy."}, +} var currentAdditions = []Change{ - {ChangeID: "proofkit.adoption.transactional-materialization", Summary: "Add separate read-only plan, compare-and-swap apply, and state-bound recovery routes that compile owner-admitted adoption candidates into canonical repository artifacts."}, - {ChangeID: "proofkit.repository.transaction-protocol", Summary: "Add a bounded repository-confined transaction owner with immutable journals, exact before-state checks, deterministic resume, and byte-identical rollback for cooperative writers."}, + {ChangeID: "proofkit.project-state.next-action", Summary: "Add a bounded next command that maps each admitted structural project state to exactly one non-authoritative repository action."}, + {ChangeID: "proofkit.project-state.status", Summary: "Add a read-only status command that classifies a bounded normalized materialized-project and transaction observation; admitted in-bound records bind exact content digests, while unread out-of-bound records identify only their invalid class, without claiming native verification or workflow completion."}, } -var currentMigrationSteps = []string{} +var currentMigrationSteps = []string{ + "Replace agentic-proofkit change-workflow-plan invocations with agentic-proofkit change plan; input and output JSON contracts are unchanged.", + "Update CLI-contract consumers to require commandRouteGrammar.omittedRoutePolicy=command_id; commands without an explicit route continue to resolve to their stable command ID.", +} func validateCurrentChangeRecord(record Record, notes string) error { if !slices.Equal(record.BreakingChanges, currentBreakingChanges) { @@ -221,7 +227,7 @@ func validateCurrentChangeRecord(record Record, notes string) error { func currentExpectedReleaseNotes() string { lines := []string{ - "# @research-engineering/agentic-proofkit 0.8.0", + "# @research-engineering/agentic-proofkit 0.9.0", "", "## Breaking Contract Changes", "", @@ -243,9 +249,13 @@ func currentExpectedReleaseNotes() string { "", "## Migration", "", - "No consumer migration is required.", + "Migration is required:", "", ) + for _, step := range currentMigrationSteps { + lines = append(lines, "- "+step) + } + lines = append(lines, "") lines = append(lines, "## Platform Requirements", "", @@ -258,6 +268,7 @@ func currentExpectedReleaseNotes() string { "- Agent workflow plans, prompts, text, and envelopes are derived guidance and do not execute agents, repository mutations, native witnesses, CI, release, rollout, or production operations.", "- Brief agent-route packets cap pretty JSON at 3072 bytes and may defer oversized argv to explicit full detail; the bound does not claim tokenizer-specific token counts.", "- Complete nested public structural contracts remain blocked under SCHEMA-01; current CLI contracts own exact root variants only.", + "- Project status and next classify materialized repository structure only; they do not execute native verification, validate receipt currentness or trust, or declare workflow completion.", "- The selected requirement-source v2 codec remains internal; current requirement sources are not migrated and no source cutover is claimed.", "- TSX source parsing remains unsupported.", "", @@ -266,7 +277,7 @@ func currentExpectedReleaseNotes() string { "Primary npm channel:", "", "```bash", - "npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.8.0", + "npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.9.0", "```", "", "Pre-1.0 npm consumers must keep this dependency exact-pinned.", @@ -277,7 +288,7 @@ func currentExpectedReleaseNotes() string { "", "## Rollback", "", - "- Pin npm consumers to the previous admitted version 0.7.0 with `npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.7.0`.", + "- Pin npm consumers to the previous admitted version 0.8.0 with `npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.8.0`.", "- Treat local package artifacts as candidates until registry identity is proven.", ) return strings.Join(lines, "\n") + "\n" diff --git a/internal/tools/workflowsmoke/project_navigation_smoke.go b/internal/tools/workflowsmoke/project_navigation_smoke.go new file mode 100644 index 0000000..7f2c832 --- /dev/null +++ b/internal/tools/workflowsmoke/project_navigation_smoke.go @@ -0,0 +1,90 @@ +package workflowsmoke + +import ( + "bytes" + "context" + "errors" + "fmt" + "os" + + "github.com/research-engineering/agentic-proofkit/internal/command/projectstatus" +) + +func verifyProjectNavigation(ctx context.Context, run Runner) (returnErr error) { + repositoryRoot, err := os.MkdirTemp("", "proofkit-workflow-smoke-") + if err != nil { + return fmt.Errorf("create workflow smoke repository: %w", err) + } + defer func() { + returnErr = errors.Join(returnErr, os.RemoveAll(repositoryRoot)) + }() + + expectedStatus, err := projectstatus.Inspect(ctx, repositoryRoot) + if err != nil { + return fmt.Errorf("build expected project status: %w", err) + } + expectedNext, err := projectstatus.NextFromStatus(expectedStatus) + if err != nil { + return fmt.Errorf("build expected project next action: %w", err) + } + expectedStatusText, err := projectstatus.StatusText(expectedStatus) + if err != nil { + return fmt.Errorf("build expected project status text: %w", err) + } + expectedStatusPlain, err := projectstatus.RenderText(expectedStatusText) + if err != nil { + return fmt.Errorf("render expected project status text: %w", err) + } + expectedNextText, err := projectstatus.NextText(expectedNext) + if err != nil { + return fmt.Errorf("build expected project next text: %w", err) + } + expectedNextPlain, err := projectstatus.RenderText(expectedNextText) + if err != nil { + return fmt.Errorf("render expected project next text: %w", err) + } + + status, err := invoke(ctx, run, "project status JSON", unreadInvocation("status", "--repo-root", repositoryRoot)) + if err != nil { + return err + } + if err := verifyExactJSONObject(status, expectedStatus.JSONValue(), "project status JSON"); err != nil { + return err + } + compactStatus, err := invoke(ctx, run, "project status compact JSON", unreadInvocation("--json-layout", "compact", "status", "--repo-root", repositoryRoot)) + if err != nil { + return err + } + if err := verifyExactJSONObject(compactStatus, expectedStatus.JSONValue(), "project status compact JSON"); err != nil { + return err + } + if err := verifyCanonicalCompactJSON(compactStatus.Stdout, expectedStatus.JSONValue()); err != nil { + return fmt.Errorf("project status compact JSON: %w", err) + } + statusText, err := invoke(ctx, run, "project status text", unreadInvocation("status", "--repo-root", repositoryRoot, "--format", "text", "--color", "never")) + if err != nil { + return err + } + if !bytes.Equal(statusText.Stdout, []byte(expectedStatusPlain)) || bytes.Contains(statusText.Stdout, []byte("\x1b[")) { + return fmt.Errorf("project status text does not equal the command-owned plain-text projection") + } + + next, err := invoke(ctx, run, "project next JSON", unreadInvocation("next", "--repo-root", repositoryRoot)) + if err != nil { + return err + } + if err := verifyExactJSONObject(next, expectedNext.JSONValue(), "project next JSON"); err != nil { + return err + } + nextText, err := invoke(ctx, run, "project next text", unreadInvocation("next", "--repo-root", repositoryRoot, "--format", "text", "--color", "never")) + if err != nil { + return err + } + if !bytes.Equal(nextText.Stdout, []byte(expectedNextPlain)) || bytes.Contains(nextText.Stdout, []byte("\x1b[")) { + return fmt.Errorf("project next text does not equal the command-owned plain-text projection") + } + if err := verifyFailure(ctx, run, "project status required root", unreadInvocation("status"), "requires --repo-root"); err != nil { + return err + } + return verifyFailure(ctx, run, "project next JSON color denial", unreadInvocation("next", "--repo-root", repositoryRoot, "--color", "never"), "--color requires --format text") +} diff --git a/internal/tools/workflowsmoke/workflow_smoke.go b/internal/tools/workflowsmoke/workflow_smoke.go index a3c9033..5d5d837 100644 --- a/internal/tools/workflowsmoke/workflow_smoke.go +++ b/internal/tools/workflowsmoke/workflow_smoke.go @@ -75,8 +75,11 @@ func Verify(ctx context.Context, run Runner) error { if err != nil { return fmt.Errorf("build expected native evidence guidance text: %w", err) } + if err := verifyFailure(ctx, run, "retired flat planner route", unreadInvocation("change-workflow-plan", "--input", "-"), "unsupported command"); err != nil { + return err + } - plan, err := invoke(ctx, run, "planner JSON", bytesInvocation(input, "change-workflow-plan", "--input", "-")) + plan, err := invoke(ctx, run, "planner JSON", bytesInvocation(input, "change", "plan", "--input", "-")) if err != nil { return err } @@ -84,7 +87,7 @@ func Verify(ctx context.Context, run Runner) error { return err } - compact, err := invoke(ctx, run, "planner compact JSON", bytesInvocation(input, "--json-layout", "compact", "change-workflow-plan", "--input", "-")) + compact, err := invoke(ctx, run, "planner compact JSON", bytesInvocation(input, "--json-layout", "compact", "change", "plan", "--input", "-")) if err != nil { return err } @@ -95,7 +98,7 @@ func Verify(ctx context.Context, run Runner) error { return fmt.Errorf("planner compact JSON: %w", err) } - envelope, err := invoke(ctx, run, "planner agent envelope", bytesInvocation(input, "change-workflow-plan", "--input", "-", "--agent-envelope")) + envelope, err := invoke(ctx, run, "planner agent envelope", bytesInvocation(input, "change", "plan", "--input", "-", "--agent-envelope")) if err != nil { return err } @@ -103,7 +106,7 @@ func Verify(ctx context.Context, run Runner) error { return err } - text, err := invoke(ctx, run, "planner text", bytesInvocation(input, "change-workflow-plan", "--input", "-", "--format", "text", "--color", "never")) + text, err := invoke(ctx, run, "planner text", bytesInvocation(input, "change", "plan", "--input", "-", "--format", "text", "--color", "never")) if err != nil { return err } @@ -111,23 +114,23 @@ func Verify(ctx context.Context, run Runner) error { return fmt.Errorf("planner text does not equal the command-owned plain-text projection") } - if err := verifyFailure(ctx, run, "required input", bytesInvocation(nil, "change-workflow-plan"), "requires --input "); err != nil { + if err := verifyFailure(ctx, run, "required input", bytesInvocation(nil, "change", "plan"), "requires --input "); err != nil { return err } - if err := verifyFailure(ctx, run, "pre-read input pointer", bytesInvocation(nil, "change-workflow-plan", "--input", "proofkit-smoke-missing-input.json", "--input-pointer", "invalid"), "JSON pointer"); err != nil { + if err := verifyFailure(ctx, run, "pre-read input pointer", bytesInvocation(nil, "change", "plan", "--input", "proofkit-smoke-missing-input.json", "--input-pointer", "invalid"), "JSON pointer"); err != nil { return err } - if err := verifyFailure(ctx, run, "JSON color denial", bytesInvocation(input, "change-workflow-plan", "--input", "-", "--color", "never"), "--color is valid only with --format text"); err != nil { + if err := verifyFailure(ctx, run, "JSON color denial", bytesInvocation(input, "change", "plan", "--input", "-", "--color", "never"), "--color is valid only with --format text"); err != nil { return err } - if err := verifyFailure(ctx, run, "exclusive help", bytesInvocation(input, "change-workflow-plan", "--help", "--format", "text"), "help accepts no additional arguments"); err != nil { + if err := verifyFailure(ctx, run, "exclusive help", bytesInvocation(input, "change", "plan", "--help", "--format", "text"), "help accepts no additional arguments"); err != nil { return err } - if err := verifyFailure(ctx, run, "surplus positional operand", bytesInvocation(input, "change-workflow-plan", "--input", "-", "surplus"), "unsupported argument"); err != nil { + if err := verifyFailure(ctx, run, "surplus positional operand", bytesInvocation(input, "change", "plan", "--input", "-", "surplus"), "unsupported argument"); err != nil { return err } - help, err := invoke(ctx, run, "exclusive help success", unreadInvocation("change-workflow-plan", "--help")) + help, err := invoke(ctx, run, "exclusive help success", unreadInvocation("change", "plan", "--help")) if err != nil { return err } @@ -150,7 +153,8 @@ func Verify(ctx context.Context, run Runner) error { if !bytes.Equal(guidanceText.Stdout, []byte(expectedGuidanceText)) || bytes.Contains(guidanceText.Stdout, []byte("\x1b[")) { return fmt.Errorf("no-input guidance text does not equal the command-owned plain-text projection") } - return nil + + return verifyProjectNavigation(ctx, run) } func bytesInvocation(input []byte, args ...string) Invocation { diff --git a/internal/tools/workflowsmoke/workflow_smoke_test.go b/internal/tools/workflowsmoke/workflow_smoke_test.go index 2fff4ae..517694e 100644 --- a/internal/tools/workflowsmoke/workflow_smoke_test.go +++ b/internal/tools/workflowsmoke/workflow_smoke_test.go @@ -26,17 +26,21 @@ func TestVerifyAcceptsApplicationCLI(t *testing.T) { func TestVerifyRejectsCarrierContractMutations(t *testing.T) { mutations := []struct { - name string - match string - apply func(workflowsmoke.Result) workflowsmoke.Result + name string + match string + matchPrefix bool + apply func(workflowsmoke.Result) workflowsmoke.Result }{ - {name: "planner identity", match: "change-workflow-plan --input -", apply: replaceStdout(`{"reportKind":"wrong"}\n`)}, - {name: "workflow profile identity", match: "change-workflow-plan --input -", apply: replaceStdoutFragment(`"workflowProfileId": "proofkit.reviewed-change.v1"`, `"workflowProfileId": "wrong"`)}, - {name: "compact layout", match: "--json-layout compact change-workflow-plan --input -", apply: replaceStdout("{\n \"reportKind\": \"proofkit.change-workflow-plan\"\n}\n")}, - {name: "envelope identity", match: "change-workflow-plan --input - --agent-envelope", apply: replaceStdout(`{"envelopeId":"wrong"}\n`)}, - {name: "planner text styling", match: "change-workflow-plan --input - --format text --color never", apply: replaceStdout("\x1b[31mChange workflow plan\x1b[0m\n")}, - {name: "planner text suffix", match: "change-workflow-plan --input - --format text --color never", apply: appendStdout("surplus\n")}, - {name: "failure stdout", match: "change-workflow-plan", apply: func(result workflowsmoke.Result) workflowsmoke.Result { + {name: "retired planner route", match: "change-workflow-plan --input -", apply: func(result workflowsmoke.Result) workflowsmoke.Result { + return workflowsmoke.Result{ExitCode: 0, Stdout: []byte("{}\n")} + }}, + {name: "planner identity", match: "change plan --input -", apply: replaceStdout(`{"reportKind":"wrong"}\n`)}, + {name: "workflow profile identity", match: "change plan --input -", apply: replaceStdoutFragment(`"workflowProfileId": "proofkit.reviewed-change.v1"`, `"workflowProfileId": "wrong"`)}, + {name: "compact layout", match: "--json-layout compact change plan --input -", apply: replaceStdout("{\n \"reportKind\": \"proofkit.change-workflow-plan\"\n}\n")}, + {name: "envelope identity", match: "change plan --input - --agent-envelope", apply: replaceStdout(`{"envelopeId":"wrong"}\n`)}, + {name: "planner text styling", match: "change plan --input - --format text --color never", apply: replaceStdout("\x1b[31mChange workflow plan\x1b[0m\n")}, + {name: "planner text suffix", match: "change plan --input - --format text --color never", apply: appendStdout("surplus\n")}, + {name: "failure stdout", match: "change plan", apply: func(result workflowsmoke.Result) workflowsmoke.Result { result.Stdout = []byte("unexpected") return result }}, @@ -44,13 +48,17 @@ func TestVerifyRejectsCarrierContractMutations(t *testing.T) { {name: "guidance applicability", match: "native-evidence-guidance", apply: replaceStdoutFragment(`"applicabilityClass": "always"`, `"applicabilityClass": "external_process"`)}, {name: "guidance middle slot", match: "native-evidence-guidance", apply: replaceStdoutFragment(`"slotId": "output_bounds"`, `"slotId": "wrong"`)}, {name: "guidance text suffix", match: "native-evidence-guidance --format text --color never", apply: appendStdout("surplus\n")}, + {name: "project status identity", match: "status --repo-root ", matchPrefix: true, apply: replaceStdoutFragment(`"reportKind": "proofkit.project-status"`, `"reportKind": "wrong"`)}, + {name: "project next identity", match: "next --repo-root ", matchPrefix: true, apply: replaceStdoutFragment(`"packetKind": "proofkit.project-next-action"`, `"packetKind": "wrong"`)}, } for _, mutation := range mutations { t.Run(mutation.name, func(t *testing.T) { applied := false runner := func(ctx context.Context, invocation workflowsmoke.Invocation) (workflowsmoke.Result, error) { result, err := applicationRunner(ctx, invocation) - if err == nil && !applied && strings.Join(invocation.Args, " ") == mutation.match { + joined := strings.Join(invocation.Args, " ") + matches := joined == mutation.match || (mutation.matchPrefix && strings.HasPrefix(joined, mutation.match)) + if err == nil && !applied && matches { result = mutation.apply(result) applied = true } diff --git a/package-lock.json b/package-lock.json index 9dfc540..50e81dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@research-engineering/agentic-proofkit", - "version": "0.8.0", + "version": "0.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@research-engineering/agentic-proofkit", - "version": "0.8.0", + "version": "0.9.0", "cpu": [ "arm64", "x64" diff --git a/package.json b/package.json index e798ecc..bad6182 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.8.0", + "version": "0.9.0", "type": "module", "license": "MIT", "sideEffects": false, diff --git a/proofkit/cli-contract.v2.json b/proofkit/cli-contract.v2.json index 4076ba6..a6a88d3 100644 --- a/proofkit/cli-contract.v2.json +++ b/proofkit/cli-contract.v2.json @@ -12,7 +12,8 @@ "maximumTokens": 4, "separator": " ", "tokenPattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", - "ambiguityPolicy": "no_route_is_prefix_of_another" + "ambiguityPolicy": "no_route_is_prefix_of_another", + "omittedRoutePolicy": "command_id" }, "globalOptions": { "jsonLayout": { @@ -111,7 +112,7 @@ "rootDefinitionDigest": "sha256:3dbf760a8f8b0a4093c1dd4edc23367c5ef8027b31c18a01490afb657fcc03ad", "nativeSource": { "path": "internal/command/adoptionmaterialization", - "canonicalDigest": "sha256:25fde227d9a26bc6fac85be5963b4f227e5cac06b876b14565a863ff45b40f11", + "canonicalDigest": "sha256:2940a39db54e51167e3f3373aa38525fc734fddc3902ac7d7ed1fdf326066cfe", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -142,17 +143,17 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:0195a152392ad4cecd99a2d0c147ee8e25a034a32e4f730033766399a030c5c3", + "canonicalDigest": "sha256:8d0b8e8bc2bda79e78ad42c0271295c2d3e86865aec80d0a995df93c9c759b3e", "evidenceClass": "source_checkout" }, { "path": "internal/command/adoptionmaterialization", - "canonicalDigest": "sha256:25fde227d9a26bc6fac85be5963b4f227e5cac06b876b14565a863ff45b40f11", + "canonicalDigest": "sha256:2940a39db54e51167e3f3373aa38525fc734fddc3902ac7d7ed1fdf326066cfe", "evidenceClass": "source_checkout" }, { "path": "internal/kernel/repositorytransaction", - "canonicalDigest": "sha256:a0c1d96df99382bd334cd7ddaa3de2d191c5c7f166ece0c720e3791fec87350f", + "canonicalDigest": "sha256:9847e806c891410d428477672b06d32db289bfa89ba2b83868cba08aa76422a0", "evidenceClass": "source_checkout" } ], @@ -249,7 +250,7 @@ "rootDefinitionDigest": "sha256:c18995fb310dbde102bea231d83ec3756ffdf863199acbc88237471d06278c9e", "nativeSource": { "path": "internal/command/adoptionmaterialization", - "canonicalDigest": "sha256:25fde227d9a26bc6fac85be5963b4f227e5cac06b876b14565a863ff45b40f11", + "canonicalDigest": "sha256:2940a39db54e51167e3f3373aa38525fc734fddc3902ac7d7ed1fdf326066cfe", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -280,17 +281,17 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:0195a152392ad4cecd99a2d0c147ee8e25a034a32e4f730033766399a030c5c3", + "canonicalDigest": "sha256:8d0b8e8bc2bda79e78ad42c0271295c2d3e86865aec80d0a995df93c9c759b3e", "evidenceClass": "source_checkout" }, { "path": "internal/command/adoptionmaterialization", - "canonicalDigest": "sha256:25fde227d9a26bc6fac85be5963b4f227e5cac06b876b14565a863ff45b40f11", + "canonicalDigest": "sha256:2940a39db54e51167e3f3373aa38525fc734fddc3902ac7d7ed1fdf326066cfe", "evidenceClass": "source_checkout" }, { "path": "internal/kernel/repositorytransaction", - "canonicalDigest": "sha256:a0c1d96df99382bd334cd7ddaa3de2d191c5c7f166ece0c720e3791fec87350f", + "canonicalDigest": "sha256:9847e806c891410d428477672b06d32db289bfa89ba2b83868cba08aa76422a0", "evidenceClass": "source_checkout" } ], @@ -394,17 +395,17 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:0195a152392ad4cecd99a2d0c147ee8e25a034a32e4f730033766399a030c5c3", + "canonicalDigest": "sha256:8d0b8e8bc2bda79e78ad42c0271295c2d3e86865aec80d0a995df93c9c759b3e", "evidenceClass": "source_checkout" }, { "path": "internal/command/adoptionmaterialization", - "canonicalDigest": "sha256:25fde227d9a26bc6fac85be5963b4f227e5cac06b876b14565a863ff45b40f11", + "canonicalDigest": "sha256:2940a39db54e51167e3f3373aa38525fc734fddc3902ac7d7ed1fdf326066cfe", "evidenceClass": "source_checkout" }, { "path": "internal/kernel/repositorytransaction", - "canonicalDigest": "sha256:a0c1d96df99382bd334cd7ddaa3de2d191c5c7f166ece0c720e3791fec87350f", + "canonicalDigest": "sha256:9847e806c891410d428477672b06d32db289bfa89ba2b83868cba08aa76422a0", "evidenceClass": "source_checkout" } ], @@ -1185,7 +1186,7 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:0195a152392ad4cecd99a2d0c147ee8e25a034a32e4f730033766399a030c5c3", + "canonicalDigest": "sha256:8d0b8e8bc2bda79e78ad42c0271295c2d3e86865aec80d0a995df93c9c759b3e", "evidenceClass": "source_checkout" }, { @@ -1423,6 +1424,10 @@ }, { "command": "change-workflow-plan", + "route": [ + "change", + "plan" + ], "input": "required", "stdin": true, "inputPointer": true, @@ -2728,6 +2733,95 @@ } } }, + { + "command": "next", + "input": "none", + "stdin": false, + "inputPointer": false, + "scopeClass": "explicit_filesystem_scan", + "outputModes": [ + "json", + "text" + ], + "allowedFlags": [ + "--color", + "--format", + "--repo-root" + ], + "requiredFlags": [ + "--repo-root" + ], + "singleOccurrenceFlags": [ + "--color", + "--format", + "--repo-root" + ], + "flagChoices": { + "--color": [ + "auto", + "never" + ], + "--format": [ + "json", + "text" + ] + }, + "flagPresenceRequirements": [ + { + "flag": "--color", + "requiredFlagValues": [ + { + "flag": "--format", + "value": "text" + } + ], + "requiredFlags": [] + } + ], + "outputContract": { + "contractId": "proofkit.next.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.next.output.v1.root-shape", + "rootDefinitionDigest": "sha256:b046def1ec1d608e3c76efd84127df7cd8b0a10a920e7ad9b8b850826432d3af", + "nativeSource": { + "path": "internal/command/projectstatus", + "canonicalDigest": "sha256:0899fabb94eab999a17e1a63924c517a063418e2553f49f09b1eb859bf2bc4ed", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/app/project_status_command_test.go", + "test": "TestNextOutputUsesExactRootShape", + "command": "go test ./internal/app -run '^TestNextOutputUsesExactRootShape$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "one non-executable next-action projection derived from the same bounded project snapshot as status", + "bounded text preserves exactly projectState, actionClass, executable, commandRoute, contextRef, requiredDecision, and issueCodes; packetId, snapshotId, statusRef, and nonClaims remain JSON-only", + "root-shape-only definition proofkit.next.output.v1.root-shape; nested fields, types, cardinalities, and native witness execution remain native-owner claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-WORKFLOW-012", + "REQ-PROOFKIT-WORKFLOW-013", + "REQ-PROOFKIT-WORKFLOW-014", + "REQ-PROOFKIT-WORKFLOW-015" + ], + "flagChoices": { + "--color": [ + "auto", + "never" + ], + "--format": [ + "json", + "text" + ] + } + } + }, { "command": "obligation-decision", "input": "required", @@ -2963,7 +3057,7 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:0195a152392ad4cecd99a2d0c147ee8e25a034a32e4f730033766399a030c5c3", + "canonicalDigest": "sha256:8d0b8e8bc2bda79e78ad42c0271295c2d3e86865aec80d0a995df93c9c759b3e", "evidenceClass": "source_checkout" }, { @@ -6253,7 +6347,7 @@ "rootDefinitionDigest": "sha256:3c842174dff5361e7f83166469b832805e05aa314b073c16234b5b64e346281e", "nativeSource": { "path": "internal/app", - "canonicalDigest": "sha256:0195a152392ad4cecd99a2d0c147ee8e25a034a32e4f730033766399a030c5c3", + "canonicalDigest": "sha256:8d0b8e8bc2bda79e78ad42c0271295c2d3e86865aec80d0a995df93c9c759b3e", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -6282,7 +6376,7 @@ "rootDefinitionDigest": "sha256:0ea95e277ebe44cd2de42c29b47c38686ac0b6b390d8965367437b3fe138e209", "nativeSource": { "path": "internal/app", - "canonicalDigest": "sha256:0195a152392ad4cecd99a2d0c147ee8e25a034a32e4f730033766399a030c5c3", + "canonicalDigest": "sha256:8d0b8e8bc2bda79e78ad42c0271295c2d3e86865aec80d0a995df93c9c759b3e", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { @@ -6505,6 +6599,96 @@ ] } }, + { + "command": "status", + "input": "none", + "stdin": false, + "inputPointer": false, + "scopeClass": "explicit_filesystem_scan", + "outputModes": [ + "json", + "text" + ], + "allowedFlags": [ + "--color", + "--format", + "--repo-root" + ], + "requiredFlags": [ + "--repo-root" + ], + "singleOccurrenceFlags": [ + "--color", + "--format", + "--repo-root" + ], + "flagChoices": { + "--color": [ + "auto", + "never" + ], + "--format": [ + "json", + "text" + ] + }, + "flagPresenceRequirements": [ + { + "flag": "--color", + "requiredFlagValues": [ + { + "flag": "--format", + "value": "text" + } + ], + "requiredFlags": [] + } + ], + "outputContract": { + "contractId": "proofkit.status.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.status.output.v1.root-shape", + "rootDefinitionDigest": "sha256:4d26ca7b7f6be8120fbac40519db02d8e5ae8ba7264208c714fbb9f1cb88ef15", + "nativeSource": { + "path": "internal/command/projectstatus", + "canonicalDigest": "sha256:0899fabb94eab999a17e1a63924c517a063418e2553f49f09b1eb859bf2bc4ed", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/app/project_status_command_test.go", + "test": "TestStatusOutputUsesExactRootShape", + "command": "go test ./internal/app -run '^TestStatusOutputUsesExactRootShape$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "bounded read-only classification over a normalized transaction, manifest, child-currentness, child-admission, and cross-record-closure observation; admitted in-bound records bind exact content digests and unread out-of-bound records bind only their invalid class", + "verification_required means structurally admitted and never means complete, merge-ready, release-ready, or production-ready", + "bounded text preserves exactly projectState, snapshotId, nextAction.actionClass, and issueCodes; project, manifest, transaction, child, closure, action-identity, and nonClaims coordinates remain JSON-only", + "root-shape-only definition proofkit.status.output.v1.root-shape; nested fields, types, cardinalities, and native witness execution remain native-owner claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-WORKFLOW-012", + "REQ-PROOFKIT-WORKFLOW-013", + "REQ-PROOFKIT-WORKFLOW-014", + "REQ-PROOFKIT-WORKFLOW-015" + ], + "flagChoices": { + "--color": [ + "auto", + "never" + ], + "--format": [ + "json", + "text" + ] + } + } + }, { "command": "test-evidence-inventory", "input": "required", @@ -10758,6 +10942,52 @@ }, "canonicalDigest": "sha256:218011a133540f57ef74f8748747e00d33b6757c9f77725ecef39f45fb19423f" }, + { + "definitionId": "proofkit.next.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": [ + "action", + "issueCodes", + "nonClaims", + "packetId", + "packetKind", + "projectState", + "schemaVersion", + "snapshotId", + "statusRef" + ], + "requiredFields": [ + "action", + "issueCodes", + "nonClaims", + "packetId", + "packetKind", + "projectState", + "schemaVersion", + "snapshotId", + "statusRef" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:b046def1ec1d608e3c76efd84127df7cd8b0a10a920e7ad9b8b850826432d3af" + }, { "definitionId": "proofkit.obligation-decision.input.v1.root-shape", "schemaVersion": 1, @@ -15373,6 +15603,54 @@ }, "canonicalDigest": "sha256:338f720a24bd36f4ab34cad9b14d3bd26389119e5c713146f1c4eb019f4a0637" }, + { + "definitionId": "proofkit.status.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": [ + "issueCodes", + "manifestId", + "nextAction", + "nonClaims", + "projectId", + "projectState", + "reportKind", + "schemaVersion", + "snapshotId", + "statusId" + ], + "requiredFields": [ + "issueCodes", + "manifestId", + "nextAction", + "nonClaims", + "projectId", + "projectState", + "reportKind", + "schemaVersion", + "snapshotId", + "statusId" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:4d26ca7b7f6be8120fbac40519db02d8e5ae8ba7264208c714fbb9f1cb88ef15" + }, { "definitionId": "proofkit.test-evidence-inventory.input.v2.root-shape", "schemaVersion": 2, diff --git a/proofkit/command-families.v1.json b/proofkit/command-families.v1.json index 9723151..5ea32b3 100644 --- a/proofkit/command-families.v1.json +++ b/proofkit/command-families.v1.json @@ -68,6 +68,15 @@ "migration-plan" ] }, + { + "familyId": "project-state-navigation", + "label": "Project state navigation", + "purpose": "Classify a materialized project and expose one bounded next action.", + "commands": [ + "next", + "status" + ] + }, { "familyId": "proof-artifact-governance", "label": "Proof artifact governance", diff --git a/proofkit/requirement-bindings.json b/proofkit/requirement-bindings.json index 449eff8..c623c85 100644 --- a/proofkit/requirement-bindings.json +++ b/proofkit/requirement-bindings.json @@ -776,6 +776,14 @@ "proofState": "witness_backed", "nonClaims": ["A source-bound version edge does not authenticate registry publication, provider ingestion, consumer adoption, native witness truth, rollout, or production readiness."] }, + { + "requirementId": "REQ-PROOFKIT-SPEC-035", + "ownerId": "proofkit.spec-proof-core", + "specPath": "docs/specs/proofkit-spec-proof-core/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": ["A source-bound version edge does not authenticate registry publication, provider ingestion, consumer migration, native witness truth, rollout, or production readiness."] + }, { "requirementId": "REQ-PROOFKIT-WORKFLOW-001", "ownerId": "proofkit.agent-workflow", @@ -863,6 +871,41 @@ "claimLevel": "blocking", "proofState": "witness_backed", "nonClaims": ["Public-surface closure does not claim wheel documentation parity and does not prove registry publication, provider ingestion, compatibility on untested platforms, native witness truth, merge approval, rollout, or production readiness."] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-012", + "ownerId": "proofkit.agent-workflow", + "specPath": "docs/specs/proofkit-agent-workflow/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": ["Project status classifies the observed materialized-project snapshot and can become stale immediately after emission; it does not prove native witness execution or complete any declared proof scope."] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-013", + "ownerId": "proofkit.agent-workflow", + "specPath": "docs/specs/proofkit-agent-workflow/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": ["A coherent read snapshot does not prevent later filesystem mutation, authenticate repository ownership, provide power-loss or multi-reader atomicity, exclude a same-user writer that bypasses the repository transaction owner, or identify unread out-of-bound record bytes beyond their normalized invalid class."] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-014", + "ownerId": "proofkit.agent-workflow", + "specPath": "docs/specs/proofkit-agent-workflow/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": ["A next-action packet is derived guidance; it does not execute, authorize, or prove the proposed action."] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-015", + "ownerId": "proofkit.agent-workflow", + "specPath": "docs/specs/proofkit-agent-workflow/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": [ + "A caller-provided stdout writer that accepts a prefix and then fails does not provide an atomic sink, so Proofkit does not claim that such a transport leaves stdout empty.", + "CLI surface closure does not prove registry publication, provider ingestion, consumer migration, native witness truth, rollout, or production readiness." + ] } ], "bindings": [ @@ -1231,6 +1274,29 @@ "local-go" ] }, + { + "requirementId": "REQ-PROOFKIT-PACKAGE-002", + "scenarioId": "proofkit.package-boundary.project-status-output-root-witnesses", + "witnessId": "proofkit.project-status.cli-output-root.exact-witnesses", + "witnessKind": "contract", + "witnessPath": "internal/app/project_status_command_test.go", + "witnessSelectors": [ + { + "selector": "TestNextOutputUsesExactRootShape", + "command": "go test ./internal/app -run '^TestNextOutputUsesExactRootShape$'" + }, + { + "selector": "TestStatusOutputUsesExactRootShape", + "command": "go test ./internal/app -run '^TestStatusOutputUsesExactRootShape$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, { "requirementId": "REQ-PROOFKIT-PACKAGE-003", "scenarioId": "proofkit.package-boundary.outside-consumer-artifact", @@ -2528,6 +2594,29 @@ "local-go" ] }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-004", + "scenarioId": "proofkit.supply-chain-quality.project-status-cli-abi", + "witnessId": "proofkit.project-status.cli-abi.exact-output", + "witnessKind": "contract", + "witnessPath": "internal/app/project_status_command_test.go", + "witnessSelectors": [ + { + "selector": "TestNextOutputUsesExactRootShape", + "command": "go test ./internal/app -run '^TestNextOutputUsesExactRootShape$'" + }, + { + "selector": "TestStatusOutputUsesExactRootShape", + "command": "go test ./internal/app -run '^TestStatusOutputUsesExactRootShape$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, { "requirementId": "REQ-PROOFKIT-QUALITY-004", "scenarioId": "proofkit.supply-chain-quality.cli-contract-topology", @@ -5857,6 +5946,10 @@ "selector": "TestGrammarBoundariesAreExact", "command": "go test ./internal/kernel/commandroute -run '^TestGrammarBoundariesAreExact$'" }, + { + "selector": "TestOmittedRoutePolicyUsesStableCommandIdentity", + "command": "go test ./internal/kernel/commandroute -run '^TestOmittedRoutePolicyUsesStableCommandIdentity$'" + }, { "selector": "TestParseRequiresCanonicalSeparatorAndRoundTrip", "command": "go test ./internal/kernel/commandroute -run '^TestParseRequiresCanonicalSeparatorAndRoundTrip$'" @@ -6145,6 +6238,303 @@ ], "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-SPEC-035", + "scenarioId": "proofkit.spec-proof-core.project-navigation-version-edge", + "witnessId": "proofkit.project-navigation.version-edge-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/app/project_navigation_version_edge_test.go", + "witnessSelectors": [ + { + "selector": "TestProjectNavigationVersionEdgeClosesPublicRoutes", + "command": "go test ./internal/app -run '^TestProjectNavigationVersionEdgeClosesPublicRoutes$'" + }, + { + "selector": "TestProjectNavigationVersionEdgeRejectsCoordinatedChangeRecordDrift", + "command": "go test ./internal/app -run '^TestProjectNavigationVersionEdgeRejectsCoordinatedChangeRecordDrift$'" + }, + { + "selector": "TestProjectNavigationVersionEdgePreservesFrozenPredecessor", + "command": "go test ./internal/app -run '^TestProjectNavigationVersionEdgePreservesFrozenPredecessor$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-012", + "scenarioId": "proofkit.agent-workflow.project-state-total-classification", + "witnessId": "proofkit.project-state.total-classification-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/command/projectstatus/projectstatus_test.go", + "witnessSelectors": [ + { + "selector": "TestEvaluateTotalStateActionTable", + "command": "go test ./internal/command/projectstatus -run '^TestEvaluateTotalStateActionTable$'" + }, + { + "selector": "TestOutputAdmissionRejectsUnreachableClosureCombination", + "command": "go test ./internal/command/projectstatus -run '^TestOutputAdmissionRejectsUnreachableClosureCombination$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-012", + "scenarioId": "proofkit.agent-workflow.project-state-child-admission-owner", + "witnessId": "proofkit.project-state.child-admission-owner-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/command/projectstatus/dependency_test.go", + "witnessSelectors": [ + { + "selector": "TestProjectStatusDelegatesChildAdmissionToMaterializationOwner", + "command": "go test ./internal/command/projectstatus -run '^TestProjectStatusDelegatesChildAdmissionToMaterializationOwner$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-012", + "scenarioId": "proofkit.agent-workflow.project-state-child-owner-delegation", + "witnessId": "proofkit.project-state.child-owner-delegation-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/command/adoptionmaterialization/project_closure_test.go", + "witnessSelectors": [ + { + "selector": "TestAdmitMaterializedProjectRoutesEveryManifestArtifactKindThroughItsOwner", + "command": "go test ./internal/command/adoptionmaterialization -run '^TestAdmitMaterializedProjectRoutesEveryManifestArtifactKindThroughItsOwner$'" + }, + { + "selector": "TestMaterializedProjectRecordSnapshotDoesNotAliasCallerInput", + "command": "go test ./internal/command/adoptionmaterialization -run '^TestMaterializedProjectRecordSnapshotDoesNotAliasCallerInput$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-013", + "scenarioId": "proofkit.agent-workflow.project-state-bounded-inspection", + "witnessId": "proofkit.project-state.bounded-inspection-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/command/projectstatus/inspect_test.go", + "witnessSelectors": [ + { + "selector": "TestInspectClassifiesMaterializedProjectWithoutMutation", + "command": "go test ./internal/command/projectstatus -run '^TestInspectClassifiesMaterializedProjectWithoutMutation$'" + }, + { + "selector": "TestInspectFailsClosedOnSymlinksAndBoundsWithoutDisclosure", + "command": "go test ./internal/command/projectstatus -run '^TestInspectFailsClosedOnSymlinksAndBoundsWithoutDisclosure$'" + }, + { + "selector": "TestInspectRejectsCaseAliasedCanonicalRoute", + "command": "go test ./internal/command/projectstatus -run '^TestInspectRejectsCaseAliasedCanonicalRoute$'" + }, + { + "selector": "TestInspectCohortValidationClosesCleanEpochABA", + "command": "go test ./internal/command/projectstatus -run '^TestInspectCohortValidationClosesCleanEpochABA$'" + }, + { + "selector": "TestInspectCleanupFailureDominatesRetryableSnapshotChange", + "command": "go test ./internal/command/projectstatus -run '^TestInspectCleanupFailureDominatesRetryableSnapshotChange$'" + }, + { + "selector": "TestInspectMapsRecoverableControlState", + "command": "go test ./internal/command/projectstatus -run '^TestInspectMapsRecoverableControlState$'" + }, + { + "selector": "TestInspectMapsInvalidControlState", + "command": "go test ./internal/command/projectstatus -run '^TestInspectMapsInvalidControlState$'" + }, + { + "selector": "TestInspectAttemptRejectsFinalRepositoryRootReplacement", + "command": "go test ./internal/command/projectstatus -run '^TestInspectAttemptRejectsFinalRepositoryRootReplacement$'" + }, + { + "selector": "TestInspectRejectsAdmittedChildrenWithInvalidCrossRecordClosure", + "command": "go test ./internal/command/projectstatus -run '^TestInspectRejectsAdmittedChildrenWithInvalidCrossRecordClosure$'" + }, + { + "selector": "TestInspectRejectsChangingControlEpochAcrossBothAttempts", + "command": "go test ./internal/command/projectstatus -run '^TestInspectRejectsChangingControlEpochAcrossBothAttempts$'" + }, + { + "selector": "TestReadProjectFileEnforcesAggregateBoundBeforeRead", + "command": "go test ./internal/command/projectstatus -run '^TestReadProjectFileEnforcesAggregateBoundBeforeRead$'" + }, + { + "selector": "TestReadProjectFileRejectsSameByteRouteReplacement", + "command": "go test ./internal/command/projectstatus -run '^TestReadProjectFileRejectsSameByteRouteReplacement$'" + }, + { + "selector": "TestInspectDeduplicatesRepeatedIssueCodes", + "command": "go test ./internal/command/projectstatus -run '^TestInspectDeduplicatesRepeatedIssueCodes$'" + }, + { + "selector": "TestOutOfBoundManifestIdentityIsAClassificationNotByteIdentity", + "command": "go test ./internal/command/projectstatus -run '^TestOutOfBoundManifestIdentityIsAClassificationNotByteIdentity$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-013", + "scenarioId": "proofkit.agent-workflow.project-state-control-file-coherence", + "witnessId": "proofkit.project-state.control-file-coherence-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/kernel/repositorytransaction/control_inspection_test.go", + "witnessSelectors": [ + { + "selector": "TestInspectControlFileRejectsGrowthAfterRoutePreflight", + "command": "go test ./internal/kernel/repositorytransaction -run '^TestInspectControlFileRejectsGrowthAfterRoutePreflight$'" + }, + { + "selector": "TestInspectControlFileRejectsSameByteRouteReplacement", + "command": "go test ./internal/kernel/repositorytransaction -run '^TestInspectControlFileRejectsSameByteRouteReplacement$'" + }, + { + "selector": "TestInspectionLeaseDoesNotResolveAReplacementRoot", + "command": "go test ./internal/kernel/repositorytransaction -run '^TestInspectionLeaseDoesNotResolveAReplacementRoot$'" + }, + { + "selector": "TestInspectionLeaseRejectsControlNamespaceCreatedAfterOpen", + "command": "go test ./internal/kernel/repositorytransaction -run '^TestInspectionLeaseRejectsControlNamespaceCreatedAfterOpen$'" + }, + { + "selector": "TestInspectionLeasePinsRootAndExcludesCooperativeWriter", + "command": "go test ./internal/kernel/repositorytransaction -run '^TestInspectionLeasePinsRootAndExcludesCooperativeWriter$'" + }, + { + "selector": "TestInspectionLeaseExportsOnlyReadOnlyFileCapability", + "command": "go test ./internal/kernel/repositorytransaction -run '^TestInspectionLeaseExportsOnlyReadOnlyFileCapability$'" + }, + { + "selector": "TestInspectionLeaseFileCannotBeReassertedAsMutable", + "command": "go test ./internal/kernel/repositorytransaction -run '^TestInspectionLeaseFileCannotBeReassertedAsMutable$'" + }, + { + "selector": "TestInspectControlStateRejectsPartialControlObservations", + "command": "go test ./internal/kernel/repositorytransaction -run '^TestInspectControlStateRejectsPartialControlObservations$'" + }, + { + "selector": "TestInspectControlStateHashesSymlinkTargetsWithoutDisclosingThem", + "command": "go test ./internal/kernel/repositorytransaction -run '^TestInspectControlStateHashesSymlinkTargetsWithoutDisclosingThem$'" + }, + { + "selector": "TestInspectControlStateRejectsClassificationChangeHiddenByPortableNameIdentity", + "command": "go test ./internal/kernel/repositorytransaction -run '^TestInspectControlStateRejectsClassificationChangeHiddenByPortableNameIdentity$'" + }, + { + "selector": "TestInspectControlStateUsesPortableObservationFields", + "command": "go test ./internal/kernel/repositorytransaction -run '^TestInspectControlStateUsesPortableObservationFields$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-013", + "scenarioId": "proofkit.agent-workflow.project-state-exact-route-traversal", + "witnessId": "proofkit.project-state.exact-route-traversal-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/kernel/rootpath/exact_test.go", + "witnessSelectors": [ + { + "selector": "TestOpenExactRegularFileRejectsParentSymlinkABA", + "command": "go test ./internal/kernel/rootpath -run '^TestOpenExactRegularFileRejectsParentSymlinkABA$'" + }, + { + "selector": "TestOpenExactRegularFileRejectsFinalComponentABA", + "command": "go test ./internal/kernel/rootpath -run '^TestOpenExactRegularFileRejectsFinalComponentABA$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-014", + "scenarioId": "proofkit.agent-workflow.project-next-action-output-closure", + "witnessId": "proofkit.project-next-action.output-closure-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/command/projectstatus/projectstatus_test.go", + "witnessSelectors": [ + { + "selector": "TestEvaluateTotalStateActionTable", + "command": "go test ./internal/command/projectstatus -run '^TestEvaluateTotalStateActionTable$'" + }, + { + "selector": "TestOutputAdmissionRejectsReidentifiedStateActionMismatch", + "command": "go test ./internal/command/projectstatus -run '^TestOutputAdmissionRejectsReidentifiedStateActionMismatch$'" + }, + { + "selector": "TestTextProjectionIsBoundedAndSemanticallyDerived", + "command": "go test ./internal/command/projectstatus -run '^TestTextProjectionIsBoundedAndSemanticallyDerived$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-015", + "scenarioId": "proofkit.agent-workflow.project-navigation-public-cli", + "witnessId": "proofkit.project-navigation.public-cli-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/app/project_status_command_test.go", + "witnessSelectors": [ + { + "selector": "TestProjectStatusCLI", + "command": "go test ./internal/app -run '^TestProjectStatusCLI$'" + }, + { + "selector": "TestProjectStatusOutputMatrix", + "command": "go test ./internal/app -run '^TestProjectStatusOutputMatrix$'" + }, + { + "selector": "TestProjectStatusTransportFailureUsesOneBoundedWriteWithoutAtomicSinkClaim", + "command": "go test ./internal/app -run '^TestProjectStatusTransportFailureUsesOneBoundedWriteWithoutAtomicSinkClaim$'" + } + ], + "commandIds": ["proofkit.command-contract-check", "proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-015", + "scenarioId": "proofkit.agent-workflow.project-navigation-installed-carriers", + "witnessId": "proofkit.project-navigation.installed-carrier-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/tools/workflowsmoke/workflow_smoke_test.go", + "witnessSelectors": [ + { + "selector": "TestVerifyAcceptsApplicationCLI", + "command": "go test ./internal/tools/workflowsmoke -run '^TestVerifyAcceptsApplicationCLI$'" + }, + { + "selector": "TestVerifyRejectsCarrierContractMutations", + "command": "go test ./internal/tools/workflowsmoke -run '^TestVerifyRejectsCarrierContractMutations$'" + } + ], + "commandIds": ["proofkit.go-test", "proofkit.package-artifact"], + "environmentClasses": ["local-go", "local-go-python"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-015", + "scenarioId": "proofkit.agent-workflow.project-navigation-version-edge", + "witnessId": "proofkit.project-navigation.public-version-edge-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/app/project_navigation_version_edge_test.go", + "witnessSelectors": [ + { + "selector": "TestProjectNavigationVersionEdgeClosesPublicRoutes", + "command": "go test ./internal/app -run '^TestProjectNavigationVersionEdgeClosesPublicRoutes$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] } ], "witnessCommands": [ diff --git a/release/change-record.v2.json b/release/change-record.v2.json index 4125ae2..41c5ffa 100644 --- a/release/change-record.v2.json +++ b/release/change-record.v2.json @@ -1,22 +1,34 @@ { "schemaVersion": 2, - "previousVersion": "0.7.0", - "version": "0.8.0", - "changeClass": "compatible", - "breakingChanges": [], + "previousVersion": "0.8.0", + "version": "0.9.0", + "changeClass": "breaking", + "breakingChanges": [ + { + "changeId": "proofkit.agent-workflow.change-plan-route", + "summary": "Replace the flat change-workflow-plan CLI route with the hierarchical change plan route while preserving one internal command implementation and its input and output contracts." + }, + { + "changeId": "proofkit.cli-contract.omitted-route-policy", + "summary": "Make the command-id fallback for an omitted command route an explicit required CLI-contract grammar field; Proofkit source and installed-carrier validators reject contracts that omit or alter this policy." + } + ], "additions": [ { - "changeId": "proofkit.adoption.transactional-materialization", - "summary": "Add separate read-only plan, compare-and-swap apply, and state-bound recovery routes that compile owner-admitted adoption candidates into canonical repository artifacts." + "changeId": "proofkit.project-state.next-action", + "summary": "Add a bounded next command that maps each admitted structural project state to exactly one non-authoritative repository action." }, { - "changeId": "proofkit.repository.transaction-protocol", - "summary": "Add a bounded repository-confined transaction owner with immutable journals, exact before-state checks, deterministic resume, and byte-identical rollback for cooperative writers." + "changeId": "proofkit.project-state.status", + "summary": "Add a read-only status command that classifies a bounded normalized materialized-project and transaction observation; admitted in-bound records bind exact content digests, while unread out-of-bound records identify only their invalid class, without claiming native verification or workflow completion." } ], "migration": { - "required": false, - "steps": [] + "required": true, + "steps": [ + "Replace agentic-proofkit change-workflow-plan invocations with agentic-proofkit change plan; input and output JSON contracts are unchanged.", + "Update CLI-contract consumers to require commandRouteGrammar.omittedRoutePolicy=command_id; commands without an explicit route continue to resolve to their stable command ID." + ] }, "platformRequirements": [ "Published Darwin package binaries require macOS 13.0 or later on arm64 and x86_64." @@ -27,6 +39,7 @@ "Agent workflow plans, prompts, text, and envelopes are derived guidance and do not execute agents, repository mutations, native witnesses, CI, release, rollout, or production operations.", "Brief agent-route packets cap pretty JSON at 3072 bytes and may defer oversized argv to explicit full detail; the bound does not claim tokenizer-specific token counts.", "Complete nested public structural contracts remain blocked under SCHEMA-01; current CLI contracts own exact root variants only.", + "Project status and next classify materialized repository structure only; they do not execute native verification, validate receipt currentness or trust, or declare workflow completion.", "The selected requirement-source v2 codec remains internal; current requirement sources are not migrated and no source cutover is claimed.", "TSX source parsing remains unsupported." ], diff --git a/scripts/workflow_package_gate_oracle_test.go b/scripts/workflow_package_gate_oracle_test.go index 77b2f16..30437f1 100644 --- a/scripts/workflow_package_gate_oracle_test.go +++ b/scripts/workflow_package_gate_oracle_test.go @@ -1551,6 +1551,10 @@ func validateExactPlatformSmokeSteps(job githubJob) error { "cache": true, }, }, + { + Name: "Run Darwin filesystem invariants", + Run: "go test ./internal/kernel/rootpath ./internal/kernel/repositorytransaction ./internal/command/projectstatus -count=1", + }, { Name: "Run platform smoke", Run: requiredPlatformSmokeShell, From a30ce80fb330b76a0032c96feb9aa9a490d2e04c Mon Sep 17 00:00:00 2001 From: iperev Date: Sat, 5 Sep 2026 07:09:55 +0200 Subject: [PATCH 2/5] test: close project navigation proof gaps --- .../specs/proofkit-agent-workflow/overview.md | 10 +- .../requirements.v1.json | 4 +- .../proofkit-spec-proof-core/overview.md | 10 +- .../requirements.v1.json | 4 +- internal/app/cli_contract_test.go | 2 +- internal/app/command_contract_generated.go | 14 +- internal/app/command_coverage_routes.go | 2 +- .../project_navigation_abi_closure_test.go | 359 ++++++++++++++++++ .../project_navigation_version_edge_test.go | 69 +--- internal/app/project_status_command_test.go | 18 + .../v0.8.0/preserved-command-contracts.json | 26 -- .../v0.8.0/public-abi-observation.json | 264 +++++++++++++ .../app/testdata/v0.9-wire-observations.json | 2 +- .../command/projectstatus/inspect_test.go | 28 +- .../stackpreset/preset_ids_generated.go | 2 +- .../coveragemetrics/required_inventory.go | 11 +- .../project_navigation_fixture.go | 94 +++++ .../workflowsmoke/project_navigation_smoke.go | 92 ++++- .../workflowsmoke/workflow_smoke_test.go | 28 +- proofkit/cli-contract.v2.json | 14 +- proofkit/requirement-bindings.json | 33 +- 21 files changed, 953 insertions(+), 133 deletions(-) create mode 100644 internal/app/project_navigation_abi_closure_test.go delete mode 100644 internal/app/testdata/releases/v0.8.0/preserved-command-contracts.json create mode 100644 internal/app/testdata/releases/v0.8.0/public-abi-observation.json create mode 100644 internal/tools/workflowsmoke/project_navigation_fixture.go diff --git a/docs/specs/proofkit-agent-workflow/overview.md b/docs/specs/proofkit-agent-workflow/overview.md index 3bcdee1..c576a05 100644 --- a/docs/specs/proofkit-agent-workflow/overview.md +++ b/docs/specs/proofkit-agent-workflow/overview.md @@ -79,10 +79,12 @@ projections remain independently owned by the spec-proof-core package. - `REQ-PROOFKIT-WORKFLOW-012`: one truthful project-state owner, exhaustive precedence, existing child and cross-record closure owners, and no promotion of source declarations or caller status into execution evidence. -- `REQ-PROOFKIT-WORKFLOW-013`: one root-bound inspection lease, cooperative - writer exclusion, descriptor-relative exact-path traversal, bounded - content-cohort validation, fail-closed partial control observations, one - bounded retry, and a portable non-disclosing normalized-observation identity. +- `REQ-PROOFKIT-WORKFLOW-013`: one application-write-free root-bound inspection + lease, cooperative writer exclusion, descriptor-relative exact-path + traversal, bounded content-cohort validation, fail-closed partial control + observations, one bounded retry, and a portable non-disclosing + normalized-observation identity. + Filesystem-owned read metadata such as access time is outside that guarantee. - `REQ-PROOFKIT-WORKFLOW-014`: one total state-to-action table, one bounded next action, explicit owner decisions, and no embedded route universe. - `REQ-PROOFKIT-WORKFLOW-015`: status/next CLI channel and exit semantics, diff --git a/docs/specs/proofkit-agent-workflow/requirements.v1.json b/docs/specs/proofkit-agent-workflow/requirements.v1.json index 510a523..119f176 100644 --- a/docs/specs/proofkit-agent-workflow/requirements.v1.json +++ b/docs/specs/proofkit-agent-workflow/requirements.v1.json @@ -164,12 +164,12 @@ { "requirementId": "REQ-PROOFKIT-WORKFLOW-013", "ownerId": "proofkit.agent-workflow", - "invariant": "Project inspection is explicit, root-confined, non-mutating, symlink-denying, byte-bounded, and normalized-observation-bound: one inspection lease pins one repository root for the whole attempt, exports only exact read-only file capabilities, and, when the transaction control namespace exists, holds its native cooperative writer lock; transaction control is projected by its native owner as clean, recoverable with an exact transaction identity, or invalid with a portable content-bound observation epoch only after the entire namespace is observed within canonical entry, file, aggregate, depth, and file-type bounds, while overflow or unsupported shape fails without a partial packet; every manifest and child route is traversed component by component from the pinned root with exact-name admission, no symlink following, descriptor identity checks, regular-file admission, per-file one-MiB and per-pass aggregate eight-MiB limits before semantic decoding; the root identity and transaction observation are equal before and after the read and a second bounded digest pass over the manifest and every routed child equals the admitted first-pass cohort, or the operation performs at most one complete retry and then fails as concurrent change; every public state receives a snapshot identity over one total tagged observation whose admitted in-bound records bind exact content digests, whose unread out-of-bound records intentionally bind only their invalid class, whose project is unknown or admitted, whose manifest is absent, invalid with an optional bounded content digest, or admitted with identity and content digest, whose transaction carries its tagged state and epoch, whose children are ordered kind/state/expected/observed digest records, and whose cross-record closure state is explicit, without exposing raw bytes, repository paths, or caller text.", + "invariant": "Project inspection is explicit, root-confined, application-write-free, symlink-denying, byte-bounded, and normalized-observation-bound: Proofkit performs no repository mutation operation; one inspection lease pins one repository root for the whole attempt, exports only exact read-only file capabilities, and, when the transaction control namespace exists, holds its native cooperative writer lock; transaction control is projected by its native owner as clean, recoverable with an exact transaction identity, or invalid with a portable content-bound observation epoch only after the entire namespace is observed within canonical entry, file, aggregate, depth, and file-type bounds, while overflow or unsupported shape fails without a partial packet; every manifest and child route is traversed component by component from the pinned root with exact-name admission, no symlink following, descriptor identity checks, regular-file admission, per-file one-MiB and per-pass aggregate eight-MiB limits before semantic decoding; the root identity and transaction observation are equal before and after the read and a second bounded digest pass over the manifest and every routed child equals the admitted first-pass cohort, or the operation performs at most one complete retry and then fails as concurrent change; every public state receives a snapshot identity over one total tagged observation whose admitted in-bound records bind exact content digests, whose unread out-of-bound records intentionally bind only their invalid class, whose project is unknown or admitted, whose manifest is absent, invalid with an optional bounded content digest, or admitted with identity and content digest, whose transaction carries its tagged state and epoch, whose children are ordered kind/state/expected/observed digest records, and whose cross-record closure state is explicit, without exposing raw bytes, repository paths, or caller text.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], "nonClaimRefs": ["NC-PROOFKIT-WORKFLOW-013"], - "nonClaims": ["A coherent read snapshot does not prevent later filesystem mutation, authenticate repository ownership, provide power-loss or multi-reader atomicity, exclude a same-user writer that bypasses the repository transaction owner, or identify unread out-of-bound record bytes beyond their normalized invalid class."], + "nonClaims": ["A coherent read snapshot does not prevent later filesystem mutation, authenticate repository ownership, provide power-loss or multi-reader atomicity, exclude a same-user writer that bypasses the repository transaction owner, identify unread out-of-bound record bytes beyond their normalized invalid class, or prevent the filesystem from updating read-side metadata such as access time."], "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, "deferral": null, "updatePolicy": {"reviewOwnerId": "proofkit.agent-workflow", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} diff --git a/docs/specs/proofkit-spec-proof-core/overview.md b/docs/specs/proofkit-spec-proof-core/overview.md index d137801..e27dfe1 100644 --- a/docs/specs/proofkit-spec-proof-core/overview.md +++ b/docs/specs/proofkit-spec-proof-core/overview.md @@ -199,11 +199,13 @@ execution receipts, and merge policy. - `REQ-PROOFKIT-SPEC-034`: the pre-materialization-to-transactional- materialization public version edge binds all three transactional materialization routes and their exact public contracts to a compatible + byte-frozen 0.8.0 release record without coupling the historical edge to the + live release record or reinterpreting the frozen prior edge. +- `REQ-PROOFKIT-SPEC-035`: the project-state public version edge binds the exact + raw ABI identities, proves the complete semantic ABI difference after + normalizing only native-source digests, and binds status, next, the + change-plan route replacement, and omitted-route policy to one breaking release record without reinterpreting the frozen prior edge. -- `REQ-PROOFKIT-SPEC-035`: the project-state public version edge binds status, - next, and the change-plan route replacement to exact ABI and command-contract - identities plus one breaking release record without reinterpreting the - frozen prior edge. ## 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 4b35b47..14edccb 100644 --- a/docs/specs/proofkit-spec-proof-core/requirements.v1.json +++ b/docs/specs/proofkit-spec-proof-core/requirements.v1.json @@ -689,7 +689,7 @@ { "requirementId": "REQ-PROOFKIT-SPEC-034", "ownerId": "proofkit.spec-proof-core", - "invariant": "The 0.7.0-to-0.8.0 public version edge binds the exact previous and current public ABI digests; the exact addition of adopt materialize plan, apply, and recover with their public routes and input/output contract identities and digests; an explicit added-command selection policy; and the complete ordered additive inventory to one digest-bound current release change record. The edge is compatible, contains no breaking changes or migration steps, and does not mutate or reinterpret the frozen 0.6.0-to-0.7.0 edge.", + "invariant": "The byte-frozen 0.7.0-to-0.8.0 public version edge binds the exact previous and released 0.8.0 public ABI digests; the exact addition of adopt materialize plan, apply, and recover with their public routes and input/output contract identities and digests; an explicit added-command selection policy; and the complete ordered additive inventory to one digest-bound archived 0.8.0 release change record. The edge is compatible, contains no breaking changes or migration steps, and later releases cannot mutate, reinterpret, or bind it to the live current release record or live command metadata.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -702,7 +702,7 @@ { "requirementId": "REQ-PROOFKIT-SPEC-035", "ownerId": "proofkit.spec-proof-core", - "invariant": "The 0.8.0-to-0.9.0 public version edge binds the exact previous and current public ABI digests; the exact addition of status and next with their public routes and output contract identities and digests; the exact replacement of the flat change-workflow-plan route by change plan while preserving its single internal implementation and input/output contract identities and digests; and the complete ordered breaking, additive, and migration inventories to one digest-bound current release change record. The edge rejects the retired route, admits the hierarchical route, and cannot mutate or reinterpret the byte-frozen 0.7.0-to-0.8.0 edge.", + "invariant": "The 0.8.0-to-0.9.0 public version edge binds the exact raw previous and current public ABI digests and proves their complete semantic difference: status and next are the only added commands; their public routes and output contract identities and digests are exact; the flat change-workflow-plan route is replaced by change plan while preserving one internal implementation and the input/output contract identities and digests; every predecessor command is otherwise identical after normalizing only native-source canonical digests; every predecessor contract definition remains exact; added definitions are exactly the transitive closure of the added commands; and omitted-route policy is the only process-contract addition. Native-source digest churn remains bound by the raw edge digests rather than being misclassified as wire-semantic change. The complete ordered breaking, additive, and migration inventories bind to one digest-bound current release change record; the edge rejects the retired route, admits the hierarchical route, and cannot mutate or reinterpret the byte-frozen 0.7.0-to-0.8.0 edge.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index ca36cd8..19beee2 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -24,7 +24,7 @@ import ( ) const ( - cliContractPublicABISHA256 = "17b7f185adb80bcdeb6bc5e6a08cf0b95e6b6f8766d2cba634d08f59b7821e0b" + cliContractPublicABISHA256 = "a331f29499c3dbfd7e03ac4c0d56ce4d71910f735297af79ba0092b543e171f0" maxAggregateFileReadBytesForContractTest = 64 << 20 maxPackageManifestBytesForContractTest = 256 << 10 maxSourceFileBytesForContractTest = 8 << 20 diff --git a/internal/app/command_contract_generated.go b/internal/app/command_contract_generated.go index 57c3cac..1791b46 100644 --- a/internal/app/command_contract_generated.go +++ b/internal/app/command_contract_generated.go @@ -1,7 +1,7 @@ // Code generated by internal/tools/commandcontractgen; DO NOT EDIT. package app -const commandContractSourceSHA256 = "5306b7223c5c0671871272f9790195fa1064de85c638f276493c110a6c3c51ed" +const commandContractSourceSHA256 = "514f09bd74c91e6f1378e8e58483d6f5b0ef926243f37da11080f768a4afba31" type generatedCommandContractMetadata struct { InputContractSHA256 string @@ -12,15 +12,15 @@ type generatedCommandContractMetadata struct { } var generatedCommandContractMetadataByName = map[string]generatedCommandContractMetadata{ - "adopt-materialize-apply": {InputContractSHA256: "sha256:98539b75bf6d4caebc75d724e9201925a1235290791adcebc865c1b3c74976ee", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.apply-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:0d3c0d8ed58376ab4d7e55321016f547261fffd16a6a68c80fce79319b4388ac", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "apply"}}, - "adopt-materialize-plan": {InputContractSHA256: "sha256:65925b56d0332349c5048e9b1450ff0b7a3381224342df1a630128e3f4a2cde2", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.plan-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:e9df1aea8f422b097a28cfc5b395535637d905c7771b11bea805e32de976d11c", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "plan"}}, - "adopt-materialize-recover": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:89f7d0b152f092f2e14ad1b80b847fee5ffc8bffe12471899fcb2f05694d0eae", FlagChoices: map[string][]string{"--action": []string{"resume", "rollback"}, "--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "recover"}}, + "adopt-materialize-apply": {InputContractSHA256: "sha256:98539b75bf6d4caebc75d724e9201925a1235290791adcebc865c1b3c74976ee", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.apply-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:9e3f86a960b327c327beeeab4a52056152bae2b5a4141aed060ec92e5d9c201f", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "apply"}}, + "adopt-materialize-plan": {InputContractSHA256: "sha256:65925b56d0332349c5048e9b1450ff0b7a3381224342df1a630128e3f4a2cde2", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.plan-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:5d458969737cd782d5879fda8747ac3fcc5e434b76d88e7f1aa8baa02d639798", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "plan"}}, + "adopt-materialize-recover": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:8ca5aa7b580c87affc453c7aac40d043387d0187d17eb0f208b1a39ee3b4f3a6", FlagChoices: map[string][]string{"--action": []string{"resume", "rollback"}, "--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "recover"}}, "adopt-plan": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:13e3392d9005c27fed3003a1d036123fe12a341dbe16a97c0cd9f8272fb83320", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}, "--mode": []string{"audit-from-code", "code-baseline", "fresh"}, "--stack": []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"}}, RouteTokens: []string{"adopt", "plan"}}, "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{}, RouteTokens: []string{"adoption-checklist"}}, "adoption-contract-envelope": {InputContractSHA256: "sha256:c310214676ff4b6f536a5bc9d687f681a7e71f73d7a03ac932707d8cd3905cdf", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.adoption-contract-envelope.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3efb2c5161fee16fd8ac6a40dcb6d9c41fbc23e468f60621436ae9e8076e0950", FlagChoices: map[string][]string{}, RouteTokens: []string{"adoption-contract-envelope"}}, "adoption-doctor": {InputContractSHA256: "sha256:efa9acfe32bff07f56d9dc9902530df2979794289bc2f7f547f7a108a7dd0f35", 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:8fdfc6608f197e633f042f20031ae1014872a90aa3daa66885ffcaddca994766", FlagChoices: map[string][]string{}, RouteTokens: []string{"adoption-doctor"}}, "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{}, RouteTokens: []string{"adoption-workflow-plan"}}, - "agent-route": {InputContractSHA256: "sha256:c00e832b4e9eac6b858eec46e810431c0a5c9f56c5c50f055f39ee024f50014c", InputSchemaSummary: []string{"availableInputs", "browserMode", "goal", "knownChangedPaths", "mode", "nonClaims", "observedReports", "openBrowser", "routeId", "schemaVersion", "root-shape-only definition proofkit.agent-route.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:9589fd4f944365160fe15d83b9ef70e51d2e8335204857a284dde9d83061833d", FlagChoices: map[string][]string{}, RouteTokens: []string{"agent-route"}}, + "agent-route": {InputContractSHA256: "sha256:c00e832b4e9eac6b858eec46e810431c0a5c9f56c5c50f055f39ee024f50014c", InputSchemaSummary: []string{"availableInputs", "browserMode", "goal", "knownChangedPaths", "mode", "nonClaims", "observedReports", "openBrowser", "routeId", "schemaVersion", "root-shape-only definition proofkit.agent-route.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:fef80e6b47fa245b27d16c112c7798a44403b1dd2f42bbef7e73ec7339aa8834", FlagChoices: map[string][]string{}, RouteTokens: []string{"agent-route"}}, "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{}, RouteTokens: []string{"binding-partition"}}, "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{}, RouteTokens: []string{"branch-authority"}}, "capability-map-admission": {InputContractSHA256: "sha256:e49433f295c43c34d5d660ac9d656b117ed87208406b57723d25165ffec5d486", 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:bfa35fe1be210ab98f3620694ab63b52a9724f7b92cd9dcbfd7b01b2c6a3555e", FlagChoices: map[string][]string{}, RouteTokens: []string{"capability-map-admission"}}, @@ -45,7 +45,7 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "next": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:9dfa71617f9d727949a988bf726bcf0070460faa59565a8adc468994a36594c6", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"next"}}, "obligation-decision": {InputContractSHA256: "sha256:1dea2ed5c5066451d6d49b815cea99df2cdae2ef05d42fed16c8aeb45eb7f445", 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:96dc074f611bcc12e511bc803c548e4df623e2de869d3add29a3ea6386e04330", FlagChoices: map[string][]string{}, RouteTokens: []string{"obligation-decision"}}, "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{}, RouteTokens: []string{"package-runtime-dependency-admission"}}, - "pilot-admission": {InputContractSHA256: "sha256:a1d9116ce619f7d705349ff4ae44c0f4399a281ebaa9e7d62ea304ac57af59ba", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.pilot-admission.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:9880584ed1fce4a8cea4bbbe4d1fa7f1a69a060ba2ffecb9a44c9ecb18a450cd", FlagChoices: map[string][]string{}, RouteTokens: []string{"pilot-admission"}}, + "pilot-admission": {InputContractSHA256: "sha256:a1d9116ce619f7d705349ff4ae44c0f4399a281ebaa9e7d62ea304ac57af59ba", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.pilot-admission.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:56b1f66f267e0fde973baa69675c5886f652451d81f7f6b9200fd8fb10784a34", FlagChoices: map[string][]string{}, RouteTokens: []string{"pilot-admission"}}, "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{}, RouteTokens: []string{"producer-policy-self-proof"}}, "proof-obligation-algebra": {InputContractSHA256: "sha256:4f176b6bc9bdbd0d96d65c071d66447d246665bda7a23269e7927f1d0b80b043", 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:f9ee9e56b349756c55856a2dab198e1ad85db70a468c38e3aeca73cfe2ed66f6", FlagChoices: map[string][]string{}, RouteTokens: []string{"proof-obligation-algebra"}}, "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{}, RouteTokens: []string{"proof-receipt-admission"}}, @@ -84,7 +84,7 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "selective-gate-evidence": {InputContractSHA256: "sha256:8aa178ab7ca7c475c23707bc4e15fd3f9f8d57acf6f6dcf279677e7769a45586", 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:723569262bb85d9674b2a78d3bcb6e9f4cab229b71e8c784ff1b804a7fcade71", FlagChoices: map[string][]string{}, RouteTokens: []string{"selective-gate-evidence"}}, "selective-gate-obligation-decision-input": {InputContractSHA256: "sha256:85761fcbc0ea94239d55bf379d0592a6ca814e6612a2d609a651f6cdaf8ca10a", 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:ab9dddabe975238d7019266c43350afa2df1a61d4c2eb7bc23afd520b588a2da", FlagChoices: map[string][]string{}, RouteTokens: []string{"selective-gate-obligation-decision-input"}}, "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{}, RouteTokens: []string{"selective-gate-plan"}}, - "self-check": {InputContractSHA256: "sha256:b614b659a5aec20214cf25158d5c7e360d43ee024b5ceaa3308995f259b32bb4", 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:757cc5ebaa9c4dd8f634f8462840f1cfccf62ec3dd55c495b13fc331fc33eb48", FlagChoices: map[string][]string{}, RouteTokens: []string{"self-check"}}, + "self-check": {InputContractSHA256: "sha256:d2b3d3ad93ee66e9e57c2c73093a0fb3878aba6c96d39ca5aab112bf03a16a63", 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:ee7060dce28942b69bdb2b6ba5f6419a5e820f2df0d6b968867c664e26fc1312", FlagChoices: map[string][]string{}, RouteTokens: []string{"self-check"}}, "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{}, RouteTokens: []string{"spec-overview-claims"}}, "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{}, RouteTokens: []string{"spec-proof-bundle-admission"}}, "stack-preset": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:ef5920f363a4a96dcac308ea8412260a06e64ba4876460a369aefb8983130a9d", FlagChoices: map[string][]string{"--preset": []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"}}, RouteTokens: []string{"stack-preset"}}, diff --git a/internal/app/command_coverage_routes.go b/internal/app/command_coverage_routes.go index f75385c..e19f8cf 100644 --- a/internal/app/command_coverage_routes.go +++ b/internal/app/command_coverage_routes.go @@ -156,7 +156,7 @@ var commandCoverageRoutes = map[string][]commandCoverageRoute{ "stack-preset": {directCLIRoute("internal/app/command_coverage_test.go", "TestNoInputCommandsHaveCommandSpecificBehavior", semanticRouteProof("command_coverage.no_input_commands_have_command_specific_behavior"), "Stack preset CLI route must emit JSON and reject unknown preset flags."), packageFalsifierRoute("internal/command/stackpreset/stackpreset_test.go", "TestPresetInventoryIsCompleteDeterministicAndDefensivelyCopied", semanticRouteProof("stackpreset.preset_inventory_is_complete_deterministic_and_defensively_copied"), "Stack preset inventory must keep preset ids aligned with complete non-empty profile records and defensive copies."), packageFalsifierRoute("internal/command/stackpreset/stackpreset_test.go", "TestUnknownPresetIsRejected", semanticRouteProof("stackpreset.unknown_preset_is_rejected"), "Stack preset package API must reject unknown preset ids.")}, "status": { directCLIRoute("internal/app/project_status_command_test.go", "TestProjectStatusCLI", semanticRouteProof("project_status_command.status_whole_cli"), "Project status must preserve owner-admitted bounded classification across JSON, text, color, and pre-I/O argument admission paths."), - packageFalsifierRoute("internal/command/projectstatus/inspect_test.go", "TestInspectClassifiesMaterializedProjectWithoutMutation", semanticRouteProof("projectstatus.inspect_classifies_materialized_project_without_mutation"), "Project status must classify absent, admitted, and stale materialized projects without mutating transaction state or disclosing repository paths."), + packageFalsifierRoute("internal/command/projectstatus/inspect_test.go", "TestInspectClassifiesMaterializedProjectWithoutApplicationWrites", semanticRouteProof("projectstatus.inspect_classifies_materialized_project_without_application_writes"), "Project status must classify absent, admitted, and stale materialized projects without performing repository mutation operations or disclosing repository paths."), packageFalsifierRoute("internal/command/projectstatus/inspect_test.go", "TestInspectCohortValidationClosesCleanEpochABA", semanticRouteProof("projectstatus.inspect_cohort_validation_closes_clean_epoch_aba"), "Project status must reject a clean-state ABA when manifest or child content changes between its bounded observation passes."), }, "test-evidence-inventory": { diff --git a/internal/app/project_navigation_abi_closure_test.go b/internal/app/project_navigation_abi_closure_test.go new file mode 100644 index 0000000..bfa5b7d --- /dev/null +++ b/internal/app/project_navigation_abi_closure_test.go @@ -0,0 +1,359 @@ +package app + +import ( + "bytes" + "crypto/sha256" + "encoding/json" + "fmt" + "os" + "path/filepath" + "slices" + "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/commandroute" + "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" +) + +const frozenProjectNavigationPublicABIPath = "internal/app/testdata/releases/v0.8.0/public-abi-observation.json" +const frozenProjectNavigationPublicABISHA256 = "ffff23ca84e014176d854104ea07eb92b10b4d6db7822f14f7859f6f6d360997" + +const projectNavigationCommandFingerprintPolicy = "semantic_command_contract_without_native_source_digests" + +type frozenProjectNavigationPublicABI struct { + CommandFingerprintPolicy string `json:"commandFingerprintPolicy"` + Commands map[string]string `json:"commands"` + ContractDefinitions map[string]string `json:"contractDefinitions"` + ContractID string `json:"contractId"` + ContractSchemaVersion int `json:"contractSchemaVersion"` + NonClaims []string `json:"nonClaims"` + ObservationKind string `json:"observationKind"` + OrderingPolicy string `json:"orderingPolicy"` + PackageName string `json:"packageName"` + ProcessContractSHA256 string `json:"processContractSha256"` + PublicABISHA256 string `json:"publicAbiSha256"` + ReleaseVersion string `json:"releaseVersion"` + SchemaVersion int `json:"schemaVersion"` +} + +func TestProjectNavigationVersionEdgeClosesCompletePublicABIDiff(t *testing.T) { + frozen := readFrozenProjectNavigationPublicABI(t) + current := readProjectNavigationCLIContractRaw(t) + if err := verifyCompleteProjectNavigationABIDiff(frozen, current); err != nil { + t.Fatal(err) + } +} + +func TestProjectNavigationVersionEdgeRejectsUndeclaredPublicABIDrift(t *testing.T) { + frozen := readFrozenProjectNavigationPublicABI(t) + current := readProjectNavigationCLIContractRaw(t) + commands := current["commands"].([]any) + for index, raw := range commands { + record := raw.(map[string]any) + if record["command"] != "impact" { + continue + } + mutant := clonePublicABIRecord(record) + mutant["route"] = []any{"impact-drift"} + commands[index] = mutant + if err := verifyCompleteProjectNavigationABIDiff(frozen, current); err == nil { + t.Fatal("undeclared existing-command ABI drift was admitted") + } + return + } + t.Fatal("current CLI contract is missing impact") +} + +func readFrozenProjectNavigationPublicABI(t *testing.T) frozenProjectNavigationPublicABI { + t.Helper() + content, err := os.ReadFile(filepath.Join(repoRoot(t), frozenProjectNavigationPublicABIPath)) + if err != nil { + t.Fatal(err) + } + sum := sha256.Sum256(content) + if got := fmt.Sprintf("%x", sum); got != frozenProjectNavigationPublicABISHA256 { + t.Fatalf("frozen public ABI observation digest=%s, want %s", got, frozenProjectNavigationPublicABISHA256) + } + value, err := admission.DecodeJSON(bytes.NewReader(content), int64(len(content))) + if err != nil { + t.Fatal(err) + } + root, ok := value.(map[string]any) + if !ok { + t.Fatal("frozen public ABI observation must be an object") + } + assertExactObjectKeys(t, root, []string{"commandFingerprintPolicy", "commands", "contractDefinitions", "contractId", "contractSchemaVersion", "nonClaims", "observationKind", "orderingPolicy", "packageName", "processContractSha256", "publicAbiSha256", "releaseVersion", "schemaVersion"}, "frozen public ABI observation") + var observation frozenProjectNavigationPublicABI + if err := json.Unmarshal(content, &observation); err != nil { + t.Fatal(err) + } + if observation.SchemaVersion != 1 || observation.ObservationKind != "proofkit.frozen-public-abi-observation" || observation.ReleaseVersion != "0.8.0" || observation.ContractID != "proofkit.cli-contract.v2" || observation.ContractSchemaVersion != 2 || observation.PackageName != "@research-engineering/agentic-proofkit" || observation.OrderingPolicy != "lexicographic_by_identity" || observation.CommandFingerprintPolicy != projectNavigationCommandFingerprintPolicy || observation.PublicABISHA256 != "sha256:b5ea707ee5851cea6b75442e4faf20e93879371faf3636e96a98ccd23b527463" || len(observation.Commands) == 0 || len(observation.ContractDefinitions) == 0 || !slices.Equal(observation.NonClaims, []string{"Per-command fingerprints omit only native source canonical digests; the exact raw contract remains bound by publicAbiSha256.", "This frozen source observation does not authenticate registry publication, provider state, consumer migration, or runtime compatibility."}) { + t.Fatalf("frozen public ABI observation is invalid: %#v", observation) + } + for context, values := range map[string]map[string]string{"command": observation.Commands, "definition": observation.ContractDefinitions} { + for id, value := range values { + if id == "" { + t.Fatalf("frozen %s identity is empty", context) + } + if _, err := admit.SHA256Ref(value, "frozen "+context+" digest"); err != nil { + t.Fatal(err) + } + } + } + if _, err := admit.SHA256Ref(observation.ProcessContractSHA256, "frozen process contract digest"); err != nil { + t.Fatal(err) + } + return observation +} + +func readProjectNavigationCLIContractRaw(t *testing.T) map[string]any { + t.Helper() + content, err := os.ReadFile(filepath.Join(repoRoot(t), "proofkit", "cli-contract.v2.json")) + if err != nil { + t.Fatal(err) + } + value, err := admission.DecodeJSON(bytes.NewReader(content), int64(len(content))) + if err != nil { + t.Fatal(err) + } + root, ok := value.(map[string]any) + if !ok { + t.Fatal("current CLI contract must be an object") + } + return root +} + +func verifyCompleteProjectNavigationABIDiff(frozen frozenProjectNavigationPublicABI, current map[string]any) error { + schemaVersion, err := admit.CanonicalInteger(current["schemaVersion"], "current CLI contract schemaVersion") + if err != nil || int(schemaVersion) != frozen.ContractSchemaVersion || current["contractId"] != frozen.ContractID || current["packageName"] != frozen.PackageName { + return fmt.Errorf("current CLI contract header differs from the frozen predecessor") + } + commands, commandOrder, err := indexPublicABIRecords(current["commands"], "command") + if err != nil { + return err + } + if !slices.IsSorted(commandOrder) { + return fmt.Errorf("current CLI command order is not canonical") + } + addedCommands := differenceKeys(commands, frozen.Commands) + if !slices.Equal(addedCommands, []string{"next", "status"}) { + return fmt.Errorf("current CLI contract has undeclared command additions: %v", addedCommands) + } + for name, wantDigest := range frozen.Commands { + record, ok := commands[name] + if !ok { + return fmt.Errorf("current CLI contract removed predecessor command %s", name) + } + if name == "change-workflow-plan" { + record = clonePublicABIRecord(record) + delete(record, "route") + } + normalized, err := normalizePublicABICommandFingerprint(record) + if err != nil { + return fmt.Errorf("normalize current command %s: %w", name, err) + } + gotDigest, err := digest.StableJSONSHA256Ref(normalized) + if err != nil { + return fmt.Errorf("fingerprint current command %s: %w", name, err) + } + if gotDigest != wantDigest { + return fmt.Errorf("current CLI command %s has undeclared ABI drift", name) + } + } + + definitions, definitionOrder, err := indexPublicABIRecords(current["contractDefinitions"], "definitionId") + if err != nil { + return err + } + if !slices.IsSorted(definitionOrder) { + return fmt.Errorf("current CLI definition order is not canonical") + } + for id, wantDigest := range frozen.ContractDefinitions { + record, ok := definitions[id] + if !ok { + return fmt.Errorf("current CLI contract removed predecessor definition %s", id) + } + gotDigest, err := digest.StableJSONSHA256Ref(record) + if err != nil { + return fmt.Errorf("fingerprint current definition %s: %w", id, err) + } + if gotDigest != wantDigest { + return fmt.Errorf("current CLI definition %s has undeclared ABI drift", id) + } + } + addedDefinitions := differenceKeys(definitions, frozen.ContractDefinitions) + expectedDefinitions, err := addedCommandDefinitionClosure(commands, definitions, frozen.ContractDefinitions, []string{"next", "status"}) + if err != nil { + return err + } + if !slices.Equal(addedDefinitions, expectedDefinitions) { + return fmt.Errorf("current CLI definition additions are not exactly closed by added commands: got %v want %v", addedDefinitions, expectedDefinitions) + } + + process, ok := current["processContract"].(map[string]any) + if !ok { + return fmt.Errorf("current CLI process contract is invalid") + } + normalizedProcess := clonePublicABIRecord(process) + grammar, ok := process["commandRouteGrammar"].(map[string]any) + if !ok { + return fmt.Errorf("current CLI command route grammar is invalid") + } + normalizedGrammar := clonePublicABIRecord(grammar) + if normalizedGrammar["omittedRoutePolicy"] != commandroute.OmittedRoutePolicy { + return fmt.Errorf("current CLI omitted route policy is invalid") + } + delete(normalizedGrammar, "omittedRoutePolicy") + normalizedProcess["commandRouteGrammar"] = normalizedGrammar + processDigest, err := digest.StableJSONSHA256Ref(normalizedProcess) + if err != nil { + return fmt.Errorf("fingerprint normalized process contract: %w", err) + } + if processDigest != frozen.ProcessContractSHA256 { + return fmt.Errorf("current CLI process contract has undeclared ABI drift") + } + return nil +} + +func indexPublicABIRecords(raw any, identityField string) (map[string]map[string]any, []string, error) { + values, ok := raw.([]any) + if !ok { + return nil, nil, fmt.Errorf("CLI contract %s inventory must be an array", identityField) + } + indexed := make(map[string]map[string]any, len(values)) + order := make([]string, 0, len(values)) + for _, value := range values { + record, ok := value.(map[string]any) + if !ok { + return nil, nil, fmt.Errorf("CLI contract %s record must be an object", identityField) + } + identity, ok := record[identityField].(string) + if !ok || identity == "" { + return nil, nil, fmt.Errorf("CLI contract %s record has no identity", identityField) + } + if _, exists := indexed[identity]; exists { + return nil, nil, fmt.Errorf("CLI contract repeats %s %s", identityField, identity) + } + indexed[identity] = record + order = append(order, identity) + } + return indexed, order, nil +} + +func addedCommandDefinitionClosure(commands, definitions map[string]map[string]any, frozenDefinitions map[string]string, commandNames []string) ([]string, error) { + queue := []string{} + for _, name := range commandNames { + command, ok := commands[name] + if !ok { + return nil, fmt.Errorf("current CLI contract is missing added command %s", name) + } + for _, field := range []string{"inputContract", "outputContract"} { + contract, ok := command[field].(map[string]any) + if !ok { + continue + } + if root, ok := contract["rootDefinitionRef"].(string); ok && root != "" { + queue = append(queue, root) + } + } + } + visited := map[string]bool{} + result := []string{} + for len(queue) > 0 { + id := queue[0] + queue = queue[1:] + if visited[id] { + continue + } + visited[id] = true + definition, ok := definitions[id] + if !ok { + return nil, fmt.Errorf("added command references missing definition %s", id) + } + if _, existed := frozenDefinitions[id]; !existed { + result = append(result, id) + } + references, ok := definition["definitionRefs"].([]any) + if !ok { + return nil, fmt.Errorf("definition %s has invalid definitionRefs", id) + } + for _, raw := range references { + reference, ok := raw.(string) + if !ok || reference == "" { + return nil, fmt.Errorf("definition %s has invalid referenced identity", id) + } + queue = append(queue, reference) + } + } + slices.Sort(result) + return result, nil +} + +func differenceKeys[V any, W any](current map[string]V, previous map[string]W) []string { + result := []string{} + for key := range current { + if _, exists := previous[key]; !exists { + result = append(result, key) + } + } + slices.Sort(result) + return result +} + +func clonePublicABIRecord(value map[string]any) map[string]any { + clone := make(map[string]any, len(value)) + for key, item := range value { + clone[key] = item + } + return clone +} + +func normalizePublicABICommandFingerprint(value map[string]any) (map[string]any, error) { + normalized := clonePublicABIRecord(value) + for _, contractField := range []string{"inputContract", "outputContract"} { + rawContract, exists := normalized[contractField] + if !exists || rawContract == nil { + continue + } + contract, ok := rawContract.(map[string]any) + if !ok { + return nil, fmt.Errorf("%s must be an object", contractField) + } + contract = clonePublicABIRecord(contract) + if rawSource, exists := contract["nativeSource"]; exists { + source, ok := rawSource.(map[string]any) + if !ok { + return nil, fmt.Errorf("%s nativeSource must be an object", contractField) + } + source = clonePublicABIRecord(source) + if _, exists := source["canonicalDigest"]; !exists { + return nil, fmt.Errorf("%s nativeSource has no canonicalDigest", contractField) + } + delete(source, "canonicalDigest") + contract["nativeSource"] = source + } + if rawSources, exists := contract["nativeSources"]; exists { + sources, ok := rawSources.([]any) + if !ok { + return nil, fmt.Errorf("%s nativeSources must be an array", contractField) + } + normalizedSources := make([]any, len(sources)) + for index, rawSource := range sources { + source, ok := rawSource.(map[string]any) + if !ok { + return nil, fmt.Errorf("%s nativeSources[%d] must be an object", contractField, index) + } + source = clonePublicABIRecord(source) + if _, exists := source["canonicalDigest"]; !exists { + return nil, fmt.Errorf("%s nativeSources[%d] has no canonicalDigest", contractField, index) + } + delete(source, "canonicalDigest") + normalizedSources[index] = source + } + contract["nativeSources"] = normalizedSources + } + normalized[contractField] = contract + } + return normalized, nil +} diff --git a/internal/app/project_navigation_version_edge_test.go b/internal/app/project_navigation_version_edge_test.go index 429cd77..fd30774 100644 --- a/internal/app/project_navigation_version_edge_test.go +++ b/internal/app/project_navigation_version_edge_test.go @@ -19,8 +19,6 @@ import ( const projectNavigationVersionEdgePath = "internal/app/testdata/v0.9-wire-observations.json" const frozenProjectNavigationPredecessorPath = "internal/app/testdata/v0.8-wire-observations.json" const frozenProjectNavigationPredecessorSHA256 = "ed0651c53c015c00d8ed7a0db681a213e9df6248302c5f12fc898e4b6a82c5ab" -const frozenProjectNavigationCommandContractPath = "internal/app/testdata/releases/v0.8.0/preserved-command-contracts.json" -const frozenProjectNavigationCommandContractSHA256 = "907259153bb1e45e982295ec6081b40eb9f02b219c25af6004eb8c29a12c328a" type projectNavigationVersionEdge struct { AddedCommandContracts []versionEdgeCommandContract `json:"addedCommandContracts"` @@ -58,27 +56,6 @@ type versionEdgeRouteReplacement struct { PreviousRoute []string `json:"previousRoute"` } -type frozenProjectNavigationCommandContract struct { - Command string `json:"command"` - CommandRouteGrammar frozenCommandRouteGrammar `json:"commandRouteGrammar"` - InputContract versionEdgeContractIdentity `json:"inputContract"` - NonClaims []string `json:"nonClaims"` - ObservationKind string `json:"observationKind"` - OutputContract versionEdgeContractIdentity `json:"outputContract"` - PublicABISHA256 string `json:"publicAbiSha256"` - ReleaseVersion string `json:"releaseVersion"` - Route []string `json:"route"` - SchemaVersion int `json:"schemaVersion"` -} - -type frozenCommandRouteGrammar struct { - AmbiguityPolicy string `json:"ambiguityPolicy"` - MaximumTokens int `json:"maximumTokens"` - MinimumTokens int `json:"minimumTokens"` - Separator string `json:"separator"` - TokenPattern string `json:"tokenPattern"` -} - func TestProjectNavigationVersionEdgeClosesPublicRoutes(t *testing.T) { record := readProjectNavigationVersionEdge(t) root := repoRoot(t) @@ -186,53 +163,13 @@ func TestProjectNavigationVersionEdgePreservesFrozenPredecessor(t *testing.T) { if got := fmt.Sprintf("%x", digest); got != frozenProjectNavigationPredecessorSHA256 { t.Fatalf("frozen predecessor digest=%s, want %s", got, frozenProjectNavigationPredecessorSHA256) } - frozen := readFrozenProjectNavigationCommandContract(t) + frozen := readFrozenProjectNavigationPublicABI(t) record := readProjectNavigationVersionEdge(t) - replacement := record.ChangedCommandRoutes[0] - if record.PreviousVersion != frozen.ReleaseVersion || record.PreviousPublicABISHA256 != frozen.PublicABISHA256 || replacement.Command != frozen.Command || !slices.Equal(replacement.PreviousRoute, frozen.Route) || replacement.PreservedInputContract != frozen.InputContract || replacement.PreservedOutputContract != frozen.OutputContract { - t.Fatalf("version edge does not preserve the frozen predecessor contract: edge=%#v frozen=%#v", replacement, frozen) + if record.PreviousVersion != frozen.ReleaseVersion || record.PreviousPublicABISHA256 != frozen.PublicABISHA256 { + t.Fatalf("version edge does not preserve the frozen predecessor ABI: edge=%#v frozen=%#v", record, frozen) } } -func readFrozenProjectNavigationCommandContract(t *testing.T) frozenProjectNavigationCommandContract { - t.Helper() - content, err := os.ReadFile(filepath.Join(repoRoot(t), frozenProjectNavigationCommandContractPath)) - if err != nil { - t.Fatal(err) - } - digest := sha256.Sum256(content) - if got := fmt.Sprintf("%x", digest); got != frozenProjectNavigationCommandContractSHA256 { - t.Fatalf("frozen command contract digest=%s, want %s", got, frozenProjectNavigationCommandContractSHA256) - } - value, err := admission.DecodeJSON(bytes.NewReader(content), int64(len(content))) - if err != nil { - t.Fatal(err) - } - root, ok := value.(map[string]any) - if !ok { - t.Fatal("frozen command contract observation must be an object") - } - assertExactObjectKeys(t, root, []string{"command", "commandRouteGrammar", "inputContract", "nonClaims", "observationKind", "outputContract", "publicAbiSha256", "releaseVersion", "route", "schemaVersion"}, "frozen command contract observation") - assertExactObjectKeys(t, root["commandRouteGrammar"].(map[string]any), []string{"ambiguityPolicy", "maximumTokens", "minimumTokens", "separator", "tokenPattern"}, "frozen command route grammar") - assertExactObjectKeys(t, root["inputContract"].(map[string]any), []string{"contractId", "contractSha256"}, "frozen input contract") - assertExactObjectKeys(t, root["outputContract"].(map[string]any), []string{"contractId", "contractSha256"}, "frozen output contract") - var record frozenProjectNavigationCommandContract - if err := json.Unmarshal(content, &record); err != nil { - t.Fatal(err) - } - wantGrammar := frozenCommandRouteGrammar{ - AmbiguityPolicy: "no_route_is_prefix_of_another", - MaximumTokens: 4, - MinimumTokens: 1, - Separator: " ", - TokenPattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$", - } - if record.SchemaVersion != 1 || record.ObservationKind != "proofkit.frozen-command-contract-observation" || record.ReleaseVersion != "0.8.0" || record.PublicABISHA256 != "sha256:b5ea707ee5851cea6b75442e4faf20e93879371faf3636e96a98ccd23b527463" || record.Command != "change-workflow-plan" || record.CommandRouteGrammar != wantGrammar || !slices.Equal(record.Route, []string{"change-workflow-plan"}) || !slices.Equal(record.NonClaims, []string{"This frozen source observation does not authenticate registry publication, provider state, consumer migration, or runtime compatibility."}) { - t.Fatalf("frozen command contract observation is invalid: %#v", record) - } - return record -} - func readProjectNavigationVersionEdge(t *testing.T) projectNavigationVersionEdge { t.Helper() content, err := os.ReadFile(filepath.Join(repoRoot(t), projectNavigationVersionEdgePath)) diff --git a/internal/app/project_status_command_test.go b/internal/app/project_status_command_test.go index dc39ec9..360c3d4 100644 --- a/internal/app/project_status_command_test.go +++ b/internal/app/project_status_command_test.go @@ -2,6 +2,7 @@ package app import ( "bytes" + "context" "errors" "os" "path/filepath" @@ -9,6 +10,7 @@ import ( "testing" "github.com/research-engineering/agentic-proofkit/internal/command/projectstatus" + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" "github.com/research-engineering/agentic-proofkit/internal/kernel/commandroute" "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" @@ -237,6 +239,22 @@ func TestProjectStatusTransportFailureUsesOneBoundedWriteWithoutAtomicSinkClaim( } } +func TestProjectStatusCLIHonorsCanceledContextBeforeOutput(t *testing.T) { + for _, command := range []string{"next", "status"} { + t.Run(command, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + var stdout bytes.Buffer + var stderr bytes.Buffer + repositoryRoot := t.TempDir() + code := RunWithRendererAndCapabilities(ctx, []string{command, "--repo-root", repositoryRoot}, panicReader{}, &stdout, &stderr, cliexec.PathRenderer(), PresentationCapabilities{}) + if code != 1 || stdout.Len() != 0 || stderr.Len() == 0 || strings.Contains(stderr.String(), repositoryRoot) { + t.Fatalf("%s cancellation exit=%d stdout=%q stderr=%q", command, code, stdout.String(), stderr.String()) + } + }) + } +} + type prefixThenErrorWriter struct { bytes.Buffer calls int diff --git a/internal/app/testdata/releases/v0.8.0/preserved-command-contracts.json b/internal/app/testdata/releases/v0.8.0/preserved-command-contracts.json deleted file mode 100644 index 163e251..0000000 --- a/internal/app/testdata/releases/v0.8.0/preserved-command-contracts.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "schemaVersion": 1, - "observationKind": "proofkit.frozen-command-contract-observation", - "releaseVersion": "0.8.0", - "publicAbiSha256": "sha256:b5ea707ee5851cea6b75442e4faf20e93879371faf3636e96a98ccd23b527463", - "commandRouteGrammar": { - "minimumTokens": 1, - "maximumTokens": 4, - "separator": " ", - "tokenPattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", - "ambiguityPolicy": "no_route_is_prefix_of_another" - }, - "command": "change-workflow-plan", - "route": ["change-workflow-plan"], - "inputContract": { - "contractId": "proofkit.change-workflow-plan.input.v1", - "contractSha256": "sha256:e3124fc636b7f66b24daf8e1435cea11da15a741abeabe0cc3d3890b13c71625" - }, - "outputContract": { - "contractId": "proofkit.change-workflow-plan.output.v1", - "contractSha256": "sha256:cd035e9b71d83c341b1a937a18699fd727cb4b0d694983d715b064292ae4d8bd" - }, - "nonClaims": [ - "This frozen source observation does not authenticate registry publication, provider state, consumer migration, or runtime compatibility." - ] -} diff --git a/internal/app/testdata/releases/v0.8.0/public-abi-observation.json b/internal/app/testdata/releases/v0.8.0/public-abi-observation.json new file mode 100644 index 0000000..1f6e786 --- /dev/null +++ b/internal/app/testdata/releases/v0.8.0/public-abi-observation.json @@ -0,0 +1,264 @@ +{ + "commandFingerprintPolicy": "semantic_command_contract_without_native_source_digests", + "commands": { + "adopt-materialize-apply": "sha256:5f0d52c00cf834dc9ac6c8a40a29cbb441cad8f5c7b8a78999f09a555a3c8e34", + "adopt-materialize-plan": "sha256:421b9f2eeb783b683057c90b46d3184535dfb4e30be799566c3055978852ffcd", + "adopt-materialize-recover": "sha256:b24eddc0b49e73081d6b53439b8c637114f1bbfe8522f8324d1bddec32f120df", + "adopt-plan": "sha256:bf7bc41a2093727deb359c080c42734c4157c93c955ae13f83677f428309846f", + "adoption-checklist": "sha256:4301bde365eea518049173e060d61f1a2d6a12acb7f02685447d3fcd210955b0", + "adoption-contract-envelope": "sha256:24b7fcc6a98f5db44c079707d4e093bff75c5416fb9414ed0074af4cfdb8926a", + "adoption-doctor": "sha256:798b77fbc8e69874cacbf6685d93fa0e92366ad789f280ac1b0f5c85cb7e2cd2", + "adoption-workflow-plan": "sha256:78d6346f3fd49c43e33d12a5dea19bd4d5cb6fa022f7d583d39f94bd92259305", + "agent-route": "sha256:92e5a84a888a0e92d438c9d40d7f8ef3d1a22dcc7abb0367a6310183cc64589c", + "binding-partition": "sha256:7d9812754e8841ddf7cd955dd799815f025a69a392b41893d95ef1a1d06ea446", + "branch-authority": "sha256:42583306c75e9efa20f603c6419d368461f061e4ab83263291aa320e23691d32", + "capability-map-admission": "sha256:bbf7969228241619ef59403284566066e225b365140a01e413657d1e4f9167b2", + "change-workflow-plan": "sha256:96d2e85361870b15a9ad84658fc4c931019f1dbfda59bb0b88f97e96829ff7e2", + "changed-path-set": "sha256:ea8d8df0e6136f84d98bc127408f1bfeed1aaee850bb8f4b0b2604896a4f39e3", + "completion-criteria": "sha256:00a9d3cde3173dafba0f682564030c457d0fd0f61de9fbc19bdbb1cc2e3e00ac", + "conformance-profile": "sha256:c551d279669ce56b24b9326394656c9dfd6eb47c1a0c276c1670886db1d19e38", + "custom-rule-boundary": "sha256:f09cbd55617b4502190119b6c60eaa5500ae1edae73bc90cc08f4340d2872c6c", + "deployment-evidence-admission": "sha256:30beda51eef07b7470c02f14289b5a49ee72460ec54d3f8efc1ee6f123339b59", + "document-lifecycle-boundary": "sha256:c9285694816ae8ca7ecd2ae501acf3074ac2518766a22a528dfbf7fe1595c7d8", + "evidence-graph": "sha256:adf2f5a6822d058d6fbf66541159fa2c87cc40a6d7f2c67d0029fdcf264cd483", + "external-consumer": "sha256:498d350faa3e85cf6169e13a11fd2b13b40fdc59a53a95ae00e433dd64c84804", + "gradual-adoption": "sha256:39a43397353855c737e152fc944ca9376e8de5480f202dc65c6ba6b623fb3ce4", + "gradual-adoption-bootstrap": "sha256:ab4359514cea255f75e94fb4669487f4da56eda0728195be9dfe4c8576d98314", + "gradual-adoption-guidance": "sha256:5da4401fe574b6206c8242bd4a559939d25be86fde636df500511eb419301d73", + "help": "sha256:c7c895acccb0bd6e4763f27d93992b9690071cf22927f1e3a5182c894cd695fe", + "impact": "sha256:0351c86bf7b8ad604554230e790af777e70fb7fc6010d0d7c41e0c2539ea4a39", + "json-report-cli-adapter-source": "sha256:ae3e7188f2a297c77053bc620878f95da9a06518d86be329b53b179aeaf5536b", + "migration-parity-admission": "sha256:b9094e7e44e7f71589b98299d2fd9d84f22b9986b90ed63226a6660d7f35e476", + "migration-plan": "sha256:59a1da7205e8d1eaeac6425c6ae601b349a4310a35e4bc83692058c24cb3fed1", + "native-evidence-guidance": "sha256:66ef6fe61beeb2142f8fdf0a4486c855b270781fcc1ca4f07191482fdcbd8a5f", + "obligation-decision": "sha256:daba2f642ea83778e0e2fd1138d20ea8f21ae30fd8091098d0cd5088e100b4e9", + "package-runtime-dependency-admission": "sha256:ea4de2c2339eff9768d00b64ed73034ca021fd8fcd0c54571bfbb5c97ff4347d", + "pilot-admission": "sha256:6312cc5ca823d64a4f2907babc6cdbe64d185f561447e9467edccbd51b138dd9", + "producer-policy-self-proof": "sha256:f221d2e12a6dd0fbd0b8dd22365eaf4c170544abf15ea40a11c3fa6a793652db", + "proof-obligation-algebra": "sha256:40a0bdbcd38f6ea3abe35131b74698745bb3ee262bb12c5c1c47ed50d87d455e", + "proof-receipt-admission": "sha256:ad0bdb8d01674a8dcf7d01d47a2766a491554f0f34229bd67547779ee5625328", + "proof-slice": "sha256:7fe4a8d83bc96af0142182e600a93a07c1ac4120c238c89eebcde02626a0e426", + "readiness-closeout": "sha256:dfdc0430c94df86a555e10f220c6791de390a669c6e3d264da333ccf87646762", + "receipt-currentness-scope": "sha256:59f07af61715f93154a64405d18737ede83dad1384d8ee30eaa81ee36f30d931", + "receipt-producer-admission": "sha256:e2a67e3c396ec1a103b4882e2f3d6d217675b3faf649da61b47bdd573d3a1f06", + "receipt-trust-class": "sha256:dcb19494d9f93e9ac7a5131450df8eb301debbc17b3ef97de3a762487a518ad1", + "registry-consumer": "sha256:ba1993e50fbffa1e91b53acdcb80f0018931bc6a8933113daa5c7cec2bc2057e", + "registry-consumer-proof-input-compose": "sha256:afd82cc90313e3a3a0f51cf23224789e13f509458705040e92aa9925688b1264", + "release-authority": "sha256:dfad5dc22d4ee148c9ef812911a37bb92aa9b8714660da19cdfa2d271d7303f0", + "rendered-artifact-freshness": "sha256:e0ec176191308353dce416a8988adc161d44ede3cde0f3f7e7cf0169a12e86f3", + "repo-profile-admission": "sha256:db6632e3f1cada4144a0f174b043f497539ac1a39f068695db09d3ea5b2d6bd8", + "repository-inventory": "sha256:8c637fc38d4151d3fb323bbc17fcf18c707526059d17b19cd3e3216f033ac0f7", + "requirement-authoring-plan": "sha256:e5f6520e8db048f55f8e46d33dd6e1e8c32c0986a839a84ea2dc5b692215ab54", + "requirement-bindings": "sha256:8747f9f25446ca8bf543c8d4eae42c45aacf91f919f9516aa0da696c99b80e4d", + "requirement-browser-server": "sha256:3eb6e553849772503844ac0a808f504044c705e920587225ee7e0c77018e8d01", + "requirement-context-compose": "sha256:f510132944ab918aabfe79e837c502a9b36ef1ba9809b2eeebba5a66560ba840", + "requirement-context-slice": "sha256:ccf1f912d0e9cfd4f330c3b4b0a3fa8ab8811192d4d6fdf6c6c8175d5415062d", + "requirement-coverage-input-compose": "sha256:a2a38a9e95780af68adb76a8afa16ccc104787e205764616f429b76947eda789", + "requirement-coverage-view": "sha256:08af3b6ccb675c9533329c27cc44f9486d797a53770fb2f5df77740cec9484e5", + "requirement-impact-input-compose": "sha256:a80c3f5a7e4c4879c24aff806c7a6675696533d14fc938211160663f3cabff81", + "requirement-proof-resolver": "sha256:5f6b34e11fdccf381fd77fb49fd0e0b99784ea875a1184f88621e2d1e6a19345", + "requirement-proof-source-set": "sha256:bfe2aa94d1713c70ce939f794d7adc1f866ad6da71c46d347559e74faa55cbad", + "requirement-proof-view": "sha256:0f316e3da056f7e4a5a0a3ccdca72304e92b6b2abf9deec9c957672b2ef8505c", + "requirement-semantic-diff": "sha256:73d590602d2ee45533b610e8c31f9776dd5c504477a1c3c9e6325bfd4b9ee88d", + "requirement-source-admission": "sha256:d63a1b1a3b3f0b057602c6eca4dc8f9f9d2d8697eb314316c5a624044831099d", + "requirement-source-transition": "sha256:036faa999d55f92d194e4648cfeb03c9de0fc08fa5ad90669a82287acc58a1df", + "requirement-source-view": "sha256:380b829ebfcdfcf35bd024f2a4d474bde56c013068353f0f897c0fa02566ad21", + "requirement-spec-tree": "sha256:b865f5988555a56c6ba158f73da85120277ba2d6f6d764a9e475da0e6601a088", + "requirement-spec-tree-view": "sha256:a8fdbb358902c36912bc15097315630cd02b926672d1ebe94eb8aa15ebcd3125", + "requirement-traceability-graph": "sha256:ebe9aee328218caec9b94ce50ef23a13aafcdb775440d012ee1b7cbc10a9075f", + "scaffold-profile-plan": "sha256:bd1a2f627d79b38fec7b4857659f9870588f7c52e4da003abd40b01d66907d8a", + "scaffold-project-structure": "sha256:61528eb82049b3b595564079292aae805671351dcdb5c99813ba172f25bab95c", + "secret-scan": "sha256:e4fba0f0c9bb2ab59d3ae839290804cd867ae2fda51e4bb2cf7da4cb90ff34d9", + "selective-gate-evidence": "sha256:8d8a5f8b6a53dc80ea73ca3d183f7f41147ea7a895696fe1dcf56428696074cb", + "selective-gate-obligation-decision-input": "sha256:05a59d50765ebea08c209cbed21b9fd4de74b5bb9f9ff748683042fad986da32", + "selective-gate-plan": "sha256:cdcd34fb480b64170d8406acc74832fff33ee499430c2e22ad42246ea9f4045b", + "self-check": "sha256:93464e3868286e76ec193438e70c2157e448f29eb7932b0b0a2d7afef12e72f6", + "spec-overview-claims": "sha256:4c98991b271e8f9b441811a1d408fc9473642aa87effa4614e497d65cba9ec3b", + "spec-proof-bundle-admission": "sha256:eeff98b256d94200d0326e143b633deedc65531b87ce9af03f63d65f3e6968c8", + "stack-preset": "sha256:77714de54868bc398ab7c6911237957f5d9bc1cc98d218b140f0b47ca437af17", + "test-evidence-inventory": "sha256:0424529a9c6e258fb746d7a71b45673765db64241cfecf9b4d5ae40e268ed94e", + "text-policy": "sha256:8b616f59d4c1bbcc179f6ae4797d97bffae0dc9e30923be80ab68186e8d0a2c2", + "typescript-public-api-surfaces": "sha256:dc1f2dee440c0f8130f0a439e17f275b2f13b76ca22eed8ed46433c853cb5eba", + "witness-plan": "sha256:7ea2cc4ecc54743c3ec60b9ccd744272e204bbd854932ee8262e27d5eb46648f", + "witness-scheduler-plan": "sha256:3a15a957d55c7192a64789031570dd3aa2a56d5621c7a6e5d2fc309ecee5319d", + "workspace-changed-package-plan": "sha256:13ca36deb447eb2c4561657e9abdfd348c932848fe28dd92ce184b2eadc07e26", + "workspace-manifest-facts": "sha256:732e8d9cc582cbcd1b9e0d3be142f25bc1305f295378f3962368a93d1402c4b4", + "workspace-registry": "sha256:5f339565f1e0228cad9f1f59bc42a6baf3cfe8240ba412ab1a4f25c412ff7bae", + "workspace-shard-partition": "sha256:11e500cc8b3c6b18b27bdc0e838f92cdb27725cde1497e0beb38f07138cdd391" + }, + "contractDefinitions": { + "proofkit.adopt-plan.output.v1.root-shape": "sha256:b1055d057771573252ed1ebb982f2b99d5bd1059caf2c1d57373af3249ec4444", + "proofkit.adoption-checklist.input.v1.root-shape": "sha256:183d3924bcd381a0718167ed1e5c16019edab1c8ed98edb329ec0c706c179b8b", + "proofkit.adoption-checklist.output.v1.root-shape": "sha256:d4fbec44fbbc3fa81d0a1f0383524be80952f7252d9ce02fcb041d3ca4dc449d", + "proofkit.adoption-contract-envelope.input.v2.root-shape": "sha256:648d0b5cbb894e06063bdd160a7b8d3a5cbbd41aa0825b2b6593186e3ae9790b", + "proofkit.adoption-contract-envelope.output.v1.root-shape": "sha256:3c28c67b8a40e150561a97d68691298cdffc4e381dfe8d8880d9aa4edf168664", + "proofkit.adoption-doctor.input.v1.root-shape": "sha256:67b5d7169f47e8ae4e696c651f87f433f4026112f9ccd9ff8c14e1709c7972dc", + "proofkit.adoption-doctor.output.v1.root-shape": "sha256:bbcf32e471e51d65ab0cac13f76d3c3a08c58374eb672c730bcda079b42e70cb", + "proofkit.adoption-materialization.apply-input.v1.root-shape": "sha256:d9b8a2d1725e0d64edb0e4ba037ce123909b0663bbcfde8d7ca4ba2025f98df2", + "proofkit.adoption-materialization.apply-output.v1.root-shape": "sha256:1ef903ee78ca688268a0f6ab51abce1ee389f4583724a4c55b5fe94482592307", + "proofkit.adoption-materialization.plan-input.v1.root-shape": "sha256:3e44ecca4b4b5012155bc2b461d293bb624663e93650ec5fef9d16c6bc3a0f2f", + "proofkit.adoption-materialization.plan-output.v1.root-shape": "sha256:a5d7762b12030d42c107134083b038a162ec0c02151c1f7dbdef32d88433111b", + "proofkit.adoption-materialization.recover-output.v1.root-shape": "sha256:1969957f358d4abacb179b741c2718fd89044b251eefac09901ebc31f7b2d999", + "proofkit.adoption-workflow-plan.input.v1.root-shape": "sha256:4b1ea641b5b3d5541a09b42f7e70f760da272ca62c77c1769116fdffb9dd06e5", + "proofkit.adoption-workflow-plan.output.v1.root-shape": "sha256:b66f9127f43ac3c5d4e97ef1920664df96bd8da655a53c333ac6b5fc3739d63d", + "proofkit.agent-route.input.v2.root-shape": "sha256:0a37b2ce49fc2ca2dca9d95653f1efa5363b1177d79364ca82b1281d06ae5e23", + "proofkit.agent-route.output.v3.root-shape": "sha256:09d2ffb20df8eeef73e4f0e67bdcc70cc7602525bf12d9b982a92d3c2d98ddee", + "proofkit.binding-partition.input.v1.root-shape": "sha256:db3aa86a745fd5650c0558668deb0dee2c1a04042527193efb3295774e579c97", + "proofkit.binding-partition.output.v1.root-shape": "sha256:a044f95c13e445edb611c53b61dcfbfeeef3c172596b3bb7f574529039f95093", + "proofkit.branch-authority.input.v1.root-shape": "sha256:b2dd6ab268f6430dfa27cb351d7f99b6d08d09eb2d903a957030cf384883be58", + "proofkit.branch-authority.output.v1.root-shape": "sha256:09ffe941c0c08ab9399e4e17dc0e85146abaf869a47a2c770c71ceb46ab895bc", + "proofkit.capability-map-admission.input.v1.root-shape": "sha256:5d02b008d6069c2838f38e0c1af0d68b820b7a37b14092c0336c02f0a4e01f03", + "proofkit.capability-map-admission.output.v1.root-shape": "sha256:f77609e0606035c0046ecb5baefd02a8deb9567aa03c88db568faebe1d665fbb", + "proofkit.change-workflow-plan.input.v1.root-shape": "sha256:c741c11ee19f6aa74df7c9cb3865a6b174f443a8ce6fcd5ec13c6a1105d87901", + "proofkit.change-workflow-plan.output.v1.root-shape": "sha256:fb4c512272e7d6f11f81566023ca5a83d54b31e9ef22d7324ba2c266625aa53d", + "proofkit.changed-path-set.input.v1.root-shape": "sha256:c2eb73384ab1ae5345156eec9a304835acf6ffa74c8709bac2508d427d489c29", + "proofkit.changed-path-set.output.v1.root-shape": "sha256:110a5c0ee9ec150916d832ca5d5ca99e50f1c90efee31249d8dcf547b0a6bffc", + "proofkit.completion-criteria.input.v1.root-shape": "sha256:85695f5582e2c2741ea136b32d86cdb213ee96c729b558e257e0d9222e65856f", + "proofkit.completion-criteria.output.v1.root-shape": "sha256:5f3bd4bc6bfc668135ed90a373e3c33d6d63da76637255e0722868974c3b98d2", + "proofkit.conformance-profile.input.v2.root-shape": "sha256:e1596622238c513eb0f04f9472047d70530bb98467d9d7e3e99ab944106d6777", + "proofkit.conformance-profile.output.v2.root-shape": "sha256:c48f4f4813087c55e02b53a5b6bc4cc581ca76f43f97b9d9c5da62044924ab23", + "proofkit.custom-rule-boundary.input.v1.root-shape": "sha256:072b25c19600d00d636df40b1bb9f21bd1f138ce63ca595b44b50f0648f79925", + "proofkit.custom-rule-boundary.output.v1.root-shape": "sha256:9bb1c66dded3a22febeab730b198258b731051770ea3519704eb45cf5ca5e6f3", + "proofkit.deployment-evidence-admission.input.v1.root-shape": "sha256:bd9e7dc71a4651da2ae10bf34110adb10285e4ebd30210ab6bb93b956b26a12c", + "proofkit.deployment-evidence-admission.output.v1.root-shape": "sha256:c4e5f4ca22deb9f7b79b4abfdeb2fdda2b4d7413e915e703c8038abd619b212d", + "proofkit.document-lifecycle-boundary.input.v1.root-shape": "sha256:1d4d94e4b7e4a12b39432af4d51cf483d02bd7de4f78cadd9b7262b908ebe1f4", + "proofkit.document-lifecycle-boundary.output.v1.root-shape": "sha256:e1cafa2ec4595fac0365bc3910ef9018b79e078bb49a7b26e32a94e745589da1", + "proofkit.evidence-graph.input.v1.root-shape": "sha256:e3d7704e1139ec9ecae885285cc71ec6ba59f020d2bc827162ebc47d8880332b", + "proofkit.evidence-graph.output.v1.root-shape": "sha256:482b548c92db4e3a43c29414e47ffaff788b4dec9500a863e10e6506803369a8", + "proofkit.external-consumer.input.v1.root-shape": "sha256:bf99fd56342e243a63148f828b82d55cbfca09976e348bd30c100191066325a1", + "proofkit.external-consumer.output.v1.root-shape": "sha256:906c1a16937fe87113700dd6f4e4b9161ce2a2344b6722f163ad9cbb1f5c72ba", + "proofkit.gradual-adoption-bootstrap.input.v1.root-shape": "sha256:9197bf926b6c560146de8c37c07f99c0ab0700ca02d0c08369a0ab88ff7d5233", + "proofkit.gradual-adoption-bootstrap.output.v1.root-shape": "sha256:ad134eeec1ca291e73da8d945209f98b7dfcdbf3a8ce41fcc4e9ea9e05de67cc", + "proofkit.gradual-adoption-guidance.input.v1.root-shape": "sha256:d3ba1789108f9b097a76ce388b17ffda975915ba0728a1dd465fe1b5b0d25c56", + "proofkit.gradual-adoption-guidance.output.v1.root-shape": "sha256:5dc2baadf195bcdef7f441ed1f64cc4ce91df762209c0c98a9433ab3fa8bfec2", + "proofkit.gradual-adoption.input.v1.root-shape": "sha256:6c51b22526e345d56a2568aab21c7696916c4c9bafa8f146d2ddd8ca84fc2f1a", + "proofkit.gradual-adoption.output.v1.root-shape": "sha256:27f63f422c35eabe29d1fdbe8ba663d05e9bbd7aab695471f101fe6fa881c152", + "proofkit.impact.input.v2.root-shape": "sha256:2de844000fd3b54bfed60df8d6992609477a38742c56fd9a4744a07ad19a0bc6", + "proofkit.impact.output.v2.root-shape": "sha256:9ab04fde5afaba3f6bc925e8274a3fadf21f0fec020871e2575d38f2616ffb1d", + "proofkit.json-report-cli-adapter-source.output.v1.root-shape": "sha256:6c506e9a805ec1e2b998a3f91cd80dcfe47b49b174d5fcb390fe9a3b495d364c", + "proofkit.migration-parity-admission.input.v1.root-shape": "sha256:4466c6a3a7d33620c1605d4e8eea932bd90432f0efe72783cafb9d269cdb7877", + "proofkit.migration-parity-admission.output.v1.root-shape": "sha256:e6fe3414939f0c297827fd94c50ff0bfadfba18969530aac40788bb9f66193d0", + "proofkit.migration-plan.input.v1.root-shape": "sha256:a0fb2f7e9d2469b648a6933f693ba6948a9f06c06e79393c236e2ad62a721690", + "proofkit.migration-plan.output.v1.root-shape": "sha256:d2bf5285bafc193f832836b2a8ff36d29d2c2d6a8c6efeb812edc58b2b25d31a", + "proofkit.native-evidence-guidance.output.v1.root-shape": "sha256:ea40df7bad16d5893871398f31d00af8e7d0a154dd830d03441dee1f3b3f457e", + "proofkit.obligation-decision.input.v1.root-shape": "sha256:b46ca0727b8bd37c54af94d40ddb8a393c7b34352e10d98d13aa5295b5c5e99e", + "proofkit.obligation-decision.output.v1.root-shape": "sha256:d00ef83f944f8617dd20c8a02e305e4d5a30321a1e78bb0f413d68aeea15d277", + "proofkit.package-runtime-dependency-admission.input.v1.root-shape": "sha256:79457e79a5057dea7da0f933ede4b3f2d2eff15abb327d7d231d88e138fa4d15", + "proofkit.package-runtime-dependency-admission.output.v1.root-shape": "sha256:edd2d97079917745afc4c35bfb41e05594bb0e1b76ed174e15dba4d06639932b", + "proofkit.pilot-admission.input.v2.root-shape": "sha256:b3449d30c5995ec43c70a0e0b375741e91322c391865c5b9809e4beda29beb18", + "proofkit.pilot-admission.output.v1.root-shape": "sha256:ae8162846cdb4285a6c9fbf516e3500316ab7111055d83743f2c2706d8eba764", + "proofkit.producer-policy-self-proof.input.v1.root-shape": "sha256:712d3a8ac69c02c7fb348020ac44e2929325c2ac82c20fe9abf013d74f0b3fad", + "proofkit.producer-policy-self-proof.output.v1.root-shape": "sha256:76a8b86df2051f8dcd5ba67863d04afb66bf037726e7616cfc199ac2a2f85bbf", + "proofkit.proof-obligation-algebra.input.v1.root-shape": "sha256:ef13ec8945a62655b1ff804d30dc9ba3b5f712377142bf96ff5b4c3b6b6fb171", + "proofkit.proof-obligation-algebra.output.v1.root-shape": "sha256:1309b0bdd52bdb413a64b8084ce6af876ff3973e9edc82c5dd3ebe7a3b11692c", + "proofkit.proof-receipt-admission.input.v1.root-shape": "sha256:90880159e5bac4c583021791fac7acd6cbd5bd00d0f8d9e2b7761e066572b938", + "proofkit.proof-receipt-admission.output.v1.root-shape": "sha256:6ac0f95689749db3e78f56f789222475722464c3028e4ae52c45b5eb1a38b879", + "proofkit.proof-slice.input.v1.root-shape": "sha256:00bc6195cc7545c50bc8a9b3aa514423177537d02410f56ab97637fd7e5d6141", + "proofkit.proof-slice.output.v1.root-shape": "sha256:ebe71fa04a2bcea25b718a3270de37a695cf8b1cbd3338074ce5d2097e14ea8d", + "proofkit.readiness-closeout.input.v1.root-shape": "sha256:01aa2f0eb36ebe4d626d067a74bda0a98da4d6d9aab0fc29739a1f73fa23013e", + "proofkit.readiness-closeout.output.v1.root-shape": "sha256:af0c0fc7feee99b450dc665d14a9fe85a7e5f6e07a8d6fd3246c64018a7397bf", + "proofkit.receipt-currentness-scope.input.v1.root-shape": "sha256:fe1b7088d55547ce55caba88434df6e1728e522f9b68f22a006943e5c86f7073", + "proofkit.receipt-currentness-scope.output.v1.root-shape": "sha256:44a90b8eea868c9be44cbde472438dab29d2d02a0745741c94deedca4a954c4c", + "proofkit.receipt-producer-admission.input.v1.root-shape": "sha256:f13cd253ee1de8f0514d76f1e52e4a8c484f3dbd0151e961a20659cef57a75bd", + "proofkit.receipt-producer-admission.output.v1.root-shape": "sha256:f7da547e6ed0bf89a29aeb5f5426fbce04bb0f6369246ff0c74fe6ee5c4d80bf", + "proofkit.receipt-trust-class.input.v1.root-shape": "sha256:46d9d121275c7763632e51edc9fbe04829b7843b6a587a65e08954c2dbad0fe7", + "proofkit.receipt-trust-class.output.v1.root-shape": "sha256:e1a9d098ea2037cf466481824a4a3ca017ba74df0d92aa6613b7603eea794d32", + "proofkit.registry-consumer-proof-input-compose.input.v1.root-shape": "sha256:529947c77c35bbf762c410f7446655132caa829c70cbfbb6e6c5d93672cc998a", + "proofkit.registry-consumer-proof-input-compose.output.v1.root-shape": "sha256:094335d032894ba6d0681a1aa91b62e76f056d06fdffc24b1748b78f9ed954d0", + "proofkit.registry-consumer.input.v1.root-shape": "sha256:a89496595f72e9376a2cfb5a1b2639157952817add2202428356ac448c3364f1", + "proofkit.registry-consumer.output.v1.root-shape": "sha256:1610d3231112d3e2dbbcc241debe0ca1077f6a603086e10fde1d94b8262cac17", + "proofkit.release-authority.input.v1.root-shape": "sha256:31c75401d61456af4047e5611f66dc91124f4388de59278c7ad9ccf919069daf", + "proofkit.release-authority.output.v1.root-shape": "sha256:f1b0a9ef15a0ec0f51c6e86860fe108df3098bef59d8af92f1d833d688efc45c", + "proofkit.rendered-artifact-freshness.input.v1.root-shape": "sha256:8db852267c4e57b95b7498fb039f01ccb6ee1595bd7ec2837d9c35dd126adccb", + "proofkit.rendered-artifact-freshness.output.v1.root-shape": "sha256:f62fb968ec2cae41525ecac5665e4ac49add852c3d82d820073d60d9fa6ea3ef", + "proofkit.repo-profile-admission.input.v1.root-shape": "sha256:c2340140fc4e1093500fc9500ca619a05fac79767a19a63d8c6e36cb33937170", + "proofkit.repo-profile-admission.output.v1.root-shape": "sha256:063bf420f694f15383352da18ec0180cd5727337a2e4813d8de082b2a6450b2f", + "proofkit.repository-inventory.output.v1.root-shape": "sha256:02f0bd8e33d192437bb8f7c3fc1c0d01e39d428b90a8135470521a69d3d08d25", + "proofkit.requirement-authoring-plan.input.v1.root-shape": "sha256:aa8bc39d8dfd7746a4b23176a8e834b0e84a0937136bc506435fefb39d3a5d99", + "proofkit.requirement-authoring-plan.output.v2.root-shape": "sha256:47a28cb30a58048faf0d824523921fb4204120ff63b19f4833a4f2dfaaa40a13", + "proofkit.requirement-bindings.input.v1.root-shape": "sha256:b57bcec7fcdfa6a98c13051d06ff69ff05e15f2b4a19860eb71c7aeabda23018", + "proofkit.requirement-bindings.output.v1.root-shape": "sha256:ff1fbd4ad20e6c5c6b13a2a3ff443c2082c1514d7c3b34013817ee10ff0077fc", + "proofkit.requirement-browser-server.input.v3.root-shape": "sha256:7a15e92365672d1ca260fcbee8be2bd6fbb4b1e194e9996fcbfd59e73e1f4f34", + "proofkit.requirement-browser-server.output.v1.root-shape": "sha256:bfb0da899a9c77d969d2687e04e866912031a7f9cb1086e40f5038144c8924b3", + "proofkit.requirement-context-compose.input.v1.root-shape": "sha256:99984834220692385d55e421fca1a03cf596d303d3be91f88827d5860659b9f0", + "proofkit.requirement-context-compose.output.v2.root-shape": "sha256:66ed99649703edfe696684ec5a38264b03f456c3349c3b6010742187c5235f1a", + "proofkit.requirement-context-slice.input.v1.root-shape": "sha256:994d367551a46aeea385257bd27798a3bd32d8eff7f494bbf332bed71ee456f9", + "proofkit.requirement-context-slice.output.v1.root-shape": "sha256:990ecf59350ba938d31fc32c63b9b88478911782d608245a2a4457c132f5a7d2", + "proofkit.requirement-coverage-input-compose.input.v2.root-shape": "sha256:cfaf53655f04130f02ee986bdc57379593d6b63ec0ae6c7415370866000baed3", + "proofkit.requirement-coverage-input-compose.output.v2.root-shape": "sha256:e63a626870e41ce2b9ff3ecbdb82029fcea6c26d1c2893f6fbe325045dd34087", + "proofkit.requirement-coverage-view.input.v2.root-shape": "sha256:c890cddbc60c77bd9a66116caedb46d71eb45da9b657be95e6adf54f70aa07f7", + "proofkit.requirement-coverage-view.output.v3.root-shape": "sha256:9d4404f95e4a54fbe21a8817c5577d30c9cf76903c898888b9d688d88d557161", + "proofkit.requirement-impact-input-compose.input.v2.root-shape": "sha256:17489925d632b6e8543f86be0f6824818803b47cdb882278d212032b944a384f", + "proofkit.requirement-impact-input-compose.output.v2.root-shape": "sha256:809cbddcc6ed83a781d01a0ea6ed59bb7730a5916ec1f95aeafd6ae2f19b3ec5", + "proofkit.requirement-proof-resolver.input.v2.root-shape": "sha256:3b8a89fc3d32e20d40db72ccbb2a90fe8329072108fbef3a439c7df4ad89b711", + "proofkit.requirement-proof-resolver.output.v2.root-shape": "sha256:7cf374f46d230331241e723c423f3695b2e06a50fc385aae18d7341d95059307", + "proofkit.requirement-proof-source-set.input.v2.root-shape": "sha256:57c43710648281e5c4264bfadaa407d1268aecd020df22ba6641212749146a7f", + "proofkit.requirement-proof-source-set.output.v2.root-shape": "sha256:f8a03a5f5b4964cc8f7e5d00d6b962052e95ff0b91d27b0d41cc43e771310055", + "proofkit.requirement-proof-view.input.v2.root-shape": "sha256:fd96cde2473fe37924a588a5e257017e025efb73d1ec1fc063f8b715b4b7e0cb", + "proofkit.requirement-proof-view.output.v2.root-shape": "sha256:a11aef4bb9ac6eced780b75287412b3f6bd43b1302d392773e1b6db81f42ff67", + "proofkit.requirement-semantic-diff.input.v2.root-shape": "sha256:9df463380742b2424153c275365401765c5b8da8c19a116e42e108d5359ab0b3", + "proofkit.requirement-semantic-diff.output.v2.root-shape": "sha256:25a530d869c69ff644ad53f84abbcbdaa03559577c99e244e5e944ec3a811ff9", + "proofkit.requirement-source-admission.input.v1.root-shape": "sha256:1ef5b4ff14c7ee7e5f0b76d0e8cdedb5f9fad32cbcfdd7b3930493eae14670e6", + "proofkit.requirement-source-admission.output.v1.root-shape": "sha256:cd9db4e8db0336364a4815d1c7aac3ad317e42d89279c2f0924bcf1f439b0a7a", + "proofkit.requirement-source-transition.input.v1.root-shape": "sha256:4cb46831aa6d7350eaf22fbd5b9b7e154c9a660e21a901815f70378369cf38f6", + "proofkit.requirement-source-transition.output.v1.root-shape": "sha256:955fff4cf23f583cb860d4490b369f4e253c54ec6516221660c803df592cb5b7", + "proofkit.requirement-source-view.input.v1.root-shape": "sha256:ae046f9795c4e524f95fcf47e8cdbd008c1d9424d66ecfb17683ea3c0abf3df8", + "proofkit.requirement-source-view.output.v1.root-shape": "sha256:26591b6039c8a3550854dc161a1a60b9259f3fd5be80790950d9ec8e36346620", + "proofkit.requirement-spec-tree-view.input.v1.root-shape": "sha256:bfecbc18fe9e747ce79bbe03b83db5a7f7178d58f514b0b9d23ed26cb2190f32", + "proofkit.requirement-spec-tree-view.output.v2.root-shape": "sha256:9a03221240f61eeb94157bfb556edee325840b6e54f5d8a79169c2a9e93d3b4c", + "proofkit.requirement-spec-tree.input.v1.root-shape": "sha256:394f54e615f552af8012c9ff762b83095ec9dabcc163aa19d52404f02589ea73", + "proofkit.requirement-spec-tree.output.v1.root-shape": "sha256:929838c2402c4512c97721a2d8a1d9389a34fc09d992fa801e22b4527edbdcd9", + "proofkit.requirement-traceability-graph.input.v2.root-shape": "sha256:5a3b03a8103996a61ea80cfdfeef3bf18a57886cd37d58e7c8df4b0efb8a947f", + "proofkit.requirement-traceability-graph.output.v1.root-shape": "sha256:16e8aec9edb257caff0e834816dfc44e6929bbdf399055477c779383dbcf4a5c", + "proofkit.scaffold-profile-plan.input.v1.root-shape": "sha256:aec8ad7d35f5c77dbae52197834c3abc797251371b20f3ce7036353154d28f0b", + "proofkit.scaffold-profile-plan.output.v1.root-shape": "sha256:9e6cccdfd25d48ae93d2a6c247873c1db4204cdebed3ec7d83fe332c8023ecc9", + "proofkit.scaffold-project-structure.input.v1.root-shape": "sha256:4eaa681eb5a480b123693a8e87815958e7ae7e0a409a71189c671fca4e345ede", + "proofkit.scaffold-project-structure.output.v1.root-shape": "sha256:4ffbee5bc142abb84d0fb8fbdcc25dac1b043310e3b728b7b8de190d30bb03fe", + "proofkit.secret-scan.input.v1.root-shape": "sha256:3e8d086056134b306a27f817012f1fa6728bf7b1eb7636b37d5f180ee0f8b46a", + "proofkit.secret-scan.output.v1.root-shape": "sha256:b1e5663db3a831f670b7d76015697719c87683766088a4a62674d9fd6e34c186", + "proofkit.selective-gate-evidence.input.v1.root-shape": "sha256:a0b9c78088bb7429086cf9244ce54ba5636281899ae506e410baab9ffa320146", + "proofkit.selective-gate-evidence.output.v1.root-shape": "sha256:a5f97a52b9f74d20d757495d22e7ba736aa8e93fef4521aa2d130e65eead8e5f", + "proofkit.selective-gate-obligation-decision-input.input.v1.root-shape": "sha256:e9ff696a70ecb2b08b74c84ea3fb34bd56bdc615f6d75f151694137e15677905", + "proofkit.selective-gate-obligation-decision-input.output.v1.root-shape": "sha256:ffd3ee7cafe59efa0fb20ff99bdc33962d090f012dec81060e2b9f78b36ed08d", + "proofkit.selective-gate-plan.input.v1.root-shape": "sha256:b7bc0bc0f5860b6562039caf8a74a4b3cf10c2bec0776db642d8a6c833dfed02", + "proofkit.selective-gate-plan.output.v1.root-shape": "sha256:fc1736408f2bc4e51fdd353e89ee6912295c7b4401b237c972d4ceddd45b729b", + "proofkit.self-check.input.v1.root-shape": "sha256:f3d13182d45baade9b5870bc33472ddc177721a31b0b82fe6c3ecd2fb08ca6de", + "proofkit.self-check.output.v1.root-shape": "sha256:8635d51a9ae997df05d7b64f86f6d6c29bb7850cf85835293dddbe3a204864a5", + "proofkit.spec-overview-claims.input.v1.root-shape": "sha256:1aea724c3878b0d79354cd6320fdc67de6e0efe189882c311397d702d385e752", + "proofkit.spec-overview-claims.output.v1.root-shape": "sha256:0e9cabc13356be0946beeb84f18f48a2be287a34f73a2c0d03c548eec49d0a5e", + "proofkit.spec-proof-bundle-admission.input.v1.root-shape": "sha256:85e99552f0e1e6bc24aa23761756c7989d8d297280ae8da6f09e3e7a29cfdee3", + "proofkit.spec-proof-bundle-admission.output.v1.root-shape": "sha256:a8d651222dda8c6c3652f6dc8858aaa4b69b01eb7010727222bfcfebbecd8371", + "proofkit.stack-preset.output.v1.root-shape": "sha256:0cabd5952f7716cd6db163a2037bdb99ff12f8879712420987a4c93dbe3e0528", + "proofkit.test-evidence-inventory.input.v2.root-shape": "sha256:b89dc7cb02e288f4734da6febc02eedfe8dd113018e90b5ab4ae501cc60aff3c", + "proofkit.test-evidence-inventory.output.v2.root-shape": "sha256:12259060e4b5dde06ef1ea4440b249633af88be5555496e9daa95838b0593d22", + "proofkit.text-policy.input.v1.root-shape": "sha256:8343f50b7d23e0104beff0c2fc4272b205b40dfc122dfff4acb0a7dab49ac1ad", + "proofkit.text-policy.output.v1.root-shape": "sha256:fdd89dc7df35b4d3e13d668aba3835bcfe1dd8f45e5e417a15e04d356cbc7b90", + "proofkit.typescript-public-api-surfaces.input.v1.root-shape": "sha256:3595f9120e43ab94658e320057650ed68b9fa1b81b8c450423949453e4c4b80d", + "proofkit.typescript-public-api-surfaces.output.v1.root-shape": "sha256:4f3c85ffe365b67bb763099c51c712ca5ce2c3e6a18c8f807523d26cf1b146b8", + "proofkit.witness-plan.input.v1.root-shape": "sha256:4cd56bcc87a5a38ab7767797e2c3c3aa58de8d8785375a740aae2abfdcd895e1", + "proofkit.witness-plan.output.v1.root-shape": "sha256:54bbc6306250bb8d5e5e92355380427c36f7f607b60d72dd1408600a25a7f588", + "proofkit.witness-scheduler-plan.input.v1.root-shape": "sha256:13be42b86615146db538ca509127b8685503bfcc38053954db049948dc975224", + "proofkit.witness-scheduler-plan.output.v1.root-shape": "sha256:fa148200a4050d07122235767624170bd477d2d7e307cb62e6877c0ac1469e09", + "proofkit.workspace-changed-package-plan.input.v1.root-shape": "sha256:65f68f90d6b8d5227c43f6f7a86676d80c23874fa3f678ea154daf1923ba8743", + "proofkit.workspace-changed-package-plan.output.v1.root-shape": "sha256:6431ffd5078f661d919ea9d74dc670d3ddf964b815639c31dd2ad4d9b7265b99", + "proofkit.workspace-manifest-facts.input.v1.root-shape": "sha256:27561dac52db8ce803e5ae75ec1f8113822896fa7ae283c1786448ef6bbcc95c", + "proofkit.workspace-manifest-facts.output.v1.root-shape": "sha256:20b1c2c0caeab5c64c22e1ac8445bba65712b8907d73d960c6162fe6d4786ea8", + "proofkit.workspace-registry.input.v1.root-shape": "sha256:eff179bdd299387a66ac3c9c41b404687cd7e3ea8770d411f8ac443bd8d48fed", + "proofkit.workspace-registry.output.v1.root-shape": "sha256:87881114c7ae0a0ea62b08644df9cba0d919f53dfcf402ecc772edcd81b5965b", + "proofkit.workspace-shard-partition.input.v1.root-shape": "sha256:21148091a01be99a42b3b44694565afd69c63fe63326ca2589a20836573c3807", + "proofkit.workspace-shard-partition.output.v1.root-shape": "sha256:4b54ef65d891ea3ac80459bc3d2f2f2bb5b7306e06450c5d8dde6bca5da0b2f4" + }, + "contractId": "proofkit.cli-contract.v2", + "contractSchemaVersion": 2, + "nonClaims": [ + "Per-command fingerprints omit only native source canonical digests; the exact raw contract remains bound by publicAbiSha256.", + "This frozen source observation does not authenticate registry publication, provider state, consumer migration, or runtime compatibility." + ], + "observationKind": "proofkit.frozen-public-abi-observation", + "orderingPolicy": "lexicographic_by_identity", + "packageName": "@research-engineering/agentic-proofkit", + "processContractSha256": "sha256:ce986ac43352ac3faf602dea8ee09c9faad802e82a5eef377fce35a2c43b18f4", + "publicAbiSha256": "sha256:b5ea707ee5851cea6b75442e4faf20e93879371faf3636e96a98ccd23b527463", + "releaseVersion": "0.8.0", + "schemaVersion": 1 +} diff --git a/internal/app/testdata/v0.9-wire-observations.json b/internal/app/testdata/v0.9-wire-observations.json index 0d711a9..745c3dd 100644 --- a/internal/app/testdata/v0.9-wire-observations.json +++ b/internal/app/testdata/v0.9-wire-observations.json @@ -9,7 +9,7 @@ "changeRecordRef": "release/change-record.v2.json", "changeRecordSha256": "sha256:6aaa914c454d6f135ece86b632b8618911b71d6b5da08bc2c25657c09228f4f3", "previousPublicAbiSha256": "sha256:b5ea707ee5851cea6b75442e4faf20e93879371faf3636e96a98ccd23b527463", - "currentPublicAbiSha256": "sha256:17b7f185adb80bcdeb6bc5e6a08cf0b95e6b6f8766d2cba634d08f59b7821e0b", + "currentPublicAbiSha256": "sha256:a331f29499c3dbfd7e03ac4c0d56ce4d71910f735297af79ba0092b543e171f0", "addedCommandContracts": [ { "command": "next", diff --git a/internal/command/projectstatus/inspect_test.go b/internal/command/projectstatus/inspect_test.go index a749cc1..dc245ed 100644 --- a/internal/command/projectstatus/inspect_test.go +++ b/internal/command/projectstatus/inspect_test.go @@ -21,8 +21,8 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" ) -func TestInspectClassifiesMaterializedProjectWithoutMutation(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.086999612230625810638279009641652637656804117475637022915480714642144225105240") +func TestInspectClassifiesMaterializedProjectWithoutApplicationWrites(t *testing.T) { + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.068153284639677751912209073851318961044240216422390589277786880896123148215480") root := t.TempDir() status, err := Inspect(context.Background(), root) if err != nil { @@ -432,6 +432,30 @@ func TestInspectHonorsCancellationBeforeReads(t *testing.T) { } } +func TestInspectHonorsCancellationBetweenBoundedReads(t *testing.T) { + root := t.TempDir() + materializeTestProject(t, root) + ctx, cancel := context.WithCancel(context.Background()) + readCount := 0 + dependencies := defaultInspectionDependencies + dependencies.readFile = func(ctx context.Context, lease *repositorytransaction.InspectionLease, path string, budget *readBudget) (fileObservation, error) { + observation, err := readProjectFile(ctx, lease, path, budget) + if err == nil { + readCount++ + if readCount == 1 { + cancel() + } + } + return observation, err + } + if _, err := inspectWithDependencies(ctx, root, dependencies); !errors.Is(err, context.Canceled) { + t.Fatalf("inspectWithDependencies() error = %v, want context cancellation", err) + } + if readCount != 1 { + t.Fatalf("read count=%d, want cancellation before the second bounded read", readCount) + } +} + func materializeTestProject(t *testing.T, root string) { t.Helper() if err := os.WriteFile(filepath.Join(root, "README.md"), []byte("# Pilot\n"), 0o644); err != nil { diff --git a/internal/command/stackpreset/preset_ids_generated.go b/internal/command/stackpreset/preset_ids_generated.go index 32fe5fc..2c51bb5 100644 --- a/internal/command/stackpreset/preset_ids_generated.go +++ b/internal/command/stackpreset/preset_ids_generated.go @@ -1,6 +1,6 @@ // Code generated by internal/tools/commandcontractgen; DO NOT EDIT. package stackpreset -const presetContractSourceSHA256 = "5306b7223c5c0671871272f9790195fa1064de85c638f276493c110a6c3c51ed" +const presetContractSourceSHA256 = "514f09bd74c91e6f1378e8e58483d6f5b0ef926243f37da11080f768a4afba31" var presetIDs = []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"} diff --git a/internal/tools/coveragemetrics/required_inventory.go b/internal/tools/coveragemetrics/required_inventory.go index 4d25d70..100f799 100644 --- a/internal/tools/coveragemetrics/required_inventory.go +++ b/internal/tools/coveragemetrics/required_inventory.go @@ -113,11 +113,12 @@ func requiredBindingWitnessInventory() map[inventoryKey]requiredInventoryEntry { witnessPath: "internal/command/projectstatus/inspect_test.go", selectors: []string{ "TestInspectAttemptRejectsFinalRepositoryRootReplacement", - "TestInspectClassifiesMaterializedProjectWithoutMutation", + "TestInspectClassifiesMaterializedProjectWithoutApplicationWrites", "TestInspectCleanupFailureDominatesRetryableSnapshotChange", "TestInspectCohortValidationClosesCleanEpochABA", "TestInspectDeduplicatesRepeatedIssueCodes", "TestInspectFailsClosedOnSymlinksAndBoundsWithoutDisclosure", + "TestInspectHonorsCancellationBetweenBoundedReads", "TestInspectMapsInvalidControlState", "TestInspectMapsRecoverableControlState", "TestInspectRejectsAdmittedChildrenWithInvalidCrossRecordClosure", @@ -166,6 +167,7 @@ func requiredBindingWitnessInventory() map[inventoryKey]requiredInventoryEntry { witnessPath: "internal/app/project_status_command_test.go", selectors: []string{ "TestProjectStatusCLI", + "TestProjectStatusCLIHonorsCanceledContextBeforeOutput", "TestProjectStatusOutputMatrix", "TestProjectStatusTransportFailureUsesOneBoundedWriteWithoutAtomicSinkClaim", }, @@ -786,6 +788,13 @@ func requiredBindingWitnessInventory() map[inventoryKey]requiredInventoryEntry { "TestProjectNavigationVersionEdgeRejectsCoordinatedChangeRecordDrift", }, }, + {"REQ-PROOFKIT-SPEC-035", "proofkit.spec-proof-core.project-navigation-public-abi-diff"}: { + witnessPath: "internal/app/project_navigation_abi_closure_test.go", + selectors: []string{ + "TestProjectNavigationVersionEdgeClosesCompletePublicABIDiff", + "TestProjectNavigationVersionEdgeRejectsUndeclaredPublicABIDrift", + }, + }, {"REQ-PROOFKIT-RETIRE-006", "proofkit.consumer-infra-retirement.migration-parity-admission"}: { witnessPath: "internal/command/migrationparityadmission/migrationparityadmission_test.go", selectors: []string{"TestBuildProjectsEveryCallerDeclaredStatusAndSummaryField"}, diff --git a/internal/tools/workflowsmoke/project_navigation_fixture.go b/internal/tools/workflowsmoke/project_navigation_fixture.go new file mode 100644 index 0000000..c544372 --- /dev/null +++ b/internal/tools/workflowsmoke/project_navigation_fixture.go @@ -0,0 +1,94 @@ +package workflowsmoke + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/research-engineering/agentic-proofkit/internal/command/adoptionmaterialization" + "github.com/research-engineering/agentic-proofkit/internal/command/adoptionplan" + "github.com/research-engineering/agentic-proofkit/internal/command/repositoryinventory" + "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" +) + +func materializationSmokeInput(ctx context.Context, repositoryRoot string) (any, []byte, error) { + if err := os.WriteFile(filepath.Join(repositoryRoot, "README.md"), []byte("# Installed workflow smoke\n"), 0o600); err != nil { + return nil, nil, fmt.Errorf("write workflow smoke repository seed: %w", err) + } + inventory, err := repositoryinventory.Scan(ctx, repositoryRoot) + if err != nil { + return nil, nil, fmt.Errorf("scan workflow smoke repository: %w", err) + } + sourcePlan, err := adoptionplan.Build(adoptionplan.IntentFresh, inventory, "") + if err != nil { + return nil, nil, fmt.Errorf("build workflow smoke adoption plan: %w", err) + } + requirementNonClaims := []any{"Installed workflow fixture does not prove rollout."} + value := map[string]any{ + "schemaVersion": json.Number("1"), "requestKind": adoptionmaterialization.RequestKind, + "requestId": "proofkit.workflow-smoke.materialization", "projectId": "proofkit.workflow-smoke", "sourcePlan": sourcePlan.JSONValue(), + "requirementSources": []any{map[string]any{ + "schemaVersion": json.Number("1"), "sourceId": "proofkit.workflow-smoke.requirements", "specPackagePath": "docs/specs/workflow-smoke", + "overviewPath": "docs/specs/workflow-smoke/overview.md", "requirementsPath": "docs/specs/workflow-smoke/requirements.v1.json", + "nonClaims": []any{"Installed workflow source fixture does not prove production readiness."}, + "requirements": []any{map[string]any{ + "claimLevel": "blocking", "deferral": nil, "invariant": "Installed carriers preserve admitted materialized-project navigation.", + "lifecycle": map[string]any{"evidenceRefs": []any{}, "replacementRequirementIds": []any{}, "state": "active"}, + "nonClaimRefs": []any{}, "nonClaims": requirementNonClaims, "ownerId": "proofkit.workflow-smoke.owner", + "proofBindingRefs": []any{"proofkit/requirement-bindings.json"}, "requirementId": "REQ-PROOFKIT-WORKFLOW-SMOKE-001", "riskClass": "high", + "updatePolicy": map[string]any{"requiresImpactDeclaration": true, "requiresProofBindingReview": true, "reviewOwnerId": "proofkit.workflow-smoke.owner"}, + }}, + }}, + "requirementProofBinding": map[string]any{ + "path": "proofkit/requirement-bindings.json", + "record": map[string]any{ + "schemaVersion": json.Number("1"), "bindingId": "proofkit.workflow-smoke.bindings", + "requirements": []any{map[string]any{ + "claimLevel": "blocking", "nonClaims": requirementNonClaims, "ownerId": "proofkit.workflow-smoke.owner", + "proofState": "witness_backed", "requirementId": "REQ-PROOFKIT-WORKFLOW-SMOKE-001", "specPath": "docs/specs/workflow-smoke/requirements.v1.json", + }}, + "bindings": []any{map[string]any{ + "commandIds": []any{"proofkit.workflow-smoke.test"}, "environmentClasses": []any{"local-go"}, "requirementId": "REQ-PROOFKIT-WORKFLOW-SMOKE-001", + "scenarioId": "proofkit.workflow-smoke.materialization", "witnessId": "proofkit.workflow-smoke.witness", + "witnessKind": "contract", "witnessPath": "internal/workflow_smoke/materialization_test.go", + }}, + "witnessCommands": []any{map[string]any{ + "command": "go test ./internal/workflow_smoke", "commandId": "proofkit.workflow-smoke.test", "environmentClasses": []any{"local-go"}, + }}, + "selection": map[string]any{"changedPaths": []any{}, "ownerIds": []any{}, "requirementIds": []any{}}, + "nonClaims": []any{"Installed workflow binding fixture does not execute witnesses."}, + }, + }, + "testEvidenceInventory": map[string]any{ + "path": "proofkit/test-evidence-inventory.json", + "record": map[string]any{ + "schemaVersion": json.Number("1"), "inventoryId": "proofkit.workflow-smoke.inventory", "authority": "caller_owned_inventory", + "entries": []any{map[string]any{ + "testId": "proofkit.workflow-smoke.materialization", "selector": "go test ./internal/workflow_smoke -run TestMaterialization", + "sourcePath": "internal/workflow_smoke/materialization_test.go", "ownerId": "proofkit.workflow-smoke.owner", + "evidenceClass": "declared_semantic_falsifier_route", "requirementRefs": []any{"REQ-PROOFKIT-WORKFLOW-SMOKE-001"}, + "ownerInvariantRefs": []any{}, "commandRefs": []any{"proofkit.workflow-smoke.test"}, "witnessRefs": []any{"proofkit.workflow-smoke.witness"}, + "falsifier": map[string]any{ + "falsifierId": "proofkit.workflow-smoke.falsifier", "negativeCaseId": "proofkit.workflow-smoke.case", + "wrongImplementationClassId": "proofkit.workflow-smoke.wrong", "dominanceGroup": "proofkit.workflow-smoke.materialization", "supersedes": []any{}, + }, + "oracle": map[string]any{ + "oracleId": "proofkit.workflow-smoke.oracle", "oracleKind": "negative_exit_and_diagnostic", + "expectedPublicOutcome": "invalid materialization fails closed", + "assertionSummary": "A contradictory materialization request is rejected before mutation.", + }, + "nonClaims": []any{}, + }}, + "nonClaims": []any{"Installed workflow inventory fixture does not execute native tests."}, + }, + }, + "nonClaims": []any{"Installed workflow materialization request is test-only."}, + } + payload, err := stablejson.Marshal(value) + if err != nil { + return nil, nil, fmt.Errorf("encode workflow smoke materialization input: %w", err) + } + return value, payload, nil +} diff --git a/internal/tools/workflowsmoke/project_navigation_smoke.go b/internal/tools/workflowsmoke/project_navigation_smoke.go index 7f2c832..bff9c0e 100644 --- a/internal/tools/workflowsmoke/project_navigation_smoke.go +++ b/internal/tools/workflowsmoke/project_navigation_smoke.go @@ -6,8 +6,12 @@ import ( "errors" "fmt" "os" + "path/filepath" + "github.com/research-engineering/agentic-proofkit/internal/command/adoptionmaterialization" "github.com/research-engineering/agentic-proofkit/internal/command/projectstatus" + "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/repositorytransaction" ) func verifyProjectNavigation(ctx context.Context, run Runner) (returnErr error) { @@ -86,5 +90,91 @@ func verifyProjectNavigation(ctx context.Context, run Runner) (returnErr error) if err := verifyFailure(ctx, run, "project status required root", unreadInvocation("status"), "requires --repo-root"); err != nil { return err } - return verifyFailure(ctx, run, "project next JSON color denial", unreadInvocation("next", "--repo-root", repositoryRoot, "--color", "never"), "--color requires --format text") + if err := verifyFailure(ctx, run, "project next JSON color denial", unreadInvocation("next", "--repo-root", repositoryRoot, "--color", "never"), "--color requires --format text"); err != nil { + return err + } + return verifyMaterializedProjectNavigation(ctx, run, repositoryRoot) +} + +func verifyMaterializedProjectNavigation(ctx context.Context, run Runner, repositoryRoot string) error { + rawInput, input, err := materializationSmokeInput(ctx, repositoryRoot) + if err != nil { + return err + } + expectedPlan, err := adoptionmaterialization.BuildPlan(ctx, rawInput, repositoryRoot) + if err != nil { + return fmt.Errorf("build expected installed materialization plan: %w", err) + } + planResult, err := invoke(ctx, run, "installed materialization plan", bytesInvocation(input, "adopt", "materialize", "plan", "--input", "-", "--repo-root", repositoryRoot)) + if err != nil { + return err + } + if err := verifyExactJSONObject(planResult, expectedPlan.JSONValue(), "installed materialization plan"); err != nil { + return err + } + applyResult, err := invoke(ctx, run, "installed materialization apply", bytesInvocation( + input, + "adopt", "materialize", "apply", "--input", "-", "--repo-root", repositoryRoot, + "--expect-transaction", expectedPlan.Transaction.TransactionID, + "--expect-desired-state", expectedPlan.Transaction.DesiredStateID, + )) + if err != nil { + return err + } + receiptValue, err := admission.DecodeJSON(bytes.NewReader(applyResult.Stdout), int64(len(applyResult.Stdout))) + if err != nil { + return fmt.Errorf("decode installed materialization receipt: %w", err) + } + receipt, err := adoptionmaterialization.AdmitReceiptOutput(receiptValue) + if err != nil { + return fmt.Errorf("admit installed materialization receipt: %w", err) + } + if receipt.State != adoptionmaterialization.ReceiptStatePassed || receipt.Operation != adoptionmaterialization.OperationApply || receipt.TransactionResult == nil || receipt.TransactionResult.State != repositorytransaction.StateApplied || receipt.ExpectedTransactionID != expectedPlan.Transaction.TransactionID || receipt.ExpectedDesiredStateID != expectedPlan.Transaction.DesiredStateID { + return fmt.Errorf("installed materialization receipt does not prove the expected applied transaction") + } + if err := verifyInstalledProjectState(ctx, run, repositoryRoot, projectstatus.StateVerificationRequired, projectstatus.ActionRunRepositoryVerification, "materialized"); err != nil { + return err + } + if len(expectedPlan.Manifest.Routes) == 0 { + return fmt.Errorf("installed materialization plan has no routed child") + } + driftPath := filepath.Join(repositoryRoot, filepath.FromSlash(expectedPlan.Manifest.Routes[0].Path)) + file, err := os.OpenFile(driftPath, os.O_APPEND|os.O_WRONLY, 0) + if err != nil { + return fmt.Errorf("open installed materialization child for drift: %w", err) + } + if _, writeErr := file.Write([]byte{'\n'}); writeErr != nil { + _ = file.Close() + return fmt.Errorf("drift installed materialization child: %w", writeErr) + } + if err := file.Close(); err != nil { + return fmt.Errorf("close drifted installed materialization child: %w", err) + } + return verifyInstalledProjectState(ctx, run, repositoryRoot, projectstatus.StateStale, projectstatus.ActionRematerializeProject, "drifted materialized") +} + +func verifyInstalledProjectState(ctx context.Context, run Runner, repositoryRoot string, wantState projectstatus.ProjectState, wantAction, label string) error { + expectedStatus, err := projectstatus.Inspect(ctx, repositoryRoot) + if err != nil { + return fmt.Errorf("build expected %s project status: %w", label, err) + } + if expectedStatus.ProjectState != wantState || expectedStatus.NextAction.ActionClass != wantAction { + return fmt.Errorf("expected %s project state/action is inconsistent", label) + } + statusResult, err := invoke(ctx, run, label+" project status", unreadInvocation("status", "--repo-root", repositoryRoot)) + if err != nil { + return err + } + if err := verifyExactJSONObject(statusResult, expectedStatus.JSONValue(), label+" project status"); err != nil { + return err + } + expectedNext, err := projectstatus.NextFromStatus(expectedStatus) + if err != nil { + return fmt.Errorf("build expected %s project next action: %w", label, err) + } + nextResult, err := invoke(ctx, run, label+" project next action", unreadInvocation("next", "--repo-root", repositoryRoot)) + if err != nil { + return err + } + return verifyExactJSONObject(nextResult, expectedNext.JSONValue(), label+" project next action") } diff --git a/internal/tools/workflowsmoke/workflow_smoke_test.go b/internal/tools/workflowsmoke/workflow_smoke_test.go index 517694e..0e158bf 100644 --- a/internal/tools/workflowsmoke/workflow_smoke_test.go +++ b/internal/tools/workflowsmoke/workflow_smoke_test.go @@ -8,11 +8,13 @@ import ( "io" "os" "os/exec" + "path/filepath" "strings" "testing" "time" "github.com/research-engineering/agentic-proofkit/internal/app" + "github.com/research-engineering/agentic-proofkit/internal/command/adoptionmaterialization" "github.com/research-engineering/agentic-proofkit/internal/tools/workflowsmoke" ) @@ -26,10 +28,11 @@ func TestVerifyAcceptsApplicationCLI(t *testing.T) { func TestVerifyRejectsCarrierContractMutations(t *testing.T) { mutations := []struct { - name string - match string - matchPrefix bool - apply func(workflowsmoke.Result) workflowsmoke.Result + name string + match string + matchPrefix bool + materializedOnly bool + apply func(workflowsmoke.Result) workflowsmoke.Result }{ {name: "retired planner route", match: "change-workflow-plan --input -", apply: func(result workflowsmoke.Result) workflowsmoke.Result { return workflowsmoke.Result{ExitCode: 0, Stdout: []byte("{}\n")} @@ -50,6 +53,9 @@ func TestVerifyRejectsCarrierContractMutations(t *testing.T) { {name: "guidance text suffix", match: "native-evidence-guidance --format text --color never", apply: appendStdout("surplus\n")}, {name: "project status identity", match: "status --repo-root ", matchPrefix: true, apply: replaceStdoutFragment(`"reportKind": "proofkit.project-status"`, `"reportKind": "wrong"`)}, {name: "project next identity", match: "next --repo-root ", matchPrefix: true, apply: replaceStdoutFragment(`"packetKind": "proofkit.project-next-action"`, `"packetKind": "wrong"`)}, + {name: "materialized project status failure", match: "status --repo-root ", matchPrefix: true, materializedOnly: true, apply: func(result workflowsmoke.Result) workflowsmoke.Result { + return workflowsmoke.Result{ExitCode: 1, Stderr: []byte("injected materialized-only failure\n")} + }}, } for _, mutation := range mutations { t.Run(mutation.name, func(t *testing.T) { @@ -58,6 +64,9 @@ func TestVerifyRejectsCarrierContractMutations(t *testing.T) { result, err := applicationRunner(ctx, invocation) joined := strings.Join(invocation.Args, " ") matches := joined == mutation.match || (mutation.matchPrefix && strings.HasPrefix(joined, mutation.match)) + if matches && mutation.materializedOnly && !hasMaterializedProject(invocation) { + matches = false + } if err == nil && !applied && matches { result = mutation.apply(result) applied = true @@ -74,6 +83,17 @@ func TestVerifyRejectsCarrierContractMutations(t *testing.T) { } } +func hasMaterializedProject(invocation workflowsmoke.Invocation) bool { + for index := 0; index+1 < len(invocation.Args); index++ { + if invocation.Args[index] != "--repo-root" { + continue + } + _, err := os.Stat(filepath.Join(invocation.Args[index+1], filepath.FromSlash(adoptionmaterialization.ProjectManifestPath))) + return err == nil + } + return false +} + func TestRunProcessRejectsTimeout(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 500*time.Millisecond) defer cancel() diff --git a/proofkit/cli-contract.v2.json b/proofkit/cli-contract.v2.json index a6a88d3..4b3f7cd 100644 --- a/proofkit/cli-contract.v2.json +++ b/proofkit/cli-contract.v2.json @@ -143,7 +143,7 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:8d0b8e8bc2bda79e78ad42c0271295c2d3e86865aec80d0a995df93c9c759b3e", + "canonicalDigest": "sha256:e2b081d812ee0a551e608cf3e59fce28d143a9be6588cea45018709af48628fa", "evidenceClass": "source_checkout" }, { @@ -281,7 +281,7 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:8d0b8e8bc2bda79e78ad42c0271295c2d3e86865aec80d0a995df93c9c759b3e", + "canonicalDigest": "sha256:e2b081d812ee0a551e608cf3e59fce28d143a9be6588cea45018709af48628fa", "evidenceClass": "source_checkout" }, { @@ -395,7 +395,7 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:8d0b8e8bc2bda79e78ad42c0271295c2d3e86865aec80d0a995df93c9c759b3e", + "canonicalDigest": "sha256:e2b081d812ee0a551e608cf3e59fce28d143a9be6588cea45018709af48628fa", "evidenceClass": "source_checkout" }, { @@ -1186,7 +1186,7 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:8d0b8e8bc2bda79e78ad42c0271295c2d3e86865aec80d0a995df93c9c759b3e", + "canonicalDigest": "sha256:e2b081d812ee0a551e608cf3e59fce28d143a9be6588cea45018709af48628fa", "evidenceClass": "source_checkout" }, { @@ -3057,7 +3057,7 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:8d0b8e8bc2bda79e78ad42c0271295c2d3e86865aec80d0a995df93c9c759b3e", + "canonicalDigest": "sha256:e2b081d812ee0a551e608cf3e59fce28d143a9be6588cea45018709af48628fa", "evidenceClass": "source_checkout" }, { @@ -6347,7 +6347,7 @@ "rootDefinitionDigest": "sha256:3c842174dff5361e7f83166469b832805e05aa314b073c16234b5b64e346281e", "nativeSource": { "path": "internal/app", - "canonicalDigest": "sha256:8d0b8e8bc2bda79e78ad42c0271295c2d3e86865aec80d0a995df93c9c759b3e", + "canonicalDigest": "sha256:e2b081d812ee0a551e608cf3e59fce28d143a9be6588cea45018709af48628fa", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -6376,7 +6376,7 @@ "rootDefinitionDigest": "sha256:0ea95e277ebe44cd2de42c29b47c38686ac0b6b390d8965367437b3fe138e209", "nativeSource": { "path": "internal/app", - "canonicalDigest": "sha256:8d0b8e8bc2bda79e78ad42c0271295c2d3e86865aec80d0a995df93c9c759b3e", + "canonicalDigest": "sha256:e2b081d812ee0a551e608cf3e59fce28d143a9be6588cea45018709af48628fa", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { diff --git a/proofkit/requirement-bindings.json b/proofkit/requirement-bindings.json index c623c85..6928479 100644 --- a/proofkit/requirement-bindings.json +++ b/proofkit/requirement-bindings.json @@ -886,7 +886,7 @@ "specPath": "docs/specs/proofkit-agent-workflow/requirements.v1.json", "claimLevel": "blocking", "proofState": "witness_backed", - "nonClaims": ["A coherent read snapshot does not prevent later filesystem mutation, authenticate repository ownership, provide power-loss or multi-reader atomicity, exclude a same-user writer that bypasses the repository transaction owner, or identify unread out-of-bound record bytes beyond their normalized invalid class."] + "nonClaims": ["A coherent read snapshot does not prevent later filesystem mutation, authenticate repository ownership, provide power-loss or multi-reader atomicity, exclude a same-user writer that bypasses the repository transaction owner, identify unread out-of-bound record bytes beyond their normalized invalid class, or prevent the filesystem from updating read-side metadata such as access time."] }, { "requirementId": "REQ-PROOFKIT-WORKFLOW-014", @@ -6262,6 +6262,25 @@ "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] }, + { + "requirementId": "REQ-PROOFKIT-SPEC-035", + "scenarioId": "proofkit.spec-proof-core.project-navigation-public-abi-diff", + "witnessId": "proofkit.project-navigation.public-abi-diff-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/app/project_navigation_abi_closure_test.go", + "witnessSelectors": [ + { + "selector": "TestProjectNavigationVersionEdgeClosesCompletePublicABIDiff", + "command": "go test ./internal/app -run '^TestProjectNavigationVersionEdgeClosesCompletePublicABIDiff$'" + }, + { + "selector": "TestProjectNavigationVersionEdgeRejectsUndeclaredPublicABIDrift", + "command": "go test ./internal/app -run '^TestProjectNavigationVersionEdgeRejectsUndeclaredPublicABIDrift$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, { "requirementId": "REQ-PROOFKIT-WORKFLOW-012", "scenarioId": "proofkit.agent-workflow.project-state-total-classification", @@ -6323,8 +6342,8 @@ "witnessPath": "internal/command/projectstatus/inspect_test.go", "witnessSelectors": [ { - "selector": "TestInspectClassifiesMaterializedProjectWithoutMutation", - "command": "go test ./internal/command/projectstatus -run '^TestInspectClassifiesMaterializedProjectWithoutMutation$'" + "selector": "TestInspectClassifiesMaterializedProjectWithoutApplicationWrites", + "command": "go test ./internal/command/projectstatus -run '^TestInspectClassifiesMaterializedProjectWithoutApplicationWrites$'" }, { "selector": "TestInspectFailsClosedOnSymlinksAndBoundsWithoutDisclosure", @@ -6374,6 +6393,10 @@ "selector": "TestInspectDeduplicatesRepeatedIssueCodes", "command": "go test ./internal/command/projectstatus -run '^TestInspectDeduplicatesRepeatedIssueCodes$'" }, + { + "selector": "TestInspectHonorsCancellationBetweenBoundedReads", + "command": "go test ./internal/command/projectstatus -run '^TestInspectHonorsCancellationBetweenBoundedReads$'" + }, { "selector": "TestOutOfBoundManifestIdentityIsAClassificationNotByteIdentity", "command": "go test ./internal/command/projectstatus -run '^TestOutOfBoundManifestIdentityIsAClassificationNotByteIdentity$'" @@ -6490,6 +6513,10 @@ "selector": "TestProjectStatusCLI", "command": "go test ./internal/app -run '^TestProjectStatusCLI$'" }, + { + "selector": "TestProjectStatusCLIHonorsCanceledContextBeforeOutput", + "command": "go test ./internal/app -run '^TestProjectStatusCLIHonorsCanceledContextBeforeOutput$'" + }, { "selector": "TestProjectStatusOutputMatrix", "command": "go test ./internal/app -run '^TestProjectStatusOutputMatrix$'" From ef124f29cc7c50caa18062ae11c9f9e02c13ce50 Mon Sep 17 00:00:00 2001 From: iperev Date: Sat, 5 Sep 2026 07:17:25 +0200 Subject: [PATCH 3/5] docs: keep release facts machine-owned --- docs/specs/proofkit-spec-proof-core/overview.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/specs/proofkit-spec-proof-core/overview.md b/docs/specs/proofkit-spec-proof-core/overview.md index e27dfe1..cf425b1 100644 --- a/docs/specs/proofkit-spec-proof-core/overview.md +++ b/docs/specs/proofkit-spec-proof-core/overview.md @@ -199,8 +199,8 @@ execution receipts, and merge policy. - `REQ-PROOFKIT-SPEC-034`: the pre-materialization-to-transactional- materialization public version edge binds all three transactional materialization routes and their exact public contracts to a compatible - byte-frozen 0.8.0 release record without coupling the historical edge to the - live release record or reinterpreting the frozen prior edge. + byte-frozen predecessor release record without coupling the historical edge + to the live release record or reinterpreting the frozen prior edge. - `REQ-PROOFKIT-SPEC-035`: the project-state public version edge binds the exact raw ABI identities, proves the complete semantic ABI difference after normalizing only native-source digests, and binds status, next, the From 7c280a16dcecd5a26a0c11d7295eaa5de269be4c Mon Sep 17 00:00:00 2001 From: iperev Date: Sat, 5 Sep 2026 09:05:41 +0200 Subject: [PATCH 4/5] fix: close project navigation review findings --- .../specs/proofkit-agent-workflow/overview.md | 5 +- .../requirements.v1.json | 5 +- internal/app/cli_contract_test.go | 2 +- internal/app/command_contract_generated.go | 18 +-- internal/app/command_coverage_routes.go | 4 +- internal/app/command_descriptors.go | 4 +- .../project_navigation_abi_closure_test.go | 20 --- .../project_navigation_abi_mutation_test.go | 151 ++++++++++++++++++ internal/app/project_status_command.go | 32 +++- internal/app/project_status_command_test.go | 76 ++++++++- .../compact-current-production-consumers.json | 2 + .../app/testdata/v0.9-wire-observations.json | 6 +- .../command/projectstatus/dependency_test.go | 62 +++++++ internal/command/projectstatus/inspect.go | 12 +- .../command/projectstatus/inspect_test.go | 76 +++++++++ .../stackpreset/preset_ids_generated.go | 2 +- internal/tools/coveragemetrics/main_test.go | 6 + .../coveragemetrics/required_inventory.go | 24 ++- internal/tools/packageverify/main.go | 6 +- .../tools/packageverify/workflow_carrier.go | 19 +++ .../packageverify/workflow_carrier_test.go | 56 +++++++ internal/tools/pythonpackage/verify.go | 16 +- .../tools/pythonpackage/workflow_carrier.go | 44 +++++ .../pythonpackage/workflow_carrier_test.go | 71 ++++++++ proofkit/cli-contract.v2.json | 18 +-- proofkit/requirement-bindings.json | 73 ++++++++- 26 files changed, 723 insertions(+), 87 deletions(-) create mode 100644 internal/app/project_navigation_abi_mutation_test.go create mode 100644 internal/tools/packageverify/workflow_carrier.go create mode 100644 internal/tools/packageverify/workflow_carrier_test.go create mode 100644 internal/tools/pythonpackage/workflow_carrier.go create mode 100644 internal/tools/pythonpackage/workflow_carrier_test.go diff --git a/docs/specs/proofkit-agent-workflow/overview.md b/docs/specs/proofkit-agent-workflow/overview.md index c576a05..f0e9097 100644 --- a/docs/specs/proofkit-agent-workflow/overview.md +++ b/docs/specs/proofkit-agent-workflow/overview.md @@ -88,8 +88,9 @@ projections remain independently owned by the spec-proof-core package. - `REQ-PROOFKIT-WORKFLOW-014`: one total state-to-action table, one bounded next action, explicit owner decisions, and no embedded route universe. - `REQ-PROOFKIT-WORKFLOW-015`: status/next CLI channel and exit semantics, - pre-emission failure discipline, one bounded stdout write without claiming - atomicity from a failing external sink, and a versioned breaking replacement + checkpointed pre-emission failure discipline, one bounded stdout write + without claiming cancellation rollback or atomicity from an external sink, + and a versioned breaking replacement of the flat change route by `change plan` across source and installed carriers. Shared stable-JSON/diagnostic hardening is owned by the supply-chain-quality diff --git a/docs/specs/proofkit-agent-workflow/requirements.v1.json b/docs/specs/proofkit-agent-workflow/requirements.v1.json index 119f176..0e9799e 100644 --- a/docs/specs/proofkit-agent-workflow/requirements.v1.json +++ b/docs/specs/proofkit-agent-workflow/requirements.v1.json @@ -190,14 +190,15 @@ { "requirementId": "REQ-PROOFKIT-WORKFLOW-015", "ownerId": "proofkit.agent-workflow", - "invariant": "The public status and next commands require exactly one explicit --repo-root, default to stable ANSI-free JSON, admit text and terminal-only color through the shared presentation contract, return exit zero for every successfully classified project state, including bounded project-record decoding or admission failure, and return exit one without beginning output emission for invocation, confinement, cancellation, inspection-bound, concurrent-change, cleanup, or serialization failure; successful serialization reaches stdout through one bounded write, and a transport failure returns exit one without claiming atomic behavior from a writer that accepted a prefix before failing; the same versioned breaking public edge adds both commands, replaces the flat change-workflow-plan route with change plan without adding a route-alias registry or second implementation, rejects the retired route, admits the hierarchical route, and closes descriptor, help, CLI contract, command family, contract map, ABI, source witness, installed npm, and installed wheel surfaces.", + "invariant": "The public status and next commands require exactly one explicit --repo-root, default to stable ANSI-free JSON, admit text and terminal-only color through the shared presentation contract, return exit zero for every successfully classified project state, including bounded project-record decoding or admission failure, and return exit one without beginning output emission for invocation, confinement, inspection-bound, concurrent-change, cleanup, serialization, or cancellation observed at bounded operation checkpoints and the final pre-emission checkpoint; successful serialization reaches stdout through one bounded write, and a transport failure returns exit one without claiming atomic behavior from a writer that accepted a prefix before failing; the same versioned breaking public edge adds both commands, replaces the flat change-workflow-plan route with change plan without adding a route-alias registry or second implementation, rejects the retired route, admits the hierarchical route, and closes descriptor, help, CLI contract, command family, contract map, ABI, source witness, installed npm, and installed wheel surfaces.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], "nonClaimRefs": ["NC-PROOFKIT-WORKFLOW-015"], "nonClaims": [ "A caller-provided stdout writer that accepts a prefix and then fails does not provide an atomic sink, so Proofkit does not claim that such a transport leaves stdout empty.", - "CLI surface closure does not prove registry publication, provider ingestion, consumer migration, native witness truth, rollout, or production readiness." + "CLI surface closure does not prove registry publication, provider ingestion, consumer migration, native witness truth, rollout, or production readiness.", + "Cancellation racing after the final pre-emission checkpoint cannot retract bytes subsequently accepted by an external writer." ], "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, "deferral": null, diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index 19beee2..98a82a4 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -24,7 +24,7 @@ import ( ) const ( - cliContractPublicABISHA256 = "a331f29499c3dbfd7e03ac4c0d56ce4d71910f735297af79ba0092b543e171f0" + cliContractPublicABISHA256 = "06bc3a98cb26f50e988b2bab88ee5d0aee9ebdab10a8fe116d8933364e7cd2f4" maxAggregateFileReadBytesForContractTest = 64 << 20 maxPackageManifestBytesForContractTest = 256 << 10 maxSourceFileBytesForContractTest = 8 << 20 diff --git a/internal/app/command_contract_generated.go b/internal/app/command_contract_generated.go index 1791b46..53ee9f4 100644 --- a/internal/app/command_contract_generated.go +++ b/internal/app/command_contract_generated.go @@ -1,7 +1,7 @@ // Code generated by internal/tools/commandcontractgen; DO NOT EDIT. package app -const commandContractSourceSHA256 = "514f09bd74c91e6f1378e8e58483d6f5b0ef926243f37da11080f768a4afba31" +const commandContractSourceSHA256 = "133fcfee23ce5e81cc0a6f6b8325ea90e21625c6255a568448de4cf0d03904a0" type generatedCommandContractMetadata struct { InputContractSHA256 string @@ -12,15 +12,15 @@ type generatedCommandContractMetadata struct { } var generatedCommandContractMetadataByName = map[string]generatedCommandContractMetadata{ - "adopt-materialize-apply": {InputContractSHA256: "sha256:98539b75bf6d4caebc75d724e9201925a1235290791adcebc865c1b3c74976ee", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.apply-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:9e3f86a960b327c327beeeab4a52056152bae2b5a4141aed060ec92e5d9c201f", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "apply"}}, - "adopt-materialize-plan": {InputContractSHA256: "sha256:65925b56d0332349c5048e9b1450ff0b7a3381224342df1a630128e3f4a2cde2", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.plan-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:5d458969737cd782d5879fda8747ac3fcc5e434b76d88e7f1aa8baa02d639798", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "plan"}}, - "adopt-materialize-recover": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:8ca5aa7b580c87affc453c7aac40d043387d0187d17eb0f208b1a39ee3b4f3a6", FlagChoices: map[string][]string{"--action": []string{"resume", "rollback"}, "--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "recover"}}, + "adopt-materialize-apply": {InputContractSHA256: "sha256:98539b75bf6d4caebc75d724e9201925a1235290791adcebc865c1b3c74976ee", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.apply-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:57df1df73802292a5dfb3b2ca62d817345acff404477f716b7e02a825b2f4107", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "apply"}}, + "adopt-materialize-plan": {InputContractSHA256: "sha256:65925b56d0332349c5048e9b1450ff0b7a3381224342df1a630128e3f4a2cde2", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.plan-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:09118bc911537bc78339a81bd7774550340b883a9c92b8be87be5c893326efd0", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "plan"}}, + "adopt-materialize-recover": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:0366a6616ee096862719c5cdbba47cc507aef9a250c876c8f35673c0438afcb8", FlagChoices: map[string][]string{"--action": []string{"resume", "rollback"}, "--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "recover"}}, "adopt-plan": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:13e3392d9005c27fed3003a1d036123fe12a341dbe16a97c0cd9f8272fb83320", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}, "--mode": []string{"audit-from-code", "code-baseline", "fresh"}, "--stack": []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"}}, RouteTokens: []string{"adopt", "plan"}}, "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{}, RouteTokens: []string{"adoption-checklist"}}, "adoption-contract-envelope": {InputContractSHA256: "sha256:c310214676ff4b6f536a5bc9d687f681a7e71f73d7a03ac932707d8cd3905cdf", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.adoption-contract-envelope.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3efb2c5161fee16fd8ac6a40dcb6d9c41fbc23e468f60621436ae9e8076e0950", FlagChoices: map[string][]string{}, RouteTokens: []string{"adoption-contract-envelope"}}, "adoption-doctor": {InputContractSHA256: "sha256:efa9acfe32bff07f56d9dc9902530df2979794289bc2f7f547f7a108a7dd0f35", 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:8fdfc6608f197e633f042f20031ae1014872a90aa3daa66885ffcaddca994766", FlagChoices: map[string][]string{}, RouteTokens: []string{"adoption-doctor"}}, "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{}, RouteTokens: []string{"adoption-workflow-plan"}}, - "agent-route": {InputContractSHA256: "sha256:c00e832b4e9eac6b858eec46e810431c0a5c9f56c5c50f055f39ee024f50014c", InputSchemaSummary: []string{"availableInputs", "browserMode", "goal", "knownChangedPaths", "mode", "nonClaims", "observedReports", "openBrowser", "routeId", "schemaVersion", "root-shape-only definition proofkit.agent-route.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:fef80e6b47fa245b27d16c112c7798a44403b1dd2f42bbef7e73ec7339aa8834", FlagChoices: map[string][]string{}, RouteTokens: []string{"agent-route"}}, + "agent-route": {InputContractSHA256: "sha256:c00e832b4e9eac6b858eec46e810431c0a5c9f56c5c50f055f39ee024f50014c", InputSchemaSummary: []string{"availableInputs", "browserMode", "goal", "knownChangedPaths", "mode", "nonClaims", "observedReports", "openBrowser", "routeId", "schemaVersion", "root-shape-only definition proofkit.agent-route.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:ad70c213408169defdfc05a2b095b793a9f9e9d9e4a7a3d5ec2e32cd83132f00", FlagChoices: map[string][]string{}, RouteTokens: []string{"agent-route"}}, "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{}, RouteTokens: []string{"binding-partition"}}, "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{}, RouteTokens: []string{"branch-authority"}}, "capability-map-admission": {InputContractSHA256: "sha256:e49433f295c43c34d5d660ac9d656b117ed87208406b57723d25165ffec5d486", 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:bfa35fe1be210ab98f3620694ab63b52a9724f7b92cd9dcbfd7b01b2c6a3555e", FlagChoices: map[string][]string{}, RouteTokens: []string{"capability-map-admission"}}, @@ -42,10 +42,10 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "migration-parity-admission": {InputContractSHA256: "sha256:0b36c0e68da3b857dac4b13e7b3bd523052459106133aa8c908a4352682e6c05", 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:8e0f8af2b205817f018b0fe133fe789661caa29007695e036bfcab63c1830f47", FlagChoices: map[string][]string{}, RouteTokens: []string{"migration-parity-admission"}}, "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{}, RouteTokens: []string{"migration-plan"}}, "native-evidence-guidance": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:c9306d800668ecea18aaced6a21334036a935570f267baed356e6a4888025c8d", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"native-evidence-guidance"}}, - "next": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:9dfa71617f9d727949a988bf726bcf0070460faa59565a8adc468994a36594c6", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"next"}}, + "next": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:92f6d3dc427a795ec97112e6c3ce55ebd4ab670e4323b4faa8215310a8492747", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"next"}}, "obligation-decision": {InputContractSHA256: "sha256:1dea2ed5c5066451d6d49b815cea99df2cdae2ef05d42fed16c8aeb45eb7f445", 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:96dc074f611bcc12e511bc803c548e4df623e2de869d3add29a3ea6386e04330", FlagChoices: map[string][]string{}, RouteTokens: []string{"obligation-decision"}}, "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{}, RouteTokens: []string{"package-runtime-dependency-admission"}}, - "pilot-admission": {InputContractSHA256: "sha256:a1d9116ce619f7d705349ff4ae44c0f4399a281ebaa9e7d62ea304ac57af59ba", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.pilot-admission.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:56b1f66f267e0fde973baa69675c5886f652451d81f7f6b9200fd8fb10784a34", FlagChoices: map[string][]string{}, RouteTokens: []string{"pilot-admission"}}, + "pilot-admission": {InputContractSHA256: "sha256:a1d9116ce619f7d705349ff4ae44c0f4399a281ebaa9e7d62ea304ac57af59ba", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.pilot-admission.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:33f5142785396fa40de9ea05f8c466e88a116cc5230345569d1bb55ca91401ca", FlagChoices: map[string][]string{}, RouteTokens: []string{"pilot-admission"}}, "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{}, RouteTokens: []string{"producer-policy-self-proof"}}, "proof-obligation-algebra": {InputContractSHA256: "sha256:4f176b6bc9bdbd0d96d65c071d66447d246665bda7a23269e7927f1d0b80b043", 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:f9ee9e56b349756c55856a2dab198e1ad85db70a468c38e3aeca73cfe2ed66f6", FlagChoices: map[string][]string{}, RouteTokens: []string{"proof-obligation-algebra"}}, "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{}, RouteTokens: []string{"proof-receipt-admission"}}, @@ -84,11 +84,11 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "selective-gate-evidence": {InputContractSHA256: "sha256:8aa178ab7ca7c475c23707bc4e15fd3f9f8d57acf6f6dcf279677e7769a45586", 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:723569262bb85d9674b2a78d3bcb6e9f4cab229b71e8c784ff1b804a7fcade71", FlagChoices: map[string][]string{}, RouteTokens: []string{"selective-gate-evidence"}}, "selective-gate-obligation-decision-input": {InputContractSHA256: "sha256:85761fcbc0ea94239d55bf379d0592a6ca814e6612a2d609a651f6cdaf8ca10a", 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:ab9dddabe975238d7019266c43350afa2df1a61d4c2eb7bc23afd520b588a2da", FlagChoices: map[string][]string{}, RouteTokens: []string{"selective-gate-obligation-decision-input"}}, "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{}, RouteTokens: []string{"selective-gate-plan"}}, - "self-check": {InputContractSHA256: "sha256:d2b3d3ad93ee66e9e57c2c73093a0fb3878aba6c96d39ca5aab112bf03a16a63", 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:ee7060dce28942b69bdb2b6ba5f6419a5e820f2df0d6b968867c664e26fc1312", FlagChoices: map[string][]string{}, RouteTokens: []string{"self-check"}}, + "self-check": {InputContractSHA256: "sha256:c8a71b9e6d59bf34cfa1261382a567fe6eed67d7e23256a36661ee434fedefb4", 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:614cf6e5329ee9a97c4d2c8429bf7853acc661abdfc65c496f4b96fd13b5c87b", FlagChoices: map[string][]string{}, RouteTokens: []string{"self-check"}}, "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{}, RouteTokens: []string{"spec-overview-claims"}}, "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{}, RouteTokens: []string{"spec-proof-bundle-admission"}}, "stack-preset": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:ef5920f363a4a96dcac308ea8412260a06e64ba4876460a369aefb8983130a9d", FlagChoices: map[string][]string{"--preset": []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"}}, RouteTokens: []string{"stack-preset"}}, - "status": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:29cf593bf349fd5ec7276f28ce75817a44a8b12fc7aea5b4c055cfaf4188924c", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"status"}}, + "status": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:3ac87fa98c4650fc3b758ab4a43dee1beeb12f9aee3631a35a2ddf5d0fc5f899", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"status"}}, "test-evidence-inventory": {InputContractSHA256: "sha256:c2e26a02e127eec069275c9afa902a20cb9e062f4008db35e042d8fe0e3fdd60", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.test-evidence-inventory.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:06fd4be6a34e73708f8539fed04ec41b2a4c9c79cf0e0dcc311496c0e6aafcbf", FlagChoices: map[string][]string{}, RouteTokens: []string{"test-evidence-inventory"}}, "text-policy": {InputContractSHA256: "sha256:afc366b0bcdcb33d7b85d3347f832cb647de68cb737cfa81a52e55f5b4901038", 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:94031bd5ee75115e80ebe10cb8c3c7b77806dc349721ba550cacaa99ff8184e4", FlagChoices: map[string][]string{}, RouteTokens: []string{"text-policy"}}, "typescript-public-api-surfaces": {InputContractSHA256: "sha256:11ddef5ae86c8f5ff9fcb8cfac540c8ea680cd61233e54e9ba74563b28020e2a", 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:f26372435db16de436cd5483089d06d3723496574b28d80de97ee36ccddf1587", FlagChoices: map[string][]string{}, RouteTokens: []string{"typescript-public-api-surfaces"}}, diff --git a/internal/app/command_coverage_routes.go b/internal/app/command_coverage_routes.go index e19f8cf..5d94687 100644 --- a/internal/app/command_coverage_routes.go +++ b/internal/app/command_coverage_routes.go @@ -101,7 +101,7 @@ var commandCoverageRoutes = map[string][]commandCoverageRoute{ packageFalsifierRoute("internal/command/nativeevidenceguidance/guidance_test.go", "TestGuidancePurityPredicates", semanticRouteProof("nativeevidenceguidance.guidance_purity_predicates"), "Native evidence guidance must remain deterministic and return fresh caller-owned projections without ambient authority."), }, "next": { - directCLIRoute("internal/app/project_status_command_test.go", "TestProjectStatusCLI", semanticRouteProof("project_status_command.next_whole_cli"), "Project next must preserve one owner-admitted non-executable action across JSON, text, color, and argument-admission paths."), + directCLIRoute("internal/app/project_status_command_test.go", "TestNextCLI", semanticRouteProof("project_status_command.next_whole_cli"), "Project next must preserve one owner-admitted non-executable action across JSON, text, color, and argument-admission paths."), packageFalsifierRoute("internal/command/projectstatus/projectstatus_test.go", "TestEvaluateTotalStateActionTable", semanticRouteProof("projectstatus.evaluate_total_state_action_table"), "Project next must remain a total one-action projection for every admitted project state without claiming execution or completion."), }, "obligation-decision": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/obligationdecision/obligationdecision_test.go", "TestBuildAdmitsSatisfiedBlockingObligationsAndRejectsMissingReceipt", semanticRouteProof("obligationdecision.build_admits_satisfied_blocking_obligations_and_rejects_missing_receipt"), "Obligation decision must fail blocking obligations that lack satisfying evidence states.")}, @@ -155,7 +155,7 @@ var commandCoverageRoutes = map[string][]commandCoverageRoute{ "spec-proof-bundle-admission": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/specproofbundleadmission/specproofbundleadmission_test.go", "TestBuildRejectsForgedReceiptAdmissionChild", semanticRouteProof("specproofbundleadmission.build_rejects_forged_receipt_admission_child"), "Spec proof bundle admission must reject forged child receipt admission reports.")}, "stack-preset": {directCLIRoute("internal/app/command_coverage_test.go", "TestNoInputCommandsHaveCommandSpecificBehavior", semanticRouteProof("command_coverage.no_input_commands_have_command_specific_behavior"), "Stack preset CLI route must emit JSON and reject unknown preset flags."), packageFalsifierRoute("internal/command/stackpreset/stackpreset_test.go", "TestPresetInventoryIsCompleteDeterministicAndDefensivelyCopied", semanticRouteProof("stackpreset.preset_inventory_is_complete_deterministic_and_defensively_copied"), "Stack preset inventory must keep preset ids aligned with complete non-empty profile records and defensive copies."), packageFalsifierRoute("internal/command/stackpreset/stackpreset_test.go", "TestUnknownPresetIsRejected", semanticRouteProof("stackpreset.unknown_preset_is_rejected"), "Stack preset package API must reject unknown preset ids.")}, "status": { - directCLIRoute("internal/app/project_status_command_test.go", "TestProjectStatusCLI", semanticRouteProof("project_status_command.status_whole_cli"), "Project status must preserve owner-admitted bounded classification across JSON, text, color, and pre-I/O argument admission paths."), + directCLIRoute("internal/app/project_status_command_test.go", "TestStatusCLI", semanticRouteProof("project_status_command.status_whole_cli"), "Project status must preserve owner-admitted bounded classification across JSON, text, color, and pre-I/O argument admission paths."), packageFalsifierRoute("internal/command/projectstatus/inspect_test.go", "TestInspectClassifiesMaterializedProjectWithoutApplicationWrites", semanticRouteProof("projectstatus.inspect_classifies_materialized_project_without_application_writes"), "Project status must classify absent, admitted, and stale materialized projects without performing repository mutation operations or disclosing repository paths."), packageFalsifierRoute("internal/command/projectstatus/inspect_test.go", "TestInspectCohortValidationClosesCleanEpochABA", semanticRouteProof("projectstatus.inspect_cohort_validation_closes_clean_epoch_aba"), "Project status must reject a clean-state ABA when manifest or child content changes between its bounded observation passes."), }, diff --git a/internal/app/command_descriptors.go b/internal/app/command_descriptors.go index 86b62ea..8793b39 100644 --- a/internal/app/command_descriptors.go +++ b/internal/app/command_descriptors.go @@ -125,7 +125,7 @@ var commandDescriptors = []commandDescriptor{ 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("native-evidence-guidance", commandInputNone, flags("--color", "--format"), modes("json", "text"), ownerDirs("nativeevidenceguidance"), withRunner(commandRunnerAgentWorkflow), withSemanticAppTests("TestAgentWorkflowCLITruthTable"), withFlagChoices("--color", "auto", "never"), withFlagChoices("--format", "json", "text"), withSingleOccurrenceFlags("--color")), - command("next", commandInputNone, flags("--color", "--format", "--repo-root"), modes("json", "text"), ownerDirs("projectstatus"), withRunner(commandRunnerProjectStatus), withSemanticAppTests("TestProjectStatusCLI"), withScopeClass(commandScopeExplicitFileSystemScan), withRequiredFlags("--repo-root"), withFlagChoices("--color", "auto", "never"), withFlagChoices("--format", "json", "text"), withFlagPresenceAndRequiredValue("--color", "--format", "text"), withSingleOccurrenceFlags("--color", "--repo-root")), + command("next", commandInputNone, flags("--color", "--format", "--repo-root"), modes("json", "text"), ownerDirs("projectstatus"), withRunner(commandRunnerProjectStatus), withSemanticAppTests("TestNextCLI"), withScopeClass(commandScopeExplicitFileSystemScan), withRequiredFlags("--repo-root"), withFlagChoices("--color", "auto", "never"), withFlagChoices("--format", "json", "text"), withFlagPresenceAndRequiredValue("--color", "--format", "text"), withSingleOccurrenceFlags("--color", "--repo-root")), 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")), command("pilot-admission", commandInputRequired, flags("--contract-envelope", "--input", "--input-pointer", "--pilot", "--stack-diverse"), modes("json"), ownerDirs("pilotadmission"), withRunner(commandRunnerPilotAdmission), withContractEnvelope(), withFlagValueRequirement("--pilot", "all", "--contract-envelope")), @@ -171,7 +171,7 @@ var commandDescriptors = []commandDescriptor{ command("spec-overview-claims", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("specoverviewclaims")), command("spec-proof-bundle-admission", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("specproofbundleadmission")), command("stack-preset", commandInputNone, flags("--preset"), modes("json"), ownerDirs("stackpreset"), withRunner(commandRunnerStackPreset), withSemanticAppTests("TestNoInputCommandsHaveCommandSpecificBehavior"), withRequiredFlags("--preset")), - command("status", commandInputNone, flags("--color", "--format", "--repo-root"), modes("json", "text"), ownerDirs("projectstatus"), withRunner(commandRunnerProjectStatus), withSemanticAppTests("TestProjectStatusCLI"), withScopeClass(commandScopeExplicitFileSystemScan), withRequiredFlags("--repo-root"), withFlagChoices("--color", "auto", "never"), withFlagChoices("--format", "json", "text"), withFlagPresenceAndRequiredValue("--color", "--format", "text"), withSingleOccurrenceFlags("--color", "--repo-root")), + command("status", commandInputNone, flags("--color", "--format", "--repo-root"), modes("json", "text"), ownerDirs("projectstatus"), withRunner(commandRunnerProjectStatus), withSemanticAppTests("TestStatusCLI"), withScopeClass(commandScopeExplicitFileSystemScan), withRequiredFlags("--repo-root"), withFlagChoices("--color", "auto", "never"), withFlagChoices("--format", "json", "text"), withFlagPresenceAndRequiredValue("--color", "--format", "text"), withSingleOccurrenceFlags("--color", "--repo-root")), 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")), diff --git a/internal/app/project_navigation_abi_closure_test.go b/internal/app/project_navigation_abi_closure_test.go index bfa5b7d..ab4d62f 100644 --- a/internal/app/project_navigation_abi_closure_test.go +++ b/internal/app/project_navigation_abi_closure_test.go @@ -45,26 +45,6 @@ func TestProjectNavigationVersionEdgeClosesCompletePublicABIDiff(t *testing.T) { } } -func TestProjectNavigationVersionEdgeRejectsUndeclaredPublicABIDrift(t *testing.T) { - frozen := readFrozenProjectNavigationPublicABI(t) - current := readProjectNavigationCLIContractRaw(t) - commands := current["commands"].([]any) - for index, raw := range commands { - record := raw.(map[string]any) - if record["command"] != "impact" { - continue - } - mutant := clonePublicABIRecord(record) - mutant["route"] = []any{"impact-drift"} - commands[index] = mutant - if err := verifyCompleteProjectNavigationABIDiff(frozen, current); err == nil { - t.Fatal("undeclared existing-command ABI drift was admitted") - } - return - } - t.Fatal("current CLI contract is missing impact") -} - func readFrozenProjectNavigationPublicABI(t *testing.T) frozenProjectNavigationPublicABI { t.Helper() content, err := os.ReadFile(filepath.Join(repoRoot(t), frozenProjectNavigationPublicABIPath)) diff --git a/internal/app/project_navigation_abi_mutation_test.go b/internal/app/project_navigation_abi_mutation_test.go new file mode 100644 index 0000000..84a95f4 --- /dev/null +++ b/internal/app/project_navigation_abi_mutation_test.go @@ -0,0 +1,151 @@ +package app + +import ( + "encoding/json" + "slices" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" +) + +func TestProjectNavigationVersionEdgeRejectsUndeclaredPublicABIDrift(t *testing.T) { + frozen := readFrozenProjectNavigationPublicABI(t) + firstDefinition := sortedFirstKey(t, frozen.ContractDefinitions) + tests := []struct { + name string + mutate func(map[string]any) + }{ + {name: "header", mutate: func(current map[string]any) { current["packageName"] = "unexpected" }}, + {name: "predecessor command removed", mutate: func(current map[string]any) { + removePublicABIRecord(t, current, "commands", "command", "impact") + }}, + {name: "predecessor command changed", mutate: func(current map[string]any) { + mutatePublicABIRecord(t, current, "commands", "command", "impact", func(record map[string]any) { record["route"] = []any{"impact-drift"} }) + }}, + {name: "command order", mutate: func(current map[string]any) { swapFirstPublicABIRecords(t, current, "commands") }}, + {name: "unexpected command", mutate: func(current map[string]any) { + appendRenamedPublicABIRecord(t, current, "commands", "command", "impact", "zz-unexpected-command") + }}, + {name: "predecessor definition removed", mutate: func(current map[string]any) { + removePublicABIRecord(t, current, "contractDefinitions", "definitionId", firstDefinition) + }}, + {name: "predecessor definition changed", mutate: func(current map[string]any) { + mutatePublicABIRecord(t, current, "contractDefinitions", "definitionId", firstDefinition, func(record map[string]any) { record["unexpectedField"] = true }) + }}, + {name: "definition order", mutate: func(current map[string]any) { swapFirstPublicABIRecords(t, current, "contractDefinitions") }}, + {name: "unexpected definition", mutate: func(current map[string]any) { + appendRenamedPublicABIRecord(t, current, "contractDefinitions", "definitionId", firstDefinition, "proofkit.zz-unexpected.definition") + }}, + {name: "process contract", mutate: func(current map[string]any) { + process := clonePublicABIRecord(current["processContract"].(map[string]any)) + process["successExitCode"] = json.Number("9") + current["processContract"] = process + }}, + {name: "omitted route policy missing", mutate: func(current map[string]any) { mutateOmittedRoutePolicy(t, current, nil) }}, + {name: "omitted route policy changed", mutate: func(current map[string]any) { mutateOmittedRoutePolicy(t, current, "unexpected") }}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + current := readProjectNavigationCLIContractRaw(t) + test.mutate(current) + if err := verifyCompleteProjectNavigationABIDiff(frozen, current); err == nil { + t.Fatal("undeclared public ABI drift was admitted") + } + }) + } + + t.Run("native source digest drift is intentionally normalized", func(t *testing.T) { + current := readProjectNavigationCLIContractRaw(t) + mutatePublicABIRecord(t, current, "commands", "command", "impact", func(record map[string]any) { + for _, field := range []string{"inputContract", "outputContract"} { + contract := clonePublicABIRecord(record[field].(map[string]any)) + source := clonePublicABIRecord(contract["nativeSource"].(map[string]any)) + source["canonicalDigest"] = digest.SHA256TextRef("updated native source bytes") + contract["nativeSource"] = source + record[field] = contract + } + }) + if err := verifyCompleteProjectNavigationABIDiff(frozen, current); err != nil { + t.Fatalf("native source digest drift should be normalized: %v", err) + } + }) +} + +func sortedFirstKey(t *testing.T, values map[string]string) string { + t.Helper() + keys := make([]string, 0, len(values)) + for key := range values { + keys = append(keys, key) + } + slices.Sort(keys) + if len(keys) == 0 { + t.Fatal("frozen public ABI inventory is empty") + } + return keys[0] +} + +func mutatePublicABIRecord(t *testing.T, current map[string]any, inventory string, identityField string, identity string, mutate func(map[string]any)) { + t.Helper() + values := current[inventory].([]any) + for index, raw := range values { + record := raw.(map[string]any) + if record[identityField] != identity { + continue + } + mutant := clonePublicABIRecord(record) + mutate(mutant) + values[index] = mutant + return + } + t.Fatalf("%s is missing %s %s", inventory, identityField, identity) +} + +func removePublicABIRecord(t *testing.T, current map[string]any, inventory string, identityField string, identity string) { + t.Helper() + values := current[inventory].([]any) + for index, raw := range values { + if raw.(map[string]any)[identityField] == identity { + current[inventory] = append(append([]any{}, values[:index]...), values[index+1:]...) + return + } + } + t.Fatalf("%s is missing %s %s", inventory, identityField, identity) +} + +func swapFirstPublicABIRecords(t *testing.T, current map[string]any, inventory string) { + t.Helper() + values := current[inventory].([]any) + if len(values) < 2 { + t.Fatalf("%s has fewer than two records", inventory) + } + values[0], values[1] = values[1], values[0] +} + +func appendRenamedPublicABIRecord(t *testing.T, current map[string]any, inventory string, identityField string, sourceIdentity string, newIdentity string) { + t.Helper() + values := current[inventory].([]any) + for _, raw := range values { + record := raw.(map[string]any) + if record[identityField] != sourceIdentity { + continue + } + mutant := clonePublicABIRecord(record) + mutant[identityField] = newIdentity + current[inventory] = append(values, mutant) + return + } + t.Fatalf("%s is missing %s %s", inventory, identityField, sourceIdentity) +} + +func mutateOmittedRoutePolicy(t *testing.T, current map[string]any, replacement any) { + t.Helper() + process := clonePublicABIRecord(current["processContract"].(map[string]any)) + grammar := clonePublicABIRecord(process["commandRouteGrammar"].(map[string]any)) + if replacement == nil { + delete(grammar, "omittedRoutePolicy") + } else { + grammar["omittedRoutePolicy"] = replacement + } + process["commandRouteGrammar"] = grammar + current["processContract"] = process +} diff --git a/internal/app/project_status_command.go b/internal/app/project_status_command.go index aabc932..2549e42 100644 --- a/internal/app/project_status_command.go +++ b/internal/app/project_status_command.go @@ -25,16 +25,24 @@ func runProjectStatus(ctx context.Context, command string, args []string, stdout writeDiagnostic(stderr, err) return 1 } - return projectStatusResult(command, options, status, stdout, stderr, capabilities) + return projectStatusResult(ctx, command, options, status, stdout, stderr, capabilities) } -func projectStatusResult(command string, options projectStatusArgs, status projectstatus.Status, stdout io.Writer, stderr io.Writer, capabilities PresentationCapabilities) int { +func projectStatusResult(ctx context.Context, command string, options projectStatusArgs, status projectstatus.Status, stdout io.Writer, stderr io.Writer, capabilities PresentationCapabilities) int { + if err := projectStatusCancellation(ctx); err != nil { + writeDiagnostic(stderr, err) + return 1 + } if command == "status" { if options.format == "json" { + if err := projectStatusCancellation(ctx); err != nil { + writeDiagnostic(stderr, err) + return 1 + } return writeJSON(status.JSONValue(), 0, nil, stdout, stderr) } lines, err := projectstatus.StatusText(status) - return writeProjectStatusText(lines, options.color, capabilities, stdout, stderr, err) + return writeProjectStatusText(ctx, lines, options.color, capabilities, stdout, stderr, err) } if command != "next" { writeDiagnosticf(stderr, "unsupported project status command") @@ -46,10 +54,14 @@ func projectStatusResult(command string, options projectStatusArgs, status proje return 1 } if options.format == "json" { + if err := projectStatusCancellation(ctx); err != nil { + writeDiagnostic(stderr, err) + return 1 + } return writeJSON(next.JSONValue(), 0, nil, stdout, stderr) } lines, err := projectstatus.NextText(next) - return writeProjectStatusText(lines, options.color, capabilities, stdout, stderr, err) + return writeProjectStatusText(ctx, lines, options.color, capabilities, stdout, stderr, err) } func parseProjectStatusArgs(command string, args []string) (projectStatusArgs, error) { @@ -106,7 +118,7 @@ func missingProjectStatusValue(flag string) error { } } -func writeProjectStatusText(lines []projectstatus.TextLine, color string, capabilities PresentationCapabilities, stdout io.Writer, stderr io.Writer, lineErr error) int { +func writeProjectStatusText(ctx context.Context, lines []projectstatus.TextLine, color string, capabilities PresentationCapabilities, stdout io.Writer, stderr io.Writer, lineErr error) int { if lineErr != nil { return writeText("", 1, lineErr, stdout, stderr) } @@ -119,9 +131,19 @@ func writeProjectStatusText(lines []projectstatus.TextLine, color string, capabi if err == nil && color == "never" && output != plain { err = fmt.Errorf("project status text projection drifted") } + if err == nil { + err = projectStatusCancellation(ctx) + } return writeText(output, 0, err, stdout, stderr) } +func projectStatusCancellation(ctx context.Context) error { + if err := ctx.Err(); err != nil { + return fmt.Errorf("project status cancelled before output: %w", err) + } + return nil +} + func projectStatusTerminalText(lines []projectstatus.TextLine) terminalText { tokens := make([]terminalTextToken, 0, len(lines)*2) for _, line := range lines { diff --git a/internal/app/project_status_command_test.go b/internal/app/project_status_command_test.go index 360c3d4..debbee7 100644 --- a/internal/app/project_status_command_test.go +++ b/internal/app/project_status_command_test.go @@ -63,7 +63,7 @@ func TestProjectStatusOutputMatrix(t *testing.T) { t.Run(string(state)+"/"+command, func(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer - code := projectStatusResult(command, projectStatusArgs{color: "never", format: "json", repositoryRoot: "unused"}, status, &stdout, &stderr, PresentationCapabilities{}) + code := projectStatusResult(context.Background(), command, projectStatusArgs{color: "never", format: "json", repositoryRoot: "unused"}, status, &stdout, &stderr, PresentationCapabilities{}) if code != 0 || stderr.Len() != 0 { t.Fatalf("JSON exit=%d stderr=%q stdout=%q", code, stderr.String(), stdout.String()) } @@ -81,7 +81,7 @@ func TestProjectStatusOutputMatrix(t *testing.T) { } stdout.Reset() - code = projectStatusResult(command, projectStatusArgs{color: "never", format: "text", repositoryRoot: "unused"}, status, &stdout, &stderr, PresentationCapabilities{}) + code = projectStatusResult(context.Background(), command, projectStatusArgs{color: "never", format: "text", repositoryRoot: "unused"}, status, &stdout, &stderr, PresentationCapabilities{}) if code != 0 || stderr.Len() != 0 || stdout.Len() == 0 || strings.Contains(stdout.String(), "\x1b[") || !strings.Contains(stdout.String(), string(state)) { t.Fatalf("text exit=%d stderr=%q stdout=%q", code, stderr.String(), stdout.String()) } @@ -90,9 +90,59 @@ func TestProjectStatusOutputMatrix(t *testing.T) { } } +func TestStatusCLI(t *testing.T) { + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.031193726938621836740724170288022222122010000090102720212406989617137099281165") + repositoryRoot := t.TempDir() + code, output, diagnostic := executeAgentWorkflowCLI(t, []string{"status", "--repo-root", repositoryRoot}, panicReader{}, PresentationCapabilities{}) + if code != 0 || diagnostic != "" { + t.Fatalf("status exit=%d stderr=%q stdout=%q", code, diagnostic, output) + } + status, err := projectstatus.AdmitStatusOutput(decodeCLIJSON(t, output)) + if err != nil || status.ProjectState != projectstatus.StateUninitialized || strings.Contains(output, repositoryRoot) { + t.Fatalf("status admission=%v state=%s output=%q", err, status.ProjectState, output) + } + code, output, diagnostic = executeAgentWorkflowCLI(t, []string{"status", "--repo-root", repositoryRoot, "--format", "text", "--color", "auto"}, panicReader{}, PresentationCapabilities{StdoutIsTTY: true}) + if code != 0 || diagnostic != "" || !strings.Contains(output, string(projectstatus.StateUninitialized)) || !strings.Contains(output, "\x1b[") || strings.Contains(output, repositoryRoot) { + t.Fatalf("status text exit=%d stderr=%q stdout=%q", code, diagnostic, output) + } + missingRoot := filepath.Join(t.TempDir(), "missing") + code, output, diagnostic = executeAgentWorkflowCLI(t, []string{"status", "--repo-root", missingRoot, "--format", "yaml"}, panicReader{}, PresentationCapabilities{}) + if code != 1 || output != "" || !strings.Contains(diagnostic, "--format") || strings.Contains(diagnostic, missingRoot) { + t.Fatalf("status pre-I/O admission exit=%d stderr=%q stdout=%q", code, diagnostic, output) + } +} + +func TestNextCLI(t *testing.T) { + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.039475651267823643074585805569841885668429584666474166773786344007852047031918") + repositoryRoot := t.TempDir() + statusCode, statusOutput, statusDiagnostic := executeAgentWorkflowCLI(t, []string{"status", "--repo-root", repositoryRoot}, panicReader{}, PresentationCapabilities{}) + if statusCode != 0 || statusDiagnostic != "" { + t.Fatalf("status exit=%d stderr=%q stdout=%q", statusCode, statusDiagnostic, statusOutput) + } + status, err := projectstatus.AdmitStatusOutput(decodeCLIJSON(t, statusOutput)) + if err != nil { + t.Fatal(err) + } + code, output, diagnostic := executeAgentWorkflowCLI(t, []string{"next", "--repo-root", repositoryRoot}, panicReader{}, PresentationCapabilities{}) + if code != 0 || diagnostic != "" { + t.Fatalf("next exit=%d stderr=%q stdout=%q", code, diagnostic, output) + } + next, err := projectstatus.AdmitNextOutput(decodeCLIJSON(t, output)) + if err != nil || next.StatusRef != status.StatusID || next.Action.ActionClass != projectstatus.ActionChooseAdoptionMode || strings.Contains(output, repositoryRoot) { + t.Fatalf("next admission=%v packet=%#v output=%q", err, next, output) + } + code, output, diagnostic = executeAgentWorkflowCLI(t, []string{"next", "--repo-root", repositoryRoot, "--format", "text", "--color", "auto"}, panicReader{}, PresentationCapabilities{StdoutIsTTY: true}) + if code != 0 || diagnostic != "" || !strings.Contains(output, string(projectstatus.ActionChooseAdoptionMode)) || !strings.Contains(output, "\x1b[") || strings.Contains(output, repositoryRoot) { + t.Fatalf("next text exit=%d stderr=%q stdout=%q", code, diagnostic, output) + } + missingRoot := filepath.Join(t.TempDir(), "missing") + code, output, diagnostic = executeAgentWorkflowCLI(t, []string{"next", "--repo-root", missingRoot, "--format", "json", "--color", "auto"}, panicReader{}, PresentationCapabilities{}) + if code != 1 || output != "" || !strings.Contains(diagnostic, "--color") || strings.Contains(diagnostic, missingRoot) { + t.Fatalf("next pre-I/O admission exit=%d stderr=%q stdout=%q", code, diagnostic, output) + } +} + func TestProjectStatusCLI(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.082990774938213415032196768034286988848197095097772338307445528941413312751637") - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.016575929375243753473232212796372621717203348455914493779019079597618731316529") repositoryRoot := t.TempDir() t.Run("status and next preserve owner output", func(t *testing.T) { @@ -233,7 +283,7 @@ func TestProjectStatusTransportFailureUsesOneBoundedWriteWithoutAtomicSinkClaim( status := admittedProjectStatusFixture(t, projectstatus.StateUninitialized) stdout := &prefixThenErrorWriter{maximum: 7} var stderr bytes.Buffer - code := projectStatusResult("status", projectStatusArgs{color: "never", format: "json", repositoryRoot: "unused"}, status, stdout, &stderr, PresentationCapabilities{}) + code := projectStatusResult(context.Background(), "status", projectStatusArgs{color: "never", format: "json", repositoryRoot: "unused"}, status, stdout, &stderr, PresentationCapabilities{}) if code != 1 || stdout.calls != 1 || stdout.Len() != stdout.maximum || !strings.Contains(stderr.String(), "write output") { t.Fatalf("transport failure exit=%d calls=%d stdout=%q stderr=%q", code, stdout.calls, stdout.String(), stderr.String()) } @@ -253,6 +303,22 @@ func TestProjectStatusCLIHonorsCanceledContextBeforeOutput(t *testing.T) { } }) } + + status := admittedProjectStatusFixture(t, projectstatus.StateUninitialized) + for _, command := range []string{"next", "status"} { + for _, format := range []string{"json", "text"} { + t.Run(command+"/pre-emission/"+format, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + var stdout bytes.Buffer + var stderr bytes.Buffer + code := projectStatusResult(ctx, command, projectStatusArgs{color: "never", format: format, repositoryRoot: "unused"}, status, &stdout, &stderr, PresentationCapabilities{}) + if code != 1 || stdout.Len() != 0 || !strings.Contains(stderr.String(), "cancel") { + t.Fatalf("%s/%s cancellation exit=%d stdout=%q stderr=%q", command, format, code, stdout.String(), stderr.String()) + } + }) + } + } } type prefixThenErrorWriter struct { diff --git a/internal/app/testdata/compact-current-production-consumers.json b/internal/app/testdata/compact-current-production-consumers.json index b30ec85..179e23b 100644 --- a/internal/app/testdata/compact-current-production-consumers.json +++ b/internal/app/testdata/compact-current-production-consumers.json @@ -53,8 +53,10 @@ "internal/tools/browsertestserver/main.go", "internal/tools/coveragemetrics/main.go", "internal/tools/packageverify/main.go", + "internal/tools/packageverify/workflow_carrier.go", "internal/tools/pythonpackage/main.go", "internal/tools/pythonpackage/verify.go", + "internal/tools/pythonpackage/workflow_carrier.go", "internal/tools/workflowsmoke/process.go", "internal/tools/workflowsmoke/project_navigation_smoke.go", "internal/tools/workflowsmoke/workflow_smoke.go" diff --git a/internal/app/testdata/v0.9-wire-observations.json b/internal/app/testdata/v0.9-wire-observations.json index 745c3dd..ec18025 100644 --- a/internal/app/testdata/v0.9-wire-observations.json +++ b/internal/app/testdata/v0.9-wire-observations.json @@ -9,14 +9,14 @@ "changeRecordRef": "release/change-record.v2.json", "changeRecordSha256": "sha256:6aaa914c454d6f135ece86b632b8618911b71d6b5da08bc2c25657c09228f4f3", "previousPublicAbiSha256": "sha256:b5ea707ee5851cea6b75442e4faf20e93879371faf3636e96a98ccd23b527463", - "currentPublicAbiSha256": "sha256:a331f29499c3dbfd7e03ac4c0d56ce4d71910f735297af79ba0092b543e171f0", + "currentPublicAbiSha256": "sha256:06bc3a98cb26f50e988b2bab88ee5d0aee9ebdab10a8fe116d8933364e7cd2f4", "addedCommandContracts": [ { "command": "next", "route": ["next"], "outputContract": { "contractId": "proofkit.next.output.v1", - "contractSha256": "sha256:9dfa71617f9d727949a988bf726bcf0070460faa59565a8adc468994a36594c6" + "contractSha256": "sha256:92f6d3dc427a795ec97112e6c3ce55ebd4ab670e4323b4faa8215310a8492747" } }, { @@ -24,7 +24,7 @@ "route": ["status"], "outputContract": { "contractId": "proofkit.status.output.v1", - "contractSha256": "sha256:29cf593bf349fd5ec7276f28ce75817a44a8b12fc7aea5b4c055cfaf4188924c" + "contractSha256": "sha256:3ac87fa98c4650fc3b758ab4a43dee1beeb12f9aee3631a35a2ddf5d0fc5f899" } } ], diff --git a/internal/command/projectstatus/dependency_test.go b/internal/command/projectstatus/dependency_test.go index b2a42e8..e9f6c7b 100644 --- a/internal/command/projectstatus/dependency_test.go +++ b/internal/command/projectstatus/dependency_test.go @@ -1,9 +1,11 @@ package projectstatus import ( + "go/ast" "go/parser" "go/token" "os" + "path" "strings" "testing" ) @@ -33,6 +35,66 @@ func TestProjectStatusDelegatesChildAdmissionToMaterializationOwner(t *testing.T } } +func TestProjectStatusProductionTopologyForbidsRepositoryMutationCalls(t *testing.T) { + forbiddenCalls := map[string]map[string]struct{}{ + "os": { + "Chmod": {}, "Chown": {}, "Create": {}, "CreateTemp": {}, "Chtimes": {}, "Lchown": {}, "Link": {}, + "Mkdir": {}, "MkdirAll": {}, "MkdirTemp": {}, "OpenFile": {}, "Remove": {}, "RemoveAll": {}, "Rename": {}, + "Symlink": {}, "Truncate": {}, "WriteFile": {}, + }, + "github.com/research-engineering/agentic-proofkit/internal/kernel/repositorytransaction": { + "Apply": {}, "Recover": {}, + }, + } + for _, entry := range mustProductionGoFiles(t) { + content, err := os.ReadFile(entry) + if err != nil { + t.Fatal(err) + } + parsed, err := parser.ParseFile(token.NewFileSet(), entry, content, parser.SkipObjectResolution) + if err != nil { + t.Fatal(err) + } + aliases := map[string]string{} + for _, imported := range parsed.Imports { + importPath := strings.Trim(imported.Path.Value, "\"") + if _, tracked := forbiddenCalls[importPath]; !tracked { + continue + } + alias := path.Base(importPath) + if imported.Name != nil { + alias = imported.Name.Name + if alias == "." || alias == "_" { + t.Fatalf("%s uses unsupported import alias %q for %s", entry, alias, importPath) + } + } + aliases[alias] = importPath + } + ast.Inspect(parsed, func(node ast.Node) bool { + call, ok := node.(*ast.CallExpr) + if !ok { + return true + } + selector, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + owner, ok := selector.X.(*ast.Ident) + if !ok { + return true + } + importPath, tracked := aliases[owner.Name] + if !tracked { + return true + } + if _, forbidden := forbiddenCalls[importPath][selector.Sel.Name]; forbidden { + t.Errorf("%s calls repository mutation primitive %s.%s", entry, owner.Name, selector.Sel.Name) + } + return true + }) + } +} + func mustProductionGoFiles(t *testing.T) []string { t.Helper() entries, err := os.ReadDir(".") diff --git a/internal/command/projectstatus/inspect.go b/internal/command/projectstatus/inspect.go index 57475d6..39b3b95 100644 --- a/internal/command/projectstatus/inspect.go +++ b/internal/command/projectstatus/inspect.go @@ -107,7 +107,17 @@ func inspectAttempt(ctx context.Context, repositoryRoot string, dependencies ins if err := lease.VerifyRootIdentity(); err != nil { return Status{}, err } - return evaluate(snapshot) + if err := ctx.Err(); err != nil { + return Status{}, fmt.Errorf("project status inspection cancelled before evaluation: %w", err) + } + status, err = evaluate(snapshot) + if err != nil { + return Status{}, err + } + if err := ctx.Err(); err != nil { + return Status{}, fmt.Errorf("project status inspection cancelled before completion: %w", err) + } + return status, nil } func observeTransaction(value repositorytransaction.ControlInspection) (transactionObservation, error) { diff --git a/internal/command/projectstatus/inspect_test.go b/internal/command/projectstatus/inspect_test.go index dc245ed..0cba448 100644 --- a/internal/command/projectstatus/inspect_test.go +++ b/internal/command/projectstatus/inspect_test.go @@ -6,8 +6,10 @@ import ( "encoding/json" "errors" "fmt" + "io/fs" "os" "path/filepath" + "reflect" "strings" "testing" @@ -24,10 +26,12 @@ import ( func TestInspectClassifiesMaterializedProjectWithoutApplicationWrites(t *testing.T) { commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.068153284639677751912209073851318961044240216422390589277786880896123148215480") root := t.TempDir() + before := snapshotProjectTree(t, root) status, err := Inspect(context.Background(), root) if err != nil { t.Fatal(err) } + assertProjectTreeUnchanged(t, root, before) if status.ProjectState != StateUninitialized || status.NextAction.ActionClass != ActionChooseAdoptionMode { t.Fatalf("Inspect() = %#v", status) } @@ -36,10 +40,12 @@ func TestInspectClassifiesMaterializedProjectWithoutApplicationWrites(t *testing } materializeTestProject(t, root) + before = snapshotProjectTree(t, root) status, err = Inspect(context.Background(), root) if err != nil { t.Fatal(err) } + assertProjectTreeUnchanged(t, root, before) if status.ProjectState != StateVerificationRequired || status.ProjectID != "pilot.project" || status.ManifestID == "" { t.Fatalf("Inspect() = %#v", status) } @@ -48,15 +54,66 @@ func TestInspectClassifiesMaterializedProjectWithoutApplicationWrites(t *testing if err := os.WriteFile(sourcePath, []byte("{}\n"), 0o644); err != nil { t.Fatal(err) } + before = snapshotProjectTree(t, root) status, err = Inspect(context.Background(), root) if err != nil { t.Fatal(err) } + assertProjectTreeUnchanged(t, root, before) if status.ProjectState != StateStale || !reflectIssue(status.IssueCodes, IssueChildDigestMismatch) { t.Fatalf("Inspect() after drift = %#v", status) } } +type projectTreeEntry struct { + content []byte + mode fs.FileMode + route string + symlinkRef string +} + +func snapshotProjectTree(t *testing.T, root string) []projectTreeEntry { + t.Helper() + entries := []projectTreeEntry{} + err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + route, err := filepath.Rel(root, path) + if err != nil { + return err + } + info, err := os.Lstat(path) + if err != nil { + return err + } + snapshot := projectTreeEntry{mode: info.Mode(), route: filepath.ToSlash(route)} + switch { + case info.Mode().IsRegular(): + snapshot.content, err = os.ReadFile(path) + case info.Mode()&fs.ModeSymlink != 0: + snapshot.symlinkRef, err = os.Readlink(path) + } + if err != nil { + return err + } + entries = append(entries, snapshot) + return nil + }) + if err != nil { + t.Fatalf("snapshot project tree: %v", err) + } + return entries +} + +func assertProjectTreeUnchanged(t *testing.T, root string, before []projectTreeEntry) { + t.Helper() + after := snapshotProjectTree(t, root) + if !reflect.DeepEqual(after, before) { + t.Fatalf("project tree changed during inspection:\nbefore=%#v\nafter=%#v", before, after) + } +} + func TestInspectRejectsAdmittedChildrenWithInvalidCrossRecordClosure(t *testing.T) { root := t.TempDir() materializeTestProject(t, root) @@ -456,6 +513,25 @@ func TestInspectHonorsCancellationBetweenBoundedReads(t *testing.T) { } } +func TestInspectHonorsCancellationAfterFinalControlObservation(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + controlReads := 0 + dependencies := defaultInspectionDependencies + dependencies.inspectControl = func(context.Context, *repositorytransaction.InspectionLease) (repositorytransaction.ControlInspection, error) { + controlReads++ + if controlReads == 2 { + cancel() + } + return repositorytransaction.ControlInspection{ + EpochID: digest.SHA256TextRef("stable control epoch"), + State: repositorytransaction.ControlStateClean, + }, nil + } + if _, err := inspectWithDependencies(ctx, t.TempDir(), dependencies); !errors.Is(err, context.Canceled) { + t.Fatalf("inspectWithDependencies() error = %v, want context cancellation", err) + } +} + func materializeTestProject(t *testing.T, root string) { t.Helper() if err := os.WriteFile(filepath.Join(root, "README.md"), []byte("# Pilot\n"), 0o644); err != nil { diff --git a/internal/command/stackpreset/preset_ids_generated.go b/internal/command/stackpreset/preset_ids_generated.go index 2c51bb5..6e59c8b 100644 --- a/internal/command/stackpreset/preset_ids_generated.go +++ b/internal/command/stackpreset/preset_ids_generated.go @@ -1,6 +1,6 @@ // Code generated by internal/tools/commandcontractgen; DO NOT EDIT. package stackpreset -const presetContractSourceSHA256 = "514f09bd74c91e6f1378e8e58483d6f5b0ef926243f37da11080f768a4afba31" +const presetContractSourceSHA256 = "133fcfee23ce5e81cc0a6f6b8325ea90e21625c6255a568448de4cf0d03904a0" var presetIDs = []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"} diff --git a/internal/tools/coveragemetrics/main_test.go b/internal/tools/coveragemetrics/main_test.go index f2b7c86..526ef5b 100644 --- a/internal/tools/coveragemetrics/main_test.go +++ b/internal/tools/coveragemetrics/main_test.go @@ -598,9 +598,12 @@ func TestBindingWitnessSelectorsRequireExactCriticalInventories(t *testing.T) { "proofkit.agent-workflow.no-ambient-authority", "proofkit.agent-workflow.prompt-coordinate-and-escalation-closure", "proofkit.agent-workflow.project-navigation-installed-carriers", + "proofkit.agent-workflow.project-navigation-installed-npm-carrier-closure", + "proofkit.agent-workflow.project-navigation-installed-wheel-carrier-closure", "proofkit.agent-workflow.project-navigation-public-cli", "proofkit.agent-workflow.project-navigation-version-edge", "proofkit.agent-workflow.project-next-action-output-closure", + "proofkit.agent-workflow.project-state-application-write-free-topology", "proofkit.agent-workflow.project-state-bounded-inspection", "proofkit.agent-workflow.project-state-child-owner-delegation", "proofkit.agent-workflow.project-state-control-file-coherence", @@ -652,6 +655,9 @@ func TestBindingWitnessSelectorsRequireExactCriticalInventories(t *testing.T) { "proofkit.spec-proof-core.declared-route-mapping-without-assurance", "proofkit.spec-proof-core.requirement-authoring-ref-provenance", "proofkit.spec-proof-core.requirement-browser-one-shot-cleanup", + "proofkit.spec-proof-core.project-navigation-public-abi-diff", + "proofkit.spec-proof-core.project-navigation-public-abi-diff-mutations", + "proofkit.spec-proof-core.project-navigation-version-edge", "proofkit.spec-proof-core.test-inventory-and-coverage-view", } { index := -1 diff --git a/internal/tools/coveragemetrics/required_inventory.go b/internal/tools/coveragemetrics/required_inventory.go index 100f799..dfeed05 100644 --- a/internal/tools/coveragemetrics/required_inventory.go +++ b/internal/tools/coveragemetrics/required_inventory.go @@ -118,6 +118,7 @@ func requiredBindingWitnessInventory() map[inventoryKey]requiredInventoryEntry { "TestInspectCohortValidationClosesCleanEpochABA", "TestInspectDeduplicatesRepeatedIssueCodes", "TestInspectFailsClosedOnSymlinksAndBoundsWithoutDisclosure", + "TestInspectHonorsCancellationAfterFinalControlObservation", "TestInspectHonorsCancellationBetweenBoundedReads", "TestInspectMapsInvalidControlState", "TestInspectMapsRecoverableControlState", @@ -145,6 +146,10 @@ func requiredBindingWitnessInventory() map[inventoryKey]requiredInventoryEntry { "TestInspectionLeaseRejectsControlNamespaceCreatedAfterOpen", }, }, + {"REQ-PROOFKIT-WORKFLOW-013", "proofkit.agent-workflow.project-state-application-write-free-topology"}: { + witnessPath: "internal/command/projectstatus/dependency_test.go", + selectors: []string{"TestProjectStatusProductionTopologyForbidsRepositoryMutationCalls"}, + }, {"REQ-PROOFKIT-WORKFLOW-013", "proofkit.agent-workflow.project-state-exact-route-traversal"}: { witnessPath: "internal/kernel/rootpath/exact_test.go", selectors: []string{"TestOpenExactRegularFileRejectsFinalComponentABA", "TestOpenExactRegularFileRejectsParentSymlinkABA"}, @@ -163,13 +168,23 @@ func requiredBindingWitnessInventory() map[inventoryKey]requiredInventoryEntry { witnessPath: "internal/tools/workflowsmoke/workflow_smoke_test.go", selectors: []string{"TestVerifyAcceptsApplicationCLI", "TestVerifyRejectsCarrierContractMutations"}, }, + {"REQ-PROOFKIT-WORKFLOW-015", "proofkit.agent-workflow.project-navigation-installed-npm-carrier-closure"}: { + witnessPath: "internal/tools/packageverify/workflow_carrier_test.go", + selectors: []string{"TestInstalledNPMWorkflowCarrierClosure"}, + }, + {"REQ-PROOFKIT-WORKFLOW-015", "proofkit.agent-workflow.project-navigation-installed-wheel-carrier-closure"}: { + witnessPath: "internal/tools/pythonpackage/workflow_carrier_test.go", + selectors: []string{"TestInstalledPythonWorkflowCarrierClosure"}, + }, {"REQ-PROOFKIT-WORKFLOW-015", "proofkit.agent-workflow.project-navigation-public-cli"}: { witnessPath: "internal/app/project_status_command_test.go", selectors: []string{ + "TestNextCLI", "TestProjectStatusCLI", "TestProjectStatusCLIHonorsCanceledContextBeforeOutput", "TestProjectStatusOutputMatrix", "TestProjectStatusTransportFailureUsesOneBoundedWriteWithoutAtomicSinkClaim", + "TestStatusCLI", }, }, {"REQ-PROOFKIT-WORKFLOW-015", "proofkit.agent-workflow.project-navigation-version-edge"}: { @@ -790,10 +805,11 @@ func requiredBindingWitnessInventory() map[inventoryKey]requiredInventoryEntry { }, {"REQ-PROOFKIT-SPEC-035", "proofkit.spec-proof-core.project-navigation-public-abi-diff"}: { witnessPath: "internal/app/project_navigation_abi_closure_test.go", - selectors: []string{ - "TestProjectNavigationVersionEdgeClosesCompletePublicABIDiff", - "TestProjectNavigationVersionEdgeRejectsUndeclaredPublicABIDrift", - }, + selectors: []string{"TestProjectNavigationVersionEdgeClosesCompletePublicABIDiff"}, + }, + {"REQ-PROOFKIT-SPEC-035", "proofkit.spec-proof-core.project-navigation-public-abi-diff-mutations"}: { + witnessPath: "internal/app/project_navigation_abi_mutation_test.go", + selectors: []string{"TestProjectNavigationVersionEdgeRejectsUndeclaredPublicABIDrift"}, }, {"REQ-PROOFKIT-RETIRE-006", "proofkit.consumer-infra-retirement.migration-parity-admission"}: { witnessPath: "internal/command/migrationparityadmission/migrationparityadmission_test.go", diff --git a/internal/tools/packageverify/main.go b/internal/tools/packageverify/main.go index a2fd282..0a31a11 100644 --- a/internal/tools/packageverify/main.go +++ b/internal/tools/packageverify/main.go @@ -2643,11 +2643,7 @@ func verifyInstalledJSONABI(consumer string) error { if err := verifyInstalledAgentRouteEnvelopeModes(consumer); err != nil { return err } - if err := workflowsmoke.VerifyProcess(context.Background(), workflowsmoke.ProcessCarrier{ - Directory: consumer, - Executable: "npm", - Prefix: []string{"--silent", "exec", "--offline", "--", "agentic-proofkit"}, - }); err != nil { + if err := verifyInstalledNPMWorkflowSmoke(consumer); err != nil { return fmt.Errorf("outside consumer agent-workflow smoke failed: %w", err) } return nil diff --git a/internal/tools/packageverify/workflow_carrier.go b/internal/tools/packageverify/workflow_carrier.go new file mode 100644 index 0000000..40c14a6 --- /dev/null +++ b/internal/tools/packageverify/workflow_carrier.go @@ -0,0 +1,19 @@ +package main + +import ( + "context" + + "github.com/research-engineering/agentic-proofkit/internal/tools/workflowsmoke" +) + +func verifyInstalledNPMWorkflowSmoke(consumer string) error { + return workflowsmoke.VerifyProcess(context.Background(), installedNPMWorkflowCarrier(consumer)) +} + +func installedNPMWorkflowCarrier(consumer string) workflowsmoke.ProcessCarrier { + return workflowsmoke.ProcessCarrier{ + Directory: consumer, + Executable: "npm", + Prefix: []string{"--silent", "exec", "--offline", "--", "agentic-proofkit"}, + } +} diff --git a/internal/tools/packageverify/workflow_carrier_test.go b/internal/tools/packageverify/workflow_carrier_test.go new file mode 100644 index 0000000..46a7fd9 --- /dev/null +++ b/internal/tools/packageverify/workflow_carrier_test.go @@ -0,0 +1,56 @@ +package main + +import ( + "go/ast" + "go/parser" + "go/token" + "path/filepath" + "reflect" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/tools/workflowsmoke" +) + +func TestInstalledNPMWorkflowCarrierClosure(t *testing.T) { + consumer := filepath.Join("consumer", "root") + want := workflowsmoke.ProcessCarrier{ + Directory: consumer, + Executable: "npm", + Prefix: []string{"--silent", "exec", "--offline", "--", "agentic-proofkit"}, + } + if got := installedNPMWorkflowCarrier(consumer); !reflect.DeepEqual(got, want) { + t.Fatalf("installedNPMWorkflowCarrier()=%#v, want %#v", got, want) + } + assertFunctionCalls(t, "main.go", "verifyInstalledJSONABI", "verifyInstalledNPMWorkflowSmoke") +} + +func assertFunctionCalls(t *testing.T, sourcePath string, functionName string, calleeName string) { + t.Helper() + parsed, err := parser.ParseFile(token.NewFileSet(), sourcePath, nil, parser.SkipObjectResolution) + if err != nil { + t.Fatal(err) + } + for _, declaration := range parsed.Decls { + function, ok := declaration.(*ast.FuncDecl) + if !ok || function.Name.Name != functionName || function.Body == nil { + continue + } + calls := 0 + ast.Inspect(function.Body, func(node ast.Node) bool { + call, ok := node.(*ast.CallExpr) + if !ok { + return true + } + callee, ok := call.Fun.(*ast.Ident) + if ok && callee.Name == calleeName { + calls++ + } + return true + }) + if calls != 1 { + t.Fatalf("%s must call %s exactly once, got %d", functionName, calleeName, calls) + } + return + } + t.Fatalf("function %s is missing from %s", functionName, sourcePath) +} diff --git a/internal/tools/pythonpackage/verify.go b/internal/tools/pythonpackage/verify.go index 55e37df..149618e 100644 --- a/internal/tools/pythonpackage/verify.go +++ b/internal/tools/pythonpackage/verify.go @@ -505,11 +505,8 @@ func verifyInstalledPythonWheel(consumer string, venvPython string, wheelPath st 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") } - if err := verifyInstalledWorkflowSmoke(consumer, environment, venvPython, "-m", "agentic_proofkit"); err != nil { - return fmt.Errorf("python module agent-workflow smoke failed: %w", err) - } - if err := verifyInstalledWorkflowSmoke(consumer, environment, binPath); err != nil { - return fmt.Errorf("python console script agent-workflow smoke failed: %w", err) + if err := verifyInstalledPythonWorkflowSmokes(consumer, environment, venvPython, binPath); err != nil { + return err } if err := verifyInstalledPythonPresetContinuation(consumer, venvPython, expectedContract, environment); err != nil { return err @@ -530,15 +527,6 @@ func installPythonWheel(venvPython string, wheelPath string, environment []strin return nil } -func verifyInstalledWorkflowSmoke(dir string, environment []string, executable string, prefix ...string) error { - return workflowsmoke.VerifyProcess(context.Background(), workflowsmoke.ProcessCarrier{ - Directory: dir, - Executable: executable, - Environment: environment, - Prefix: append([]string(nil), prefix...), - }) -} - func verifyInstalledPythonPresetContinuation(consumer string, venvPython string, expectedContract []byte, baseEnvironment []string) error { emptyPath := filepath.Join(consumer, "empty-path") if err := os.Mkdir(emptyPath, 0o700); err != nil && !os.IsExist(err) { diff --git a/internal/tools/pythonpackage/workflow_carrier.go b/internal/tools/pythonpackage/workflow_carrier.go new file mode 100644 index 0000000..2f4b692 --- /dev/null +++ b/internal/tools/pythonpackage/workflow_carrier.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "fmt" + + "github.com/research-engineering/agentic-proofkit/internal/tools/workflowsmoke" +) + +type installedPythonWorkflowCarrier struct { + label string + carrier workflowsmoke.ProcessCarrier +} + +func installedPythonWorkflowCarriers(dir string, environment []string, venvPython string, binPath string) []installedPythonWorkflowCarrier { + return []installedPythonWorkflowCarrier{ + { + label: "python module", + carrier: workflowsmoke.ProcessCarrier{ + Directory: dir, + Executable: venvPython, + Environment: append([]string(nil), environment...), + Prefix: []string{"-m", "agentic_proofkit"}, + }, + }, + { + label: "python console script", + carrier: workflowsmoke.ProcessCarrier{ + Directory: dir, + Executable: binPath, + Environment: append([]string(nil), environment...), + }, + }, + } +} + +func verifyInstalledPythonWorkflowSmokes(dir string, environment []string, venvPython string, binPath string) error { + for _, candidate := range installedPythonWorkflowCarriers(dir, environment, venvPython, binPath) { + if err := workflowsmoke.VerifyProcess(context.Background(), candidate.carrier); err != nil { + return fmt.Errorf("%s agent-workflow smoke failed: %w", candidate.label, err) + } + } + return nil +} diff --git a/internal/tools/pythonpackage/workflow_carrier_test.go b/internal/tools/pythonpackage/workflow_carrier_test.go new file mode 100644 index 0000000..88174a1 --- /dev/null +++ b/internal/tools/pythonpackage/workflow_carrier_test.go @@ -0,0 +1,71 @@ +package main + +import ( + "go/ast" + "go/parser" + "go/token" + "reflect" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/tools/workflowsmoke" +) + +func TestInstalledPythonWorkflowCarrierClosure(t *testing.T) { + environment := []string{"PATH=/isolated", "PROOFKIT_TEST=1"} + got := installedPythonWorkflowCarriers("consumer", environment, "python", "agentic-proofkit") + environment[0] = "PATH=/mutated" + want := []installedPythonWorkflowCarrier{ + { + label: "python module", + carrier: workflowsmoke.ProcessCarrier{ + Directory: "consumer", + Executable: "python", + Environment: []string{"PATH=/isolated", "PROOFKIT_TEST=1"}, + Prefix: []string{"-m", "agentic_proofkit"}, + }, + }, + { + label: "python console script", + carrier: workflowsmoke.ProcessCarrier{ + Directory: "consumer", + Executable: "agentic-proofkit", + Environment: []string{"PATH=/isolated", "PROOFKIT_TEST=1"}, + }, + }, + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("installedPythonWorkflowCarriers()=%#v, want %#v", got, want) + } + assertPythonFunctionCalls(t, "verify.go", "verifyInstalledPythonWheel", "verifyInstalledPythonWorkflowSmokes") +} + +func assertPythonFunctionCalls(t *testing.T, sourcePath string, functionName string, calleeName string) { + t.Helper() + parsed, err := parser.ParseFile(token.NewFileSet(), sourcePath, nil, parser.SkipObjectResolution) + if err != nil { + t.Fatal(err) + } + for _, declaration := range parsed.Decls { + function, ok := declaration.(*ast.FuncDecl) + if !ok || function.Name.Name != functionName || function.Body == nil { + continue + } + calls := 0 + ast.Inspect(function.Body, func(node ast.Node) bool { + call, ok := node.(*ast.CallExpr) + if !ok { + return true + } + callee, ok := call.Fun.(*ast.Ident) + if ok && callee.Name == calleeName { + calls++ + } + return true + }) + if calls != 1 { + t.Fatalf("%s must call %s exactly once, got %d", functionName, calleeName, calls) + } + return + } + t.Fatalf("function %s is missing from %s", functionName, sourcePath) +} diff --git a/proofkit/cli-contract.v2.json b/proofkit/cli-contract.v2.json index 4b3f7cd..12e73f9 100644 --- a/proofkit/cli-contract.v2.json +++ b/proofkit/cli-contract.v2.json @@ -143,7 +143,7 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:e2b081d812ee0a551e608cf3e59fce28d143a9be6588cea45018709af48628fa", + "canonicalDigest": "sha256:7fa037cfe6147827468954667d103b27796ff43c1d1f74d81be1f545d7534a8e", "evidenceClass": "source_checkout" }, { @@ -281,7 +281,7 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:e2b081d812ee0a551e608cf3e59fce28d143a9be6588cea45018709af48628fa", + "canonicalDigest": "sha256:7fa037cfe6147827468954667d103b27796ff43c1d1f74d81be1f545d7534a8e", "evidenceClass": "source_checkout" }, { @@ -395,7 +395,7 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:e2b081d812ee0a551e608cf3e59fce28d143a9be6588cea45018709af48628fa", + "canonicalDigest": "sha256:7fa037cfe6147827468954667d103b27796ff43c1d1f74d81be1f545d7534a8e", "evidenceClass": "source_checkout" }, { @@ -1186,7 +1186,7 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:e2b081d812ee0a551e608cf3e59fce28d143a9be6588cea45018709af48628fa", + "canonicalDigest": "sha256:7fa037cfe6147827468954667d103b27796ff43c1d1f74d81be1f545d7534a8e", "evidenceClass": "source_checkout" }, { @@ -2787,7 +2787,7 @@ "rootDefinitionDigest": "sha256:b046def1ec1d608e3c76efd84127df7cd8b0a10a920e7ad9b8b850826432d3af", "nativeSource": { "path": "internal/command/projectstatus", - "canonicalDigest": "sha256:0899fabb94eab999a17e1a63924c517a063418e2553f49f09b1eb859bf2bc4ed", + "canonicalDigest": "sha256:050ff57dee13ac2cc7c71ab4f2ed94c8d9a73f945544ecb57c18c07da4ed5c13", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { @@ -3057,7 +3057,7 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:e2b081d812ee0a551e608cf3e59fce28d143a9be6588cea45018709af48628fa", + "canonicalDigest": "sha256:7fa037cfe6147827468954667d103b27796ff43c1d1f74d81be1f545d7534a8e", "evidenceClass": "source_checkout" }, { @@ -6347,7 +6347,7 @@ "rootDefinitionDigest": "sha256:3c842174dff5361e7f83166469b832805e05aa314b073c16234b5b64e346281e", "nativeSource": { "path": "internal/app", - "canonicalDigest": "sha256:e2b081d812ee0a551e608cf3e59fce28d143a9be6588cea45018709af48628fa", + "canonicalDigest": "sha256:7fa037cfe6147827468954667d103b27796ff43c1d1f74d81be1f545d7534a8e", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -6376,7 +6376,7 @@ "rootDefinitionDigest": "sha256:0ea95e277ebe44cd2de42c29b47c38686ac0b6b390d8965367437b3fe138e209", "nativeSource": { "path": "internal/app", - "canonicalDigest": "sha256:e2b081d812ee0a551e608cf3e59fce28d143a9be6588cea45018709af48628fa", + "canonicalDigest": "sha256:7fa037cfe6147827468954667d103b27796ff43c1d1f74d81be1f545d7534a8e", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { @@ -6653,7 +6653,7 @@ "rootDefinitionDigest": "sha256:4d26ca7b7f6be8120fbac40519db02d8e5ae8ba7264208c714fbb9f1cb88ef15", "nativeSource": { "path": "internal/command/projectstatus", - "canonicalDigest": "sha256:0899fabb94eab999a17e1a63924c517a063418e2553f49f09b1eb859bf2bc4ed", + "canonicalDigest": "sha256:050ff57dee13ac2cc7c71ab4f2ed94c8d9a73f945544ecb57c18c07da4ed5c13", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { diff --git a/proofkit/requirement-bindings.json b/proofkit/requirement-bindings.json index 6928479..ebf84eb 100644 --- a/proofkit/requirement-bindings.json +++ b/proofkit/requirement-bindings.json @@ -904,7 +904,8 @@ "proofState": "witness_backed", "nonClaims": [ "A caller-provided stdout writer that accepts a prefix and then fails does not provide an atomic sink, so Proofkit does not claim that such a transport leaves stdout empty.", - "CLI surface closure does not prove registry publication, provider ingestion, consumer migration, native witness truth, rollout, or production readiness." + "CLI surface closure does not prove registry publication, provider ingestion, consumer migration, native witness truth, rollout, or production readiness.", + "Cancellation racing after the final pre-emission checkpoint cannot retract bytes subsequently accepted by an external writer." ] } ], @@ -6272,7 +6273,18 @@ { "selector": "TestProjectNavigationVersionEdgeClosesCompletePublicABIDiff", "command": "go test ./internal/app -run '^TestProjectNavigationVersionEdgeClosesCompletePublicABIDiff$'" - }, + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-SPEC-035", + "scenarioId": "proofkit.spec-proof-core.project-navigation-public-abi-diff-mutations", + "witnessId": "proofkit.project-navigation.public-abi-diff-mutation-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/app/project_navigation_abi_mutation_test.go", + "witnessSelectors": [ { "selector": "TestProjectNavigationVersionEdgeRejectsUndeclaredPublicABIDrift", "command": "go test ./internal/app -run '^TestProjectNavigationVersionEdgeRejectsUndeclaredPublicABIDrift$'" @@ -6397,6 +6409,10 @@ "selector": "TestInspectHonorsCancellationBetweenBoundedReads", "command": "go test ./internal/command/projectstatus -run '^TestInspectHonorsCancellationBetweenBoundedReads$'" }, + { + "selector": "TestInspectHonorsCancellationAfterFinalControlObservation", + "command": "go test ./internal/command/projectstatus -run '^TestInspectHonorsCancellationAfterFinalControlObservation$'" + }, { "selector": "TestOutOfBoundManifestIdentityIsAClassificationNotByteIdentity", "command": "go test ./internal/command/projectstatus -run '^TestOutOfBoundManifestIdentityIsAClassificationNotByteIdentity$'" @@ -6405,6 +6421,21 @@ "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-013", + "scenarioId": "proofkit.agent-workflow.project-state-application-write-free-topology", + "witnessId": "proofkit.project-state.application-write-free-topology-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/command/projectstatus/dependency_test.go", + "witnessSelectors": [ + { + "selector": "TestProjectStatusProductionTopologyForbidsRepositoryMutationCalls", + "command": "go test ./internal/command/projectstatus -run '^TestProjectStatusProductionTopologyForbidsRepositoryMutationCalls$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, { "requirementId": "REQ-PROOFKIT-WORKFLOW-013", "scenarioId": "proofkit.agent-workflow.project-state-control-file-coherence", @@ -6509,6 +6540,10 @@ "witnessKind": "contract", "witnessPath": "internal/app/project_status_command_test.go", "witnessSelectors": [ + { + "selector": "TestNextCLI", + "command": "go test ./internal/app -run '^TestNextCLI$'" + }, { "selector": "TestProjectStatusCLI", "command": "go test ./internal/app -run '^TestProjectStatusCLI$'" @@ -6524,6 +6559,10 @@ { "selector": "TestProjectStatusTransportFailureUsesOneBoundedWriteWithoutAtomicSinkClaim", "command": "go test ./internal/app -run '^TestProjectStatusTransportFailureUsesOneBoundedWriteWithoutAtomicSinkClaim$'" + }, + { + "selector": "TestStatusCLI", + "command": "go test ./internal/app -run '^TestStatusCLI$'" } ], "commandIds": ["proofkit.command-contract-check", "proofkit.go-test"], @@ -6548,6 +6587,36 @@ "commandIds": ["proofkit.go-test", "proofkit.package-artifact"], "environmentClasses": ["local-go", "local-go-python"] }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-015", + "scenarioId": "proofkit.agent-workflow.project-navigation-installed-npm-carrier-closure", + "witnessId": "proofkit.project-navigation.installed-npm-carrier-closure-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/tools/packageverify/workflow_carrier_test.go", + "witnessSelectors": [ + { + "selector": "TestInstalledNPMWorkflowCarrierClosure", + "command": "go test ./internal/tools/packageverify -run '^TestInstalledNPMWorkflowCarrierClosure$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-015", + "scenarioId": "proofkit.agent-workflow.project-navigation-installed-wheel-carrier-closure", + "witnessId": "proofkit.project-navigation.installed-wheel-carrier-closure-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/tools/pythonpackage/workflow_carrier_test.go", + "witnessSelectors": [ + { + "selector": "TestInstalledPythonWorkflowCarrierClosure", + "command": "go test ./internal/tools/pythonpackage -run '^TestInstalledPythonWorkflowCarrierClosure$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, { "requirementId": "REQ-PROOFKIT-WORKFLOW-015", "scenarioId": "proofkit.agent-workflow.project-navigation-version-edge", From 070abde62974d06d40f00a06d4b978eadb4a15d5 Mon Sep 17 00:00:00 2001 From: iperev Date: Sat, 5 Sep 2026 09:25:56 +0200 Subject: [PATCH 5/5] fix: canonicalize project control observations --- internal/app/cli_contract_test.go | 2 +- internal/app/command_contract_generated.go | 8 +-- internal/app/project_status_command_test.go | 25 ++++++-- .../app/testdata/v0.9-wire-observations.json | 2 +- .../command/projectstatus/inspect_test.go | 62 +++++++++++++++++++ .../stackpreset/preset_ids_generated.go | 2 +- .../control_inspection_test.go | 47 +++++++++++--- .../control_observation.go | 8 ++- proofkit/cli-contract.v2.json | 6 +- 9 files changed, 136 insertions(+), 26 deletions(-) diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index 98a82a4..a1be0f4 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -24,7 +24,7 @@ import ( ) const ( - cliContractPublicABISHA256 = "06bc3a98cb26f50e988b2bab88ee5d0aee9ebdab10a8fe116d8933364e7cd2f4" + cliContractPublicABISHA256 = "9a6842b45a218d6caa5da517b0b20f861e13c35a2900e92d34361cdf771781f7" maxAggregateFileReadBytesForContractTest = 64 << 20 maxPackageManifestBytesForContractTest = 256 << 10 maxSourceFileBytesForContractTest = 8 << 20 diff --git a/internal/app/command_contract_generated.go b/internal/app/command_contract_generated.go index 53ee9f4..0b3edfc 100644 --- a/internal/app/command_contract_generated.go +++ b/internal/app/command_contract_generated.go @@ -1,7 +1,7 @@ // Code generated by internal/tools/commandcontractgen; DO NOT EDIT. package app -const commandContractSourceSHA256 = "133fcfee23ce5e81cc0a6f6b8325ea90e21625c6255a568448de4cf0d03904a0" +const commandContractSourceSHA256 = "4a409dd87ab13fa3f3951c16438f1d0f1595cc0ca2f6a1e1317c5c0e2fc9801e" type generatedCommandContractMetadata struct { InputContractSHA256 string @@ -12,9 +12,9 @@ type generatedCommandContractMetadata struct { } var generatedCommandContractMetadataByName = map[string]generatedCommandContractMetadata{ - "adopt-materialize-apply": {InputContractSHA256: "sha256:98539b75bf6d4caebc75d724e9201925a1235290791adcebc865c1b3c74976ee", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.apply-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:57df1df73802292a5dfb3b2ca62d817345acff404477f716b7e02a825b2f4107", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "apply"}}, - "adopt-materialize-plan": {InputContractSHA256: "sha256:65925b56d0332349c5048e9b1450ff0b7a3381224342df1a630128e3f4a2cde2", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.plan-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:09118bc911537bc78339a81bd7774550340b883a9c92b8be87be5c893326efd0", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "plan"}}, - "adopt-materialize-recover": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:0366a6616ee096862719c5cdbba47cc507aef9a250c876c8f35673c0438afcb8", FlagChoices: map[string][]string{"--action": []string{"resume", "rollback"}, "--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "recover"}}, + "adopt-materialize-apply": {InputContractSHA256: "sha256:98539b75bf6d4caebc75d724e9201925a1235290791adcebc865c1b3c74976ee", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.apply-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:8dea77f631337d8b6b4137c27bb3f432ade41f197327743b7b7d8c5296d922d0", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "apply"}}, + "adopt-materialize-plan": {InputContractSHA256: "sha256:65925b56d0332349c5048e9b1450ff0b7a3381224342df1a630128e3f4a2cde2", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.plan-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:b53763c6e129cb62ead5ebf05193ec5316a701bb4277a90fe9fb47f5d8e9d6c3", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "plan"}}, + "adopt-materialize-recover": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:925376cf78507421710b59485da9798d2b79d1bac65da5a3bfc6f4f8aa64fadd", FlagChoices: map[string][]string{"--action": []string{"resume", "rollback"}, "--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "recover"}}, "adopt-plan": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:13e3392d9005c27fed3003a1d036123fe12a341dbe16a97c0cd9f8272fb83320", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}, "--mode": []string{"audit-from-code", "code-baseline", "fresh"}, "--stack": []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"}}, RouteTokens: []string{"adopt", "plan"}}, "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{}, RouteTokens: []string{"adoption-checklist"}}, "adoption-contract-envelope": {InputContractSHA256: "sha256:c310214676ff4b6f536a5bc9d687f681a7e71f73d7a03ac932707d8cd3905cdf", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.adoption-contract-envelope.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3efb2c5161fee16fd8ac6a40dcb6d9c41fbc23e468f60621436ae9e8076e0950", FlagChoices: map[string][]string{}, RouteTokens: []string{"adoption-contract-envelope"}}, diff --git a/internal/app/project_status_command_test.go b/internal/app/project_status_command_test.go index debbee7..3f43e49 100644 --- a/internal/app/project_status_command_test.go +++ b/internal/app/project_status_command_test.go @@ -309,18 +309,33 @@ func TestProjectStatusCLIHonorsCanceledContextBeforeOutput(t *testing.T) { for _, format := range []string{"json", "text"} { t.Run(command+"/pre-emission/"+format, func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) - cancel() - var stdout bytes.Buffer + defer cancel() + lateContext := &cancelAfterFirstCheckContext{Context: ctx, cancel: cancel} + stdout := &prefixThenErrorWriter{maximum: 7} var stderr bytes.Buffer - code := projectStatusResult(ctx, command, projectStatusArgs{color: "never", format: format, repositoryRoot: "unused"}, status, &stdout, &stderr, PresentationCapabilities{}) - if code != 1 || stdout.Len() != 0 || !strings.Contains(stderr.String(), "cancel") { - t.Fatalf("%s/%s cancellation exit=%d stdout=%q stderr=%q", command, format, code, stdout.String(), stderr.String()) + code := projectStatusResult(lateContext, command, projectStatusArgs{color: "never", format: format, repositoryRoot: "unused"}, status, stdout, &stderr, PresentationCapabilities{}) + if code != 1 || stdout.calls != 0 || lateContext.checks < 2 || !strings.Contains(stderr.String(), "cancel") { + t.Fatalf("%s/%s late cancellation exit=%d writes=%d checks=%d stderr=%q", command, format, code, stdout.calls, lateContext.checks, stderr.String()) } }) } } } +type cancelAfterFirstCheckContext struct { + context.Context + cancel context.CancelFunc + checks int +} + +func (ctx *cancelAfterFirstCheckContext) Err() error { + ctx.checks++ + if ctx.checks == 2 { + ctx.cancel() + } + return ctx.Context.Err() +} + type prefixThenErrorWriter struct { bytes.Buffer calls int diff --git a/internal/app/testdata/v0.9-wire-observations.json b/internal/app/testdata/v0.9-wire-observations.json index ec18025..b67c10b 100644 --- a/internal/app/testdata/v0.9-wire-observations.json +++ b/internal/app/testdata/v0.9-wire-observations.json @@ -9,7 +9,7 @@ "changeRecordRef": "release/change-record.v2.json", "changeRecordSha256": "sha256:6aaa914c454d6f135ece86b632b8618911b71d6b5da08bc2c25657c09228f4f3", "previousPublicAbiSha256": "sha256:b5ea707ee5851cea6b75442e4faf20e93879371faf3636e96a98ccd23b527463", - "currentPublicAbiSha256": "sha256:06bc3a98cb26f50e988b2bab88ee5d0aee9ebdab10a8fe116d8933364e7cd2f4", + "currentPublicAbiSha256": "sha256:9a6842b45a218d6caa5da517b0b20f861e13c35a2900e92d34361cdf771781f7", "addedCommandContracts": [ { "command": "next", diff --git a/internal/command/projectstatus/inspect_test.go b/internal/command/projectstatus/inspect_test.go index 0cba448..f57c6d0 100644 --- a/internal/command/projectstatus/inspect_test.go +++ b/internal/command/projectstatus/inspect_test.go @@ -213,6 +213,41 @@ func TestInspectCohortValidationClosesCleanEpochABA(t *testing.T) { if reads != 4 { t.Fatalf("read count = %d, want two complete two-pass attempts", reads) } + for _, changedPath := range []string{adoptionmaterialization.ProjectManifestPath, "docs/specs/pilot/requirements.v1.json"} { + for _, changeDigest := range []bool{false, true} { + t.Run(fmt.Sprintf("%s/digest-change=%t", changedPath, changeDigest), func(t *testing.T) { + root := t.TempDir() + materializeTestProject(t, root) + pathReads := 0 + dependencies := defaultInspectionDependencies + dependencies.inspectControl = func(context.Context, *repositorytransaction.InspectionLease) (repositorytransaction.ControlInspection, error) { + return control, nil + } + dependencies.readFile = func(ctx context.Context, lease *repositorytransaction.InspectionLease, path string, budget *readBudget) (fileObservation, error) { + observation, err := readProjectFile(ctx, lease, path, budget) + if err == nil && path == changedPath { + pathReads++ + if observation.state != fileRead { + t.Fatalf("cohort fixture state=%s, want read", observation.state) + } + if changeDigest && pathReads%2 == 0 { + observation.content = append(observation.content, '\n') + observation.digest = digest.SHA256BytesRef(observation.content) + } + } + return observation, err + } + status, err := inspectWithDependencies(context.Background(), root, dependencies) + if changeDigest { + if err == nil || !strings.Contains(err.Error(), "both bounded inspection attempts") || pathReads != 4 { + t.Fatalf("digest drift error=%v reads=%d, want rejection after two two-pass attempts", err, pathReads) + } + } else if err != nil || status.ProjectState != StateVerificationRequired || pathReads != 2 { + t.Fatalf("stable cohort state=%s error=%v reads=%d", status.ProjectState, err, pathReads) + } + }) + } + } } func TestInspectCleanupFailureDominatesRetryableSnapshotChange(t *testing.T) { @@ -318,6 +353,33 @@ func TestInspectMapsInvalidControlState(t *testing.T) { if status.ProjectState != StateBlocked || status.NextAction.ActionClass != ActionRepairControlState || !reflectIssue(status.IssueCodes, IssueTransactionInvalid) { t.Fatalf("Inspect()=%#v, want invalid transaction classification", status) } + var expectedStatus Status + var expectedNext Next + for index, names := range [][]string{{"a", "Z"}, {"z", "a"}, {"A", "z"}} { + root := t.TempDir() + controlDirectory := filepath.Join(root, filepath.FromSlash(repositorytransaction.ControlDirectory)) + if err := os.MkdirAll(controlDirectory, 0o700); err != nil { + t.Fatal(err) + } + for _, name := range names { + if err := os.WriteFile(filepath.Join(controlDirectory, name), []byte("opaque"), 0o600); err != nil { + t.Fatal(err) + } + } + status, err := Inspect(context.Background(), root) + if err != nil || status.ProjectState != StateBlocked { + t.Fatalf("portable invalid namespace status=%#v error=%v", status, err) + } + next, err := NextFromStatus(status) + if err != nil { + t.Fatal(err) + } + if index == 0 { + expectedStatus, expectedNext = status, next + } else if !reflect.DeepEqual(status, expectedStatus) || !reflect.DeepEqual(next, expectedNext) { + t.Fatal("portable-equivalent control names changed status or next identity") + } + } } func TestInspectAttemptRejectsFinalRepositoryRootReplacement(t *testing.T) { diff --git a/internal/command/stackpreset/preset_ids_generated.go b/internal/command/stackpreset/preset_ids_generated.go index 6e59c8b..7f81698 100644 --- a/internal/command/stackpreset/preset_ids_generated.go +++ b/internal/command/stackpreset/preset_ids_generated.go @@ -1,6 +1,6 @@ // Code generated by internal/tools/commandcontractgen; DO NOT EDIT. package stackpreset -const presetContractSourceSHA256 = "133fcfee23ce5e81cc0a6f6b8325ea90e21625c6255a568448de4cf0d03904a0" +const presetContractSourceSHA256 = "4a409dd87ab13fa3f3951c16438f1d0f1595cc0ca2f6a1e1317c5c0e2fc9801e" var presetIDs = []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"} diff --git a/internal/kernel/repositorytransaction/control_inspection_test.go b/internal/kernel/repositorytransaction/control_inspection_test.go index 4379294..c44a962 100644 --- a/internal/kernel/repositorytransaction/control_inspection_test.go +++ b/internal/kernel/repositorytransaction/control_inspection_test.go @@ -641,12 +641,37 @@ func TestInspectControlStateNormalizesPortableEntryNames(t *testing.T) { {"caf\u00e9", "cafe\u0301"}, {"Custom", "custom"}, } { - left := invalidControlEpochForName(t, pair[0]) - right := invalidControlEpochForName(t, pair[1]) + left := invalidControlEpochForNames(t, pair[0]) + right := invalidControlEpochForNames(t, pair[1]) if left != right { t.Fatalf("portable-equivalent names %q and %q produced different epochs", pair[0], pair[1]) } } + var expectedEpoch string + for _, names := range [][3]string{ + {"a", "Z", "caf\u00e9"}, + {"A", "z", "cafe\u0301"}, + {"a", "z", "caf\u00e9"}, + } { + for _, order := range [][3]int{{0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 0, 1}, {2, 1, 0}} { + permuted := []string{names[order[0]], names[order[1]], names[order[2]]} + entries := []fs.DirEntry{testNamedDirEntry(permuted[0]), testNamedDirEntry(permuted[1]), testNamedDirEntry(permuted[2])} + if err := sortInspectionEntries(entries); err != nil { + t.Fatal(err) + } + gotOrder := []string{entries[0].Name(), entries[1].Name(), entries[2].Name()} + wantOrder := []string{names[0], names[2], names[1]} + if !reflect.DeepEqual(gotOrder, wantOrder) { + t.Fatalf("portable entry order=%q, want %q", gotOrder, wantOrder) + } + epoch := invalidControlEpochForNames(t, permuted...) + if expectedEpoch == "" { + expectedEpoch = epoch + } else if epoch != expectedEpoch { + t.Fatalf("portable-equivalent entry set %q changed epoch", permuted) + } + } + } } func TestInspectControlStateRejectsClassificationChangeHiddenByPortableNameIdentity(t *testing.T) { @@ -679,21 +704,27 @@ func TestInspectControlStateRejectsClassificationChangeHiddenByPortableNameIdent } func TestControlObservationRejectsPortableEntryAliasCollision(t *testing.T) { - entries := []fs.DirEntry{testNamedDirEntry("caf\u00e9"), testNamedDirEntry("cafe\u0301")} - if err := sortInspectionEntries(entries); !errors.Is(err, errControlObservationShape) { - t.Fatalf("sortInspectionEntries() error=%v, want unsupported shape", err) + for _, pair := range [][2]string{{"caf\u00e9", "cafe\u0301"}, {"Z", "z"}, {"same", "same"}} { + for _, order := range [][2]int{{0, 1}, {1, 0}} { + entries := []fs.DirEntry{testNamedDirEntry(pair[order[0]]), testNamedDirEntry(pair[order[1]])} + if err := sortInspectionEntries(entries); !errors.Is(err, errControlObservationShape) { + t.Fatalf("sortInspectionEntries() error=%v, want unsupported shape", err) + } + } } } -func invalidControlEpochForName(t *testing.T, name string) string { +func invalidControlEpochForNames(t *testing.T, names ...string) string { t.Helper() rootPath := t.TempDir() controlPath := filepath.Join(rootPath, filepath.FromSlash(ControlDirectory)) if err := os.MkdirAll(controlPath, 0o700); err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(controlPath, name), []byte("portable\n"), 0o600); err != nil { - t.Fatal(err) + for _, name := range names { + if err := os.WriteFile(filepath.Join(controlPath, name), []byte("portable\n"), 0o600); err != nil { + t.Fatal(err) + } } inspection, err := InspectControlState(context.Background(), rootPath) if err != nil || inspection.State != ControlStateInvalid { diff --git a/internal/kernel/repositorytransaction/control_observation.go b/internal/kernel/repositorytransaction/control_observation.go index 46c3265..7e455dc 100644 --- a/internal/kernel/repositorytransaction/control_observation.go +++ b/internal/kernel/repositorytransaction/control_observation.go @@ -165,12 +165,14 @@ func sortInspectionEntries(entries []fs.DirEntry) error { if err != nil { return errControlObservationShape } - if _, exists := keys[key]; exists { + keys[entry.Name()] = key + } + sort.Slice(entries, func(left, right int) bool { return keys[entries[left].Name()] < keys[entries[right].Name()] }) + for index := 1; index < len(entries); index++ { + if keys[entries[index-1].Name()] == keys[entries[index].Name()] { return errControlObservationShape } - keys[key] = entry.Name() } - sort.Slice(entries, func(left, right int) bool { return keys[entries[left].Name()] < keys[entries[right].Name()] }) return nil } diff --git a/proofkit/cli-contract.v2.json b/proofkit/cli-contract.v2.json index 12e73f9..df37fab 100644 --- a/proofkit/cli-contract.v2.json +++ b/proofkit/cli-contract.v2.json @@ -153,7 +153,7 @@ }, { "path": "internal/kernel/repositorytransaction", - "canonicalDigest": "sha256:9847e806c891410d428477672b06d32db289bfa89ba2b83868cba08aa76422a0", + "canonicalDigest": "sha256:fd549a1f5795524d4f7feed3a4ebcef03e3d6fe53d6ce7e8e24ff5014de9a9cd", "evidenceClass": "source_checkout" } ], @@ -291,7 +291,7 @@ }, { "path": "internal/kernel/repositorytransaction", - "canonicalDigest": "sha256:9847e806c891410d428477672b06d32db289bfa89ba2b83868cba08aa76422a0", + "canonicalDigest": "sha256:fd549a1f5795524d4f7feed3a4ebcef03e3d6fe53d6ce7e8e24ff5014de9a9cd", "evidenceClass": "source_checkout" } ], @@ -405,7 +405,7 @@ }, { "path": "internal/kernel/repositorytransaction", - "canonicalDigest": "sha256:9847e806c891410d428477672b06d32db289bfa89ba2b83868cba08aa76422a0", + "canonicalDigest": "sha256:fd549a1f5795524d4f7feed3a4ebcef03e3d6fe53d6ce7e8e24ff5014de9a9cd", "evidenceClass": "source_checkout" } ],