From 328ff3ebb4e389c2a66577f9604294d0df9daaae Mon Sep 17 00:00:00 2001 From: iperev Date: Mon, 31 Aug 2026 13:10:45 +0200 Subject: [PATCH 01/11] feat: add agent workflow planning primitives --- cmd/agentic-proofkit/main.go | 8 +- docs/proofkit-contract-map.md | 2 + .../specs/proofkit-agent-workflow/overview.md | 68 + .../requirements.v1.json | 157 + .../proofkit-spec-proof-core/overview.md | 10 +- .../requirements.v1.json | 4 +- .../proofkit-supply-chain-quality/overview.md | 7 + .../requirements.v1.json | 26 + go.mod | 2 +- internal/app/agent_workflow_args.go | 100 + internal/app/agent_workflow_command.go | 65 + internal/app/agent_workflow_command_test.go | 340 + .../app/agent_workflow_version_edge_test.go | 136 + internal/app/app.go | 10 + internal/app/app_test.go | 6 +- internal/app/cli_abi_test.go | 6 +- internal/app/cli_contract_test.go | 21 +- internal/app/command_contract_generated.go | 24 +- internal/app/command_coverage_routes.go | 62 +- internal/app/command_coverage_test.go | 4 +- internal/app/command_descriptors.go | 12 +- .../app/command_family_catalog_generated.go | 3 +- internal/app/command_help.go | 4 + internal/app/compact_contract_cli_test.go | 41 + .../compact_contract_release_closure_test.go | 79 +- ...compact_contract_wire_observations_test.go | 424 +- .../app/native_evidence_guidance_command.go | 38 + internal/app/requirement_context_cli_test.go | 8 +- internal/app/terminal_style.go | 79 + internal/app/terminal_style_test.go | 59 + .../compact-v2-release-change-record.json | 51 + .../compact-v2-wire-observations.json | 5867 +++++++++++++++++ .../app/testdata/v0.5-wire-observations.json | 28 + .../adoptionchecklist_test.go | 2 +- .../adoptioncontract/adoptioncontract_test.go | 2 +- .../adoptiondoctor/adoptiondoctor_test.go | 2 +- internal/command/adoptiondoctor/envelope.go | 11 +- .../adoptionworkflow/adoptionworkflow_test.go | 2 +- .../command/agentroute/agentroute_test.go | 60 +- internal/command/agentroute/envelope.go | 10 +- .../bindingpartition/bindingpartition_test.go | 2 +- .../branchauthority/branchauthority_test.go | 2 +- .../capability_map_admission_test.go | 2 +- .../changedpathset/changedpathset_test.go | 2 +- .../command/changeworkflowplan/admission.go | 341 + .../changeworkflowplan/admission_test.go | 47 + .../changeworkflowplan/agent_envelope.go | 148 + .../change_workflow_plan_test.go | 76 + .../changeworkflowplan/context_closure.go | 90 + .../context_closure_test.go | 222 + .../command/changeworkflowplan/decision.go | 104 + .../changeworkflowplan/dependency_test.go | 96 + internal/command/changeworkflowplan/model.go | 116 + internal/command/changeworkflowplan/prompt.go | 113 + .../command/changeworkflowplan/prompt_test.go | 145 + internal/command/changeworkflowplan/report.go | 140 + .../command/changeworkflowplan/state_test.go | 146 + .../changeworkflowplan/test_helpers_test.go | 140 + internal/command/changeworkflowplan/text.go | 71 + .../command/changeworkflowplan/text_test.go | 66 + .../completioncriteria_test.go | 2 +- .../conformanceprofile_test.go | 6 +- .../customruleboundary_test.go | 2 +- .../deployment_evidence_admission_test.go | 2 +- .../documentlifecycle_test.go | 2 +- .../externalconsumer/externalconsumer_test.go | 2 +- .../gradualadoption/gradualadoption_test.go | 4 +- .../command/gradualadoption/guidance_test.go | 2 +- internal/command/impact/impact_test.go | 2 +- .../json_report_cli_adapter_source.go | 211 +- .../json_report_cli_adapter_source_test.go | 137 +- .../migrationparityadmission_test.go | 2 +- .../migrationplan/migrationplan_test.go | 2 +- .../nativeevidenceguidance/dependency_test.go | 58 + .../nativeevidenceguidance/guidance.go | 163 + .../nativeevidenceguidance/guidance_test.go | 157 + .../obligationdecision_test.go | 2 +- .../package_runtime_dependency_test.go | 2 +- .../pilotadmission/pilotadmission_test.go | 2 +- .../producerpolicyselfproof_test.go | 2 +- .../projectstructure/projectstructure_test.go | 4 +- .../proofbindingtestinventory_test.go | 2 +- .../proof_obligation_algebra_test.go | 2 +- .../proofreceiptadmission_test.go | 2 +- internal/command/publicapi/public_api.go | 7 +- internal/command/publicapi/public_api_test.go | 4 +- .../readinesscloseout_test.go | 2 +- .../receipt_currentness_scope_test.go | 2 +- .../receiptproduceradmission_test.go | 2 +- .../receipt_trust_class_test.go | 2 +- .../registryconsumer/registryconsumer_test.go | 4 +- .../registry_consumer_input_compose_test.go | 4 +- .../releaseauthority/releaseauthority_test.go | 2 +- .../rendered_artifact_freshness_test.go | 2 +- .../repo_profile_admission_test.go | 2 +- .../requirement_authoring_plan_test.go | 2 +- .../compact_contract_test.go | 4 +- .../requirementbinding/projections_test.go | 6 +- .../command/requirementbrowser/server_test.go | 2 +- .../command/requirementcontext/compose.go | 2 +- .../requirementcontext_test.go | 4 +- .../requirementcoverageinput_test.go | 6 +- .../requirementcoverageview_test.go | 2 +- .../requirementdiff/requirementdiff_test.go | 2 +- .../requirementgraph/requirementgraph_test.go | 4 +- .../requirementimpactinput_test.go | 2 +- .../requirementproofsourceset_test.go | 2 +- .../requirementproofview_test.go | 2 +- .../requirementsourceadmission_test.go | 2 +- .../requirementsourcetransition_test.go | 2 +- .../requirementsourceview_test.go | 2 +- .../requirementspectree_test.go | 4 +- .../scaffoldprofileplan_test.go | 2 +- .../command/secretscan/secretscan_test.go | 2 +- .../selectivegateevidence_test.go | 6 +- .../selectivegateplan_test.go | 2 +- .../specoverviewclaims_test.go | 4 +- .../specproofbundleadmission_test.go | 2 +- .../stackpreset/preset_ids_generated.go | 2 +- .../command/stackpreset/stackpreset_test.go | 4 +- .../testevidenceinventory_test.go | 4 +- internal/command/textpolicy/textpolicy.go | 9 +- .../command/textpolicy/textpolicy_test.go | 2 +- .../command/witnessplan/witnessplan_test.go | 4 +- .../witnessschedulerplan_test.go | 2 +- .../workspace_manifest_facts_test.go | 4 +- .../workspaceplanning_test.go | 4 +- .../workspaceregistry_test.go | 2 +- internal/kernel/admit/fields.go | 38 +- internal/kernel/admit/fields_test.go | 73 +- .../kernel/agentenvelope/agentenvelope.go | 29 +- .../agentenvelope/agentenvelope_test.go | 14 +- .../agentenvelope/reference_identity.go | 260 + .../agentenvelope/reference_identity_test.go | 222 + internal/kernel/digest/digest.go | 6 +- internal/kernel/jsonpointer/jsonpointer.go | 40 +- .../kernel/jsonpointer/jsonpointer_test.go | 25 + internal/kernel/stablejson/stablejson.go | 66 +- internal/kernel/stablejson/stablejson_test.go | 120 +- .../kernel/unicodepolicy/unicodepolicy.go | 80 + .../unicodepolicy/unicodepolicy_test.go | 92 + .../testsupport/nodetestselector/selector.go | 86 + .../nodetestselector/selector_test.go | 35 + internal/tools/browserproofverify/main.go | 14 +- .../tools/browserproofverify/main_test.go | 16 + internal/tools/commandcontractgen/main.go | 7 +- .../tools/commandcontractgen/main_test.go | 9 +- internal/tools/commandoracle/corpus.go | 3 +- internal/tools/commandoracle/events.go | 3 +- internal/tools/coveragemetrics/main.go | 4 +- internal/tools/mermaidcheck/main.go | 8 +- internal/tools/npmregistry/main.go | 7 +- internal/tools/packageverify/main.go | 52 +- internal/tools/packageverify/main_test.go | 19 +- internal/tools/pypiregistry/main.go | 6 +- internal/tools/pythonpackage/verify.go | 38 +- internal/tools/releasechange/record_test.go | 23 +- internal/tools/releasecloseoutinput/main.go | 5 +- internal/tools/releasemanifest/main.go | 41 +- internal/tools/releasepreflight/main.go | 13 +- internal/tools/releasesbom/main.go | 2 +- internal/tools/textpolicyinput/main.go | 14 +- .../tools/workflowsmoke/workflow_smoke.go | 220 + .../workflowsmoke/workflow_smoke_test.go | 68 + package-lock.json | 4 +- package.json | 2 +- proofkit/cli-contract.v2.json | 474 +- proofkit/command-families.v1.json | 9 + proofkit/requirement-bindings.json | 548 ++ proofkit/witness-plan.json | 1 + release/change-record.v2.json | 36 +- scripts/browser-proof-execution.mjs | 88 +- scripts/browser-proof-execution.test.mjs | 12 + scripts/browser-proof-inputs.mjs | 9 +- scripts/source-hygiene.mjs | 15 +- scripts/source_hygiene_test.go | 41 + scripts/stable-json.mjs | 164 +- scripts/stable-json.test.mjs | 142 +- scripts/stable_json_selector_test.go | 47 + scripts/validate-self-hosting-receipts.go | 12 +- .../validate-self-hosting-receipts_test.go | 13 +- scripts/write-browser-proof.mjs | 9 +- 182 files changed, 13269 insertions(+), 934 deletions(-) create mode 100644 docs/specs/proofkit-agent-workflow/overview.md create mode 100644 docs/specs/proofkit-agent-workflow/requirements.v1.json create mode 100644 internal/app/agent_workflow_args.go create mode 100644 internal/app/agent_workflow_command.go create mode 100644 internal/app/agent_workflow_command_test.go create mode 100644 internal/app/agent_workflow_version_edge_test.go create mode 100644 internal/app/native_evidence_guidance_command.go create mode 100644 internal/app/terminal_style.go create mode 100644 internal/app/terminal_style_test.go create mode 100644 internal/app/testdata/compact-v2-release-change-record.json create mode 100644 internal/app/testdata/compact-v2-wire-observations.json create mode 100644 internal/app/testdata/v0.5-wire-observations.json create mode 100644 internal/command/changeworkflowplan/admission.go create mode 100644 internal/command/changeworkflowplan/admission_test.go create mode 100644 internal/command/changeworkflowplan/agent_envelope.go create mode 100644 internal/command/changeworkflowplan/change_workflow_plan_test.go create mode 100644 internal/command/changeworkflowplan/context_closure.go create mode 100644 internal/command/changeworkflowplan/context_closure_test.go create mode 100644 internal/command/changeworkflowplan/decision.go create mode 100644 internal/command/changeworkflowplan/dependency_test.go create mode 100644 internal/command/changeworkflowplan/model.go create mode 100644 internal/command/changeworkflowplan/prompt.go create mode 100644 internal/command/changeworkflowplan/prompt_test.go create mode 100644 internal/command/changeworkflowplan/report.go create mode 100644 internal/command/changeworkflowplan/state_test.go create mode 100644 internal/command/changeworkflowplan/test_helpers_test.go create mode 100644 internal/command/changeworkflowplan/text.go create mode 100644 internal/command/changeworkflowplan/text_test.go create mode 100644 internal/command/nativeevidenceguidance/dependency_test.go create mode 100644 internal/command/nativeevidenceguidance/guidance.go create mode 100644 internal/command/nativeevidenceguidance/guidance_test.go create mode 100644 internal/kernel/agentenvelope/reference_identity.go create mode 100644 internal/kernel/agentenvelope/reference_identity_test.go create mode 100644 internal/kernel/unicodepolicy/unicodepolicy.go create mode 100644 internal/kernel/unicodepolicy/unicodepolicy_test.go create mode 100644 internal/testsupport/nodetestselector/selector.go create mode 100644 internal/testsupport/nodetestselector/selector_test.go create mode 100644 internal/tools/workflowsmoke/workflow_smoke.go create mode 100644 internal/tools/workflowsmoke/workflow_smoke_test.go create mode 100644 scripts/stable_json_selector_test.go diff --git a/cmd/agentic-proofkit/main.go b/cmd/agentic-proofkit/main.go index d265fde..164c76d 100644 --- a/cmd/agentic-proofkit/main.go +++ b/cmd/agentic-proofkit/main.go @@ -5,6 +5,7 @@ import ( "fmt" "os" + "github.com/mattn/go-isatty" "github.com/research-engineering/agentic-proofkit/internal/app" "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" ) @@ -18,5 +19,10 @@ func main() { _, _ = fmt.Fprintln(os.Stderr, err) os.Exit(1) } - os.Exit(app.RunWithRenderer(context.Background(), os.Args[1:], os.Stdin, os.Stdout, os.Stderr, renderer)) + _, noColorPresent := os.LookupEnv("NO_COLOR") + capabilities := app.PresentationCapabilities{ + StdoutIsTTY: isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()), + NoColorPresent: noColorPresent, + } + os.Exit(app.RunWithRendererAndCapabilities(context.Background(), os.Args[1:], os.Stdin, os.Stdout, os.Stderr, renderer, capabilities)) } diff --git a/docs/proofkit-contract-map.md b/docs/proofkit-contract-map.md index 8421c0e..25f96cc 100644 --- a/docs/proofkit-contract-map.md +++ b/docs/proofkit-contract-map.md @@ -39,6 +39,7 @@ 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 | total checkpoint relation, reference-closed next-stage context, deterministic agent prompts, bounded text/JSON/envelope projections, and repository-neutral native-evidence guidance | 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 | `init`, `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` | adoption intent, aggregate adoption contract envelope, checklist facts, target paths, owner routes, caller-extracted stale authority vocabulary facts, explicit pre-spec capability observations, pilot records, stack preset id, optional init preset id | dry-run route selection, 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, pilot shape admission | final files, final requirements, rollout policy, text extraction from files, code observation extraction, pilot truth | selected child output, plan, 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 | @@ -122,6 +123,7 @@ Semantic context routes are `requirement-context-compose`, | State or goal | Next Proofkit route | Stop or escalation condition | |---|---|---| | The agent does not know where to start. | `init`, or `init --preset` with `fresh`, `code-baseline`, `code-audit`, `legacy`, or `change-set` | Treat output as dry-run route guidance only. Stop before scanning, writing files, or making requirements authoritative. | +| An agent needs a bounded, deterministic stage transition for an engineering change. | `change-workflow-plan`; 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. Stop before treating the plan or guidance 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. | `scaffold-project-structure`, `adoption-workflow-plan`, or `stack-preset` | Stop before writing files; the consumer owns materialization, overwrite policy, and final requirement text. | | Candidate boundary is uncertain. | `adoption-doctor` or `gradual-adoption-guidance --agent-envelope` | Escalate to owner review when the boundary is advisory, ambiguous, or missing native witnesses. | diff --git a/docs/specs/proofkit-agent-workflow/overview.md b/docs/specs/proofkit-agent-workflow/overview.md new file mode 100644 index 0000000..a415e06 --- /dev/null +++ b/docs/specs/proofkit-agent-workflow/overview.md @@ -0,0 +1,68 @@ +# Proofkit Agent Workflow Spec + +This spec owns Proofkit's generic support for planning one engineering-change +snapshot and for teaching a consuming repository to define executable native +evidence. It does not own consumer product meaning, repository policy, +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 + the ordered stages `architecture`, `design`, `implementation_plan`, + `implementation`, `verification`, `pull_request`, and `closeout`. +2. `native-evidence-guidance` exposes one versioned 22-slot template for + repository-owned falsifiers, oracles, bounds, lifecycle, and non-claims. +3. Existing descriptors, dispatch, command families, root-shape CLI contracts, + agent envelopes, and package gates provide public-surface closure. + +Neither command scans a repository. Both command cores are stateless pure +projections with no filesystem, Git, process, environment, clock, random, +network, container, or provider dependency. No setup facade, agent-route extension, +external prompt resource, persisted experiment state, or second source codec is +introduced. + +## Requirements + +- `REQ-PROOFKIT-WORKFLOW-001`: one-time typed admission, pure deterministic + projection, and derived-output authority boundary. +- `REQ-PROOFKIT-WORKFLOW-002`: exact seven-stage order, prefix/checkpoint + biconditional, and a disjoint terminal output. +- `REQ-PROOFKIT-WORKFLOW-003`: 28 active state rows, one terminal row, and an + exact successor-state delta that preserves governing authority and context. +- `REQ-PROOFKIT-WORKFLOW-004`: typed subject/finding resolution and internally + consistent caller-declared digests. +- `REQ-PROOFKIT-WORKFLOW-005`: shared 256-byte stable-ID admission before work, + global candidate-dependency resolution, one explicit nullable + governing-authority reference, bounded least dependency closure over + explicit seeds, exact omission accounting, and output-byte limits. +- `REQ-PROOFKIT-WORKFLOW-006`: only existing launcher and presentation + capabilities outside explicit input, with no ambient repository authority, + setup facade, hidden policy, or agent-route extension. +- `REQ-PROOFKIT-WORKFLOW-007`: pure deterministic repository-neutral + native-evidence guidance from one versioned typed table. +- `REQ-PROOFKIT-WORKFLOW-008`: display-safe caller admission, no-leak denial, + bounded actionable text with required coordinates, and no styling of caller + values. +- `REQ-PROOFKIT-WORKFLOW-009`: explicit text/color selection and confinement of + ANSI to eligible terminal stdout. +- `REQ-PROOFKIT-WORKFLOW-010`: one owner per semantic relation and the + factor-proven dedicated two-command carrier topology, with no runtime + experiment or external template residue. +- `REQ-PROOFKIT-WORKFLOW-011`: a finite factorized CLI relation closed across + descriptor/dispatcher/family/help/root-contract/witness/generated/package + surfaces, with npm-only non-runtime specification docs and cross-channel + runtime behavior proof. + +Shared stable-JSON/diagnostic hardening is owned by the supply-chain-quality +spec. Typed local-reference closure is owned by the existing agent-envelope +requirement. This workflow spec consumes those owners without duplicating them. + +## Non-Claims + +- Generated prompts are not authenticated and need not be obeyed or adequate. +- Static prompt bytes do not prove provider token use or model reasoning. +- Least dependency closure does not prove context truth, freshness, semantic + sufficiency, or global minimality. +- A finite pilot does not prove universal repository fit. +- This spec does not select a persisted requirement-source codec. +- This spec does not approve merge, release, rollout, or production readiness. diff --git a/docs/specs/proofkit-agent-workflow/requirements.v1.json b/docs/specs/proofkit-agent-workflow/requirements.v1.json new file mode 100644 index 0000000..48d9b6b --- /dev/null +++ b/docs/specs/proofkit-agent-workflow/requirements.v1.json @@ -0,0 +1,157 @@ +{ + "schemaVersion": 1, + "sourceId": "proofkit.agent-workflow.requirements", + "specPackagePath": "docs/specs/proofkit-agent-workflow", + "overviewPath": "docs/specs/proofkit-agent-workflow/overview.md", + "requirementsPath": "docs/specs/proofkit-agent-workflow/requirements.v1.json", + "requirements": [ + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-001", + "ownerId": "proofkit.agent-workflow", + "invariant": "Change-workflow planning admits one explicit caller snapshot into an immutable typed projection, decides only from that projection, and deterministically returns one derived non-authoritative plan without executing or supervising agents, witnesses, repository operations, providers, CI, merge, release, rollout, or deployment; no prompt, report, envelope, view, test, or generated record becomes a second semantic owner.", + "claimLevel": "blocking", + "riskClass": "high", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-WORKFLOW-001"], + "nonClaims": ["The planner does not authenticate caller assertions or prove that a planned action was executed, correct, reviewed, or completed."], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.agent-workflow", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-002", + "ownerId": "proofkit.agent-workflow", + "invariant": "The workflow owns exactly the ordered stages architecture, design, implementation_plan, implementation, verification, pull_request, and closeout; completedStageIds is exactly one prefix of that order, every incomplete prefix has a non-null checkpoint and an active stage equal to the first stage outside the prefix, and the complete prefix has a null checkpoint if and only if the output is the disjoint workflow_complete variant with no active-stage, action, checkpoint-state, or prompt field.", + "claimLevel": "blocking", + "riskClass": "high", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-WORKFLOW-002"], + "nonClaims": ["Stage ordering does not prove the semantic adequacy of any architecture, design, plan, implementation, verification, pull request, or closeout artifact."], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.agent-workflow", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-003", + "ownerId": "proofkit.agent-workflow", + "invariant": "The active checkpoint is exactly one closed discriminated variant not_started, ready_for_review, review_findings, or review_passed; one independently enumerable total non-overlapping relation maps the 28 active stage/checkpoint rows to exactly one stage-specific first action, review, repair, or accept_stage, the twenty-ninth complete-prefix row maps only to workflow_complete, every accept_stage packet declares exactly one successorStateDelta containing only completedStageIds and checkpoint, applying that delta by replacing only those two input fields while preserving schemaVersion, governingAuthorityRefId, contextRefs, and requiredContextRefIds produces exactly one legal successor snapshot, and every surplus, missing, or otherwise illegal field combination fails closed.", + "claimLevel": "blocking", + "riskClass": "high", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-WORKFLOW-003"], + "nonClaims": ["The snapshot relation does not own pending work, elapsed time, predecessor history, waiver, abort, reopen, or external task-state transitions."], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.agent-workflow", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-004", + "ownerId": "proofkit.agent-workflow", + "invariant": "Every ready_for_review, review_findings, and review_passed checkpoint binds the caller-declared stage artifact through one subjectRefId that resolves exactly one artifact context reference and one equal lowercase SHA-256 subject digest; assessed variants require an equal assessmentSubjectDigest, review_findings resolves each findingRef exactly once to a finding context reference, and a malformed, absent, surplus, unresolved, wrong-kind, or mismatched identity fails before an action is projected.", + "claimLevel": "blocking", + "riskClass": "high", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-WORKFLOW-004"], + "nonClaims": ["Digest equality proves only internal equality of caller-declared identity fields and does not bind actual bytes, authenticate the reviewer, establish currentness or trust, or prove assessment quality."], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.agent-workflow", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-005", + "ownerId": "proofkit.agent-workflow", + "invariant": "The planner admits 0 to 48 strictly refId-sorted unique candidate context references; every candidate ID, nullable governingAuthorityRefId, required seed ID, dependency ID, subject ID, and finding ID is first admitted by the shared stable RuleID owner with a 256 UTF-8 byte maximum before sorting, indexing, closure, or projection; every dependency of every candidate resolves exactly once before seed selection or closure, including dependencies of candidates that the final closure omits; the governingAuthorityRefId when present resolves exactly one authority reference; it admits 0 to 12 required seed IDs, at most 12 finding references, at most 8 dependencies per candidate, at most 384 UTF-8 bytes per artifact path, and at most 18432 aggregate candidate-path bytes; it returns the unique least dependency-closed subset containing the governing authority, required, subject, and finding seeds, fails closed above 24 retained references or 9216 retained-path bytes, derives the exact sorted candidate-minus-retained omission set, summarizes that set once in an envelope, and fails JSON, envelope, and text outputs above 128 KiB, 128 KiB, and 8192 bytes before partial output.", + "claimLevel": "blocking", + "riskClass": "high", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-WORKFLOW-005"], + "nonClaims": ["Structural least closure relative to caller-declared seeds and edges does not prove context truth, freshness, semantic sufficiency, global minimality, or provider token usage."], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.agent-workflow", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-006", + "ownerId": "proofkit.agent-workflow", + "invariant": "After existing bounded CLI transport, immutable installed-launcher profile admission, and immutable presentation-capability capture, the semantic planner accepts no repository root, caller prompt, caller-selected bound, command, environment, receipt body, or external authority field and has no filesystem, Git, process, environment, clock, random, network, container, or provider dependency; the CLI reads only the explicit input transport, the existing launcher-profile and optional Python-executable transport capabilities, and startup TTY/NO_COLOR presence needed by requested presentation, and the capability adds no setup facade, ambient scan, hidden repository policy, consumer-specific route, or agent-route extension.", + "claimLevel": "blocking", + "riskClass": "high", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-WORKFLOW-006"], + "nonClaims": ["Explicit caller input does not establish repository identity, policy, source meaning, authorization to mutate, or the truth of observations made outside Proofkit."], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.agent-workflow", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-007", + "ownerId": "proofkit.agent-workflow", + "invariant": "Native-evidence guidance has exactly one versioned repository-neutral semantic owner that requires exactly the declared semantic owner, subject/currentness, execution root/source snapshot, exact argv, stdin/input bytes, toolchain/platform/locale/timezone/dependencies, positive control, independent near-miss falsifier, executable oracle, input/work bounds, environment/network policy, CPU/memory/disk/file-descriptor/PID bounds, stdout/stderr bounds, timeout/process-group/detached-child policy, exit mapping, nondisclosure, cleanup/crash inventory, receipt identity, currentness/trust, escalation, retirement, and non-claim slots; every admitted static-resource or CLI delivery is deterministically bound to that owner, and JSON and text are derived without input, mutable shared state, filesystem, Git, process, environment, clock, random, network, container, provider, or other ambient dependency.", + "claimLevel": "blocking", + "riskClass": "high", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-WORKFLOW-007"], + "nonClaims": ["The template does not generate, execute, approve, authenticate, or prove adequacy of a consuming repository's script, witness, oracle, receipt, policy, or cleanup implementation."], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.agent-workflow", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-008", + "ownerId": "proofkit.agent-workflow", + "invariant": "Every workflow caller-visible value is admitted through its typed owner; display-safe values reject malformed UTF-8, secret-shaped content, ASCII controls, DEL, Unicode Cc, Cf, Zl, and Zp without echo; every active prompt packet projects observed fact, uncertainty, the resolved governingAuthorityRefId or a closed missing-owner escalation target, exact admitted coordinates, candidate action, a missing-consumer-witness state, non-claim, stop condition, and expected successor from command-owned tables, and when governingAuthorityRefId is null the packet forbids repository mutation until the caller supplies it; planner text contains the state, active stage and action when present, checkpoint, owner or escalation, stop condition, expected successor, omission count, and one non-claim within 24 lines and 8192 bytes; guidance text contains every slot ID, required decision, and completion criterion within 48 lines and 16384 bytes; caller-owned values are never terminal-styled.", + "claimLevel": "blocking", + "riskClass": "high", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-WORKFLOW-008"], + "nonClaims": ["Bounded nondisclosure admission does not prove absence of every possible sensitive value or every terminal interpretation."], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.agent-workflow", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-009", + "ownerId": "proofkit.agent-workflow", + "invariant": "Workflow commands default to JSON and color never; text and color auto require explicit flags, auto is eligible only for terminal stdout when immutable startup capabilities report a TTY and no NO_COLOR setting, admitted style tokens strip exactly to canonical plain text, and JSON, stderr, redirected stdout, files, prompts, envelopes, reports, receipts, and generated records remain ANSI-free.", + "claimLevel": "blocking", + "riskClass": "high", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-WORKFLOW-009"], + "nonClaims": ["Terminal styling is presentation only and does not claim identical rendering across terminal emulators."], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.agent-workflow", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-010", + "ownerId": "proofkit.agent-workflow", + "invariant": "The capability has exactly one stage/action semantic owner and exactly one evidence-guidance semantic owner; an independently frozen factorized comparison over planner carriers, guidance owners, and guidance carriers selects exactly one dedicated planner CLI carrier and one dedicated guidance CLI carrier without duplicating guidance meaning, while the implementation retains one coherent workflow command family and no persisted experiment matrix, candidate receipt, source-codec variant, external prompt duplicate, runtime guidance resource, repository adapter, setup state, or generic agent executor; changing one stage or guidance slot changes one semantic owner before ordinary generated projections are refreshed.", + "claimLevel": "blocking", + "riskClass": "medium", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-WORKFLOW-010"], + "nonClaims": ["Structural minimality does not prove universal usability, open-world optimality, lower provider token use, or lower cognitive cost for every user and repository."], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.agent-workflow", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-011", + "ownerId": "proofkit.agent-workflow", + "invariant": "Every admitted workflow command, option, and runtime guidance resource participates in one finite factorized relation over carrier identity, global-option position/cardinality/value, command-option cardinality/value, help exclusivity, positional-operand count, input transport, output mode, envelope, and presentation capability; that relation is closed across descriptor, dedicated dispatcher, command family, help, contract-map route, root-shape CLI input/output summary, positive whole-process witness, independent negative falsifier, generated contract projection, breaking version-edge wire observation, and supported installed npm and wheel smokes for each admitted CLI carrier that exercise required input, pre-read input-pointer admission, JSON layout, agent envelope, text, color denial, exclusive help, surplus positional operands, and no-input guidance classes as applicable; the npm package admits the durable workflow specification as channel-specific non-runtime documentation through its existing docs projection, every runtime-required resource remains closed in each package channel, the selected topology adds no external runtime or template resource, and omission, surplus, cross-wire, or stale projection fails the repository gate.", + "claimLevel": "blocking", + "riskClass": "high", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-WORKFLOW-011"], + "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."], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.agent-workflow", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + } + ], + "nonClaims": [ + "Generated plans, prompts, templates, envelopes, views, reports, and patches remain derived guidance and are not semantic, proof, merge, release, rollout, or readiness authority.", + "Proofkit does not own consuming-repository requirement meaning, engineering policy, witnesses, merge decisions, release decisions, rollout decisions, or production-readiness decisions.", + "This source neither selects nor authorizes a persisted requirement-source codec or a migration away from the current requirement source." + ] +} diff --git a/docs/specs/proofkit-spec-proof-core/overview.md b/docs/specs/proofkit-spec-proof-core/overview.md index 435eea1..e8dfda1 100644 --- a/docs/specs/proofkit-spec-proof-core/overview.md +++ b/docs/specs/proofkit-spec-proof-core/overview.md @@ -35,9 +35,13 @@ execution receipts, and merge policy. candidates explicit, keep merge approval consumer-owned, and fail closed for unknown or unmatched proof inputs. - `REQ-PROOFKIT-SPEC-005`: rendered proof views and agent envelopes remain - bounded, reference-closed derived presentations over structured source, - count each semantic omission once, and never become canonical proof or - requirement authority. + bounded, reference-closed derived presentations over structured source and + never become canonical proof or requirement authority; agent envelopes also + admit lossless safe local identities before display sanitization, omit unsafe + identities without turning a redaction label into a lookup key, count each + semantic omission once, and use unique, pairwise-disjoint local target-ID + domains so each retained untyped local reference resolves to at most one + target in their union. - `REQ-PROOFKIT-SPEC-006`: test evidence inventory, explicit test-discovery draft projections, proof-binding-derived inventory projections, normalized inventory projections, coverage-view input composition, and requirement diff --git a/docs/specs/proofkit-spec-proof-core/requirements.v1.json b/docs/specs/proofkit-spec-proof-core/requirements.v1.json index 4914c79..420552c 100644 --- a/docs/specs/proofkit-spec-proof-core/requirements.v1.json +++ b/docs/specs/proofkit-spec-proof-core/requirements.v1.json @@ -116,9 +116,9 @@ { "requirementId": "REQ-PROOFKIT-SPEC-005", "ownerId": "proofkit.spec-proof-core", - "invariant": "Rendered proof views and agent envelopes remain bounded, reference-closed derived presentations over structured source, count each semantic omission once, and never become canonical proof or requirement authority.", + "invariant": "Rendered proof views and agent envelopes remain bounded, reference-closed derived presentations over structured source and never become canonical proof or requirement authority; agent envelopes additionally admit local target IDs and untyped local references as lossless safe identities before display sanitization, omit unsafe identities without using a replacement label as a lookup key, count each semantic omission once, and require target IDs to be unique within each local domain and pairwise disjoint across local domains so every retained untyped local reference resolves to at most one target in their union.", "claimLevel": "blocking", - "riskClass": "medium", + "riskClass": "high", "proofBindingRefs": [ "proofkit/requirement-bindings.json" ], diff --git a/docs/specs/proofkit-supply-chain-quality/overview.md b/docs/specs/proofkit-supply-chain-quality/overview.md index 18945f8..4f3eb94 100644 --- a/docs/specs/proofkit-supply-chain-quality/overview.md +++ b/docs/specs/proofkit-supply-chain-quality/overview.md @@ -180,6 +180,13 @@ vulnerability absence, or consumer rollout safety by itself. commit identities, admitted status expressions, the required aggregate shell program, and read-only treatment of an existing release are source-oracle invariants. +- `REQ-PROOFKIT-QUALITY-026`: every public or retained-evidence stable JSON + implementation accepts only the repository-pinned Unicode scalar set and matches one + preimplementation byte corpus for the pinned control, format, and separator + table without changing decoded values. +- `REQ-PROOFKIT-QUALITY-027`: report-visible diagnostics and structural text + apply the same scalar, control/format, and split-secret taxonomy and replace + rejected caller values as a whole without echoing any fragment. ## Non-Claims diff --git a/docs/specs/proofkit-supply-chain-quality/requirements.v1.json b/docs/specs/proofkit-supply-chain-quality/requirements.v1.json index 7ff411d..a2e5527 100644 --- a/docs/specs/proofkit-supply-chain-quality/requirements.v1.json +++ b/docs/specs/proofkit-supply-chain-quality/requirements.v1.json @@ -333,6 +333,32 @@ "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, "deferral": null, "updatePolicy": {"reviewOwnerId": "proofkit.supply-chain-quality", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-026", + "ownerId": "proofkit.supply-chain-quality", + "invariant": "Every repository-owned stable JSON implementation used by a public or retained-evidence path accepts only Unicode 15.0.0 scalar strings, rejecting malformed UTF-8 byte strings and unpaired UTF-16 surrogates as applicable, emits the same deterministic semantic-preserving JSON escapes for the pinned Cc, Cf, Zl, and Zp range table, preserves ordinary decoded values, and remains byte-compatible with one independently authored literal corpus frozen before implementation.", + "claimLevel": "blocking", + "riskClass": "high", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-QUALITY-026"], + "nonClaims": ["This bounded encoding policy does not prove terminal-rendering equivalence or safety of arbitrary downstream decoders."], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.supply-chain-quality", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-027", + "ownerId": "proofkit.supply-chain-quality", + "invariant": "Every report-visible diagnostic and structural-text sanitizer accepts only Unicode 15.0.0 scalar strings, recognizes the pinned Cc, Cf, Zl, and Zp range table and contiguous or control-split secret-shaped values through one shared taxonomy, and replaces any rejected caller-owned value as a whole with one fixed command-owned label without emitting its bytes, code points, prefix, suffix, or caller-controlled replacement text.", + "claimLevel": "blocking", + "riskClass": "high", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-QUALITY-027"], + "nonClaims": ["This bounded diagnostic taxonomy does not prove confidentiality of values outside its admitted secret and control classes."], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.supply-chain-quality", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} } ] } diff --git a/go.mod b/go.mod index 0703710..8f67345 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ tool ( require go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect require ( + github.com/mattn/go-isatty v0.0.22 go.yaml.in/yaml/v3 v3.0.5 golang.org/x/mod v0.37.0 golang.org/x/tools v0.47.0 @@ -25,7 +26,6 @@ require ( github.com/fatih/color v1.19.0 // indirect github.com/google/renameio v1.0.1 // indirect github.com/mattn/go-colorable v0.1.15 // indirect - github.com/mattn/go-isatty v0.0.22 // indirect github.com/mattn/go-runewidth v0.0.24 // indirect github.com/mattn/go-shellwords v1.0.13 // indirect github.com/rhysd/actionlint v1.7.12 // indirect diff --git a/internal/app/agent_workflow_args.go b/internal/app/agent_workflow_args.go new file mode 100644 index 0000000..b9a315f --- /dev/null +++ b/internal/app/agent_workflow_args.go @@ -0,0 +1,100 @@ +package app + +import ( + "fmt" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/jsonpointer" +) + +type agentWorkflowArgs struct { + inputPath string + inputPointer jsonpointer.Pointer + pointerPresent bool + format string + formatExplicit bool + color string + colorExplicit bool + agentEnvelope bool +} + +func parseAgentWorkflowArgs(command string, args []string) (agentWorkflowArgs, error) { + options := agentWorkflowArgs{format: "json", color: "never"} + 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) + } + if seen[argument] { + return agentWorkflowArgs{}, fmt.Errorf("%s may be specified only once", argument) + } + seen[argument] = true + 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) + } + 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) + } + seen[argument] = true + value := args[index+1] + index++ + switch argument { + case "--color": + if value != "auto" && value != "never" { + return agentWorkflowArgs{}, fmt.Errorf("--color requires one of: auto, never") + } + options.color = value + options.colorExplicit = true + case "--format": + if value != "json" && value != "text" { + return agentWorkflowArgs{}, fmt.Errorf("--format requires one of: json, text") + } + options.format = value + options.formatExplicit = true + case "--input": + options.inputPath = value + case "--input-pointer": + pointer, err := jsonpointer.Parse(value) + if err != nil { + return agentWorkflowArgs{}, err + } + options.inputPointer = pointer + options.pointerPresent = true + } + default: + return agentWorkflowArgs{}, fmt.Errorf("unsupported argument for %s: %s", command, argument) + } + } + if command == "change-workflow-plan" && options.inputPath == "" { + return agentWorkflowArgs{}, fmt.Errorf("change-workflow-plan requires --input ") + } + if options.colorExplicit && options.format != "text" { + return agentWorkflowArgs{}, fmt.Errorf("--color is valid only with --format text") + } + if options.agentEnvelope && options.format != "json" { + return agentWorkflowArgs{}, fmt.Errorf("--agent-envelope is valid only with JSON output") + } + return options, nil +} + +func missingAgentWorkflowValue(command string, flag string) error { + switch flag { + case "--input": + return fmt.Errorf("%s requires --input ", command) + case "--input-pointer": + return fmt.Errorf("--input-pointer requires a JSON pointer") + case "--format": + return fmt.Errorf("--format requires one of: json, text") + case "--color": + return fmt.Errorf("--color requires one of: auto, never") + default: + return fmt.Errorf("unsupported argument for %s: %s", command, flag) + } +} diff --git a/internal/app/agent_workflow_command.go b/internal/app/agent_workflow_command.go new file mode 100644 index 0000000..9f2e36f --- /dev/null +++ b/internal/app/agent_workflow_command.go @@ -0,0 +1,65 @@ +package app + +import ( + "io" + + "github.com/research-engineering/agentic-proofkit/internal/command/changeworkflowplan" + "github.com/research-engineering/agentic-proofkit/internal/kernel/jsonpointer" +) + +func runAgentWorkflowCommand(command string, args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer, capabilities PresentationCapabilities) int { + options, err := parseAgentWorkflowArgs(command, args) + if err != nil { + writeDiagnostic(stderr, err) + return 1 + } + switch command { + case "native-evidence-guidance": + return runNativeEvidenceGuidance(options, stdout, stderr, capabilities) + case "change-workflow-plan": + // Continue through the explicit-input transport below. + default: + writeDiagnosticf(stderr, "unsupported agent workflow command") + return 1 + } + input, err := readInput(options.inputPath, stdin) + if err != nil { + writeDiagnostic(stderr, err) + return 1 + } + if options.pointerPresent { + input, err = jsonpointer.SelectParsed(input, options.inputPointer) + if err != nil { + writeDiagnostic(stderr, err) + return 1 + } + } + if options.agentEnvelope { + output, err := changeworkflowplan.BuildAgentEnvelope(input) + return writeJSON(output, 0, err, stdout, stderr) + } + if options.format == "text" { + lines, err := changeworkflowplan.BuildTextProjection(input) + if err != nil { + return writeText("", 1, err, stdout, stderr) + } + view := changeWorkflowTerminalText(lines) + output, err := renderTerminalText(view, options.color, capabilities) + return writeText(output, 0, err, stdout, stderr) + } + output, err := changeworkflowplan.Build(input) + return writeJSON(output, 0, err, stdout, stderr) +} + +func changeWorkflowTerminalText(lines []changeworkflowplan.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/agent_workflow_command_test.go b/internal/app/agent_workflow_command_test.go new file mode 100644 index 0000000..d584f1d --- /dev/null +++ b/internal/app/agent_workflow_command_test.go @@ -0,0 +1,340 @@ +package app + +import ( + "bytes" + "io" + "strings" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/command/changeworkflowplan" + "github.com/research-engineering/agentic-proofkit/internal/command/nativeevidenceguidance" + "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" + "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" + "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" +) + +const validChangeWorkflowInput = `{"checkpoint":{"state":"not_started"},"completedStageIds":[],"contextRefs":[],"governingAuthorityRefId":null,"requiredContextRefIds":[],"schemaVersion":1}` + +func TestAgentWorkflowCLITruthTable(t *testing.T) { + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.110027854485263740098018592752615689790850067351556445289115876582278838003288") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.008921652518915565373824823596859113418310007148763537766174180247340157842807") + const ( + semanticOutputClasses = 24 + frozenRejectionClasses = 44 + extraRejectionCases = 2 + helpClasses = 10 + colorClasses = 8 + ) + if got, want := semanticOutputClasses+frozenRejectionClasses+extraRejectionCases+helpClasses+colorClasses, 88; got != want { + t.Fatalf("agent workflow CLI truth-table cardinality = %d, want %d", got, want) + } + t.Run("semantic output classes", testAgentWorkflowSemanticOutputClasses) + t.Run("usage errors precede input", testAgentWorkflowUsageErrorsPrecedeInput) + t.Run("exclusive help classes", testAgentWorkflowHelpClasses) + t.Run("terminal capability product", testAgentWorkflowTerminalCapabilityProduct) +} + +func testAgentWorkflowSemanticOutputClasses(t *testing.T) { + workflowValue, err := admission.DecodeJSON(strings.NewReader(validChangeWorkflowInput), 1<<20) + if err != nil { + t.Fatalf("decode workflow fixture: %v", err) + } + wantPlan, err := changeworkflowplan.Build(workflowValue) + if err != nil { + t.Fatalf("build workflow fixture: %v", err) + } + wantText, err := changeworkflowplan.BuildText(workflowValue) + if err != nil { + t.Fatalf("build workflow text fixture: %v", err) + } + wantGuidanceText, err := nativeevidenceguidance.RenderPlainText() + if err != nil { + t.Fatalf("build guidance text fixture: %v", err) + } + + for _, explicitFormat := range []bool{false, true} { + for _, layout := range []string{"default", "pretty", "compact"} { + 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", "-"} + if explicitFormat { + args = append(args, "--format", "json") + } + if envelope { + args = append(args, "--agent-envelope") + } + args = withWorkflowLayout(layout, args) + status, stdout, stderr := executeAgentWorkflowCLI(t, args, strings.NewReader(validChangeWorkflowInput), PresentationCapabilities{}) + if status != 0 || stderr != "" || strings.Contains(stdout, "\x1b[") { + t.Fatalf("status=%d stderr=%q stdout=%q", status, stderr, stdout) + } + value := decodeCLIJSON(t, stdout) + if envelope { + record := value.(map[string]any) + if record["envelopeId"] != "proofkit.change-workflow-plan.agent-envelope" { + t.Fatalf("unexpected envelope identity: %#v", record["envelopeId"]) + } + } else if !equalCLIJSON(t, value, wantPlan) { + t.Fatalf("planner JSON projection drifted") + } + assertJSONLayout(t, stdout, layout) + }) + } + } + } + + 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"}, + } { + status, stdout, stderr := executeAgentWorkflowCLI(t, args, strings.NewReader(validChangeWorkflowInput), PresentationCapabilities{}) + if status != 0 || stderr != "" || stdout != wantText || strings.Contains(stdout, "\x1b[") { + t.Fatalf("planner text status=%d stderr=%q stdout=%q want=%q", status, stderr, stdout, wantText) + } + } + + for _, explicitFormat := range []bool{false, true} { + for _, layout := range []string{"default", "pretty", "compact"} { + t.Run(strings.Join([]string{"guidance", formatClass(explicitFormat), layout}, "/"), func(t *testing.T) { + args := []string{"native-evidence-guidance"} + if explicitFormat { + args = append(args, "--format", "json") + } + args = withWorkflowLayout(layout, args) + status, stdout, stderr := executeAgentWorkflowCLI(t, args, strings.NewReader("unread"), PresentationCapabilities{}) + if status != 0 || stderr != "" || strings.Contains(stdout, "\x1b[") { + t.Fatalf("status=%d stderr=%q stdout=%q", status, stderr, stdout) + } + record := decodeCLIJSON(t, stdout).(map[string]any) + if record["guidanceId"] != nativeevidenceguidance.GuidanceID { + t.Fatalf("unexpected guidance identity: %#v", record["guidanceId"]) + } + assertJSONLayout(t, stdout, layout) + }) + } + } + for _, args := range [][]string{ + {"native-evidence-guidance", "--format", "text"}, + {"native-evidence-guidance", "--format", "text", "--color", "never"}, + {"native-evidence-guidance", "--format", "text", "--color", "auto"}, + } { + status, stdout, stderr := executeAgentWorkflowCLI(t, args, strings.NewReader("unread"), PresentationCapabilities{}) + if status != 0 || stderr != "" || stdout != wantGuidanceText || strings.Contains(stdout, "\x1b[") { + t.Fatalf("guidance text status=%d stderr=%q stdout=%q want=%q", status, stderr, stdout, wantGuidanceText) + } + } +} + +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/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"}, + "guidance/input": {"native-evidence-guidance", "--input", "-"}, + "guidance/pointer": {"native-evidence-guidance", "--input-pointer", "/workflow"}, + "guidance/output": {"native-evidence-guidance", "--output", "out.json"}, + "guidance/envelope": {"native-evidence-guidance", "--agent-envelope"}, + "guidance/json auto": {"native-evidence-guidance", "--color", "auto"}, + "guidance/json explicit never": {"native-evidence-guidance", "--color", "never"}, + "guidance/text layout pretty": {"--json-layout", "pretty", "native-evidence-guidance", "--format", "text"}, + "guidance/text layout compact": {"--json-layout", "compact", "native-evidence-guidance", "--format", "text"}, + "guidance/duplicate format": {"native-evidence-guidance", "--format", "json", "--format", "text"}, + "guidance/duplicate color": {"native-evidence-guidance", "--color", "never", "--color", "auto"}, + "guidance/unknown flag": {"native-evidence-guidance", "--unknown"}, + "guidance/missing format value": {"native-evidence-guidance", "--format"}, + "guidance/missing color value": {"native-evidence-guidance", "--color"}, + "guidance/bad format": {"native-evidence-guidance", "--format", "yaml"}, + "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 guidance": {"--json-layout", "native-evidence-guidance"}, + "global/bad layout planner": {"--json-layout", "dense", "change-workflow-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 guidance": {"--json-layout", "pretty", "--json-layout", "compact", "native-evidence-guidance"}, + } + if got, want := len(cases)-2, 44; got != want { + t.Fatalf("frozen rejection class count = %d, want %d", got, want) + } + for name, args := range cases { + args := args + t.Run(name, func(t *testing.T) { + status, stdout, stderr := executeAgentWorkflowCLI(t, args, panicReader{}, PresentationCapabilities{StdoutIsTTY: true}) + if status != 1 || stdout != "" || stderr == "" || strings.Contains(stderr, "\x1b[") { + t.Fatalf("status=%d stdout=%q stderr=%q", status, stdout, stderr) + } + }) + } +} + +func testAgentWorkflowHelpClasses(t *testing.T) { + valid := [][]string{ + {"change-workflow-plan", "--help"}, + {"change-workflow-plan", "-h"}, + {"native-evidence-guidance", "--help"}, + {"native-evidence-guidance", "-h"}, + } + for _, args := range valid { + status, stdout, stderr := executeAgentWorkflowCLI(t, args, panicReader{}, PresentationCapabilities{StdoutIsTTY: true}) + if status != 0 || stdout == "" || stderr != "" || strings.Contains(stdout, "\x1b[") { + t.Fatalf("args=%v status=%d stdout=%q stderr=%q", args, status, stdout, stderr) + } + } + invalid := []struct { + 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", "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{"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"}, + } + for _, item := range invalid { + status, stdout, stderr := executeAgentWorkflowCLI(t, item.args, panicReader{}, PresentationCapabilities{StdoutIsTTY: true}) + if status != 1 || stdout != "" || !strings.Contains(stderr, item.want) || strings.Contains(stderr, "\x1b[") { + t.Fatalf("args=%v status=%d stdout=%q stderr=%q", item.args, status, stdout, stderr) + } + } +} + +func testAgentWorkflowTerminalCapabilityProduct(t *testing.T) { + workflowValue, err := admission.DecodeJSON(strings.NewReader(validChangeWorkflowInput), 1<<20) + if err != nil { + t.Fatal(err) + } + wantWorkflowText, err := changeworkflowplan.BuildText(workflowValue) + if err != nil { + t.Fatal(err) + } + wantGuidanceText, err := nativeevidenceguidance.RenderPlainText() + if err != nil { + t.Fatal(err) + } + commands := []struct { + name string + args []string + 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: "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 { + for _, tty := range []bool{false, true} { + for _, noColorPresent := range []bool{false, true} { + name := command.name + "/tty=" + boolName(tty) + "/no-color=" + boolName(noColorPresent) + t.Run(name, func(t *testing.T) { + status, stdout, stderr := executeAgentWorkflowCLI(t, command.args, command.stdin(), PresentationCapabilities{StdoutIsTTY: tty, NoColorPresent: noColorPresent}) + if status != 0 || stderr != "" { + t.Fatalf("status=%d stderr=%q", status, stderr) + } + wantANSI := tty && !noColorPresent + if gotANSI := strings.Contains(stdout, "\x1b["); gotANSI != wantANSI { + t.Fatalf("ANSI presence=%t want=%t stdout=%q", gotANSI, wantANSI, stdout) + } + if plain := stripTestANSI(stdout); plain != command.plain { + t.Fatalf("stripped output=%q want=%q", plain, command.plain) + } + }) + } + } + } +} + +func executeAgentWorkflowCLI(t *testing.T, args []string, stdin io.Reader, capabilities PresentationCapabilities) (int, string, string) { + t.Helper() + var stdout bytes.Buffer + var stderr bytes.Buffer + status := RunWithRendererAndCapabilities(t.Context(), args, stdin, &stdout, &stderr, cliexec.PathRenderer(), capabilities) + return status, stdout.String(), stderr.String() +} + +func decodeCLIJSON(t *testing.T, output string) any { + t.Helper() + value, err := admission.DecodeJSON(strings.NewReader(output), 1<<20) + if err != nil { + t.Fatalf("decode CLI JSON: %v\n%s", err, output) + } + return value +} + +func equalCLIJSON(t *testing.T, left any, right any) bool { + t.Helper() + leftBytes, err := marshalCompactForTest(left) + if err != nil { + t.Fatal(err) + } + rightBytes, err := marshalCompactForTest(right) + if err != nil { + t.Fatal(err) + } + return bytes.Equal(leftBytes, rightBytes) +} + +func marshalCompactForTest(value any) ([]byte, error) { + return stablejson.MarshalLayout(value, stablejson.LayoutCompact) +} + +func withWorkflowLayout(layout string, args []string) []string { + if layout == "default" { + return args + } + return append([]string{"--json-layout", layout}, args...) +} + +func assertJSONLayout(t *testing.T, output string, layout string) { + t.Helper() + if layout == "compact" && strings.Count(output, "\n") != 1 { + t.Fatalf("compact JSON has non-terminal newlines: %q", output) + } + if layout != "compact" && strings.Count(output, "\n") < 2 { + t.Fatalf("pretty JSON lacks structural newlines: %q", output) + } +} + +func formatClass(explicit bool) string { + if explicit { + return "explicit-json" + } + return "default-json" +} + +func envelopeClass(value bool) string { + if value { + return "envelope" + } + return "report" +} + +func boolName(value bool) string { + if value { + return "true" + } + return "false" +} diff --git a/internal/app/agent_workflow_version_edge_test.go b/internal/app/agent_workflow_version_edge_test.go new file mode 100644 index 0000000..ba7fc35 --- /dev/null +++ b/internal/app/agent_workflow_version_edge_test.go @@ -0,0 +1,136 @@ +package app + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "path/filepath" + "slices" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/tools/releasechange" +) + +const agentWorkflowVersionEdgePath = "internal/app/testdata/v0.5-wire-observations.json" + +type agentWorkflowVersionEdge struct { + AddedCommandContracts []agentWorkflowCommandContract `json:"addedCommandContracts"` + BreakingChangeIDs []string `json:"breakingChangeIds"` + 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 agentWorkflowCommandContract struct { + Command string `json:"command"` + InputContractSHA256 string `json:"inputContractSha256"` + OutputContractSHA256 string `json:"outputContractSha256"` +} + +func TestAgentWorkflowVersionEdgeClosesPublicWireAdditions(t *testing.T) { + record := readAgentWorkflowVersionEdge(t) + releaseRecord, err := releasechange.Read(filepath.Join(repoRoot(t), releasechange.RecordPath)) + if err != nil { + t.Fatal(err) + } + if err := validateAgentWorkflowVersionEdge(record, releaseRecord); err != nil { + t.Fatal(err) + } + + mutants := []struct { + name string + mutate func(*agentWorkflowVersionEdge) + }{ + {name: "current ABI", mutate: func(value *agentWorkflowVersionEdge) { value.CurrentPublicABISHA256 += "0" }}, + {name: "previous ABI", mutate: func(value *agentWorkflowVersionEdge) { value.PreviousPublicABISHA256 = value.CurrentPublicABISHA256 }}, + {name: "command contract", mutate: func(value *agentWorkflowVersionEdge) { value.AddedCommandContracts[0].OutputContractSHA256 += "0" }}, + {name: "missing command", mutate: func(value *agentWorkflowVersionEdge) { value.AddedCommandContracts = value.AddedCommandContracts[1:] }}, + {name: "breaking owner", mutate: func(value *agentWorkflowVersionEdge) { value.BreakingChangeIDs[0] += ".drift" }}, + } + for _, mutant := range mutants { + t.Run(mutant.name, func(t *testing.T) { + value := cloneAgentWorkflowVersionEdge(record) + mutant.mutate(&value) + if err := validateAgentWorkflowVersionEdge(value, releaseRecord); err == nil { + t.Fatal("version-edge mutant was admitted") + } + }) + } +} + +func readAgentWorkflowVersionEdge(t *testing.T) agentWorkflowVersionEdge { + t.Helper() + content, err := os.ReadFile(filepath.Join(repoRoot(t), agentWorkflowVersionEdgePath)) + 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("agent workflow version edge must be an object") + } + assertExactObjectKeys(t, root, []string{"addedCommandContracts", "breakingChangeIds", "currentPublicAbiSha256", "edgeId", "evidenceClass", "nonClaims", "previousPublicAbiSha256", "previousVersion", "schemaVersion", "version"}, "agent workflow version edge") + commandContracts, ok := root["addedCommandContracts"].([]any) + if !ok { + t.Fatal("agent workflow version edge addedCommandContracts must be an array") + } + for index, raw := range commandContracts { + item, ok := raw.(map[string]any) + if !ok { + t.Fatalf("added command contract %d must be an object", index) + } + assertExactObjectKeys(t, item, []string{"command", "inputContractSha256", "outputContractSha256"}, fmt.Sprintf("added command contract %d", index)) + } + var decoded agentWorkflowVersionEdge + if err := json.Unmarshal(content, &decoded); err != nil { + t.Fatal(err) + } + return decoded +} + +func validateAgentWorkflowVersionEdge(record agentWorkflowVersionEdge, releaseRecord releasechange.Record) error { + if record.SchemaVersion != 1 || record.EdgeID != "proofkit.public-wire.0.4.0-to-0.5.0" || record.EvidenceClass != "owner_authored_frozen_version_edge_observation" { + return fmt.Errorf("version-edge identity is invalid") + } + if record.PreviousVersion != releaseRecord.PreviousVersion || record.Version != releaseRecord.Version { + return fmt.Errorf("version-edge release identity is stale") + } + if record.PreviousPublicABISHA256 != "sha256:fc03740aea9e7f525a4388e5d7f557cde07e11b0db0c05101fe937c28a1129d9" || record.CurrentPublicABISHA256 != "sha256:"+cliContractPublicABISHA256 || record.PreviousPublicABISHA256 == record.CurrentPublicABISHA256 { + return fmt.Errorf("version-edge ABI identity is invalid") + } + expectedCommands := []agentWorkflowCommandContract{ + {Command: "change-workflow-plan", InputContractSHA256: generatedCommandContractMetadataByName["change-workflow-plan"].InputContractSHA256, OutputContractSHA256: generatedCommandContractMetadataByName["change-workflow-plan"].OutputContractSHA256}, + {Command: "native-evidence-guidance", InputContractSHA256: generatedCommandContractMetadataByName["native-evidence-guidance"].InputContractSHA256, OutputContractSHA256: generatedCommandContractMetadataByName["native-evidence-guidance"].OutputContractSHA256}, + } + if !slices.Equal(record.AddedCommandContracts, expectedCommands) { + return fmt.Errorf("version-edge added command contracts are not exact") + } + breaking := make([]string, 0, len(releaseRecord.BreakingChanges)) + for _, change := range releaseRecord.BreakingChanges { + breaking = append(breaking, change.ChangeID) + } + if !slices.Equal(record.BreakingChangeIDs, breaking) { + return fmt.Errorf("version-edge breaking change owners are not exact") + } + if !slices.Equal(record.NonClaims, []string{"This owner-authored version-edge observation binds reviewed public contract identities; it does not authenticate Git history, registry publication, provider ingestion, native witness truth, rollout, or production readiness."}) { + return fmt.Errorf("version-edge non-claims are not exact") + } + return nil +} + +func cloneAgentWorkflowVersionEdge(record agentWorkflowVersionEdge) agentWorkflowVersionEdge { + record.AddedCommandContracts = append([]agentWorkflowCommandContract(nil), record.AddedCommandContracts...) + record.BreakingChangeIDs = append([]string(nil), record.BreakingChangeIDs...) + record.NonClaims = append([]string(nil), record.NonClaims...) + return record +} diff --git a/internal/app/app.go b/internal/app/app.go index d649758..9070606 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -24,6 +24,10 @@ func Run(ctx context.Context, args []string, stdin io.Reader, stdout io.Writer, } func RunWithRenderer(ctx context.Context, args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer, renderer cliexec.Renderer) int { + return RunWithRendererAndCapabilities(ctx, args, stdin, stdout, stderr, renderer, PresentationCapabilities{}) +} + +func RunWithRendererAndCapabilities(ctx context.Context, args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer, renderer cliexec.Renderer, capabilities PresentationCapabilities) int { args, layout, layoutExplicit, err := parseProcessOptions(args) if err != nil { writeDiagnostic(stderr, err) @@ -48,6 +52,10 @@ func RunWithRenderer(ctx context.Context, args []string, stdin io.Reader, stdout } return writeText(commandUsageWithRenderer(descriptor, renderer), 0, nil, stdout, stderr) } + if descriptor.runner != commandRunnerHelp && hasCommandHelpFlag(args[1:]) { + writeDiagnosticf(stderr, "%s help accepts no additional arguments", descriptor.name) + return 1 + } parsedArguments := classifyDescriptorArguments(descriptor, args[1:]) if err := validateFlagConstraints(descriptor, parsedArguments); err != nil { writeDiagnostic(stderr, err) @@ -128,6 +136,8 @@ func RunWithRenderer(ctx context.Context, args []string, stdin io.Reader, stdout supportsContractEnvelope: true, supportsMaterializationFile: false, }) + case commandRunnerAgentWorkflow: + return runAgentWorkflowCommand(args[0], args[1:], stdin, stdout, stderr, capabilities) case commandRunnerAgentRoute: return runAgentRoute(args[1:], stdin, stdout, stderr, renderer) case commandRunnerContractEnvelope: diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 6476b72..d271bfc 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -249,7 +249,7 @@ func TestDuplicateFormatIsRejectedBeforeInputRead(t *testing.T) { } func TestSelfCheckRejectsDuplicateKeys(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.031584524343444160231600312979302740662395207275659075147050938676784401609593") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.001861517770766202473963888516884835242792304213831715864713877788735391460974") var stdout bytes.Buffer var stderr bytes.Buffer status := Run(t.Context(), []string{"self-check", "--input", "-"}, strings.NewReader(`{"schemaVersion":1,"schemaVersion":2}`), &stdout, &stderr) @@ -621,7 +621,7 @@ func TestCLIDiagnosticsRedactSecretLikeCallerLabels(t *testing.T) { if strings.Contains(stderr.String(), secret) { t.Fatalf("stderr leaked secret-shaped caller label: %s", stderr.String()) } - if !strings.Contains(stderr.String(), "") { + if !strings.Contains(stderr.String(), "") { t.Fatalf("stderr=%q, want redaction placeholder", stderr.String()) } }) @@ -729,7 +729,7 @@ func TestCLIDiagnosticsRedactControlAndOversizedCallerLabels(t *testing.T) { name: "control rune", command: "bad\ncommand", forbidden: "bad\ncommand", - wantMarker: "", + wantMarker: "", maxStderr: 128, }, { diff --git a/internal/app/cli_abi_test.go b/internal/app/cli_abi_test.go index cac059b..055171a 100644 --- a/internal/app/cli_abi_test.go +++ b/internal/app/cli_abi_test.go @@ -23,7 +23,7 @@ import ( ) func TestCLIABIGoldenCorpus(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.019844608890608007981435607465632554695607593958274064855839894961505266938135") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.102360021140288204186268416546750706075798003415784832214940311061632258667048") specPath := filepath.Join(repoRoot(t), "docs/specs/proofkit-package-boundary/requirements.v1.json") specContent, err := os.ReadFile(specPath) if err != nil { @@ -1075,7 +1075,7 @@ func TestOutputWriterRejectsDeterministicParentSwap(t *testing.T) { } func TestRequirementBrowserServerSpecTreeCLIABI(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.053429100218927507111827924045562630374131695751215621621358073482708475349353") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.046945996885146731930598876553138115759772643621882641426648437097942560096546") var input any if err := json.Unmarshal([]byte(cliRequirementSpecTreeInput()), &input); err != nil { t.Fatalf("root-oracle input must be JSON: %v", err) @@ -1229,7 +1229,7 @@ func waitForBrowserLauncherURL(t *testing.T, urlFile string, result <-chan int, } func TestAdoptionDoctorCLIABI(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.069327963569220983248235596502245621103210642467353785655205406140014773697960") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.048250751995595430571387829349469851157034053692383982230116642539208551575451") cases := []struct { name string args []string diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index 56f6b64..25fb172 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -18,11 +18,12 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/command/agentroute" "github.com/research-engineering/agentic-proofkit/internal/command/stackpreset" "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" ) const ( - cliContractPublicABISHA256 = "fc03740aea9e7f525a4388e5d7f557cde07e11b0db0c05101fe937c28a1129d9" + cliContractPublicABISHA256 = "2af9b2e3b77b8309f295a2ffeeec70cb4de4d51cb6fbfa45e7733da810ddc1df" maxAggregateFileReadBytesForContractTest = 64 << 20 maxPackageManifestBytesForContractTest = 256 << 10 maxSourceFileBytesForContractTest = 8 << 20 @@ -726,10 +727,19 @@ func assertBoundCommandContract(t *testing.T, command string, direction string, t.Fatalf("%s %s selector is not source-checkout evidence", command, direction) } resolved := resolvedCommandContract(t, value, definitions) - encoded, err := json.Marshal(resolved) + intermediate, err := json.Marshal(resolved) if err != nil { t.Fatal(err) } + canonicalValue, err := admission.DecodeJSON(bytes.NewReader(intermediate), int64(len(intermediate))) + if err != nil { + t.Fatal(err) + } + encoded, err := stablejson.MarshalLayout(canonicalValue, stablejson.LayoutCompact) + if err != nil { + t.Fatal(err) + } + encoded = bytes.TrimSuffix(encoded, []byte{'\n'}) sum := sha256.Sum256(encoded) gotDigest := "sha256:" + fmt.Sprintf("%x", sum[:]) if gotDigest != wantDigest { @@ -1335,7 +1345,7 @@ func stringsAsAny(values []string) []any { } func TestHelpCommandContractForms(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.079695171929558035833123525411377372802577641283912049521939698994087384245655") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.021600527281664046126364606050061357909698227432892268285315907924565443932022") for _, args := range [][]string{{"help"}, {"help", "--help"}, {"help", "-h"}, {"--help"}, {"-h"}, {"help", "repo-profile-admission"}, {"repo-profile-admission", "--help"}} { t.Run(strings.Join(args, " "), func(t *testing.T) { var stdout bytes.Buffer @@ -1683,10 +1693,11 @@ func assertCLIContractSchema(t *testing.T) { if err := json.Unmarshal(processContract["helpGrammar"], &helpGrammar); err != nil { t.Fatalf("decode help grammar: %v", err) } - assertKeys(t, "CLI help grammar", keysAny(helpGrammar), []string{"commandHelpFlags", "helpCatalogFormsSource", "helpCommandPositionalTarget", "helpReadsCommandInput", "rootHelpFlags"}) + assertKeys(t, "CLI help grammar", keysAny(helpGrammar), []string{"commandHelpExclusive", "commandHelpFlags", "helpCatalogFormsSource", "helpCommandPositionalTarget", "helpReadsCommandInput", "rootHelpFlags"}) assertStringSet(t, stringsFromAny(helpGrammar["rootHelpFlags"].([]any)), []string{"--help", "-h"}, "root help flags") assertStringSet(t, stringsFromAny(helpGrammar["commandHelpFlags"].([]any)), []string{"--help", "-h"}, "command help flags") - if helpGrammar["helpCommandPositionalTarget"] != "optional_supported_command" || + if helpGrammar["commandHelpExclusive"] != true || + helpGrammar["helpCommandPositionalTarget"] != "optional_supported_command" || helpGrammar["helpCatalogFormsSource"] != "proofkit/command-families.v1.json" || helpGrammar["helpReadsCommandInput"] != false { t.Fatalf("CLI help grammar does not describe runtime help routing: %#v", helpGrammar) diff --git a/internal/app/command_contract_generated.go b/internal/app/command_contract_generated.go index af1a940..c9ecd5b 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 = "bde529df71be7bda804184bb461b7a599520ad1deb362e1bb322a298b24b5965" +const commandContractSourceSHA256 = "cbac189e838e846c66616ff6219e495c640cf9918cb831e0a4892c5dc38934ee" type generatedCommandContractMetadata struct { InputContractSHA256 string @@ -13,15 +13,16 @@ type generatedCommandContractMetadata struct { var generatedCommandContractMetadataByName = map[string]generatedCommandContractMetadata{ "adoption-checklist": {InputContractSHA256: "sha256:4e6c4c9b369279837a5894c0b3f842a411dce529b91c91cb2d4ec63eb5ee4c2c", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.adoption-checklist.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:9d0d0e60f0935407fd31007d8502459663eb4c7228dc5e3c7727ae2c9907bdc9", FlagChoices: map[string][]string{}}, "adoption-contract-envelope": {InputContractSHA256: "sha256: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{}}, - "adoption-doctor": {InputContractSHA256: "sha256:43c741dab52b0e0204eace2b693c091aeb9c280b4d8c27e956dc524188bd9f42", 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:b7d03277879eaec1d928b870a51242d399f17ee03e16dd7fc355af1109b4831f", FlagChoices: map[string][]string{}}, + "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{}}, "adoption-workflow-plan": {InputContractSHA256: "sha256:b32ae67179d7b6dcf1ea66cb6b2b2691c8367ce2e2be367619b65973166da55c", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.adoption-workflow-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:8d64cb53ebd0307e3cebc3435286a3d2a1ee8a0ad6f7514fc0fb3285db0f565b", FlagChoices: map[string][]string{}}, - "agent-route": {InputContractSHA256: "sha256:e18596c4c440322c5596f2d4981a2f3f91bef5136d3c41bd7bf800e627d46f7e", InputSchemaSummary: []string{"availableInputs", "browserMode", "goal", "knownChangedPaths", "mode", "nonClaims", "observedReports", "openBrowser", "routeId", "schemaVersion", "root-shape-only definition proofkit.agent-route.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:a39e222f92f21a54f5bad5dec84fb68dadd6b06d83cb9b46d0b942fe944288b3", FlagChoices: map[string][]string{}}, + "agent-route": {InputContractSHA256: "sha256:6b5af8287f2972bbef4c68c247f43fb16d0f0d8739e5e6d3a66543af20d2644d", InputSchemaSummary: []string{"availableInputs", "browserMode", "goal", "knownChangedPaths", "mode", "nonClaims", "observedReports", "openBrowser", "routeId", "schemaVersion", "root-shape-only definition proofkit.agent-route.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:44ec313a43360b6138ad6c3ae5de4abd51bbf312060880c108a6351606695915", FlagChoices: map[string][]string{}}, "binding-partition": {InputContractSHA256: "sha256:366ad082045af52b2ac6604f18626d0f285b2db73b45d9a82687b8d3b0d2b3fd", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.binding-partition.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:52840879e13a00ef9a4abaad6cdb33000511674d5f9003fb56f387fdf58fadc8", FlagChoices: map[string][]string{}}, "branch-authority": {InputContractSHA256: "sha256:8a3ed74978898593fbdbf1f7fa684dae450fbd9019edcd60d07f818d63363ed4", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.branch-authority.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3c7dc74842299b92cd5baf57cc8666e9415963091359e5faf654e28da89561f1", FlagChoices: map[string][]string{}}, "capability-map-admission": {InputContractSHA256: "sha256:efe87497cf6a32cfd1f9c9c191393cac8781d1e9211914a8a7b6aaa44e7f371d", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.capability-map-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:bfb32535fd51bed168111912acb1651527897fe59a592119dc0032ce9286c9db", FlagChoices: map[string][]string{}}, + "change-workflow-plan": {InputContractSHA256: "sha256:0851ad1ad3990009ca769e6f7e7e4402f289c35484123ccb88097ab21c3b9f18", 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:1a9f6fd253fa01c55f29990d67f1e46ead8068c7f16880df45a645021c26dae1", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}}, "changed-path-set": {InputContractSHA256: "sha256:8fe97426a58969e3e8dcbd52ed44540666b4de6be0487e8a3bc5088ae9c0f933", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.changed-path-set.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:abccdbf78e67f633ce49c34e8849c03f08ce42fa934a4c68969720c5045bf593", FlagChoices: map[string][]string{}}, "completion-criteria": {InputContractSHA256: "sha256:99c49c44b001e40383787e4c55f66621b8a8315f09635f1baf2326dc09bec4e6", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.completion-criteria.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:c90bb9605c7a22914104701a068dded510534bdeb60f4d601555d46c2d3d8a6d", FlagChoices: map[string][]string{}}, - "conformance-profile": {InputContractSHA256: "sha256: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:c6564fe7f19c0bc4bcd1e8dc42521a0153a995a8d75a4f28001c595e7feea915", FlagChoices: map[string][]string{}}, + "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{}}, "custom-rule-boundary": {InputContractSHA256: "sha256:18119bc01230b9cb01cbb56bab531c1d2153c689aea3863b24de4f345eec5ede", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.custom-rule-boundary.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:8a80f33974c60f960daa4f9803f2b5dfb34ff323418f5f0ab75d7b348653a34a", FlagChoices: map[string][]string{}}, "deployment-evidence-admission": {InputContractSHA256: "sha256:c3472b87497be14c107dcf95f2c98594d21ec88e514919fe8fb7da858206421d", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.deployment-evidence-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:818fb2b7dbf67be1da27cc62e78c7bd7fdaddaa38fa5ec80f0cad64fbcb60d90", FlagChoices: map[string][]string{}}, "document-lifecycle-boundary": {InputContractSHA256: "sha256:393836f0ce8e23df351e01d7dec221908ae76fa9b1a045b96fce229eecc844ed", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.document-lifecycle-boundary.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:65c7c0338df60d83d71df9fce3a7e60ce0b0d86819a38e2a6ffeac58b4abc934", FlagChoices: map[string][]string{}}, @@ -33,12 +34,13 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "help": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "", FlagChoices: map[string][]string{}}, "impact": {InputContractSHA256: "sha256:41d3107414837955ee408d5ce94949a4c1a6b76f6949e6c1dc224bd06f6b09bc", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.impact.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:73066e9a5ca48f21936111ffb7223900fb629875997f4e7b16d7fef9c4177972", FlagChoices: map[string][]string{}}, "init": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:3e59a3002327c759e5e747f8baacaa63a4d6784e1a1c520f0a54e01af3f2faa0", FlagChoices: map[string][]string{}}, - "json-report-cli-adapter-source": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:5fcab8ffc599c54045ee04e5eb5abbe0175791e74d5f7edb2940e89424c5b4aa", FlagChoices: map[string][]string{}}, + "json-report-cli-adapter-source": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:dbde6809a1702e1161be48a85da1d0971f55be96c0f8432778dd5d5ce757c3b0", FlagChoices: map[string][]string{}}, "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{}}, "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{}}, + "native-evidence-guidance": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:ed40668acec3a8e2ae097b0ce011994e7facf6feda23e0057c4dcbe660a1a1b7", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}}, "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{}}, "package-runtime-dependency-admission": {InputContractSHA256: "sha256:fc85887af9b8fcd899d245f0db30b2f2f68609822fc268126bf999082bb4115f", InputSchemaSummary: []string{"schemaVersion=1", "reportId", "expectedDependencySpec", "expectedLockfileIntegrity", "expectedPackageName", "expectedPackageVersion", "admissibleLocations{}", "packageResolution{}", "nonClaims[]", "root-shape-only definition proofkit.package-runtime-dependency-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:c012032e8c8212fd50bc2e85669cc610609ca2124ebc992c9e88f44a1ad2d5fc", FlagChoices: map[string][]string{}}, - "pilot-admission": {InputContractSHA256: "sha256: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:bb4d444e17324785e574126a262c09b826c6d2d6199ef1e5a8a18e9e408ef462", FlagChoices: map[string][]string{}}, + "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:c6f47e00793881556f05739099b9a082f82961f876d343152761474fae4016c2", FlagChoices: map[string][]string{}}, "producer-policy-self-proof": {InputContractSHA256: "sha256:d48e18826000c8d415f3c44b6c686e1da6ed962ef7ca36c9f705de8c68d034f9", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.producer-policy-self-proof.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:e82a3989a743f8babc6069f7af82b1dd1ea62bad8dbb18d95e105b36f74e4276", FlagChoices: map[string][]string{}}, "proof-obligation-algebra": {InputContractSHA256: "sha256: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{}}, "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{}}, @@ -55,8 +57,8 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "requirement-authoring-plan": {InputContractSHA256: "sha256:208d7d47109dee1ec355ae3970937690ae528a9cc0cb0eb885d7cc72d843f1e8", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.requirement-authoring-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:e995d289c4a44310add784bbafa5a3a50ec305c3809a89506dd3b49914fbe28f", FlagChoices: map[string][]string{}}, "requirement-bindings": {InputContractSHA256: "sha256:4771b7ed1e23b20c983060deb8f8e65391052f0e5a61cf0f5c67c0e73b8fc5dd", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.requirement-bindings.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:7821c7b23ff2c0ca83c64039c22400d90660cad73a60b9afb46829c539c61168", FlagChoices: map[string][]string{}}, "requirement-browser-server": {InputContractSHA256: "sha256:acb36856dc08ae8efa68986bb4c419f480951b1ad9ee9a43fc1d6c53d30ee71b", InputSchemaSummary: []string{"workspace mode: schemaVersion=2", "workspace mode: workspaceId", "workspace mode: context=proofkit.requirement-context schemaVersion=2 with strict v1 adapter", "workspace mode: diffInput=proofkit.requirement-semantic-diff-input schemaVersion=2 (optional)", "workspace mode: graphInput=proofkit.requirement-traceability-graph-input schemaVersion=2 (optional)", "--session-mode values: browse|one-shot-question", "one-shot-question requires --view workspace --serve --open", "--session-timeout-seconds is 1..7200 and requires one-shot-question", "source|proof|coverage|spec-tree modes retain their owner input contracts", "root-shape-only definition proofkit.requirement-browser-server.input.v3.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:16e741d88e5ede4271c5e769c724e72164fcba461f0c6c17d285f318f8e03005", FlagChoices: map[string][]string{}}, - "requirement-context-compose": {InputContractSHA256: "sha256:04603ae61b50c44e47dde2d11f4b730bb5e91f53645bcda6dbda80b708ef2781", InputSchemaSummary: []string{"schemaVersion=1", "catalogId", "specTree.path", "requirementSources[] (non-empty)", "requirementSources[].nodeId", "requirementSources[].path", "expectedSourceDigest (optional sha256 ref)", "proofBinding.path (optional)", "coverage.path (optional)", "exact catalog paths only; no discovery", "root-shape-only definition proofkit.requirement-context-compose.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:499413fd829de7f275f624a13f2de51afca9a6c201066eb69d95487d9abcab0b", FlagChoices: map[string][]string{}}, - "requirement-context-slice": {InputContractSHA256: "sha256:4962dbf8bdde43384ceb375620e6b00f759157a4e6ceb1614d6e58a9f33ee583", InputSchemaSummary: []string{"schemaVersion=1", "sliceId", "context=proofkit.requirement-context schemaVersion=2 with strict v1 adapter", "query.profile=routing|specification|proof|coverage|review", "query.nodeIds[]|requirementIds[]|ownerIds[]|lifecycleStates[]", "query.maxDepth=0..512", "query.maxNodes=1..4096", "query.maxRequirements=1..16384", "root-shape-only definition proofkit.requirement-context-slice.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:93460f60611eeaf2a13d6e8779841fe78746e1fde9db7fa5e41c91c3b6b49ca4", FlagChoices: map[string][]string{}}, + "requirement-context-compose": {InputContractSHA256: "sha256:3b06ebca2a07d01b34005d915918b8a7743ef8175901ee8dd0a37ecfb6ee80fe", InputSchemaSummary: []string{"schemaVersion=1", "catalogId", "specTree.path", "requirementSources[] (non-empty)", "requirementSources[].nodeId", "requirementSources[].path", "expectedSourceDigest (optional sha256 ref)", "proofBinding.path (optional)", "coverage.path (optional)", "exact catalog paths only; no discovery", "root-shape-only definition proofkit.requirement-context-compose.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:a2b8bf00d2308628e1835fe890dc6e9c4f402c531726f0bdaffad1d7100a7466", FlagChoices: map[string][]string{}}, + "requirement-context-slice": {InputContractSHA256: "sha256:883e864e44944270f7b85c013635835e5e1f1a49a7fd7ca3ebe860c7b56da601", InputSchemaSummary: []string{"schemaVersion=1", "sliceId", "context=proofkit.requirement-context schemaVersion=2 with strict v1 adapter", "query.profile=routing|specification|proof|coverage|review", "query.nodeIds[]|requirementIds[]|ownerIds[]|lifecycleStates[]", "query.maxDepth=0..512", "query.maxNodes=1..4096", "query.maxRequirements=1..16384", "root-shape-only definition proofkit.requirement-context-slice.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:971f78ec94bb29a24057343ccfb3d1192134d2ac88968aab5193e3d64ac87506", FlagChoices: map[string][]string{}}, "requirement-coverage-input-compose": {InputContractSHA256: "sha256:c18294cc0dc76949ea7df200d45d3eca7a1d0e48ff086778502f6b88eaeda614", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.requirement-coverage-input-compose.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:fd38161adcb4f676d58135cdfd2b01d99806fcdce85df97a993258650118bde4", FlagChoices: map[string][]string{}}, "requirement-coverage-view": {InputContractSHA256: "sha256:40b06176f12dc9ec92226d7c01be53df5aa2b0e949a7791a894c8cf148e44b21", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.requirement-coverage-view.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:4ec6c0bb6d616d23b777e59721a3ff045e6b447401333300031d641218c926bf", FlagChoices: map[string][]string{}}, "requirement-impact-input-compose": {InputContractSHA256: "sha256:c80c57489205004f92603fec541ce3d36dd0d9b65109dcfefb97bcfb07b90679", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.requirement-impact-input-compose.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:b0b689f4d0b5bafc52c6041a2aa3583c9c8628aa0f13a0f0d7c42a610ed1a0d6", FlagChoices: map[string][]string{}}, @@ -76,13 +78,13 @@ 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{}}, "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{}}, "selective-gate-plan": {InputContractSHA256: "sha256:5293a5a4c7d8426cf637e6f8d252095ca0eb1714365bb89bec83307b778c678a", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.selective-gate-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:d7bffed853af5595af08b03859be01c283a3bdff1b3502d94ddc190889977647", FlagChoices: map[string][]string{}}, - "self-check": {InputContractSHA256: "sha256:347a12b80bfd182fced99f02d49358645aa1c839ac5c6603810d2d6a0ebbf40f", 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:6d732343f74ddce1fd9f926fda52ca36378736af2b2bdf464fd87f4054c17433", FlagChoices: map[string][]string{}}, + "self-check": {InputContractSHA256: "sha256:768c1fc1d1aea0ab27e73e5b25927975153f5eac9887fb96ca07280640a39c46", 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:f5a0a7f7d177080ff2726b1827383c4bb8bace291c5af83d1d6a93bfb20b41c5", FlagChoices: map[string][]string{}}, "spec-overview-claims": {InputContractSHA256: "sha256:2490dcd34ba7485e13f8f33e8a288a0463c4c52cc6b0d82c57777466927e49a4", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.spec-overview-claims.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:554f3a7020e9820ccb90672629fd769c52b2f298f356040aa3b0a817666cbfbf", FlagChoices: map[string][]string{}}, "spec-proof-bundle-admission": {InputContractSHA256: "sha256:6b6c2875b6476e63a1911e7d6112d9999df2babbee969f84abc4c9e4b470c933", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.spec-proof-bundle-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:e9e0eb66cebca3b99fe5036fb2e7327a9284934ed76f58818d18094d0546fc52", FlagChoices: map[string][]string{}}, "stack-preset": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:f495e9ade4e1e7af7a8f8b2059f7611cc016e6080363afccf76d8dfc2dbc6d2d", FlagChoices: map[string][]string{"--preset": []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"}}}, "test-evidence-inventory": {InputContractSHA256: "sha256:8d07bc37143833d4332106ca332bbc06abf24adddccf6d7448e5faf42771c19e", 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:4e91925c8c135fa8f95121ebb2e7824f5a68ba41fa9b0171c07ac0501b14ce59", FlagChoices: map[string][]string{}}, - "text-policy": {InputContractSHA256: "sha256:d686238fdba68d22a701030ecbfaa5d6fcb7229b3d12638b45c5473670eadd35", 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:ca8635ae272627e37453e03c4d535904757c73fea96d4c55defbc1d647b1f80e", FlagChoices: map[string][]string{}}, - "typescript-public-api-surfaces": {InputContractSHA256: "sha256:e8e55a53390c50fe0d8e669907318474c237327f2f094b3c78908b4fd6941035", InputSchemaSummary: []string{"schemaVersion=1", "machineContract=public_api_surfaces", "entries[].packageManifestPath", "entries[].packageName", "entries[].exportKey", "entries[].exportConditions[] (non-empty, sorted unique by condition)", "entries[].exportConditions[].condition", "entries[].exportConditions[].path", "entries[].exportConditions[].sourcePath (declared and canonical target .ts/.mts/.cts)", "entries[].runtimeExports[]", "entries[].typeExports[]", "entries[].deniedExportKeys[] (optional)", "sourceGrammar=fail_closed_restricted_typescript_exports_v1", "maxSourceFileBytes=8388608", "maxPackageManifestBytes=262144", "maxAggregateFileReadBytes=67108864", "root-shape-only definition proofkit.typescript-public-api-surfaces.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:2f05ea8073e7a2d4a08de962461f5fb31a78f14e77f9ce2406e3ce40ec7beb68", FlagChoices: map[string][]string{}}, + "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{}}, + "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{}}, "witness-plan": {InputContractSHA256: "sha256:7814c5d27487a361bac77045afe32c6449c24881f04d5496da7182b3f2c0c1ee", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.witness-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:722c577bf5cf1dab8c9da8d18fc0634e1b9a6471aa5ca0b05f55b730cdd0d303", FlagChoices: map[string][]string{}}, "witness-scheduler-plan": {InputContractSHA256: "sha256:972c782dc8c5f012380acba2f7e80030adccef3a93c63255e60b2ac75af9cc4c", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.witness-scheduler-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:1c87ee7d359b28e66e5236ff8f8e779d9d8b452f8337b589f8b7bcfd17db1dc4", FlagChoices: map[string][]string{}}, "workspace-changed-package-plan": {InputContractSHA256: "sha256:77528c486d3b95c85be7c653d544ae9190662e5a3293cc70475efb28e8fe7485", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.workspace-changed-package-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:02db1649b4d5aaa4b5abff0579b6edbbd89af43a58fcafe0b1258fa396a78570", FlagChoices: map[string][]string{}}, diff --git a/internal/app/command_coverage_routes.go b/internal/app/command_coverage_routes.go index 2e05673..0fc3577 100644 --- a/internal/app/command_coverage_routes.go +++ b/internal/app/command_coverage_routes.go @@ -7,6 +7,8 @@ import ( "math/big" "sort" "strings" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" ) // Static routes remain declaration-only candidates. The command-oracle owner @@ -62,28 +64,39 @@ var commandCoverageRoutes = map[string][]commandCoverageRoute{ "binding-partition": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/bindingpartition/bindingpartition_test.go", "TestBuildRejectsCrossSurfaceRouteReferenceWithoutDelegation", semanticRouteProof("bindingpartition.build_rejects_cross_surface_route_reference_without_delegation"), "Binding partition admission must reject undelegated cross-surface proof route references.")}, "branch-authority": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/branchauthority/branchauthority_test.go", "TestBuildAdmitsAlignedRequiredBranchAndRejectsRequiredDrift", semanticRouteProof("branchauthority.build_admits_aligned_required_branch_and_rejects_required_drift"), "Branch authority must reject required branch drift.")}, "capability-map-admission": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/capabilitymapadmission/capability_map_admission_test.go", "TestBuildCodeBaselineFailsMissingCandidateRequirementAndAnchor", semanticRouteProof("capabilitymapadmission.build_code_baseline_rejects_missing_candidate_or_anchor"), "Capability map admission must fail code_baseline mode when candidate requirement ids or active scenario anchors are missing, while keeping outputs candidate-only.")}, - "changed-path-set": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/changedpathset/changedpathset_test.go", "TestBuildDeduplicatesAndFailsClosedOnInvalidPaths", semanticRouteProof("changedpathset.build_deduplicates_and_fails_closed_on_invalid_paths"), "Changed path set must deduplicate caller path sources and fail closed with redacted invalid-path diagnostics.")}, - "completion-criteria": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/completioncriteria/completioncriteria_test.go", "TestBuildBlocksUnsatisfiedBlockingCriterion", semanticRouteProof("completioncriteria.build_blocks_unsatisfied_blocking_criterion"), "Completion criteria must fail when a blocking criterion is not satisfied.")}, + "change-workflow-plan": { + requiredInputAdmissionRoute, + directCLIRoute("internal/app/agent_workflow_command_test.go", "TestAgentWorkflowCLITruthTable", semanticRouteProof("agent_workflow_command.agent_workflow_cli_truth_table"), "Change workflow planning must preserve the public JSON, text, envelope, rejection, help, and color contract across the full CLI truth table."), + packageFalsifierRoute("internal/command/changeworkflowplan/state_test.go", "TestWorkflowStatePredicates", semanticRouteProof("changeworkflowplan.workflow_state_predicates"), "Change workflow planning must implement the total 29-state checkpoint relation without inventing repository state."), + packageFalsifierRoute("internal/command/changeworkflowplan/state_test.go", "TestWorkflowCheckpointPredicates", semanticRouteProof("changeworkflowplan.workflow_checkpoint_predicates"), "Change workflow planning must fail closed for invalid checkpoints and impossible completion histories."), + }, + "changed-path-set": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/changedpathset/changedpathset_test.go", "TestBuildDeduplicatesAndFailsClosedOnInvalidPaths", semanticRouteProof("changedpathset.build_deduplicates_and_fails_closed_on_invalid_paths"), "Changed path set must deduplicate caller path sources and fail closed with redacted invalid-path diagnostics.")}, + "completion-criteria": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/completioncriteria/completioncriteria_test.go", "TestBuildBlocksUnsatisfiedBlockingCriterion", semanticRouteProof("completioncriteria.build_blocks_unsatisfied_blocking_criterion"), "Completion criteria must fail when a blocking criterion is not satisfied.")}, "conformance-profile": { requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/conformanceprofile/conformanceprofile_test.go", "TestBuildProfileResolvesRequiredSurfaceAndRejectsMissingSurface", semanticRouteProof("conformanceprofile.build_profile_resolves_required_surface_and_rejects_missing_surface"), "Conformance profile resolution must reject required surfaces absent from the proof contract."), packageFalsifierRoute("internal/command/conformanceprofile/conformanceprofile_test.go", "TestBuildVerificationRejectsDuplicateProfiles", semanticRouteProof("conformanceprofile.build_verification_rejects_duplicate_profiles"), "Conformance profile verification must reject duplicate profile identities."), packageFalsifierRoute("internal/command/conformanceprofile/conformanceprofile_test.go", "TestListReturnsSortedProfileIDsAndRejectsInvalidInput", semanticRouteProof("conformanceprofile.list_returns_sorted_profile_ids_and_rejects_invalid_input"), "Conformance profile listing must preserve deterministic public ids and reject invalid input."), }, - "custom-rule-boundary": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/customruleboundary/customruleboundary_test.go", "TestBuildAdmitsBoundedCustomRuleAndRejectsUnsafeEffects", semanticRouteProof("customruleboundary.build_admits_bounded_custom_rule_and_rejects_unsafe_effects"), "Custom-rule boundary reports must reject unsafe custom-rule effects while keeping custom rules local and non-authoritative.")}, - "deployment-evidence-admission": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/deploymentevidenceadmission/deployment_evidence_admission_test.go", "TestBuildAdmitsCandidateEvidenceAndRejectsUnpinnedImages", semanticRouteProof("deployment_evidence_admission.build_admits_candidate_evidence_and_rejects_unpinned_images"), "Deployment evidence admission must reject unpinned image references while admitting explicit candidate evidence.")}, - "document-lifecycle-boundary": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/documentlifecycle/documentlifecycle_test.go", "TestBuildAdmitsCurrentDurableDocumentAndRejectsAuthorityDrift", semanticRouteProof("documentlifecycle.build_admits_current_durable_document_and_rejects_authority_drift"), "Document lifecycle boundary reports must reject active authority drift across current, generated, rendered, temporary, and archived surfaces.")}, - "evidence-graph": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/requirementbinding/projections_test.go", "TestBuildEvidenceGraphBuildsGraphAndRejectsFailedReport", semanticRouteProof("projections.build_evidence_graph_builds_graph_and_rejects_failed_report"), "Evidence graph projection must emit graph output only from passed requirement bindings.")}, - "external-consumer": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/externalconsumer/externalconsumer_test.go", "TestBuildAdmitsExternalConsumerProofAndRejectsWorkspaceLock", semanticRouteProof("externalconsumer.build_admits_external_consumer_proof_and_rejects_workspace_lock"), "External consumer evidence must reject lockfiles that resolve through the local workspace.")}, - "gradual-adoption": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/gradualadoption/gradualadoption_test.go", "TestBuildRejectsRollbackShellControlCommand", semanticRouteProof("gradualadoption.build_rejects_rollback_shell_control_command"), "Gradual adoption reports must reject shell-control rollback commands.")}, - "gradual-adoption-bootstrap": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/gradualadoption/gradualadoption_test.go", "TestBootstrapRejectsUnknownRootAndNestedFields", semanticRouteProof("gradualadoption.bootstrap_rejects_unknown_root_and_nested_fields"), "Gradual adoption bootstrap must reject unknown root and nested input fields.")}, - "gradual-adoption-guidance": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/gradualadoption/guidance_test.go", "TestGuidanceEnforcementFailsClosedForCandidateBoundaries", semanticRouteProof("guidance.guidance_enforcement_fails_closed_for_candidate_boundaries"), "Gradual adoption guidance must fail closed for candidate boundaries in enforcement modes.")}, - "help": {directCLIRoute("internal/app/cli_contract_test.go", "TestHelpCommandContractForms", semanticRouteProof("cli_contract.help_command_contract_forms"), "Help command forms must emit the documented usage contract.")}, - "impact": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/impact/impact_test.go", "TestBuildRoutesChangedRecordToObligationAndRejectsUnboundProofChange", semanticRouteProof("impact.build_routes_changed_record_to_obligation_and_rejects_unbound_proof_change"), "Impact analysis must route changed requirement records to obligations and reject unbound proof-like changes.")}, - "init": {directCLIRoute("internal/app/cli_abi_test.go", "TestCLIABIGoldenCorpus", semanticRouteProof("cli_abi.init_golden_corpus"), "Init CLI ABI must emit dry-run route guidance without reading stdin, scanning, writing, or promoting repository facts.")}, - "json-report-cli-adapter-source": {packageFalsifierRoute("internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source_test.go", "TestGeneratedTypeScriptAdapterExecutesCoreSemantics", semanticRouteProof("json_report_cli_adapter_source.generated_type_script_adapter_executes_core_semantics"), "JSON report CLI adapter source generation must emit executable TypeScript that preserves parser, stable JSON, subprocess exit-code, stdout, stderr, and redacted direct-main semantics.")}, - "migration-parity-admission": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/migrationparityadmission/migrationparityadmission_test.go", "TestBuildAdmitsCallerDeclaredMatchAndRejectsDigestDrift", semanticRouteProof("migrationparityadmission.build_admits_caller_declared_match_and_rejects_digest_drift"), "Migration parity admission must reject caller-declared matches whose supplied digests differ without claiming native digest verification.")}, - "migration-plan": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/migrationplan/migrationplan_test.go", "TestSortedFollowUpCommandsRejectsShellControlTokens", semanticRouteProof("migrationplan.sorted_follow_up_commands_rejects_shell_control_tokens"), "Migration plans must reject shell-control follow-up commands.")}, + "custom-rule-boundary": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/customruleboundary/customruleboundary_test.go", "TestBuildAdmitsBoundedCustomRuleAndRejectsUnsafeEffects", semanticRouteProof("customruleboundary.build_admits_bounded_custom_rule_and_rejects_unsafe_effects"), "Custom-rule boundary reports must reject unsafe custom-rule effects while keeping custom rules local and non-authoritative.")}, + "deployment-evidence-admission": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/deploymentevidenceadmission/deployment_evidence_admission_test.go", "TestBuildAdmitsCandidateEvidenceAndRejectsUnpinnedImages", semanticRouteProof("deployment_evidence_admission.build_admits_candidate_evidence_and_rejects_unpinned_images"), "Deployment evidence admission must reject unpinned image references while admitting explicit candidate evidence.")}, + "document-lifecycle-boundary": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/documentlifecycle/documentlifecycle_test.go", "TestBuildAdmitsCurrentDurableDocumentAndRejectsAuthorityDrift", semanticRouteProof("documentlifecycle.build_admits_current_durable_document_and_rejects_authority_drift"), "Document lifecycle boundary reports must reject active authority drift across current, generated, rendered, temporary, and archived surfaces.")}, + "evidence-graph": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/requirementbinding/projections_test.go", "TestBuildEvidenceGraphBuildsGraphAndRejectsFailedReport", semanticRouteProof("projections.build_evidence_graph_builds_graph_and_rejects_failed_report"), "Evidence graph projection must emit graph output only from passed requirement bindings.")}, + "external-consumer": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/externalconsumer/externalconsumer_test.go", "TestBuildAdmitsExternalConsumerProofAndRejectsWorkspaceLock", semanticRouteProof("externalconsumer.build_admits_external_consumer_proof_and_rejects_workspace_lock"), "External consumer evidence must reject lockfiles that resolve through the local workspace.")}, + "gradual-adoption": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/gradualadoption/gradualadoption_test.go", "TestBuildRejectsRollbackShellControlCommand", semanticRouteProof("gradualadoption.build_rejects_rollback_shell_control_command"), "Gradual adoption reports must reject shell-control rollback commands.")}, + "gradual-adoption-bootstrap": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/gradualadoption/gradualadoption_test.go", "TestBootstrapRejectsUnknownRootAndNestedFields", semanticRouteProof("gradualadoption.bootstrap_rejects_unknown_root_and_nested_fields"), "Gradual adoption bootstrap must reject unknown root and nested input fields.")}, + "gradual-adoption-guidance": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/gradualadoption/guidance_test.go", "TestGuidanceEnforcementFailsClosedForCandidateBoundaries", semanticRouteProof("guidance.guidance_enforcement_fails_closed_for_candidate_boundaries"), "Gradual adoption guidance must fail closed for candidate boundaries in enforcement modes.")}, + "help": {directCLIRoute("internal/app/cli_contract_test.go", "TestHelpCommandContractForms", semanticRouteProof("cli_contract.help_command_contract_forms"), "Help command forms must emit the documented usage contract.")}, + "impact": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/impact/impact_test.go", "TestBuildRoutesChangedRecordToObligationAndRejectsUnboundProofChange", semanticRouteProof("impact.build_routes_changed_record_to_obligation_and_rejects_unbound_proof_change"), "Impact analysis must route changed requirement records to obligations and reject unbound proof-like changes.")}, + "init": {directCLIRoute("internal/app/cli_abi_test.go", "TestCLIABIGoldenCorpus", semanticRouteProof("cli_abi.init_golden_corpus"), "Init CLI ABI must emit dry-run route guidance without reading stdin, scanning, writing, or promoting repository facts.")}, + "json-report-cli-adapter-source": {packageFalsifierRoute("internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source_test.go", "TestGeneratedTypeScriptAdapterExecutesCoreSemantics", semanticRouteProof("json_report_cli_adapter_source.generated_type_script_adapter_executes_core_semantics"), "JSON report CLI adapter source generation must emit executable TypeScript that preserves parser, stable JSON, subprocess exit-code, stdout, stderr, and redacted direct-main semantics.")}, + "migration-parity-admission": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/migrationparityadmission/migrationparityadmission_test.go", "TestBuildAdmitsCallerDeclaredMatchAndRejectsDigestDrift", semanticRouteProof("migrationparityadmission.build_admits_caller_declared_match_and_rejects_digest_drift"), "Migration parity admission must reject caller-declared matches whose supplied digests differ without claiming native digest verification.")}, + "migration-plan": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/migrationplan/migrationplan_test.go", "TestSortedFollowUpCommandsRejectsShellControlTokens", semanticRouteProof("migrationplan.sorted_follow_up_commands_rejects_shell_control_tokens"), "Migration plans must reject shell-control follow-up commands.")}, + "native-evidence-guidance": { + directCLIRoute("internal/app/agent_workflow_command_test.go", "TestAgentWorkflowCLITruthTable", semanticRouteProof("agent_workflow_command.native_evidence_guidance_cli_truth_table"), "Native evidence guidance must preserve the public JSON, text, rejection, help, and color contract across the CLI truth table."), + 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."), + }, "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.")}, @@ -284,10 +297,25 @@ func (route commandCoverageRoute) sourceOracleBinding(command string) commandCov } func (route commandCoverageRoute) sourceOracleMarker(command string) string { - raw, err := json.Marshal(route.sourceOracleBinding(command)) + binding := route.sourceOracleBinding(command) + raw, err := stablejson.MarshalLayout(map[string]any{ + "commandRef": binding.CommandRef, + "expectedPublicOutcome": binding.ExpectedPublicOutcome, + "falsifierId": binding.FalsifierID, + "negativeCaseId": binding.NegativeCaseID, + "oracleId": binding.OracleID, + "oracleKind": binding.OracleKind, + "schemaVersion": binding.SchemaVersion, + "selector": binding.Selector, + "semanticRouteInvariantId": binding.SemanticRouteInvariantID, + "sourcePath": binding.SourcePath, + "testId": binding.TestID, + "wrongImplementationClassId": binding.WrongImplementationClass, + }, stablejson.LayoutCompact) if err != nil { panic(err) } + raw = raw[:len(raw)-1] digest := sha256.Sum256(raw) decimal := new(big.Int).SetBytes(digest[:]).Text(10) return "proofkit.command_coverage.source_oracle.v1." + strings.Repeat("0", 78-len(decimal)) + decimal diff --git a/internal/app/command_coverage_test.go b/internal/app/command_coverage_test.go index 05add1d..93c2c2d 100644 --- a/internal/app/command_coverage_test.go +++ b/internal/app/command_coverage_test.go @@ -466,7 +466,7 @@ func TestRequiredInputCommandsRejectMalformedCallerRecords(t *testing.T) { } func TestNoInputCommandsHaveCommandSpecificBehavior(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.102282482911729620466899441592793892640151459429551384318004264301951934933567") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.070498585436389324351139272251942930131041611602879828983669878551280280496659") t.Run("stack-preset", func(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer @@ -526,6 +526,8 @@ func noInputRuntimeSmokeArgs(command string) ([]string, bool) { return []string{"init", "--preset", "fresh"}, true case "json-report-cli-adapter-source": return []string{"json-report-cli-adapter-source", "--language", "typescript"}, true + case "native-evidence-guidance": + return []string{"native-evidence-guidance"}, true case "stack-preset": return []string{"stack-preset", "--preset", "typescript_workspace"}, true default: diff --git a/internal/app/command_descriptors.go b/internal/app/command_descriptors.go index 733f77c..3ddcd4c 100644 --- a/internal/app/command_descriptors.go +++ b/internal/app/command_descriptors.go @@ -22,6 +22,7 @@ const ( commandRunnerAdoptionContractEnvelope commandRunner = "adoption_contract_envelope" commandRunnerAdoptionDoctor commandRunner = "adoption_doctor" commandRunnerAdoptionWorkflow commandRunner = "adoption_workflow" + commandRunnerAgentWorkflow commandRunner = "agent_workflow" commandRunnerAgentRoute commandRunner = "agent_route" commandRunnerConformanceProfile commandRunner = "conformance_profile" commandRunnerContractEnvelope commandRunner = "contract_envelope" @@ -98,6 +99,7 @@ var commandDescriptors = []commandDescriptor{ command("binding-partition", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("bindingpartition")), command("branch-authority", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("branchauthority")), command("capability-map-admission", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("capabilitymapadmission")), + command("change-workflow-plan", commandInputRequired, flags("--agent-envelope", "--color", "--format", "--input", "--input-pointer"), modes("json", "text"), ownerDirs("changeworkflowplan"), withRunner(commandRunnerAgentWorkflow), withSemanticAppTests("TestAgentWorkflowCLITruthTable"), withAgentEnvelope(), withFlagChoices("--color", "auto", "never"), withFlagChoices("--format", "json", "text"), withSingleOccurrenceFlags("--agent-envelope", "--color", "--input", "--input-pointer")), command("changed-path-set", commandInputRequired, flags("--agent-envelope", "--input", "--input-pointer"), modes("json"), ownerDirs("changedpathset"), withRunner(commandRunnerPlanning), withAgentEnvelope()), command("completion-criteria", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("completioncriteria")), command("conformance-profile", commandInputRequired, flags("--format", "--input", "--input-pointer", "--list", "--profile", "--verify"), modes("json", "markdown"), ownerDirs("conformanceprofile"), withRunner(commandRunnerConformanceProfile), withExactlyOneOfFlags("--list", "--profile", "--verify"), withFlagValueRequirement("--format", "markdown", "--profile")), @@ -115,6 +117,7 @@ var commandDescriptors = []commandDescriptor{ command("json-report-cli-adapter-source", commandInputNone, flags("--format", "--language"), modes("json"), ownerDirs("jsonreportcliadaptersource"), withRunner(commandRunnerJSONReportCLIAdapterSource), withRequiredFlags("--language")), command("migration-parity-admission", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("migrationparityadmission")), 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("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")), @@ -175,6 +178,7 @@ var knownCommandRunners = map[commandRunner]struct{}{ commandRunnerAdoptionContractEnvelope: {}, commandRunnerAdoptionDoctor: {}, commandRunnerAdoptionWorkflow: {}, + commandRunnerAgentWorkflow: {}, commandRunnerAgentRoute: {}, commandRunnerConformanceProfile: {}, commandRunnerContractEnvelope: {}, @@ -220,7 +224,7 @@ func command(name string, input commandInputMode, allowedFlags []string, outputM option(&descriptor) } if slices.Contains(descriptor.allowedFlags, "--format") { - descriptor.singleOccurrenceFlags = []string{"--format"} + descriptor.singleOccurrenceFlags = sortedUniqueStrings(append(descriptor.singleOccurrenceFlags, "--format")) } explicitFlagChoices := cloneStringMap(descriptor.flagValueChoices) if metadata, ok := generatedCommandContractMetadataByName[name]; ok { @@ -349,6 +353,12 @@ func withSingleOccurrenceFlags(flags ...string) commandDescriptorOption { } } +func sortedUniqueStrings(values []string) []string { + result := cloneStrings(values) + sort.Strings(result) + return slices.Compact(result) +} + func flags(values ...string) []string { return cloneStrings(values) } diff --git a/internal/app/command_family_catalog_generated.go b/internal/app/command_family_catalog_generated.go index 4fc55b1..45d301f 100644 --- a/internal/app/command_family_catalog_generated.go +++ b/internal/app/command_family_catalog_generated.go @@ -1,13 +1,14 @@ // Code generated by internal/tools/commandfamilygen; DO NOT EDIT. package app -const commandFamilyCatalogSourceSHA256 = "5373aceca3b2d69d0d687e68b3801c6b914c2259f71569e476aa7a38383cd166" +const commandFamilyCatalogSourceSHA256 = "53119e4f643386395a29dc61e09ca1f3cc3bdf61075d1d27ade6eb46ee5fe4dd" func generatedCommandFamilyCatalog() commandFamilyCatalog { return commandFamilyCatalog{ CatalogID: "proofkit.command-families.v1", Families: []commandFamily{ {ID: "adoption-lifecycle", Label: "Adoption lifecycle", Purpose: "Select and assess repository adoption stages.", Commands: []string{"adoption-checklist", "adoption-doctor", "adoption-workflow-plan", "gradual-adoption", "gradual-adoption-bootstrap", "gradual-adoption-guidance", "init", "pilot-admission"}}, + {ID: "agent-workflow-planning", Label: "Agent workflow planning", Purpose: "Plan bounded engineering-change stages and expose repository-neutral native-evidence guidance.", Commands: []string{"change-workflow-plan", "native-evidence-guidance"}}, {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"}}, diff --git a/internal/app/command_help.go b/internal/app/command_help.go index cd641b9..0007f56 100644 --- a/internal/app/command_help.go +++ b/internal/app/command_help.go @@ -12,6 +12,10 @@ func isCommandHelpRequest(args []string) bool { return len(args) == 2 && args[0] != "help" && (args[1] == "--help" || args[1] == "-h") } +func hasCommandHelpFlag(args []string) bool { + return slices.Contains(args, "--help") || slices.Contains(args, "-h") +} + func commandUsage(descriptor commandDescriptor) string { return commandUsageWithRenderer(descriptor, cliexec.PathRenderer()) } diff --git a/internal/app/compact_contract_cli_test.go b/internal/app/compact_contract_cli_test.go index 564a448..e390794 100644 --- a/internal/app/compact_contract_cli_test.go +++ b/internal/app/compact_contract_cli_test.go @@ -452,3 +452,44 @@ func runCLIForJSON(t *testing.T, args []string, input string) map[string]any { } return record } + +func compactV2WireContract() map[string]any { + return map[string]any{ + "schema_version": json.Number("2"), + "authority_state": "caller_owned_declaration", + "contract_id": "proofkit.test.compact", + "contract_kind": "requirement_proof_route_declaration", + "normalization_profile": "proofkit.compact.declaration.v2", + "non_claims": []any{"Compact test input does not execute witnesses."}, + "surface_columns": []any{"surface_id", "required_environment_classes", "preconditioned_environment_classes"}, + "surfaces": []any{[]any{"proofkit.surface", []any{"local-go"}, []any{}}}, + "witness_columns": []any{"selector", "environment_classes", "verify_commands", "resolution_order_index"}, + "binding_columns": []any{"requirement_id", "surface_id", "scenario_id", "invariant_role", "owned_invariant", "blocking_status", "required_environment_classes", "positive_witness", "falsification_witness", "verify_commands", "declared_mutation_resistance_claim_id"}, + "bindings": []any{[]any{ + "REQ-PROOFKIT-COMPACT-001", + "proofkit.surface", + "proofkit.surface::scenario.compact", + "contract", + "proofkit.compact", + "blocking", + []any{"local-go"}, + []any{"tests/proofkit_positive_test.go::TestAcceptsCompactContract", []any{"local-go"}, []any{"go test ./..."}, json.Number("0")}, + []any{"tests/proofkit_falsification_test.go::TestRejectsCompactRegression", []any{"local-go"}, []any{"go test ./..."}, json.Number("1")}, + []any{"go test ./..."}, + "no_known_advisory_gap", + }}, + } +} + +func strictJSONObjectFromText(t *testing.T, text, context string) map[string]any { + t.Helper() + value, err := admission.DecodeJSON(strings.NewReader(text), int64(len(text))) + if err != nil { + t.Fatalf("decode %s: %v", context, err) + } + record, ok := value.(map[string]any) + if !ok { + t.Fatalf("%s must be an object", context) + } + return record +} diff --git a/internal/app/compact_contract_release_closure_test.go b/internal/app/compact_contract_release_closure_test.go index bba3029..78a2a2a 100644 --- a/internal/app/compact_contract_release_closure_test.go +++ b/internal/app/compact_contract_release_closure_test.go @@ -18,7 +18,10 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/tools/releasechange" ) -const compactV2WireManifestPath = "internal/app/testdata/compact-v2-wire-deltas.json" +const ( + compactV2ReleaseRecordPath = "internal/app/testdata/compact-v2-release-change-record.json" + compactV2WireManifestPath = "internal/app/testdata/compact-v2-wire-deltas.json" +) type compactWireManifest struct { Baseline compactWireBaseline `json:"baseline"` @@ -107,7 +110,7 @@ var expectedCompactMetadataFreshnessDirections = []string{ func TestCompactV2WireDeltaReleaseAndParentContractClosure(t *testing.T) { manifest := readCompactWireManifest(t) root := repoRoot(t) - record, err := releasechange.Read(filepath.Join(root, releasechange.RecordPath)) + record, err := releasechange.Read(filepath.Join(root, compactV2ReleaseRecordPath)) if err != nil { t.Fatalf("read release change record: %v", err) } @@ -135,11 +138,7 @@ func TestCompactV2WireDeltaReleaseAndParentContractClosure(t *testing.T) { migrationSteps[digest] = step } - contract := readCLIContract(t) - commands := make(map[string]cliContractCommand, len(contract.Commands)) - for _, command := range contract.Commands { - commands[command.Command] = command - } + frozenV2 := readCompactV2WireObservationDocument(t) referencedBreaking := map[string]struct{}{} referencedSteps := map[string]struct{}{} previousDeltaID := "" @@ -166,7 +165,7 @@ func TestCompactV2WireDeltaReleaseAndParentContractClosure(t *testing.T) { referencedSteps[delta.MigrationStepDigest] = struct{}{} } if delta.Class == "parent_contract" || delta.Class == "metadata_freshness" { - assertCompactParentContractDelta(t, commands, delta) + assertCompactParentContractDelta(t, frozenV2.CLIContractDirectionDigests, delta) } } assertCompactParentContractChangeSetClosure(t, manifest) @@ -378,35 +377,14 @@ func assertSHA256Ref(t *testing.T, value, context string) { } } -func assertCompactParentContractDelta(t *testing.T, commands map[string]cliContractCommand, delta compactWireDelta) { +func assertCompactParentContractDelta(t *testing.T, directionDigests map[string]string, delta compactWireDelta) { t.Helper() - command, ok := commands[delta.Surface] + actual, ok := directionDigests[delta.Surface+"|"+delta.Direction] if !ok { - t.Fatalf("parent-contract delta %s references unknown command %s", delta.DeltaID, delta.Surface) - } - raw := command.InputContract - if delta.Direction == "output" { - raw = command.OutputContract - } else if delta.Direction != "input" { - t.Fatalf("parent-contract delta %s has invalid direction %s", delta.DeltaID, delta.Direction) - } - if raw == nil { - t.Fatalf("parent-contract delta %s references absent %s contract", delta.DeltaID, delta.Direction) - } - content, err := json.Marshal(raw) - if err != nil { - t.Fatalf("encode parent-contract delta %s source: %v", delta.DeltaID, err) - } - actual, err := admission.DecodeJSON(bytes.NewReader(content), int64(len(content))) - if err != nil { - t.Fatalf("admit parent-contract delta %s source: %v", delta.DeltaID, err) - } - encoded, err := stablejson.Marshal(actual) - if err != nil { - t.Fatalf("encode parent-contract delta %s: %v", delta.DeltaID, err) + t.Fatalf("parent-contract delta %s references absent frozen direction %s|%s", delta.DeltaID, delta.Surface, delta.Direction) } - if delta.New.ValueSHA256 != sha256Text(string(encoded)) { - t.Fatalf("parent-contract delta %s digest=%s want %s", delta.DeltaID, sha256Text(string(encoded)), delta.New.ValueSHA256) + if delta.New.ValueSHA256 != actual { + t.Fatalf("parent-contract delta %s frozen digest=%s want %s", delta.DeltaID, actual, delta.New.ValueSHA256) } } @@ -428,7 +406,7 @@ func assertCompactParentContractChangeSetClosure(t *testing.T, manifest compactW if got := sha256Text(string(encodedBaseline)); got != manifest.Baseline.CLIContractDirectionDigestsSHA256 { t.Fatalf("baseline CLI contract direction digest=%s want %s", got, manifest.Baseline.CLIContractDirectionDigestsSHA256) } - current := currentCLIContractDirectionDigests(t) + current := readCompactV2WireObservationDocument(t).CLIContractDirectionDigests allKeys := map[string]struct{}{} for key := range baseline.CLIContractDirectionDigests { allKeys[key] = struct{}{} @@ -458,7 +436,7 @@ func assertCompactParentContractChangeSetClosure(t *testing.T, manifest compactW func assertCompactParentContractSemanticClassification(t *testing.T, manifest compactWireManifest) { t.Helper() oldObservations := readCompactV1WireObservations(t) - currentObservations := currentCompactV2WireObservations(t) + currentObservations := readCompactV2WireObservations(t) metadataOnly := []string{} for _, delta := range manifest.Deltas { if delta.Class != "parent_contract" && delta.Class != "metadata_freshness" { @@ -508,35 +486,6 @@ func compactWithoutNativeSourceDigest(t *testing.T, value any, context string) a return record } -func currentCLIContractDirectionDigests(t *testing.T) map[string]string { - t.Helper() - result := map[string]string{} - for _, command := range readCLIContract(t).Commands { - for _, item := range []struct { - direction string - value any - }{{direction: "input", value: command.InputContract}, {direction: "output", value: command.OutputContract}} { - if item.value == nil { - continue - } - content, err := json.Marshal(item.value) - if err != nil { - t.Fatalf("encode %s %s contract: %v", command.Command, item.direction, err) - } - canonical, err := admission.DecodeJSON(bytes.NewReader(content), int64(len(content))) - if err != nil { - t.Fatalf("admit %s %s contract: %v", command.Command, item.direction, err) - } - encoded, err := stablejson.Marshal(canonical) - if err != nil { - t.Fatalf("canonicalize %s %s contract: %v", command.Command, item.direction, err) - } - result[command.Command+"|"+item.direction] = sha256Text(string(encoded)) - } - } - return result -} - func validSHA256Digest(value string) bool { if len(value) != len("sha256:")+sha256.Size*2 || !strings.HasPrefix(value, "sha256:") { return false diff --git a/internal/app/compact_contract_wire_observations_test.go b/internal/app/compact_contract_wire_observations_test.go index 85e7643..6c37541 100644 --- a/internal/app/compact_contract_wire_observations_test.go +++ b/internal/app/compact_contract_wire_observations_test.go @@ -2,24 +2,24 @@ package app import ( "bytes" - "context" "encoding/json" "fmt" "os" "path/filepath" "sort" + "strconv" "strings" "testing" - "time" - "github.com/research-engineering/agentic-proofkit/internal/command/requirementbrowser" "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" - "github.com/research-engineering/agentic-proofkit/internal/kernel/compactproofcontract" "github.com/research-engineering/agentic-proofkit/internal/kernel/jsonpointer" "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" ) -const compactV1WireObservationsPath = "internal/app/testdata/compact-v1-wire-observations.json" +const ( + compactV1WireObservationsPath = "internal/app/testdata/compact-v1-wire-observations.json" + compactV2WireObservationsPath = "internal/app/testdata/compact-v2-wire-observations.json" +) type compactWireObservations struct { CLIContractDirectionDigests map[string]string `json:"cliContractDirectionDigests"` @@ -98,10 +98,10 @@ var expectedCompactWireObservationKeys = []string{ "test-evidence-inventory|output|union", } -func TestCompactV2WireDeltasResolveAgainstFrozenAndCurrentObservations(t *testing.T) { +func TestCompactV2WireDeltasResolveAgainstFrozenVersionEdgeObservations(t *testing.T) { manifest := readCompactWireManifest(t) oldObservations := readCompactV1WireObservations(t) - currentObservations := currentCompactV2WireObservations(t) + currentObservations := readCompactV2WireObservations(t) baselineBytes, err := stablejson.Marshal(oldObservations) if err != nil { t.Fatalf("encode normalized compact v1 observations: %v", err) @@ -135,13 +135,22 @@ func TestCompactV2WireDeltasResolveAgainstFrozenAndCurrentObservations(t *testin func readCompactV1WireObservations(t *testing.T) map[string]any { t.Helper() - decoded := readCompactV1WireObservationDocument(t) + return compactWireObservationMap(t, readCompactV1WireObservationDocument(t), "compact v1") +} + +func readCompactV2WireObservations(t *testing.T) map[string]any { + t.Helper() + return compactWireObservationMap(t, readCompactV2WireObservationDocument(t), "compact v2") +} + +func compactWireObservationMap(t *testing.T, decoded compactWireObservations, context string) map[string]any { + t.Helper() result := make(map[string]any, len(decoded.Observations)) previous := "" for _, observation := range decoded.Observations { key := compactWireObservationKey(observation.Surface, observation.Direction, observation.Variant) if previous != "" && previous >= key { - t.Fatalf("compact v1 wire observations must be sorted and unique: %s before %s", previous, key) + t.Fatalf("%s wire observations must be sorted and unique: %s before %s", context, previous, key) } previous = key result[key] = observation.Document @@ -151,410 +160,59 @@ func readCompactV1WireObservations(t *testing.T) map[string]any { func readCompactV1WireObservationDocument(t *testing.T) compactWireObservations { t.Helper() - content, err := os.ReadFile(filepath.Join(repoRoot(t), compactV1WireObservationsPath)) + return readCompactWireObservationDocument(t, compactV1WireObservationsPath, 1, "compact v1") +} + +func readCompactV2WireObservationDocument(t *testing.T) compactWireObservations { + t.Helper() + return readCompactWireObservationDocument(t, compactV2WireObservationsPath, 2, "compact v2") +} + +func readCompactWireObservationDocument(t *testing.T, relativePath string, schemaVersion int, context string) compactWireObservations { + t.Helper() + content, err := os.ReadFile(filepath.Join(repoRoot(t), relativePath)) if err != nil { - t.Fatalf("read compact v1 wire observations: %v", err) + t.Fatalf("read %s wire observations: %v", context, err) } value, err := admission.DecodeJSON(bytes.NewReader(content), int64(len(content))) if err != nil { - t.Fatalf("admit compact v1 wire observations: %v", err) + t.Fatalf("admit %s wire observations: %v", context, err) } record, ok := value.(map[string]any) if !ok { - t.Fatal("compact v1 wire observations must be an object") + t.Fatalf("%s wire observations must be an object", context) } - assertExactObjectKeys(t, record, []string{"cliContractDirectionDigests", "observations", "schemaVersion"}, "compact v1 wire observations") - if number, ok := record["schemaVersion"].(json.Number); !ok || number.String() != "1" { - t.Fatalf("compact v1 wire observations schemaVersion=%v want 1", record["schemaVersion"]) + assertExactObjectKeys(t, record, []string{"cliContractDirectionDigests", "observations", "schemaVersion"}, context+" wire observations") + if number, ok := record["schemaVersion"].(json.Number); !ok || number.String() != strconv.Itoa(schemaVersion) { + t.Fatalf("%s wire observations schemaVersion=%v want %d", context, record["schemaVersion"], schemaVersion) } raw, ok := record["observations"].([]any) if !ok || len(raw) == 0 { - t.Fatal("compact v1 wire observations must contain observations") + t.Fatalf("%s wire observations must contain observations", context) } for index, value := range raw { observation, ok := value.(map[string]any) if !ok { - t.Fatalf("compact v1 wire observation %d must be an object", index) + t.Fatalf("%s wire observation %d must be an object", context, index) } - assertExactObjectKeys(t, observation, []string{"direction", "document", "surface", "variant"}, fmt.Sprintf("compact v1 wire observation %d", index)) + assertExactObjectKeys(t, observation, []string{"direction", "document", "surface", "variant"}, fmt.Sprintf("%s wire observation %d", context, index)) } decoded, err := admission.DecodeTypedJSON[compactWireObservations](bytes.NewReader(content), int64(len(content))) if err != nil { - t.Fatalf("decode compact v1 wire observations: %v", err) + t.Fatalf("decode %s wire observations: %v", context, err) } if len(decoded.CLIContractDirectionDigests) == 0 { - t.Fatal("compact v1 wire observations must contain CLI contract direction digests") + t.Fatalf("%s wire observations must contain CLI contract direction digests", context) } for key, value := range decoded.CLIContractDirectionDigests { parts := strings.Split(key, "|") if len(parts) != 2 || (parts[1] != "input" && parts[1] != "output") || !validSHA256Digest(value) { - t.Fatalf("compact v1 CLI contract direction digest %s=%q is invalid", key, value) + t.Fatalf("%s CLI contract direction digest %s=%q is invalid", context, key, value) } } return decoded } -func currentCompactV2WireObservations(t *testing.T) map[string]any { - t.Helper() - compactInput := compactV2WireContract() - contract, err := compactproofcontract.Admit(compactInput) - if err != nil { - t.Fatalf("admit current compact wire contract: %v", err) - } - resolver, err := contract.ResolverProjection(compactproofcontract.ResolverOptions{LocalEnvironmentClasses: []string{"local-go"}}) - if err != nil { - t.Fatalf("build current compact resolver observation: %v", err) - } - adoptionInput := strictJSONObjectFromText(t, cliAdoptionContractEnvelopeInput(), "current adoption aggregate input") - pilotDirect := cliPilotInput("proofkit.cli.pilot.first", false) - pilotEnvelope := strictJSONObjectFromText(t, cliPilotContractEnvelopeInput("all"), "current pilot envelope input") - conformanceInput := strictJSONObjectFromText(t, cliConformanceProfileInput(), "current conformance input") - conformanceOutput := runCLIForJSON(t, []string{"conformance-profile", "--input", "-", "--profile", "local"}, cliJSON(conformanceInput)) - - impactFixture := strings.Replace( - cliImpactInputComposeInput(), - `"paths":["docs/specs/proofkit-cli-impact/requirements.v1.json"]`, - `"paths":["docs/specs/proofkit-cli-impact/requirements.v1.json","internal/app/cli_abi_test.go"]`, - 1, - ) - impactComposeInput := strictJSONObjectFromText(t, impactFixture, "current impact compose input") - impactInput := runCLIForJSON(t, []string{"requirement-impact-input-compose", "--input", "-"}, cliJSON(impactComposeInput)) - impactOutput := runCLIForJSON(t, []string{"impact", "--input", "-"}, cliJSON(impactInput)) - - sourceInput := cliProofSourceSetInput(t, "canonical_contract") - sourceOutput := runAppJSON(t, []string{"requirement-proof-source-set", "--input", "-"}, sourceInput) - sourceFragment := strictJSONObjectFromText(t, sourceInput["sources"].([]any)[0].(map[string]any)["text"].(string), "current compact source fragment") - sourceRow := compactSourceRoleObservation(t, sourceInput["sourceSet"].(map[string]any)) - - coverageComposeInput := strictJSONObjectFromText(t, cliCoverageInputComposeInput(), "current coverage compose input") - coverageInput := runCLIForJSON(t, []string{"requirement-coverage-input-compose", "--input", "-"}, cliJSON(coverageComposeInput)) - coverageOutput := runCLIForJSON(t, []string{"requirement-coverage-view", "--input", "-", "--format", "json"}, cliJSON(coverageInput)) - coverageScenario := firstCompactCoverageScenario(t, coverageOutput) - - inventoryInput := strictJSONObjectFromText(t, cliProofBindingDerivedInventoryInput(), "current proof-binding inventory input") - inventoryOutput := runCLIForJSON(t, []string{"test-evidence-inventory", "--input", "-", "--projection", "proof-binding-derived", "--normalized-inventory"}, cliJSON(inventoryInput)) - proofViewOutput := runCLIForJSON(t, []string{"requirement-proof-view", "--input", "-", "--format", "json", "--local-environment-class", "local-go"}, cliJSON(compactInput)) - - result := map[string]any{ - compactWireObservationKey("adoption-contract-envelope", "input", "aggregate"): adoptionInput, - compactWireObservationKey("compact-contract", "input", "direct"): compactInput, - compactWireObservationKey("compact-resolver", "output", "resolver"): resolver, - compactWireObservationKey("conformance-profile", "input", "direct"): conformanceInput, - compactWireObservationKey("conformance-profile", "output", "profile"): conformanceOutput, - compactWireObservationKey("impact", "input", "direct"): impactInput, - compactWireObservationKey("impact", "output", "direct"): impactOutput, - compactWireObservationKey("pilot-admission", "input", "direct"): pilotDirect, - compactWireObservationKey("pilot-admission", "input", "envelope"): pilotEnvelope, - compactWireObservationKey("requirement-coverage-input-compose", "input", "wrapper"): coverageComposeInput, - compactWireObservationKey("requirement-coverage-input-compose", "output", "wrapper"): coverageInput, - compactWireObservationKey("requirement-coverage-view", "input", "wrapper"): coverageInput, - compactWireObservationKey("requirement-coverage-view", "output", "compact-scenario"): coverageScenario, - compactWireObservationKey("requirement-coverage-view", "output", "root"): coverageOutput, - compactWireObservationKey("requirement-impact-input-compose", "input", "wrapper"): impactComposeInput, - compactWireObservationKey("requirement-impact-input-compose", "output", "wrapper"): impactInput, - compactWireObservationKey("requirement-proof-resolver", "output", "resolver"): resolver, - compactWireObservationKey("requirement-proof-source-set", "input", "canonical-envelope"): sourceInput["canonicalEnvelope"], - compactWireObservationKey("requirement-proof-source-set", "input", "fragment"): sourceFragment, - compactWireObservationKey("requirement-proof-source-set", "input", "source-row"): sourceRow, - compactWireObservationKey("requirement-proof-source-set", "input", "source-set"): sourceInput["sourceSet"], - compactWireObservationKey("requirement-proof-source-set", "input", "wrapper"): sourceInput, - compactWireObservationKey("requirement-proof-source-set", "output", "wrapper"): sourceOutput, - compactWireObservationKey("requirement-proof-view", "input", "compact"): compactInput, - compactWireObservationKey("requirement-proof-view", "output", "compact"): proofViewOutput, - compactWireObservationKey("test-evidence-inventory", "input", "proof-binding"): inventoryInput, - compactWireObservationKey("test-evidence-inventory", "output", "normalized"): inventoryOutput, - } - for key, observation := range currentCompactParentContractObservations(t) { - result[key] = observation - } - for key, observation := range currentCompactContextChainObservations(t) { - result[key] = observation - } - return result -} - -func currentCompactContextChainObservations(t *testing.T) map[string]any { - t.Helper() - root := t.TempDir() - coverageCompose := strictJSONObjectFromText(t, cliCoverageInputComposeInput(), "compact context coverage compose input") - coverageInput := runCLIForJSON(t, []string{"requirement-coverage-input-compose", "--input", "-"}, cliJSON(coverageCompose)) - requirementSource := coverageCompose["requirementSource"].(map[string]any) - sourceID := requirementSource["sourceId"].(string) - tree := map[string]any{ - "schemaVersion": json.Number("2"), "treeId": "proofkit.compact.context-tree", "rootNodeId": "proofkit.compact.context-root", - "callerAnnotations": []any{}, "edges": []any{}, "overlays": []any{}, - "nodes": []any{map[string]any{ - "nodeId": "proofkit.compact.context-root", "nodeKind": "meta_spec", "label": "Compact context fixture", "displayOrder": json.Number("1"), - "callerAnnotations": []any{}, - "sourceRefs": []any{map[string]any{ - "sourceRefId": "proofkit.compact.context-root.requirements", "sourceRefKind": "source_id", - "sourceRole": "requirements", "sourceId": sourceID, - }}, - }}, - } - writeCLIJSONFixture(t, root, "proofkit/spec-tree.json", tree) - writeCLIJSONFixture(t, root, "docs/specs/compact/requirements.v1.json", requirementSource) - writeCLIJSONFixture(t, root, "proofkit/coverage-input.json", coverageInput) - catalog := map[string]any{ - "schemaVersion": json.Number("1"), "catalogId": "proofkit.compact.context", - "specTree": map[string]any{"path": "proofkit/spec-tree.json"}, - "requirementSources": []any{map[string]any{ - "nodeId": "proofkit.compact.context-root", "path": "docs/specs/compact/requirements.v1.json", - }}, - "coverage": map[string]any{"path": "proofkit/coverage-input.json"}, - } - base := runAppJSON(t, []string{"requirement-context-compose", "--input", "-", "--repo-root", root}, catalog) - sliceInput := map[string]any{ - "schemaVersion": json.Number("1"), "sliceId": "proofkit.compact.context.slice", "context": base, - "query": map[string]any{"profile": "coverage", "requirementIds": []any{"REQ-PROOFKIT-CLI-COVERAGE-001"}}, - } - sliceOutput := runAppJSON(t, []string{"requirement-context-slice", "--input", "-"}, sliceInput) - - requirements := requirementSource["requirements"].([]any) - requirements[0].(map[string]any)["invariant"] = "Compact context fixture changed invariant." - writeCLIJSONFixture(t, root, "docs/specs/compact/requirements.v1.json", requirementSource) - current := runAppJSON(t, []string{"requirement-context-compose", "--input", "-", "--repo-root", root}, catalog) - diffInput := map[string]any{ - "schemaVersion": json.Number("2"), "diffId": "proofkit.compact.context.diff", - "baseContext": base, "currentContext": current, - } - diffOutput := runAppJSON(t, []string{"requirement-semantic-diff", "--input", "-"}, diffInput) - graphInput := map[string]any{ - "schemaVersion": json.Number("2"), "graphId": "proofkit.compact.context.graph", "context": current, - } - graphOutput := runAppJSON(t, []string{"requirement-traceability-graph", "--input", "-"}, graphInput) - workspaceInput := map[string]any{ - "schemaVersion": json.Number("2"), "workspaceId": "proofkit.compact.context.workspace", "context": current, - "diffInput": diffInput, "graphInput": graphInput, - } - browserHandle, err := requirementbrowser.StartServer(workspaceInput, requirementbrowser.Options{Host: "127.0.0.1", Port: 0, PortSet: true, View: "workspace"}) - if err != nil { - t.Fatalf("start compact context workspace server: %v", err) - } - closeCompactObservationServer(t, browserHandle) - - baseCoverage := jsonObjectField(t, jsonObjectField(t, base, "projections"), "coverage") - currentCoverage := jsonObjectField(t, jsonObjectField(t, current, "projections"), "coverage") - sliceCoverage := jsonObjectField(t, jsonObjectField(t, sliceOutput, "projections"), "coverage") - baseIdentity := compactCoverageIdentityObservation(t, baseCoverage) - assertCompactIdentityObservation(t, sliceCoverage, baseIdentity, "context slice output") - assertCompactIdentityObservation(t, currentCoverage, baseIdentity, "current context output") - if diffOutput["baseSnapshotId"] != base["snapshotId"] || diffOutput["currentSnapshotId"] != current["snapshotId"] { - t.Fatalf("semantic diff output lost context identity: base=%v current=%v", diffOutput["baseSnapshotId"], diffOutput["currentSnapshotId"]) - } - if graphOutput["snapshotId"] != current["snapshotId"] { - t.Fatalf("traceability graph output snapshotId=%v want %v", graphOutput["snapshotId"], current["snapshotId"]) - } - if browserHandle.SnapshotID != current["snapshotId"] { - t.Fatalf("browser runtime snapshotId=%v want %v", browserHandle.SnapshotID, current["snapshotId"]) - } - baseCoverageDigest := compactWireValueDigest(t, baseCoverage) - return map[string]any{ - compactWireObservationKey("requirement-browser-server", "output", "workspace-runtime"): map[string]any{ - "snapshotId": browserHandle.SnapshotID, - }, - compactWireObservationKey("requirement-context-compose", "output", "coverage-runtime"): map[string]any{ - "coverageSha256": baseCoverageDigest, "identities": baseIdentity, - }, - compactWireObservationKey("requirement-context-slice", "output", "coverage-runtime"): map[string]any{ - "coverageSha256": compactWireValueDigest(t, sliceCoverage), "identities": compactCoverageIdentityObservation(t, sliceCoverage), - }, - compactWireObservationKey("requirement-semantic-diff", "output", "coverage-runtime"): map[string]any{ - "baseSnapshotId": diffOutput["baseSnapshotId"], "currentSnapshotId": diffOutput["currentSnapshotId"], - "outputSha256": compactWireValueDigest(t, diffOutput), - }, - compactWireObservationKey("requirement-traceability-graph", "output", "coverage-runtime"): map[string]any{ - "outputSha256": compactWireValueDigest(t, graphOutput), "snapshotId": graphOutput["snapshotId"], - }, - } -} - -func compactCoverageIdentityObservation(t *testing.T, value any) map[string]any { - t.Helper() - bindingIDs := map[string]struct{}{} - routeIDs := map[string]struct{}{} - collectCompactIdentityValues(value, bindingIDs, routeIDs) - if len(bindingIDs) == 0 || len(routeIDs) == 0 { - t.Fatalf("compact coverage projection has no stable identities: bindings=%v routes=%v", sortedSetKeys(bindingIDs), sortedSetKeys(routeIDs)) - } - return map[string]any{ - "bindingRecordIds": stringsToAny(sortedSetKeys(bindingIDs)), - "witnessRouteIds": stringsToAny(sortedSetKeys(routeIDs)), - } -} - -func collectCompactIdentityValues(value any, bindingIDs, routeIDs map[string]struct{}) { - switch typed := value.(type) { - case map[string]any: - for key, child := range typed { - if text, ok := child.(string); ok { - switch key { - case "bindingRecordId": - bindingIDs[text] = struct{}{} - case "witnessRouteId": - routeIDs[text] = struct{}{} - } - } - collectCompactIdentityValues(child, bindingIDs, routeIDs) - } - case []any: - for _, child := range typed { - collectCompactIdentityValues(child, bindingIDs, routeIDs) - } - } -} - -func assertCompactIdentityObservation(t *testing.T, value any, want map[string]any, context string) { - t.Helper() - got := compactCoverageIdentityObservation(t, value) - if !compactJSONEqual(got, want) { - t.Fatalf("%s identity projection=%v want %v", context, got, want) - } -} - -func stringsToAny(values []string) []any { - result := make([]any, len(values)) - for index, value := range values { - result[index] = value - } - return result -} - -func closeCompactObservationServer(t *testing.T, handle requirementbrowser.ServerHandle) { - t.Helper() - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := handle.Close(ctx); err != nil { - t.Fatalf("close compact context workspace server: %v", err) - } - select { - case err := <-handle.Done(): - if err != nil { - t.Fatalf("wait for compact context workspace server: %v", err) - } - case <-ctx.Done(): - t.Fatalf("wait for compact context workspace server: %v", ctx.Err()) - } -} - -func compactWireValueDigest(t *testing.T, value any) string { - t.Helper() - content, err := json.Marshal(value) - if err != nil { - t.Fatalf("encode compact nested wire value: %v", err) - } - return sha256Text(string(content)) -} - -func currentCompactParentContractObservations(t *testing.T) map[string]any { - t.Helper() - contract := readCLIContract(t) - commands := make(map[string]cliContractCommand, len(contract.Commands)) - for _, command := range contract.Commands { - commands[command.Command] = command - } - result := map[string]any{} - for _, key := range expectedCompactWireObservationKeys { - parts := strings.Split(key, "|") - if len(parts) != 3 || parts[2] != "union" { - continue - } - command, ok := commands[parts[0]] - if !ok { - t.Fatalf("expected parent observation references unknown command %s", parts[0]) - } - raw := command.InputContract - if parts[1] == "output" { - raw = command.OutputContract - } - if raw == nil { - t.Fatalf("expected parent observation %s has no contract", key) - } - content, err := json.Marshal(raw) - if err != nil { - t.Fatalf("encode expected parent observation %s: %v", key, err) - } - value, err := admission.DecodeJSON(bytes.NewReader(content), int64(len(content))) - if err != nil { - t.Fatalf("admit expected parent observation %s: %v", key, err) - } - record := value.(map[string]any) - result[key] = map[string]any{"contract": record} - } - return result -} - -func compactV2WireContract() map[string]any { - return map[string]any{ - "schema_version": json.Number("2"), - "authority_state": "caller_owned_declaration", - "contract_id": "proofkit.test.compact", - "contract_kind": "requirement_proof_route_declaration", - "normalization_profile": "proofkit.compact.declaration.v2", - "non_claims": []any{"Compact test input does not execute witnesses."}, - "surface_columns": []any{"surface_id", "required_environment_classes", "preconditioned_environment_classes"}, - "surfaces": []any{[]any{"proofkit.surface", []any{"local-go"}, []any{}}}, - "witness_columns": []any{"selector", "environment_classes", "verify_commands", "resolution_order_index"}, - "binding_columns": []any{"requirement_id", "surface_id", "scenario_id", "invariant_role", "owned_invariant", "blocking_status", "required_environment_classes", "positive_witness", "falsification_witness", "verify_commands", "declared_mutation_resistance_claim_id"}, - "bindings": []any{[]any{ - "REQ-PROOFKIT-COMPACT-001", - "proofkit.surface", - "proofkit.surface::scenario.compact", - "contract", - "proofkit.compact", - "blocking", - []any{"local-go"}, - []any{"tests/proofkit_positive_test.go::TestAcceptsCompactContract", []any{"local-go"}, []any{"go test ./..."}, json.Number("0")}, - []any{"tests/proofkit_falsification_test.go::TestRejectsCompactRegression", []any{"local-go"}, []any{"go test ./..."}, json.Number("1")}, - []any{"go test ./..."}, - "no_known_advisory_gap", - }}, - } -} - -func strictJSONObjectFromText(t *testing.T, text, context string) map[string]any { - t.Helper() - value, err := admission.DecodeJSON(strings.NewReader(text), int64(len(text))) - if err != nil { - t.Fatalf("decode %s: %v", context, err) - } - record, ok := value.(map[string]any) - if !ok { - t.Fatalf("%s must be an object", context) - } - return record -} - -func compactSourceRoleObservation(t *testing.T, sourceSet map[string]any) map[string]any { - t.Helper() - columns := sourceSet["source_columns"].([]any) - rows := sourceSet["sources"].([]any) - if len(rows) == 0 { - t.Fatal("current source set has no source rows") - } - row := rows[0].([]any) - for index, raw := range columns { - if raw == "role" { - return map[string]any{"role": row[index]} - } - } - t.Fatal("current source set has no role column") - return nil -} - -func firstCompactCoverageScenario(t *testing.T, report map[string]any) map[string]any { - t.Helper() - requirements := report["requirementCoverage"].([]any) - if len(requirements) == 0 { - t.Fatal("current compact coverage report has no requirements") - } - scenarios := requirements[0].(map[string]any)["scenarios"].([]any) - if len(scenarios) == 0 { - t.Fatal("current compact coverage report has no scenarios") - } - return scenarios[0].(map[string]any) -} - func assertCompactObservedState(t *testing.T, delta compactWireDelta, side string, document any, want compactWireState) { t.Helper() actual, exists := selectCompactWirePointer(t, document, delta.JSONPointer, want.Presence == "absent") diff --git a/internal/app/native_evidence_guidance_command.go b/internal/app/native_evidence_guidance_command.go new file mode 100644 index 0000000..470dd88 --- /dev/null +++ b/internal/app/native_evidence_guidance_command.go @@ -0,0 +1,38 @@ +package app + +import ( + "io" + + "github.com/research-engineering/agentic-proofkit/internal/command/nativeevidenceguidance" +) + +func runNativeEvidenceGuidance(options agentWorkflowArgs, stdout io.Writer, stderr io.Writer, capabilities PresentationCapabilities) int { + if options.format == "json" { + guidance, err := nativeevidenceguidance.Build() + if err != nil { + return writeJSON(nil, 0, err, stdout, stderr) + } + return writeJSON(guidance.JSONValue(), 0, nil, stdout, stderr) + } + view, err := nativeEvidenceGuidanceTerminalText() + if err != nil { + return writeText("", 1, err, stdout, stderr) + } + output, err := renderTerminalText(view, options.color, capabilities) + return writeText(output, 0, err, stdout, stderr) +} + +func nativeEvidenceGuidanceTerminalText() (terminalText, error) { + lines, err := nativeevidenceguidance.TextProjection() + if err != nil { + return terminalText{}, err + } + tokens := make([]terminalTextToken, 0, len(lines)*2) + for _, line := range lines { + tokens = append(tokens, + terminalTextToken{kind: terminalTokenLabel, text: line.Label}, + terminalTextToken{kind: terminalTokenPlain, text: ": " + line.Value + "\n"}, + ) + } + return newTerminalText(tokens...), nil +} diff --git a/internal/app/requirement_context_cli_test.go b/internal/app/requirement_context_cli_test.go index d85258e..0ecf709 100644 --- a/internal/app/requirement_context_cli_test.go +++ b/internal/app/requirement_context_cli_test.go @@ -21,10 +21,10 @@ import ( ) func TestRequirementContextCommandsComposeThroughWholeCLI(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.015005723751943372102052485735054220409079175497974852772238314306849909704926") - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.049495764657602855686625469543032083639688769989400372107516147133048789250421") - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.057489097167034237791564307746461892229693375121512105387246196474554343257848") - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.011976964944325039688029974963654018529660938760094284739788591455188270918568") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.097109304805955804866101416335094065400345464281933061498534528351192063227949") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.057550300858348527262188220465911463572498394140028790265329600017071610078423") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.113360119918311296442974485154252820603874120183883324682989554068354518930712") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.064478426397925014174625523910101201444776310349803627924470071707127905708426") root := t.TempDir() tree := map[string]any{ "schemaVersion": json.Number("2"), "treeId": "consumer.spec-tree", "rootNodeId": "consumer.root", diff --git a/internal/app/terminal_style.go b/internal/app/terminal_style.go new file mode 100644 index 0000000..c45ebb6 --- /dev/null +++ b/internal/app/terminal_style.go @@ -0,0 +1,79 @@ +package app + +import ( + "fmt" + "strings" +) + +const ( + ansiReset = "\x1b[0m" + ansiLabel = "\x1b[1;36m" + ansiSecondary = "\x1b[36m" +) + +// PresentationCapabilities are immutable process-boundary facts. Library +// callers use the zero value, which never emits terminal control sequences. +type PresentationCapabilities struct { + StdoutIsTTY bool + NoColorPresent bool +} + +type terminalTokenKind string + +const ( + terminalTokenPlain terminalTokenKind = "plain" + terminalTokenLabel terminalTokenKind = "label" + terminalTokenSecondary terminalTokenKind = "secondary" +) + +type terminalTextToken struct { + kind terminalTokenKind + text string +} + +type terminalText struct { + tokens []terminalTextToken +} + +func newTerminalText(tokens ...terminalTextToken) terminalText { + return terminalText{tokens: append([]terminalTextToken(nil), tokens...)} +} + +func renderTerminalText(value terminalText, colorMode string, capabilities PresentationCapabilities) (string, error) { + useANSI := colorMode == "auto" && capabilities.StdoutIsTTY && !capabilities.NoColorPresent + if colorMode != "never" && colorMode != "auto" { + return "", fmt.Errorf("unsupported color mode") + } + var builder strings.Builder + for _, token := range value.tokens { + style, err := terminalTokenStyle(token.kind) + if err != nil { + return "", err + } + if useANSI && style != "" && token.text != "" { + builder.WriteString(style) + builder.WriteString(token.text) + builder.WriteString(ansiReset) + continue + } + builder.WriteString(token.text) + } + return builder.String(), nil +} + +func plainTerminalText(value terminalText) (string, error) { + return renderTerminalText(value, "never", PresentationCapabilities{}) +} + +func terminalTokenStyle(kind terminalTokenKind) (string, error) { + switch kind { + case terminalTokenPlain: + return "", nil + case terminalTokenLabel: + return ansiLabel, nil + case terminalTokenSecondary: + return ansiSecondary, nil + default: + return "", fmt.Errorf("unsupported terminal token kind") + } +} diff --git a/internal/app/terminal_style_test.go b/internal/app/terminal_style_test.go new file mode 100644 index 0000000..ff749f5 --- /dev/null +++ b/internal/app/terminal_style_test.go @@ -0,0 +1,59 @@ +package app + +import ( + "strings" + "testing" +) + +func TestTerminalStyleCapabilityRelation(t *testing.T) { + view := newTerminalText( + terminalTextToken{kind: terminalTokenLabel, text: "Action"}, + terminalTextToken{kind: terminalTokenPlain, text: ": inspect\n"}, + terminalTextToken{kind: terminalTokenSecondary, text: "Next"}, + terminalTextToken{kind: terminalTokenPlain, text: ": verify\n"}, + ) + plain, err := plainTerminalText(view) + if err != nil { + t.Fatalf("plainTerminalText() error = %v", err) + } + for _, test := range []struct { + name string + mode string + capabilities PresentationCapabilities + wantANSI bool + }{ + {name: "default never on tty", mode: "never", capabilities: PresentationCapabilities{StdoutIsTTY: true}}, + {name: "auto pipe", mode: "auto"}, + {name: "auto tty", mode: "auto", capabilities: PresentationCapabilities{StdoutIsTTY: true}, wantANSI: true}, + {name: "auto tty no color present", mode: "auto", capabilities: PresentationCapabilities{StdoutIsTTY: true, NoColorPresent: true}}, + } { + t.Run(test.name, func(t *testing.T) { + got, err := renderTerminalText(view, test.mode, test.capabilities) + if err != nil { + t.Fatalf("renderTerminalText() error = %v", err) + } + if gotANSI := strings.Contains(got, "\x1b["); gotANSI != test.wantANSI { + t.Fatalf("ANSI presence = %t, want %t in %q", gotANSI, test.wantANSI, got) + } + if stripped := stripTestANSI(got); stripped != plain { + t.Fatalf("stripped text = %q, want %q", stripped, plain) + } + }) + } +} + +func TestTerminalStyleRejectsUnknownModesAndTokens(t *testing.T) { + if _, err := renderTerminalText(newTerminalText(), "always", PresentationCapabilities{}); err == nil { + t.Fatal("expected unsupported color mode error") + } + if _, err := renderTerminalText(newTerminalText(terminalTextToken{kind: "unknown", text: "x"}), "never", PresentationCapabilities{}); err == nil { + t.Fatal("expected unsupported token kind error") + } +} + +func stripTestANSI(value string) string { + for _, sequence := range []string{ansiLabel, ansiSecondary, ansiReset} { + value = strings.ReplaceAll(value, sequence, "") + } + return value +} diff --git a/internal/app/testdata/compact-v2-release-change-record.json b/internal/app/testdata/compact-v2-release-change-record.json new file mode 100644 index 0000000..71b13a3 --- /dev/null +++ b/internal/app/testdata/compact-v2-release-change-record.json @@ -0,0 +1,51 @@ +{ + "schemaVersion": 2, + "previousVersion": "0.3.0", + "version": "0.4.0", + "changeClass": "breaking", + "breakingChanges": [ + { + "changeId": "proofkit.compact.consumer-contract-v2", + "summary": "Compact-aware adoption, pilot, conformance, impact, proof-view, coverage, inventory, context composition, context slicing, semantic diff, traceability graph, browser, source-set, and resolver command contracts advance atomically and reject legacy compact payloads." + }, + { + "changeId": "proofkit.compact.declaration-contract-v2", + "summary": "Compact requirement proof inputs now use declaration-only v2 discriminators, rename mutation metadata as a caller claim, and remove assurance-shaped proof state and synthetic checked or finding counts." + }, + { + "changeId": "proofkit.compact.identity-role-closure", + "summary": "Compact bindings now preserve requirement, surface, and scenario identity while witness routes preserve binding, role, selector, and JSON-safe resolution order across every decision-bearing projection." + }, + { + "changeId": "proofkit.compact.source-set-v2", + "summary": "Compact source-set normalization advances to wrapper, envelope, and source-set schema v2 with declaration-specific roles and fragment contract v3." + } + ], + "additions": [ + { + "changeId": "proofkit.requirement-source.typed-model", + "summary": "Add a bounded representation-neutral internal typed model for requirement-source v2 experiments without adding a source codec, persistence format, public CLI, or source cutover." + } + ], + "migration": { + "required": true, + "steps": [ + "Replace compact root schema_version 1, authority_state canonical, contract_kind requirement_proof_binding, and normalization_profile proofkit.compact.v1 with schema_version 2, authority_state caller_owned_declaration, contract_kind requirement_proof_route_declaration, and normalization_profile proofkit.compact.declaration.v2.", + "Remove proof_contract_state; replace mutation_resistance_state with declared_mutation_resistance_claim_id; and stop consuming proofContractState, mutationResistanceContext, checkedWitnessSelectorCount, and findingCount from compact resolver projections.", + "Preserve each compact binding as requirementId, surfaceId, and scenarioId plus bindingRecordId, and preserve each witness route as bindingRecordId, role, selector, JSON-safe resolutionOrderIndex from 0 through 9007199254740991, and witnessRouteId; do not collapse equal selectors across positive and falsification roles.", + "Replace changedRecordIds with changedRequirementIds, replace resolver projectionKind proofkit.requirement-proof-resolver with proofkit.requirement-proof-route-resolver, and consume compact-aware adoption, pilot, conformance, impact, proof-view, coverage, inventory, context-compose, context-slice, semantic-diff, traceability-graph, browser, source-set, and resolver parent contracts at their 0.4.0 versions.", + "Regenerate requirement proof source sets with wrapper, canonical envelope, canonical contract, and source-set schema version 2, declaration-specific source roles, and fragment contract requirement-proof-route-declarations/fragment/v3." + ] + }, + "platformRequirements": [ + "Published Darwin package binaries require macOS 12.0 or later on arm64 and x86_64." + ], + "knownLimitations": [ + "Complete nested public structural contracts remain blocked under SCHEMA-01; current CLI contracts own exact root variants only.", + "The requirement-source v2 codec remains unselected until SOURCE-CODEC-01 passes; the typed model is internal and no source cutover is claimed.", + "TSX source parsing remains unsupported." + ], + "rollback": { + "strategy": "previous_admitted_version" + } +} diff --git a/internal/app/testdata/compact-v2-wire-observations.json b/internal/app/testdata/compact-v2-wire-observations.json new file mode 100644 index 0000000..7c472cd --- /dev/null +++ b/internal/app/testdata/compact-v2-wire-observations.json @@ -0,0 +1,5867 @@ +{ + "cliContractDirectionDigests": { + "adoption-checklist|input": "sha256:90ce948cee2d26ec4943d3997d9e7a1f1e7e9fe58300440089aaee97331ae269", + "adoption-checklist|output": "sha256:a5dfb84bd458215ec1512eca4e8688abb566e6ffc4d29a476a100ca370c95ccf", + "adoption-contract-envelope|input": "sha256:6cc68424faff9a02d89fb8cc06dd1528048c5647c7fc617a7b828ac8314cb7c5", + "adoption-contract-envelope|output": "sha256:7c62bb035f151f33451625c659d01484af41c7d673fc915e77842b04b9143083", + "adoption-doctor|input": "sha256:d855e1f17e535675db39f32ef60cc42b5b15ef8b60abdcb9f37778bcdc1d85df", + "adoption-doctor|output": "sha256:7ccfd45cd101f0eed882c1b5c3d856b8ea60ffe1238143d63881b8ed3a5632d2", + "adoption-workflow-plan|input": "sha256:93d2f709f6143c8c6af92e5c5e2260a066f3b28a2fdc606c282727d47cf67d30", + "adoption-workflow-plan|output": "sha256:4e85bc541cc16ddf4d597a2961a7b8229d190924ace2165a5d865d040fdbaed1", + "agent-route|input": "sha256:3696bac6a97d09abc3c20b6e1aabdd9626dcf3b9f619f936d76a7ca396ce030b", + "agent-route|output": "sha256:08b46ea3f421cdbcf50d5a5df1cefda3e30e23edaebfa482e9b6e9f88fc84615", + "binding-partition|input": "sha256:2fc84488a7e232c346bf533d9b0fcc06cd5b9cdb85832b7ccaa3456cf76cb750", + "binding-partition|output": "sha256:560684d07728fdd56123adf0c0e88b56f3c0ac3638199edc63ae788018181fc3", + "branch-authority|input": "sha256:a76bd0efdeb0b2e951d9073b8d0aa763efca7424393d873a6f02762d4ef35639", + "branch-authority|output": "sha256:b14415fe3fefddb759013b6bff866978e985e8906f7bf8ce40f727c1785a4d6e", + "capability-map-admission|input": "sha256:a2394a538b017e2ce4407dd326c78d38d495cfcd53a2cb07b7f47995b8957606", + "capability-map-admission|output": "sha256:3b5b7fb88c1dc7f6de9be8ecf32d87dc0554dccb90d5aaedabe9d8ea69062664", + "changed-path-set|input": "sha256:477213957882b70a2dcbf5f18b94938a9eb2ac92ceceb9b47b7bdcb746304ffb", + "changed-path-set|output": "sha256:75256cb5f7321c8dea23677208903a57313600a892f0066fbe3bc687de9efeca", + "completion-criteria|input": "sha256:da91df861f4f947e40292af94631bffc29fb8e7012d7f98344963bd653ff46f2", + "completion-criteria|output": "sha256:84e020bd54f4a9b787c86882c6d6f149f0edaddcb0fba332ab00f0c2facb50ee", + "conformance-profile|input": "sha256:fb042c5797ee1f55b02efbddfa6be87bf1730b8e0efd22983b4fe425e5fe5203", + "conformance-profile|output": "sha256:2cb62861094f005ca0c62fdb48ef23798497c18a31c4318c45fef0c85333eb11", + "custom-rule-boundary|input": "sha256:e5b78e018232aa4d9eb40cf3ca7b9dfaf10a50145b0ff47c8002a34a9c187c45", + "custom-rule-boundary|output": "sha256:c830846c7b75d4f3e719160f6c504108c8ffc4473debef17d0fd8c70c41f08e0", + "deployment-evidence-admission|input": "sha256:b8b6b63d00dddd8751d0e5e3705bca9bb32f624090adaa72c9b4293761427a0d", + "deployment-evidence-admission|output": "sha256:ade8bcd7c8d19b4f09a72b2d46eefa4f48dca00f0ae62f49ef63e174ea98f755", + "document-lifecycle-boundary|input": "sha256:7c08b961bc526a7ad9e28dbdf965803053b949b1b567fb0a92b9d88401e3a2bd", + "document-lifecycle-boundary|output": "sha256:4b9c6353cb9b5a1b8098aed5c0640bdabf98fb375bc21c0850294b7441321f0b", + "evidence-graph|input": "sha256:74adb489ab86459f682d36b230e81cc8f3044e497cbc9b02aa1e1aff7e813653", + "evidence-graph|output": "sha256:8392a1aacc60d4b8413259da93832a830b4dbdb1881107a31f5520e9e056de70", + "external-consumer|input": "sha256:bede327d649f8941afa36a9fc4859979872e5b6d24cd781832d5e50112cf42f2", + "external-consumer|output": "sha256:513b01be1c77b07fcbb1966c4dbbf7d3a5906a9b2e4782cb1a7bcb7f05030951", + "gradual-adoption-bootstrap|input": "sha256:0dcbe9dd68cd7572d93e1441fd324c0830e0da96f71ca7981178334f8c1642f3", + "gradual-adoption-bootstrap|output": "sha256:a6a7b6ae5f5553b06b816b56250a99faef1ff81ef32eeb07609942cb9261bbfe", + "gradual-adoption-guidance|input": "sha256:082c6531409d31872ac46a08ac7ea0ece8de1914fd457ff39bc1cecd5b558656", + "gradual-adoption-guidance|output": "sha256:98e5c5e265f0cffcd35c04756b98c0bbc0d96a712fd0ffcd106c639b586669de", + "gradual-adoption|input": "sha256:3a416920a940c8e385c52add4c9a6d5a38f43a6ef0e73b12ce2b562652aa1a47", + "gradual-adoption|output": "sha256:ee96a48a89446f1fbce35c22f810845219bd13697e074b53f1053b9abcec7dac", + "impact|input": "sha256:809d344e54815d94917f603b1473625425b598e8ab3f1d49f860c5ecbe9bcfb8", + "impact|output": "sha256:6f107f179d59151be76c5d7e99240211f0ddcaea540d885af0975a5564a7e1ed", + "init|output": "sha256:db951e20cb9525c1d2b43989ab57624f1923a3ea3d67370157d951950c6b69d6", + "json-report-cli-adapter-source|output": "sha256:02e5ee1f4d8fb0e26ae694246e82f853e86b946539d9ac0b475739e3f4e7b537", + "migration-parity-admission|input": "sha256:355ff2cd9c8a0fca4745c12e46be7169a48339da8e3031d59daa06b1780f718d", + "migration-parity-admission|output": "sha256:5e823bd68d4172e3baedb1d53cd51cc94ed291db493955b524108bc8a056e03e", + "migration-plan|input": "sha256:157d81c35e99fa35256c299cafcf8e987b29a1c8fefd7ab69c9187fdfd18c5f7", + "migration-plan|output": "sha256:ae4ef30e1985ab7206c3d68012128402d0be9734b42db63ef2c7ac8d1732c5be", + "obligation-decision|input": "sha256:4f0ed148b2513395f5d91bb4d9f71d7faf1cd17d4fa15e6876fcf4cff740e3e6", + "obligation-decision|output": "sha256:b47cd87134fdbb7a4dbd260ed7dec9cd961c87fadc49b78328aedb76b22ec8cb", + "package-runtime-dependency-admission|input": "sha256:bd4e7f329f608906ee98a33987cf1ef36a7928031e31f1defefef0f0294eb92b", + "package-runtime-dependency-admission|output": "sha256:157b093b52c495a3b7e89130002c735e4c47237ef53e2f91b54955d842a61c0e", + "pilot-admission|input": "sha256:14c6a96a792711c1ce7cea82c42be66aabb19120f116d0d04212706ffc30ec4e", + "pilot-admission|output": "sha256:6c37a9c16971bed4db083f549406750ed4a39f483c235364ee51c3ac8e3b8d74", + "producer-policy-self-proof|input": "sha256:322335b38d26b1096737838b4a18ed1abbbceb2c938410e7298a114e4f3f9fd8", + "producer-policy-self-proof|output": "sha256:b724fe6bad790a17cc8de9115579137662e09b8ccf533f74d5ff941bc3857e29", + "proof-obligation-algebra|input": "sha256:0bf813eefbc86c46367433fc3628ce203d295bb1419a5ae9e9425f08f4ccc61c", + "proof-obligation-algebra|output": "sha256:44cc34e4659a00653bb93c15be2a35525f0e68756e6053b83b80e53fe20bdc3f", + "proof-receipt-admission|input": "sha256:86138e62942771d3d490585b1d075ec97d460b66cad541ddb89c639b38c9398f", + "proof-receipt-admission|output": "sha256:e0aad2bbe43e7effd28a512d028a89c25761511a11d0e4119c6e91698d1d9c57", + "proof-slice|input": "sha256:c0d58bb85234ffc50bee59324fb299a50a44b416b26ecb552791e8fcfbea9cca", + "proof-slice|output": "sha256:d30b1254b9b7202d332770eb2ff4606eac3c0668b2830e2f817d06c922d0d887", + "readiness-closeout|input": "sha256:8bdec8cc9f2f66c02a6c98321510c9527abc78d444c9cb4eb30d90d1c9c51d50", + "readiness-closeout|output": "sha256:4d6cbb25ae4961aa5ff05eb6f114c50811997373d13c5a34b8e9a9c19af3bb60", + "receipt-currentness-scope|input": "sha256:8c3975d3f2473c8e90f9eeb48129a6f8a8743f79dc697f453a96943dea8ff692", + "receipt-currentness-scope|output": "sha256:75699507b82a0bc64ae610ac1640ef8263e12acb5f416629ba168d304b684c4d", + "receipt-producer-admission|input": "sha256:8c607af3d24427bd8c02db890884f33f24e568ec7c04232fc47649f5d06849ca", + "receipt-producer-admission|output": "sha256:a5c9218d94b61d040ec3d463d9ee470710534a94ba3813b4495805d0cbc2f273", + "receipt-trust-class|input": "sha256:a280cb9370fa0da8a3ccfdbb788f41a94f43fdd2b0616d16058eff83ec828e37", + "receipt-trust-class|output": "sha256:fad2ceda46286a3f43c215625b808e3f35a0ede4f557e4e8461bcece33ded0bd", + "registry-consumer-proof-input-compose|input": "sha256:52876989f242e8718fc973f1177fa624c894628450ffe42ae2b0c6065b202b6e", + "registry-consumer-proof-input-compose|output": "sha256:6db33860c9f6516f451fcac96e3cc969fdf73421b91e1ba352339991c0735dcf", + "registry-consumer|input": "sha256:15967657ffeb8934bc8d79690337f8ee00bc08e84c626ad07b933dae32db21e4", + "registry-consumer|output": "sha256:ec4bda535dd3ce1dd584a70fb1c67495598701294ce297c68dad9c9ee9fec71c", + "release-authority|input": "sha256:41e27d280e65cbfdb400f1afae0f8871e767296183eccf5addefc4a6e76a5e36", + "release-authority|output": "sha256:b36fba22ae2a1b8f94e57e7d213defdb5bb96a3fc847b89067dd136ec2f67a13", + "rendered-artifact-freshness|input": "sha256:84d853d842099f7347ccc267f14fcfb061c62c425f0fcf6944599afeada20ff5", + "rendered-artifact-freshness|output": "sha256:6ca371b35594fa5bdf151e166e6bb11538a68b457df381b61e35f2f6641a21d5", + "repo-profile-admission|input": "sha256:a730a2aaf7ab96b2f7451e4fc3793c09c060fcb284b3aa04102a047a090120d2", + "repo-profile-admission|output": "sha256:be544930dab8f1fcf04f2423ac2e1132fe932a1d4e6c4b4342d87d39bdad8913", + "requirement-authoring-plan|input": "sha256:57639ac1f5730cb036111c279c02bd1ca933bc3739057102a8f5bc66b6e2a5e0", + "requirement-authoring-plan|output": "sha256:56fe83745257ddc721ae9d27c1f8de166bc6d5d1f2ed95a4696f9a5bf88e23d3", + "requirement-bindings|input": "sha256:f4000a7948841954f8bc92e7d7cd58d4e8d1c4a43d7179264b93defe69033dd7", + "requirement-bindings|output": "sha256:5db1c67a808acc1ebfc464fe5d817a5396b11cd12684109e47a586d387f759a8", + "requirement-browser-server|input": "sha256:2c0e74874879650486c37660c5dd2e37e497b8f30a53820e4e5c3595476d2262", + "requirement-browser-server|output": "sha256:8834d2d30c136a04f24442446c2f51a2d64e5e84c99b469bb746089993dca8e7", + "requirement-context-compose|input": "sha256:90ea9a7adcf25941e2fba8ebceafc0797b1b192766c41571e68e8e92245a1883", + "requirement-context-compose|output": "sha256:26ab63781377a13e34446346acc77ce011149bb13fd6a55c7b2b3402da082b62", + "requirement-context-slice|input": "sha256:4111bb1d20d473df2b7dccac36628cbb7425b3239cf661359ce1d481c69da96e", + "requirement-context-slice|output": "sha256:17bbecd4ca34de26e477d3e7788de9ebd90702c25b6698354ad3eb9b3a2759ca", + "requirement-coverage-input-compose|input": "sha256:b95003601c3b19929e6760c62173bcd630ccb86e536063a8a21078034e35a9dc", + "requirement-coverage-input-compose|output": "sha256:1795d91a93b7a1fba08c507c6b791df24b712938510f744d504952121c022dcd", + "requirement-coverage-view|input": "sha256:31ad080adcd6e58ad21c670c5016ad91d28dabc1edb7e8bb03a8d6c98fa3ae03", + "requirement-coverage-view|output": "sha256:1388cbe40f51a804380c4f99dbd97deb0e5711c10391ce15164e5eb66b6d2033", + "requirement-impact-input-compose|input": "sha256:7a48347d513cd327d076d253252a014db07502eb952eb4d818b0d1c9841ff643", + "requirement-impact-input-compose|output": "sha256:07f5bc2cca2edeb08b6fd7d4659504114f3747ca68119971e8a3ce955bce24ef", + "requirement-proof-resolver|input": "sha256:31bc06fd1a58e718ba20daa5226244dda3480d5e7688d91f81efb44b4402e39c", + "requirement-proof-resolver|output": "sha256:51ae126ed6c53aee773474bec3c837742bd1fd87ba8b881cc1dad79cc98b1bcc", + "requirement-proof-source-set|input": "sha256:105053e5484e4ee1c3e9e3643fab4e71e4d8b062ed39e08139db5bc5490272d9", + "requirement-proof-source-set|output": "sha256:87ba5cc065eb2232e98b451648dec7224029e3a661724c6f9cea28ad8f236c13", + "requirement-proof-view|input": "sha256:6f553c8c054e4269eea657524ab7ac77cee33fe5962229ad4b0c49a5ee7e03b6", + "requirement-proof-view|output": "sha256:7d3eae95fcad5141b36db3e9919ae14453bcf9ea6891a7e2a93b34753c0a3556", + "requirement-semantic-diff|input": "sha256:165bf2a7a925f0a17e613bcec2da873e1765c1548ab335e49de0326219894f53", + "requirement-semantic-diff|output": "sha256:69952ae26e9b8bca104d9fdfcc14827a3516c5716a5922fe9bd5781566c2365b", + "requirement-source-admission|input": "sha256:41a41e324e798eed9a0f96ed1f0f1f702610e103ccbab48c4c47b63679344cc1", + "requirement-source-admission|output": "sha256:429043e682c5eb2eb803d7075f2a4ba18881b12fc57871b99f9207b40427291c", + "requirement-source-transition|input": "sha256:71a9073d1a196d64ff58833d043ae1319907c483a8de51c24d206223ab43035d", + "requirement-source-transition|output": "sha256:2c41afe8780edbb1015be124ffb985e207496891a6dee442c67df4739a17101f", + "requirement-source-view|input": "sha256:3535450f217b8bcc100f3d7a203b042aa8cfad1573a1661fd2917c3715ae3802", + "requirement-source-view|output": "sha256:15526f5b59b357a40f5aed3676a521a51241ca8c6a0efb911a68802ee52ecfb5", + "requirement-spec-tree-view|input": "sha256:85ec9298fe8ff9b509cd037e173ad0c8f98c03383a9c0a4283103010421db0b2", + "requirement-spec-tree-view|output": "sha256:e3b3179e81b01df750b21f967b45439f8bc10dd6e5bb9374ff5e179cb91dfbfc", + "requirement-spec-tree|input": "sha256:6690fea7f8219c786cc0a2e7454d4fcf438e4fe37df19d51ccef7046594bf586", + "requirement-spec-tree|output": "sha256:d7a4d54d1ff99fa665a119130b3886a6bf4583eb0fa07b96825a2b4b76e7e293", + "requirement-traceability-graph|input": "sha256:5dea2ed230ebd0afcd284bd9b505ef92b7e8875ca91518020a07b2e02d8c8c20", + "requirement-traceability-graph|output": "sha256:b783832ae3b8fedb1b2545b4a7de55e5b20fe790bba1624278f5cf4b0114a9f6", + "scaffold-profile-plan|input": "sha256:b842ca93be6efd7eb0a7d1180a2f53c0cac403075b81736308e2cb4199e62581", + "scaffold-profile-plan|output": "sha256:5adfe10b774614d4574448a5d8114b485a07670fbf7248ac405f10096b7665fa", + "scaffold-project-structure|input": "sha256:a87826db01747b7e32f9da902b503658fd86a972161171a65181291d915795c0", + "scaffold-project-structure|output": "sha256:70d829c9b3cd56f24eeaeab6c4f16eb236496dc429aa10c851e680f315ec2602", + "secret-scan|input": "sha256:8403fb30faa50ef762ca47cc985a6a1eb017b9ed5511e21d5f7528f19e56571c", + "secret-scan|output": "sha256:876dd256c89048d2baf6779799955627ca7e218ebc771994c4a32256d6bba285", + "selective-gate-evidence|input": "sha256:3078ef7ccb10cfa7ae37d6aefff32feff4d65542031d5fe28d6c3333d0e85484", + "selective-gate-evidence|output": "sha256:ede14200aa93d348c723e0a1f73ae9bb4e081a7b1d3f1dda2e989abba0bb6225", + "selective-gate-obligation-decision-input|input": "sha256:2fa3461bb0555eddab104c93ab4009705f10d451da613a852117d21f319ebb1d", + "selective-gate-obligation-decision-input|output": "sha256:e42ca716fb503ff4ae63d25a1046238f9ed620e506abed1426dacb77f516d658", + "selective-gate-plan|input": "sha256:3868403db3593a5ef6abbdd1cc74801c5091e11f1981b42ff9cef9290382c3c3", + "selective-gate-plan|output": "sha256:ea9e401196673d71f006df7a1426b2365e6ecf1fcda5600f7f2245d79a1756cb", + "self-check|input": "sha256:064fd0c763a1bc9a2dac49a2ac49c28da8a39b10d7757bfe0e00db49fe6ba566", + "self-check|output": "sha256:bde5c6c8f362ce92dee798843b69c74ed19732abcf3490c22e9e8d7a1d22ecbf", + "spec-overview-claims|input": "sha256:15534d7667c37e9fa013a698f0f50be5fd0812c1a4034fa2721198347ff48b77", + "spec-overview-claims|output": "sha256:b2ef970a6995d7cef561727dc43995bffe532db8c10005ccf8386e57319a5b5d", + "spec-proof-bundle-admission|input": "sha256:f9b4557dddc5ee02519d659ccc658c3367e8fe3611f1a56d0cef39d5787d18db", + "spec-proof-bundle-admission|output": "sha256:011d840c4d1890659cbe10b9bfcd8caf7abe48c4463bd2f5f9db637adb68cbc1", + "stack-preset|output": "sha256:b4543c977c63b832c2a84d1b8e4733b8a58256b6e810f082b11591957899ce9f", + "test-evidence-inventory|input": "sha256:5a3887ee5bb04b2059eb8966e029026fcace91f631623cf0cec8853612f2aa85", + "test-evidence-inventory|output": "sha256:3cf31e81537f2a673e048edbe0f56d973b423efce54c66a21f169796dd0ec6cd", + "text-policy|input": "sha256:9c6e80290c86842eb40bbe8df6a3f6b63f70a98ab31a77f8f10946bd1dd2bf77", + "text-policy|output": "sha256:c403c5b85af3aa109fa1fdbf2b0807cece72778821bbdf4fa1c0efe48fd5d94f", + "typescript-public-api-surfaces|input": "sha256:2645b2d57e7630664fd02bea20efc3a3cd89044cf401621a231f1637b63d8a0a", + "typescript-public-api-surfaces|output": "sha256:feec090f498e27b7b07c22181d41f4ddc10fd7178b91908301a117b98f24903e", + "witness-plan|input": "sha256:e38a84562e6c45d09ed2591019958c3427795f0f998d9dbb0bd3bfa05ccdde39", + "witness-plan|output": "sha256:6038ffd69dd3fe998d9d2fb0ab4af8a7e145543abdab2634fbda522d2163a3a3", + "witness-scheduler-plan|input": "sha256:8080755d63124f147fec56114c84ce1934dfed797d5388988030c30888e24752", + "witness-scheduler-plan|output": "sha256:5e4a687f0ba9b3222fcd33f44eb83719b730e4c4a969916c99fe10e69fbd6e17", + "workspace-changed-package-plan|input": "sha256:2221d3d3cda2205758892bd3903a606a2670538b65859b1bb66e025b076dc1e6", + "workspace-changed-package-plan|output": "sha256:76c616880d8e3b1677446cf34a31236b9c643ed5bda88ad053159d5fe6876eee", + "workspace-manifest-facts|input": "sha256:83bb596e9f7a07d113642c957c2279b0f04d15a87b0f5b68db099b3ccc8f53af", + "workspace-manifest-facts|output": "sha256:056621f899f38a5f20169612264aaf692af3b92a0567e2520cafa18af90dbbd8", + "workspace-registry|input": "sha256:0f13c5872bfa891485d37027694763885b77d0ee30768a244f4931fc17f91c0a", + "workspace-registry|output": "sha256:9aa34994a28b71580ed4742163770356f66229a2b30a3b71a43c623b827efcee", + "workspace-shard-partition|input": "sha256:699951cddb9fb98ab54ff89da68d36534b85db664dd4d91c8c5587785e3ea104", + "workspace-shard-partition|output": "sha256:6ed0271efd047a5d5fb8173c9871af9f27077902a816509a432972144ed96735" + }, + "observations": [ + { + "direction": "input", + "document": { + "envelopeId": "proofkit.cli.adoption.aggregate", + "gradual": { + "bootstrap": { + "bootstrapId": "proofkit.cli.bootstrap", + "commands": [ + "go test ./internal/command/gradualadoption" + ], + "defaultMode": "observe", + "nonClaims": [ + "CLI bootstrap fixture does not prove rollout readiness." + ], + "paths": { + "adoptionGuidancePath": "proofkit/guidance.json", + "adoptionProfilePath": "proofkit/profile.json", + "adoptionReportArtifactPath": "artifacts/proofkit/adoption.json", + "guidanceReportArtifactPath": "artifacts/proofkit/guidance.json", + "witnessPlanInputPath": "proofkit/witness-plan.json" + }, + "scopeEvidence": { + "checkedScope": "none", + "touchedRuleIds": [] + } + }, + "guidance": { + "agentGuidance": { + "artifactPath": "artifacts/proofkit/guidance.json", + "blockedPreconditions": [], + "callerSuggestedAutofixCandidates": [], + "commands": [], + "minimalAdoptionPath": [ + "Keep proofkit adoption non-blocking until owner review." + ], + "proofBindingsMissing": [], + "reportKind": "proofkit.gradual-adoption-guidance", + "requiredNextActions": [], + "routeQuestions": [ + "what changed", + "what proves it", + "who owns it" + ], + "schemaId": "proofkit.gradual-adoption-guidance.v1" + }, + "defaultMode": "observe", + "guidanceId": "proofkit.cli.guidance", + "modernization": { + "candidateBoundaries": [], + "promoteOnlyAfterOwnerReview": true + }, + "nonClaims": [ + "CLI guidance fixture does not prove rollout readiness." + ], + "ownerRoute": { + "evidencePaths": [ + "artifacts/proofkit/source.json" + ], + "primaryOwner": "repo-maintainers", + "proofBindingPaths": [ + "proofkit/proof-bindings.json" + ], + "specPaths": [ + "docs/specs/cli-adoption/requirements.v1.json" + ] + }, + "scopeEvidence": { + "checkedScope": "none", + "touchedRuleIds": [] + } + }, + "input": { + "adoptionId": "proofkit.cli.adoption", + "adoptionMode": "non_blocking", + "agentReport": { + "artifactPath": "artifacts/proofkit/gradual-adoption.json", + "outputMode": "non_blocking", + "reportKind": "proofkit.gradual-adoption", + "routeQuestions": [ + "what changed", + "what proves it", + "who owns it" + ], + "schemaId": "proofkit.gradual-adoption-profile.v1" + }, + "budget": { + "copiedVerifierFileCount": 0, + "customRuleCount": 0, + "maxAddedSeconds": 10, + "maxCustomRuleCount": 0, + "maxProfileLines": 80, + "maxSetupMinutes": 20, + "profileLines": 42 + }, + "module": { + "moduleId": "proofkit.cli.module", + "requirementIds": [ + "REQ-PROOFKIT-CLI-ADOPTION-001" + ], + "specPath": "docs/specs/cli-adoption/requirements.v1.json" + }, + "nativeWitnesses": { + "commands": [ + { + "argv": [ + "go", + "test", + "./..." + ], + "cachePolicy": "read-only", + "credentialClass": "none", + "cwd": ".", + "environment": { + "allowlist": [], + "classes": [ + "local-go" + ], + "inherit": "none" + }, + "exitCodePolicy": { + "kind": "zero", + "successCodes": [ + 0 + ] + }, + "expectedArtifacts": [], + "id": "proofkit.cli.command", + "networkPolicy": "none", + "parallelGroup": "local-go", + "schemaVersion": 1, + "timeoutMs": 60000 + } + ], + "vocabulary": { + "artifactKinds": [ + "json" + ], + "credentialClasses": [ + "none" + ], + "environmentClassPolicies": [ + { + "cachePolicies": [ + "read-only" + ], + "credentialClasses": [ + "none" + ], + "environmentClass": "local-go", + "networkPolicies": [ + "none" + ] + } + ], + "environmentClasses": [ + "local-go" + ], + "parallelGroups": [ + "local-go" + ] + } + }, + "nonClaims": [ + "CLI gradual adoption fixture does not prove rollout readiness." + ], + "packageVersionRef": "agentic-proofkit@local", + "proofBinding": { + "bindingFormat": "requirement_to_witness", + "bindingPath": "proofkit/proof-bindings.json", + "requirementIds": [ + "REQ-PROOFKIT-CLI-ADOPTION-001" + ], + "witnessCommandIds": [ + "proofkit.cli.command" + ] + }, + "repository": { + "customRuleBoundary": "profile_only", + "primaryLanguages": [ + "go" + ], + "profilePath": "proofkit/profile.json", + "repositoryClass": "go_cli", + "repositoryId": "proofkit.cli.repo", + "verifierCodeCopied": false + }, + "rollback": { + "disableCommand": "remove proofkit gradual-adoption report from non-blocking CI", + "owner": "repo-maintainers", + "versionPin": "agentic-proofkit@previous" + }, + "rolloutClaim": false, + "schemaVersion": 1 + }, + "schema": "proofkit.gradual-adoption-profile.v1" + }, + "nonClaims": [ + "CLI aggregate fixture does not execute native witnesses." + ], + "pilot": { + "input": { + "agentReportRoutes": [ + { + "artifactPath": "artifacts/proofkit/report.json", + "command": "agentic-proofkit render", + "expectedUpdatePath": "docs/specs/proofkit/requirements.v1.json", + "reportKind": "proofkit.report", + "schemaId": "proofkit.schema", + "taskType": "proofkit.review" + } + ], + "blockingRequirements": { + "dispositionPolicy": "all_blocking_requirements_must_be_witnessed_or_explicitly_deferred", + "explicitlyDeferredRequirements": 0, + "requirements": [ + { + "evidence": "docs/specs/proofkit/requirements.v1.json", + "owner": "proofkit", + "requirementId": "REQ-PROOFKIT-001", + "status": "witness_backed" + } + ], + "totalBlockingRequirements": 1, + "unmappedRequirements": 0, + "witnessBackedRequirements": 1 + }, + "cacheNegativeChecks": [], + "cacheScheduler": { + "cacheKeyInputs": [ + "go.mod" + ], + "destructiveConcurrencyAllowed": false, + "invalidationInputs": [ + "go.sum" + ], + "maxParallelGroups": 1, + "parallelGroups": [ + "local" + ], + "schedulerPolicy": "bounded_parallel_groups" + }, + "falsePositiveBudget": { + "dispositionOwner": "proofkit", + "enforcementMode": "non_blocking", + "maxAllowedFalsePositiveCount": 0, + "sampleWindowRuns": 1 + }, + "impactDemos": [ + { + "demoId": "proofkit.cli.impact.demo", + "generatedMirrorPaths": [ + "docs/generated/requirements.md" + ], + "impactInput": { + "baseCommit": "base", + "baseRef": "main", + "changedBindingRecordIds": [], + "changedPaths": [ + "docs/specs/proofkit/requirements.v1.json" + ], + "changedRequirementIds": [ + "REQ-PROOFKIT-001" + ], + "changedWitnessPathCoverage": [], + "generatedArtifactRules": [], + "headCommit": null, + "headRef": "feature/proofkit", + "ignoredProofLikePaths": [], + "obligationCatalog": [ + { + "bindingRecordId": "sha256:d460cbea38175960f7dc9b0ea95827b560dc52a3bd4b21dd390148b52b8f0b10", + "blockingStatus": "blocking", + "commands": [ + "go test ./..." + ], + "declaredMutationResistanceClaimId": "claim.unverified", + "declaredWitnessRoutes": [ + { + "bindingRecordId": "sha256:d460cbea38175960f7dc9b0ea95827b560dc52a3bd4b21dd390148b52b8f0b10", + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 0, + "role": "positive", + "selector": "internal/proofkit/witness_test.go::TestWitness", + "verifyCommands": [ + "go test ./..." + ], + "witnessRouteId": "sha256:3661d326952707970d5ccb8c50cce5677c774723d9ef2c20f39c2319108095d0" + }, + { + "bindingRecordId": "sha256:d460cbea38175960f7dc9b0ea95827b560dc52a3bd4b21dd390148b52b8f0b10", + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 1, + "role": "falsification", + "selector": "internal/proofkit/witness_test.go::TestWitnessFalsification", + "verifyCommands": [ + "go test ./..." + ], + "witnessRouteId": "sha256:f1740d608b6d75ffd7d2beec0f78121b025b285714c9cfc3ca6c295c5067dc29" + } + ], + "preconditioned": false, + "requiredEnvironmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-001", + "scenarioId": "proofkit.surface::proofkit.scenario", + "surfaceId": "proofkit.surface" + } + ], + "preexistingFailures": [], + "proofLikePaths": [], + "schemaVersion": 2, + "unboundProofChangeRationale": "No unbound proof-like path changed." + }, + "sourceOwnedChangedPaths": [ + "docs/specs/proofkit/requirements.v1.json" + ] + } + ], + "infrastructureBudget": { + "copiedVerifierFileCount": 0, + "customRuleCount": 0, + "customRules": [], + "manualTruthSurfaceCount": 0, + "manualUpdateStepCount": 0, + "maxCustomRuleCount": 0, + "maxManualTruthSurfaceCount": 0, + "maxManualUpdateStepCount": 0, + "maxProfileLines": 100, + "profileLines": 20 + }, + "nonClaims": [ + "CLI pilot fixture does not claim rollout readiness." + ], + "packageVersionRef": "package.json", + "pilotId": "proofkit.cli.pilot.first", + "pilotMode": "non_blocking", + "profile": { + "commandMatcherBridge": "none", + "customRuleBoundary": "profile_only", + "primaryLanguages": [ + "go" + ], + "repositoryClass": "go_cli", + "repositoryId": "proofkit.cli", + "structuredWitnessCommands": true, + "verifierCodeCopied": false + }, + "rollback": { + "owner": "proofkit", + "rollbackCommand": "agentic-proofkit previous-version", + "versionPin": "package.json" + }, + "rolloutClaim": false, + "schemaVersion": 2, + "timingBudget": { + "maxAddedSeconds": 5, + "measuredSeparately": true, + "reportArtifactPath": "artifacts/proofkit/timing.json", + "trackedFixtureAsBaseline": false + } + }, + "schema": "proofkit.pilot-admission.v2", + "stackDiverseInput": { + "agentReportRoutes": [ + { + "artifactPath": "artifacts/proofkit/report.json", + "command": "agentic-proofkit render", + "expectedUpdatePath": "docs/specs/proofkit/requirements.v1.json", + "reportKind": "proofkit.report", + "schemaId": "proofkit.schema", + "taskType": "proofkit.review" + } + ], + "blockingRequirements": { + "dispositionPolicy": "all_blocking_requirements_must_be_witnessed_or_explicitly_deferred", + "explicitlyDeferredRequirements": 0, + "requirements": [ + { + "evidence": "docs/specs/proofkit/requirements.v1.json", + "owner": "proofkit", + "requirementId": "REQ-PROOFKIT-001", + "status": "witness_backed" + } + ], + "totalBlockingRequirements": 1, + "unmappedRequirements": 0, + "witnessBackedRequirements": 1 + }, + "cacheNegativeChecks": [ + { + "checkId": "proofkit.cache.package_version", + "evidence": "Cache invalidates on package_version changes.", + "expectedOutcome": "invalidate_output", + "invalidatedInputClass": "package_version", + "liveOrCredentialedCacheable": false + }, + { + "checkId": "proofkit.cache.profile", + "evidence": "Cache invalidates on profile changes.", + "expectedOutcome": "invalidate_output", + "invalidatedInputClass": "profile", + "liveOrCredentialedCacheable": false + }, + { + "checkId": "proofkit.cache.schema", + "evidence": "Cache invalidates on schema changes.", + "expectedOutcome": "invalidate_output", + "invalidatedInputClass": "schema", + "liveOrCredentialedCacheable": false + }, + { + "checkId": "proofkit.cache.source", + "evidence": "Cache invalidates on source changes.", + "expectedOutcome": "invalidate_output", + "invalidatedInputClass": "source", + "liveOrCredentialedCacheable": false + } + ], + "cacheScheduler": { + "cacheKeyInputs": [ + "go.mod" + ], + "destructiveConcurrencyAllowed": false, + "invalidationInputs": [ + "go.sum" + ], + "maxParallelGroups": 1, + "parallelGroups": [ + "local" + ], + "schedulerPolicy": "bounded_parallel_groups" + }, + "falsePositiveBudget": { + "dispositionOwner": "proofkit", + "enforcementMode": "non_blocking", + "maxAllowedFalsePositiveCount": 0, + "sampleWindowRuns": 1 + }, + "impactDemos": [ + { + "demoId": "proofkit.cli.impact.demo.stack", + "generatedMirrorPaths": [ + "docs/generated/requirements.md" + ], + "impactInput": { + "baseCommit": "base", + "baseRef": "main", + "changedBindingRecordIds": [ + "sha256:d460cbea38175960f7dc9b0ea95827b560dc52a3bd4b21dd390148b52b8f0b10" + ], + "changedPaths": [ + "docs/specs/proofkit/requirements.v1.json", + "internal/proofkit/witness_test.go" + ], + "changedRequirementIds": [ + "REQ-PROOFKIT-001" + ], + "changedWitnessPathCoverage": [ + { + "path": "internal/proofkit/witness_test.go", + "routes": [ + { + "bindingRecordId": "sha256:d460cbea38175960f7dc9b0ea95827b560dc52a3bd4b21dd390148b52b8f0b10", + "resolutionOrderIndex": 0, + "role": "positive", + "selector": "internal/proofkit/witness_test.go::TestWitness", + "witnessRouteId": "sha256:3661d326952707970d5ccb8c50cce5677c774723d9ef2c20f39c2319108095d0" + } + ] + } + ], + "generatedArtifactRules": [], + "headCommit": null, + "headRef": "feature/proofkit", + "ignoredProofLikePaths": [], + "obligationCatalog": [ + { + "bindingRecordId": "sha256:d460cbea38175960f7dc9b0ea95827b560dc52a3bd4b21dd390148b52b8f0b10", + "blockingStatus": "blocking", + "commands": [ + "go test ./..." + ], + "declaredMutationResistanceClaimId": "claim.unverified", + "declaredWitnessRoutes": [ + { + "bindingRecordId": "sha256:d460cbea38175960f7dc9b0ea95827b560dc52a3bd4b21dd390148b52b8f0b10", + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 0, + "role": "positive", + "selector": "internal/proofkit/witness_test.go::TestWitness", + "verifyCommands": [ + "go test ./..." + ], + "witnessRouteId": "sha256:3661d326952707970d5ccb8c50cce5677c774723d9ef2c20f39c2319108095d0" + }, + { + "bindingRecordId": "sha256:d460cbea38175960f7dc9b0ea95827b560dc52a3bd4b21dd390148b52b8f0b10", + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 1, + "role": "falsification", + "selector": "internal/proofkit/witness_test.go::TestWitnessFalsification", + "verifyCommands": [ + "go test ./..." + ], + "witnessRouteId": "sha256:f1740d608b6d75ffd7d2beec0f78121b025b285714c9cfc3ca6c295c5067dc29" + } + ], + "preconditioned": false, + "requiredEnvironmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-001", + "scenarioId": "proofkit.surface::proofkit.scenario", + "surfaceId": "proofkit.surface" + } + ], + "preexistingFailures": [], + "proofLikePaths": [], + "schemaVersion": 2, + "unboundProofChangeRationale": "No unbound proof-like path changed." + }, + "sourceOwnedChangedPaths": [ + "docs/specs/proofkit/requirements.v1.json" + ] + } + ], + "infrastructureBudget": { + "copiedVerifierFileCount": 0, + "customRuleCount": 0, + "customRules": [], + "manualTruthSurfaceCount": 0, + "manualUpdateStepCount": 0, + "maxCustomRuleCount": 0, + "maxManualTruthSurfaceCount": 0, + "maxManualUpdateStepCount": 0, + "maxProfileLines": 100, + "profileLines": 20 + }, + "nonClaims": [ + "CLI pilot fixture does not claim rollout readiness." + ], + "packageVersionRef": "package.json", + "pilotId": "proofkit.cli.pilot.stack-diverse", + "pilotMode": "non_blocking", + "profile": { + "commandMatcherBridge": "none", + "customRuleBoundary": "profile_only", + "primaryLanguages": [ + "go" + ], + "repositoryClass": "go_cli", + "repositoryId": "proofkit.cli", + "structuredWitnessCommands": true, + "verifierCodeCopied": false + }, + "rollback": { + "owner": "proofkit", + "rollbackCommand": "agentic-proofkit previous-version", + "versionPin": "package.json" + }, + "rolloutClaim": false, + "schemaVersion": 2, + "stackDiversity": { + "baselinePilotId": "proofkit.cli.pilot.first", + "dimensions": [ + { + "baseline": "baseline-docs_spec_layout", + "candidate": "candidate-docs_spec_layout", + "dimension": "docs_spec_layout", + "evidence": "docs/evidence/docs_spec_layout.md" + }, + { + "baseline": "baseline-generated_artifact_policy", + "candidate": "candidate-generated_artifact_policy", + "dimension": "generated_artifact_policy", + "evidence": "docs/evidence/generated_artifact_policy.md" + }, + { + "baseline": "baseline-language_runtime_test_shape", + "candidate": "candidate-language_runtime_test_shape", + "dimension": "language_runtime_test_shape", + "evidence": "docs/evidence/language_runtime_test_shape.md" + }, + { + "baseline": "baseline-proof_environment_classes", + "candidate": "candidate-proof_environment_classes", + "dimension": "proof_environment_classes", + "evidence": "docs/evidence/proof_environment_classes.md" + }, + { + "baseline": "baseline-repository_topology", + "candidate": "candidate-repository_topology", + "dimension": "repository_topology", + "evidence": "docs/evidence/repository_topology.md" + } + ] + }, + "timingBudget": { + "maxAddedSeconds": 5, + "measuredSeparately": true, + "reportArtifactPath": "artifacts/proofkit/timing.json", + "trackedFixtureAsBaseline": false + } + } + }, + "schema": "proofkit.adoption-contract-envelope.v2", + "workflow": { + "schema": "proofkit.adoption-workflow.v1", + "workflow": { + "inputRefs": [ + { + "inputKind": "release_authority", + "path": "proofkit/release-authority.v1.json", + "refId": "proofkit.release-authority" + }, + { + "inputKind": "registry_consumer", + "path": "proofkit/registry-consumer.v1.json", + "refId": "proofkit.registry-consumer" + } + ], + "nonClaims": [ + "CLI workflow fixture does not execute generated commands." + ], + "presetId": null, + "scenario": "release_channel", + "schemaVersion": 1, + "workflowId": "proofkit.cli.workflow" + } + } + }, + "surface": "adoption-contract-envelope", + "variant": "aggregate" + }, + { + "direction": "input", + "document": { + "contract": { + "closed": true, + "compatibilitySummary": [ + "schemaVersion=2", + "root-shape-only definition proofkit.adoption-contract-envelope.input.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.adoption-contract-envelope.input.v2", + "nativeAdmissionWitnessSelector": { + "command": "go test ./internal/command/adoptioncontract -run '^TestBuildDelegatesModesWithParity$'", + "evidenceClass": "source_checkout", + "path": "internal/command/adoptioncontract/adoptioncontract_test.go", + "test": "TestBuildDelegatesModesWithParity" + }, + "nativeSource": { + "canonicalDigest": "sha256:167819db65616fcb8dec0e42ed91ee2f037db5d127ce1001db953ce683d2fd84", + "evidenceClass": "source_checkout", + "path": "internal/command/adoptioncontract" + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-011" + ], + "rootDefinitionDigest": "sha256:98fd5df72714bf773f3b90ad675ddcd9b214454fa083012c28f59a1cf3241b77", + "rootDefinitionRef": "proofkit.adoption-contract-envelope.input.v2.root-shape", + "rootType": "object", + "schemaVersion": 2 + } + }, + "surface": "adoption-contract-envelope", + "variant": "union" + }, + { + "direction": "output", + "document": { + "contract": { + "closed": true, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.adoption-contract-envelope.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.adoption-contract-envelope.output.v1", + "nativeOutputWitnessSelector": { + "command": "go test ./internal/app -run '^TestAdoptionContractEnvelopeCLIABI$'", + "evidenceClass": "source_checkout", + "path": "internal/app/cli_abi_test.go", + "test": "TestAdoptionContractEnvelopeCLIABI" + }, + "nativeSource": { + "canonicalDigest": "sha256:167819db65616fcb8dec0e42ed91ee2f037db5d127ce1001db953ce683d2fd84", + "evidenceClass": "source_checkout", + "path": "internal/command/adoptioncontract" + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-011" + ], + "rootDefinitionDigest": "sha256:6da2b641d38a05c97b6088bec7a38d8c9a76aa386eb101e48f9727bceae5cfa2", + "rootDefinitionRef": "proofkit.adoption-contract-envelope.output.v1.root-shape", + "rootType": "union", + "schemaVersion": 1 + } + }, + "surface": "adoption-contract-envelope", + "variant": "union" + }, + { + "direction": "input", + "document": { + "authority_state": "caller_owned_declaration", + "binding_columns": [ + "requirement_id", + "surface_id", + "scenario_id", + "invariant_role", + "owned_invariant", + "blocking_status", + "required_environment_classes", + "positive_witness", + "falsification_witness", + "verify_commands", + "declared_mutation_resistance_claim_id" + ], + "bindings": [ + [ + "REQ-PROOFKIT-COMPACT-001", + "proofkit.surface", + "proofkit.surface::scenario.compact", + "contract", + "proofkit.compact", + "blocking", + [ + "local-go" + ], + [ + "tests/proofkit_positive_test.go::TestAcceptsCompactContract", + [ + "local-go" + ], + [ + "go test ./..." + ], + 0 + ], + [ + "tests/proofkit_falsification_test.go::TestRejectsCompactRegression", + [ + "local-go" + ], + [ + "go test ./..." + ], + 1 + ], + [ + "go test ./..." + ], + "no_known_advisory_gap" + ] + ], + "contract_id": "proofkit.test.compact", + "contract_kind": "requirement_proof_route_declaration", + "non_claims": [ + "Compact test input does not execute witnesses." + ], + "normalization_profile": "proofkit.compact.declaration.v2", + "schema_version": 2, + "surface_columns": [ + "surface_id", + "required_environment_classes", + "preconditioned_environment_classes" + ], + "surfaces": [ + [ + "proofkit.surface", + [ + "local-go" + ], + [] + ] + ], + "witness_columns": [ + "selector", + "environment_classes", + "verify_commands", + "resolution_order_index" + ] + }, + "surface": "compact-contract", + "variant": "direct" + }, + { + "direction": "output", + "document": { + "bindings": [ + { + "bindingRecordId": "sha256:3e12335736f4aae18fc88fa1b7d704a77dbdbb7889ab6584f792adf43eb3e803", + "blockingStatus": "blocking", + "declaredMutationResistanceClaimId": "no_known_advisory_gap", + "invariantRole": "contract", + "ownedInvariant": "proofkit.compact", + "preconditioned": false, + "requiredEnvironmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-COMPACT-001", + "scenarioId": "proofkit.surface::scenario.compact", + "surfaceId": "proofkit.surface", + "testWitnesses": { + "falsification": { + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 1, + "role": "falsification", + "selector": "tests/proofkit_falsification_test.go::TestRejectsCompactRegression", + "verifyCommandRefs": [ + "go test ./..." + ], + "witnessRouteId": "sha256:e030b29fb58b3e0836b666bcbd1c3c7c64b7268d5b7a76b74130f361e1fcc4d4" + }, + "positive": { + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 0, + "role": "positive", + "selector": "tests/proofkit_positive_test.go::TestAcceptsCompactContract", + "verifyCommandRefs": [ + "go test ./..." + ], + "witnessRouteId": "sha256:23e2c5c66d485bd4bc3fe051283f2fbd752d55e827e26f7564e3903a49d8753a" + } + }, + "verifyCommands": [ + "go test ./..." + ] + } + ], + "commands": [ + { + "bindingRecordIds": [ + "sha256:3e12335736f4aae18fc88fa1b7d704a77dbdbb7889ab6584f792adf43eb3e803" + ], + "environmentClasses": [ + "local-go" + ], + "verifyCommandRef": "go test ./...", + "witnessRouteIds": [ + "sha256:23e2c5c66d485bd4bc3fe051283f2fbd752d55e827e26f7564e3903a49d8753a", + "sha256:e030b29fb58b3e0836b666bcbd1c3c7c64b7268d5b7a76b74130f361e1fcc4d4" + ] + } + ], + "conformanceProofContract": { + "bindings": [ + { + "bindingRecordId": "sha256:3e12335736f4aae18fc88fa1b7d704a77dbdbb7889ab6584f792adf43eb3e803", + "blockingStatus": "blocking", + "declaredMutationResistanceClaimId": "no_known_advisory_gap", + "invariantRole": "contract", + "ownedInvariant": "proofkit.compact", + "requiredEnvironmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-COMPACT-001", + "scenarioId": "proofkit.surface::scenario.compact", + "surfaceId": "proofkit.surface", + "verifyCommands": [ + "go test ./..." + ], + "witnessRefs": [ + { + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 1, + "role": "falsification", + "selector": "tests/proofkit_falsification_test.go::TestRejectsCompactRegression", + "verifyCommands": [ + "go test ./..." + ], + "witnessRouteId": "sha256:e030b29fb58b3e0836b666bcbd1c3c7c64b7268d5b7a76b74130f361e1fcc4d4" + }, + { + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 0, + "role": "positive", + "selector": "tests/proofkit_positive_test.go::TestAcceptsCompactContract", + "verifyCommands": [ + "go test ./..." + ], + "witnessRouteId": "sha256:23e2c5c66d485bd4bc3fe051283f2fbd752d55e827e26f7564e3903a49d8753a" + } + ] + } + ], + "contractId": "proofkit.test.compact", + "declarationKind": "proofkit.requirement-proof-route-declaration", + "schemaVersion": 2, + "surfaces": [ + { + "preconditionedEnvironmentClasses": [], + "requiredEnvironmentClasses": [ + "local-go" + ], + "surfaceId": "proofkit.surface" + } + ] + }, + "contractId": "proofkit.test.compact", + "environmentClasses": [ + { + "bindingRecordIds": [ + "sha256:3e12335736f4aae18fc88fa1b7d704a77dbdbb7889ab6584f792adf43eb3e803" + ], + "environmentClass": "local-go", + "surfaceIds": [ + "proofkit.surface" + ], + "witnessRouteIds": [ + "sha256:23e2c5c66d485bd4bc3fe051283f2fbd752d55e827e26f7564e3903a49d8753a", + "sha256:e030b29fb58b3e0836b666bcbd1c3c7c64b7268d5b7a76b74130f361e1fcc4d4" + ] + } + ], + "localEnvironmentPolicy": { + "authority": "caller_provided", + "localEnvironmentClasses": [ + "local-go" + ] + }, + "nonClaims": [ + "Compact test input does not execute witnesses.", + "Consumers own local environment-class policy supplied to the preconditioned projection.", + "Requirement proof route declarations do not assess oracle quality, mutation adequacy, finding absence, or finding completeness.", + "Requirement proof route declarations do not execute witnesses or prove execution-backed coverage.", + "Requirement proof route declarations do not prove freshness, authenticity, trust, assurance, merge approval, rollout, or production readiness.", + "Requirement proof route declarations do not prove that witness selectors exist or resolve." + ], + "projectionKind": "proofkit.requirement-proof-route-resolver", + "scenarios": [ + { + "bindingRecordId": "sha256:3e12335736f4aae18fc88fa1b7d704a77dbdbb7889ab6584f792adf43eb3e803", + "requirementId": "REQ-PROOFKIT-COMPACT-001", + "scenarioId": "proofkit.surface::scenario.compact", + "surfaceId": "proofkit.surface" + } + ], + "schemaVersion": 2, + "surfaces": [ + { + "bindingRecordIds": [ + "sha256:3e12335736f4aae18fc88fa1b7d704a77dbdbb7889ab6584f792adf43eb3e803" + ], + "requirementIds": [ + "REQ-PROOFKIT-COMPACT-001" + ], + "surfaceId": "proofkit.surface" + } + ], + "witnessRoutes": [ + { + "bindingRecordId": "sha256:3e12335736f4aae18fc88fa1b7d704a77dbdbb7889ab6584f792adf43eb3e803", + "environmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-COMPACT-001", + "resolutionOrderIndex": 0, + "role": "positive", + "scenarioId": "proofkit.surface::scenario.compact", + "selector": "tests/proofkit_positive_test.go::TestAcceptsCompactContract", + "surfaceId": "proofkit.surface", + "verifyCommandRefs": [ + "go test ./..." + ], + "witnessRouteId": "sha256:23e2c5c66d485bd4bc3fe051283f2fbd752d55e827e26f7564e3903a49d8753a" + }, + { + "bindingRecordId": "sha256:3e12335736f4aae18fc88fa1b7d704a77dbdbb7889ab6584f792adf43eb3e803", + "environmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-COMPACT-001", + "resolutionOrderIndex": 1, + "role": "falsification", + "scenarioId": "proofkit.surface::scenario.compact", + "selector": "tests/proofkit_falsification_test.go::TestRejectsCompactRegression", + "surfaceId": "proofkit.surface", + "verifyCommandRefs": [ + "go test ./..." + ], + "witnessRouteId": "sha256:e030b29fb58b3e0836b666bcbd1c3c7c64b7268d5b7a76b74130f361e1fcc4d4" + } + ] + }, + "surface": "compact-resolver", + "variant": "resolver" + }, + { + "direction": "input", + "document": { + "manifest": { + "authorityState": "canonical", + "contractId": "proofkit.cli.conformance", + "contractKind": "proofkit.conformance-manifest", + "nonClaims": [ + "Conformance profile CLI fixture is not live proof." + ], + "normalizationProfile": "proofkit.cli.v1", + "profiles": [ + { + "allowedEnvironmentClasses": [ + "local-go" + ], + "nonClaims": [ + "Local profile CLI fixture does not execute commands." + ], + "optionalSurfaceIds": [], + "preconditionPolicy": "local_only", + "profileId": "local", + "purpose": "Local proof profile.", + "requiredSurfaceIds": [ + "surface.local" + ] + } + ], + "schemaVersion": 1, + "sourceContract": "docs/contracts/requirement-proof-bindings.v1.json" + }, + "policy": { + "blockingStatuses": [ + "blocking" + ], + "expectedManifest": { + "authorityState": "canonical", + "contractId": "proofkit.cli.conformance", + "contractKind": "proofkit.conformance-manifest", + "nonClaims": [ + "Conformance profile CLI fixture is not live proof." + ], + "normalizationProfile": "proofkit.cli.v1", + "sourceContract": "docs/contracts/requirement-proof-bindings.v1.json" + }, + "failOnUnusedAllowedEnvironmentClass": true, + "knownEnvironmentClasses": [ + "local-go" + ], + "localEnvironmentClasses": [ + "local-go" + ] + }, + "profileId": "local", + "proofContract": { + "bindings": [ + { + "bindingRecordId": "sha256:2cfb4159c75dcf43c1b21fcdc921b880c652e3ff9f0f9a18a5fb26875e3d71d6", + "blockingStatus": "blocking", + "declaredMutationResistanceClaimId": "proofkit.claim.mutation.cli", + "invariantRole": "contract", + "ownedInvariant": "proofkit.cli.invariant", + "requiredEnvironmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-001", + "scenarioId": "surface.local::proofkit.scenario", + "surfaceId": "surface.local", + "verifyCommands": [ + "go test ./..." + ], + "witnessRefs": [ + { + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 1, + "role": "falsification", + "selector": "internal/test.go::TestOK", + "verifyCommands": [ + "go test ./... -run TestFalsification" + ], + "witnessRouteId": "sha256:0e673dd96922c7b02edabd26f2d19a97d69b742acb4878c52ec839c698eac8da" + }, + { + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 0, + "role": "positive", + "selector": "internal/test.go::TestOK", + "verifyCommands": [ + "go test ./... -run TestPositive" + ], + "witnessRouteId": "sha256:86260d57b0e6de6547f206121f0996a54002c00e6e6b78e21f7355e0c1d686a4" + } + ] + } + ], + "contractId": "proofkit.cli.proof-contract", + "declarationKind": "proofkit.requirement-proof-route-declaration", + "schemaVersion": 2, + "surfaces": [ + { + "preconditionedEnvironmentClasses": [], + "requiredEnvironmentClasses": [ + "local-go" + ], + "surfaceId": "surface.local" + } + ] + }, + "schemaVersion": 2 + }, + "surface": "conformance-profile", + "variant": "direct" + }, + { + "direction": "input", + "document": { + "contract": { + "closed": true, + "compatibilitySummary": [ + "schemaVersion=2", + "root-shape-only definition proofkit.conformance-profile.input.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.conformance-profile.input.v2", + "nativeAdmissionWitnessSelector": { + "command": "go test ./internal/command/conformanceprofile -run '^TestBuildProfileResolvesRequiredSurfaceAndRejectsMissingSurface$'", + "evidenceClass": "source_checkout", + "path": "internal/command/conformanceprofile/conformanceprofile_test.go", + "test": "TestBuildProfileResolvesRequiredSurfaceAndRejectsMissingSurface" + }, + "nativeSource": { + "canonicalDigest": "sha256:fba2ae58bf66cfc43834368dec84ca85abd11e08a541786dc59eafaf7628c322", + "evidenceClass": "source_checkout", + "path": "internal/command/conformanceprofile" + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ], + "rootDefinitionDigest": "sha256:b343c1093476cd9f50970f58608e43c1a6ae681a2c06b17c217ab4f22ea454e5", + "rootDefinitionRef": "proofkit.conformance-profile.input.v2.root-shape", + "rootType": "object", + "schemaVersion": 2 + } + }, + "surface": "conformance-profile", + "variant": "union" + }, + { + "direction": "output", + "document": { + "bindingCount": 1, + "commandCount": 3, + "commandExecutionState": "not_run", + "environmentClasses": [ + "local-go" + ], + "failures": [], + "nonClaims": [ + "Local profile CLI fixture does not execute commands." + ], + "optionalSurfaceCount": 0, + "preconditionedBindingCount": 0, + "profileId": "local", + "profileResolutionState": "resolved", + "purpose": "Local proof profile.", + "requiredSurfaceCount": 1, + "requirementCount": 1, + "scenarioCount": 1, + "schemaVersion": 2, + "surfaceCount": 1, + "surfaces": [ + "surface.local" + ], + "verifyCommands": [ + "go test ./...", + "go test ./... -run TestFalsification", + "go test ./... -run TestPositive" + ], + "witnessMappingCount": 2 + }, + "surface": "conformance-profile", + "variant": "profile" + }, + { + "direction": "output", + "document": { + "contract": { + "closed": true, + "compatibilitySummary": [ + "schemaVersion=2", + "root-shape-only definition proofkit.conformance-profile.output.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.conformance-profile.output.v2", + "nativeOutputWitnessSelector": { + "command": "go test ./internal/command/conformanceprofile -run '^TestBuildProfileResolvesRequiredSurfaceAndRejectsMissingSurface$'", + "evidenceClass": "source_checkout", + "path": "internal/command/conformanceprofile/conformanceprofile_test.go", + "test": "TestBuildProfileResolvesRequiredSurfaceAndRejectsMissingSurface" + }, + "nativeSource": { + "canonicalDigest": "sha256:fba2ae58bf66cfc43834368dec84ca85abd11e08a541786dc59eafaf7628c322", + "evidenceClass": "source_checkout", + "path": "internal/command/conformanceprofile" + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ], + "rootDefinitionDigest": "sha256:b3b1ed639969bba5213bb70e0a4ab9f86b69f180ad677ced78e600f87d55fb4f", + "rootDefinitionRef": "proofkit.conformance-profile.output.v2.root-shape", + "rootType": "object", + "schemaVersion": 2 + } + }, + "surface": "conformance-profile", + "variant": "union" + }, + { + "direction": "input", + "document": { + "contract": { + "closed": true, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.evidence-graph.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.evidence-graph.input.v1", + "nativeAdmissionWitnessSelector": { + "command": "go test ./internal/command/requirementbinding -run '^TestBuildEvidenceGraphBuildsGraphAndRejectsFailedReport$'", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementbinding/projections_test.go", + "test": "TestBuildEvidenceGraphBuildsGraphAndRejectsFailedReport" + }, + "nativeSource": { + "canonicalDigest": "sha256:ded6cdd4f6a6f12b7cf2b75e08f58e3960c6d51e7e0c7516b92750d239976aec", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementbinding" + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-003" + ], + "rootDefinitionDigest": "sha256:7ffe236ab203cf411dbfda6008ec8c10cc15691261031d4ed9ed90d81525dc12", + "rootDefinitionRef": "proofkit.evidence-graph.input.v1.root-shape", + "rootType": "object", + "schemaVersion": 1 + } + }, + "surface": "evidence-graph", + "variant": "union" + }, + { + "direction": "output", + "document": { + "contract": { + "closed": true, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.evidence-graph.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.evidence-graph.output.v1", + "nativeOutputWitnessSelector": { + "command": "go test ./internal/command/requirementbinding -run '^TestBuildEvidenceGraphBuildsGraphAndRejectsFailedReport$'", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementbinding/projections_test.go", + "test": "TestBuildEvidenceGraphBuildsGraphAndRejectsFailedReport" + }, + "nativeSource": { + "canonicalDigest": "sha256:ded6cdd4f6a6f12b7cf2b75e08f58e3960c6d51e7e0c7516b92750d239976aec", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementbinding" + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-003" + ], + "rootDefinitionDigest": "sha256:2be3545f9329bb4e4ba4971fc04bf5811959cf0087fd7ab3a7c2c8b45b8d714a", + "rootDefinitionRef": "proofkit.evidence-graph.output.v1.root-shape", + "rootType": "object", + "schemaVersion": 1 + } + }, + "surface": "evidence-graph", + "variant": "union" + }, + { + "direction": "input", + "document": { + "baseCommit": "base-sha", + "baseRef": "main", + "changedBindingRecordIds": [], + "changedPaths": [ + "docs/specs/proofkit-cli-impact/requirements.v1.json", + "internal/app/cli_abi_test.go" + ], + "changedRequirementIds": [ + "REQ-PROOFKIT-CLI-IMPACT-001" + ], + "changedWitnessPathCoverage": [ + { + "path": "internal/app/cli_abi_test.go", + "routes": [ + { + "bindingRecordId": "sha256:1e5d0eec1e1c97631b7ed0efd7abe2098619be102620ec69ace2e5823f671ec6", + "resolutionOrderIndex": 0, + "role": "positive", + "selector": "internal/app/cli_abi_test.go::positive_impact", + "witnessRouteId": "sha256:2afbbd9a905e6886c3fe137a3b55969c263112805c33d7a3cb3767571ab90fd6" + }, + { + "bindingRecordId": "sha256:1e5d0eec1e1c97631b7ed0efd7abe2098619be102620ec69ace2e5823f671ec6", + "resolutionOrderIndex": 1, + "role": "falsification", + "selector": "internal/app/cli_abi_test.go::negative_impact", + "witnessRouteId": "sha256:6d21b451f44c488c6b9f06b56f2abe0ac2f22f12a05b10ac8d181d0a1714b334" + } + ] + } + ], + "generatedArtifactRules": [], + "headCommit": null, + "headRef": "feature/impact", + "ignoredProofLikePaths": [], + "nonClaims": [ + "CLI impact input composition fixture does not execute native tests.", + "CLI impact proof-like policy fixture does not prove proof adequacy." + ], + "obligationCatalog": [ + { + "bindingRecordId": "sha256:1e5d0eec1e1c97631b7ed0efd7abe2098619be102620ec69ace2e5823f671ec6", + "blockingStatus": "blocking", + "commands": [ + "go test ./internal/app" + ], + "declaredMutationResistanceClaimId": "claim.no_known_advisory_gap", + "declaredWitnessRoutes": [ + { + "bindingRecordId": "sha256:1e5d0eec1e1c97631b7ed0efd7abe2098619be102620ec69ace2e5823f671ec6", + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 0, + "role": "positive", + "selector": "internal/app/cli_abi_test.go::positive_impact", + "verifyCommands": [ + "go test ./internal/app" + ], + "witnessRouteId": "sha256:2afbbd9a905e6886c3fe137a3b55969c263112805c33d7a3cb3767571ab90fd6" + }, + { + "bindingRecordId": "sha256:1e5d0eec1e1c97631b7ed0efd7abe2098619be102620ec69ace2e5823f671ec6", + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 1, + "role": "falsification", + "selector": "internal/app/cli_abi_test.go::negative_impact", + "verifyCommands": [ + "go test ./internal/app" + ], + "witnessRouteId": "sha256:6d21b451f44c488c6b9f06b56f2abe0ac2f22f12a05b10ac8d181d0a1714b334" + } + ], + "preconditioned": false, + "requiredEnvironmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-CLI-IMPACT-001", + "scenarioId": "proofkit.cli.impact::scenario", + "surfaceId": "proofkit.cli.impact" + } + ], + "preexistingFailures": [], + "proofLikePaths": [], + "schemaVersion": 2 + }, + "surface": "impact", + "variant": "direct" + }, + { + "direction": "input", + "document": { + "contract": { + "closed": true, + "compatibilitySummary": [ + "schemaVersion=2", + "root-shape-only definition proofkit.impact.input.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.impact.input.v2", + "nativeAdmissionWitnessSelector": { + "command": "go test ./internal/command/impact -run '^TestBuildRoutesChangedRecordToObligationAndRejectsUnboundProofChange$'", + "evidenceClass": "source_checkout", + "path": "internal/command/impact/impact_test.go", + "test": "TestBuildRoutesChangedRecordToObligationAndRejectsUnboundProofChange" + }, + "nativeSource": { + "canonicalDigest": "sha256:a3735158aab6ad0c6c8d5173cc07906b900165addb2a3a8b58eefe95a8abc365", + "evidenceClass": "source_checkout", + "path": "internal/command/impact" + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ], + "rootDefinitionDigest": "sha256:aa30fb2ab02bc74ef18f9cf599d319d06a44e374851c07f5d921e86654a42e57", + "rootDefinitionRef": "proofkit.impact.input.v2.root-shape", + "rootType": "object", + "schemaVersion": 2 + } + }, + "surface": "impact", + "variant": "union" + }, + { + "direction": "output", + "document": { + "baseCommit": "base-sha", + "baseRef": "main", + "changedPaths": [ + "docs/specs/proofkit-cli-impact/requirements.v1.json", + "internal/app/cli_abi_test.go" + ], + "changedRequirementIds": [ + "REQ-PROOFKIT-CLI-IMPACT-001" + ], + "failures": [], + "headCommit": null, + "headRef": "feature/impact", + "impactState": "ok", + "nonClaims": [ + "CLI impact input composition fixture does not execute native tests.", + "CLI impact proof-like policy fixture does not prove proof adequacy.", + "Impact reports classify caller-owned changed paths and proof records only.", + "Impact reports do not decide the consuming repository fallback policy for unbound or unknown proof changes.", + "Impact reports do not run git, scan repositories, execute witnesses, authenticate receipts, approve merge, or prove proof freshness." + ], + "obligations": [ + { + "bindingRecordId": "sha256:1e5d0eec1e1c97631b7ed0efd7abe2098619be102620ec69ace2e5823f671ec6", + "blockingStatus": "blocking", + "changeReasons": [ + "proof_witness_changed", + "requirement_changed" + ], + "commands": [ + "go test ./internal/app" + ], + "declaredMutationResistanceClaimId": "claim.no_known_advisory_gap", + "declaredWitnessRoutes": [ + { + "bindingRecordId": "sha256:1e5d0eec1e1c97631b7ed0efd7abe2098619be102620ec69ace2e5823f671ec6", + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 0, + "role": "positive", + "selector": "internal/app/cli_abi_test.go::positive_impact", + "verifyCommands": [ + "go test ./internal/app" + ], + "witnessRouteId": "sha256:2afbbd9a905e6886c3fe137a3b55969c263112805c33d7a3cb3767571ab90fd6" + }, + { + "bindingRecordId": "sha256:1e5d0eec1e1c97631b7ed0efd7abe2098619be102620ec69ace2e5823f671ec6", + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 1, + "role": "falsification", + "selector": "internal/app/cli_abi_test.go::negative_impact", + "verifyCommands": [ + "go test ./internal/app" + ], + "witnessRouteId": "sha256:6d21b451f44c488c6b9f06b56f2abe0ac2f22f12a05b10ac8d181d0a1714b334" + } + ], + "preconditioned": false, + "requiredEnvironmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-CLI-IMPACT-001", + "scenarioId": "proofkit.cli.impact::scenario", + "surfaceId": "proofkit.cli.impact", + "witnessRoutes": [ + { + "bindingRecordId": "sha256:1e5d0eec1e1c97631b7ed0efd7abe2098619be102620ec69ace2e5823f671ec6", + "resolutionOrderIndex": 0, + "role": "positive", + "selector": "internal/app/cli_abi_test.go::positive_impact", + "witnessRouteId": "sha256:2afbbd9a905e6886c3fe137a3b55969c263112805c33d7a3cb3767571ab90fd6" + }, + { + "bindingRecordId": "sha256:1e5d0eec1e1c97631b7ed0efd7abe2098619be102620ec69ace2e5823f671ec6", + "resolutionOrderIndex": 1, + "role": "falsification", + "selector": "internal/app/cli_abi_test.go::negative_impact", + "witnessRouteId": "sha256:6d21b451f44c488c6b9f06b56f2abe0ac2f22f12a05b10ac8d181d0a1714b334" + } + ] + } + ], + "schemaVersion": 2, + "unboundProofChanges": [] + }, + "surface": "impact", + "variant": "direct" + }, + { + "direction": "output", + "document": { + "contract": { + "closed": true, + "compatibilitySummary": [ + "schemaVersion=2", + "root-shape-only definition proofkit.impact.output.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.impact.output.v2", + "nativeOutputWitnessSelector": { + "command": "go test ./internal/command/impact -run '^TestBuildRoutesChangedRecordToObligationAndRejectsUnboundProofChange$'", + "evidenceClass": "source_checkout", + "path": "internal/command/impact/impact_test.go", + "test": "TestBuildRoutesChangedRecordToObligationAndRejectsUnboundProofChange" + }, + "nativeSource": { + "canonicalDigest": "sha256:a3735158aab6ad0c6c8d5173cc07906b900165addb2a3a8b58eefe95a8abc365", + "evidenceClass": "source_checkout", + "path": "internal/command/impact" + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ], + "rootDefinitionDigest": "sha256:e6dacd55cace039ca8e24bdec628b1498d0c12bbfcdb60c46854ab2047c3f2d7", + "rootDefinitionRef": "proofkit.impact.output.v2.root-shape", + "rootType": "object", + "schemaVersion": 2 + } + }, + "surface": "impact", + "variant": "union" + }, + { + "direction": "input", + "document": { + "agentReportRoutes": [ + { + "artifactPath": "artifacts/proofkit/report.json", + "command": "agentic-proofkit render", + "expectedUpdatePath": "docs/specs/proofkit/requirements.v1.json", + "reportKind": "proofkit.report", + "schemaId": "proofkit.schema", + "taskType": "proofkit.review" + } + ], + "blockingRequirements": { + "dispositionPolicy": "all_blocking_requirements_must_be_witnessed_or_explicitly_deferred", + "explicitlyDeferredRequirements": 0, + "requirements": [ + { + "evidence": "docs/specs/proofkit/requirements.v1.json", + "owner": "proofkit", + "requirementId": "REQ-PROOFKIT-001", + "status": "witness_backed" + } + ], + "totalBlockingRequirements": 1, + "unmappedRequirements": 0, + "witnessBackedRequirements": 1 + }, + "cacheNegativeChecks": [], + "cacheScheduler": { + "cacheKeyInputs": [ + "go.mod" + ], + "destructiveConcurrencyAllowed": false, + "invalidationInputs": [ + "go.sum" + ], + "maxParallelGroups": 1, + "parallelGroups": [ + "local" + ], + "schedulerPolicy": "bounded_parallel_groups" + }, + "falsePositiveBudget": { + "dispositionOwner": "proofkit", + "enforcementMode": "non_blocking", + "maxAllowedFalsePositiveCount": 0, + "sampleWindowRuns": 1 + }, + "impactDemos": [ + { + "demoId": "proofkit.cli.impact.demo", + "generatedMirrorPaths": [ + "docs/generated/requirements.md" + ], + "impactInput": { + "baseCommit": "base", + "baseRef": "main", + "changedBindingRecordIds": [], + "changedPaths": [ + "docs/specs/proofkit/requirements.v1.json" + ], + "changedRequirementIds": [ + "REQ-PROOFKIT-001" + ], + "changedWitnessPathCoverage": [], + "generatedArtifactRules": [], + "headCommit": null, + "headRef": "feature/proofkit", + "ignoredProofLikePaths": [], + "obligationCatalog": [ + { + "bindingRecordId": "sha256:d460cbea38175960f7dc9b0ea95827b560dc52a3bd4b21dd390148b52b8f0b10", + "blockingStatus": "blocking", + "commands": [ + "go test ./..." + ], + "declaredMutationResistanceClaimId": "claim.unverified", + "declaredWitnessRoutes": [ + { + "bindingRecordId": "sha256:d460cbea38175960f7dc9b0ea95827b560dc52a3bd4b21dd390148b52b8f0b10", + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 0, + "role": "positive", + "selector": "internal/proofkit/witness_test.go::TestWitness", + "verifyCommands": [ + "go test ./..." + ], + "witnessRouteId": "sha256:3661d326952707970d5ccb8c50cce5677c774723d9ef2c20f39c2319108095d0" + }, + { + "bindingRecordId": "sha256:d460cbea38175960f7dc9b0ea95827b560dc52a3bd4b21dd390148b52b8f0b10", + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 1, + "role": "falsification", + "selector": "internal/proofkit/witness_test.go::TestWitnessFalsification", + "verifyCommands": [ + "go test ./..." + ], + "witnessRouteId": "sha256:f1740d608b6d75ffd7d2beec0f78121b025b285714c9cfc3ca6c295c5067dc29" + } + ], + "preconditioned": false, + "requiredEnvironmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-001", + "scenarioId": "proofkit.surface::proofkit.scenario", + "surfaceId": "proofkit.surface" + } + ], + "preexistingFailures": [], + "proofLikePaths": [], + "schemaVersion": 2, + "unboundProofChangeRationale": "No unbound proof-like path changed." + }, + "sourceOwnedChangedPaths": [ + "docs/specs/proofkit/requirements.v1.json" + ] + } + ], + "infrastructureBudget": { + "copiedVerifierFileCount": 0, + "customRuleCount": 0, + "customRules": [], + "manualTruthSurfaceCount": 0, + "manualUpdateStepCount": 0, + "maxCustomRuleCount": 0, + "maxManualTruthSurfaceCount": 0, + "maxManualUpdateStepCount": 0, + "maxProfileLines": 100, + "profileLines": 20 + }, + "nonClaims": [ + "CLI pilot fixture does not claim rollout readiness." + ], + "packageVersionRef": "package.json", + "pilotId": "proofkit.cli.pilot.first", + "pilotMode": "non_blocking", + "profile": { + "commandMatcherBridge": "none", + "customRuleBoundary": "profile_only", + "primaryLanguages": [ + "go" + ], + "repositoryClass": "go_cli", + "repositoryId": "proofkit.cli", + "structuredWitnessCommands": true, + "verifierCodeCopied": false + }, + "rollback": { + "owner": "proofkit", + "rollbackCommand": "agentic-proofkit previous-version", + "versionPin": "package.json" + }, + "rolloutClaim": false, + "schemaVersion": 2, + "timingBudget": { + "maxAddedSeconds": 5, + "measuredSeparately": true, + "reportArtifactPath": "artifacts/proofkit/timing.json", + "trackedFixtureAsBaseline": false + } + }, + "surface": "pilot-admission", + "variant": "direct" + }, + { + "direction": "input", + "document": { + "input": { + "agentReportRoutes": [ + { + "artifactPath": "artifacts/proofkit/report.json", + "command": "agentic-proofkit render", + "expectedUpdatePath": "docs/specs/proofkit/requirements.v1.json", + "reportKind": "proofkit.report", + "schemaId": "proofkit.schema", + "taskType": "proofkit.review" + } + ], + "blockingRequirements": { + "dispositionPolicy": "all_blocking_requirements_must_be_witnessed_or_explicitly_deferred", + "explicitlyDeferredRequirements": 0, + "requirements": [ + { + "evidence": "docs/specs/proofkit/requirements.v1.json", + "owner": "proofkit", + "requirementId": "REQ-PROOFKIT-001", + "status": "witness_backed" + } + ], + "totalBlockingRequirements": 1, + "unmappedRequirements": 0, + "witnessBackedRequirements": 1 + }, + "cacheNegativeChecks": [], + "cacheScheduler": { + "cacheKeyInputs": [ + "go.mod" + ], + "destructiveConcurrencyAllowed": false, + "invalidationInputs": [ + "go.sum" + ], + "maxParallelGroups": 1, + "parallelGroups": [ + "local" + ], + "schedulerPolicy": "bounded_parallel_groups" + }, + "falsePositiveBudget": { + "dispositionOwner": "proofkit", + "enforcementMode": "non_blocking", + "maxAllowedFalsePositiveCount": 0, + "sampleWindowRuns": 1 + }, + "impactDemos": [ + { + "demoId": "proofkit.cli.impact.demo", + "generatedMirrorPaths": [ + "docs/generated/requirements.md" + ], + "impactInput": { + "baseCommit": "base", + "baseRef": "main", + "changedBindingRecordIds": [], + "changedPaths": [ + "docs/specs/proofkit/requirements.v1.json" + ], + "changedRequirementIds": [ + "REQ-PROOFKIT-001" + ], + "changedWitnessPathCoverage": [], + "generatedArtifactRules": [], + "headCommit": null, + "headRef": "feature/proofkit", + "ignoredProofLikePaths": [], + "obligationCatalog": [ + { + "bindingRecordId": "sha256:d460cbea38175960f7dc9b0ea95827b560dc52a3bd4b21dd390148b52b8f0b10", + "blockingStatus": "blocking", + "commands": [ + "go test ./..." + ], + "declaredMutationResistanceClaimId": "claim.unverified", + "declaredWitnessRoutes": [ + { + "bindingRecordId": "sha256:d460cbea38175960f7dc9b0ea95827b560dc52a3bd4b21dd390148b52b8f0b10", + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 0, + "role": "positive", + "selector": "internal/proofkit/witness_test.go::TestWitness", + "verifyCommands": [ + "go test ./..." + ], + "witnessRouteId": "sha256:3661d326952707970d5ccb8c50cce5677c774723d9ef2c20f39c2319108095d0" + }, + { + "bindingRecordId": "sha256:d460cbea38175960f7dc9b0ea95827b560dc52a3bd4b21dd390148b52b8f0b10", + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 1, + "role": "falsification", + "selector": "internal/proofkit/witness_test.go::TestWitnessFalsification", + "verifyCommands": [ + "go test ./..." + ], + "witnessRouteId": "sha256:f1740d608b6d75ffd7d2beec0f78121b025b285714c9cfc3ca6c295c5067dc29" + } + ], + "preconditioned": false, + "requiredEnvironmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-001", + "scenarioId": "proofkit.surface::proofkit.scenario", + "surfaceId": "proofkit.surface" + } + ], + "preexistingFailures": [], + "proofLikePaths": [], + "schemaVersion": 2, + "unboundProofChangeRationale": "No unbound proof-like path changed." + }, + "sourceOwnedChangedPaths": [ + "docs/specs/proofkit/requirements.v1.json" + ] + } + ], + "infrastructureBudget": { + "copiedVerifierFileCount": 0, + "customRuleCount": 0, + "customRules": [], + "manualTruthSurfaceCount": 0, + "manualUpdateStepCount": 0, + "maxCustomRuleCount": 0, + "maxManualTruthSurfaceCount": 0, + "maxManualUpdateStepCount": 0, + "maxProfileLines": 100, + "profileLines": 20 + }, + "nonClaims": [ + "CLI pilot fixture does not claim rollout readiness." + ], + "packageVersionRef": "package.json", + "pilotId": "proofkit.cli.pilot.first", + "pilotMode": "non_blocking", + "profile": { + "commandMatcherBridge": "none", + "customRuleBoundary": "profile_only", + "primaryLanguages": [ + "go" + ], + "repositoryClass": "go_cli", + "repositoryId": "proofkit.cli", + "structuredWitnessCommands": true, + "verifierCodeCopied": false + }, + "rollback": { + "owner": "proofkit", + "rollbackCommand": "agentic-proofkit previous-version", + "versionPin": "package.json" + }, + "rolloutClaim": false, + "schemaVersion": 2, + "timingBudget": { + "maxAddedSeconds": 5, + "measuredSeparately": true, + "reportArtifactPath": "artifacts/proofkit/timing.json", + "trackedFixtureAsBaseline": false + } + }, + "schema": "proofkit.pilot-admission.v2", + "stackDiverseInput": { + "agentReportRoutes": [ + { + "artifactPath": "artifacts/proofkit/report.json", + "command": "agentic-proofkit render", + "expectedUpdatePath": "docs/specs/proofkit/requirements.v1.json", + "reportKind": "proofkit.report", + "schemaId": "proofkit.schema", + "taskType": "proofkit.review" + } + ], + "blockingRequirements": { + "dispositionPolicy": "all_blocking_requirements_must_be_witnessed_or_explicitly_deferred", + "explicitlyDeferredRequirements": 0, + "requirements": [ + { + "evidence": "docs/specs/proofkit/requirements.v1.json", + "owner": "proofkit", + "requirementId": "REQ-PROOFKIT-001", + "status": "witness_backed" + } + ], + "totalBlockingRequirements": 1, + "unmappedRequirements": 0, + "witnessBackedRequirements": 1 + }, + "cacheNegativeChecks": [ + { + "checkId": "proofkit.cache.package_version", + "evidence": "Cache invalidates on package_version changes.", + "expectedOutcome": "invalidate_output", + "invalidatedInputClass": "package_version", + "liveOrCredentialedCacheable": false + }, + { + "checkId": "proofkit.cache.profile", + "evidence": "Cache invalidates on profile changes.", + "expectedOutcome": "invalidate_output", + "invalidatedInputClass": "profile", + "liveOrCredentialedCacheable": false + }, + { + "checkId": "proofkit.cache.schema", + "evidence": "Cache invalidates on schema changes.", + "expectedOutcome": "invalidate_output", + "invalidatedInputClass": "schema", + "liveOrCredentialedCacheable": false + }, + { + "checkId": "proofkit.cache.source", + "evidence": "Cache invalidates on source changes.", + "expectedOutcome": "invalidate_output", + "invalidatedInputClass": "source", + "liveOrCredentialedCacheable": false + } + ], + "cacheScheduler": { + "cacheKeyInputs": [ + "go.mod" + ], + "destructiveConcurrencyAllowed": false, + "invalidationInputs": [ + "go.sum" + ], + "maxParallelGroups": 1, + "parallelGroups": [ + "local" + ], + "schedulerPolicy": "bounded_parallel_groups" + }, + "falsePositiveBudget": { + "dispositionOwner": "proofkit", + "enforcementMode": "non_blocking", + "maxAllowedFalsePositiveCount": 0, + "sampleWindowRuns": 1 + }, + "impactDemos": [ + { + "demoId": "proofkit.cli.impact.demo.stack", + "generatedMirrorPaths": [ + "docs/generated/requirements.md" + ], + "impactInput": { + "baseCommit": "base", + "baseRef": "main", + "changedBindingRecordIds": [ + "sha256:d460cbea38175960f7dc9b0ea95827b560dc52a3bd4b21dd390148b52b8f0b10" + ], + "changedPaths": [ + "docs/specs/proofkit/requirements.v1.json", + "internal/proofkit/witness_test.go" + ], + "changedRequirementIds": [ + "REQ-PROOFKIT-001" + ], + "changedWitnessPathCoverage": [ + { + "path": "internal/proofkit/witness_test.go", + "routes": [ + { + "bindingRecordId": "sha256:d460cbea38175960f7dc9b0ea95827b560dc52a3bd4b21dd390148b52b8f0b10", + "resolutionOrderIndex": 0, + "role": "positive", + "selector": "internal/proofkit/witness_test.go::TestWitness", + "witnessRouteId": "sha256:3661d326952707970d5ccb8c50cce5677c774723d9ef2c20f39c2319108095d0" + } + ] + } + ], + "generatedArtifactRules": [], + "headCommit": null, + "headRef": "feature/proofkit", + "ignoredProofLikePaths": [], + "obligationCatalog": [ + { + "bindingRecordId": "sha256:d460cbea38175960f7dc9b0ea95827b560dc52a3bd4b21dd390148b52b8f0b10", + "blockingStatus": "blocking", + "commands": [ + "go test ./..." + ], + "declaredMutationResistanceClaimId": "claim.unverified", + "declaredWitnessRoutes": [ + { + "bindingRecordId": "sha256:d460cbea38175960f7dc9b0ea95827b560dc52a3bd4b21dd390148b52b8f0b10", + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 0, + "role": "positive", + "selector": "internal/proofkit/witness_test.go::TestWitness", + "verifyCommands": [ + "go test ./..." + ], + "witnessRouteId": "sha256:3661d326952707970d5ccb8c50cce5677c774723d9ef2c20f39c2319108095d0" + }, + { + "bindingRecordId": "sha256:d460cbea38175960f7dc9b0ea95827b560dc52a3bd4b21dd390148b52b8f0b10", + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 1, + "role": "falsification", + "selector": "internal/proofkit/witness_test.go::TestWitnessFalsification", + "verifyCommands": [ + "go test ./..." + ], + "witnessRouteId": "sha256:f1740d608b6d75ffd7d2beec0f78121b025b285714c9cfc3ca6c295c5067dc29" + } + ], + "preconditioned": false, + "requiredEnvironmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-001", + "scenarioId": "proofkit.surface::proofkit.scenario", + "surfaceId": "proofkit.surface" + } + ], + "preexistingFailures": [], + "proofLikePaths": [], + "schemaVersion": 2, + "unboundProofChangeRationale": "No unbound proof-like path changed." + }, + "sourceOwnedChangedPaths": [ + "docs/specs/proofkit/requirements.v1.json" + ] + } + ], + "infrastructureBudget": { + "copiedVerifierFileCount": 0, + "customRuleCount": 0, + "customRules": [], + "manualTruthSurfaceCount": 0, + "manualUpdateStepCount": 0, + "maxCustomRuleCount": 0, + "maxManualTruthSurfaceCount": 0, + "maxManualUpdateStepCount": 0, + "maxProfileLines": 100, + "profileLines": 20 + }, + "nonClaims": [ + "CLI pilot fixture does not claim rollout readiness." + ], + "packageVersionRef": "package.json", + "pilotId": "proofkit.cli.pilot.stack-diverse", + "pilotMode": "non_blocking", + "profile": { + "commandMatcherBridge": "none", + "customRuleBoundary": "profile_only", + "primaryLanguages": [ + "go" + ], + "repositoryClass": "go_cli", + "repositoryId": "proofkit.cli", + "structuredWitnessCommands": true, + "verifierCodeCopied": false + }, + "rollback": { + "owner": "proofkit", + "rollbackCommand": "agentic-proofkit previous-version", + "versionPin": "package.json" + }, + "rolloutClaim": false, + "schemaVersion": 2, + "stackDiversity": { + "baselinePilotId": "proofkit.cli.pilot.first", + "dimensions": [ + { + "baseline": "baseline-docs_spec_layout", + "candidate": "candidate-docs_spec_layout", + "dimension": "docs_spec_layout", + "evidence": "docs/evidence/docs_spec_layout.md" + }, + { + "baseline": "baseline-generated_artifact_policy", + "candidate": "candidate-generated_artifact_policy", + "dimension": "generated_artifact_policy", + "evidence": "docs/evidence/generated_artifact_policy.md" + }, + { + "baseline": "baseline-language_runtime_test_shape", + "candidate": "candidate-language_runtime_test_shape", + "dimension": "language_runtime_test_shape", + "evidence": "docs/evidence/language_runtime_test_shape.md" + }, + { + "baseline": "baseline-proof_environment_classes", + "candidate": "candidate-proof_environment_classes", + "dimension": "proof_environment_classes", + "evidence": "docs/evidence/proof_environment_classes.md" + }, + { + "baseline": "baseline-repository_topology", + "candidate": "candidate-repository_topology", + "dimension": "repository_topology", + "evidence": "docs/evidence/repository_topology.md" + } + ] + }, + "timingBudget": { + "maxAddedSeconds": 5, + "measuredSeparately": true, + "reportArtifactPath": "artifacts/proofkit/timing.json", + "trackedFixtureAsBaseline": false + } + } + }, + "surface": "pilot-admission", + "variant": "envelope" + }, + { + "direction": "input", + "document": { + "contract": { + "closed": true, + "compatibilitySummary": [ + "schemaVersion=2", + "root-shape-only definition proofkit.pilot-admission.input.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.pilot-admission.input.v2", + "nativeAdmissionWitnessSelector": { + "command": "go test ./internal/command/pilotadmission -run '^TestBuildRejectsUnknownPilotContractField$'", + "evidenceClass": "source_checkout", + "path": "internal/command/pilotadmission/pilotadmission_test.go", + "test": "TestBuildRejectsUnknownPilotContractField" + }, + "nativeSource": { + "canonicalDigest": "sha256:439a7efece3c003b1463758f206b236ce261a2f566ec59b4bddd83f815035850", + "evidenceClass": "source_checkout", + "path": "internal/command/pilotadmission" + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ], + "rootDefinitionDigest": "sha256:e3f15bcf1927483bfda42ca49d7f6624c0d0fadac81ccab1266c1a6ae8f528f1", + "rootDefinitionRef": "proofkit.pilot-admission.input.v2.root-shape", + "rootType": "object", + "schemaVersion": 2 + } + }, + "surface": "pilot-admission", + "variant": "union" + }, + { + "direction": "output", + "document": { + "contract": { + "closed": true, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.pilot-admission.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.pilot-admission.output.v1", + "nativeOutputWitnessSelector": { + "command": "go test ./internal/app -run '^TestStandaloneMultiVariantCommandsUseExactRootShapes$'", + "evidenceClass": "source_checkout", + "path": "internal/app/cli_abi_test.go", + "test": "TestStandaloneMultiVariantCommandsUseExactRootShapes" + }, + "nativeSources": [ + { + "canonicalDigest": "sha256:6e888333db0edc8a6eaeb91dad38b876f0bbee22a43c1bd2424e155e5a58dc88", + "evidenceClass": "source_checkout", + "path": "internal/app" + }, + { + "canonicalDigest": "sha256:439a7efece3c003b1463758f206b236ce261a2f566ec59b4bddd83f815035850", + "evidenceClass": "source_checkout", + "path": "internal/command/pilotadmission" + } + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ], + "rootDefinitionDigest": "sha256:b4e4dc05858e4ef86804e153db5e901c7c2af354d8d88b572ea86358e1194a57", + "rootDefinitionRef": "proofkit.pilot-admission.output.v1.root-shape", + "rootType": "union", + "schemaVersion": 1 + } + }, + "surface": "pilot-admission", + "variant": "union" + }, + { + "direction": "input", + "document": { + "contract": { + "closed": true, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.proof-slice.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.proof-slice.input.v1", + "nativeAdmissionWitnessSelector": { + "command": "go test ./internal/command/requirementbinding -run '^TestBuildProofSliceSelectsRequirementsAndRejectsFailedReport$'", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementbinding/projections_test.go", + "test": "TestBuildProofSliceSelectsRequirementsAndRejectsFailedReport" + }, + "nativeSource": { + "canonicalDigest": "sha256:ded6cdd4f6a6f12b7cf2b75e08f58e3960c6d51e7e0c7516b92750d239976aec", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementbinding" + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-003" + ], + "rootDefinitionDigest": "sha256:71914c95c70dbace0ebce1dc4609624e3b91c994cd8c5adc20ed84d4200f0ac9", + "rootDefinitionRef": "proofkit.proof-slice.input.v1.root-shape", + "rootType": "object", + "schemaVersion": 1 + } + }, + "surface": "proof-slice", + "variant": "union" + }, + { + "direction": "output", + "document": { + "contract": { + "closed": true, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.proof-slice.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.proof-slice.output.v1", + "nativeOutputWitnessSelector": { + "command": "go test ./internal/command/requirementbinding -run '^TestBuildProofSliceSelectsRequirementsAndRejectsFailedReport$'", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementbinding/projections_test.go", + "test": "TestBuildProofSliceSelectsRequirementsAndRejectsFailedReport" + }, + "nativeSource": { + "canonicalDigest": "sha256:ded6cdd4f6a6f12b7cf2b75e08f58e3960c6d51e7e0c7516b92750d239976aec", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementbinding" + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-003" + ], + "rootDefinitionDigest": "sha256:5fbdf25c90a2020525f97453c9d9fe5a774fe0e2eacf00d1b2aea3ff5f4fa0f0", + "rootDefinitionRef": "proofkit.proof-slice.output.v1.root-shape", + "rootType": "object", + "schemaVersion": 1 + } + }, + "surface": "proof-slice", + "variant": "union" + }, + { + "direction": "input", + "document": { + "contract": { + "closed": true, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-bindings.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.requirement-bindings.input.v1", + "nativeAdmissionWitnessSelector": { + "command": "go test ./internal/command/requirementbinding -run '^TestBuildReportFailsUnknownRequirementBinding$'", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementbinding/projections_test.go", + "test": "TestBuildReportFailsUnknownRequirementBinding" + }, + "nativeSource": { + "canonicalDigest": "sha256:ded6cdd4f6a6f12b7cf2b75e08f58e3960c6d51e7e0c7516b92750d239976aec", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementbinding" + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-003" + ], + "rootDefinitionDigest": "sha256:e7da70a2267f2ca13771e864bba6a1a0416132c2d56decb5db9f9694de021daf", + "rootDefinitionRef": "proofkit.requirement-bindings.input.v1.root-shape", + "rootType": "object", + "schemaVersion": 1 + } + }, + "surface": "requirement-bindings", + "variant": "union" + }, + { + "direction": "output", + "document": { + "contract": { + "closed": true, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.requirement-bindings.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.requirement-bindings.output.v1", + "nativeOutputWitnessSelector": { + "command": "go test ./internal/command/requirementbinding -run '^TestBuildReportFailsUnknownRequirementBinding$'", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementbinding/projections_test.go", + "test": "TestBuildReportFailsUnknownRequirementBinding" + }, + "nativeSource": { + "canonicalDigest": "sha256:ded6cdd4f6a6f12b7cf2b75e08f58e3960c6d51e7e0c7516b92750d239976aec", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementbinding" + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-003" + ], + "rootDefinitionDigest": "sha256:f31dd686f28fce4bf74265b557dc25428179cbc282753b4bea01439cd98f73c2", + "rootDefinitionRef": "proofkit.requirement-bindings.output.v1.root-shape", + "rootType": "object", + "schemaVersion": 1 + } + }, + "surface": "requirement-bindings", + "variant": "union" + }, + { + "direction": "input", + "document": { + "contract": { + "closed": true, + "compatibilitySummary": [ + "workspace mode: schemaVersion=2", + "workspace mode: workspaceId", + "workspace mode: context=proofkit.requirement-context schemaVersion=2 with strict v1 adapter", + "workspace mode: diffInput=proofkit.requirement-semantic-diff-input schemaVersion=2 (optional)", + "workspace mode: graphInput=proofkit.requirement-traceability-graph-input schemaVersion=2 (optional)", + "--session-mode values: browse|one-shot-question", + "one-shot-question requires --view workspace --serve --open", + "--session-timeout-seconds is 1..7200 and requires one-shot-question", + "source|proof|coverage|spec-tree modes retain their owner input contracts", + "root-shape-only definition proofkit.requirement-browser-server.input.v3.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.requirement-browser-server.input.v3", + "jsonLayoutRule": "allowed for non-serving JSON plans; serving and one-shot modes reject the global layout option because one-shot terminal packets are fixed compact JSON", + "nativeAdmissionWitnessSelector": { + "command": "go test ./internal/command/requirementbrowser -run '^TestV2DigestCoverageProjections$'", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementbrowser/workspace_test.go", + "test": "TestV2DigestCoverageProjections" + }, + "nativeSource": { + "canonicalDigest": "sha256:df4d6ca7f6f08342e48e44dad7cfbbcb25869c039814efd875e6435b4aeefa33", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementbrowser" + }, + "oneShotRequirements": { + "open": true, + "serve": true, + "view": "workspace" + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-009", + "REQ-PROOFKIT-SPEC-021" + ], + "rootDefinitionDigest": "sha256:e6af5b9edb1284ab2ff2f1a276a6b924127b99810cd9446bffbc971f638a0997", + "rootDefinitionRef": "proofkit.requirement-browser-server.input.v3.root-shape", + "rootType": "object", + "schemaVersion": 3, + "sessionModeValues": [ + "browse", + "one-shot-question" + ], + "sessionTimeoutSeconds": { + "maximum": 7200, + "minimum": 1, + "requiresSessionMode": "one-shot-question" + }, + "workspaceOptionalFields": [ + "diffInput", + "graphInput" + ], + "workspaceRequiredFields": [ + "context", + "schemaVersion", + "workspaceId" + ] + } + }, + "surface": "requirement-browser-server", + "variant": "union" + }, + { + "direction": "output", + "document": { + "snapshotId": "sha256:d91a190ca30bc3123b0618b960f6fefd22ee62a8a4a9c23556faa3766da6dc11" + }, + "surface": "requirement-browser-server", + "variant": "workspace-runtime" + }, + { + "direction": "output", + "document": { + "coverageSha256": "sha256:1f1c73fa67ffbdcc60d051e8f8b24f9bb16a52af25ea8217991712d00f54f83f", + "identities": { + "bindingRecordIds": [ + "sha256:d3dd5656d5d1196af01be86473b156d9ed4338f907f38321803ec0293b5a3061" + ], + "witnessRouteIds": [ + "sha256:6c2d9be9266cab6a9be42147798b25643dc92665f6f644e2443996f175598f82", + "sha256:dab948afb2765258e891a77b2c56d135e7e80168698adfa5cd092c3f8e2401bb" + ] + } + }, + "surface": "requirement-context-compose", + "variant": "coverage-runtime" + }, + { + "direction": "output", + "document": { + "coverageSha256": "sha256:75985707ffb0d45514c238f0c4cd329ebdba3a4f22812390c32a9ae260b831b5", + "identities": { + "bindingRecordIds": [ + "sha256:d3dd5656d5d1196af01be86473b156d9ed4338f907f38321803ec0293b5a3061" + ], + "witnessRouteIds": [ + "sha256:6c2d9be9266cab6a9be42147798b25643dc92665f6f644e2443996f175598f82", + "sha256:dab948afb2765258e891a77b2c56d135e7e80168698adfa5cd092c3f8e2401bb" + ] + } + }, + "surface": "requirement-context-slice", + "variant": "coverage-runtime" + }, + { + "direction": "input", + "document": { + "contract": { + "authority": "requirement coverage input compose input contract", + "closed": true, + "commonRequiredFields": [ + "coverageUniverse", + "localEnvironmentPolicy", + "options", + "ownerInvariantRegistry", + "requirementSource", + "schemaVersion", + "selectedOwnerIds", + "viewInputId" + ], + "compatibilitySummary": [ + "schemaVersion=2", + "root-shape-only definition proofkit.requirement-coverage-input-compose.input.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.requirement-coverage-input-compose.input.v2", + "modes": { + "direct": { + "admittedInventoryAuthority": "caller_owned_inventory", + "forbids": [ + "compactProofContract", + "normalizedTestEvidenceInventory" + ], + "nonClaim": "Direct mode admits passed child reports and rejects source-set or candidate inventory inputs that need an explicit normalization/materialization envelope.", + "rejects": [ + "caller_owned_inventory_source_set", + "caller_owned_test_discovery_candidate_inventory" + ], + "requires": [ + "requirementProofBinding", + "testEvidenceInventory" + ] + }, + "normalized": { + "forbids": [ + "requirementProofBinding", + "testEvidenceInventory" + ], + "nonClaim": "Normalized mode preserves source-set provenance through normalizedTestEvidenceInventory.", + "requires": [ + "compactProofContract", + "normalizedTestEvidenceInventory" + ] + } + }, + "nativeAdmissionWitnessSelector": { + "command": "go test ./internal/command/requirementcoverageinput -run '^TestBuildComposesInputPreservesDeclaredUniverseAndAllowsDownstreamFailures$'", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementcoverageinput/requirementcoverageinput_test.go", + "test": "TestBuildComposesInputPreservesDeclaredUniverseAndAllowsDownstreamFailures" + }, + "nativeSource": { + "canonicalDigest": "sha256:4c10b1cd675af7d7753bb72a44fca1bb9a568bcd4d3733ee1907e6c4e22757f8", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementcoverageinput" + }, + "nonClaims": [ + "Coverage input composition does not execute native tests, authenticate receipts, decide freshness, or approve merge." + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-006" + ], + "rootDefinitionDigest": "sha256:f6300d6d0f80f5066fe4079433978132ee2bb1df75e602e59cf3dc37a385d3ce", + "rootDefinitionRef": "proofkit.requirement-coverage-input-compose.input.v2.root-shape", + "rootType": "object", + "schemaVersion": 2 + } + }, + "surface": "requirement-coverage-input-compose", + "variant": "union" + }, + { + "direction": "input", + "document": { + "compactProofContract": { + "authority_state": "caller_owned_declaration", + "binding_columns": [ + "requirement_id", + "surface_id", + "scenario_id", + "invariant_role", + "owned_invariant", + "blocking_status", + "required_environment_classes", + "positive_witness", + "falsification_witness", + "verify_commands", + "declared_mutation_resistance_claim_id" + ], + "bindings": [ + [ + "REQ-PROOFKIT-CLI-COVERAGE-001", + "proofkit.cli.coverage", + "proofkit.cli.coverage::scenario", + "contract", + "proofkit.cli.coverage.invariant", + "blocking", + [ + "local-go" + ], + [ + "internal/app/cli_abi_test.go::positive", + [ + "local-go" + ], + [ + "go test ./internal/app" + ], + 0 + ], + [ + "internal/app/cli_abi_test.go::falsification", + [ + "local-go" + ], + [ + "go test ./internal/app" + ], + 1 + ], + [ + "go test ./internal/app" + ], + "claim.no_known_advisory_gap" + ] + ], + "contract_id": "proofkit.cli.coverage.compact", + "contract_kind": "requirement_proof_route_declaration", + "non_claims": [ + "CLI compact fixture does not execute witnesses." + ], + "normalization_profile": "proofkit.compact.declaration.v2", + "schema_version": 2, + "surface_columns": [ + "surface_id", + "required_environment_classes", + "preconditioned_environment_classes" + ], + "surfaces": [ + [ + "proofkit.cli.coverage", + [ + "local-go" + ], + [] + ] + ], + "witness_columns": [ + "selector", + "environment_classes", + "verify_commands", + "resolution_order_index" + ] + }, + "composerInputId": "proofkit.cli.coverage.compose", + "coverageUniverse": { + "authority": "caller_owned_inventory", + "codeSurfaces": [ + { + "ownerId": "proofkit.cli.coverage", + "path": "internal/app", + "surfaceId": "proofkit.cli.coverage.code" + } + ], + "commandRefs": [], + "completenessDeclaration": "selected_owner_surfaces", + "nonClaims": [ + "CLI coverage composer universe is selected-owner scope only." + ], + "ownerIds": [ + "proofkit.cli.coverage" + ], + "schemaVersion": 1, + "specSurfaces": [ + { + "ownerId": "proofkit.cli.coverage", + "path": "docs/specs/proofkit-cli-coverage/requirements.v1.json", + "surfaceId": "proofkit.cli.coverage.spec" + } + ], + "testSurfaces": [], + "universeId": "proofkit.cli.coverage.compose.universe" + }, + "localEnvironmentPolicy": { + "authority": "caller_provided", + "localEnvironmentClasses": [ + "local-go" + ] + }, + "normalizedTestEvidenceInventory": { + "entrySources": [], + "inputPaths": [], + "inventory": { + "authority": "caller_owned_inventory", + "entries": [ + { + "commandRefs": [ + "proofkit.cli.coverage.command" + ], + "evidenceClass": "declared_semantic_falsifier_route", + "falsifier": { + "dominanceGroup": "cli.coverage", + "falsifierId": "falsifier.cli.coverage", + "negativeCaseId": "case.cli.coverage", + "supersedes": [], + "wrongImplementationClassId": "wrong.cli.coverage" + }, + "nonClaims": [], + "oracle": { + "assertionSummary": "Missing inventory leaves coverage failed.", + "expectedPublicOutcome": "failed report with diagnostic", + "oracleId": "oracle.cli.coverage", + "oracleKind": "negative_exit_and_diagnostic" + }, + "ownerId": "proofkit.cli.coverage", + "ownerInvariantRefs": [], + "requirementRefs": [ + "REQ-PROOFKIT-CLI-COVERAGE-001" + ], + "selector": "go test ./internal/app -run TestCoverage", + "sourcePath": "internal/app/cli_abi_test.go", + "testId": "test.cli.coverage.semantic", + "witnessRefs": [] + } + ], + "inventoryId": "proofkit.cli.coverage.inventory", + "nonClaims": [ + "CLI coverage inventory fixture does not execute native tests." + ], + "schemaVersion": 1 + }, + "nonClaims": [ + "CLI normalized inventory fixture does not execute tests." + ], + "normalizedInventoryId": "proofkit.cli.coverage.inventory.normalized", + "normalizedKind": "proofkit.test-evidence-inventory.normalized", + "schemaVersion": 1, + "sourceAuthority": "caller_owned_inventory", + "sourceColumns": [ + "source_id", + "path", + "sha256", + "role", + "non_claims" + ], + "sourceCount": 0, + "sources": [] + }, + "options": { + "scope": "graph" + }, + "ownerInvariantRegistry": null, + "requirementSource": { + "nonClaims": [ + "CLI coverage composer fixture source does not own native tests." + ], + "overviewPath": "docs/specs/proofkit-cli-coverage/overview.md", + "requirements": [ + { + "claimLevel": "blocking", + "deferral": null, + "invariant": "CLI coverage input composition preserves direct view input ABI.", + "lifecycle": { + "evidenceRefs": [], + "replacementRequirementIds": [], + "state": "active" + }, + "nonClaimRefs": [], + "nonClaims": [ + "CLI coverage composer fixture does not execute tests." + ], + "ownerId": "proofkit.cli.coverage", + "proofBindingRefs": [ + "proofkit/requirement-bindings.json" + ], + "requirementId": "REQ-PROOFKIT-CLI-COVERAGE-001", + "riskClass": "high", + "updatePolicy": { + "requiresImpactDeclaration": true, + "requiresProofBindingReview": true, + "reviewOwnerId": "proofkit.cli.coverage" + } + } + ], + "requirementsPath": "docs/specs/proofkit-cli-coverage/requirements.v1.json", + "schemaVersion": 1, + "sourceId": "proofkit.cli.coverage.source", + "specPackagePath": "docs/specs/proofkit-cli-coverage" + }, + "schemaVersion": 2, + "selectedOwnerIds": [ + "proofkit.cli.coverage" + ], + "viewInputId": "proofkit.cli.coverage.compose.view" + }, + "surface": "requirement-coverage-input-compose", + "variant": "wrapper" + }, + { + "direction": "output", + "document": { + "contract": { + "authority": "requirement coverage input compose output contract", + "closed": true, + "compatibilitySummary": [ + "compactProofContract", + "coverageUniverse", + "localEnvironmentPolicy", + "options", + "ownerInvariantRegistry", + "requirementProofBinding", + "requirementSource", + "schemaVersion", + "testEvidenceInventory", + "viewInputId", + "root-shape-only definition proofkit.requirement-coverage-input-compose.output.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.requirement-coverage-input-compose.output.v2", + "nativeOutputWitnessSelector": { + "command": "go test ./internal/command/requirementcoverageinput -run '^TestBuildComposesInputPreservesDeclaredUniverseAndAllowsDownstreamFailures$'", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementcoverageinput/requirementcoverageinput_test.go", + "test": "TestBuildComposesInputPreservesDeclaredUniverseAndAllowsDownstreamFailures" + }, + "nativeSource": { + "canonicalDigest": "sha256:4c10b1cd675af7d7753bb72a44fca1bb9a568bcd4d3733ee1907e6c4e22757f8", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementcoverageinput" + }, + "nonClaims": [ + "Coverage input composition emits a direct requirement-coverage-view input and does not execute native tests, authenticate receipts, decide freshness, or approve merge." + ], + "optionalFields": [ + "normalizedTestEvidenceInventory" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-006" + ], + "provenanceRule": "When normalizedTestEvidenceInventory is present, it preserves the admitted source-set envelope and its nested inventory must equal testEvidenceInventory.", + "requiredFields": [ + "compactProofContract", + "coverageUniverse", + "localEnvironmentPolicy", + "options", + "ownerInvariantRegistry", + "requirementProofBinding", + "requirementSource", + "schemaVersion", + "testEvidenceInventory", + "viewInputId" + ], + "rootDefinitionDigest": "sha256:0f264c996a058fb8aff102e42b95a4610d3a584b35687cc870b3d16c9589cfed", + "rootDefinitionRef": "proofkit.requirement-coverage-input-compose.output.v2.root-shape", + "rootType": "object", + "schemaVersion": 2 + } + }, + "surface": "requirement-coverage-input-compose", + "variant": "union" + }, + { + "direction": "output", + "document": { + "compactProofContract": { + "authority_state": "caller_owned_declaration", + "binding_columns": [ + "requirement_id", + "surface_id", + "scenario_id", + "invariant_role", + "owned_invariant", + "blocking_status", + "required_environment_classes", + "positive_witness", + "falsification_witness", + "verify_commands", + "declared_mutation_resistance_claim_id" + ], + "bindings": [ + [ + "REQ-PROOFKIT-CLI-COVERAGE-001", + "proofkit.cli.coverage", + "proofkit.cli.coverage::scenario", + "contract", + "proofkit.cli.coverage.invariant", + "blocking", + [ + "local-go" + ], + [ + "internal/app/cli_abi_test.go::positive", + [ + "local-go" + ], + [ + "go test ./internal/app" + ], + 0 + ], + [ + "internal/app/cli_abi_test.go::falsification", + [ + "local-go" + ], + [ + "go test ./internal/app" + ], + 1 + ], + [ + "go test ./internal/app" + ], + "claim.no_known_advisory_gap" + ] + ], + "contract_id": "proofkit.cli.coverage.compact", + "contract_kind": "requirement_proof_route_declaration", + "non_claims": [ + "CLI compact fixture does not execute witnesses." + ], + "normalization_profile": "proofkit.compact.declaration.v2", + "schema_version": 2, + "surface_columns": [ + "surface_id", + "required_environment_classes", + "preconditioned_environment_classes" + ], + "surfaces": [ + [ + "proofkit.cli.coverage", + [ + "local-go" + ], + [] + ] + ], + "witness_columns": [ + "selector", + "environment_classes", + "verify_commands", + "resolution_order_index" + ] + }, + "coverageUniverse": { + "authority": "caller_owned_inventory", + "codeSurfaces": [ + { + "ownerId": "proofkit.cli.coverage", + "path": "internal/app", + "surfaceId": "proofkit.cli.coverage.code" + } + ], + "commandRefs": [ + "proofkit.cli.coverage.command" + ], + "completenessDeclaration": "selected_owner_surfaces", + "nonClaims": [ + "CLI coverage composer universe is selected-owner scope only." + ], + "ownerIds": [ + "proofkit.cli.coverage" + ], + "schemaVersion": 1, + "specSurfaces": [ + { + "ownerId": "proofkit.cli.coverage", + "path": "docs/specs/proofkit-cli-coverage/requirements.v1.json", + "surfaceId": "proofkit.cli.coverage.spec" + } + ], + "testSurfaces": [ + { + "ownerId": "proofkit.cli.coverage", + "path": "internal/app/cli_abi_test.go", + "surfaceId": "surface.observed_test.proofkit_cli_coverage.elichplnmjlnkdai" + } + ], + "universeId": "proofkit.cli.coverage.compose.universe" + }, + "localEnvironmentPolicy": { + "authority": "caller_provided", + "localEnvironmentClasses": [ + "local-go" + ] + }, + "normalizedTestEvidenceInventory": { + "entrySources": [], + "inputPaths": [], + "inventory": { + "authority": "caller_owned_inventory", + "entries": [ + { + "commandRefs": [ + "proofkit.cli.coverage.command" + ], + "evidenceClass": "declared_semantic_falsifier_route", + "falsifier": { + "dominanceGroup": "cli.coverage", + "falsifierId": "falsifier.cli.coverage", + "negativeCaseId": "case.cli.coverage", + "supersedes": [], + "wrongImplementationClassId": "wrong.cli.coverage" + }, + "nonClaims": [], + "oracle": { + "assertionSummary": "Missing inventory leaves coverage failed.", + "expectedPublicOutcome": "failed report with diagnostic", + "oracleId": "oracle.cli.coverage", + "oracleKind": "negative_exit_and_diagnostic" + }, + "ownerId": "proofkit.cli.coverage", + "ownerInvariantRefs": [], + "qualityFindings": [], + "requirementRefs": [ + "REQ-PROOFKIT-CLI-COVERAGE-001" + ], + "selector": "go test ./internal/app -run TestCoverage", + "sourcePath": "internal/app/cli_abi_test.go", + "testId": "test.cli.coverage.semantic", + "witnessRefs": [] + } + ], + "inventoryId": "proofkit.cli.coverage.inventory", + "nonClaims": [ + "CLI coverage inventory fixture does not execute native tests.", + "Test evidence inventory reports do not approve merge, release, rollout, or repository policy.", + "Test evidence inventory reports do not authenticate runner output or receipt producers.", + "Test evidence inventory reports do not execute native tests.", + "Test evidence inventory reports do not prove repository inventory completeness.", + "Test evidence inventory reports do not resolve selectors or review caller-authored oracle quality.", + "Test evidence inventory reports do not verify that a caller-declared falsifier supersession dominates the superseded falsifier." + ], + "schemaVersion": 1 + }, + "nonClaims": [ + "CLI normalized inventory fixture does not execute tests." + ], + "normalizedInventoryId": "proofkit.cli.coverage.inventory.normalized", + "normalizedKind": "proofkit.test-evidence-inventory.normalized", + "schemaVersion": 1, + "sourceAuthority": "caller_owned_inventory", + "sourceColumns": [ + "source_id", + "path", + "sha256", + "role", + "non_claims" + ], + "sourceCount": 0, + "sources": [] + }, + "options": { + "scope": "graph" + }, + "ownerInvariantRegistry": null, + "requirementProofBinding": null, + "requirementSource": { + "nonClaims": [ + "CLI coverage composer fixture source does not own native tests." + ], + "overviewPath": "docs/specs/proofkit-cli-coverage/overview.md", + "requirements": [ + { + "claimLevel": "blocking", + "deferral": null, + "invariant": "CLI coverage input composition preserves direct view input ABI.", + "lifecycle": { + "evidenceRefs": [], + "replacementRequirementIds": [], + "state": "active" + }, + "nonClaimRefs": [], + "nonClaims": [ + "CLI coverage composer fixture does not execute tests." + ], + "ownerId": "proofkit.cli.coverage", + "proofBindingRefs": [ + "proofkit/requirement-bindings.json" + ], + "requirementId": "REQ-PROOFKIT-CLI-COVERAGE-001", + "riskClass": "high", + "updatePolicy": { + "requiresImpactDeclaration": true, + "requiresProofBindingReview": true, + "reviewOwnerId": "proofkit.cli.coverage" + } + } + ], + "requirementsPath": "docs/specs/proofkit-cli-coverage/requirements.v1.json", + "schemaVersion": 1, + "sourceId": "proofkit.cli.coverage.source", + "specPackagePath": "docs/specs/proofkit-cli-coverage" + }, + "schemaVersion": 2, + "testEvidenceInventory": { + "authority": "caller_owned_inventory", + "entries": [ + { + "commandRefs": [ + "proofkit.cli.coverage.command" + ], + "evidenceClass": "declared_semantic_falsifier_route", + "falsifier": { + "dominanceGroup": "cli.coverage", + "falsifierId": "falsifier.cli.coverage", + "negativeCaseId": "case.cli.coverage", + "supersedes": [], + "wrongImplementationClassId": "wrong.cli.coverage" + }, + "nonClaims": [], + "oracle": { + "assertionSummary": "Missing inventory leaves coverage failed.", + "expectedPublicOutcome": "failed report with diagnostic", + "oracleId": "oracle.cli.coverage", + "oracleKind": "negative_exit_and_diagnostic" + }, + "ownerId": "proofkit.cli.coverage", + "ownerInvariantRefs": [], + "qualityFindings": [], + "requirementRefs": [ + "REQ-PROOFKIT-CLI-COVERAGE-001" + ], + "selector": "go test ./internal/app -run TestCoverage", + "sourcePath": "internal/app/cli_abi_test.go", + "testId": "test.cli.coverage.semantic", + "witnessRefs": [] + } + ], + "inventoryId": "proofkit.cli.coverage.inventory", + "nonClaims": [ + "CLI coverage inventory fixture does not execute native tests.", + "Test evidence inventory reports do not approve merge, release, rollout, or repository policy.", + "Test evidence inventory reports do not authenticate runner output or receipt producers.", + "Test evidence inventory reports do not execute native tests.", + "Test evidence inventory reports do not prove repository inventory completeness.", + "Test evidence inventory reports do not resolve selectors or review caller-authored oracle quality.", + "Test evidence inventory reports do not verify that a caller-declared falsifier supersession dominates the superseded falsifier." + ], + "schemaVersion": 1 + }, + "viewInputId": "proofkit.cli.coverage.compose.view" + }, + "surface": "requirement-coverage-input-compose", + "variant": "wrapper" + }, + { + "direction": "input", + "document": { + "contract": { + "closed": true, + "compatibilitySummary": [ + "schemaVersion=2", + "root-shape-only definition proofkit.requirement-coverage-view.input.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.requirement-coverage-view.input.v2", + "nativeAdmissionWitnessSelector": { + "command": "go test ./internal/command/requirementcoverageview -run '^TestBuildJSONRejectsRouteOnlyCoverageForBlockingRequirement$'", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementcoverageview/requirementcoverageview_test.go", + "test": "TestBuildJSONRejectsRouteOnlyCoverageForBlockingRequirement" + }, + "nativeSource": { + "canonicalDigest": "sha256:5a60148d843025a2ad7a2b72566792778120fc7979c5ec9c9db8dcac42e7dffd", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementcoverageview" + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-006" + ], + "rootDefinitionDigest": "sha256:f42c9218c861b1e30ec3692fa0097da393e36eea2049d4aac1a05a8b0a40d81d", + "rootDefinitionRef": "proofkit.requirement-coverage-view.input.v2.root-shape", + "rootType": "object", + "schemaVersion": 2 + } + }, + "surface": "requirement-coverage-view", + "variant": "union" + }, + { + "direction": "input", + "document": { + "compactProofContract": { + "authority_state": "caller_owned_declaration", + "binding_columns": [ + "requirement_id", + "surface_id", + "scenario_id", + "invariant_role", + "owned_invariant", + "blocking_status", + "required_environment_classes", + "positive_witness", + "falsification_witness", + "verify_commands", + "declared_mutation_resistance_claim_id" + ], + "bindings": [ + [ + "REQ-PROOFKIT-CLI-COVERAGE-001", + "proofkit.cli.coverage", + "proofkit.cli.coverage::scenario", + "contract", + "proofkit.cli.coverage.invariant", + "blocking", + [ + "local-go" + ], + [ + "internal/app/cli_abi_test.go::positive", + [ + "local-go" + ], + [ + "go test ./internal/app" + ], + 0 + ], + [ + "internal/app/cli_abi_test.go::falsification", + [ + "local-go" + ], + [ + "go test ./internal/app" + ], + 1 + ], + [ + "go test ./internal/app" + ], + "claim.no_known_advisory_gap" + ] + ], + "contract_id": "proofkit.cli.coverage.compact", + "contract_kind": "requirement_proof_route_declaration", + "non_claims": [ + "CLI compact fixture does not execute witnesses." + ], + "normalization_profile": "proofkit.compact.declaration.v2", + "schema_version": 2, + "surface_columns": [ + "surface_id", + "required_environment_classes", + "preconditioned_environment_classes" + ], + "surfaces": [ + [ + "proofkit.cli.coverage", + [ + "local-go" + ], + [] + ] + ], + "witness_columns": [ + "selector", + "environment_classes", + "verify_commands", + "resolution_order_index" + ] + }, + "coverageUniverse": { + "authority": "caller_owned_inventory", + "codeSurfaces": [ + { + "ownerId": "proofkit.cli.coverage", + "path": "internal/app", + "surfaceId": "proofkit.cli.coverage.code" + } + ], + "commandRefs": [ + "proofkit.cli.coverage.command" + ], + "completenessDeclaration": "selected_owner_surfaces", + "nonClaims": [ + "CLI coverage composer universe is selected-owner scope only." + ], + "ownerIds": [ + "proofkit.cli.coverage" + ], + "schemaVersion": 1, + "specSurfaces": [ + { + "ownerId": "proofkit.cli.coverage", + "path": "docs/specs/proofkit-cli-coverage/requirements.v1.json", + "surfaceId": "proofkit.cli.coverage.spec" + } + ], + "testSurfaces": [ + { + "ownerId": "proofkit.cli.coverage", + "path": "internal/app/cli_abi_test.go", + "surfaceId": "surface.observed_test.proofkit_cli_coverage.elichplnmjlnkdai" + } + ], + "universeId": "proofkit.cli.coverage.compose.universe" + }, + "localEnvironmentPolicy": { + "authority": "caller_provided", + "localEnvironmentClasses": [ + "local-go" + ] + }, + "normalizedTestEvidenceInventory": { + "entrySources": [], + "inputPaths": [], + "inventory": { + "authority": "caller_owned_inventory", + "entries": [ + { + "commandRefs": [ + "proofkit.cli.coverage.command" + ], + "evidenceClass": "declared_semantic_falsifier_route", + "falsifier": { + "dominanceGroup": "cli.coverage", + "falsifierId": "falsifier.cli.coverage", + "negativeCaseId": "case.cli.coverage", + "supersedes": [], + "wrongImplementationClassId": "wrong.cli.coverage" + }, + "nonClaims": [], + "oracle": { + "assertionSummary": "Missing inventory leaves coverage failed.", + "expectedPublicOutcome": "failed report with diagnostic", + "oracleId": "oracle.cli.coverage", + "oracleKind": "negative_exit_and_diagnostic" + }, + "ownerId": "proofkit.cli.coverage", + "ownerInvariantRefs": [], + "qualityFindings": [], + "requirementRefs": [ + "REQ-PROOFKIT-CLI-COVERAGE-001" + ], + "selector": "go test ./internal/app -run TestCoverage", + "sourcePath": "internal/app/cli_abi_test.go", + "testId": "test.cli.coverage.semantic", + "witnessRefs": [] + } + ], + "inventoryId": "proofkit.cli.coverage.inventory", + "nonClaims": [ + "CLI coverage inventory fixture does not execute native tests.", + "Test evidence inventory reports do not approve merge, release, rollout, or repository policy.", + "Test evidence inventory reports do not authenticate runner output or receipt producers.", + "Test evidence inventory reports do not execute native tests.", + "Test evidence inventory reports do not prove repository inventory completeness.", + "Test evidence inventory reports do not resolve selectors or review caller-authored oracle quality.", + "Test evidence inventory reports do not verify that a caller-declared falsifier supersession dominates the superseded falsifier." + ], + "schemaVersion": 1 + }, + "nonClaims": [ + "CLI normalized inventory fixture does not execute tests." + ], + "normalizedInventoryId": "proofkit.cli.coverage.inventory.normalized", + "normalizedKind": "proofkit.test-evidence-inventory.normalized", + "schemaVersion": 1, + "sourceAuthority": "caller_owned_inventory", + "sourceColumns": [ + "source_id", + "path", + "sha256", + "role", + "non_claims" + ], + "sourceCount": 0, + "sources": [] + }, + "options": { + "scope": "graph" + }, + "ownerInvariantRegistry": null, + "requirementProofBinding": null, + "requirementSource": { + "nonClaims": [ + "CLI coverage composer fixture source does not own native tests." + ], + "overviewPath": "docs/specs/proofkit-cli-coverage/overview.md", + "requirements": [ + { + "claimLevel": "blocking", + "deferral": null, + "invariant": "CLI coverage input composition preserves direct view input ABI.", + "lifecycle": { + "evidenceRefs": [], + "replacementRequirementIds": [], + "state": "active" + }, + "nonClaimRefs": [], + "nonClaims": [ + "CLI coverage composer fixture does not execute tests." + ], + "ownerId": "proofkit.cli.coverage", + "proofBindingRefs": [ + "proofkit/requirement-bindings.json" + ], + "requirementId": "REQ-PROOFKIT-CLI-COVERAGE-001", + "riskClass": "high", + "updatePolicy": { + "requiresImpactDeclaration": true, + "requiresProofBindingReview": true, + "reviewOwnerId": "proofkit.cli.coverage" + } + } + ], + "requirementsPath": "docs/specs/proofkit-cli-coverage/requirements.v1.json", + "schemaVersion": 1, + "sourceId": "proofkit.cli.coverage.source", + "specPackagePath": "docs/specs/proofkit-cli-coverage" + }, + "schemaVersion": 2, + "testEvidenceInventory": { + "authority": "caller_owned_inventory", + "entries": [ + { + "commandRefs": [ + "proofkit.cli.coverage.command" + ], + "evidenceClass": "declared_semantic_falsifier_route", + "falsifier": { + "dominanceGroup": "cli.coverage", + "falsifierId": "falsifier.cli.coverage", + "negativeCaseId": "case.cli.coverage", + "supersedes": [], + "wrongImplementationClassId": "wrong.cli.coverage" + }, + "nonClaims": [], + "oracle": { + "assertionSummary": "Missing inventory leaves coverage failed.", + "expectedPublicOutcome": "failed report with diagnostic", + "oracleId": "oracle.cli.coverage", + "oracleKind": "negative_exit_and_diagnostic" + }, + "ownerId": "proofkit.cli.coverage", + "ownerInvariantRefs": [], + "qualityFindings": [], + "requirementRefs": [ + "REQ-PROOFKIT-CLI-COVERAGE-001" + ], + "selector": "go test ./internal/app -run TestCoverage", + "sourcePath": "internal/app/cli_abi_test.go", + "testId": "test.cli.coverage.semantic", + "witnessRefs": [] + } + ], + "inventoryId": "proofkit.cli.coverage.inventory", + "nonClaims": [ + "CLI coverage inventory fixture does not execute native tests.", + "Test evidence inventory reports do not approve merge, release, rollout, or repository policy.", + "Test evidence inventory reports do not authenticate runner output or receipt producers.", + "Test evidence inventory reports do not execute native tests.", + "Test evidence inventory reports do not prove repository inventory completeness.", + "Test evidence inventory reports do not resolve selectors or review caller-authored oracle quality.", + "Test evidence inventory reports do not verify that a caller-declared falsifier supersession dominates the superseded falsifier." + ], + "schemaVersion": 1 + }, + "viewInputId": "proofkit.cli.coverage.compose.view" + }, + "surface": "requirement-coverage-view", + "variant": "wrapper" + }, + { + "direction": "output", + "document": { + "bindingRecordId": "sha256:d3dd5656d5d1196af01be86473b156d9ed4338f907f38321803ec0293b5a3061", + "bindingVerifyCommands": [ + "go test ./internal/app" + ], + "declaredWitnessRoutes": [ + { + "bindingRecordId": "sha256:d3dd5656d5d1196af01be86473b156d9ed4338f907f38321803ec0293b5a3061", + "environmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-CLI-COVERAGE-001", + "resolutionOrderIndex": 0, + "role": "positive", + "scenarioId": "proofkit.cli.coverage::scenario", + "selector": "internal/app/cli_abi_test.go::positive", + "surfaceId": "proofkit.cli.coverage", + "verifyCommands": [ + "go test ./internal/app" + ], + "witnessRouteId": "sha256:6c2d9be9266cab6a9be42147798b25643dc92665f6f644e2443996f175598f82" + }, + { + "bindingRecordId": "sha256:d3dd5656d5d1196af01be86473b156d9ed4338f907f38321803ec0293b5a3061", + "environmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-CLI-COVERAGE-001", + "resolutionOrderIndex": 1, + "role": "falsification", + "scenarioId": "proofkit.cli.coverage::scenario", + "selector": "internal/app/cli_abi_test.go::falsification", + "surfaceId": "proofkit.cli.coverage", + "verifyCommands": [ + "go test ./internal/app" + ], + "witnessRouteId": "sha256:dab948afb2765258e891a77b2c56d135e7e80168698adfa5cd092c3f8e2401bb" + } + ], + "environmentClasses": [ + "local-go" + ], + "requiredEnvironmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-CLI-COVERAGE-001", + "scenarioId": "proofkit.cli.coverage::scenario", + "surfaceId": "proofkit.cli.coverage", + "verifyCommands": [ + "go test ./internal/app" + ] + }, + "surface": "requirement-coverage-view", + "variant": "compact-scenario" + }, + { + "direction": "output", + "document": { + "authority": "lookup_only", + "bindingId": "", + "commandCoverage": [ + { + "commandId": "proofkit.cli.coverage.command", + "coverageState": "command_declared_semantic_falsifier_route_present", + "failures": [], + "testIds": [ + "test.cli.coverage.semantic" + ], + "tests": [ + { + "commandRefs": [ + "proofkit.cli.coverage.command" + ], + "dominanceGroup": "cli.coverage", + "evidenceClass": "declared_semantic_falsifier_route", + "expectedPublicOutcome": "failed report with diagnostic", + "falsifierId": "falsifier.cli.coverage", + "negativeCaseId": "case.cli.coverage", + "nonClaims": [], + "oracleId": "oracle.cli.coverage", + "oracleKind": "negative_exit_and_diagnostic", + "oracleSummary": "Missing inventory leaves coverage failed.", + "ownerId": "proofkit.cli.coverage", + "ownerInvariantRefs": [], + "qualityFindings": [], + "requirementRefs": [ + "REQ-PROOFKIT-CLI-COVERAGE-001" + ], + "selector": "go test ./internal/app -run TestCoverage", + "sourcePath": "internal/app/cli_abi_test.go", + "supersedes": [], + "supersessionDeclarationRef": "", + "testId": "test.cli.coverage.semantic", + "witnessRefs": [], + "wrongImplementationClassId": "wrong.cli.coverage" + } + ] + } + ], + "commandCoverageCount": 1, + "completenessDeclaration": "selected_owner_surfaces", + "contractId": "proofkit.cli.coverage.compact", + "coverageBasis": { + "fullRepositoryOutOfScopeSourceRequirements": [], + "ownerIds": [ + "proofkit.cli.coverage" + ], + "testInventoryDigest": "sha256:ae58462633f7a458997bb0fc8eaed48eb1a248c12df28434b60ce12e1b34eb45" + }, + "coverageUniverseId": "proofkit.cli.coverage.compose.universe", + "deadZones": [], + "failureClassifications": [], + "failureCount": 0, + "failures": [], + "guidanceSummary": { + "failureCount": 0, + "nextAction": "Inspect caller-owned coverage classifications before making repository policy decisions.", + "nonClaim": "Guidance summary does not execute tests, approve edits, or replace caller-owned review.", + "state": "passed", + "warningCount": 0 + }, + "nonClaims": [ + "CLI coverage composer fixture source does not own native tests.", + "CLI coverage composer universe is selected-owner scope only.", + "CLI coverage inventory fixture does not execute native tests.", + "Coverage basis commitments prove internal projection consistency only; they do not authenticate source inputs or producer identity.", + "Mapped declaration routes do not prove selector resolution, oracle quality, execution, freshness, trust, sensitivity, or assurance closure.", + "Requirement coverage views are rendered lookup products only.", + "Requirement coverage views classify only caller-owned structured inputs.", + "Requirement coverage views do not scan repositories, execute native tests, authenticate receipts, decide proof freshness, or approve merge.", + "Route-only smoke entries do not satisfy declared semantic-falsifier route mapping.", + "Test evidence inventory reports do not approve merge, release, rollout, or repository policy.", + "Test evidence inventory reports do not authenticate runner output or receipt producers.", + "Test evidence inventory reports do not execute native tests.", + "Test evidence inventory reports do not prove repository inventory completeness.", + "Test evidence inventory reports do not resolve selectors or review caller-authored oracle quality.", + "Test evidence inventory reports do not verify that a caller-declared falsifier supersession dominates the superseded falsifier." + ], + "ownerInvariantCoverage": [], + "ownerInvariantCoverageCount": 0, + "ownerInvariantRegistryId": "", + "proofMode": "compact", + "requirementCoverage": [ + { + "claimLevel": "blocking", + "commandIds": [ + "proofkit.cli.coverage.command" + ], + "coverageState": "mapped_to_declared_semantic_falsifier_route", + "declaredWitnessRoutes": [ + { + "bindingRecordId": "sha256:d3dd5656d5d1196af01be86473b156d9ed4338f907f38321803ec0293b5a3061", + "environmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-CLI-COVERAGE-001", + "resolutionOrderIndex": 0, + "role": "positive", + "scenarioId": "proofkit.cli.coverage::scenario", + "selector": "internal/app/cli_abi_test.go::positive", + "surfaceId": "proofkit.cli.coverage", + "verifyCommands": [ + "go test ./internal/app" + ], + "witnessRouteId": "sha256:6c2d9be9266cab6a9be42147798b25643dc92665f6f644e2443996f175598f82" + }, + { + "bindingRecordId": "sha256:d3dd5656d5d1196af01be86473b156d9ed4338f907f38321803ec0293b5a3061", + "environmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-CLI-COVERAGE-001", + "resolutionOrderIndex": 1, + "role": "falsification", + "scenarioId": "proofkit.cli.coverage::scenario", + "selector": "internal/app/cli_abi_test.go::falsification", + "surfaceId": "proofkit.cli.coverage", + "verifyCommands": [ + "go test ./internal/app" + ], + "witnessRouteId": "sha256:dab948afb2765258e891a77b2c56d135e7e80168698adfa5cd092c3f8e2401bb" + } + ], + "environmentClasses": [ + "local-go" + ], + "evidenceClass": "declared_semantic_falsifier_route", + "failures": [], + "invariant": "CLI coverage input composition preserves direct view input ABI.", + "lifecycleState": "active", + "nonClaims": [ + "CLI coverage composer fixture does not execute tests." + ], + "ownerId": "proofkit.cli.coverage", + "requirementId": "REQ-PROOFKIT-CLI-COVERAGE-001", + "scenarioCount": 1, + "scenarios": [ + { + "bindingRecordId": "sha256:d3dd5656d5d1196af01be86473b156d9ed4338f907f38321803ec0293b5a3061", + "bindingVerifyCommands": [ + "go test ./internal/app" + ], + "declaredWitnessRoutes": [ + { + "bindingRecordId": "sha256:d3dd5656d5d1196af01be86473b156d9ed4338f907f38321803ec0293b5a3061", + "environmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-CLI-COVERAGE-001", + "resolutionOrderIndex": 0, + "role": "positive", + "scenarioId": "proofkit.cli.coverage::scenario", + "selector": "internal/app/cli_abi_test.go::positive", + "surfaceId": "proofkit.cli.coverage", + "verifyCommands": [ + "go test ./internal/app" + ], + "witnessRouteId": "sha256:6c2d9be9266cab6a9be42147798b25643dc92665f6f644e2443996f175598f82" + }, + { + "bindingRecordId": "sha256:d3dd5656d5d1196af01be86473b156d9ed4338f907f38321803ec0293b5a3061", + "environmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-CLI-COVERAGE-001", + "resolutionOrderIndex": 1, + "role": "falsification", + "scenarioId": "proofkit.cli.coverage::scenario", + "selector": "internal/app/cli_abi_test.go::falsification", + "surfaceId": "proofkit.cli.coverage", + "verifyCommands": [ + "go test ./internal/app" + ], + "witnessRouteId": "sha256:dab948afb2765258e891a77b2c56d135e7e80168698adfa5cd092c3f8e2401bb" + } + ], + "environmentClasses": [ + "local-go" + ], + "requiredEnvironmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-CLI-COVERAGE-001", + "scenarioId": "proofkit.cli.coverage::scenario", + "surfaceId": "proofkit.cli.coverage", + "verifyCommands": [ + "go test ./internal/app" + ] + } + ], + "specPath": "docs/specs/proofkit-cli-coverage/requirements.v1.json", + "testIds": [ + "test.cli.coverage.semantic" + ], + "tests": [ + { + "commandRefs": [ + "proofkit.cli.coverage.command" + ], + "dominanceGroup": "cli.coverage", + "evidenceClass": "declared_semantic_falsifier_route", + "expectedPublicOutcome": "failed report with diagnostic", + "falsifierId": "falsifier.cli.coverage", + "negativeCaseId": "case.cli.coverage", + "nonClaims": [], + "oracleId": "oracle.cli.coverage", + "oracleKind": "negative_exit_and_diagnostic", + "oracleSummary": "Missing inventory leaves coverage failed.", + "ownerId": "proofkit.cli.coverage", + "ownerInvariantRefs": [], + "qualityFindings": [], + "requirementRefs": [ + "REQ-PROOFKIT-CLI-COVERAGE-001" + ], + "selector": "go test ./internal/app -run TestCoverage", + "sourcePath": "internal/app/cli_abi_test.go", + "supersedes": [], + "supersessionDeclarationRef": "", + "testId": "test.cli.coverage.semantic", + "witnessRefs": [], + "wrongImplementationClassId": "wrong.cli.coverage" + } + ], + "verifyCommands": [ + "go test ./internal/app" + ] + } + ], + "requirementCoverageCount": 1, + "schemaVersion": 3, + "sourceId": "proofkit.cli.coverage.source", + "state": "passed", + "testInventoryId": "proofkit.cli.coverage.inventory", + "unmappedTests": [], + "viewInputId": "proofkit.cli.coverage.compose.view", + "viewKind": "proofkit.requirement-coverage-view", + "warningClassifications": [], + "warningCount": 0, + "warnings": [] + }, + "surface": "requirement-coverage-view", + "variant": "root" + }, + { + "direction": "output", + "document": { + "contract": { + "authority": "requirement coverage view output contract", + "classificationIds": [ + "declared_dead_zone", + "failed_test_inventory", + "missing_requirement_binding", + "missing_declared_test_route", + "nonsemantic_command_evidence", + "nonsemantic_governance_evidence", + "not_applicable_with_reason", + "owner_scope_violation", + "proof_route_candidate_only", + "routing_smoke_only", + "unclassified_gap", + "unknown_reference" + ], + "classificationRecords": { + "failureClassifications": { + "severity": "failure", + "sourceField": "failures" + }, + "warningClassifications": { + "severity": "warning", + "sourceField": "warnings" + } + }, + "closed": true, + "compatibilitySummary": [ + "coverageBasis", + "failureClassifications", + "failures", + "ownerInvariantCoverage", + "state", + "warningClassifications", + "warnings", + "root-shape-only definition proofkit.requirement-coverage-view.output.v3.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.requirement-coverage-view.output.v3", + "nativeOutputWitnessSelector": { + "command": "go test ./internal/command/requirementcoverageview -run '^TestBuildJSONRejectsRouteOnlyCoverageForBlockingRequirement$'", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementcoverageview/requirementcoverageview_test.go", + "test": "TestBuildJSONRejectsRouteOnlyCoverageForBlockingRequirement" + }, + "nativeSource": { + "canonicalDigest": "sha256:5a60148d843025a2ad7a2b72566792778120fc7979c5ec9c9db8dcac42e7dffd", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementcoverageview" + }, + "nonClaims": [ + "Classification records describe Proofkit coverage diagnostics only; consumer repositories own severity policy, merge admission, escalation, and native witness execution.", + "Coverage basis commitments prove internal projection consistency only; they do not authenticate source inputs or producer identity." + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-006" + ], + "requiredFields": [ + "coverageBasis", + "failureClassifications", + "failures", + "ownerInvariantCoverage", + "state", + "warningClassifications", + "warnings" + ], + "rootDefinitionDigest": "sha256:2365e4778d6c24c0af9507ce93bfba0a89c4ba9ece8da1c5b25b4581d6b80fa1", + "rootDefinitionRef": "proofkit.requirement-coverage-view.output.v3.root-shape", + "rootType": "object", + "schemaVersion": 3 + } + }, + "surface": "requirement-coverage-view", + "variant": "union" + }, + { + "direction": "input", + "document": { + "contract": { + "closed": true, + "compatibilitySummary": [ + "schemaVersion=2", + "root-shape-only definition proofkit.requirement-impact-input-compose.input.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.requirement-impact-input-compose.input.v2", + "nativeAdmissionWitnessSelector": { + "command": "go test ./internal/command/requirementimpactinput -run '^TestBuildComposesInputAndRoutesChangedBlockingRequirement$'", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementimpactinput/requirementimpactinput_test.go", + "test": "TestBuildComposesInputAndRoutesChangedBlockingRequirement" + }, + "nativeSource": { + "canonicalDigest": "sha256:28ab57ec44b4b4190db3c3ff50358c5cebdd36b625d1cfa07492fe9dac63f6e3", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementimpactinput" + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-010" + ], + "rootDefinitionDigest": "sha256:1ca9c341fecac869c675302fc547865674bb42605b7ed66b3733e3db15e85150", + "rootDefinitionRef": "proofkit.requirement-impact-input-compose.input.v2.root-shape", + "rootType": "object", + "schemaVersion": 2 + } + }, + "surface": "requirement-impact-input-compose", + "variant": "union" + }, + { + "direction": "input", + "document": { + "baseCommit": "base-sha", + "baseCompactProofContract": { + "authority_state": "caller_owned_declaration", + "binding_columns": [ + "requirement_id", + "surface_id", + "scenario_id", + "invariant_role", + "owned_invariant", + "blocking_status", + "required_environment_classes", + "positive_witness", + "falsification_witness", + "verify_commands", + "declared_mutation_resistance_claim_id" + ], + "bindings": [ + [ + "REQ-PROOFKIT-CLI-IMPACT-001", + "proofkit.cli.impact", + "proofkit.cli.impact::scenario", + "contract", + "proofkit.cli.impact.invariant", + "blocking", + [ + "local-go" + ], + [ + "internal/app/cli_abi_test.go::positive_impact", + [ + "local-go" + ], + [ + "go test ./internal/app" + ], + 0 + ], + [ + "internal/app/cli_abi_test.go::negative_impact", + [ + "local-go" + ], + [ + "go test ./internal/app" + ], + 1 + ], + [ + "go test ./internal/app" + ], + "claim.no_known_advisory_gap" + ] + ], + "contract_id": "proofkit.cli.impact.compact", + "contract_kind": "requirement_proof_route_declaration", + "non_claims": [ + "CLI impact compact fixture does not execute witnesses." + ], + "normalization_profile": "proofkit.compact.declaration.v2", + "schema_version": 2, + "surface_columns": [ + "surface_id", + "required_environment_classes", + "preconditioned_environment_classes" + ], + "surfaces": [ + [ + "proofkit.cli.impact", + [ + "local-go" + ], + [] + ] + ], + "witness_columns": [ + "selector", + "environment_classes", + "verify_commands", + "resolution_order_index" + ] + }, + "baseRef": "main", + "baseRequirementSources": [ + { + "nonClaims": [ + "CLI impact composer base source does not own native tests." + ], + "overviewPath": "docs/specs/proofkit-cli-impact/overview.md", + "requirements": [ + { + "claimLevel": "blocking", + "deferral": null, + "invariant": "CLI impact input composition preserves baseline impact routing.", + "lifecycle": { + "evidenceRefs": [], + "replacementRequirementIds": [], + "state": "active" + }, + "nonClaimRefs": [], + "nonClaims": [ + "CLI impact composer fixture does not execute tests." + ], + "ownerId": "proofkit.cli.impact", + "proofBindingRefs": [ + "proofkit/requirement-bindings.json" + ], + "requirementId": "REQ-PROOFKIT-CLI-IMPACT-001", + "riskClass": "high", + "updatePolicy": { + "requiresImpactDeclaration": true, + "requiresProofBindingReview": true, + "reviewOwnerId": "proofkit.cli.impact" + } + } + ], + "requirementsPath": "docs/specs/proofkit-cli-impact/requirements.v1.json", + "schemaVersion": 1, + "sourceId": "proofkit.cli.impact.source", + "specPackagePath": "docs/specs/proofkit-cli-impact" + } + ], + "changedPathSources": [ + { + "paths": [ + "docs/specs/proofkit-cli-impact/requirements.v1.json", + "internal/app/cli_abi_test.go" + ], + "sourceId": "git_diff" + } + ], + "composerInputId": "proofkit.cli.impact.compose", + "currentCompactProofContract": { + "authority_state": "caller_owned_declaration", + "binding_columns": [ + "requirement_id", + "surface_id", + "scenario_id", + "invariant_role", + "owned_invariant", + "blocking_status", + "required_environment_classes", + "positive_witness", + "falsification_witness", + "verify_commands", + "declared_mutation_resistance_claim_id" + ], + "bindings": [ + [ + "REQ-PROOFKIT-CLI-IMPACT-001", + "proofkit.cli.impact", + "proofkit.cli.impact::scenario", + "contract", + "proofkit.cli.impact.invariant", + "blocking", + [ + "local-go" + ], + [ + "internal/app/cli_abi_test.go::positive_impact", + [ + "local-go" + ], + [ + "go test ./internal/app" + ], + 0 + ], + [ + "internal/app/cli_abi_test.go::negative_impact", + [ + "local-go" + ], + [ + "go test ./internal/app" + ], + 1 + ], + [ + "go test ./internal/app" + ], + "claim.no_known_advisory_gap" + ] + ], + "contract_id": "proofkit.cli.impact.compact", + "contract_kind": "requirement_proof_route_declaration", + "non_claims": [ + "CLI impact compact fixture does not execute witnesses." + ], + "normalization_profile": "proofkit.compact.declaration.v2", + "schema_version": 2, + "surface_columns": [ + "surface_id", + "required_environment_classes", + "preconditioned_environment_classes" + ], + "surfaces": [ + [ + "proofkit.cli.impact", + [ + "local-go" + ], + [] + ] + ], + "witness_columns": [ + "selector", + "environment_classes", + "verify_commands", + "resolution_order_index" + ] + }, + "currentRequirementSources": [ + { + "nonClaims": [ + "CLI impact composer current source does not own native tests." + ], + "overviewPath": "docs/specs/proofkit-cli-impact/overview.md", + "requirements": [ + { + "claimLevel": "blocking", + "deferral": null, + "invariant": "CLI impact input composition preserves changed requirement routing.", + "lifecycle": { + "evidenceRefs": [], + "replacementRequirementIds": [], + "state": "active" + }, + "nonClaimRefs": [], + "nonClaims": [ + "CLI impact composer fixture does not execute tests." + ], + "ownerId": "proofkit.cli.impact", + "proofBindingRefs": [ + "proofkit/requirement-bindings.json" + ], + "requirementId": "REQ-PROOFKIT-CLI-IMPACT-001", + "riskClass": "high", + "updatePolicy": { + "requiresImpactDeclaration": true, + "requiresProofBindingReview": true, + "reviewOwnerId": "proofkit.cli.impact" + } + } + ], + "requirementsPath": "docs/specs/proofkit-cli-impact/requirements.v1.json", + "schemaVersion": 1, + "sourceId": "proofkit.cli.impact.source", + "specPackagePath": "docs/specs/proofkit-cli-impact" + } + ], + "generatedArtifactPolicyState": { + "source": "generated_artifact_verifier", + "state": "complete", + "uncoveredGeneratedPaths": [] + }, + "generatedArtifactRules": [], + "headCommit": null, + "headRef": "feature/impact", + "localEnvironmentPolicy": { + "localEnvironmentClasses": [ + "local-go" + ] + }, + "nonClaims": [ + "CLI impact input composition fixture does not execute native tests." + ], + "preexistingFailures": [], + "proofBindingSourcePaths": [ + "proofkit/requirement-bindings.json" + ], + "proofLikePathPolicy": { + "ignoredProofLikePaths": [], + "nonClaims": [ + "CLI impact proof-like policy fixture does not prove proof adequacy." + ], + "proofLikePathPatterns": [] + }, + "schemaVersion": 2 + }, + "surface": "requirement-impact-input-compose", + "variant": "wrapper" + }, + { + "direction": "output", + "document": { + "contract": { + "authority": "requirement impact input compose output contract", + "closed": true, + "compatibilitySummary": [ + "baseCommit", + "baseRef", + "changedBindingRecordIds", + "changedPaths", + "changedRequirementIds", + "changedWitnessPathCoverage", + "generatedArtifactRules", + "headCommit", + "headRef", + "ignoredProofLikePaths", + "nonClaims", + "obligationCatalog", + "preexistingFailures", + "proofLikePaths", + "schemaVersion", + "root-shape-only definition proofkit.requirement-impact-input-compose.output.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.requirement-impact-input-compose.output.v2", + "nativeOutputWitnessSelector": { + "command": "go test ./internal/command/requirementimpactinput -run '^TestBuildComposesInputAndRoutesChangedBlockingRequirement$'", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementimpactinput/requirementimpactinput_test.go", + "test": "TestBuildComposesInputAndRoutesChangedBlockingRequirement" + }, + "nativeSource": { + "canonicalDigest": "sha256:28ab57ec44b4b4190db3c3ff50358c5cebdd36b625d1cfa07492fe9dac63f6e3", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementimpactinput" + }, + "nonClaims": [ + "Impact input composition emits a direct impact command input and does not execute native tests, authenticate receipts, decide freshness, or approve merge." + ], + "optionalFields": [ + "unboundProofChangeRationale" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-010" + ], + "requiredFields": [ + "baseCommit", + "baseRef", + "changedBindingRecordIds", + "changedPaths", + "changedRequirementIds", + "changedWitnessPathCoverage", + "generatedArtifactRules", + "headCommit", + "headRef", + "ignoredProofLikePaths", + "nonClaims", + "obligationCatalog", + "preexistingFailures", + "proofLikePaths", + "schemaVersion" + ], + "rootDefinitionDigest": "sha256:68a5ab234566459ce56772aac9a5c5b471647753390dbc1c2a62cabf0f489ef2", + "rootDefinitionRef": "proofkit.requirement-impact-input-compose.output.v2.root-shape", + "rootType": "object", + "schemaVersion": 2 + } + }, + "surface": "requirement-impact-input-compose", + "variant": "union" + }, + { + "direction": "output", + "document": { + "baseCommit": "base-sha", + "baseRef": "main", + "changedBindingRecordIds": [], + "changedPaths": [ + "docs/specs/proofkit-cli-impact/requirements.v1.json", + "internal/app/cli_abi_test.go" + ], + "changedRequirementIds": [ + "REQ-PROOFKIT-CLI-IMPACT-001" + ], + "changedWitnessPathCoverage": [ + { + "path": "internal/app/cli_abi_test.go", + "routes": [ + { + "bindingRecordId": "sha256:1e5d0eec1e1c97631b7ed0efd7abe2098619be102620ec69ace2e5823f671ec6", + "resolutionOrderIndex": 0, + "role": "positive", + "selector": "internal/app/cli_abi_test.go::positive_impact", + "witnessRouteId": "sha256:2afbbd9a905e6886c3fe137a3b55969c263112805c33d7a3cb3767571ab90fd6" + }, + { + "bindingRecordId": "sha256:1e5d0eec1e1c97631b7ed0efd7abe2098619be102620ec69ace2e5823f671ec6", + "resolutionOrderIndex": 1, + "role": "falsification", + "selector": "internal/app/cli_abi_test.go::negative_impact", + "witnessRouteId": "sha256:6d21b451f44c488c6b9f06b56f2abe0ac2f22f12a05b10ac8d181d0a1714b334" + } + ] + } + ], + "generatedArtifactRules": [], + "headCommit": null, + "headRef": "feature/impact", + "ignoredProofLikePaths": [], + "nonClaims": [ + "CLI impact input composition fixture does not execute native tests.", + "CLI impact proof-like policy fixture does not prove proof adequacy." + ], + "obligationCatalog": [ + { + "bindingRecordId": "sha256:1e5d0eec1e1c97631b7ed0efd7abe2098619be102620ec69ace2e5823f671ec6", + "blockingStatus": "blocking", + "commands": [ + "go test ./internal/app" + ], + "declaredMutationResistanceClaimId": "claim.no_known_advisory_gap", + "declaredWitnessRoutes": [ + { + "bindingRecordId": "sha256:1e5d0eec1e1c97631b7ed0efd7abe2098619be102620ec69ace2e5823f671ec6", + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 0, + "role": "positive", + "selector": "internal/app/cli_abi_test.go::positive_impact", + "verifyCommands": [ + "go test ./internal/app" + ], + "witnessRouteId": "sha256:2afbbd9a905e6886c3fe137a3b55969c263112805c33d7a3cb3767571ab90fd6" + }, + { + "bindingRecordId": "sha256:1e5d0eec1e1c97631b7ed0efd7abe2098619be102620ec69ace2e5823f671ec6", + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 1, + "role": "falsification", + "selector": "internal/app/cli_abi_test.go::negative_impact", + "verifyCommands": [ + "go test ./internal/app" + ], + "witnessRouteId": "sha256:6d21b451f44c488c6b9f06b56f2abe0ac2f22f12a05b10ac8d181d0a1714b334" + } + ], + "preconditioned": false, + "requiredEnvironmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-CLI-IMPACT-001", + "scenarioId": "proofkit.cli.impact::scenario", + "surfaceId": "proofkit.cli.impact" + } + ], + "preexistingFailures": [], + "proofLikePaths": [], + "schemaVersion": 2 + }, + "surface": "requirement-impact-input-compose", + "variant": "wrapper" + }, + { + "direction": "input", + "document": { + "contract": { + "closed": true, + "compatibilitySummary": [ + "schemaVersion=2", + "root-shape-only definition proofkit.requirement-proof-resolver.input.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.requirement-proof-resolver.input.v2", + "nativeAdmissionWitnessSelector": { + "command": "go test ./internal/command/requirementbinding -run '^TestBuildResolverRejectsUnscopedCompactIdentity$'", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementbinding/compact_contract_test.go", + "test": "TestBuildResolverRejectsUnscopedCompactIdentity" + }, + "nativeSource": { + "canonicalDigest": "sha256:ded6cdd4f6a6f12b7cf2b75e08f58e3960c6d51e7e0c7516b92750d239976aec", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementbinding" + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ], + "rootDefinitionDigest": "sha256:a9004b9ddd9f4109326bfc233e6d4629090373d8651488b6663328273826952d", + "rootDefinitionRef": "proofkit.requirement-proof-resolver.input.v2.root-shape", + "rootType": "object", + "schemaVersion": 2 + } + }, + "surface": "requirement-proof-resolver", + "variant": "union" + }, + { + "direction": "output", + "document": { + "bindings": [ + { + "bindingRecordId": "sha256:3e12335736f4aae18fc88fa1b7d704a77dbdbb7889ab6584f792adf43eb3e803", + "blockingStatus": "blocking", + "declaredMutationResistanceClaimId": "no_known_advisory_gap", + "invariantRole": "contract", + "ownedInvariant": "proofkit.compact", + "preconditioned": false, + "requiredEnvironmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-COMPACT-001", + "scenarioId": "proofkit.surface::scenario.compact", + "surfaceId": "proofkit.surface", + "testWitnesses": { + "falsification": { + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 1, + "role": "falsification", + "selector": "tests/proofkit_falsification_test.go::TestRejectsCompactRegression", + "verifyCommandRefs": [ + "go test ./..." + ], + "witnessRouteId": "sha256:e030b29fb58b3e0836b666bcbd1c3c7c64b7268d5b7a76b74130f361e1fcc4d4" + }, + "positive": { + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 0, + "role": "positive", + "selector": "tests/proofkit_positive_test.go::TestAcceptsCompactContract", + "verifyCommandRefs": [ + "go test ./..." + ], + "witnessRouteId": "sha256:23e2c5c66d485bd4bc3fe051283f2fbd752d55e827e26f7564e3903a49d8753a" + } + }, + "verifyCommands": [ + "go test ./..." + ] + } + ], + "commands": [ + { + "bindingRecordIds": [ + "sha256:3e12335736f4aae18fc88fa1b7d704a77dbdbb7889ab6584f792adf43eb3e803" + ], + "environmentClasses": [ + "local-go" + ], + "verifyCommandRef": "go test ./...", + "witnessRouteIds": [ + "sha256:23e2c5c66d485bd4bc3fe051283f2fbd752d55e827e26f7564e3903a49d8753a", + "sha256:e030b29fb58b3e0836b666bcbd1c3c7c64b7268d5b7a76b74130f361e1fcc4d4" + ] + } + ], + "conformanceProofContract": { + "bindings": [ + { + "bindingRecordId": "sha256:3e12335736f4aae18fc88fa1b7d704a77dbdbb7889ab6584f792adf43eb3e803", + "blockingStatus": "blocking", + "declaredMutationResistanceClaimId": "no_known_advisory_gap", + "invariantRole": "contract", + "ownedInvariant": "proofkit.compact", + "requiredEnvironmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-COMPACT-001", + "scenarioId": "proofkit.surface::scenario.compact", + "surfaceId": "proofkit.surface", + "verifyCommands": [ + "go test ./..." + ], + "witnessRefs": [ + { + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 1, + "role": "falsification", + "selector": "tests/proofkit_falsification_test.go::TestRejectsCompactRegression", + "verifyCommands": [ + "go test ./..." + ], + "witnessRouteId": "sha256:e030b29fb58b3e0836b666bcbd1c3c7c64b7268d5b7a76b74130f361e1fcc4d4" + }, + { + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 0, + "role": "positive", + "selector": "tests/proofkit_positive_test.go::TestAcceptsCompactContract", + "verifyCommands": [ + "go test ./..." + ], + "witnessRouteId": "sha256:23e2c5c66d485bd4bc3fe051283f2fbd752d55e827e26f7564e3903a49d8753a" + } + ] + } + ], + "contractId": "proofkit.test.compact", + "declarationKind": "proofkit.requirement-proof-route-declaration", + "schemaVersion": 2, + "surfaces": [ + { + "preconditionedEnvironmentClasses": [], + "requiredEnvironmentClasses": [ + "local-go" + ], + "surfaceId": "proofkit.surface" + } + ] + }, + "contractId": "proofkit.test.compact", + "environmentClasses": [ + { + "bindingRecordIds": [ + "sha256:3e12335736f4aae18fc88fa1b7d704a77dbdbb7889ab6584f792adf43eb3e803" + ], + "environmentClass": "local-go", + "surfaceIds": [ + "proofkit.surface" + ], + "witnessRouteIds": [ + "sha256:23e2c5c66d485bd4bc3fe051283f2fbd752d55e827e26f7564e3903a49d8753a", + "sha256:e030b29fb58b3e0836b666bcbd1c3c7c64b7268d5b7a76b74130f361e1fcc4d4" + ] + } + ], + "localEnvironmentPolicy": { + "authority": "caller_provided", + "localEnvironmentClasses": [ + "local-go" + ] + }, + "nonClaims": [ + "Compact test input does not execute witnesses.", + "Consumers own local environment-class policy supplied to the preconditioned projection.", + "Requirement proof route declarations do not assess oracle quality, mutation adequacy, finding absence, or finding completeness.", + "Requirement proof route declarations do not execute witnesses or prove execution-backed coverage.", + "Requirement proof route declarations do not prove freshness, authenticity, trust, assurance, merge approval, rollout, or production readiness.", + "Requirement proof route declarations do not prove that witness selectors exist or resolve." + ], + "projectionKind": "proofkit.requirement-proof-route-resolver", + "scenarios": [ + { + "bindingRecordId": "sha256:3e12335736f4aae18fc88fa1b7d704a77dbdbb7889ab6584f792adf43eb3e803", + "requirementId": "REQ-PROOFKIT-COMPACT-001", + "scenarioId": "proofkit.surface::scenario.compact", + "surfaceId": "proofkit.surface" + } + ], + "schemaVersion": 2, + "surfaces": [ + { + "bindingRecordIds": [ + "sha256:3e12335736f4aae18fc88fa1b7d704a77dbdbb7889ab6584f792adf43eb3e803" + ], + "requirementIds": [ + "REQ-PROOFKIT-COMPACT-001" + ], + "surfaceId": "proofkit.surface" + } + ], + "witnessRoutes": [ + { + "bindingRecordId": "sha256:3e12335736f4aae18fc88fa1b7d704a77dbdbb7889ab6584f792adf43eb3e803", + "environmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-COMPACT-001", + "resolutionOrderIndex": 0, + "role": "positive", + "scenarioId": "proofkit.surface::scenario.compact", + "selector": "tests/proofkit_positive_test.go::TestAcceptsCompactContract", + "surfaceId": "proofkit.surface", + "verifyCommandRefs": [ + "go test ./..." + ], + "witnessRouteId": "sha256:23e2c5c66d485bd4bc3fe051283f2fbd752d55e827e26f7564e3903a49d8753a" + }, + { + "bindingRecordId": "sha256:3e12335736f4aae18fc88fa1b7d704a77dbdbb7889ab6584f792adf43eb3e803", + "environmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-COMPACT-001", + "resolutionOrderIndex": 1, + "role": "falsification", + "scenarioId": "proofkit.surface::scenario.compact", + "selector": "tests/proofkit_falsification_test.go::TestRejectsCompactRegression", + "surfaceId": "proofkit.surface", + "verifyCommandRefs": [ + "go test ./..." + ], + "witnessRouteId": "sha256:e030b29fb58b3e0836b666bcbd1c3c7c64b7268d5b7a76b74130f361e1fcc4d4" + } + ] + }, + "surface": "requirement-proof-resolver", + "variant": "resolver" + }, + { + "direction": "output", + "document": { + "contract": { + "closed": true, + "compatibilitySummary": [ + "schemaVersion=2", + "root-shape-only definition proofkit.requirement-proof-resolver.output.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.requirement-proof-resolver.output.v2", + "nativeOutputWitnessSelector": { + "command": "go test ./internal/command/requirementbinding -run '^TestBuildResolverRejectsUnscopedCompactIdentity$'", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementbinding/compact_contract_test.go", + "test": "TestBuildResolverRejectsUnscopedCompactIdentity" + }, + "nativeSource": { + "canonicalDigest": "sha256:ded6cdd4f6a6f12b7cf2b75e08f58e3960c6d51e7e0c7516b92750d239976aec", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementbinding" + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ], + "rootDefinitionDigest": "sha256:42bbed6d902300afd15cf763e164896f9ac05567a67fa62a50801d9e90bc4e05", + "rootDefinitionRef": "proofkit.requirement-proof-resolver.output.v2.root-shape", + "rootType": "object", + "schemaVersion": 2 + } + }, + "surface": "requirement-proof-resolver", + "variant": "union" + }, + { + "direction": "input", + "document": { + "authorityState": "caller_owned_requirement_proof_route_source", + "bindingColumns": [ + "requirement_id", + "surface_id", + "scenario_id", + "invariant_role", + "owned_invariant", + "blocking_status", + "required_environment_classes", + "positive_witness", + "falsification_witness", + "verify_commands", + "declared_mutation_resistance_claim_id" + ], + "contractId": "requirement-proof-route-declarations/v2", + "contractKind": "requirement_proof_route_declaration_source", + "nonClaims": [ + "CLI source-set fixture does not prove repository coverage." + ], + "normalizationProfile": "json/v2:utf8+lf+declaration-row-arrays", + "schemaVersion": 2, + "surfaceColumns": [ + "surface_id", + "required_environment_classes", + "preconditioned_environment_classes" + ], + "witnessColumns": [ + "selector", + "environment_classes", + "verify_commands", + "resolution_order_index" + ] + }, + "surface": "requirement-proof-source-set", + "variant": "canonical-envelope" + }, + { + "direction": "input", + "document": { + "authority_state": "caller_owned_requirement_proof_route_fragment", + "bindings": [ + [ + "REQ-PROOFKIT-SOURCE-001", + "owned_invariant", + "contract", + "blocking", + [ + "local-go" + ], + [ + "internal/source_test.go::TestPositive", + 0 + ], + [ + "internal/source_test.go::TestNegative", + 1 + ], + [ + "go test ./..." + ], + "claim.checked" + ] + ], + "contract_id": "requirement-proof-route-declarations/fragment/v3", + "contract_kind": "requirement_proof_route_declaration_fragment", + "normalization_profile": "json/v2:utf8+lf+owner-defaulted-declaration-row-arrays", + "schema_version": 2, + "source_id": "source.local", + "surfaces": [ + [ + "source.local", + [ + "local-go" + ], + [] + ] + ] + }, + "surface": "requirement-proof-source-set", + "variant": "fragment" + }, + { + "direction": "input", + "document": { + "role": "requirement_proof_route_declaration_fragment" + }, + "surface": "requirement-proof-source-set", + "variant": "source-row" + }, + { + "direction": "input", + "document": { + "authority_state": "caller_owned_requirement_proof_route_source_index", + "contract_id": "requirement-proof-route-declarations/source-set/v2", + "contract_kind": "requirement_proof_route_declaration_source_set", + "non_claims": [ + "CLI source-set fixture does not prove repository coverage." + ], + "normalization_profile": "json/v2:utf8+lf+ordered-source-refs", + "schema_version": 2, + "source_columns": [ + "source_id", + "path", + "sha256", + "role", + "non_claims" + ], + "sources": [ + [ + "source.local", + "docs/contracts/requirement-proof-routes/local.v3.json", + "4c800ece11e43aad6c8ec6e671b69843a2b48b203c296c8585824029d4898731", + "requirement_proof_route_declaration_fragment", + [ + "CLI source owns its fixture rows." + ] + ] + ] + }, + "surface": "requirement-proof-source-set", + "variant": "source-set" + }, + { + "direction": "input", + "document": { + "contract": { + "authority": "requirement proof source-set input admission", + "closed": true, + "compatibilitySummary": [ + "canonicalEnvelope", + "schemaVersion", + "sourceSet", + "sources", + "root-shape-only definition proofkit.requirement-proof-source-set.input.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.requirement-proof-source-set.input.v2", + "nativeAdmissionWitnessSelector": { + "command": "go test ./internal/command/requirementproofsourceset -run '^TestBuildSelectsSourceSetRowsAndEmitsResolverInput$'", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementproofsourceset/requirementproofsourceset_test.go", + "test": "TestBuildSelectsSourceSetRowsAndEmitsResolverInput" + }, + "nativeSource": { + "canonicalDigest": "sha256:9acea8f3fdb09a6297d715d86a5d796209c8def51a24fa96b96dc780c38f97d1", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementproofsourceset" + }, + "nonClaims": [ + "The command does not discover repository files, infer source completeness, execute witnesses, decide proof freshness, approve merge, or approve rollout." + ], + "optionalFields": { + "projection": { + "default": { + "kind": "canonical_contract" + }, + "fields": { + "kind": { + "default": "canonical_contract", + "enum": [ + "canonical_contract", + "resolver_input" + ] + }, + "selectedSourceIds": { + "default": "all sourceSet rows", + "format": "non-empty array of stable source_id values when provided", + "semantics": "Restricts output to caller-selected source rows while preserving source-set order." + } + } + } + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-002" + ], + "requiredFields": [ + "canonicalEnvelope", + "schemaVersion", + "sourceSet", + "sources" + ], + "rootDefinitionDigest": "sha256:9b38b93e0b8a2ba2b72e32d606d3cb2372fa685c9a702ae31ae42437945f2eb0", + "rootDefinitionRef": "proofkit.requirement-proof-source-set.input.v2.root-shape", + "rootType": "object", + "schemaVersion": 2 + } + }, + "surface": "requirement-proof-source-set", + "variant": "union" + }, + { + "direction": "input", + "document": { + "canonicalEnvelope": { + "authorityState": "caller_owned_requirement_proof_route_source", + "bindingColumns": [ + "requirement_id", + "surface_id", + "scenario_id", + "invariant_role", + "owned_invariant", + "blocking_status", + "required_environment_classes", + "positive_witness", + "falsification_witness", + "verify_commands", + "declared_mutation_resistance_claim_id" + ], + "contractId": "requirement-proof-route-declarations/v2", + "contractKind": "requirement_proof_route_declaration_source", + "nonClaims": [ + "CLI source-set fixture does not prove repository coverage." + ], + "normalizationProfile": "json/v2:utf8+lf+declaration-row-arrays", + "schemaVersion": 2, + "surfaceColumns": [ + "surface_id", + "required_environment_classes", + "preconditioned_environment_classes" + ], + "witnessColumns": [ + "selector", + "environment_classes", + "verify_commands", + "resolution_order_index" + ] + }, + "projection": { + "kind": "canonical_contract" + }, + "schemaVersion": 2, + "sourceSet": { + "authority_state": "caller_owned_requirement_proof_route_source_index", + "contract_id": "requirement-proof-route-declarations/source-set/v2", + "contract_kind": "requirement_proof_route_declaration_source_set", + "non_claims": [ + "CLI source-set fixture does not prove repository coverage." + ], + "normalization_profile": "json/v2:utf8+lf+ordered-source-refs", + "schema_version": 2, + "source_columns": [ + "source_id", + "path", + "sha256", + "role", + "non_claims" + ], + "sources": [ + [ + "source.local", + "docs/contracts/requirement-proof-routes/local.v3.json", + "4c800ece11e43aad6c8ec6e671b69843a2b48b203c296c8585824029d4898731", + "requirement_proof_route_declaration_fragment", + [ + "CLI source owns its fixture rows." + ] + ] + ] + }, + "sources": [ + { + "path": "docs/contracts/requirement-proof-routes/local.v3.json", + "text": "{\n \"authority_state\": \"caller_owned_requirement_proof_route_fragment\",\n \"bindings\": [\n [\n \"REQ-PROOFKIT-SOURCE-001\",\n \"owned_invariant\",\n \"contract\",\n \"blocking\",\n [\n \"local-go\"\n ],\n [\n \"internal/source_test.go::TestPositive\",\n 0\n ],\n [\n \"internal/source_test.go::TestNegative\",\n 1\n ],\n [\n \"go test ./...\"\n ],\n \"claim.checked\"\n ]\n ],\n \"contract_id\": \"requirement-proof-route-declarations/fragment/v3\",\n \"contract_kind\": \"requirement_proof_route_declaration_fragment\",\n \"normalization_profile\": \"json/v2:utf8+lf+owner-defaulted-declaration-row-arrays\",\n \"schema_version\": 2,\n \"source_id\": \"source.local\",\n \"surfaces\": [\n [\n \"source.local\",\n [\n \"local-go\"\n ],\n []\n ]\n ]\n}\n" + } + ] + }, + "surface": "requirement-proof-source-set", + "variant": "wrapper" + }, + { + "direction": "output", + "document": { + "contract": { + "closed": true, + "commonFields": [ + "inputPaths", + "projectionKind", + "schemaVersion", + "selectedSourceIds", + "sourceCount", + "sourceSetCount" + ], + "compatibilitySummary": [ + "schemaVersion=2", + "root-shape-only definition proofkit.requirement-proof-source-set.output.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.requirement-proof-source-set.output.v2", + "nativeOutputWitnessSelector": { + "command": "go test ./internal/command/requirementproofsourceset -run '^TestBuildSelectsSourceSetRowsAndEmitsResolverInput$'", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementproofsourceset/requirementproofsourceset_test.go", + "test": "TestBuildSelectsSourceSetRowsAndEmitsResolverInput" + }, + "nativeSource": { + "canonicalDigest": "sha256:9acea8f3fdb09a6297d715d86a5d796209c8def51a24fa96b96dc780c38f97d1", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementproofsourceset" + }, + "nonClaims": [ + "Resolver projection is a shape-compatible data product, not proof freshness, native test execution, or consumer merge authority." + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-002" + ], + "rootDefinitionDigest": "sha256:ed81d03ea6ea56fd9ba12c48995111e8a90a8c1feb336fe95be195b25ccc0254", + "rootDefinitionRef": "proofkit.requirement-proof-source-set.output.v2.root-shape", + "rootType": "object", + "schemaVersion": 2, + "variants": [ + { + "payloadField": "contract", + "projectionKind": "proofkit.requirement-proof-source-set.canonical_contract" + }, + { + "payloadField": "resolverInput", + "projectionKind": "proofkit.requirement-proof-source-set.resolver_input" + } + ] + } + }, + "surface": "requirement-proof-source-set", + "variant": "union" + }, + { + "direction": "output", + "document": { + "contract": { + "authority_state": "caller_owned_requirement_proof_route_source", + "binding_columns": [ + "requirement_id", + "surface_id", + "scenario_id", + "invariant_role", + "owned_invariant", + "blocking_status", + "required_environment_classes", + "positive_witness", + "falsification_witness", + "verify_commands", + "declared_mutation_resistance_claim_id" + ], + "bindings": [ + [ + "REQ-PROOFKIT-SOURCE-001", + "source.local", + "source.local::owned_invariant", + "contract", + "owned_invariant", + "blocking", + [ + "local-go" + ], + [ + "internal/source_test.go::TestPositive", + [ + "local-go" + ], + [ + "go test ./..." + ], + 0 + ], + [ + "internal/source_test.go::TestNegative", + [ + "local-go" + ], + [ + "go test ./..." + ], + 1 + ], + [ + "go test ./..." + ], + "claim.checked" + ] + ], + "contract_id": "requirement-proof-route-declarations/v2", + "contract_kind": "requirement_proof_route_declaration_source", + "non_claims": [ + "CLI source-set fixture does not prove repository coverage." + ], + "normalization_profile": "json/v2:utf8+lf+declaration-row-arrays", + "schema_version": 2, + "surface_columns": [ + "surface_id", + "required_environment_classes", + "preconditioned_environment_classes" + ], + "surfaces": [ + [ + "source.local", + [ + "local-go" + ], + [] + ] + ], + "witness_columns": [ + "selector", + "environment_classes", + "verify_commands", + "resolution_order_index" + ] + }, + "inputPaths": [ + "docs/contracts/requirement-proof-routes/local.v3.json" + ], + "projectionKind": "proofkit.requirement-proof-source-set.canonical_contract", + "schemaVersion": 2, + "selectedSourceIds": [ + "source.local" + ], + "sourceCount": 1, + "sourceSetCount": 1 + }, + "surface": "requirement-proof-source-set", + "variant": "wrapper" + }, + { + "direction": "input", + "document": { + "authority_state": "caller_owned_declaration", + "binding_columns": [ + "requirement_id", + "surface_id", + "scenario_id", + "invariant_role", + "owned_invariant", + "blocking_status", + "required_environment_classes", + "positive_witness", + "falsification_witness", + "verify_commands", + "declared_mutation_resistance_claim_id" + ], + "bindings": [ + [ + "REQ-PROOFKIT-COMPACT-001", + "proofkit.surface", + "proofkit.surface::scenario.compact", + "contract", + "proofkit.compact", + "blocking", + [ + "local-go" + ], + [ + "tests/proofkit_positive_test.go::TestAcceptsCompactContract", + [ + "local-go" + ], + [ + "go test ./..." + ], + 0 + ], + [ + "tests/proofkit_falsification_test.go::TestRejectsCompactRegression", + [ + "local-go" + ], + [ + "go test ./..." + ], + 1 + ], + [ + "go test ./..." + ], + "no_known_advisory_gap" + ] + ], + "contract_id": "proofkit.test.compact", + "contract_kind": "requirement_proof_route_declaration", + "non_claims": [ + "Compact test input does not execute witnesses." + ], + "normalization_profile": "proofkit.compact.declaration.v2", + "schema_version": 2, + "surface_columns": [ + "surface_id", + "required_environment_classes", + "preconditioned_environment_classes" + ], + "surfaces": [ + [ + "proofkit.surface", + [ + "local-go" + ], + [] + ] + ], + "witness_columns": [ + "selector", + "environment_classes", + "verify_commands", + "resolution_order_index" + ] + }, + "surface": "requirement-proof-view", + "variant": "compact" + }, + { + "direction": "input", + "document": { + "contract": { + "closed": true, + "compatibilitySummary": [ + "schemaVersion=2", + "root-shape-only definition proofkit.requirement-proof-view.input.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.requirement-proof-view.input.v2", + "nativeAdmissionWitnessSelector": { + "command": "go test ./internal/command/requirementproofview -run '^TestBuildMarkdownEscapesCallerControlledCompactFields$'", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementproofview/requirementproofview_test.go", + "test": "TestBuildMarkdownEscapesCallerControlledCompactFields" + }, + "nativeSource": { + "canonicalDigest": "sha256:f71d93a5e3c434f8cefdc18a92cc1360400555351597bfaf7c0cc7a360b79444", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementproofview" + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ], + "rootDefinitionDigest": "sha256:998efcaf322251ec9ff402cbf8f4db84f981aca3c24bd2071c0c302ddcca5490", + "rootDefinitionRef": "proofkit.requirement-proof-view.input.v2.root-shape", + "rootType": "object", + "schemaVersion": 2 + } + }, + "surface": "requirement-proof-view", + "variant": "union" + }, + { + "direction": "output", + "document": { + "authority": "lookup_only", + "bindingCount": 1, + "bindings": [ + { + "bindingRecordId": "sha256:3e12335736f4aae18fc88fa1b7d704a77dbdbb7889ab6584f792adf43eb3e803", + "blockingStatus": "blocking", + "declaredMutationResistanceClaimId": "no_known_advisory_gap", + "declaredWitnessRoutes": [ + { + "bindingRecordId": "sha256:3e12335736f4aae18fc88fa1b7d704a77dbdbb7889ab6584f792adf43eb3e803", + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 1, + "role": "falsification", + "selector": "tests/proofkit_falsification_test.go::TestRejectsCompactRegression", + "verifyCommands": [ + "go test ./..." + ], + "witnessRouteId": "sha256:e030b29fb58b3e0836b666bcbd1c3c7c64b7268d5b7a76b74130f361e1fcc4d4" + }, + { + "bindingRecordId": "sha256:3e12335736f4aae18fc88fa1b7d704a77dbdbb7889ab6584f792adf43eb3e803", + "environmentClasses": [ + "local-go" + ], + "resolutionOrderIndex": 0, + "role": "positive", + "selector": "tests/proofkit_positive_test.go::TestAcceptsCompactContract", + "verifyCommands": [ + "go test ./..." + ], + "witnessRouteId": "sha256:23e2c5c66d485bd4bc3fe051283f2fbd752d55e827e26f7564e3903a49d8753a" + } + ], + "invariantRole": "contract", + "ownedInvariant": "proofkit.compact", + "preconditioned": false, + "requiredEnvironmentClasses": [ + "local-go" + ], + "requirementId": "REQ-PROOFKIT-COMPACT-001", + "scenarioId": "proofkit.surface::scenario.compact", + "surfaceId": "proofkit.surface", + "verifyCommands": [ + "go test ./..." + ] + } + ], + "commandCount": 1, + "contractId": "proofkit.test.compact", + "localEnvironmentPolicy": { + "authority": "caller_provided", + "localEnvironmentClasses": [ + "local-go" + ] + }, + "nonClaims": [ + "Compact requirement proof views are rendered from explicit compact contract facts only.", + "Compact requirement proof views do not infer spec paths, owner routes, or local environment policy.", + "Compact test input does not execute witnesses.", + "Consumers own local environment-class policy supplied to the preconditioned projection.", + "Requirement proof route declarations do not assess oracle quality, mutation adequacy, finding absence, or finding completeness.", + "Requirement proof route declarations do not execute witnesses or prove execution-backed coverage.", + "Requirement proof route declarations do not prove freshness, authenticity, trust, assurance, merge approval, rollout, or production readiness.", + "Requirement proof route declarations do not prove that witness selectors exist or resolve.", + "Requirement proof views are rendered lookup products only.", + "Requirement proof views do not execute native witnesses.", + "Requirement proof views do not own requirement meaning.", + "Requirement proof views do not prove receipt freshness, command success, merge approval, or rollout readiness." + ], + "preconditionedBindingCount": 0, + "requirementCount": 1, + "schemaVersion": 2, + "viewKind": "proofkit.compact-requirement-proof-view" + }, + "surface": "requirement-proof-view", + "variant": "compact" + }, + { + "direction": "output", + "document": { + "contract": { + "closed": true, + "compatibilitySummary": [ + "schemaVersion=2", + "root-shape-only definition proofkit.requirement-proof-view.output.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.requirement-proof-view.output.v2", + "nativeOutputWitnessSelector": { + "command": "go test ./internal/command/requirementproofview -run '^TestBuildMarkdownEscapesCallerControlledCompactFields$'", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementproofview/requirementproofview_test.go", + "test": "TestBuildMarkdownEscapesCallerControlledCompactFields" + }, + "nativeSource": { + "canonicalDigest": "sha256:f71d93a5e3c434f8cefdc18a92cc1360400555351597bfaf7c0cc7a360b79444", + "evidenceClass": "source_checkout", + "path": "internal/command/requirementproofview" + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004" + ], + "rootDefinitionDigest": "sha256:3dd563fd6eaedc12994b16234f72228b76fd1155e29033ca1b027fe1e0289d19", + "rootDefinitionRef": "proofkit.requirement-proof-view.output.v2.root-shape", + "rootType": "object", + "schemaVersion": 2 + } + }, + "surface": "requirement-proof-view", + "variant": "union" + }, + { + "direction": "output", + "document": { + "baseSnapshotId": "sha256:641bef7cd208a9502da434eaead78266cefc5d69814d24c11642cb6ded90981b", + "currentSnapshotId": "sha256:d91a190ca30bc3123b0618b960f6fefd22ee62a8a4a9c23556faa3766da6dc11", + "outputSha256": "sha256:10ae49a7e7b483196d3a99df904f0936223d2aa4a32a05ac6dc0b47a89d25cea" + }, + "surface": "requirement-semantic-diff", + "variant": "coverage-runtime" + }, + { + "direction": "output", + "document": { + "outputSha256": "sha256:2b1fd23514320f11f93ce4840ae5075b3a56c22e00477ff974890cdd82f73b67", + "snapshotId": "sha256:d91a190ca30bc3123b0618b960f6fefd22ee62a8a4a9c23556faa3766da6dc11" + }, + "surface": "requirement-traceability-graph", + "variant": "coverage-runtime" + }, + { + "direction": "input", + "document": { + "commandRefPolicy": { + "prefix": "proofkit_cli" + }, + "compactProofContract": { + "authority_state": "caller_owned_declaration", + "binding_columns": [ + "requirement_id", + "surface_id", + "scenario_id", + "invariant_role", + "owned_invariant", + "blocking_status", + "required_environment_classes", + "positive_witness", + "falsification_witness", + "verify_commands", + "declared_mutation_resistance_claim_id" + ], + "bindings": [ + [ + "REQ-PROOFKIT-CLI-001", + "proofkit.cli", + "proofkit.cli::scenario", + "contract", + "proofkit.cli.invariant", + "blocking", + [ + "local-go" + ], + [ + "internal/app/cli_positive_test.go::TestAcceptsCLIContract", + [ + "local-go" + ], + [ + "go test" + ], + 0 + ], + [ + "internal/app/cli_falsification_test.go::TestRejectsCLIRegression", + [ + "local-go" + ], + [ + "go test" + ], + 1 + ], + [ + "go test" + ], + "claim.no_known_advisory_gap" + ] + ], + "contract_id": "proofkit.cli.compact", + "contract_kind": "requirement_proof_route_declaration", + "non_claims": [ + "CLI compact fixture does not execute witnesses." + ], + "normalization_profile": "proofkit.compact.declaration.v2", + "schema_version": 2, + "surface_columns": [ + "surface_id", + "required_environment_classes", + "preconditioned_environment_classes" + ], + "surfaces": [ + [ + "proofkit.cli", + [ + "local-go" + ], + [] + ] + ], + "witness_columns": [ + "selector", + "environment_classes", + "verify_commands", + "resolution_order_index" + ] + }, + "inventoryId": "proofkit.cli.derived.inventory", + "nonClaims": [ + "CLI projection fixture does not execute native tests." + ], + "requirementSource": { + "requirements": [ + { + "ownerId": "proofkit.cli", + "requirementId": "REQ-PROOFKIT-CLI-001" + } + ] + }, + "schemaVersion": 2 + }, + "surface": "test-evidence-inventory", + "variant": "proof-binding" + }, + { + "direction": "input", + "document": { + "contract": { + "authority": "test evidence inventory admission and projection admission", + "closed": true, + "compatibilitySummary": [ + "schemaVersion=2", + "root-shape-only definition proofkit.test-evidence-inventory.input.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.test-evidence-inventory.input.v2", + "nativeAdmissionWitnessSelector": { + "command": "go test ./internal/command/testevidenceinventory -run '^TestBuildRejectsIncompleteDeclaredOracleMetadataAndDuplicateFalsifier$'", + "evidenceClass": "source_checkout", + "path": "internal/command/testevidenceinventory/testevidenceinventory_test.go", + "test": "TestBuildRejectsIncompleteDeclaredOracleMetadataAndDuplicateFalsifier" + }, + "nativeSources": [ + { + "canonicalDigest": "sha256:93c68e7bd85f435dd82b1a9ab663add5ecc37bd45948ec3ce49c8af18a90051a", + "evidenceClass": "source_checkout", + "path": "internal/command/proofbindingtestinventory" + }, + { + "canonicalDigest": "sha256:60983b8f25544c7b85e818f202de053fa15557b0de37a7df35aa0d58b58c3888", + "evidenceClass": "source_checkout", + "path": "internal/command/testevidenceinventory" + } + ], + "nonClaims": [ + "Projection modes are closed vocabulary: proof-binding-derived or discovery-draft.", + "Discovery draft candidate output must be materialized into strict inventory before it can affect coverage.", + "Proof-binding-derived routes remain proof_route_candidate entries until a consumer owner materializes an executable semantic oracle.", + "The derived inventory remains caller-owned input for downstream coverage views." + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-006" + ], + "projectionVariants": { + "discovery-draft": { + "appliesWhen": "--projection discovery-draft", + "authority": "caller-owned test discovery draft projection admission", + "candidateOutput": { + "authority": "caller_owned_test_discovery_candidate_inventory", + "candidateKind": "proofkit.test-inventory-discovery-draft.candidate-inventory", + "strictInventoryAdmission": "rejected_until_owner_materialized" + }, + "fields": { + "discoveredTests[].nonClaims": { + "default": [] + }, + "nonClaims": { + "default": [] + }, + "repository.nonClaims": { + "default": [] + }, + "runner.nonClaims": { + "default": [] + } + }, + "nonClaim": "Discovery draft reports candidate-only guidance from explicit caller-owned facts; it does not scan repositories, execute tests, emit execution-backed semantic evidence, or close coverage.", + "normalizedInventory": "not_supported", + "requiredFields": [ + "authority", + "discoveredTests", + "discoveredTests[].title", + "draftId", + "repository", + "runner", + "schemaVersion" + ] + }, + "proof-binding-derived": { + "appliesWhen": "--projection proof-binding-derived", + "authority": "proof-binding-derived test inventory projection admission", + "candidateOutput": { + "evidenceClass": "proof_route_candidate", + "semanticCoverage": "not_satisfied_until_owner_materialized" + }, + "fields": { + "commandRefPolicy": { + "fields": { + "prefix": { + "format": "stable rule id namespace used to derive commandRefs", + "required": true + } + }, + "required": true + }, + "compactProofContract": { + "format": "compact requirement proof binding contract", + "required": true + }, + "inventoryId": { + "format": "stable rule id", + "required": true + }, + "nonClaims": { + "default": [] + }, + "requirementSource": { + "fields": { + "requirements": { + "itemFields": { + "ownerId": "stable rule id", + "requirementId": "stable rule id" + }, + "required": true + } + }, + "required": true + }, + "schemaVersion": { + "required": true, + "value": 2 + } + }, + "nonClaim": "This projection input does not make Proofkit the owner of requirement meaning, native test execution, semantic coverage, repository scanning, proof freshness, or merge policy.", + "normalizedInventory": "supported" + }, + "strict-inventory": { + "appliesWhen": "default without --projection", + "authority": "caller-owned strict test inventory admission", + "nonClaim": "Strict inventory admission validates caller-owned evidence records; Proofkit does not execute those tests.", + "normalizedInventory": "supported" + } + }, + "rootDefinitionDigest": "sha256:4ec00274f91fd3e6e8bf67ac55125e1a9566e7d2233137ed3fa968539532125e", + "rootDefinitionRef": "proofkit.test-evidence-inventory.input.v2.root-shape", + "rootType": "object", + "schemaVersion": 2 + } + }, + "surface": "test-evidence-inventory", + "variant": "union" + }, + { + "direction": "output", + "document": { + "entrySources": [], + "inputPaths": [], + "inventory": { + "authority": "caller_owned_inventory", + "entries": [ + { + "commandRefs": [ + "proofkit_cli.proofkit.cli.verify.go_test" + ], + "evidenceClass": "proof_route_candidate", + "falsifier": null, + "nonClaims": [ + "This inventory entry does not execute native tests or authenticate receipts.", + "This inventory entry projects proof-route wiring only and cannot satisfy semantic coverage." + ], + "oracle": null, + "ownerId": "proofkit.cli", + "ownerInvariantRefs": [], + "qualityFindings": [], + "requirementRefs": [ + "REQ-PROOFKIT-CLI-001" + ], + "selector": "internal/app/cli_falsification_test.go::TestRejectsCLIRegression", + "sourcePath": "internal/app/cli_falsification_test.go", + "testId": "test.proof_route.gljhambakfmjolmpilbjmacnihjdnmjepapjcokfmmkanfhgahofngpfchlegfce", + "witnessRefs": [ + "sha256:0531a6ba4f63856925b36ac7213d763e9a93c84f664a7f10a18f709fc15e0fce" + ] + } + ], + "inventoryId": "proofkit.cli.derived.inventory", + "nonClaims": [ + "CLI projection fixture does not execute native tests.", + "Proof-binding-derived test inventories do not execute native tests or authenticate receipts.", + "Proof-binding-derived test inventories do not prove repository inventory completeness, proof freshness, merge approval, release approval, rollout approval, or production readiness.", + "Proof-binding-derived test inventories project caller-owned proof binding falsification witnesses and remain subject to caller-owned coverage universes.", + "Test evidence inventory reports do not approve merge, release, rollout, or repository policy.", + "Test evidence inventory reports do not authenticate runner output or receipt producers.", + "Test evidence inventory reports do not execute native tests.", + "Test evidence inventory reports do not prove repository inventory completeness.", + "Test evidence inventory reports do not resolve selectors or review caller-authored oracle quality.", + "Test evidence inventory reports do not verify that a caller-declared falsifier supersession dominates the superseded falsifier." + ], + "schemaVersion": 1 + }, + "nonClaims": [ + "Normalized test evidence inventory does not discover repository files, execute tests, authenticate receipts, or approve merge, release, rollout, or repository policy.", + "Normalized test evidence inventory is a deterministic projection over explicit caller-owned inventory input." + ], + "normalizedInventoryId": "proofkit.cli.derived.inventory.normalized", + "normalizedKind": "proofkit.test-evidence-inventory.normalized", + "projectionKind": "proofkit.proof-binding-test-inventory", + "projectionSummary": { + "commandRefCount": 1, + "entryCount": 1, + "routeEntryMappings": [ + { + "bindingRecordId": "sha256:a1f16fb42f3743e1cc9f7e09c03a047a0871ee72915881cd8752348cc60d1915", + "requirementId": "REQ-PROOFKIT-CLI-001", + "resolutionOrderIndex": 1, + "role": "falsification", + "scenarioId": "proofkit.cli::scenario", + "selector": "internal/app/cli_falsification_test.go::TestRejectsCLIRegression", + "surfaceId": "proofkit.cli", + "testId": "test.proof_route.gljhambakfmjolmpilbjmacnihjdnmjepapjcokfmmkanfhgahofngpfchlegfce", + "witnessRouteId": "sha256:0531a6ba4f63856925b36ac7213d763e9a93c84f664a7f10a18f709fc15e0fce" + } + ], + "schemaVersion": 2 + }, + "schemaVersion": 1, + "sourceAuthority": "caller_owned_inventory", + "sourceColumns": [ + "source_id", + "path", + "sha256", + "role", + "non_claims" + ], + "sourceCount": 0, + "sources": [] + }, + "surface": "test-evidence-inventory", + "variant": "normalized" + }, + { + "direction": "output", + "document": { + "contract": { + "agentActionPlan": { + "decisionOwner": "consumer_repository", + "nonClaim": "Agent actions are deterministic guidance over admitted caller-owned inventory facts and do not execute tests, approve merge, or decide semantic truth.", + "sourceDiagnosticKeys": [ + "failures", + "warnings" + ] + }, + "authority": "test evidence inventory output contract", + "classificationIds": [ + "candidate_only", + "declared_duplicate_falsifier", + "duplicate_falsifier_candidate", + "empty_oracle", + "fixture_leak_risk", + "flaky_time", + "implementation_mirror", + "import_cost_leak", + "invalid_falsifier_supersession", + "missing_edge", + "missing_executable_command_ref", + "missing_declared_route_anchor", + "mock_tests_mock", + "over_broad_integration", + "proof_route_candidate", + "routing_smoke_only", + "selector_fragility", + "snapshot_without_oracle", + "tautology", + "unasserted_diagnostic", + "incomplete_declared_oracle_metadata", + "wrong_boundary", + "wrong_evidence_boundary" + ], + "classificationRecords": { + "failureClassifications": { + "severity": "failure", + "sourceDiagnosticKey": "failures" + }, + "warningClassifications": { + "severity": "warning", + "sourceDiagnosticKey": "warnings" + } + }, + "closed": true, + "compatibilitySummary": [ + "schemaVersion=2", + "root-shape-only definition proofkit.test-evidence-inventory.output.v2.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "contractId": "proofkit.test-evidence-inventory.output.v2", + "nativeOutputWitnessSelector": { + "command": "go test ./internal/command/testevidenceinventory -run '^TestBuildRejectsIncompleteDeclaredOracleMetadataAndDuplicateFalsifier$'", + "evidenceClass": "source_checkout", + "path": "internal/command/testevidenceinventory/testevidenceinventory_test.go", + "test": "TestBuildRejectsIncompleteDeclaredOracleMetadataAndDuplicateFalsifier" + }, + "nativeSources": [ + { + "canonicalDigest": "sha256:93c68e7bd85f435dd82b1a9ab663add5ecc37bd45948ec3ce49c8af18a90051a", + "evidenceClass": "source_checkout", + "path": "internal/command/proofbindingtestinventory" + }, + { + "canonicalDigest": "sha256:60983b8f25544c7b85e818f202de053fa15557b0de37a7df35aa0d58b58c3888", + "evidenceClass": "source_checkout", + "path": "internal/command/testevidenceinventory" + } + ], + "nonClaims": [ + "Test evidence inventory classifications describe admitted caller-owned inventory facts and Proofkit-derived structural diagnostics only.", + "Proofkit does not infer weak tests from names, prose, mocks, sleeps, snapshots, or repository scans." + ], + "oracleFields": { + "assertionSummary": "non-empty caller-authored assertion summary for declared semantic-falsifier, contract-admission, and property-or-fuzz routes", + "expectedPublicOutcome": "non-empty caller-authored expected public outcome for declared semantic-falsifier, contract-admission, and property-or-fuzz routes", + "oracleId": "stable rule id", + "oracleKind": "stable rule id" + }, + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-SPEC-006" + ], + "qualityFindingFields": { + "class": "known weak-test class", + "evidenceRefs": "sorted stable rule id refs", + "findingId": "stable rule id", + "nonClaims": "sorted caller-owned non-claims", + "ownerReviewState": "candidate|confirmed", + "severity": "failure|warning" + }, + "rootDefinitionDigest": "sha256:6688b487f1c9936dd1583ab0d90e689c75cae278b04bb431950b9979aba4efa7", + "rootDefinitionRef": "proofkit.test-evidence-inventory.output.v2.root-shape", + "rootType": "object", + "schemaVersion": 2 + } + }, + "surface": "test-evidence-inventory", + "variant": "union" + } + ], + "schemaVersion": 2 +} + diff --git a/internal/app/testdata/v0.5-wire-observations.json b/internal/app/testdata/v0.5-wire-observations.json new file mode 100644 index 0000000..8b847eb --- /dev/null +++ b/internal/app/testdata/v0.5-wire-observations.json @@ -0,0 +1,28 @@ +{ + "schemaVersion": 1, + "edgeId": "proofkit.public-wire.0.4.0-to-0.5.0", + "previousVersion": "0.4.0", + "version": "0.5.0", + "evidenceClass": "owner_authored_frozen_version_edge_observation", + "previousPublicAbiSha256": "sha256:fc03740aea9e7f525a4388e5d7f557cde07e11b0db0c05101fe937c28a1129d9", + "currentPublicAbiSha256": "sha256:2af9b2e3b77b8309f295a2ffeeec70cb4de4d51cb6fbfa45e7733da810ddc1df", + "addedCommandContracts": [ + { + "command": "change-workflow-plan", + "inputContractSha256": "sha256:0851ad1ad3990009ca769e6f7e7e4402f289c35484123ccb88097ab21c3b9f18", + "outputContractSha256": "sha256:1a9f6fd253fa01c55f29990d67f1e46ead8068c7f16880df45a645021c26dae1" + }, + { + "command": "native-evidence-guidance", + "inputContractSha256": "", + "outputContractSha256": "sha256:ed40668acec3a8e2ae097b0ce011994e7facf6feda23e0057c4dcbe660a1a1b7" + } + ], + "breakingChangeIds": [ + "proofkit.agent-envelope.local-identity-closure", + "proofkit.stable-json.unicode-scalar-v2" + ], + "nonClaims": [ + "This owner-authored version-edge observation binds reviewed public contract identities; it does not authenticate Git history, registry publication, provider ingestion, native witness truth, rollout, or production readiness." + ] +} diff --git a/internal/command/adoptionchecklist/adoptionchecklist_test.go b/internal/command/adoptionchecklist/adoptionchecklist_test.go index 20c3038..13dbc53 100644 --- a/internal/command/adoptionchecklist/adoptionchecklist_test.go +++ b/internal/command/adoptionchecklist/adoptionchecklist_test.go @@ -10,7 +10,7 @@ import ( ) func TestBuildClassifiesRequiredChecklistItemsAndPreservesOptionalNonFailures(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.069749879516188799356142053767399217128942865655819702242129484856747289540132") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.082438870412319112278242336710384449607452146725689050310681504683060604203472") record, exitCode, err := Build(validChecklistInput()) if err != nil { t.Fatalf("Build() error=%v", err) diff --git a/internal/command/adoptioncontract/adoptioncontract_test.go b/internal/command/adoptioncontract/adoptioncontract_test.go index 9344181..234a5b4 100644 --- a/internal/command/adoptioncontract/adoptioncontract_test.go +++ b/internal/command/adoptioncontract/adoptioncontract_test.go @@ -171,7 +171,7 @@ func TestValidateOptionsOwnsModePilotAndCompatibilityPolicy(t *testing.T) { } func TestBuildDelegatesModesWithParity(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.073115937715906245510885594108709053081835764670748024087132020466101700061433") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.084672551697880440617552878981992690168989003939799146874674900502469106750922") cases := []struct { name string options Options diff --git a/internal/command/adoptiondoctor/adoptiondoctor_test.go b/internal/command/adoptiondoctor/adoptiondoctor_test.go index c08a4e0..1204e89 100644 --- a/internal/command/adoptiondoctor/adoptiondoctor_test.go +++ b/internal/command/adoptiondoctor/adoptiondoctor_test.go @@ -77,7 +77,7 @@ func TestBuildEnforceTouchedSkipsGapsOutsideTouchedSelection(t *testing.T) { } func TestBuildFailsEnforcementForCandidateBoundaryAndMissingRoutes(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.009316750311738530526586138484790439891684089174716743044696722390531958762956") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.019121161710258748465811401750561144787409810075600633401815903137990004014878") input := baseInput() input["mode"] = "enforce-all" input["checkedScope"] = "all" diff --git a/internal/command/adoptiondoctor/envelope.go b/internal/command/adoptiondoctor/envelope.go index e885bb6..79d32f1 100644 --- a/internal/command/adoptiondoctor/envelope.go +++ b/internal/command/adoptiondoctor/envelope.go @@ -68,17 +68,18 @@ func commandRefs(routes []ownerRoute) []map[string]any { func contextRefs(routes []ownerRoute, candidates []candidateBoundary, surfaces []authoritySurface) []map[string]any { refs := []map[string]any{} for _, route := range routes { - for _, path := range append(append([]string{}, route.SpecPaths...), append(route.ProofBindingPaths, route.NativeWitnessRefs...)...) { - refs = append(refs, contextRef(route.RouteID+"."+path, path, route.Owner, "owner_route")) + paths := append(append([]string{}, route.SpecPaths...), append(route.ProofBindingPaths, route.NativeWitnessRefs...)...) + for index, path := range paths { + refs = append(refs, contextRef(fmt.Sprintf("%s.context.%03d", route.RouteID, index+1), path, route.Owner, "owner_route")) } } for _, candidate := range candidates { - for _, path := range candidate.AffectedPaths { - refs = append(refs, contextRef(candidate.BoundaryID+"."+path, path, candidate.CandidateOwner, "candidate_boundary")) + for index, path := range candidate.AffectedPaths { + refs = append(refs, contextRef(fmt.Sprintf("%s.context.%03d", candidate.BoundaryID, index+1), path, candidate.CandidateOwner, "candidate_boundary")) } } for _, surface := range surfaces { - refs = append(refs, contextRef(surface.SurfaceID+"."+surface.Path, surface.Path, surface.Owner, "stale_authority_surface")) + refs = append(refs, contextRef(surface.SurfaceID+".context", surface.Path, surface.Owner, "stale_authority_surface")) } sort.Slice(refs, func(left int, right int) bool { return refs[left]["refId"].(string) < refs[right]["refId"].(string) diff --git a/internal/command/adoptionworkflow/adoptionworkflow_test.go b/internal/command/adoptionworkflow/adoptionworkflow_test.go index 1521301..acaf3ef 100644 --- a/internal/command/adoptionworkflow/adoptionworkflow_test.go +++ b/internal/command/adoptionworkflow/adoptionworkflow_test.go @@ -18,7 +18,7 @@ func TestBuildRejectsUnknownInputRefField(t *testing.T) { } func TestBuildGeneratesBoundedCommandArgv(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.071485626206261445985975684684155922995969280410048467184237861241913337881344") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.042007474063434113226412640301108503934031277804250309702701090283402397774706") result, err := BuildResult(validWorkflowInput()) if err != nil { t.Fatalf("BuildResult() error=%v", err) diff --git a/internal/command/agentroute/agentroute_test.go b/internal/command/agentroute/agentroute_test.go index 79dfe75..ec94e81 100644 --- a/internal/command/agentroute/agentroute_test.go +++ b/internal/command/agentroute/agentroute_test.go @@ -3,14 +3,16 @@ package agentroute import ( "encoding/json" "fmt" - "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" "sort" "strings" "testing" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" + "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" ) func TestBuildRoutesRequirementSourceAndBlocksUnknownGoal(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.052032679560954279991581640663936146133283843978254811517731144801933075103271") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.038668421823758140886315800700905449361627837129826444684062024338436022702756") t.Parallel() report, exitCode, err := Build(map[string]any{ @@ -1290,6 +1292,56 @@ func TestBuildEnvelopeCreatesUniqueRefsForRepeatedRenderCommands(t *testing.T) { } } +func TestEnvelopeProducerOwnsUniqueDisjointLocalIdentities(t *testing.T) { + report, exitCode, err := Build(map[string]any{ + "schemaVersion": jsonNumber("1"), + "routeId": "consumer.route.render_all", + "goal": "render_human_view", + "mode": "observe", + "availableInputs": []any{ + map[string]any{"kind": "coverage_view_input", "ref": "docs/contracts/coverage-view-input.v1.json"}, + map[string]any{"kind": "proof_binding", "ref": "docs/contracts/requirement-proof-bindings/module.json"}, + map[string]any{"kind": "requirement_source", "ref": "docs/specs/module/requirements.v1.json"}, + }, + }) + if err != nil || exitCode != 0 { + t.Fatalf("Build() exit=%d error=%v", exitCode, err) + } + reportID := report["reportId"].(string) + commands := mapsFromAny(report["nextCommands"]) + contexts := append(guidanceContextRefs(mapFromAny(report["guidanceSlice"]), reportID), inputContextRefs(commands, reportID)...) + assertProducerIdentityDomain(t, commandContextRefs(commands, reportID), "commandId", map[string]string{}) + commandDomain := producerIdentityDomain(t, commandContextRefs(commands, reportID), "commandId") + assertProducerIdentityDomain(t, contexts, "refId", commandDomain) +} + +func assertProducerIdentityDomain(t *testing.T, records []map[string]any, key string, foreign map[string]string) { + t.Helper() + producerIdentityDomain(t, records, key) + for _, record := range records { + identity := record[key].(string) + if owner, collision := foreign[identity]; collision { + t.Fatalf("%s identity %q collides with %s domain", key, identity, owner) + } + } +} + +func producerIdentityDomain(t *testing.T, records []map[string]any, key string) map[string]string { + t.Helper() + seen := map[string]string{} + for _, record := range records { + identity, err := admit.RuleID(record[key], "agent-route "+key) + if err != nil { + t.Fatalf("%s identity %v is invalid: %v", key, record[key], err) + } + if prior, duplicate := seen[identity]; duplicate { + t.Fatalf("duplicate %s identity %q; prior domain=%s", key, identity, prior) + } + seen[identity] = key + } + return seen +} + func TestBuildEnvelopeKeepsMachineRefIDsBoundedForLongInputRefs(t *testing.T) { t.Parallel() @@ -1318,7 +1370,7 @@ func TestBuildEnvelopeKeepsMachineRefIDsBoundedForLongInputRefs(t *testing.T) { } func TestBuildEnvelopeKeepsBlockedRoutesAsStopSignals(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.039089309512279322876166263378306645627644382088544574407694362092047953400284") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.099464793393504510007974878444175682277628390527264398410332327062001832620126") t.Parallel() envelope, exitCode, err := BuildEnvelope(map[string]any{ @@ -1350,7 +1402,7 @@ func TestBuildEnvelopeKeepsBlockedRoutesAsStopSignals(t *testing.T) { } func TestBuildEnvelopeCarriesBlockedObservedReportPreconditions(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.112527168675876041372970118427108526301468859106766054318947512648421264085367") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.104071069265834506379769173345836014913259107788654398096334463906261390666732") t.Parallel() envelope, exitCode, err := BuildEnvelope(map[string]any{ diff --git a/internal/command/agentroute/envelope.go b/internal/command/agentroute/envelope.go index a2a0ad1..cc2d111 100644 --- a/internal/command/agentroute/envelope.go +++ b/internal/command/agentroute/envelope.go @@ -291,7 +291,15 @@ func inputRefID(reportID string, inputRef string) string { } func shortTextHash(text string) string { - return strings.TrimPrefix(digest.SHA256TextRef(text), "sha256:")[:16] + hexToken := []byte(strings.TrimPrefix(digest.SHA256TextRef(text), "sha256:")[:16]) + for index, value := range hexToken { + if value >= '0' && value <= '9' { + hexToken[index] = 'a' + value - '0' + continue + } + hexToken[index] = 'k' + value - 'a' + } + return string(hexToken) } func requiredInputLabel(item map[string]any) string { diff --git a/internal/command/bindingpartition/bindingpartition_test.go b/internal/command/bindingpartition/bindingpartition_test.go index b441319..302dc8b 100644 --- a/internal/command/bindingpartition/bindingpartition_test.go +++ b/internal/command/bindingpartition/bindingpartition_test.go @@ -8,7 +8,7 @@ import ( ) func TestBuildRejectsCrossSurfaceRouteReferenceWithoutDelegation(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.052171025929495006005566277017011611265246234017302535159618229629097907645234") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.110309146081359910709631703704262165828281547205541367081938214321694298792717") record, exitCode, err := Build(validBindingPartitionInput(false)) if err != nil { t.Fatalf("Build() error = %v", err) diff --git a/internal/command/branchauthority/branchauthority_test.go b/internal/command/branchauthority/branchauthority_test.go index 941eeb4..d7a8869 100644 --- a/internal/command/branchauthority/branchauthority_test.go +++ b/internal/command/branchauthority/branchauthority_test.go @@ -8,7 +8,7 @@ import ( ) func TestBuildAdmitsAlignedRequiredBranchAndRejectsRequiredDrift(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.071179729500816627799208554853464300619289513317734324260194435111694229088770") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.006246519585105251684624732206882456828785038106904394850474532555486460156892") record, exitCode, err := Build(validBranchAuthorityInput("main")) if err != nil { t.Fatalf("Build() error=%v", err) diff --git a/internal/command/capabilitymapadmission/capability_map_admission_test.go b/internal/command/capabilitymapadmission/capability_map_admission_test.go index c8c86ac..3536946 100644 --- a/internal/command/capabilitymapadmission/capability_map_admission_test.go +++ b/internal/command/capabilitymapadmission/capability_map_admission_test.go @@ -36,7 +36,7 @@ func TestBuildCodeBaselineEmitsCandidateRequirementsAndBindings(t *testing.T) { } func TestBuildCodeBaselineFailsMissingCandidateRequirementAndAnchor(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.112196325409848896148926885908046594150757046298431637036021245352716060102765") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.041970606749231645876678604048526384598001204543928460199336186892471779260591") t.Parallel() input := validCapabilityMapInput("code_baseline") diff --git a/internal/command/changedpathset/changedpathset_test.go b/internal/command/changedpathset/changedpathset_test.go index b122860..62c643c 100644 --- a/internal/command/changedpathset/changedpathset_test.go +++ b/internal/command/changedpathset/changedpathset_test.go @@ -66,7 +66,7 @@ func validateContextJSONPointers(output any, contextRefs []any) (int, error) { } func TestBuildDeduplicatesAndFailsClosedOnInvalidPaths(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.060535997331386831986160802626392669912987256775670711883112001232124039341072") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.042433562921757846797576859053552522047936987715135990809815206155635675496110") result, err := Build(map[string]any{ "schemaVersion": json.Number("1"), "reportId": "proofkit.test.changed-path-set", diff --git a/internal/command/changeworkflowplan/admission.go b/internal/command/changeworkflowplan/admission.go new file mode 100644 index 0000000..55b7796 --- /dev/null +++ b/internal/command/changeworkflowplan/admission.go @@ -0,0 +1,341 @@ +package changeworkflowplan + +import ( + "fmt" + "unicode/utf8" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" + "github.com/research-engineering/agentic-proofkit/internal/kernel/unicodepolicy" +) + +var checkpointStates = map[string]struct{}{ + "not_started": {}, + "ready_for_review": {}, + "review_findings": {}, + "review_passed": {}, +} + +var contextRefKinds = map[string]struct{}{ + "artifact": {}, + "authority": {}, + "finding": {}, + "witness": {}, +} + +func admitInput(raw any) (admittedInput, error) { + record, ok := raw.(map[string]any) + if !ok { + return admittedInput{}, reject("proofkit.workflow.input_object", "change workflow input must be an object") + } + if err := knownKeys(record, []string{"checkpoint", "completedStageIds", "contextRefs", "governingAuthorityRefId", "requiredContextRefIds", "schemaVersion"}, "proofkit.workflow.input_fields"); err != nil { + return admittedInput{}, err + } + for _, key := range []string{"checkpoint", "completedStageIds", "contextRefs", "governingAuthorityRefId", "requiredContextRefIds", "schemaVersion"} { + if _, exists := record[key]; !exists { + return admittedInput{}, reject("proofkit.workflow.input_fields", "change workflow input is missing a required field") + } + } + if !admit.JSONNumberEquals(record["schemaVersion"], 1) { + return admittedInput{}, reject("proofkit.workflow.schema_version", "change workflow schemaVersion must be 1") + } + completed, err := admitCompletedStages(record["completedStageIds"]) + if err != nil { + return admittedInput{}, err + } + checkpointValue, err := admitCheckpoint(record["checkpoint"], len(completed) == len(stageTable)) + if err != nil { + return admittedInput{}, err + } + contextRefs, err := admitContextRefs(record["contextRefs"]) + if err != nil { + return admittedInput{}, err + } + governingAuthorityRefID, err := admitNullableRefID(record["governingAuthorityRefId"]) + if err != nil { + return admittedInput{}, err + } + required, err := admitRefIDArray(record["requiredContextRefIds"], "required context refs", maxRequiredContextRefs) + if err != nil { + return admittedInput{}, err + } + input := admittedInput{ + Checkpoint: checkpointValue, + CompletedStageIDs: completed, + ContextRefs: contextRefs, + GoverningAuthorityRefID: governingAuthorityRefID, + RequiredContextRefIDs: required, + SchemaVersion: 1, + } + if err := validateGlobalReferences(input); err != nil { + return admittedInput{}, err + } + return input, nil +} + +func admitCompletedStages(raw any) ([]string, error) { + values, ok := raw.([]any) + if !ok || len(values) > len(stageTable) { + return nil, reject("proofkit.workflow.stage_prefix", "completedStageIds must be a stage prefix") + } + result := make([]string, len(values)) + for index, rawValue := range values { + value, ok := rawValue.(string) + if !ok || value != stageTable[index].ID { + return nil, reject("proofkit.workflow.stage_prefix", "completedStageIds must be a stage prefix") + } + result[index] = value + } + return result, nil +} + +func admitCheckpoint(raw any, complete bool) (*checkpoint, error) { + if complete { + if raw != nil { + return nil, reject("proofkit.workflow.complete_checkpoint", "a complete workflow requires a null checkpoint") + } + return nil, nil + } + record, ok := raw.(map[string]any) + if !ok { + return nil, reject("proofkit.workflow.incomplete_checkpoint", "an incomplete workflow requires a checkpoint object") + } + state, err := admit.Enum(record["state"], checkpointStates, "change workflow checkpoint state") + if err != nil { + return nil, reject("proofkit.workflow.checkpoint_variant", "checkpoint state is invalid") + } + expectedKeys := map[string][]string{ + "not_started": {"state"}, + "ready_for_review": {"state", "subjectDigest", "subjectRefId"}, + "review_findings": {"assessmentSubjectDigest", "findingRefs", "state", "subjectDigest", "subjectRefId"}, + "review_passed": {"assessmentSubjectDigest", "state", "subjectDigest", "subjectRefId"}, + }[state] + if err := knownKeys(record, expectedKeys, "proofkit.workflow.checkpoint_fields"); err != nil { + return nil, err + } + for _, key := range expectedKeys { + if _, exists := record[key]; !exists { + return nil, reject("proofkit.workflow.checkpoint_fields", "checkpoint variant is missing a required field") + } + } + result := &checkpoint{State: state, FindingRefs: []string{}} + if state == "not_started" { + return result, nil + } + result.SubjectRefID, err = admitRefID(record["subjectRefId"]) + if err != nil { + return nil, err + } + result.SubjectDigest, err = admitDigest(record["subjectDigest"]) + if err != nil { + return nil, err + } + if state == "review_findings" || state == "review_passed" { + result.AssessmentSubjectDigest, err = admitDigest(record["assessmentSubjectDigest"]) + if err != nil { + return nil, err + } + if result.AssessmentSubjectDigest != result.SubjectDigest { + return nil, reject("proofkit.workflow.assessment_digest_mismatch", "assessmentSubjectDigest must equal subjectDigest") + } + } + if state == "review_findings" { + result.FindingRefs, err = admitRefIDArray(record["findingRefs"], "finding refs", maxFindings) + if err != nil { + return nil, err + } + if len(result.FindingRefs) == 0 { + return nil, reject("proofkit.workflow.finding_refs", "review_findings requires at least one finding ref") + } + } + return result, nil +} + +func admitContextRefs(raw any) ([]contextRef, error) { + values, ok := raw.([]any) + if !ok || len(values) > maxCandidateContextRefs { + return nil, reject("proofkit.workflow.candidate_context_limit", "contextRefs exceeds the candidate limit") + } + result := make([]contextRef, 0, len(values)) + pathBytes := 0 + for _, rawValue := range values { + record, ok := rawValue.(map[string]any) + if !ok { + return nil, reject("proofkit.workflow.context_ref_object", "contextRefs entries must be objects") + } + if err := knownKeys(record, []string{"artifactPath", "dependencyRefIds", "refId", "refKind", "subjectDigest"}, "proofkit.workflow.context_ref_fields"); err != nil { + return nil, err + } + for _, key := range []string{"artifactPath", "dependencyRefIds", "refId", "refKind", "subjectDigest"} { + if _, exists := record[key]; !exists { + return nil, reject("proofkit.workflow.context_ref_fields", "context ref is missing a required field") + } + } + refID, err := admitRefID(record["refId"]) + if err != nil { + return nil, err + } + kind, err := admit.Enum(record["refKind"], contextRefKinds, "change workflow context ref kind") + if err != nil { + return nil, reject("proofkit.workflow.context_ref_kind", "context ref kind is invalid") + } + pathValue, err := admitDisplayPath(record["artifactPath"]) + if err != nil { + return nil, err + } + digestValue, err := admitDigest(record["subjectDigest"]) + if err != nil { + return nil, err + } + dependencies, err := admitRefIDArray(record["dependencyRefIds"], "dependency refs", maxDependenciesPerRef) + if err != nil { + return nil, err + } + pathBytes += len(pathValue) + if pathBytes > maxCandidatePathBytes { + return nil, reject("proofkit.workflow.candidate_path_bytes_exceeded", "candidate context paths exceed the aggregate byte limit") + } + result = append(result, contextRef{ArtifactPath: pathValue, DependencyRefIDs: dependencies, RefID: refID, RefKind: kind, SubjectDigest: digestValue}) + } + for index := range result { + if index > 0 && result[index-1].RefID >= result[index].RefID { + return nil, reject("proofkit.workflow.context_ref_order", "contextRefs must be strictly sorted and unique by refId") + } + } + return result, nil +} + +func admitRefIDArray(raw any, label string, limit int) ([]string, error) { + values, ok := raw.([]any) + if !ok || len(values) > limit { + return nil, reject("proofkit.workflow.ref_array_bound", label+" exceed their bound") + } + result := make([]string, len(values)) + for index, rawValue := range values { + value, err := admitRefID(rawValue) + if err != nil { + return nil, err + } + result[index] = value + if index > 0 && result[index-1] >= result[index] { + return nil, reject("proofkit.workflow.ref_array_order", label+" must be strictly sorted and unique") + } + } + return result, nil +} + +func admitRefID(raw any) (string, error) { + value, err := admit.RuleID(raw, "change workflow reference id") + if err != nil { + return "", reject("proofkit.workflow.ref_id_limit_exceeded", "reference id is invalid") + } + return value, nil +} + +func admitNullableRefID(raw any) (*string, error) { + if raw == nil { + return nil, nil + } + value, err := admitRefID(raw) + if err != nil { + return nil, err + } + return &value, nil +} + +func admitDigest(raw any) (string, error) { + value, err := admit.SHA256Ref(raw, "change workflow subject digest") + if err != nil { + return "", reject("proofkit.workflow.subject_digest", "subject digest must be canonical lowercase sha256") + } + return value, nil +} + +func admitDisplayPath(raw any) (string, error) { + value, ok := raw.(string) + if !ok || len(value) > maxPathBytes || !utf8.ValidString(value) || admit.ContainsSecretLikeValue(value) { + return "", reject("proofkit.workflow.artifact_path", "artifact path is invalid") + } + for _, character := range value { + if unicodepolicy.IsUnsafeScalar(character) { + return "", reject("proofkit.workflow.artifact_path", "artifact path contains a display-unsafe scalar") + } + } + pathValue, err := admit.SafeRepoRelativePath(value, "change workflow artifact path") + if err != nil { + return "", reject("proofkit.workflow.artifact_path", "artifact path must be a canonical repository-relative POSIX path") + } + return pathValue, nil +} + +func validateGlobalReferences(input admittedInput) error { + byID := make(map[string]contextRef, len(input.ContextRefs)) + for _, ref := range input.ContextRefs { + byID[ref.RefID] = ref + } + for _, ref := range input.ContextRefs { + for _, dependencyID := range ref.DependencyRefIDs { + if _, ok := byID[dependencyID]; !ok { + return reject("proofkit.workflow.context_dependency_missing", "a context dependency does not resolve") + } + } + } + for _, requiredID := range input.RequiredContextRefIDs { + if _, ok := byID[requiredID]; !ok { + return reject("proofkit.workflow.required_context_missing", "a required context ref does not resolve") + } + } + if input.GoverningAuthorityRefID != nil { + ref, ok := byID[*input.GoverningAuthorityRefID] + if !ok || ref.RefKind != "authority" { + return reject("proofkit.workflow.governing_authority", "governingAuthorityRefId must resolve an authority ref") + } + } + if input.Checkpoint == nil || input.Checkpoint.State == "not_started" { + return nil + } + subject, ok := byID[input.Checkpoint.SubjectRefID] + if !ok || subject.RefKind != "artifact" || subject.SubjectDigest != input.Checkpoint.SubjectDigest { + return reject("proofkit.workflow.subject_identity", "checkpoint subject must resolve an equal artifact digest") + } + for _, findingID := range input.Checkpoint.FindingRefs { + finding, ok := byID[findingID] + if !ok || finding.RefKind != "finding" { + return reject("proofkit.workflow.finding_identity", "finding refs must resolve finding context refs") + } + } + return nil +} + +func knownKeys(record map[string]any, expected []string, ruleID string) error { + if err := admit.KnownKeys(record, expected, "change workflow record"); err != nil { + return reject(ruleID, "change workflow record has unsupported fields") + } + return nil +} + +type ruleError struct { + ruleID string + message string +} + +func (err ruleError) Error() string { return err.ruleID + ": " + err.message } + +func reject(ruleID string, message string) error { + return ruleError{ruleID: ruleID, message: message} +} + +// RuleID returns the stable failing rule without exposing rejected caller data. +func RuleID(err error) string { + if typed, ok := err.(ruleError); ok { + return typed.ruleID + } + return "proofkit.workflow.internal_error" +} + +func cloneStrings(values []string) []string { + return append([]string{}, values...) +} + +func invalidState(message string) error { + return fmt.Errorf("invalid admitted workflow state: %s", message) +} diff --git a/internal/command/changeworkflowplan/admission_test.go b/internal/command/changeworkflowplan/admission_test.go new file mode 100644 index 0000000..b2d9538 --- /dev/null +++ b/internal/command/changeworkflowplan/admission_test.go @@ -0,0 +1,47 @@ +package changeworkflowplan + +import ( + "strings" + "testing" +) + +func TestWorkflowIdentityPredicates(t *testing.T) { + t.Run("assessment_equality", func(t *testing.T) { + input := reviewInput("review_passed") + input["checkpoint"].(map[string]any)["assessmentSubjectDigest"] = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + requireReject(t, input) + }) + t.Run("canonical_digest", func(t *testing.T) { + input := reviewInput("ready_for_review") + input["checkpoint"].(map[string]any)["subjectDigest"] = strings.ToUpper(testDigest) + requireReject(t, input) + }) + t.Run("declared_digest_equality", func(t *testing.T) { + input := reviewInput("ready_for_review") + input["checkpoint"].(map[string]any)["subjectDigest"] = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + requireReject(t, input) + }) + t.Run("finding_kind", func(t *testing.T) { + input := reviewInput("review_findings") + input["checkpoint"].(map[string]any)["findingRefs"] = []any{"ctx.authority"} + requireReject(t, input) + }) + t.Run("identity_failure", func(t *testing.T) { + mutants := []func(map[string]any){ + func(input map[string]any) { delete(input["checkpoint"].(map[string]any), "subjectRefId") }, + func(input map[string]any) { input["checkpoint"].(map[string]any)["surplus"] = true }, + func(input map[string]any) { input["checkpoint"].(map[string]any)["subjectRefId"] = "missing.artifact" }, + func(input map[string]any) { input["checkpoint"].(map[string]any)["subjectRefId"] = "ctx.authority" }, + } + for _, mutate := range mutants { + input := reviewInput("ready_for_review") + mutate(input) + requireReject(t, input) + } + }) + t.Run("subject_artifact", func(t *testing.T) { + input := reviewInput("ready_for_review") + input["checkpoint"].(map[string]any)["subjectRefId"] = "ctx.authority" + requireReject(t, input) + }) +} diff --git a/internal/command/changeworkflowplan/agent_envelope.go b/internal/command/changeworkflowplan/agent_envelope.go new file mode 100644 index 0000000..11ff036 --- /dev/null +++ b/internal/command/changeworkflowplan/agent_envelope.go @@ -0,0 +1,148 @@ +package changeworkflowplan + +import ( + "crypto/sha256" + "encoding/hex" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/agentenvelope" + "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" +) + +func agentEnvelope(value projection) (map[string]any, error) { + contextRefs := make([]map[string]any, 0, len(value.Closure.Retained)) + for _, ref := range value.Closure.Retained { + contextRefs = append(contextRefs, map[string]any{ + "kind": ref.RefKind, + "nonClaim": "Caller-declared context does not prove source truth, freshness, completeness, or authority.", + "owner": "consumer_repository", + "purpose": "Admitted change-workflow context selected by least dependency closure.", + "ref": ref.ArtifactPath, + "refId": ref.RefID, + "role": contextRole(ref.RefKind), + "subjectDigest": ref.SubjectDigest, + }) + } + omitted := []map[string]any{} + if len(value.Closure.Omitted) > 0 { + omitted = append(omitted, map[string]any{ + "kind": "context_ref", + "nonClaim": "Omitted context remains caller-owned and may require a wider or full gate.", + "omissionId": "proofkit.change-workflow-plan.omitted-context", + "omittedCount": len(value.Closure.Omitted), + "reason": "candidate_not_required_by_least_dependency_closure", + }) + } + actionPlan := []map[string]any{} + blocked := []map[string]any{} + if value.Decision.OutputKind == "next_action" { + prompt := value.Prompt + actionPlan = append(actionPlan, map[string]any{ + "commandIds": []any{}, + "evidenceRefs": stringsValue(contextRefIDs(value.Closure.Retained)), + "instruction": prompt["candidateAction"], + "nonClaims": []any{promptNonClaim}, + "owner": prompt["ownerOrEscalationTarget"], + "phase": value.Decision.ActiveStageID, + "rationale": "The admitted checkpoint relation selects exactly one next action for the active stage.", + "stepId": "proofkit.change-workflow-plan.next-action", + }) + if value.Input.GoverningAuthorityRefID == nil { + blocked = append(blocked, map[string]any{ + "description": missingOwnerStop, + "evidenceRefs": []any{}, + "nonClaim": "This blocker does not identify or authorize a consuming-repository owner.", + "owner": "consumer_repository", + "preconditionId": "proofkit.change-workflow-plan.missing-governing-authority", + }) + } + } + sourceHash, err := stableHash(value.Plan) + if err != nil { + return nil, err + } + envelope := agentenvelope.Build(agentenvelope.Input{ + ActionPlan: actionPlan, + BlockedPreconditions: blocked, + Bounds: map[string]any{ + "escalation": "Caller must inspect omitted context or unresolved governing authority before mutation.", + "fanout": envelopeFanout(len(omitted), len(blocked)), + "maxActionItems": len(actionPlan), + "maxCommandRefs": 0, + "maxContextRefs": len(contextRefs), + "maxOmittedItems": len(omitted), + "maxReceiptRefs": 0, + "maxTokenBudget": nil, + "nonClaim": "Bounds count emitted records only and do not prove tokenizer-specific cost or semantic sufficiency.", + "omittedCount": len(value.Closure.Omitted), + }, + ClarificationQuestion: []map[string]any{}, + Commands: []map[string]any{}, + ContextRefs: contextRefs, + EnvelopeID: "proofkit.change-workflow-plan.agent-envelope", + NonClaims: boundaryNonClaims, + Omitted: omitted, + ReceiptRefs: []map[string]any{}, + RouteQuestions: []map[string]any{ + workflowQuestion("proofkit.agent.question.what-changed", "what changed", contextRefIDs(value.Closure.Retained)), + workflowQuestion("proofkit.agent.question.what-proves-it", "what proves it", contextRefIDs(value.Closure.Retained)), + workflowQuestion("proofkit.agent.question.who-owns-it", "who owns it", governingEvidence(value.Input.GoverningAuthorityRefID)), + }, + SourceReport: map[string]any{ + "artifactRef": nil, + "nonClaim": "Plan identity does not authenticate caller context or prove action execution.", + "reportId": reportKind, + "reportKind": reportKind, + "stableHash": sourceHash, + "state": "passed", + }, + }) + if err := enforceJSONCap(envelope); err != nil { + return nil, err + } + return envelope, nil +} + +func stableHash(value map[string]any) (string, error) { + encoded, err := stablejson.Marshal(value) + if err != nil { + return "", err + } + digest := sha256.Sum256(encoded) + return "sha256:" + hex.EncodeToString(digest[:]), nil +} + +func contextRole(kind string) string { + if kind == "authority" { + return "semantic_owner" + } + if kind == "finding" { + return "review_finding" + } + if kind == "witness" { + return "proof_binding" + } + return "owner_surface" +} + +func envelopeFanout(omitted int, blocked int) string { + if omitted > 0 || blocked > 0 { + return "wide_or_full_gate_required" + } + return "bounded" +} + +func workflowQuestion(id string, question string, refs []string) map[string]any { + return map[string]any{ + "evidenceRefs": stringsValue(refs), + "nonClaim": "Routing questions do not prove caller context completeness or answer truth.", + "question": question, + "questionId": id, + } +} + +func governingEvidence(value *string) []string { + if value == nil { + return []string{} + } + return []string{*value} +} diff --git a/internal/command/changeworkflowplan/change_workflow_plan_test.go b/internal/command/changeworkflowplan/change_workflow_plan_test.go new file mode 100644 index 0000000..9cb065a --- /dev/null +++ b/internal/command/changeworkflowplan/change_workflow_plan_test.go @@ -0,0 +1,76 @@ +package changeworkflowplan + +import "testing" + +func TestWorkflowPurityPredicates(t *testing.T) { + t.Run("admit_once", func(t *testing.T) { + input := initialInput() + result, err := Project(input) + if err != nil { + t.Fatal(err) + } + input["completedStageIds"] = []any{"architecture"} + if result.Plan["activeStageId"] != "architecture" { + t.Fatal("projected result retained mutable caller input") + } + }) + t.Run("derived_non_authority", func(t *testing.T) { + plan := requireBuild(t, initialInput()) + if plan["authority"] != "derived_non_authoritative_plan" { + t.Fatal("plan does not deny semantic authority") + } + if len(plan["nonClaims"].([]any)) != len(boundaryNonClaims) { + t.Fatal("plan lost boundary non-claims") + } + envelope, err := BuildAgentEnvelope(reviewInput("ready_for_review")) + if err != nil { + t.Fatal(err) + } + cost := envelope["costContract"].(map[string]any) + if cost["referenceClosurePreserved"] != true || cost["prunedLocalReferenceCount"] != 0 { + t.Fatalf("agent envelope lost local reference closure: %v", cost) + } + }) + t.Run("deterministic_output", func(t *testing.T) { + input := reviewInput("review_passed") + left, err := Project(input) + if err != nil { + t.Fatal(err) + } + right, err := Project(cloneMap(input)) + if err != nil { + t.Fatal(err) + } + if canonical(t, left.Plan) != canonical(t, right.Plan) || left.Text != right.Text || canonical(t, left.AgentEnvelope) != canonical(t, right.AgentEnvelope) { + t.Fatal("equal inputs produced unequal projections") + } + }) + t.Run("explicit_input", func(t *testing.T) { + input := initialInput() + input["repositoryRoot"] = "/tmp/repo" + requireReject(t, input) + }) + t.Run("no_execution", func(t *testing.T) { + result := requireBuild(t, initialInput()) + if result["outputKind"] != "next_action" { + t.Fatal("pure planner did not return its single projection") + } + }) + t.Run("single_output", func(t *testing.T) { + active := requireBuild(t, initialInput()) + terminal := requireBuild(t, terminalInput()) + if active["outputKind"] != "next_action" || terminal["outputKind"] != "workflow_complete" { + t.Fatal("output variants are not disjoint and total") + } + }) + t.Run("typed_decisions_only", func(t *testing.T) { + input := initialInput() + before := canonical(t, input) + if _, err := Project(input); err != nil { + t.Fatal(err) + } + if canonical(t, input) != before { + t.Fatal("projection mutated caller input") + } + }) +} diff --git a/internal/command/changeworkflowplan/context_closure.go b/internal/command/changeworkflowplan/context_closure.go new file mode 100644 index 0000000..b01f322 --- /dev/null +++ b/internal/command/changeworkflowplan/context_closure.go @@ -0,0 +1,90 @@ +package changeworkflowplan + +import "sort" + +func leastClosure(input admittedInput) (closureResult, error) { + byID := make(map[string]contextRef, len(input.ContextRefs)) + for _, ref := range input.ContextRefs { + byID[ref.RefID] = ref + } + seeds := make([]string, 0, len(input.RequiredContextRefIDs)+maxFindings+2) + seeds = append(seeds, input.RequiredContextRefIDs...) + if input.GoverningAuthorityRefID != nil { + seeds = append(seeds, *input.GoverningAuthorityRefID) + } + if input.Checkpoint != nil && input.Checkpoint.State != "not_started" { + seeds = append(seeds, input.Checkpoint.SubjectRefID) + seeds = append(seeds, input.Checkpoint.FindingRefs...) + } + sort.Strings(seeds) + selected := make(map[string]struct{}, len(seeds)) + pending := append([]string{}, seeds...) + for len(pending) > 0 { + id := pending[0] + pending = pending[1:] + if _, exists := selected[id]; exists { + continue + } + ref, exists := byID[id] + if !exists { + return closureResult{}, reject("proofkit.workflow.context_seed_missing", "a closure seed does not resolve") + } + selected[id] = struct{}{} + pending = append(pending, ref.DependencyRefIDs...) + } + result := closureResult{Omitted: []contextRef{}, Retained: []contextRef{}} + retainedPathBytes := 0 + for _, ref := range input.ContextRefs { + if _, ok := selected[ref.RefID]; ok { + result.Retained = append(result.Retained, ref) + retainedPathBytes += len(ref.ArtifactPath) + continue + } + result.Omitted = append(result.Omitted, ref) + } + if retainedPathBytes > maxRetainedPathBytes { + return closureResult{}, reject("proofkit.workflow.retained_path_bytes_exceeded", "retained context paths exceed the byte limit") + } + if len(result.Retained) > maxRetainedContextRefs { + return closureResult{}, reject("proofkit.workflow.retained_context_limit_exceeded", "retained context exceeds the reference limit") + } + return result, nil +} + +func contextRefIDs(refs []contextRef) []string { + result := make([]string, len(refs)) + for index, ref := range refs { + result[index] = ref.RefID + } + return result +} + +func contextRefPaths(refs []contextRef) []string { + result := make([]string, len(refs)) + for index, ref := range refs { + result[index] = ref.ArtifactPath + } + return result +} + +func contextRefsValue(refs []contextRef) []any { + result := make([]any, 0, len(refs)) + for _, ref := range refs { + result = append(result, map[string]any{ + "artifactPath": ref.ArtifactPath, + "dependencyRefIds": stringsValue(ref.DependencyRefIDs), + "refId": ref.RefID, + "refKind": ref.RefKind, + "subjectDigest": ref.SubjectDigest, + }) + } + return result +} + +func stringsValue(values []string) []any { + result := make([]any, len(values)) + for index, value := range values { + result[index] = value + } + return result +} diff --git a/internal/command/changeworkflowplan/context_closure_test.go b/internal/command/changeworkflowplan/context_closure_test.go new file mode 100644 index 0000000..715de22 --- /dev/null +++ b/internal/command/changeworkflowplan/context_closure_test.go @@ -0,0 +1,222 @@ +package changeworkflowplan + +import ( + "strings" + "testing" +) + +func TestWorkflowClosurePredicates(t *testing.T) { + t.Run("all_seeds_retained", func(t *testing.T) { + input := reviewInput("review_findings") + input["requiredContextRefIds"] = []any{"ctx.authority"} + plan := requireBuild(t, input) + requireEqual(t, plan["retainedContextRefIds"], []any{"ctx.artifact", "ctx.authority", "ctx.finding"}) + }) + t.Run("candidate_bound", func(t *testing.T) { + input := initialInput() + refs := make([]any, maxCandidateContextRefs+1) + for index := range refs { + id := "ctx." + twoDigits(index+1) + refs[index] = contextValue(id, "artifact", testDigest, nil) + } + input["contextRefs"] = refs + requireReject(t, input) + + for _, mutate := range []func([]any){ + func(values []any) { values[0], values[1] = values[1], values[0] }, + func(values []any) { values[1] = cloneValue(values[0]) }, + } { + input = initialInput() + values := []any{contextValue("ctx.01", "artifact", testDigest, nil), contextValue("ctx.02", "artifact", testDigest, nil)} + mutate(values) + input["contextRefs"] = values + requireReject(t, input) + } + input = initialInput() + ref := contextValue("ctx.path", "artifact", testDigest, nil) + ref["artifactPath"] = "evidence/" + strings.Repeat("p", maxPathBytes-len("evidence/")-len(".json")+1) + ".json" + input["contextRefs"] = []any{ref} + requireReject(t, input) + }) + t.Run("cycles_finite", func(t *testing.T) { + input := initialInput() + input["contextRefs"] = []any{ + contextValue("cycle.a", "artifact", testDigest, []string{"cycle.b"}), + contextValue("cycle.b", "witness", "sha256:3333333333333333333333333333333333333333333333333333333333333333", []string{"cycle.a"}), + } + input["requiredContextRefIds"] = []any{"cycle.a"} + plan := requireBuild(t, input) + requireEqual(t, plan["retainedContextRefIds"], []any{"cycle.a", "cycle.b"}) + }) + t.Run("dependency_bound", func(t *testing.T) { + input := initialInput() + dependencies := make([]string, maxDependenciesPerRef+1) + for index := range dependencies { + dependencies[index] = "dep." + twoDigits(index+1) + } + input["contextRefs"] = []any{contextValue("ctx.root", "artifact", testDigest, dependencies)} + requireReject(t, input) + for _, invalid := range [][]string{{"dep.02", "dep.01"}, {"dep.01", "dep.01"}, {"dep.missing"}} { + input = initialInput() + input["contextRefs"] = []any{contextValue("ctx.root", "artifact", testDigest, invalid)} + requireReject(t, input) + } + }) + t.Run("dependency_closed", func(t *testing.T) { + input := initialInput() + input["contextRefs"] = []any{ + contextValue("ctx.root", "artifact", testDigest, []string{"ctx.witness"}), + contextValue("ctx.witness", "witness", "sha256:4444444444444444444444444444444444444444444444444444444444444444", nil), + } + input["requiredContextRefIds"] = []any{"ctx.root"} + plan := requireBuild(t, input) + requireEqual(t, plan["retainedContextRefIds"], []any{"ctx.root", "ctx.witness"}) + }) + t.Run("empty_context", func(t *testing.T) { + plan := requireBuild(t, initialInput()) + requireEqual(t, plan["retainedContextRefIds"], []any{}) + requireEqual(t, plan["omittedContextRefIds"], []any{}) + }) + t.Run("exact_omissions", func(t *testing.T) { + input := initialInput() + input["contextRefs"] = []any{ + contextValue("ctx.keep", "artifact", testDigest, nil), + contextValue("ctx.omit", "finding", "sha256:3333333333333333333333333333333333333333333333333333333333333333", nil), + } + input["requiredContextRefIds"] = []any{"ctx.keep"} + plan := requireBuild(t, input) + requireEqual(t, plan["omittedContextRefIds"], []any{"ctx.omit"}) + }) + t.Run("json_envelope_caps", func(t *testing.T) { + oversized := map[string]any{"payload": strings.Repeat("x", maxJSONBytes)} + if err := enforceJSONCap(oversized); err == nil { + t.Fatal("oversized JSON was admitted") + } + if _, err := BuildAgentEnvelope(initialInput()); err != nil { + t.Fatalf("bounded envelope rejected: %v", err) + } + }) + t.Run("least_fixed_point", func(t *testing.T) { + input := initialInput() + input["contextRefs"] = []any{ + contextValue("ctx.branch", "artifact", testDigest, []string{"ctx.leaf"}), + contextValue("ctx.irrelevant", "finding", "sha256:5555555555555555555555555555555555555555555555555555555555555555", nil), + contextValue("ctx.leaf", "witness", "sha256:4444444444444444444444444444444444444444444444444444444444444444", nil), + } + input["requiredContextRefIds"] = []any{"ctx.branch"} + plan := requireBuild(t, input) + requireEqual(t, plan["retainedContextRefIds"], []any{"ctx.branch", "ctx.leaf"}) + }) + t.Run("ref_id_byte_bound", func(t *testing.T) { + maxID := "r" + strings.Repeat("a", 255) + input := initialInput() + input["contextRefs"] = []any{contextValue(maxID, "artifact", testDigest, nil)} + input["requiredContextRefIds"] = []any{maxID} + if _, err := Build(input); err != nil { + t.Fatalf("256-byte ID rejected: %v", err) + } + overID := "r" + strings.Repeat("a", 256) + mutants := []map[string]any{} + candidate := initialInput() + candidate["contextRefs"] = []any{contextValue(overID, "artifact", testDigest, nil)} + mutants = append(mutants, candidate) + governing := initialInput() + governing["governingAuthorityRefId"] = overID + mutants = append(mutants, governing) + required := initialInput() + required["requiredContextRefIds"] = []any{overID} + mutants = append(mutants, required) + dependency := initialInput() + dependency["contextRefs"] = []any{contextValue("ctx.root", "artifact", testDigest, []string{overID})} + mutants = append(mutants, dependency) + subject := reviewInput("ready_for_review") + subject["checkpoint"].(map[string]any)["subjectRefId"] = overID + mutants = append(mutants, subject) + finding := reviewInput("review_findings") + finding["checkpoint"].(map[string]any)["findingRefs"] = []any{overID} + mutants = append(mutants, finding) + for _, mutant := range mutants { + err := requireReject(t, mutant) + if RuleID(err) != "proofkit.workflow.ref_id_limit_exceeded" { + t.Fatalf("unexpected rule %s", RuleID(err)) + } + } + }) + t.Run("retained_bound", func(t *testing.T) { + input := initialInput() + refs := make([]any, maxRetainedContextRefs+1) + for index := range refs { + id := "ctx." + twoDigits(index+1) + dependencies := []string{} + if index+1 < len(refs) { + dependencies = []string{"ctx." + twoDigits(index+2)} + } + refs[index] = contextValue(id, "artifact", testDigest, dependencies) + } + input["contextRefs"] = refs + input["requiredContextRefIds"] = []any{"ctx.01"} + if rule := RuleID(requireReject(t, input)); rule != "proofkit.workflow.retained_context_limit_exceeded" { + t.Fatalf("unexpected retained-count rule %s", rule) + } + input = initialInput() + refs = make([]any, maxRetainedContextRefs+1) + for index := range refs { + id := "path." + twoDigits(index+1) + dependencies := []string{} + if index+1 < len(refs) { + dependencies = []string{"path." + twoDigits(index+2)} + } + ref := contextValue(id, "artifact", testDigest, dependencies) + ref["artifactPath"] = "evidence/" + twoDigits(index+1) + "-" + strings.Repeat("p", 367) + ".json" + refs[index] = ref + } + input["contextRefs"] = refs + input["requiredContextRefIds"] = []any{"path.01"} + if rule := RuleID(requireReject(t, input)); rule != "proofkit.workflow.retained_path_bytes_exceeded" { + t.Fatalf("unexpected retained-path rule %s", rule) + } + }) + t.Run("seed_bound", func(t *testing.T) { + input := initialInput() + seeds := make([]any, maxRequiredContextRefs+1) + for index := range seeds { + seeds[index] = "ctx." + twoDigits(index+1) + } + input["requiredContextRefIds"] = seeds + requireReject(t, input) + for _, invalid := range []([]any){{"ctx.02", "ctx.01"}, {"ctx.01", "ctx.01"}} { + input = initialInput() + input["requiredContextRefIds"] = invalid + requireReject(t, input) + } + }) + t.Run("single_envelope_omission", func(t *testing.T) { + input := initialInput() + input["contextRefs"] = []any{contextValue("ctx.omit", "artifact", testDigest, nil)} + envelope, err := BuildAgentEnvelope(input) + if err != nil { + t.Fatal(err) + } + omitted := envelope["omitted"].([]any) + if len(omitted) != 1 || omitted[0].(map[string]any)["omittedCount"] != 1 { + t.Fatalf("unexpected omission summary: %v", omitted) + } + }) + t.Run("text_cap", func(t *testing.T) { + lines, err := BuildTextProjection(initialInput()) + if err != nil { + t.Fatal(err) + } + lines[0].Label = strings.Repeat("x", maxTextBytes) + if _, err := RenderText(lines); err == nil { + t.Fatal("oversized text was admitted") + } + }) +} + +func twoDigits(value int) string { + if value < 10 { + return "0" + string(rune('0'+value)) + } + return string([]byte{byte('0' + value/10), byte('0' + value%10)}) +} diff --git a/internal/command/changeworkflowplan/decision.go b/internal/command/changeworkflowplan/decision.go new file mode 100644 index 0000000..e7881b3 --- /dev/null +++ b/internal/command/changeworkflowplan/decision.go @@ -0,0 +1,104 @@ +package changeworkflowplan + +var checkpointRelation = buildCheckpointRelation() + +func buildCheckpointRelation() []stateRow { + rows := make([]stateRow, 0, len(stageTable)*len(checkpointStates)+1) + for stageIndex, stage := range stageTable { + for _, state := range []string{"not_started", "ready_for_review", "review_findings", "review_passed"} { + action := stage.FirstAction + switch state { + case "ready_for_review": + action = "review" + case "review_findings": + action = "repair" + case "review_passed": + action = "accept_stage" + } + rows = append(rows, stateRow{ + Action: action, + ActiveStageID: stage.ID, + CheckpointState: state, + CompletedCount: stageIndex, + OutputKind: "next_action", + }) + } + } + return append(rows, stateRow{CompletedCount: len(stageTable), OutputKind: "workflow_complete", Terminal: true}) +} + +func decide(input admittedInput) (stateDecision, error) { + for _, row := range checkpointRelation { + if row.CompletedCount != len(input.CompletedStageIDs) { + continue + } + if row.Terminal { + if input.Checkpoint != nil { + return stateDecision{}, invalidState("terminal checkpoint is non-null") + } + return stateDecision{OutputKind: row.OutputKind}, nil + } + if input.Checkpoint == nil || input.Checkpoint.State != row.CheckpointState { + continue + } + decision := stateDecision{ + Action: row.Action, + ActiveStageID: row.ActiveStageID, + CheckpointState: row.CheckpointState, + OutputKind: row.OutputKind, + } + if row.Action == "accept_stage" { + completed := append(cloneStrings(input.CompletedStageIDs), row.ActiveStageID) + var nextCheckpoint *checkpoint + if len(completed) < len(stageTable) { + nextCheckpoint = &checkpoint{State: "not_started", FindingRefs: []string{}} + } + decision.SuccessorStateDelta = &successorStateDelta{Checkpoint: nextCheckpoint, CompletedStageIDs: completed} + } + return decision, nil + } + return stateDecision{}, invalidState("no checkpoint relation row matched") +} + +// MergeSuccessor applies the only fields owned by a successor delta and +// returns a fresh input value for ordinary admission tests and integrations. +func MergeSuccessor(prior map[string]any, delta map[string]any) (map[string]any, error) { + if err := knownKeys(delta, []string{"checkpoint", "completedStageIds"}, "proofkit.workflow.successor_delta_fields"); err != nil { + return nil, err + } + if _, ok := delta["checkpoint"]; !ok { + return nil, reject("proofkit.workflow.successor_delta_fields", "successor delta is missing checkpoint") + } + if _, ok := delta["completedStageIds"]; !ok { + return nil, reject("proofkit.workflow.successor_delta_fields", "successor delta is missing completedStageIds") + } + merged := make(map[string]any, len(prior)) + for key, value := range prior { + merged[key] = cloneJSONValue(value) + } + merged["completedStageIds"] = cloneJSONValue(delta["completedStageIds"]) + merged["checkpoint"] = cloneJSONValue(delta["checkpoint"]) + if _, err := admitInput(merged); err != nil { + return nil, err + } + return merged, nil +} + +func cloneJSONValue(value any) any { + switch typed := value.(type) { + case map[string]any: + result := make(map[string]any, len(typed)) + for key, child := range typed { + result[key] = cloneJSONValue(child) + } + return result + case []any: + result := make([]any, len(typed)) + for index, child := range typed { + result[index] = cloneJSONValue(child) + } + return result + default: + return typed + } +} diff --git a/internal/command/changeworkflowplan/dependency_test.go b/internal/command/changeworkflowplan/dependency_test.go new file mode 100644 index 0000000..f5d85fb --- /dev/null +++ b/internal/command/changeworkflowplan/dependency_test.go @@ -0,0 +1,96 @@ +package changeworkflowplan + +import ( + "go/parser" + "go/token" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestWorkflowAmbientAuthorityPredicates(t *testing.T) { + production := productionSource(t) + t.Run("bounded_transport_only", func(t *testing.T) { + if _, err := Build(initialInput()); err != nil { + t.Fatalf("explicit bounded input was rejected: %v", err) + } + }) + t.Run("forbidden_fields", func(t *testing.T) { + for _, field := range []string{"repositoryRoot", "prompt", "maxContext", "command", "environment", "receipt", "authority"} { + input := initialInput() + input[field] = "caller-value" + requireReject(t, input) + } + }) + t.Run("no_clock_random_network", func(t *testing.T) { + assertNoImports(t, production, "time", "math/rand", "crypto/rand", "net", "net/http") + }) + t.Run("no_filesystem_git", func(t *testing.T) { + assertNoImports(t, production, "io/fs", "os", "path/filepath") + assertNoSourceTokens(t, production, "git.Command", "go-git") + }) + t.Run("no_process_environment", func(t *testing.T) { + assertNoImports(t, production, "os/exec") + assertNoSourceTokens(t, production, "Getenv(", "LookupEnv(", "Environ(", "Command(") + }) + t.Run("no_setup_or_route", func(t *testing.T) { + assertNoSourceTokens(t, production, "agentroute", "setup facade", "repository scan", "consumer-specific") + }) +} + +type sourceUnit struct { + name string + imports map[string]struct{} + text string +} + +func productionSource(t *testing.T) []sourceUnit { + t.Helper() + entries, err := os.ReadDir(".") + if err != nil { + t.Fatal(err) + } + result := []sourceUnit{} + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") || strings.HasSuffix(entry.Name(), "_test.go") { + continue + } + content, err := os.ReadFile(entry.Name()) + if err != nil { + t.Fatal(err) + } + parsed, err := parser.ParseFile(token.NewFileSet(), entry.Name(), content, parser.ImportsOnly) + if err != nil { + t.Fatal(err) + } + imports := map[string]struct{}{} + for _, imported := range parsed.Imports { + imports[strings.Trim(imported.Path.Value, "\"")] = struct{}{} + } + result = append(result, sourceUnit{name: filepath.Base(entry.Name()), imports: imports, text: string(content)}) + } + return result +} + +func assertNoImports(t *testing.T, units []sourceUnit, forbidden ...string) { + t.Helper() + for _, unit := range units { + for _, imported := range forbidden { + if _, exists := unit.imports[imported]; exists { + t.Fatalf("%s imports forbidden package %s", unit.name, imported) + } + } + } +} + +func assertNoSourceTokens(t *testing.T, units []sourceUnit, forbidden ...string) { + t.Helper() + for _, unit := range units { + for _, tokenValue := range forbidden { + if strings.Contains(unit.text, tokenValue) { + t.Fatalf("%s contains forbidden production token %q", unit.name, tokenValue) + } + } + } +} diff --git a/internal/command/changeworkflowplan/model.go b/internal/command/changeworkflowplan/model.go new file mode 100644 index 0000000..8f89576 --- /dev/null +++ b/internal/command/changeworkflowplan/model.go @@ -0,0 +1,116 @@ +package changeworkflowplan + +const ( + reportKind = "proofkit.change-workflow-plan" + maxCandidateContextRefs = 48 + maxCandidatePathBytes = 18_432 + maxDependenciesPerRef = 8 + maxFindings = 12 + maxJSONBytes = 128 << 10 + maxPathBytes = 384 + maxRequiredContextRefs = 12 + maxRetainedContextRefs = 24 + maxRetainedPathBytes = 9_216 + maxTextBytes = 8_192 + maxTextLines = 24 + missingAuthorityTarget = "missing_consuming_repository_semantic_owner" + missingConsumerWitness = "missing_consumer_witness" +) + +var stageTable = []stageDefinition{ + {ID: "architecture", FirstAction: "author"}, + {ID: "design", FirstAction: "author"}, + {ID: "implementation_plan", FirstAction: "author"}, + {ID: "implementation", FirstAction: "implement"}, + {ID: "verification", FirstAction: "verify"}, + {ID: "pull_request", FirstAction: "open_pull_request"}, + {ID: "closeout", FirstAction: "closeout"}, +} + +var boundaryNonClaims = []string{ + "Change workflow plans do not authenticate caller assertions, context, digests, checkpoint state, or external state.", + "Change workflow plans do not approve repository edits, merge, release, rollout, or production readiness.", + "Change workflow plans do not execute or supervise agents, native witnesses, repository operations, providers, or CI.", + "Change workflow prompts, reports, text, and envelopes are derived projections and are not semantic authority.", +} + +type stageDefinition struct { + ID string + FirstAction string +} + +type contextRef struct { + ArtifactPath string + DependencyRefIDs []string + RefID string + RefKind string + SubjectDigest string +} + +type checkpoint struct { + AssessmentSubjectDigest string + FindingRefs []string + State string + SubjectDigest string + SubjectRefID string +} + +type admittedInput struct { + Checkpoint *checkpoint + CompletedStageIDs []string + ContextRefs []contextRef + GoverningAuthorityRefID *string + RequiredContextRefIDs []string + SchemaVersion int +} + +type stateDecision struct { + Action string + ActiveStageID string + CheckpointState string + OutputKind string + SuccessorStateDelta *successorStateDelta +} + +type stateRow struct { + Action string + ActiveStageID string + CheckpointState string + CompletedCount int + OutputKind string + Terminal bool +} + +type successorStateDelta struct { + Checkpoint *checkpoint + CompletedStageIDs []string +} + +type closureResult struct { + Omitted []contextRef + Retained []contextRef +} + +type projection struct { + Decision stateDecision + Input admittedInput + Closure closureResult + Prompt map[string]any + Plan map[string]any +} + +// TextLine separates command-owned labels from admitted values so a caller may +// style labels without parsing or altering semantic text values. +type TextLine struct { + Label string + Value string +} + +// Result is the complete set of deterministic presentation projections for +// one admitted snapshot. Callers select one projection; none is authoritative. +type Result struct { + AgentEnvelope map[string]any + Plan map[string]any + Text string + TextLines []TextLine +} diff --git a/internal/command/changeworkflowplan/prompt.go b/internal/command/changeworkflowplan/prompt.go new file mode 100644 index 0000000..f09935f --- /dev/null +++ b/internal/command/changeworkflowplan/prompt.go @@ -0,0 +1,113 @@ +package changeworkflowplan + +const ( + promptUncertainty = "Caller-declared context, digests, checkpoint state, and external state are unauthenticated." + promptNonClaim = "This packet does not prove execution, correctness, review quality, merge approval, release approval, rollout, or production readiness." + missingOwnerStop = "Do not mutate the repository until the caller supplies the consuming-repository semantic owner and governing authority coordinates." +) + +type actionProfile struct { + CandidateAction string + ExpectedNextCheckpoint string + StopCondition string +} + +var promptProfiles = map[string]actionProfile{ + "author": { + CandidateAction: "Author only the active-stage artifact under the consuming repository's semantic owner and bind the result to a caller-owned artifact reference and digest.", + StopCondition: "Stop after the artifact and digest are ready for independent review.", + ExpectedNextCheckpoint: "ready_for_review", + }, + "implement": { + CandidateAction: "Implement only the accepted owner-scoped plan, preserving its non-claims and adding its native positive and negative proof paths.", + StopCondition: "Stop after the implementation artifact and digest are ready for independent review.", + ExpectedNextCheckpoint: "ready_for_review", + }, + "verify": { + CandidateAction: "Run the consuming repository's positive controls and independent near-miss falsifiers against the exact caller-declared subject.", + StopCondition: "Stop after bounded verification evidence and its subject digest are ready for independent review.", + ExpectedNextCheckpoint: "ready_for_review", + }, + "open_pull_request": { + CandidateAction: "Open a pull request only under consuming-repository policy for the exact reviewed head and expose its native checks without claiming merge authority.", + StopCondition: "Stop after the pull-request artifact and exact head digest are ready for independent review.", + ExpectedNextCheckpoint: "ready_for_review", + }, + "closeout": { + CandidateAction: "Assemble bounded closeout evidence for the exact reviewed subject without merging, releasing, rolling out, or declaring production readiness.", + StopCondition: "Stop after the closeout artifact and digest are ready for independent review.", + ExpectedNextCheckpoint: "ready_for_review", + }, + "review": { + CandidateAction: "Independently attempt to falsify the active-stage artifact under its declared owner, bounds, non-claims, and exact subject digest.", + StopCondition: "Stop with review_findings bound to every admitted finding, or review_passed bound to the unchanged assessed subject digest.", + ExpectedNextCheckpoint: "review_findings_or_review_passed", + }, + "repair": { + CandidateAction: "Repair every referenced finding under the same semantic owner, produce a new subject digest, and do not emit a pass claim.", + StopCondition: "Stop when the repaired artifact and new digest are ready for another independent review.", + ExpectedNextCheckpoint: "ready_for_review", + }, + "accept_stage": { + CandidateAction: "Apply only the reported successorStateDelta to the prior snapshot, preserve all context fields byte-for-byte, and submit the merged snapshot for ordinary admission.", + StopCondition: "Stop after constructing the merged immutable snapshot; do not infer execution, approval, merge, or release from stage acceptance.", + ExpectedNextCheckpoint: "successor_state_delta", + }, +} + +func buildPrompt(input admittedInput, decision stateDecision, closure closureResult) (map[string]any, error) { + profile, ok := promptProfiles[decision.Action] + if !ok { + return nil, invalidState("action has no prompt profile") + } + ownerTarget := missingAuthorityTarget + if input.GoverningAuthorityRefID != nil { + ownerTarget = *input.GoverningAuthorityRefID + } + stopCondition := profile.StopCondition + if input.GoverningAuthorityRefID == nil { + stopCondition += " " + missingOwnerStop + } + observedFact := map[string]any{ + "activeStageId": decision.ActiveStageID, + "checkpointState": decision.CheckpointState, + "completedStageIds": stringsValue(input.CompletedStageIDs), + "governingAuthorityRefId": nullableStringValue(input.GoverningAuthorityRefID), + "omittedContextRefIds": stringsValue(contextRefIDs(closure.Omitted)), + "retainedContextRefIds": stringsValue(contextRefIDs(closure.Retained)), + } + coordinates := map[string]any{ + "activeStageId": decision.ActiveStageID, + "checkpointState": decision.CheckpointState, + "retainedArtifactPaths": stringsValue(contextRefPaths(closure.Retained)), + "retainedContextRefIds": stringsValue(contextRefIDs(closure.Retained)), + } + if input.GoverningAuthorityRefID != nil { + coordinates["governingAuthorityRefId"] = *input.GoverningAuthorityRefID + } + if input.Checkpoint != nil && input.Checkpoint.State != "not_started" { + coordinates["subjectDigest"] = input.Checkpoint.SubjectDigest + coordinates["subjectRefId"] = input.Checkpoint.SubjectRefID + } + if input.Checkpoint != nil && len(input.Checkpoint.FindingRefs) > 0 { + coordinates["findingRefs"] = stringsValue(input.Checkpoint.FindingRefs) + } + return map[string]any{ + "candidateAction": profile.CandidateAction, + "coordinates": coordinates, + "expectedNextCheckpoint": profile.ExpectedNextCheckpoint, + "nonClaim": promptNonClaim, + "observedFact": observedFact, + "ownerOrEscalationTarget": ownerTarget, + "proofCommandOrMissingWitness": map[string]any{"state": missingConsumerWitness}, + "stopCondition": stopCondition, + "uncertainty": promptUncertainty, + }, nil +} + +func nullableStringValue(value *string) any { + if value == nil { + return nil + } + return *value +} diff --git a/internal/command/changeworkflowplan/prompt_test.go b/internal/command/changeworkflowplan/prompt_test.go new file mode 100644 index 0000000..5bfc6eb --- /dev/null +++ b/internal/command/changeworkflowplan/prompt_test.go @@ -0,0 +1,145 @@ +package changeworkflowplan + +import ( + "strings" + "testing" +) + +func TestWorkflowPromptPredicates(t *testing.T) { + t.Run("caller_not_styled", func(t *testing.T) { + input := initialInput() + input["contextRefs"] = []any{contextValue("ctx.value", "artifact", testDigest, nil)} + input["requiredContextRefIds"] = []any{"ctx.value"} + result, err := Project(input) + if err != nil { + t.Fatal(err) + } + if containsANSI(canonical(t, result.Plan)) || containsANSI(result.Text) || containsANSI(canonical(t, result.AgentEnvelope)) { + t.Fatal("caller projection contains ANSI styling") + } + }) + t.Run("candidate_action", func(t *testing.T) { + for index := range stageTable { + for _, state := range []string{"not_started", "ready_for_review", "review_findings", "review_passed"} { + plan := requireBuild(t, inputForStage(index, state)) + prompt := plan["prompt"].(map[string]any) + if len(prompt) != 9 { + t.Fatalf("prompt has %d fields", len(prompt)) + } + profile := promptProfiles[plan["action"].(string)] + if prompt["candidateAction"] != profile.CandidateAction { + t.Fatal("candidate action drifted from action owner") + } + } + } + }) + t.Run("coordinates", func(t *testing.T) { + plan := requireBuild(t, reviewInput("review_findings")) + coordinates := plan["prompt"].(map[string]any)["coordinates"].(map[string]any) + want := []string{"activeStageId", "checkpointState", "findingRefs", "governingAuthorityRefId", "retainedArtifactPaths", "retainedContextRefIds", "subjectDigest", "subjectRefId"} + got := make([]string, 0, len(coordinates)) + for _, key := range want { + if _, exists := coordinates[key]; !exists { + t.Fatalf("missing coordinate %s", key) + } + got = append(got, key) + } + if len(got) != len(coordinates) { + t.Fatalf("unexpected coordinate set: %v", coordinates) + } + }) + t.Run("expected_checkpoint", func(t *testing.T) { + for action, profile := range promptProfiles { + if profile.ExpectedNextCheckpoint == "" { + t.Fatalf("action %s has no expected checkpoint", action) + } + } + }) + t.Run("missing_governing_authority_stop", func(t *testing.T) { + input := initialInput() + input["contextRefs"] = []any{contextValue("ctx.authority", "authority", testDigest, nil)} + input["requiredContextRefIds"] = []any{"ctx.authority"} + plan := requireBuild(t, input) + prompt := plan["prompt"].(map[string]any) + if !strings.Contains(prompt["stopCondition"].(string), missingOwnerStop) { + t.Fatal("retained non-governing authority did not block mutation") + } + }) + t.Run("missing_witness", func(t *testing.T) { + plan := requireBuild(t, initialInput()) + proof := plan["prompt"].(map[string]any)["proofCommandOrMissingWitness"].(map[string]any) + if proof["state"] != missingConsumerWitness || len(proof) != 1 { + t.Fatalf("unexpected proof state: %v", proof) + } + }) + t.Run("nonclaim", func(t *testing.T) { + plan := requireBuild(t, initialInput()) + value := plan["prompt"].(map[string]any)["nonClaim"].(string) + for _, denied := range []string{"execution", "merge approval", "production readiness"} { + if !strings.Contains(value, denied) { + t.Fatalf("prompt does not deny %s", denied) + } + } + }) + t.Run("observed_fact", func(t *testing.T) { + plan := requireBuild(t, initialInput()) + fact := plan["prompt"].(map[string]any)["observedFact"].(map[string]any) + want := []string{"activeStageId", "checkpointState", "completedStageIds", "governingAuthorityRefId", "omittedContextRefIds", "retainedContextRefIds"} + if len(fact) != len(want) { + t.Fatalf("observed fact has unexpected fields: %v", fact) + } + for _, key := range want { + if _, exists := fact[key]; !exists { + t.Fatalf("observed fact lacks %s", key) + } + } + }) + t.Run("owner_target", func(t *testing.T) { + missing := requireBuild(t, initialInput())["prompt"].(map[string]any) + if missing["ownerOrEscalationTarget"] != missingAuthorityTarget { + t.Fatal("missing owner target is not closed") + } + present := requireBuild(t, reviewInput("ready_for_review"))["prompt"].(map[string]any) + if present["ownerOrEscalationTarget"] != "ctx.authority" { + t.Fatal("governing owner target was not projected") + } + }) + t.Run("stop_condition", func(t *testing.T) { + for action, profile := range promptProfiles { + if !strings.HasPrefix(profile.StopCondition, "Stop ") || strings.Contains(strings.ToLower(profile.StopCondition), "wait") { + t.Fatalf("action %s has a non-terminal stop condition", action) + } + } + }) + t.Run("typed_display_admission", func(t *testing.T) { + for _, unsafePath := range []string{ + "evidence/unsafe\u2028path.json", + "evidence/unsafe\u2066path.json", + "evidence/unsafe\x1bpath.json", + "evidence/" + string([]byte{0xff}) + ".json", + } { + input := initialInput() + ref := contextValue("ctx.path", "artifact", testDigest, nil) + ref["artifactPath"] = unsafePath + input["contextRefs"] = []any{ref} + requireReject(t, input) + } + }) + t.Run("uncertainty", func(t *testing.T) { + plan := requireBuild(t, initialInput()) + if plan["prompt"].(map[string]any)["uncertainty"] != promptUncertainty { + t.Fatal("prompt uncertainty drifted") + } + }) + t.Run("unsafe_no_echo", func(t *testing.T) { + secret := "api_key=caller-sensitive-value" + input := initialInput() + ref := contextValue("ctx.path", "artifact", testDigest, nil) + ref["artifactPath"] = "evidence/" + secret + ".json" + input["contextRefs"] = []any{ref} + err := requireReject(t, input) + if strings.Contains(err.Error(), secret) || strings.Contains(err.Error(), "caller-sensitive-value") { + t.Fatal("rejection echoed caller-owned secret-shaped text") + } + }) +} diff --git a/internal/command/changeworkflowplan/report.go b/internal/command/changeworkflowplan/report.go new file mode 100644 index 0000000..14c3b0c --- /dev/null +++ b/internal/command/changeworkflowplan/report.go @@ -0,0 +1,140 @@ +package changeworkflowplan + +import ( + "encoding/json" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" +) + +// Project admits raw input once and derives every supported presentation from +// the resulting immutable values. +func Project(raw any) (Result, error) { + internal, err := project(raw) + if err != nil { + return Result{}, err + } + lines := textProjection(internal) + textValue, err := renderText(lines) + if err != nil { + return Result{}, err + } + envelope, err := agentEnvelope(internal) + if err != nil { + return Result{}, err + } + return Result{AgentEnvelope: envelope, Plan: internal.Plan, Text: textValue, TextLines: cloneTextLines(lines)}, nil +} + +func project(raw any) (projection, error) { + input, err := admitInput(raw) + if err != nil { + return projection{}, err + } + decision, err := decide(input) + if err != nil { + return projection{}, err + } + closure, err := leastClosure(input) + if err != nil { + return projection{}, err + } + prompt := map[string]any(nil) + if decision.OutputKind == "next_action" { + prompt, err = buildPrompt(input, decision, closure) + if err != nil { + return projection{}, err + } + } + internal := projection{Decision: decision, Input: input, Closure: closure, Prompt: prompt} + internal.Plan = planValue(internal) + if err := enforceJSONCap(internal.Plan); err != nil { + return projection{}, err + } + return internal, nil +} + +// Build returns the canonical JSON projection for one explicit snapshot. +func Build(raw any) (map[string]any, error) { + result, err := project(raw) + if err != nil { + return nil, err + } + return result.Plan, nil +} + +// BuildText returns the bounded plain-text projection for one explicit snapshot. +func BuildText(raw any) (string, error) { + result, err := project(raw) + if err != nil { + return "", err + } + return renderText(textProjection(result)) +} + +// BuildTextProjection returns a fresh command-owned label/value projection. +// Labels may be styled by an app; values must be emitted unchanged. +func BuildTextProjection(raw any) ([]TextLine, error) { + result, err := project(raw) + if err != nil { + return nil, err + } + return cloneTextLines(textProjection(result)), nil +} + +// BuildAgentEnvelope returns the bounded generic agent-envelope projection. +func BuildAgentEnvelope(raw any) (map[string]any, error) { + result, err := project(raw) + if err != nil { + return nil, err + } + return agentEnvelope(result) +} + +func planValue(value projection) map[string]any { + plan := map[string]any{ + "authority": "derived_non_authoritative_plan", + "completedStageIds": stringsValue(value.Input.CompletedStageIDs), + "nonClaims": stringsValue(boundaryNonClaims), + "omittedContextCount": len(value.Closure.Omitted), + "omittedContextRefIds": stringsValue(contextRefIDs(value.Closure.Omitted)), + "outputKind": value.Decision.OutputKind, + "reportId": reportKind, + "reportKind": reportKind, + "retainedContextRefIds": stringsValue(contextRefIDs(value.Closure.Retained)), + "retainedContextRefs": contextRefsValue(value.Closure.Retained), + "schemaVersion": json.Number("1"), + } + if value.Decision.OutputKind == "workflow_complete" { + return plan + } + plan["action"] = value.Decision.Action + plan["activeStageId"] = value.Decision.ActiveStageID + plan["checkpointState"] = value.Decision.CheckpointState + plan["prompt"] = value.Prompt + if value.Decision.SuccessorStateDelta != nil { + plan["successorStateDelta"] = successorValue(*value.Decision.SuccessorStateDelta) + } + return plan +} + +func successorValue(delta successorStateDelta) map[string]any { + result := map[string]any{ + "completedStageIds": stringsValue(delta.CompletedStageIDs), + "checkpoint": nil, + } + if delta.Checkpoint != nil { + result["checkpoint"] = map[string]any{"state": delta.Checkpoint.State} + } + return result +} + +func enforceJSONCap(value map[string]any) error { + encoded, err := stablejson.Marshal(value) + if err != nil { + return err + } + if len(encoded) > maxJSONBytes { + return reject("proofkit.workflow.json_output_limit", "JSON output exceeds the byte limit") + } + return nil +} diff --git a/internal/command/changeworkflowplan/state_test.go b/internal/command/changeworkflowplan/state_test.go new file mode 100644 index 0000000..18544fe --- /dev/null +++ b/internal/command/changeworkflowplan/state_test.go @@ -0,0 +1,146 @@ +package changeworkflowplan + +import ( + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" +) + +func TestWorkflowStatePredicates(t *testing.T) { + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.046999593271228979057731418772803126658652333387186990019354835268016126566224") + t.Run("checkpoint_requires_incomplete", func(t *testing.T) { + input := terminalInput() + input["checkpoint"] = map[string]any{"state": "not_started"} + requireReject(t, input) + }) + t.Run("complete_is_terminal", func(t *testing.T) { + plan := requireBuild(t, terminalInput()) + if plan["outputKind"] != "workflow_complete" { + t.Fatal("complete prefix is not terminal") + } + }) + t.Run("first_incomplete_active", func(t *testing.T) { + for index, stage := range stageTable { + plan := requireBuild(t, inputForStage(index, "not_started")) + if plan["activeStageId"] != stage.ID { + t.Fatalf("stage %d selected %v", index, plan["activeStageId"]) + } + } + }) + t.Run("incomplete_requires_checkpoint", func(t *testing.T) { + input := initialInput() + input["checkpoint"] = nil + requireReject(t, input) + }) + t.Run("prefix_only", func(t *testing.T) { + input := initialInput() + input["completedStageIds"] = []any{"design"} + requireReject(t, input) + }) + t.Run("stage_order", func(t *testing.T) { + want := []string{"architecture", "design", "implementation_plan", "implementation", "verification", "pull_request", "closeout"} + requireEqual(t, stageIDs(), want) + }) + t.Run("terminal_omits_active_fields", func(t *testing.T) { + plan := requireBuild(t, terminalInput()) + for _, key := range []string{"action", "activeStageId", "checkpointState", "prompt", "successorStateDelta"} { + if _, exists := plan[key]; exists { + t.Fatalf("terminal plan contains %s", key) + } + } + }) +} + +func TestWorkflowCheckpointPredicates(t *testing.T) { + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.057364505794091528940721933244472135772636722180139771802290163560593659249191") + t.Run("closed_variants", func(t *testing.T) { + for _, state := range []string{"not_started", "ready_for_review", "review_findings", "review_passed"} { + if _, err := Build(inputForStage(0, state)); err != nil { + t.Fatalf("closed variant %s rejected: %v", state, err) + } + } + input := initialInput() + input["checkpoint"] = map[string]any{"state": "pending"} + requireReject(t, input) + }) + t.Run("delta_exact_fields", func(t *testing.T) { + plan := requireBuild(t, inputForStage(0, "review_passed")) + delta := plan["successorStateDelta"].(map[string]any) + if len(delta) != 2 { + t.Fatalf("successor delta has %d fields", len(delta)) + } + }) + t.Run("delta_preserves_context", func(t *testing.T) { + prior := reviewInput("review_passed") + plan := requireBuild(t, prior) + merged, err := MergeSuccessor(prior, plan["successorStateDelta"].(map[string]any)) + if err != nil { + t.Fatal(err) + } + for _, key := range []string{"schemaVersion", "governingAuthorityRefId", "contextRefs", "requiredContextRefIds"} { + requireEqual(t, merged[key], prior[key]) + } + }) + t.Run("illegal_fields_rejected", func(t *testing.T) { + cases := []map[string]any{ + {"state": "not_started", "subjectRefId": "ctx.artifact"}, + {"state": "ready_for_review", "subjectRefId": "ctx.artifact"}, + {"state": "review_passed", "subjectRefId": "ctx.artifact", "subjectDigest": testDigest}, + } + for _, checkpointValue := range cases { + input := reviewInput("ready_for_review") + input["checkpoint"] = checkpointValue + requireReject(t, input) + } + }) + t.Run("merged_successor_admitted", func(t *testing.T) { + for index := range stageTable { + prior := inputForStage(index, "review_passed") + plan := requireBuild(t, prior) + merged, err := MergeSuccessor(prior, plan["successorStateDelta"].(map[string]any)) + if err != nil { + t.Fatalf("stage %d successor rejected: %v", index, err) + } + if _, err := Build(merged); err != nil { + t.Fatalf("stage %d merged successor rejected: %v", index, err) + } + } + }) + t.Run("review_passed_accepts", func(t *testing.T) { + for index := range stageTable { + plan := requireBuild(t, inputForStage(index, "review_passed")) + if plan["action"] != "accept_stage" { + t.Fatalf("stage %d action is %v", index, plan["action"]) + } + } + }) + t.Run("terminal_disjoint", func(t *testing.T) { + terminal := requireBuild(t, terminalInput()) + for index := range stageTable { + for _, state := range []string{"not_started", "ready_for_review", "review_findings", "review_passed"} { + active := requireBuild(t, inputForStage(index, state)) + if active["outputKind"] == terminal["outputKind"] { + t.Fatal("active row overlaps terminal row") + } + } + } + }) + t.Run("twenty_eight_actions", func(t *testing.T) { + if len(checkpointRelation) != 29 { + t.Fatalf("relation has %d rows", len(checkpointRelation)) + } + seen := 0 + for index := range stageTable { + for _, state := range []string{"not_started", "ready_for_review", "review_findings", "review_passed"} { + plan := requireBuild(t, inputForStage(index, state)) + if _, ok := plan["action"].(string); !ok { + t.Fatal("active row has no action") + } + seen++ + } + } + if seen != 28 { + t.Fatalf("got %d active rows", seen) + } + }) +} diff --git a/internal/command/changeworkflowplan/test_helpers_test.go b/internal/command/changeworkflowplan/test_helpers_test.go new file mode 100644 index 0000000..fcedec2 --- /dev/null +++ b/internal/command/changeworkflowplan/test_helpers_test.go @@ -0,0 +1,140 @@ +package changeworkflowplan + +import ( + "encoding/json" + "reflect" + "strings" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" +) + +const testDigest = "sha256:2222222222222222222222222222222222222222222222222222222222222222" + +func initialInput() map[string]any { + return map[string]any{ + "checkpoint": map[string]any{"state": "not_started"}, + "completedStageIds": []any{}, + "contextRefs": []any{}, + "governingAuthorityRefId": nil, + "requiredContextRefIds": []any{}, + "schemaVersion": json.Number("1"), + } +} + +func contextValue(id string, kind string, digest string, dependencies []string) map[string]any { + return map[string]any{ + "artifactPath": "evidence/" + id + ".json", + "dependencyRefIds": stringsValue(dependencies), + "refId": id, + "refKind": kind, + "subjectDigest": digest, + } +} + +func reviewInput(state string) map[string]any { + input := initialInput() + input["contextRefs"] = []any{ + contextValue("ctx.artifact", "artifact", testDigest, nil), + contextValue("ctx.authority", "authority", "sha256:1111111111111111111111111111111111111111111111111111111111111111", nil), + contextValue("ctx.finding", "finding", "sha256:3333333333333333333333333333333333333333333333333333333333333333", nil), + } + input["governingAuthorityRefId"] = "ctx.authority" + checkpointValue := map[string]any{"state": state, "subjectDigest": testDigest, "subjectRefId": "ctx.artifact"} + if state == "review_findings" || state == "review_passed" { + checkpointValue["assessmentSubjectDigest"] = testDigest + } + if state == "review_findings" { + checkpointValue["findingRefs"] = []any{"ctx.finding"} + } + input["checkpoint"] = checkpointValue + return input +} + +func inputForStage(stageIndex int, checkpointState string) map[string]any { + input := initialInput() + completed := make([]any, stageIndex) + for index := 0; index < stageIndex; index++ { + completed[index] = stageTable[index].ID + } + input["completedStageIds"] = completed + if checkpointState != "not_started" { + input = reviewInput(checkpointState) + input["completedStageIds"] = completed + } + return input +} + +func terminalInput() map[string]any { + input := initialInput() + input["completedStageIds"] = stringsValue(stageIDs()) + input["checkpoint"] = nil + return input +} + +func stageIDs() []string { + result := make([]string, len(stageTable)) + for index, stage := range stageTable { + result[index] = stage.ID + } + return result +} + +func requireBuild(t *testing.T, input map[string]any) map[string]any { + t.Helper() + result, err := Build(input) + if err != nil { + t.Fatalf("Build returned error: %v", err) + } + return result +} + +func requireReject(t *testing.T, input map[string]any) error { + t.Helper() + _, err := Build(input) + if err == nil { + t.Fatal("Build unexpectedly admitted invalid input") + } + return err +} + +func canonical(t *testing.T, value any) string { + t.Helper() + encoded, err := stablejson.MarshalLayout(value, stablejson.LayoutCompact) + if err != nil { + t.Fatalf("stable JSON: %v", err) + } + return string(encoded) +} + +func cloneMap(value map[string]any) map[string]any { + return cloneValue(value).(map[string]any) +} + +func cloneValue(value any) any { + switch typed := value.(type) { + case map[string]any: + result := make(map[string]any, len(typed)) + for key, child := range typed { + result[key] = cloneValue(child) + } + return result + case []any: + result := make([]any, len(typed)) + for index, child := range typed { + result[index] = cloneValue(child) + } + return result + default: + return typed + } +} + +func requireEqual(t *testing.T, got any, want any) { + t.Helper() + if !reflect.DeepEqual(got, want) { + t.Fatalf("mismatch\ngot: %s\nwant: %s", canonical(t, got), canonical(t, want)) + } +} + +func containsANSI(value string) bool { return strings.Contains(value, "\x1b[") } diff --git a/internal/command/changeworkflowplan/text.go b/internal/command/changeworkflowplan/text.go new file mode 100644 index 0000000..6dd46fb --- /dev/null +++ b/internal/command/changeworkflowplan/text.go @@ -0,0 +1,71 @@ +package changeworkflowplan + +import ( + "strconv" + "strings" +) + +func textProjection(value projection) []TextLine { + lines := []TextLine{ + {Label: "Change workflow plan"}, + {Label: "Output", Value: value.Decision.OutputKind}, + {Label: "Completed stages", Value: joinStrings(value.Input.CompletedStageIDs)}, + } + if value.Decision.OutputKind == "next_action" { + prompt := value.Prompt + lines = append(lines, + TextLine{Label: "Active stage", Value: value.Decision.ActiveStageID}, + TextLine{Label: "Action", Value: value.Decision.Action}, + TextLine{Label: "Checkpoint", Value: value.Decision.CheckpointState}, + TextLine{Label: "Owner or escalation", Value: prompt["ownerOrEscalationTarget"].(string)}, + TextLine{Label: "Stop condition", Value: prompt["stopCondition"].(string)}, + TextLine{Label: "Expected next checkpoint", Value: prompt["expectedNextCheckpoint"].(string)}, + ) + } else { + lines = append(lines, TextLine{Label: "Checkpoint", Value: "none"}) + } + lines = append(lines, + TextLine{Label: "Omitted context", Value: integerText(len(value.Closure.Omitted))}, + TextLine{Label: "Non-claim", Value: boundaryNonClaims[1]}, + ) + return lines +} + +// RenderText emits the canonical plain bytes from a structured projection. +func RenderText(lines []TextLine) (string, error) { + return renderText(cloneTextLines(lines)) +} + +func renderText(lines []TextLine) (string, error) { + if len(lines) > maxTextLines { + return "", reject("proofkit.workflow.text_line_limit", "text output exceeds the line limit") + } + plain := make([]string, len(lines)) + for index, line := range lines { + if line.Value == "" { + plain[index] = line.Label + continue + } + plain[index] = line.Label + ": " + line.Value + } + textValue := strings.Join(plain, "\n") + "\n" + if len(textValue) > maxTextBytes { + return "", reject("proofkit.workflow.text_output_limit", "text output exceeds the byte limit") + } + return textValue, nil +} + +func joinStrings(values []string) string { + if len(values) == 0 { + return "none" + } + return strings.Join(values, ", ") +} + +func integerText(value int) string { + return strconv.Itoa(value) +} + +func cloneTextLines(lines []TextLine) []TextLine { + return append([]TextLine{}, lines...) +} diff --git a/internal/command/changeworkflowplan/text_test.go b/internal/command/changeworkflowplan/text_test.go new file mode 100644 index 0000000..2f19297 --- /dev/null +++ b/internal/command/changeworkflowplan/text_test.go @@ -0,0 +1,66 @@ +package changeworkflowplan + +import ( + "strings" + "testing" +) + +func TestWorkflowTextPredicates(t *testing.T) { + t.Run("planner_text_bound", func(t *testing.T) { + result, err := Project(reviewInput("review_passed")) + if err != nil { + t.Fatal(err) + } + lines := strings.Split(strings.TrimSuffix(result.Text, "\n"), "\n") + if len(lines) > maxTextLines || len(result.Text) > maxTextBytes { + t.Fatalf("text exceeds bounds: %d lines, %d bytes", len(lines), len(result.Text)) + } + for _, coordinate := range []string{"architecture", "accept_stage", "review_passed", "ctx.authority", "successor_state_delta", "Omitted context: 1", "Non-claim:"} { + if !strings.Contains(result.Text, coordinate) { + t.Fatalf("text omits coordinate %q: %s", coordinate, result.Text) + } + } + }) +} + +func TestWorkflowTextProjectionParity(t *testing.T) { + input := reviewInput("review_passed") + result, err := Project(input) + if err != nil { + t.Fatal(err) + } + rendered, err := RenderText(result.TextLines) + if err != nil { + t.Fatal(err) + } + if rendered != result.Text { + t.Fatalf("structured projection drifted from canonical text\ngot: %q\nwant: %q", rendered, result.Text) + } + if result.Text != "Change workflow plan\nOutput: next_action\nCompleted stages: none\nActive stage: architecture\nAction: accept_stage\nCheckpoint: review_passed\nOwner or escalation: ctx.authority\nStop condition: Stop after constructing the merged immutable snapshot; do not infer execution, approval, merge, or release from stage acceptance.\nExpected next checkpoint: successor_state_delta\nOmitted context: 1\nNon-claim: Change workflow plans do not approve repository edits, merge, release, rollout, or production readiness.\n" { + t.Fatalf("canonical plain bytes changed: %q", result.Text) + } +} + +func TestWorkflowTextProjectionDefensiveCopy(t *testing.T) { + input := initialInput() + first, err := BuildTextProjection(input) + if err != nil { + t.Fatal(err) + } + first[0].Label = "mutated" + second, err := BuildTextProjection(input) + if err != nil { + t.Fatal(err) + } + if second[0].Label != "Change workflow plan" { + t.Fatal("text projection reused caller-mutated storage") + } + result, err := Project(input) + if err != nil { + t.Fatal(err) + } + result.TextLines[0].Label = "mutated again" + if result.Text != "Change workflow plan\nOutput: next_action\nCompleted stages: none\nActive stage: architecture\nAction: author\nCheckpoint: not_started\nOwner or escalation: missing_consuming_repository_semantic_owner\nStop condition: Stop after the artifact and digest are ready for independent review. Do not mutate the repository until the caller supplies the consuming-repository semantic owner and governing authority coordinates.\nExpected next checkpoint: ready_for_review\nOmitted context: 0\nNon-claim: Change workflow plans do not approve repository edits, merge, release, rollout, or production readiness.\n" { + t.Fatal("mutating structured projection changed canonical text bytes") + } +} diff --git a/internal/command/completioncriteria/completioncriteria_test.go b/internal/command/completioncriteria/completioncriteria_test.go index 861053d..e6039fa 100644 --- a/internal/command/completioncriteria/completioncriteria_test.go +++ b/internal/command/completioncriteria/completioncriteria_test.go @@ -8,7 +8,7 @@ import ( ) func TestBuildBlocksUnsatisfiedBlockingCriterion(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.013180397593748965161681129039250936414813610855632261268268126314868701130095") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.060093238757751048087414494533172778045450968319349399251124235403640311929783") record, exitCode, err := Build(validCompletionCriteriaInput("satisfied")) if err != nil { t.Fatalf("Build() error = %v", err) diff --git a/internal/command/conformanceprofile/conformanceprofile_test.go b/internal/command/conformanceprofile/conformanceprofile_test.go index 90f1c86..46a0304 100644 --- a/internal/command/conformanceprofile/conformanceprofile_test.go +++ b/internal/command/conformanceprofile/conformanceprofile_test.go @@ -11,7 +11,7 @@ import ( ) func TestBuildProfileResolvesRequiredSurfaceAndRejectsMissingSurface(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.018828880652012560180983294316467397502747610605037816609704340855648617186412") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.017521924525350696937249995164410482366782574304425862200993353758269414126866") result, err := BuildProfile(validConformanceProfileInput(), "local") if err != nil { t.Fatalf("BuildProfile() error=%v", err) @@ -134,7 +134,7 @@ func TestBuildProfileRejectsBindingForUndeclaredSurface(t *testing.T) { } func TestBuildVerificationRejectsDuplicateProfiles(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.049942093994206845167684609448858504077053804318310280433394779107021396059359") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.036639960952229987549870295720973534981562391579856063345276588288961992986796") input := validConformanceProfileInput() profiles := input["manifest"].(map[string]any)["profiles"].([]any) input["manifest"].(map[string]any)["profiles"] = append(profiles, profiles[0]) @@ -172,7 +172,7 @@ func TestBuildVerificationRejectsSecretLikeReportVisibleText(t *testing.T) { } func TestListReturnsSortedProfileIDsAndRejectsInvalidInput(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.051339077909994076367504988840992439039749956644468897314149305148259581754885") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.104729246445054007965678203600621319804834851985992223272065794196468497560959") input := validConformanceProfileInput() manifest := input["manifest"].(map[string]any) profile := manifest["profiles"].([]any)[0].(map[string]any) diff --git a/internal/command/customruleboundary/customruleboundary_test.go b/internal/command/customruleboundary/customruleboundary_test.go index 6b7cb97..dbdec2b 100644 --- a/internal/command/customruleboundary/customruleboundary_test.go +++ b/internal/command/customruleboundary/customruleboundary_test.go @@ -10,7 +10,7 @@ import ( ) func TestBuildAdmitsBoundedCustomRuleAndRejectsUnsafeEffects(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.025087881226507484990874681833161710116470838935948976618942966583527556385445") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.046497704421132552698102058363700040328502241838693800191968778531845096719907") record, exitCode, err := Build(validCustomRuleBoundaryInput()) if err != nil { t.Fatalf("Build() error=%v", err) diff --git a/internal/command/deploymentevidenceadmission/deployment_evidence_admission_test.go b/internal/command/deploymentevidenceadmission/deployment_evidence_admission_test.go index bfe0e1f..504c4e2 100644 --- a/internal/command/deploymentevidenceadmission/deployment_evidence_admission_test.go +++ b/internal/command/deploymentevidenceadmission/deployment_evidence_admission_test.go @@ -8,7 +8,7 @@ import ( ) func TestBuildAdmitsCandidateEvidenceAndRejectsUnpinnedImages(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.069418897389528177020444235635879439930452356193962278098825404413216668484505") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.094704258305539434923641925385994047558037254453527095265999848506667453675409") record, exitCode, err := Build(validDeploymentEvidenceInput()) if err != nil { t.Fatalf("Build() error=%v", err) diff --git a/internal/command/documentlifecycle/documentlifecycle_test.go b/internal/command/documentlifecycle/documentlifecycle_test.go index 307601d..1629958 100644 --- a/internal/command/documentlifecycle/documentlifecycle_test.go +++ b/internal/command/documentlifecycle/documentlifecycle_test.go @@ -10,7 +10,7 @@ import ( ) func TestBuildAdmitsCurrentDurableDocumentAndRejectsAuthorityDrift(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.002977780838547275321268818409231101675805953197916588621169638796768812025486") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.102216015058845344738900781252724796284847214615642762869434165828132457895378") record, exitCode, err := Build(validDocumentLifecycleInput()) if err != nil { t.Fatalf("Build() error=%v", err) diff --git a/internal/command/externalconsumer/externalconsumer_test.go b/internal/command/externalconsumer/externalconsumer_test.go index ff24bc9..4ff2350 100644 --- a/internal/command/externalconsumer/externalconsumer_test.go +++ b/internal/command/externalconsumer/externalconsumer_test.go @@ -11,7 +11,7 @@ import ( ) func TestBuildAdmitsExternalConsumerProofAndRejectsWorkspaceLock(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.030059518418876335628059921154117881105808166179626900465875767444982225149190") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.007511982496023718289618789322046194511126847157745362202397214109385639625930") input := validExternalConsumerInput(t) record, exitCode, err := Build(input) if err != nil { diff --git a/internal/command/gradualadoption/gradualadoption_test.go b/internal/command/gradualadoption/gradualadoption_test.go index 005808c..a5e35b2 100644 --- a/internal/command/gradualadoption/gradualadoption_test.go +++ b/internal/command/gradualadoption/gradualadoption_test.go @@ -37,7 +37,7 @@ func TestBuildReportsUnknownNestedGradualAdoptionFields(t *testing.T) { } func TestBuildRejectsRollbackShellControlCommand(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.035266010797425408239887120588026500100025591003049779646439503189706679453150") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.030698631272676220285142614909875256559617544703512571847158861160104610776777") input := validAdoptionInput() input["rollback"].(map[string]any)["disableCommand"] = "remove proofkit report && curl example.test" @@ -169,7 +169,7 @@ func assertMaterializedCallerCommandPrefix(t *testing.T, manifest map[string]any } func TestBootstrapRejectsUnknownRootAndNestedFields(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.111414333073228644561471369108062338215611265991082424538014863654404666653540") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.003017006002201806397275253275056144895434384250632878888086348214062413657003") cases := []struct { name string mutate func(map[string]any) diff --git a/internal/command/gradualadoption/guidance_test.go b/internal/command/gradualadoption/guidance_test.go index fc0b174..48c82b8 100644 --- a/internal/command/gradualadoption/guidance_test.go +++ b/internal/command/gradualadoption/guidance_test.go @@ -62,7 +62,7 @@ func TestGuidanceKeepsCommandOwnedNonClaims(t *testing.T) { } func TestGuidanceEnforcementFailsClosedForCandidateBoundaries(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.021210643414670754890972585342688421403224755441264357584078647180199403727299") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.044709354056905809499345300248047687983790301962467365024195419559832004542603") cases := []struct { name string mode string diff --git a/internal/command/impact/impact_test.go b/internal/command/impact/impact_test.go index 965e764..1213744 100644 --- a/internal/command/impact/impact_test.go +++ b/internal/command/impact/impact_test.go @@ -159,7 +159,7 @@ func TestBuildFailsWhenChangedRequirementHasNoCurrentBinding(t *testing.T) { } func TestBuildRoutesChangedRecordToObligationAndRejectsUnboundProofChange(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.004126818763873109094701885631231832544658829793890758986188782288995889572211") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.030107091047572360894804051055525132879054413918312412051082152497137096358221") input := validImpactInput() input["changedRequirementIds"] = []any{"REQ-PROOFKIT-001"} input["obligationCatalog"] = []any{validImpactObligation()} diff --git a/internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source.go b/internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source.go index 26bfac6..28a8718 100644 --- a/internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source.go +++ b/internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source.go @@ -65,7 +65,7 @@ func (bundle Bundle) JSONValue() map[string]any { return map[string]any{ "schemaVersion": 1, "artifactKind": "proofkit.json-report-cli-adapter-source", - "generatorId": "proofkit.json-report-cli-adapter-source.typescript.v1", + "generatorId": "proofkit.json-report-cli-adapter-source.typescript.v2", "language": bundle.Language, "format": bundle.Format, "sourceFileName": "proofkit-json-report-cli-adapter.ts", @@ -123,6 +123,19 @@ const defaultMaxBuffer = 64 * 1024 * 1024; const defaultMaxInputBytes = 32 * 1024 * 1024; const maxDiagnosticRunes = 512; const maxJsonNestingDepth = 512; +const unsafeScalarRanges: readonly (readonly [number, number, number])[] = [ + [0x000000, 0x00001f, 1], [0x00007f, 0x00009f, 1], + [0x0000ad, 0x000600, 1363], [0x000601, 0x000605, 1], + [0x00061c, 0x0006dd, 193], [0x00070f, 0x000890, 385], + [0x000891, 0x0008e2, 81], [0x00180e, 0x00200b, 2045], + [0x00200c, 0x00200f, 1], [0x002028, 0x002028, 1], + [0x002029, 0x002029, 1], [0x00202a, 0x00202e, 1], + [0x002060, 0x002064, 1], [0x002066, 0x00206f, 1], + [0x00feff, 0x00fff9, 250], [0x00fffa, 0x00fffb, 1], + [0x0110bd, 0x0110cd, 16], [0x013430, 0x01343f, 1], + [0x01bca0, 0x01bca3, 1], [0x01d173, 0x01d17a, 1], + [0x0e0001, 0x0e0020, 31], [0x0e0021, 0x0e007f, 1], +]; export type ProofkitJsonValue = | null @@ -211,9 +224,12 @@ function stableJsonValueAtDepth(value: unknown, depth: number, active: WeakSet maxJsonNestingDepth) { throw new Error("stable JSON value exceeds nesting depth limit"); } - if (value === null || typeof value === "boolean" || typeof value === "string") { + if (value === null || typeof value === "boolean") { return value; } + if (typeof value === "string") { + return assertProofkitScalarString(value); + } if (typeof value === "number") { if (!Number.isFinite(value)) { throw new Error("stable JSON value must not contain non-finite numbers"); @@ -264,7 +280,7 @@ function stableJsonValueAtDepth(value: unknown, depth: number, active: WeakSet typeof key === "symbol")) { throw new Error("stable JSON objects must not contain symbol keys"); } - for (const key of (ownKeys as string[]).sort()) { + for (const key of (ownKeys as string[]).map(assertProofkitScalarString).sort(compareProofkitScalarStrings)) { if (key.length === 0 || key.includes("\0")) { throw new Error("stable JSON object keys must be non-empty text"); } @@ -286,7 +302,114 @@ function stableJsonValueAtDepth(value: unknown, depth: number, active: WeakSet (pretty ? " ".repeat(depth + 1) : "") + writeProofkitStableJson(entry, depth + 1, layout)); + return "[" + prefix + entries.join(separator) + suffix + "]"; + } + const record = value as Readonly>; + const keys = Object.keys(record).sort(compareProofkitScalarStrings); + if (keys.length === 0) return "{}"; + const separator = pretty ? ",\n" : ","; + const prefix = pretty ? "\n" : ""; + const suffix = pretty ? "\n" + " ".repeat(depth) : ""; + const entries = keys.map((key) => + (pretty ? " ".repeat(depth + 1) : "") + quoteProofkitScalarString(key) + (pretty ? ": " : ":") + writeProofkitStableJson(record[key] as ProofkitJsonValue, depth + 1, layout) + ); + return "{" + prefix + entries.join(separator) + suffix + "}"; +} + +function assertProofkitScalarString(value: string): string { + if (!isProofkitScalarString(value)) { + throw new Error("stable JSON strings must contain only Unicode scalar values"); + } + return value; +} + +function isProofkitScalarString(value: string): boolean { + for (let index = 0; index < value.length; index += 1) { + const unit = value.charCodeAt(index); + if (unit >= 0xd800 && unit <= 0xdbff) { + const low = value.charCodeAt(index + 1); + if (!(low >= 0xdc00 && low <= 0xdfff)) { + return false; + } + index += 1; + } else if (unit >= 0xdc00 && unit <= 0xdfff) { + return false; + } + } + return true; +} + +function compareProofkitScalarStrings(left: string, right: string): number { + const leftScalars = Array.from(left, (character) => character.codePointAt(0) as number); + const rightScalars = Array.from(right, (character) => character.codePointAt(0) as number); + const sharedLength = Math.min(leftScalars.length, rightScalars.length); + for (let index = 0; index < sharedLength; index += 1) { + if (leftScalars[index] !== rightScalars[index]) return (leftScalars[index] as number) - (rightScalars[index] as number); + } + return leftScalars.length - rightScalars.length; +} + +function isProofkitUnsafeScalar(value: number): boolean { + let low = 0; + let high = unsafeScalarRanges.length; + while (low < high) { + const middle = Math.floor((low + high) / 2); + if ((unsafeScalarRanges[middle] as readonly [number, number, number])[1] < value) low = middle + 1; + else high = middle; + } + if (low === unsafeScalarRanges.length) return false; + const [start, end, step] = unsafeScalarRanges[low] as readonly [number, number, number]; + return value >= start && value <= end && (value - start) % step === 0; +} + +function quoteProofkitScalarString(value: string): string { + assertProofkitScalarString(value); + let output = "\""; + for (const character of value) { + const scalar = character.codePointAt(0) as number; + switch (scalar) { + case 0x22: output += "\\\""; break; + case 0x5c: output += "\\\\"; break; + case 0x08: output += "\\b"; break; + case 0x09: output += "\\t"; break; + case 0x0a: output += "\\n"; break; + case 0x0c: output += "\\f"; break; + case 0x0d: output += "\\r"; break; + default: output += isProofkitUnsafeScalar(scalar) ? proofkitUnicodeEscape(scalar) : character; + } + } + return output + "\""; +} + +function proofkitUnicodeEscape(value: number): string { + if (value <= 0xffff) return "\\u" + value.toString(16).padStart(4, "0"); + const adjusted = value - 0x10000; + const high = 0xd800 + (adjusted >> 10); + const low = 0xdc00 + (adjusted & 0x3ff); + return "\\u" + high.toString(16) + "\\u" + low.toString(16); +} + +function decodeProofkitUTF8(bytes: Uint8Array, fixedLabel: string): string { + try { + return new TextDecoder("utf-8", {fatal: true}).decode(bytes); + } catch { + throw new Error(fixedLabel + " is not valid UTF-8"); + } } export function formatProofkitCliError(error: unknown): string { @@ -294,7 +417,10 @@ export function formatProofkitCliError(error: unknown): string { } function redactDiagnosticValue(value: string): string { - return truncateDiagnosticValue(redactControlRunes(redactSecretLikeText(value))); + if (!isProofkitScalarString(value) || containsProofkitUnsafeScalar(value) || containsProofkitSecretLikeValue(value)) { + return ""; + } + return truncateDiagnosticValue(value); } function truncateDiagnosticValue(value: string): string { @@ -305,22 +431,27 @@ function truncateDiagnosticValue(value: string): string { return runes.slice(0, maxDiagnosticRunes).join("") + "..."; } -function redactControlRunes(value: string): string { - let output = ""; - let redacting = false; +function containsProofkitUnsafeScalar(value: string): boolean { for (const character of value) { - const codePoint = character.codePointAt(0) ?? 0; - if (codePoint < 0x20 || codePoint === 0x7f) { - if (!redacting) { - output += ""; - redacting = true; - } - continue; - } - output += character; - redacting = false; + if (isProofkitUnsafeScalar(character.codePointAt(0) as number)) return true; } - return output; + return false; +} + +function containsProofkitSecretLikeValue(value: string): boolean { + return [ + /authorization\s*:\s*[^\r\n]+/iu, + /bearer\s+[A-Za-z0-9._~+/=-]{8,}/iu, + /(?:access[-_]?token|api[-_]?key|pass(?:word|wd)|secret|token)\s*[=:]\s*\S+/iu, + /github_pat_[A-Za-z0-9_]+/iu, + /gh[pousr]_[A-Za-z0-9_]+/iu, + /sk-(?:proj-)?[A-Za-z0-9_-]{10,}/iu, + /xox[abprs]-[A-Za-z0-9-]+/iu, + /glpat-[A-Za-z0-9_-]+/iu, + /[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/iu, + /-----BEGIN [A-Z ]*PRIVATE KEY-----/iu, + /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/u, + ].some((pattern) => pattern.test(value)); } export function parseProofkitJsonReportCli( @@ -425,7 +556,7 @@ function readProofkitBoundedTextFile(filePath: string, options: ProofkitReportIn } chunks.push(Buffer.from(chunk.subarray(0, bytesRead))); } - return Buffer.concat(chunks, total).toString("utf8"); + return decodeProofkitUTF8(Buffer.concat(chunks, total), "input file"); } finally { closeSync(fd); } @@ -549,16 +680,20 @@ function runProofkitCommand(command: string, input: unknown, args: readonly stri } const processArgs = options.jsonLayout === undefined ? [] : ["--json-layout", options.jsonLayout]; const childArgs = options.inputMode === "none" ? [...processArgs, command, ...prepared.args] : [...processArgs, command, "--input", "-", ...prepared.args]; - const child = spawnSync(options.binaryPath, childArgs, { + const childResult = spawnSync(options.binaryPath, childArgs, { cwd: options.cwd, - encoding: "utf8", env: options.env, input: options.inputMode === "none" ? undefined : proofkitStableJsonString(input), maxBuffer: options.maxBuffer ?? defaultMaxBuffer, }); - if (child.error !== undefined) { - throw new Error(formatProofkitCliError(child.error)); + if (childResult.error !== undefined) { + throw new Error(formatProofkitCliError(childResult.error)); } + const child: ProofkitProcessResult = { + status: childResult.status, + stdout: decodeProofkitUTF8(childResult.stdout, "Proofkit stdout"), + stderr: decodeProofkitUTF8(childResult.stderr, "Proofkit stderr"), + }; return {child, outputFile: prepared.outputFile}; } @@ -786,7 +921,7 @@ class ProofkitJsonScanner { const char = this.source[this.index] ?? ""; this.index += 1; if (char === "\"") { - return output; + return assertProofkitScalarString(output); } if (char === "\\") { output += this.parseEscape(); @@ -987,22 +1122,14 @@ function admitRunOptions(options: ProofkitCommandRunOptions): void { } function formatReadFailure(filePath: string, error: unknown, options: ProofkitReportInputReadOptions): string { - const message = redactDiagnosticValue(filePath) + ": " + formatProofkitCliError(error); - return redactDiagnosticValue(options.failureMessagePrefix === undefined ? message : options.failureMessagePrefix + ": " + message); -} - -function redactSecretLikeText(value: string): string { - return value - .replaceAll(/authorization\s*:\s*[^\r\n]+/giu, "authorization: [REDACTED]") - .replaceAll(/bearer\s+[A-Za-z0-9._~+/=-]{8,}/giu, "Bearer [REDACTED]") - .replaceAll(/(?:access[-_]?token|api[-_]?key|pass(?:word|wd)|secret|token)\s*[=:]\s*\S+/giu, "[REDACTED]") - .replaceAll(/github_pat_[A-Za-z0-9_]+/giu, "[REDACTED]") - .replaceAll(/gh[pousr]_[A-Za-z0-9_]+/giu, "[REDACTED]") - .replaceAll(/sk-(?:proj-)?[A-Za-z0-9_-]{10,}/giu, "[REDACTED]") - .replaceAll(/xox[abprs]-[A-Za-z0-9-]+/giu, "[REDACTED]") - .replaceAll(/glpat-[A-Za-z0-9_-]+/giu, "[REDACTED]") - .replaceAll(/[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/giu, "[REDACTED]") - .replaceAll(/-----BEGIN [A-Z ]*PRIVATE KEY-----/giu, "[REDACTED]") - .replaceAll(/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/giu, "[REDACTED]"); + const fixed = ""; + const pathLabel = redactDiagnosticValue(filePath); + const errorLabel = formatProofkitCliError(error); + if (pathLabel === fixed || errorLabel === fixed) return fixed; + const message = pathLabel + ": " + errorLabel; + if (options.failureMessagePrefix === undefined) return redactDiagnosticValue(message); + const prefix = redactDiagnosticValue(options.failureMessagePrefix); + if (prefix === fixed) return fixed; + return redactDiagnosticValue(prefix + ": " + message); } ` diff --git a/internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source_test.go b/internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source_test.go index 205c46c..f144fdb 100644 --- a/internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source_test.go +++ b/internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source_test.go @@ -15,7 +15,7 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" ) -const expectedTypeScriptSourceSha256 = "sha256:dc1478adc582d4bdfeab52ca1e3facaac23cfe9a78e9351620e11edae9cd9ba0" +const expectedTypeScriptSourceSha256 = "sha256:c1c0c4608b3dee6520c4de90a24adcad342f0aeeab02fb8face911e55d00f064" func TestBuildEmitsDeterministicTypeScriptSourceBundle(t *testing.T) { if !slices.IsSorted(exportedSymbols) { @@ -163,7 +163,7 @@ func TestGeneratedSourceAvoidsGenericIndexedAssignmentDrift(t *testing.T) { } func TestGeneratedTypeScriptAdapterExecutesCoreSemantics(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.053185673671850315997459957102069176684420983647605639631428960314206272025809") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.091236415555979919321510491623647940156692204483402355271747696953075393053294") nodePath, err := exec.LookPath("node") if err != nil { t.Fatalf("node is required to prove generated TypeScript semantics: %v", err) @@ -265,6 +265,10 @@ process.stdin.on("end", () => { process.stdout.write(JSON.stringify({schemaVersion: 1, state: "passed", inputless: true}) + "\n"); process.exit(0); } + if (command === "json-invalid-utf8") { + process.stdout.write(Buffer.from([0x7b, 0x22, 0x78, 0x22, 0x3a, 0xff, 0x7d])); + process.exit(0); + } process.stderr.write("unknown command"); process.exit(2); }); @@ -320,6 +324,83 @@ assert.equal(helpText, "help text"); assert.equal(proofkitStableJsonString({z: 1, a: true}), "{\n \"a\": true,\n \"z\": 1\n}\n"); assert.equal(proofkitStableJsonString({z: 1, a: true}, "compact"), "{\"a\":true,\"z\":1}\n"); +assert.equal( + proofkitStableJsonString({value: "\u007f\u0085\u200b\u2028\u2029\u{e0001}", "\u{10000}": "supplementary", "\ue000": "bmp"}, "compact"), + "{\"value\":\"\\u007f\\u0085\\u200b\\u2028\\u2029\\udb40\\udc01\",\"\ue000\":\"bmp\",\"\u{10000}\":\"supplementary\"}\n", +); +for (const [value, expected] of [ + ["alpha", "{\"value\":\"alpha\"}\n"], + ["<&>", "{\"value\":\"<&>\"}\n"], + ["\u{1f600}", "{\"value\":\"\u{1f600}\"}\n"], + ["e\u0301", "{\"value\":\"e\u0301\"}\n"], + ["\b\t\n\f\r", "{\"value\":\"\\b\\t\\n\\f\\r\"}\n"], + ["\0", "{\"value\":\"\\u0000\"}\n"], + ["\u007f", "{\"value\":\"\\u007f\"}\n"], + ["\u0085", "{\"value\":\"\\u0085\"}\n"], + ["\u200b", "{\"value\":\"\\u200b\"}\n"], + ["\u{e0001}", "{\"value\":\"\\udb40\\udc01\"}\n"], + ["\u2028", "{\"value\":\"\\u2028\"}\n"], + ["\u2029", "{\"value\":\"\\u2029\"}\n"], +]) { + const encoded = proofkitStableJsonString({value}, "compact"); + assert.equal(encoded, expected); + assert.equal(parseProofkitJsonStrict(encoded).value, value); +} +const unsafeScalarRangesForTest = [ + [0x000000, 0x00001f, 1], [0x00007f, 0x00009f, 1], [0x0000ad, 0x000600, 1363], + [0x000601, 0x000605, 1], [0x00061c, 0x0006dd, 193], [0x00070f, 0x000890, 385], + [0x000891, 0x0008e2, 81], [0x00180e, 0x00200b, 2045], [0x00200c, 0x00200f, 1], + [0x002028, 0x002028, 1], [0x002029, 0x002029, 1], [0x00202a, 0x00202e, 1], + [0x002060, 0x002064, 1], [0x002066, 0x00206f, 1], [0x00feff, 0x00fff9, 250], + [0x00fffa, 0x00fffb, 1], [0x0110bd, 0x0110cd, 16], [0x013430, 0x01343f, 1], + [0x01bca0, 0x01bca3, 1], [0x01d173, 0x01d17a, 1], [0x0e0001, 0x0e0020, 31], + [0x0e0021, 0x0e007f, 1], +]; +const unsafeScalarsForTest = new Set(); +for (const [start, end, step] of unsafeScalarRangesForTest) { + for (let value = start; value <= end; value += step) unsafeScalarsForTest.add(value); +} +function expectedScalarEncodingForTest(value) { + if (value === 0x08) return "\\b"; + if (value === 0x09) return "\\t"; + if (value === 0x0a) return "\\n"; + if (value === 0x0c) return "\\f"; + if (value === 0x0d) return "\\r"; + if (unsafeScalarsForTest.has(value)) { + if (value <= 0xffff) return "\\u" + value.toString(16).padStart(4, "0"); + const adjusted = value - 0x10000; + return "\\u" + (0xd800 + (adjusted >> 10)).toString(16) + "\\u" + (0xdc00 + (adjusted & 0x3ff)).toString(16); + } + if (value === 0x22) return "\\\""; + if (value === 0x5c) return "\\\\"; + return String.fromCodePoint(value); +} +for (let value = 0; value <= 0x10ffff; value += 1) { + if (value >= 0xd800 && value <= 0xdfff) continue; + const scalar = String.fromCodePoint(value); + assert.equal( + proofkitStableJsonString({value: scalar}, "compact"), + "{\"value\":\"" + expectedScalarEncodingForTest(value) + "\"}\n", + "U+" + value.toString(16).padStart(4, "0"), + ); +} +for (const invalidScalar of ["\ud800", "\udc00", "\ud800a"]) { + assert.throws(() => proofkitStableJsonValue({value: invalidScalar}), /Unicode scalar values/); + assert.throws(() => proofkitStableJsonValue({[invalidScalar]: "key"}), /Unicode scalar values/); +} +for (let unit = 0xd800; unit <= 0xdfff; unit += 1) { + const invalidScalar = String.fromCharCode(unit); + assert.throws(() => proofkitStableJsonValue({value: invalidScalar}), /Unicode scalar values/); + assert.throws(() => proofkitStableJsonValue({[invalidScalar]: "key"}), /Unicode scalar values/); +} +for (let high = 0xd800; high <= 0xdbff; high += 1) { + for (let low = 0xdc00; low <= 0xdfff; low += 1) { + const scalar = String.fromCharCode(high, low); + proofkitStableJsonValue({value: scalar}); + proofkitStableJsonValue({[scalar]: "key"}); + } +} +assert.throws(() => parseProofkitJsonStrict('{"value":"\\ud800"}'), /Unicode scalar values/); const prototypeKey = JSON.parse('{"__proto__":{"polluted":true}}'); assert.equal(proofkitStableJsonString(prototypeKey), '{\n "__proto__": {\n "polluted": true\n }\n}\n'); assert.equal(({}).polluted, undefined); @@ -415,18 +496,18 @@ assert.throws( ); writeFileSync("oversize.json", "{\"ok\":true}"); assert.throws(() => readProofkitJsonReportInput("oversize.json", {maxInputBytes: 4}), /exceeds maxInputBytes/); +writeFileSync("malformed-utf8.json", Buffer.from([0x7b, 0x22, 0x78, 0x22, 0x3a, 0xff, 0x7d])); +assert.throws( + () => readProofkitJsonReportInput("malformed-utf8.json"), + (error) => error instanceof Error && /input file is not valid UTF-8/.test(error.message) && !error.message.includes("ff"), +); assert.throws( () => readProofkitJsonReportInput("missing\napi_key=abc123456789.json"), - (error) => - error instanceof Error && - //.test(error.message) && - /\[REDACTED\]/.test(error.message) && - !/abc123456789/.test(error.message) && - !/\n/.test(error.message), + (error) => error instanceof Error && error.message === "", ); assert.throws( () => readProofkitJsonReportInput("missing-prefix.json", {failureMessagePrefix: "Bearer prefixsecret123456789"}), - (error) => error instanceof Error && /Bearer \[REDACTED\]/.test(error.message) && !/prefixsecret123456789/.test(error.message), + (error) => error instanceof Error && error.message === "", ); const pass = runProofkitJsonCommand("json-pass", {z: 1, a: true}, [], {binaryPath: fakeProofkitPath, cwd: repositoryRoot}); @@ -447,11 +528,15 @@ assert.throws( ); assert.throws( () => runProofkitJsonCommand("json-secret-process-fail", {}, [], {binaryPath: fakeProofkitPath, cwd: repositoryRoot}), - /\[REDACTED\]/, + (error) => error instanceof Error && error.message === "", ); assert.throws( () => runProofkitJsonCommand("json-openai-secret-process-fail", {}, [], {binaryPath: fakeProofkitPath, cwd: repositoryRoot}), - /\[REDACTED\]/, + (error) => error instanceof Error && error.message === "", +); +assert.throws( + () => runProofkitJsonCommand("json-invalid-utf8", {}, [], {binaryPath: fakeProofkitPath, cwd: repositoryRoot}), + (error) => error instanceof Error && error.message === "Proofkit stdout is not valid UTF-8", ); const outputPass = runProofkitJsonCommand("requirement-spec-tree-view", {ok: true}, ["--output", "proofkit-output.json"], {binaryPath: fakeProofkitPath, cwd: repositoryRoot}); assert.equal(outputPass.status, 0); @@ -545,30 +630,34 @@ let errorText = ""; process.exitCode = undefined; runProofkitJsonReportCliMain({ argv: [], - run: () => { throw new Error("ghp_123456789012345678901234567890123456"); }, - writeError: (value) => { errorText += value; }, + run: () => { throw new Error("ghp_123456789012345678901234567890123456"); }, + writeError: (value) => { errorText += value; }, }); assert.equal(process.exitCode, 1); - assert.match(errorText, /\[REDACTED\]/); + assert.equal(errorText, "\n"); process.exitCode = 0; - assert.equal(formatProofkitCliError("Bearer abcdefghijklmnopqrstuvwxyz"), "Bearer [REDACTED]"); - assert.equal(formatProofkitCliError("Bearer abcdefgh"), "Bearer [REDACTED]"); - assert.equal(formatProofkitCliError("api_key=abc123456789"), "[REDACTED]"); - assert.equal(formatProofkitCliError("ghp_short"), "[REDACTED]"); + const fixedDiagnostic = ""; + assert.equal(formatProofkitCliError("Bearer abcdefghijklmnopqrstuvwxyz"), fixedDiagnostic); + assert.equal(formatProofkitCliError("Bearer abcdefgh"), fixedDiagnostic); + assert.equal(formatProofkitCliError("api_key=abc123456789"), fixedDiagnostic); + assert.equal(formatProofkitCliError("ghp_short"), fixedDiagnostic); const authorizationHeader = formatProofkitCliError("request failed: Authorization: Basic YWxpY2U6c2VjcmV0"); - assert.match(authorizationHeader, /\[REDACTED\]/); - assert.doesNotMatch(authorizationHeader, /YWxpY2U6c2VjcmV0|Basic/); + assert.equal(authorizationHeader, fixedDiagnostic); const controlRunes = formatProofkitCliError("line one\nline two\t\u007fend"); - assert.equal(controlRunes, "line oneline twoend"); + assert.equal(controlRunes, fixedDiagnostic); const truncated = formatProofkitCliError("x".repeat(520)); assert.equal(truncated.length, 512 + "...".length); assert.match(truncated, /\.\.\.$/); for (const fixture of redactionFixtures) { - const redacted = formatProofkitCliError(fixture.input); - for (const needle of fixture.sensitiveNeedles) { - assert.equal(redacted.includes(needle), false, fixture.name + " leaked " + needle + " via " + redacted); + assert.equal(formatProofkitCliError("prefix " + fixture.input + " suffix"), fixedDiagnostic, fixture.name + " contiguous"); + const wrapped = "prefix " + fixture.input + " suffix"; + for (const separator of ["\0", "\u007f", "\u0085", "\u200b", "\u{e0001}", "\u2028", "\u2029"]) { + for (const offset of [Math.floor(wrapped.length / 3), Math.floor(wrapped.length / 2), Math.floor(wrapped.length * 2 / 3)]) { + assert.equal(formatProofkitCliError(wrapped.slice(0, offset) + separator + wrapped.slice(offset)), fixedDiagnostic, fixture.name + " split"); + } } } + assert.equal(formatProofkitCliError("\ud800"), fixedDiagnostic); console.log("generated adapter semantics ok"); ` diff --git a/internal/command/migrationparityadmission/migrationparityadmission_test.go b/internal/command/migrationparityadmission/migrationparityadmission_test.go index eec86a5..1dc6b40 100644 --- a/internal/command/migrationparityadmission/migrationparityadmission_test.go +++ b/internal/command/migrationparityadmission/migrationparityadmission_test.go @@ -14,7 +14,7 @@ const testDigest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa const otherDigest = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" func TestBuildAdmitsCallerDeclaredMatchAndRejectsDigestDrift(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.031283583526771024463426071371708324157925862677375589108846795108748604685801") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.060127479049105070527667173413539037917647131744551077145203058285425049963485") input := validMigrationParityInput() record, exitCode, err := Build(input) if err != nil { diff --git a/internal/command/migrationplan/migrationplan_test.go b/internal/command/migrationplan/migrationplan_test.go index eab59bb..007ec42 100644 --- a/internal/command/migrationplan/migrationplan_test.go +++ b/internal/command/migrationplan/migrationplan_test.go @@ -8,7 +8,7 @@ import ( ) func TestSortedFollowUpCommandsRejectsShellControlTokens(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.068167811965828868524495721027219601964718026488781175287330661603079334133111") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.111566014894964379159287917127840775005896615125222459864418913754403534133338") _, err := sortedFollowUpCommands([]any{map[string]any{ "command": "go test ./... && curl example.test", "commandId": "proofkit.followup", diff --git a/internal/command/nativeevidenceguidance/dependency_test.go b/internal/command/nativeevidenceguidance/dependency_test.go new file mode 100644 index 0000000..2006a2e --- /dev/null +++ b/internal/command/nativeevidenceguidance/dependency_test.go @@ -0,0 +1,58 @@ +package nativeevidenceguidance_test + +import ( + "go/parser" + "go/token" + "os" + "strings" + "testing" +) + +func assertGuidanceProductionImports(t *testing.T) { + t.Helper() + entries, err := os.ReadDir(".") + if err != nil { + t.Fatalf("read package directory: %v", err) + } + productionFiles := 0 + for _, entry := range entries { + path := entry.Name() + if entry.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { + continue + } + productionFiles++ + source, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + parsed, err := parser.ParseFile(token.NewFileSet(), path, source, parser.ImportsOnly) + if err != nil { + t.Fatalf("parse %s: %v", path, err) + } + for _, imported := range parsed.Imports { + if isAmbientImport(imported.Path.Value) { + t.Fatalf("production import %s in %s is ambient", imported.Path.Value, path) + } + } + } + if productionFiles == 0 { + t.Fatal("native evidence guidance has no production files") + } +} + +func TestGuidanceNoAmbientDependencyPredicates(t *testing.T) { + t.Run("no_ambient_dependencies", func(t *testing.T) { + assertGuidanceProductionImports(t) + }) +} + +func isAmbientImport(quotedPath string) bool { + for _, forbidden := range []string{ + "\"os\"", "\"os/exec\"", "\"path/filepath\"", "\"io/fs\"", "\"net\"", "\"net/http\"", "\"time\"", "\"math/rand\"", "\"crypto/rand\"", "\"runtime\"", "\"syscall\"", + } { + if quotedPath == forbidden { + return true + } + } + return false +} diff --git a/internal/command/nativeevidenceguidance/guidance.go b/internal/command/nativeevidenceguidance/guidance.go new file mode 100644 index 0000000..571e48d --- /dev/null +++ b/internal/command/nativeevidenceguidance/guidance.go @@ -0,0 +1,163 @@ +// Package nativeevidenceguidance owns the repository-neutral evidence template. +package nativeevidenceguidance + +import ( + "errors" + "strings" +) + +const ( + SchemaVersion = 1 + GuidanceID = "proofkit.native-evidence-guidance.v1" + + SlotCount = 22 + MaximumTextBytes = 16 * 1024 + MaximumTextLines = 48 +) + +var errInvalidGuidanceTable = errors.New("native evidence guidance table is invalid") + +// Slot is one repository-owned decision required for executable native evidence. +type Slot struct { + Order int `json:"order"` + SlotID string `json:"slotId"` + Question string `json:"question"` + RequiredConsumerDecision string `json:"requiredConsumerDecision"` + CompletionCriterion string `json:"completionCriterion"` +} + +// Guidance is an immutable projection of the command-owned table. +type Guidance struct { + SchemaVersion int `json:"schemaVersion"` + GuidanceID string `json:"guidanceId"` + Slots []Slot `json:"slots"` + NonClaims []string `json:"nonClaims"` +} + +// JSONValue returns a fresh stablejson-compatible projection. +func (guidance Guidance) JSONValue() map[string]any { + slots := make([]any, 0, len(guidance.Slots)) + for _, slot := range guidance.Slots { + slots = append(slots, map[string]any{ + "completionCriterion": slot.CompletionCriterion, + "order": slot.Order, + "question": slot.Question, + "requiredConsumerDecision": slot.RequiredConsumerDecision, + "slotId": slot.SlotID, + }) + } + nonClaims := make([]any, len(guidance.NonClaims)) + for index, value := range guidance.NonClaims { + nonClaims[index] = value + } + return map[string]any{ + "guidanceId": guidance.GuidanceID, + "nonClaims": nonClaims, + "schemaVersion": guidance.SchemaVersion, + "slots": slots, + } +} + +// TextLine separates a command-owned label from its semantic plain-text value. +// Presentation adapters may style Label, but must preserve Value verbatim. +type TextLine struct { + Label string + Value string +} + +// guidanceTable is the sole semantic owner of native-evidence guidance. +var guidanceTable = [...]Slot{ + {Order: 1, SlotID: "semantic_owner", Question: "Who owns the invariant and its change authority?", RequiredConsumerDecision: "Select exactly one stable semantic owner and one change authority.", CompletionCriterion: "Both stable IDs are recorded and no second owner can independently change the same meaning."}, + {Order: 2, SlotID: "subject_identity_currentness", Question: "Which exact subject is proved and when is it current?", RequiredConsumerDecision: "Select the subject identity, digest algorithm, and freshness rule.", CompletionCriterion: "The exact subject digest is bound and the freshness rule deterministically rejects a stale subject."}, + {Order: 3, SlotID: "execution_root_source_snapshot", Question: "Which execution root and immutable source snapshot are admitted?", RequiredConsumerDecision: "Select one root and one immutable revision or complete source-digest set.", CompletionCriterion: "The witness binds the admitted root and revision or digests and rejects execution over a different snapshot."}, + {Order: 4, SlotID: "exact_argv", Question: "Which process invocation is admitted?", RequiredConsumerDecision: "Select one argv array without shell interpolation.", CompletionCriterion: "The executed argv equals the admitted array element-for-element and no shell parses it."}, + {Order: 5, SlotID: "stdin_input_bytes", Question: "Which stdin and input bytes are admitted?", RequiredConsumerDecision: "Select exact bytes or digests and byte bounds for every input channel.", CompletionCriterion: "The witness binds each input channel and rejects missing, substituted, or over-bound bytes."}, + {Order: 6, SlotID: "toolchain_environment", Question: "Which toolchain and deterministic environment are fixed?", RequiredConsumerDecision: "Select exact tool versions, platform, locale, timezone, and dependency identities.", CompletionCriterion: "The witness records and validates every selected identity before execution."}, + {Order: 7, SlotID: "positive_control", Question: "Which known-good subject proves the witness can succeed?", RequiredConsumerDecision: "Select one independently justified positive fixture and expected oracle result.", CompletionCriterion: "The fixture executes and produces the exact passing oracle result."}, + {Order: 8, SlotID: "near_miss_falsifier", Question: "Which plausible wrong subject must fail?", RequiredConsumerDecision: "Select one independent near-miss mutation and expected failure rule.", CompletionCriterion: "The mutated subject executes and fails under the selected stable rule while the positive control still passes."}, + {Order: 9, SlotID: "oracle_rule", Question: "Which executable oracle decides the outcome?", RequiredConsumerDecision: "Select one stable rule ID and deterministic observation mapping.", CompletionCriterion: "Every admitted observation maps to exactly one verdict and the near-miss activates the intended rule."}, + {Order: 10, SlotID: "input_work_bounds", Question: "Which cardinality and work bounds prevent excess work?", RequiredConsumerDecision: "Select numeric bounds and the point at which they dominate semantic processing.", CompletionCriterion: "Boundary fixtures pass, one-over fixtures fail before expensive per-item work, and no partial output is written."}, + {Order: 11, SlotID: "environment_network_policy", Question: "Which environment variables and network accesses are allowed?", RequiredConsumerDecision: "Select explicit allow and deny inventories.", CompletionCriterion: "Allowed accesses are observed, one denied environment read and one denied network access fail under stable rules, and undeclared access is denied."}, + {Order: 12, SlotID: "resource_bounds", Question: "Which CPU, memory, disk, file-descriptor, and PID limits apply?", RequiredConsumerDecision: "Select numeric limits and cleanup obligations.", CompletionCriterion: "Boundary use succeeds, one-over use fails deterministically, and allocated resources are released."}, + {Order: 13, SlotID: "output_bounds", Question: "Which stdout and stderr limits apply?", RequiredConsumerDecision: "Select exact byte caps and overflow behavior.", CompletionCriterion: "Boundary output succeeds and one-over output fails before any partial write."}, + {Order: 14, SlotID: "process_lifecycle", Question: "How are timeout and child processes contained?", RequiredConsumerDecision: "Select timeout, process-group termination, and detached-child policy.", CompletionCriterion: "Timeout terminates the process group, no detached child survives, and cleanup completion is observed before success."}, + {Order: 15, SlotID: "exit_mapping", Question: "How do native exit classes map to proof states?", RequiredConsumerDecision: "Select an exhaustive disjoint exit-class mapping.", CompletionCriterion: "Every native exit class maps to exactly one declared state and an unknown class fails closed."}, + {Order: 16, SlotID: "nondisclosure", Question: "Which values must never reach visible sinks?", RequiredConsumerDecision: "Select the secret, control, and sentinel corpus and all report-visible sinks.", CompletionCriterion: "No corpus value or fragment appears in stdout, stderr, reports, receipts, logs, or generated records."}, + {Order: 17, SlotID: "cleanup_crash_inventory", Question: "Which outputs may remain after success, failure, or crash?", RequiredConsumerDecision: "Select exact terminal inventories plus rollback or recovery rules.", CompletionCriterion: "Each terminal mode leaves exactly its inventory and interrupted mutation is rolled back or recovered by the declared rule."}, + {Order: 18, SlotID: "receipt_identity", Question: "Which identities must the receipt bind?", RequiredConsumerDecision: "Select command, argv, input, output, subject, and witness identities.", CompletionCriterion: "The receipt binds every selected identity by digest and any one-field substitution is rejected."}, + {Order: 19, SlotID: "currentness_trust", Question: "Which currentness and trust class is admitted?", RequiredConsumerDecision: "Select a closed class and its evidence predicates.", CompletionCriterion: "The class is owner-admitted from evidence, stale or weaker evidence is rejected, and the witness does not escalate the class."}, + {Order: 20, SlotID: "escalation", Question: "Which unknowns block and who resolves them?", RequiredConsumerDecision: "Select an owner, blocking condition, and required evidence for every unresolved state.", CompletionCriterion: "Each unresolved state blocks the pass path and names exactly one owner, condition, and evidence requirement."}, + {Order: 21, SlotID: "retirement", Question: "When may this witness be replaced or removed?", RequiredConsumerDecision: "Select replacement identity, parity predicates, owner approval, and retirement gate.", CompletionCriterion: "Replacement parity passes on the exact subject, the owner approval is bound, and the retirement gate rejects premature removal."}, + {Order: 22, SlotID: "non_claims", Question: "Which conclusions remain explicitly denied?", RequiredConsumerDecision: "Select the denied authority, scope, and readiness claims.", CompletionCriterion: "The exact non-claims remain in the report contract and no positive report language implies a denied claim."}, +} + +var guidanceNonClaims = [...]string{ + "The template does not generate, execute, approve, authenticate, or prove adequacy of a consuming repository's script, witness, oracle, receipt, policy, or cleanup implementation.", +} + +// Build returns a fresh immutable-value projection of the fixed template. +func Build() (Guidance, error) { + if err := validateGuidanceTable(); err != nil { + return Guidance{}, err + } + return Guidance{ + SchemaVersion: SchemaVersion, + GuidanceID: GuidanceID, + Slots: append([]Slot(nil), guidanceTable[:]...), + NonClaims: append([]string(nil), guidanceNonClaims[:]...), + }, nil +} + +// RenderPlainText renders the fixed guidance in at most two lines per slot. +func RenderPlainText() (string, error) { + lines, err := TextProjection() + if err != nil { + return "", err + } + var builder strings.Builder + for _, line := range lines { + builder.WriteString(line.Label) + builder.WriteString(": ") + builder.WriteString(line.Value) + builder.WriteByte('\n') + } + text := builder.String() + if len(text) > MaximumTextBytes || strings.Count(text, "\n") > MaximumTextLines { + return "", errInvalidGuidanceTable + } + return text, nil +} + +// TextProjection returns a fresh structured projection for human rendering. +func TextProjection() ([]TextLine, error) { + guidance, err := Build() + if err != nil { + return nil, err + } + lines := make([]TextLine, 0, len(guidance.Slots)*2+1) + for _, slot := range guidance.Slots { + lines = append(lines, + TextLine{Label: slot.SlotID, Value: "decision: " + slot.RequiredConsumerDecision}, + TextLine{Label: "completion", Value: slot.CompletionCriterion}, + ) + } + lines = append(lines, TextLine{Label: "non-claim", Value: guidance.NonClaims[0]}) + return lines, nil +} + +func validateGuidanceTable() error { + if len(guidanceTable) != SlotCount || len(guidanceNonClaims) != 1 || guidanceNonClaims[0] == "" { + return errInvalidGuidanceTable + } + seen := make(map[string]struct{}, len(guidanceTable)) + for index, slot := range guidanceTable { + if slot.Order != index+1 || slot.SlotID == "" || slot.Question == "" || slot.RequiredConsumerDecision == "" || slot.CompletionCriterion == "" { + return errInvalidGuidanceTable + } + if _, duplicate := seen[slot.SlotID]; duplicate { + return errInvalidGuidanceTable + } + seen[slot.SlotID] = struct{}{} + } + return nil +} diff --git a/internal/command/nativeevidenceguidance/guidance_test.go b/internal/command/nativeevidenceguidance/guidance_test.go new file mode 100644 index 0000000..398f54c --- /dev/null +++ b/internal/command/nativeevidenceguidance/guidance_test.go @@ -0,0 +1,157 @@ +package nativeevidenceguidance + +import ( + "crypto/sha256" + "fmt" + "strings" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" +) + +var slotFingerprints = [...]string{ + "4f7d1ea4203b97065c619f024d88f3f1fb1eb3df8c022a7cab69ad8af0b0d251", + "1b6b182ab32c13c303bb31cab396112256daa9d7361ef99f6cc7af6fc088fa81", + "45df453e81d723ccef3cf29eb3ccde7ba90bac72356194e49ba19f26fc252fd1", + "555d5b05c5588974c86b471dfd9af8ee9b4774ee75ff981a5148902fa9974ba8", + "80c46416ff3b4855c832b1c126180e4c0d630265a814f47874ab4a99e752a9fa", + "ecf29279f0527277c0ea62aa5210917659743357d17b88696175cd85a513c6fb", + "fed68d4a0833735bff84ab5509dfc07a233611aa7c3ca2593efdb2fe6de31f46", + "5f76850ecd6571cc081365656f89e4888bf6ce53f6f47efceaebb8e83f271427", + "8adea3ac35394b6bebf904f51a1a488cad8d156713566076c8ba529e36b725d8", + "e7ca629cbe625bb8c342dfadb1ca9ecb3e4456d7dbe394e3bf7e158b5ed8d048", + "ed669cec6efc72612632050c1173395741290639e9bca106352276212e387a0e", + "4f72cdb349f7d87a8a25ce0c6180dd403224792c710b274b9b393d6c3b64cf93", + "d5883acd09e114ca0571af5c845c9770e31b11178d59aa3bcebeead8e8ad7e51", + "d2f65a835de4d04ed0d8d5b322ab694a26ff68f85055d4a13790ea13e45bb6b8", + "d5a74ef4e8e16ee74991962d72871787e92508b32e2460344e99f87c87950aaf", + "df63e39402c6daa6aa078e18e75d741da855f9490c7d4cfad3dbcbef37aa1b55", + "10ef4f5cbc7b81d12479bb1b2c48be1d59349e40ca0859e23fd0376bca6279de", + "6d63c32605c2a21ae866e7edfb1dc6b9c752e5e87987d6cf8d62627e8f0f78d4", + "833a7d9fb8309efce8b1ad5c728fe7c393ae5d90cdfd50a71e88df3acab731e2", + "c6caf7c6e81f0aab3b4261fde6c490d46cc3db0cc3869d79dbad44553e913f7a", + "6c83ba103d8c1b4ad61d196bc90bd8c6033c3b7b35173efbc6b7be8e192cc3bb", + "05ed48165f9ef51ca30d2f0cad31050cd65beca08dca56ced734e27dca35711c", +} + +func TestGuidanceSlotPredicates(t *testing.T) { + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.011574335775061433824546899357885119356867940503348997935121282242720220286828") + guidance, err := Build() + if err != nil { + t.Fatalf("Build() error = %v", err) + } + if guidance.SchemaVersion != SchemaVersion || guidance.GuidanceID != GuidanceID || len(guidance.Slots) != SlotCount { + t.Fatalf("Build() = %#v, want fixed versioned 22-slot guidance", guidance) + } + for _, slot := range guidance.Slots { + slot := slot + t.Run("slot_"+slot.SlotID, func(t *testing.T) { + if got, want := slotFingerprint(slot), slotFingerprints[slot.Order-1]; got != want { + t.Fatalf("slot %s fingerprint = %s, want %s", slot.SlotID, got, want) + } + }) + } +} + +func TestGuidancePurityPredicates(t *testing.T) { + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.058757478738758052674807161936434157865458879934093996636398118539163086073405") + t.Run("deterministic_rebuild", func(t *testing.T) { + first, err := Build() + if err != nil { + t.Fatalf("first Build() error = %v", err) + } + first.Slots[0].SlotID = "mutated" + first.NonClaims[0] = "mutated" + second, err := Build() + if err != nil { + t.Fatalf("second Build() error = %v", err) + } + if second.Slots[0].SlotID == "mutated" || second.NonClaims[0] == "mutated" { + t.Fatalf("Build() leaked mutable state: %#v", second) + } + firstText, err := RenderPlainText() + if err != nil { + t.Fatalf("first RenderPlainText() error = %v", err) + } + secondText, err := RenderPlainText() + if err != nil { + t.Fatalf("second RenderPlainText() error = %v", err) + } + if firstText != secondText { + t.Fatal("RenderPlainText() is not deterministic") + } + }) + +} + +func TestGuidancePlainTextIsBoundedAndComplete(t *testing.T) { + text, err := RenderPlainText() + if err != nil { + t.Fatalf("RenderPlainText() error = %v", err) + } + if len(text) > MaximumTextBytes || strings.Count(text, "\n") > MaximumTextLines { + t.Fatalf("text bounds exceeded: bytes=%d lines=%d", len(text), strings.Count(text, "\n")) + } + guidance, err := Build() + if err != nil { + t.Fatalf("Build() error = %v", err) + } + for _, slot := range guidance.Slots { + for _, required := range []string{slot.SlotID, slot.RequiredConsumerDecision, slot.CompletionCriterion} { + if !strings.Contains(text, required) { + t.Fatalf("text omits required slot projection %q", required) + } + } + } +} + +func TestGuidanceTextProjectionOwnsPlainTextBytes(t *testing.T) { + lines, err := TextProjection() + if err != nil { + t.Fatalf("TextProjection() error = %v", err) + } + if got, want := len(lines), SlotCount*2+1; got != want { + t.Fatalf("TextProjection() line count = %d, want %d", got, want) + } + var projected strings.Builder + for _, line := range lines { + if line.Label == "" || line.Value == "" { + t.Fatalf("empty text coordinate: %#v", line) + } + projected.WriteString(line.Label) + projected.WriteString(": ") + projected.WriteString(line.Value) + projected.WriteByte('\n') + } + plain, err := RenderPlainText() + if err != nil { + t.Fatalf("RenderPlainText() error = %v", err) + } + if projected.String() != plain { + t.Fatalf("structured text projection drifted from plain bytes") + } +} + +func TestGuidanceJSONProjectionIsFreshAndComplete(t *testing.T) { + guidance, err := Build() + if err != nil { + t.Fatalf("Build() error = %v", err) + } + first := guidance.JSONValue() + firstSlots := first["slots"].([]any) + firstSlots[0].(map[string]any)["slotId"] = "mutated" + first["nonClaims"].([]any)[0] = "mutated" + second := guidance.JSONValue() + secondSlots := second["slots"].([]any) + if secondSlots[0].(map[string]any)["slotId"] != guidance.Slots[0].SlotID || second["nonClaims"].([]any)[0] != guidance.NonClaims[0] { + t.Fatalf("JSONValue leaked mutable projection state: %#v", second) + } + if got, want := len(secondSlots), SlotCount; got != want { + t.Fatalf("JSONValue slot count = %d, want %d", got, want) + } +} + +func slotFingerprint(slot Slot) string { + value := fmt.Sprintf("%d\x1f%s\x1f%s\x1f%s\x1f%s", slot.Order, slot.SlotID, slot.Question, slot.RequiredConsumerDecision, slot.CompletionCriterion) + return fmt.Sprintf("%x", sha256.Sum256([]byte(value))) +} diff --git a/internal/command/obligationdecision/obligationdecision_test.go b/internal/command/obligationdecision/obligationdecision_test.go index 621880a..dba61ca 100644 --- a/internal/command/obligationdecision/obligationdecision_test.go +++ b/internal/command/obligationdecision/obligationdecision_test.go @@ -9,7 +9,7 @@ import ( ) func TestBuildAdmitsSatisfiedBlockingObligationsAndRejectsMissingReceipt(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.001377148545194323854964413239799966083945326665458969842188702848644583442833") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.035346661213959533171503523197295468035986020575117976769954315363650060625537") result, err := Build(validObligationDecisionInput("satisfied", "not_applicable")) if err != nil { t.Fatalf("Build() error=%v", err) diff --git a/internal/command/packageruntimedependency/package_runtime_dependency_test.go b/internal/command/packageruntimedependency/package_runtime_dependency_test.go index 96a0d31..e6ec197 100644 --- a/internal/command/packageruntimedependency/package_runtime_dependency_test.go +++ b/internal/command/packageruntimedependency/package_runtime_dependency_test.go @@ -8,7 +8,7 @@ import ( ) func TestBuildAdmitsExternalRuntimeDependencyAndRejectsWorkspaceResolution(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.022262543555161298264303898252120278311624176226118336531091301958798480297007") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.101769836151362105683100156912005727573731050783139506739026900338622220637729") input := validPackageRuntimeDependencyInput() record, exitCode, err := Build(input) if err != nil { diff --git a/internal/command/pilotadmission/pilotadmission_test.go b/internal/command/pilotadmission/pilotadmission_test.go index 634674f..32b2805 100644 --- a/internal/command/pilotadmission/pilotadmission_test.go +++ b/internal/command/pilotadmission/pilotadmission_test.go @@ -21,7 +21,7 @@ func TestBuildAcceptsCompletePilotContract(t *testing.T) { } func TestBuildRejectsUnknownPilotContractField(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.057912462745542653837414866608340350115558004021771379883888595693744581144285") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.022694204409755669452952597914671471098671263975815015361039173611690240692220") input := validPilotInput() input["ignoredPolicy"] = true diff --git a/internal/command/producerpolicyselfproof/producerpolicyselfproof_test.go b/internal/command/producerpolicyselfproof/producerpolicyselfproof_test.go index 2d97b05..d082e88 100644 --- a/internal/command/producerpolicyselfproof/producerpolicyselfproof_test.go +++ b/internal/command/producerpolicyselfproof/producerpolicyselfproof_test.go @@ -10,7 +10,7 @@ import ( ) func TestBuildRejectsPolicyChangeProvedByNewlyAdmittedProducerTuple(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.023031113506101401680342008180057163880726687061377420204317138240585982404187") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.063770797143416748034373520228921815878182407804123939519151781443908375478611") record, exitCode, err := Build(validProducerPolicySelfProofInput()) if err != nil { t.Fatalf("Build() error = %v", err) diff --git a/internal/command/projectstructure/projectstructure_test.go b/internal/command/projectstructure/projectstructure_test.go index 6b6e2c7..6c4fbe5 100644 --- a/internal/command/projectstructure/projectstructure_test.go +++ b/internal/command/projectstructure/projectstructure_test.go @@ -14,7 +14,7 @@ import ( ) func TestBuildAdmitsProjectStructureScaffoldAndEmitsBoundedEnvelope(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.077605368601324151625037603028023832943347653489996031097831539301304492489683") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.037371132698055478830003513030413076101577486660775190823992122377324338127073") result, err := BuildResult(validProjectStructureInput()) if err != nil { t.Fatalf("BuildResult() error=%v", err) @@ -110,7 +110,7 @@ func TestBuildRejectsSecretLikeProjectStructureNonClaims(t *testing.T) { } func TestBuildRejectsProjectStructurePathDriftAndUnsafePaths(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.101379439615551172730953420664832137049480746789667740612911039267803529974896") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.055838458060501225711396534855781820639451458570073194608503610998150220419132") cases := []struct { name string mutate func(map[string]any) diff --git a/internal/command/proofbindingtestinventory/proofbindingtestinventory_test.go b/internal/command/proofbindingtestinventory/proofbindingtestinventory_test.go index b519ce4..7462553 100644 --- a/internal/command/proofbindingtestinventory/proofbindingtestinventory_test.go +++ b/internal/command/proofbindingtestinventory/proofbindingtestinventory_test.go @@ -234,7 +234,7 @@ func TestBuildRejectsUnsafeStructuredFalsificationSelector(t *testing.T) { } func TestBuildRejectsDerivedCommandRefCollision(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.015764554175908530517697174697685677859930004803180746537891295020670383915624") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.062842352098499757205217828134841664768355685286488912174783955445428381971012") input := validInput() binding := input["compactProofContract"].(map[string]any)["bindings"].([]any)[0].([]any) falsification := binding[8].([]any) diff --git a/internal/command/proofobligationalgebra/proof_obligation_algebra_test.go b/internal/command/proofobligationalgebra/proof_obligation_algebra_test.go index d9f5b86..2eba4ee 100644 --- a/internal/command/proofobligationalgebra/proof_obligation_algebra_test.go +++ b/internal/command/proofobligationalgebra/proof_obligation_algebra_test.go @@ -176,7 +176,7 @@ func equalStrings(left []string, right []string) bool { } func TestBuildAdmitsAtomicObligationAndRejectsMissingRoute(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.007102825729733194306222688479474522145093834474110859044235069588057227816672") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.041102869859879089463580716091283530617461432011352987316450558030999125563323") input := validProofObligationAlgebraInput() record, exitCode, err := Build(input) if err != nil { diff --git a/internal/command/proofreceiptadmission/proofreceiptadmission_test.go b/internal/command/proofreceiptadmission/proofreceiptadmission_test.go index 5f1a0ff..ed6dbee 100644 --- a/internal/command/proofreceiptadmission/proofreceiptadmission_test.go +++ b/internal/command/proofreceiptadmission/proofreceiptadmission_test.go @@ -10,7 +10,7 @@ import ( ) func TestBuildAdmitsAdvisoryReceiptAndRejectsMergeSatisfyingWithoutProvenance(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.046833331227659402056123694228405967213258150200955697512043665799069784736285") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.086367316685053098894220945395583898153955316069549184851388914447217184796402") record, exitCode, err := Build(validProofReceiptInput()) if err != nil { t.Fatalf("Build() error = %v", err) diff --git a/internal/command/publicapi/public_api.go b/internal/command/publicapi/public_api.go index ae2062e..62a1a14 100644 --- a/internal/command/publicapi/public_api.go +++ b/internal/command/publicapi/public_api.go @@ -12,6 +12,7 @@ import ( "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/unicodepolicy" ) const ( @@ -465,9 +466,13 @@ func (scan *scanCache) readRelativeFileSnapshot(root *os.Root, lexical string, r return admittedFileSnapshot{}, fmt.Errorf("%s changed identity or size during confined read", context) } scan.bytesRead += int64(len(content)) + decoded, err := unicodepolicy.DecodeUTF8(content) + if err != nil { + return admittedFileSnapshot{}, fmt.Errorf("TypeScript public API source is not valid UTF-8") + } snapshot := admittedFileSnapshot{ canonical: canonical, - content: string(content), + content: decoded, digest: sha256.Sum256(content), identity: before, } diff --git a/internal/command/publicapi/public_api_test.go b/internal/command/publicapi/public_api_test.go index 31a7869..9403469 100644 --- a/internal/command/publicapi/public_api_test.go +++ b/internal/command/publicapi/public_api_test.go @@ -124,7 +124,7 @@ func TestVerifyTypeScriptPackagePublicAPIRejectsUnsupportedSourceSyntaxExtension } func TestVerifyTypeScriptPackagePublicAPIRejectsExportsFromDifferentDeclaredSource(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.004792030078784317588665526148252597261312386806264755602821268098843148288440") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.102601143831468331304027263274996643907811804363888943260186270355665494704279") repoRoot := writeTypeScriptPackageFixture(t) packageRoot := filepath.Join(repoRoot, "packages", "alpha") if err := os.WriteFile(filepath.Join(packageRoot, "src", "other.ts"), []byte(`export const OTHER = 1;`), 0o600); err != nil { @@ -160,7 +160,7 @@ func TestVerifyTypeScriptPackagePublicAPIRejectsExportsFromDifferentDeclaredSour } func TestVerifyTypeScriptPackagePublicAPIRejectsExportStar(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.060836943116930548342946569118531229790270856128186493541932275964533781302400") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.084948126543701304270396527668710328555075166247372923591595608358272315175292") repoRoot := writeTypeScriptPackageFixture(t) sourcePath := filepath.Join(repoRoot, "packages", "alpha", "src", "index.ts") if err := os.WriteFile(sourcePath, []byte(`export * from "./internal";`), 0o600); err != nil { diff --git a/internal/command/readinesscloseout/readinesscloseout_test.go b/internal/command/readinesscloseout/readinesscloseout_test.go index 2049bb7..553c68c 100644 --- a/internal/command/readinesscloseout/readinesscloseout_test.go +++ b/internal/command/readinesscloseout/readinesscloseout_test.go @@ -160,7 +160,7 @@ func TestBuildAddsMandatoryBoundaryNonClaims(t *testing.T) { } func TestBuildRejectsBroadNegationAndFrontierOverclaim(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.007267731246771664992521702627347665138317306947213538578926686143640960390811") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.048421647577729214004736192002007757083539948611403437630859589578986203239714") markdown := closedFrontierMarkdown( "Future authoring rows must not require separate owner proof before merge authority is established.", ) diff --git a/internal/command/receiptcurrentnessscope/receipt_currentness_scope_test.go b/internal/command/receiptcurrentnessscope/receipt_currentness_scope_test.go index 8f8ec59..fec56d3 100644 --- a/internal/command/receiptcurrentnessscope/receipt_currentness_scope_test.go +++ b/internal/command/receiptcurrentnessscope/receipt_currentness_scope_test.go @@ -10,7 +10,7 @@ const digestA = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa const digestB = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" func TestBuildAdmitsCurrentScopedReceiptAndRejectsStaleDigest(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.022236487108238931965127542889597957904695079808409845972751946122899435814532") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.062583477063651355819549431634216824110543333801082185408854830094943563627829") input := validReceiptCurrentnessScopeInput() record, exitCode, err := Build(input) if err != nil { diff --git a/internal/command/receiptproduceradmission/receiptproduceradmission_test.go b/internal/command/receiptproduceradmission/receiptproduceradmission_test.go index 3c8d2bd..d492122 100644 --- a/internal/command/receiptproduceradmission/receiptproduceradmission_test.go +++ b/internal/command/receiptproduceradmission/receiptproduceradmission_test.go @@ -20,7 +20,7 @@ func TestBuildAcceptsMergeSatisfyingProducerReceipt(t *testing.T) { } func TestBuildRejectsAdvisoryProducerForMergeSatisfyingReceipt(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.053308755633390310507952668654187701868931366930577643796604589279359393321955") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.025389764277280783827012552921897725995672541170012681306936453725523295228359") input := validAdmission() producer := input["producers"].([]any)[0].(map[string]any) producer["admissionLevel"] = "advisory" diff --git a/internal/command/receipttrustclass/receipt_trust_class_test.go b/internal/command/receipttrustclass/receipt_trust_class_test.go index bf33322..c412f48 100644 --- a/internal/command/receipttrustclass/receipt_trust_class_test.go +++ b/internal/command/receipttrustclass/receipt_trust_class_test.go @@ -9,7 +9,7 @@ import ( ) func TestBuildAdmitsTrustedReceiptAndRejectsMissingProvenance(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.014682110212614408688669170070931583072458934622263058290594375355449658410585") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.030410956818640054490182664092187883427834500350571279664954937294396542369892") input := validReceiptTrustClassInput() record, exitCode, err := Build(input) if err != nil { diff --git a/internal/command/registryconsumer/registryconsumer_test.go b/internal/command/registryconsumer/registryconsumer_test.go index cc69b30..dbc3164 100644 --- a/internal/command/registryconsumer/registryconsumer_test.go +++ b/internal/command/registryconsumer/registryconsumer_test.go @@ -12,7 +12,7 @@ import ( ) func TestRegistryConsumerAcceptsRegistryReleaseProof(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.033718356853491749326048145854675346348034022276684730993169590765794228506662") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.034701649848424466416361577764893401884128233483125865084101267052313568115911") record, exitCode, err := Build(validRegistryConsumerInput(t)) if err != nil { t.Fatalf("Build() error = %v", err) @@ -178,7 +178,7 @@ func TestRegistryExpectedReleaseAuthorityOutputUsesOwnerDigest(t *testing.T) { } func TestRegistryConsumerRejectsLegacyRootImportProof(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.014303427678413549037642209464102836025458053002613511670388392919823523961849") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.069611182226856486814388939792127069656350462633771514327526952480435375689324") input := validRegistryConsumerInput(t) proof := input["proof"].(map[string]any) proof["rootImportOutputSha256"] = sha256Hex() diff --git a/internal/command/registryconsumerinputcompose/registry_consumer_input_compose_test.go b/internal/command/registryconsumerinputcompose/registry_consumer_input_compose_test.go index 4547ea4..92de7c8 100644 --- a/internal/command/registryconsumerinputcompose/registry_consumer_input_compose_test.go +++ b/internal/command/registryconsumerinputcompose/registry_consumer_input_compose_test.go @@ -23,7 +23,7 @@ var testRequiredPreconditionIDs = []string{ } func TestBuildComposesInputAcceptedByRegistryConsumer(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.061624550493343445773564486888927869310676729537690954962361875225762539722512") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.023911378784767042896710443491291732363632403560408817977627023926149328856779") input := validComposeInput(t) output, exitCode, err := Build(input) if err != nil { @@ -44,7 +44,7 @@ func TestBuildComposesInputAcceptedByRegistryConsumer(t *testing.T) { } func TestBuildBlocksUnavailableRequiredPreconditionsWithoutAcceptedInput(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.074432687060893820701506224431203998502881951879706934067424442564366527870298") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.013575457214699883334108189665727805186537481376269434889166294251758361114827") for _, preconditionID := range testRequiredPreconditionIDs { t.Run(preconditionID, func(t *testing.T) { input := validComposeInput(t) diff --git a/internal/command/releaseauthority/releaseauthority_test.go b/internal/command/releaseauthority/releaseauthority_test.go index c0ccf81..69f0047 100644 --- a/internal/command/releaseauthority/releaseauthority_test.go +++ b/internal/command/releaseauthority/releaseauthority_test.go @@ -97,7 +97,7 @@ func TestAdmitConsumerProjectionInputJSONRoundTripsThroughOwner(t *testing.T) { } func TestBuildRejectsPrivateSourceNPMProvenanceClaim(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.105439509272579211435567089194759430024590675934332783957047489125678984707707") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.061442232439986664341155935684067033504248665469099799771788215419617926234993") input := validRegistryReleaseInput("npm_provenance", "private") record, exitCode, err := Build(input) diff --git a/internal/command/renderedartifactfreshness/rendered_artifact_freshness_test.go b/internal/command/renderedartifactfreshness/rendered_artifact_freshness_test.go index 871b45f..71357f0 100644 --- a/internal/command/renderedartifactfreshness/rendered_artifact_freshness_test.go +++ b/internal/command/renderedartifactfreshness/rendered_artifact_freshness_test.go @@ -11,7 +11,7 @@ const digestA = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa const digestB = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" func TestBuildAdmitsFreshRenderedArtifactAndRejectsDigestDrift(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.068616017028884434484440707243549301670259924558596491643742977971172281661381") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.049617193320719810984976907462617729632660787371463429579186089921079683505007") input := validRenderedArtifactFreshnessInput() record, exitCode, err := Build(input) if err != nil { diff --git a/internal/command/repoprofileadmission/repo_profile_admission_test.go b/internal/command/repoprofileadmission/repo_profile_admission_test.go index f8392d8..d645036 100644 --- a/internal/command/repoprofileadmission/repo_profile_admission_test.go +++ b/internal/command/repoprofileadmission/repo_profile_admission_test.go @@ -8,7 +8,7 @@ import ( ) func TestBuildAdmitsValidRepoProfileAndRejectsRootPackageMismatch(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.042220601016185803105807340743508604421080385922243495458605947963572735177886") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.027996698767714019826017834835995643014194594460281065828476658982639257237081") record, exitCode, err := Build(validRepoProfileInput()) if err != nil { t.Fatalf("Build() error = %v", err) diff --git a/internal/command/requirementauthoringplan/requirement_authoring_plan_test.go b/internal/command/requirementauthoringplan/requirement_authoring_plan_test.go index 4afab68..e96f753 100644 --- a/internal/command/requirementauthoringplan/requirement_authoring_plan_test.go +++ b/internal/command/requirementauthoringplan/requirement_authoring_plan_test.go @@ -142,7 +142,7 @@ func TestBuildRejectsCandidateMissingRequiredAuthoringFields(t *testing.T) { } func TestBuildRejectsCandidateSourceAdmissionFailure(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.001844210971300640912460155205486111610590080014844769281330826828830013003484") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.083389748775835400553445979904052979137934295186556847066581462858159571590041") input := validInput() candidate := firstUpdate(input)["candidateRequirement"].(map[string]any) candidate["proofBindingRefs"] = []any{} diff --git a/internal/command/requirementbinding/compact_contract_test.go b/internal/command/requirementbinding/compact_contract_test.go index 77c9277..0fffa5f 100644 --- a/internal/command/requirementbinding/compact_contract_test.go +++ b/internal/command/requirementbinding/compact_contract_test.go @@ -26,7 +26,7 @@ func TestBuildResolverPreservesDeclaredMutationResistanceClaim(t *testing.T) { } func TestBuildResolverEmitsNamedLookupFacts(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.111196201832829118735064910982698751650497890612272762431668463661114699885279") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.020252748736197219195726021847215163853374206651017999861556513231827965208252") output, exitCode, err := BuildResolver(validCompactContract(), ResolverOptions{LocalEnvironmentClasses: []string{"local-go"}}) if err != nil { t.Fatalf("BuildResolver() error = %v", err) @@ -159,7 +159,7 @@ func TestBuildResolverRejectsCompactShellControlCommandText(t *testing.T) { } func TestBuildResolverRejectsUnscopedCompactIdentity(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.076041686007458666270617161270722640013102244122307015251002781061987907765701") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.083861718564193628743761217964380808871199122810480767631328821342986116941585") type selectorCase struct { name string mutate func(map[string]any) diff --git a/internal/command/requirementbinding/projections_test.go b/internal/command/requirementbinding/projections_test.go index 93ec93d..6acbed0 100644 --- a/internal/command/requirementbinding/projections_test.go +++ b/internal/command/requirementbinding/projections_test.go @@ -72,7 +72,7 @@ func readOwnerJSON(t *testing.T, path string) any { } func TestBuildReportFailsUnknownRequirementBinding(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.104544593850875418975815490237416802070339444461856063303404358287592673340470") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.006781441425154247333133631169445758062349553831425234148684262403215403003170") input := validRequirementBindingInput() input["bindings"].([]any)[0].(map[string]any)["requirementId"] = "REQ-PROOFKIT-MISSING" @@ -155,7 +155,7 @@ func TestBuildReportRejectsEmptyWitnessCommandEnvironmentClasses(t *testing.T) { } func TestBuildEvidenceGraphBuildsGraphAndRejectsFailedReport(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.103879939339912119536842874285920100618442305235744692163114660613220220633744") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.106664769115320409045707252880376697541304793801587746134683234647988969824937") graph, exitCode, err := BuildEvidenceGraph(validRequirementBindingInput()) if err != nil { t.Fatalf("BuildEvidenceGraph() error=%v", err) @@ -177,7 +177,7 @@ func TestBuildEvidenceGraphBuildsGraphAndRejectsFailedReport(t *testing.T) { } func TestBuildProofSliceSelectsRequirementsAndRejectsFailedReport(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.108834399078649179642042963327326693613324047941888185048583794094499189388965") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.044542665943108637823252106214058056605966553305196284592890698016502907775137") input := validRequirementBindingInput() input["selection"] = map[string]any{ "changedPaths": []any{}, diff --git a/internal/command/requirementbrowser/server_test.go b/internal/command/requirementbrowser/server_test.go index 9cad30c..0d6182a 100644 --- a/internal/command/requirementbrowser/server_test.go +++ b/internal/command/requirementbrowser/server_test.go @@ -603,7 +603,7 @@ func (writer readyWriter) Write(bytes []byte) (int, error) { } func TestStartServerFailsClosedForNonLoopbackHosts(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.023640023374539652452651315983351191617142195079844369423488208926061999224113") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.067593917059454010073799027017616832235011027418005301800769590106420738231062") for _, host := range []string{"0.0.0.0", "localhost"} { t.Run(host, func(t *testing.T) { _, err := StartServer(sourceInput(t), Options{ diff --git a/internal/command/requirementcontext/compose.go b/internal/command/requirementcontext/compose.go index a046d96..f181808 100644 --- a/internal/command/requirementcontext/compose.go +++ b/internal/command/requirementcontext/compose.go @@ -311,7 +311,7 @@ func readCatalogSource(root *os.Root, path string) (any, []byte, string, error) if err != nil { return nil, nil, "", err } - return value, content, digest.SHA256TextRef(string(content)), nil + return value, content, digest.SHA256BytesRef(content), nil } func expectedDigestCoverage(sources []Source) string { diff --git a/internal/command/requirementcontext/requirementcontext_test.go b/internal/command/requirementcontext/requirementcontext_test.go index 0b8335e..3f0e1fc 100644 --- a/internal/command/requirementcontext/requirementcontext_test.go +++ b/internal/command/requirementcontext/requirementcontext_test.go @@ -16,7 +16,7 @@ import ( ) func TestComposeAndSliceRoundTrip(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.073105413718212685720467657880735003544056444223060540529926611328658669412386") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.014989945418882269851301771813751592528811970506607223135861291503422294739361") root := fixtureRepository(t) contextValue, err := Compose(root, fixtureCatalog()) if err != nil { @@ -315,7 +315,7 @@ func resignSnapshot(t *testing.T, value map[string]any) { } func TestSliceRejectsTamperedSnapshotAndUnknownNode(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.070746173522414644645062396948230600718686825876357257505266577982734819530024") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.069162415320540756982773651649760342296713012215029052933916884217327976783225") root := fixtureRepository(t) contextValue, err := Compose(root, fixtureCatalog()) if err != nil { diff --git a/internal/command/requirementcoverageinput/requirementcoverageinput_test.go b/internal/command/requirementcoverageinput/requirementcoverageinput_test.go index e185be7..290c273 100644 --- a/internal/command/requirementcoverageinput/requirementcoverageinput_test.go +++ b/internal/command/requirementcoverageinput/requirementcoverageinput_test.go @@ -15,7 +15,7 @@ import ( ) func TestBuildComposesInputPreservesDeclaredUniverseAndAllowsDownstreamFailures(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.077615860756811261841718576176256104140210383495891744406155830486424738479038") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.083035171813898501427594128067554887214808924680328054190631175708411343072409") output, exitCode, err := Build(validComposeInput(t, baseInventoryEntries())) if err != nil { t.Fatalf("Build() error = %v", err) @@ -100,7 +100,7 @@ func mutateJSONStrings(value any) { } func TestBuildComposesDirectRequirementProofBindingAndInventory(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.035702766579458185352787503340754906774314209827550424808834264957824734855602") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.070158111768335561723850873410063317795356942001573297865826302278939990585054") input := validComposeInput(t, baseInventoryEntries()).(map[string]any) normalized := input["normalizedTestEvidenceInventory"].(map[string]any) delete(input, "compactProofContract") @@ -291,7 +291,7 @@ func TestBuildRejectsDirectInventoryWithoutNormalizedEnvelope(t *testing.T) { } func TestBuildRejectsFabricatedDirectEnvelopeWithSourceMetadata(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.082003289099884097239481684858563895288262439491982038468563724937356701005316") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.060779660800594907269041501547271398346431553050593761603864645719347862956228") input := validComposeInput(t, baseInventoryEntries()).(map[string]any) normalized := input["normalizedTestEvidenceInventory"].(map[string]any) normalized["sourceCount"] = json.Number("1") diff --git a/internal/command/requirementcoverageview/requirementcoverageview_test.go b/internal/command/requirementcoverageview/requirementcoverageview_test.go index 80e9af7..7a7fb9d 100644 --- a/internal/command/requirementcoverageview/requirementcoverageview_test.go +++ b/internal/command/requirementcoverageview/requirementcoverageview_test.go @@ -836,7 +836,7 @@ func TestBuildJSONAgentEnvelopeUsesSharedEnvelopeKernel(t *testing.T) { } func TestBuildJSONRejectsRouteOnlyCoverageForBlockingRequirement(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.031718276374641887712984669039991110874842587862832715070881908134515964679634") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.096123496880170047039891532537520304154987278051414410267040570752441936434036") input := validCoverageInput(t) entry := inventoryEntry(input) entry["evidenceClass"] = "routing_smoke_nonclaim" diff --git a/internal/command/requirementdiff/requirementdiff_test.go b/internal/command/requirementdiff/requirementdiff_test.go index af33b1c..d527514 100644 --- a/internal/command/requirementdiff/requirementdiff_test.go +++ b/internal/command/requirementdiff/requirementdiff_test.go @@ -188,7 +188,7 @@ func TestOwnerFilterPreservesStableIdentityAcrossOwnershipChange(t *testing.T) { } func TestBuildCoversCompleteRequirementChangeAlgebra(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.078549664710316599082962403097259031550446698569117313237162339371160705954076") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.020074570913264891573344859495254807591700934304007335407083834667788859243594") baseline := contextFixture(t, "The shared requirement remains stable.") current := contextFixture(t, "The shared requirement changes its invariant.") baseRequirements := baseline["projections"].(map[string]any)["requirementSources"].([]any)[0].(map[string]any)["requirements"].([]any) diff --git a/internal/command/requirementgraph/requirementgraph_test.go b/internal/command/requirementgraph/requirementgraph_test.go index 5ed4098..69c9775 100644 --- a/internal/command/requirementgraph/requirementgraph_test.go +++ b/internal/command/requirementgraph/requirementgraph_test.go @@ -18,7 +18,7 @@ import ( ) func TestBuildKeepsTraceabilityEvidencePlanesDistinct(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.022500508667327863554430021675314177435444085857004785904226266316397359221432") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.011566084091414968930866467013093603154294001800098001627050138117499521212186") contextValue := graphContextFixture(t) code := "package handler\n\nfunc Handle() { return }\n" codeDigest := digest.SHA256TextRef(code) @@ -70,7 +70,7 @@ func TestBuildKeepsTraceabilityEvidencePlanesDistinct(t *testing.T) { } func TestAdmitOutputRejectsDanglingAndIncoherentCodeParents(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.000951313542647162551115118684533813603727933293140621118035893199512478060879") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.002184183541549890800355191013261195744179206833558045852123912699485122962134") output, err := Build(graphPermutationInput(t)) if err != nil { t.Fatal(err) diff --git a/internal/command/requirementimpactinput/requirementimpactinput_test.go b/internal/command/requirementimpactinput/requirementimpactinput_test.go index 6a8b3e8..f310167 100644 --- a/internal/command/requirementimpactinput/requirementimpactinput_test.go +++ b/internal/command/requirementimpactinput/requirementimpactinput_test.go @@ -15,7 +15,7 @@ import ( ) func TestBuildComposesInputAndRoutesChangedBlockingRequirement(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.023234960845452171243291914185559159842773576460263008556765730794053386182086") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.030089205175258355525512374181401591961738735214675037864934765862429075466299") input := validComposeInput(t) currentSource := input["currentRequirementSources"].([]any)[0].(map[string]any) currentSource["requirements"].([]any)[0].(map[string]any)["invariant"] = "Requirement impact input composition must route changed blocking requirement records to caller-owned proof obligations." diff --git a/internal/command/requirementproofsourceset/requirementproofsourceset_test.go b/internal/command/requirementproofsourceset/requirementproofsourceset_test.go index d2f3524..ea479e1 100644 --- a/internal/command/requirementproofsourceset/requirementproofsourceset_test.go +++ b/internal/command/requirementproofsourceset/requirementproofsourceset_test.go @@ -127,7 +127,7 @@ func mutateFirstFragment(t *testing.T, input map[string]any, mutate func(map[str } func TestBuildSelectsSourceSetRowsAndEmitsResolverInput(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.106477495378157513392694067858829301266454618332683944940209642307095597996567") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.098739271914051045049143171660941588813365344851520126355916621566896893381817") input := validFragmentSourceSetInput(t) input["projection"] = map[string]any{ "kind": "resolver_input", diff --git a/internal/command/requirementproofview/requirementproofview_test.go b/internal/command/requirementproofview/requirementproofview_test.go index ddfb873..c9583f9 100644 --- a/internal/command/requirementproofview/requirementproofview_test.go +++ b/internal/command/requirementproofview/requirementproofview_test.go @@ -174,7 +174,7 @@ func TestBuildHTMLEscapesCallerControlledCompactFields(t *testing.T) { } func TestBuildMarkdownEscapesCallerControlledCompactFields(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.004013119203828558628733921363223531050236398733138199684571194055855497152335") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.049051314984593969889898709173890310899067493106284046166101008127654609539240") output, exitCode, err := BuildMarkdown(maliciousCompactContract(), Options{LocalEnvironmentClasses: []string{"local-go"}}) if err != nil { t.Fatalf("BuildMarkdown() error = %v", err) diff --git a/internal/command/requirementsourceadmission/requirementsourceadmission_test.go b/internal/command/requirementsourceadmission/requirementsourceadmission_test.go index 17e45f8..f2d5173 100644 --- a/internal/command/requirementsourceadmission/requirementsourceadmission_test.go +++ b/internal/command/requirementsourceadmission/requirementsourceadmission_test.go @@ -50,7 +50,7 @@ func TestEvaluateAcceptsActiveBlockingRequirementWithProofRoute(t *testing.T) { } func TestEvaluateRejectsBlockingRequirementWithoutProofRoute(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.049105822013487377720513422847314427679030355003527858634951221583838655591366") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.072077823236321697138427281525724834744695054203573403855376161988559434150965") input := validSource() requirement := input["requirements"].([]any)[0].(map[string]any) requirement["proofBindingRefs"] = []any{} diff --git a/internal/command/requirementsourcetransition/requirementsourcetransition_test.go b/internal/command/requirementsourcetransition/requirementsourcetransition_test.go index a33885c..50ad550 100644 --- a/internal/command/requirementsourcetransition/requirementsourcetransition_test.go +++ b/internal/command/requirementsourcetransition/requirementsourcetransition_test.go @@ -29,7 +29,7 @@ func TestBuildRejectsLifecycleTransitionWithoutNewEvidence(t *testing.T) { } func TestBuildRejectsRequirementSourceTransitionContractViolations(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.064796430251047638129056627618050421615250184739595429149390171993569993200591") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.055034508158962348386865908543647126084818400787416007817692072895213278443259") cases := []struct { name string want string diff --git a/internal/command/requirementsourceview/requirementsourceview_test.go b/internal/command/requirementsourceview/requirementsourceview_test.go index 24d5df9..fcce880 100644 --- a/internal/command/requirementsourceview/requirementsourceview_test.go +++ b/internal/command/requirementsourceview/requirementsourceview_test.go @@ -8,7 +8,7 @@ import ( ) func TestBuildMarkdownEscapesCallerControlledText(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.005704906455553467725586561316636717684357758836129946697278325918285718879720") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.115603095301227499403457054570913397276777831949518460396011319173029743458113") input := validRequirementSource() input["specPackagePath"] = "docs/specs/proofkit-``" input["overviewPath"] = "docs/specs/proofkit-``/overview.md" diff --git a/internal/command/requirementspectree/requirementspectree_test.go b/internal/command/requirementspectree/requirementspectree_test.go index b720f88..9a20e9a 100644 --- a/internal/command/requirementspectree/requirementspectree_test.go +++ b/internal/command/requirementspectree/requirementspectree_test.go @@ -31,7 +31,7 @@ func TestBuildAdmitsSpecTree(t *testing.T) { } func TestBuildRejectsDAGAndStaleDigest(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.101919815328177545466245213748768811374784723554857148933696569778978221061398") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.076832322614886110427268048199049716166008280359317512016290756672404686404591") input := validTreeInput() edges := input["edges"].([]any) input["edges"] = append(edges, map[string]any{"parentNodeId": "meta", "childNodeId": "submodule"}) @@ -644,7 +644,7 @@ func TestBuildViewFailsClosedForInvalidTree(t *testing.T) { } func TestBuildViewMarkdownAndHTMLAreDeterministicAndEscaped(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.003233567092711501460950786753414939758288157341158912542081410563017083291305") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.088652189724939071633744726996540891312727007664431695045518976313929419190168") input := validTreeInput() nodeMap(input, "module")["label"] = "Module " overlayMap(input, "overlay.rendered.module")["label"] = "Rendered " diff --git a/internal/command/scaffoldprofileplan/scaffoldprofileplan_test.go b/internal/command/scaffoldprofileplan/scaffoldprofileplan_test.go index d1d7726..c50c11d 100644 --- a/internal/command/scaffoldprofileplan/scaffoldprofileplan_test.go +++ b/internal/command/scaffoldprofileplan/scaffoldprofileplan_test.go @@ -8,7 +8,7 @@ import ( ) func TestBuildAcceptsCommandMatcherHints(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.066388111489441355762289763185156364551197665370738732325425583942920333391935") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.106890548054406001004200802811128777808529327608133121770809409737631248388044") result, err := BuildResult(validScaffoldInput()) if err != nil { t.Fatalf("BuildResult() error = %v", err) diff --git a/internal/command/secretscan/secretscan_test.go b/internal/command/secretscan/secretscan_test.go index 53c20b0..3497281 100644 --- a/internal/command/secretscan/secretscan_test.go +++ b/internal/command/secretscan/secretscan_test.go @@ -11,7 +11,7 @@ import ( ) func TestBuildFindsSecretLikeTextWithoutLeakingValue(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.039817040606337266696292466844606855102291955096447906749917550657133356377901") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.055206209582232463321609867585701497839805263297065023231864029580638752462167") const sentinel = "abc123456789" record, exitCode, err := Build(validInput(map[string][]byte{ "docs/ok.md": []byte("plain text\n"), diff --git a/internal/command/selectivegateevidence/selectivegateevidence_test.go b/internal/command/selectivegateevidence/selectivegateevidence_test.go index 8a90258..451c9e8 100644 --- a/internal/command/selectivegateevidence/selectivegateevidence_test.go +++ b/internal/command/selectivegateevidence/selectivegateevidence_test.go @@ -33,7 +33,7 @@ func TestBuildRejectsNonBooleanPublicAPIPlanFlag(t *testing.T) { } func TestProjectObligationDecisionBuildsInputAndRejectsUnroutedCommand(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.018388928679990711151132651696474760405977786383365901536442241248004003958482") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.088917990761715208703387156728584028126802229067385672185331276611475401525262") projected, err := ProjectObligationDecision(validProjectionInput()) if err != nil { t.Fatalf("ProjectObligationDecision() error=%v", err) @@ -247,7 +247,7 @@ func TestBuildRejectsFailedProducerAdmissionReport(t *testing.T) { } func TestBuildRejectsMergeSatisfyingEvidenceWithoutProducerAdmission(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.112550534058264454186632868263981916290936959138554398802013554630682769870797") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.012598928187089744067552607028228262907235005555210470674455355974318859179759") input := validEvidenceInput() delete(input, "producerAdmission") @@ -473,7 +473,7 @@ func TestBuildAddsBoundaryNonClaims(t *testing.T) { } func TestBuildReportsMergeEvidenceWithoutApprovingMerge(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.025252982453387603468032289952983834092931240062565910622005306249337590832380") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.045002366166899401519856978666954540484585836647606801288212397375230123404662") result, err := Build(validEvidenceInput()) if err != nil { t.Fatalf("Build() error = %v", err) diff --git a/internal/command/selectivegateplan/selectivegateplan_test.go b/internal/command/selectivegateplan/selectivegateplan_test.go index d7c8fc3..7c10a13 100644 --- a/internal/command/selectivegateplan/selectivegateplan_test.go +++ b/internal/command/selectivegateplan/selectivegateplan_test.go @@ -182,7 +182,7 @@ func TestBuildRejectsDisplayOnlyCommandShellControlTokens(t *testing.T) { } func TestBuildFailsClosedForUncoveredUnknownEdge(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.109203724066169697433019740792211532942871969238130040996597287475731754319862") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.021216979417723937319190282666772611980004038696116294692928111604113772494057") input := validPlanInput() input["unknownEdges"] = []any{unknownEdgeInput("edge.dynamic", "dynamic_or_unknown")} diff --git a/internal/command/specoverviewclaims/specoverviewclaims_test.go b/internal/command/specoverviewclaims/specoverviewclaims_test.go index e8fbe2a..2f44e59 100644 --- a/internal/command/specoverviewclaims/specoverviewclaims_test.go +++ b/internal/command/specoverviewclaims/specoverviewclaims_test.go @@ -103,7 +103,7 @@ func TestBuildRejectsPathDrift(t *testing.T) { } func TestBuildRejectsInvalidOverviewClaimBoundaryFacts(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.112682775494295917012327305050927168210732804488321621459110580984044195720333") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.071708772941318180080085691269138354451193033877238779698194084459052687236048") cases := []struct { name string err string @@ -166,7 +166,7 @@ func TestBuildRejectsInvalidOverviewClaimBoundaryFacts(t *testing.T) { } func TestBuildRejectsNonDurableRequirementCitationsForEveryNonDurableKind(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.009542672296287244834772220636187272276005726456609804775418682151340119727196") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.040101711721168417394454420228310006687353944771608587629015602089455554500668") for _, claimKind := range []string{"example_or_rationale", "quoted_or_code", "section_heading"} { t.Run(claimKind, func(t *testing.T) { input := validBoundary() diff --git a/internal/command/specproofbundleadmission/specproofbundleadmission_test.go b/internal/command/specproofbundleadmission/specproofbundleadmission_test.go index 0054e10..10682a6 100644 --- a/internal/command/specproofbundleadmission/specproofbundleadmission_test.go +++ b/internal/command/specproofbundleadmission/specproofbundleadmission_test.go @@ -213,7 +213,7 @@ func childReportValueWithReceipts(state string, exitCode string, failures []any, } func TestBuildRejectsForgedReceiptAdmissionChild(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.114880187029418565756618053091883445515213775524954029606797760062822359451460") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.022413626354530887841322966459922061567082717524509677004724621035769110085423") input := validBundleInput(t) receiptAdmission := input["receiptAdmission"].(map[string]any) receiptAdmission["receipts"] = []any{ diff --git a/internal/command/stackpreset/preset_ids_generated.go b/internal/command/stackpreset/preset_ids_generated.go index b9c6c9e..8682538 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 = "bde529df71be7bda804184bb461b7a599520ad1deb362e1bb322a298b24b5965" +const presetContractSourceSHA256 = "cbac189e838e846c66616ff6219e495c640cf9918cb831e0a4892c5dc38934ee" var presetIDs = []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"} diff --git a/internal/command/stackpreset/stackpreset_test.go b/internal/command/stackpreset/stackpreset_test.go index a611fcc..a75ea28 100644 --- a/internal/command/stackpreset/stackpreset_test.go +++ b/internal/command/stackpreset/stackpreset_test.go @@ -9,7 +9,7 @@ import ( ) func TestPresetInventoryIsCompleteDeterministicAndDefensivelyCopied(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.076944518366669573920389884498853536696905439692658896257172669916814288705961") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.112334016811474681410425476687144072136771318434208055710590185270972114208508") if len(presetIDs) != len(presets) { t.Fatalf("presetIDs=%d presets=%d, want one id per preset", len(presetIDs), len(presets)) } @@ -84,7 +84,7 @@ func TestPresetInventoryIsCompleteDeterministicAndDefensivelyCopied(t *testing.T } func TestUnknownPresetIsRejected(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.070141068460637498789620042072455227278667326750769337227893395903279066165874") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.055320857269956502155630312953328770510596812318812525805325435890019643943475") if IsPresetID("unknown") { t.Fatal("IsPresetID accepted unknown preset") } diff --git a/internal/command/testevidenceinventory/testevidenceinventory_test.go b/internal/command/testevidenceinventory/testevidenceinventory_test.go index 342f0a8..577b845 100644 --- a/internal/command/testevidenceinventory/testevidenceinventory_test.go +++ b/internal/command/testevidenceinventory/testevidenceinventory_test.go @@ -57,7 +57,7 @@ func TestBuildRejectsUnanchoredProofRouteCandidate(t *testing.T) { } func TestBuildDiscoveryDraftEmitsCandidateOnlyInventory(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.096318819151227230864150155118957615254328016145569586137207173056023856414640") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.079852718523712688279279462908322529234519011079936607265160783492663433095905") record, exitCode, err := BuildDiscoveryDraft(validDiscoveryDraft()) if err != nil { t.Fatalf("BuildDiscoveryDraft() error = %v", err) @@ -283,7 +283,7 @@ func TestBuildDiscoveryDraftRejectsUnsafeAndContradictoryFacts(t *testing.T) { } func TestBuildRejectsIncompleteDeclaredOracleMetadataAndDuplicateFalsifier(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.040537407725353522942442370438759713872175874575309347907987603239091331652992") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.040558695906086109940767673091001712973305283651826863842030853731406212097284") input := validInventory(t) entries := input.(map[string]any)["entries"].([]any) first := cloneMap(entries[0].(map[string]any)) diff --git a/internal/command/textpolicy/textpolicy.go b/internal/command/textpolicy/textpolicy.go index c52d4d0..257f3f3 100644 --- a/internal/command/textpolicy/textpolicy.go +++ b/internal/command/textpolicy/textpolicy.go @@ -6,10 +6,10 @@ import ( "path/filepath" "sort" "strings" - "unicode/utf8" "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" "github.com/research-engineering/agentic-proofkit/internal/kernel/report" + "github.com/research-engineering/agentic-proofkit/internal/kernel/unicodepolicy" ) const reportKind = "proofkit.text-policy" @@ -279,7 +279,8 @@ func isBinaryCandidate(path string, suffixes []string) bool { func verifyFile(policy Policy, path string, data []byte) ([]string, bool) { failures := []string{} - if !utf8.Valid(data) { + decoded, err := unicodepolicy.DecodeUTF8(data) + if err != nil { return []string{fmt.Sprintf("%s: not valid UTF-8", path)}, false } if policy.RequireFinalNewline && len(data) > 0 && data[len(data)-1] != '\n' { @@ -288,7 +289,7 @@ func verifyFile(policy Policy, path string, data []byte) ([]string, bool) { if policy.AsciiOnly { line := 1 column := 1 - for _, value := range string(data) { + for _, value := range decoded { switch value { case '\n': line++ @@ -308,7 +309,7 @@ func verifyFile(policy Policy, path string, data []byte) ([]string, bool) { } } if policy.RejectTrailingWhitespace { - for lineIndex, lineData := range strings.Split(string(data), "\n") { + for lineIndex, lineData := range strings.Split(decoded, "\n") { if strings.HasSuffix(lineData, " ") || strings.HasSuffix(lineData, "\t") { failures = append(failures, fmt.Sprintf("%s:%d: trailing whitespace", path, lineIndex+1)) return failures, true diff --git a/internal/command/textpolicy/textpolicy_test.go b/internal/command/textpolicy/textpolicy_test.go index c3bb399..6f2f7be 100644 --- a/internal/command/textpolicy/textpolicy_test.go +++ b/internal/command/textpolicy/textpolicy_test.go @@ -10,7 +10,7 @@ import ( ) func TestEvaluatePreservesUTF8ASCIIWhitespaceAndBinaryFalsifiers(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.031094312573272652863042278542901905945454475685240528067456704904488178168569") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.063809241610934477437716286923029349318149179919687982782786287816761572189049") result, err := Evaluate(validInput(map[string][]byte{ "binary.ZIP": []byte{0xff, 0xfe}, "docs/empty.md": {}, diff --git a/internal/command/witnessplan/witnessplan_test.go b/internal/command/witnessplan/witnessplan_test.go index 82e7717..4024c23 100644 --- a/internal/command/witnessplan/witnessplan_test.go +++ b/internal/command/witnessplan/witnessplan_test.go @@ -8,7 +8,7 @@ import ( ) func TestBuildAdmitsSafeCommandAndRejectsShellCommand(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.088457406797103795279543653822179063516575466007372745444447470717382611396483") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.085190431407182739545830738835155395836062703209880873059377949976085217132650") plan, err := Build(validWitnessPlanInput()) if err != nil { t.Fatalf("Build() error = %v", err) @@ -44,7 +44,7 @@ func TestBuildAdmitsOptionalDirectInputSchemaVersionOne(t *testing.T) { } func TestBuildProjectsRequirementBindingsToWitnessPlan(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.097064780769502204057926611195131645878255373766343559374932016116089291062973") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.012411978339439760206999646729446091824827781410305387149567149181015055010570") input := map[string]any{ "schemaVersion": json.Number("1"), "projection": "requirement-bindings", diff --git a/internal/command/witnessschedulerplan/witnessschedulerplan_test.go b/internal/command/witnessschedulerplan/witnessschedulerplan_test.go index cc7d9ae..09e0fdd 100644 --- a/internal/command/witnessschedulerplan/witnessschedulerplan_test.go +++ b/internal/command/witnessschedulerplan/witnessschedulerplan_test.go @@ -22,7 +22,7 @@ func TestBuildRejectsNetworkMetadataContradictions(t *testing.T) { } func TestBuildRejectsUnsafeParallelWriteCollision(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.104676796118456324923039124511231127526363393383801390782562378512319721449269") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.031235317036465371542022481065610116329891022492706535866504864985411543775232") input := validSchedulerPlanInput() command(input)["id"] = "proofkit.left" schedulerPolicy(input)["commandId"] = "proofkit.left" diff --git a/internal/command/workspacemanifestfacts/workspace_manifest_facts_test.go b/internal/command/workspacemanifestfacts/workspace_manifest_facts_test.go index 4bb75a7..efcbe08 100644 --- a/internal/command/workspacemanifestfacts/workspace_manifest_facts_test.go +++ b/internal/command/workspacemanifestfacts/workspace_manifest_facts_test.go @@ -13,7 +13,7 @@ import ( ) func TestBuildProjectsManifestFactsAndPlanningInputs(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.072879230585900406731220760823785361194395031187183819675511483181285804453184") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.046992911157429495726495826718827252677060139154875823240935717157131877150065") output, exitCode, err := Build(validInput(t)) if err != nil { t.Fatalf("Build() error = %v", err) @@ -79,7 +79,7 @@ func TestBuildOutputsAreAdmittedByWorkspacePlanningCommands(t *testing.T) { } func TestBuildRejectsUnsafeManifestPathAndDuplicatePackageIdentity(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.076788067606744061308755388073116836267887021333035147502494382368969043524666") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.021006600305534566600307318298219298198056731386995324010869919022968168820682") t.Run("unsafe manifest path", func(t *testing.T) { input := validInput(t).(map[string]any) input["packages"].([]any)[0].(map[string]any)["manifestPath"] = "../package.json" diff --git a/internal/command/workspaceplanning/workspaceplanning_test.go b/internal/command/workspaceplanning/workspaceplanning_test.go index 4194a74..e5339e1 100644 --- a/internal/command/workspaceplanning/workspaceplanning_test.go +++ b/internal/command/workspaceplanning/workspaceplanning_test.go @@ -8,7 +8,7 @@ import ( ) func TestChangedPackagePlanAdmitsPackagesRootAndSchema(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.000915784104466894868430403781008280151510309816212488025513280699400857148130") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.099901347117709869449512361041699546156858878065694966382459003909292505090347") input := validChangedPackagePlanInput() plan, err := BuildChangedPackagePlan(input) @@ -83,7 +83,7 @@ func TestShardPartitionRejectsUnknownNestedFields(t *testing.T) { } func TestShardPartitionAdmitsCoveredRootsAndRejectsMissingDependency(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.011525493414109086910947302412685558638860644500578844199382862426651697571033") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.078289321613275805064332964500278192564264874761019976788520205993988052208613") partition, exitCode, err := BuildShardPartition(validShardPartitionInput()) if err != nil { t.Fatalf("BuildShardPartition() error=%v", err) diff --git a/internal/command/workspaceregistry/workspaceregistry_test.go b/internal/command/workspaceregistry/workspaceregistry_test.go index 92eb449..4ab98dc 100644 --- a/internal/command/workspaceregistry/workspaceregistry_test.go +++ b/internal/command/workspaceregistry/workspaceregistry_test.go @@ -8,7 +8,7 @@ import ( ) func TestBuildAdmitsWorkspaceRegistryAndRejectsMissingScriptTarget(t *testing.T) { - commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.050787429179113333482394128925978139548470597502501014010332804049210083823789") + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.063080661639755003031032251305144344878025724678482041048084098938166996025209") record, exitCode, err := Build(validWorkspaceRegistryInput()) if err != nil { t.Fatalf("Build() error=%v", err) diff --git a/internal/kernel/admit/fields.go b/internal/kernel/admit/fields.go index 49d8a59..52e7cda 100644 --- a/internal/kernel/admit/fields.go +++ b/internal/kernel/admit/fields.go @@ -7,6 +7,8 @@ import ( "regexp" "sort" "strings" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/unicodepolicy" ) const ( @@ -35,6 +37,7 @@ var ( const ( maxDiagnosticRunes = 512 maxRuleIDBytes = 256 + redactedValueLabel = "" ) type RedactionFixture struct { @@ -159,6 +162,10 @@ func ContainsSecretLikeValue(value string) bool { return ContainsSecretTokenLikeValue(value) || ContainsURLCredentialValue(value) } +func ContainsReportVisibleUnsafeValue(value string) bool { + return unicodepolicy.ContainsUnsafeScalar(value) || ContainsSecretLikeValue(value) +} + func ContainsSecretLikePathValue(value string) bool { return ContainsURLCredentialValue(value) || secretPathContextPattern.MatchString(value) || secretPathTokenPattern.MatchString(value) } @@ -172,13 +179,16 @@ func ContainsURLCredentialValue(value string) bool { } func RedactSecretLikeValue(value string) string { - value = secretValuePattern.ReplaceAllString(value, "") - return urlUserInfoPattern.ReplaceAllString(value, "") + if ContainsSecretLikeValue(value) { + return redactedValueLabel + } + return value } func RedactDiagnosticValue(value string) string { - value = RedactSecretLikeValue(value) - value = redactControlRunes(value) + if ContainsReportVisibleUnsafeValue(value) { + return redactedValueLabel + } runes := []rune(value) if len(runes) <= maxDiagnosticRunes { return value @@ -187,24 +197,10 @@ func RedactDiagnosticValue(value string) string { } func RedactStructuralText(value string) string { - return redactControlRunes(RedactSecretLikeValue(value)) -} - -func redactControlRunes(value string) string { - var builder strings.Builder - redacting := false - for _, character := range value { - if character < 0x20 || character == 0x7f { - if !redacting { - builder.WriteString("") - redacting = true - } - continue - } - redacting = false - builder.WriteRune(character) + if ContainsReportVisibleUnsafeValue(value) { + return redactedValueLabel } - return builder.String() + return value } func DisplayOnlyCommandText(raw any, context string) (string, error) { diff --git a/internal/kernel/admit/fields_test.go b/internal/kernel/admit/fields_test.go index b00ae30..aa026be 100644 --- a/internal/kernel/admit/fields_test.go +++ b/internal/kernel/admit/fields_test.go @@ -150,56 +150,51 @@ func TestMergeNonClaimsPreservesRequiredClaimsAndRejectsSecretLikeCallerText(t * } } -func TestRedactDiagnosticValueRemovesSensitiveAndUnsafeSubstrings(t *testing.T) { +func TestRedactDiagnosticValueReplacesRejectedValuesAsAWhole(t *testing.T) { t.Parallel() - diagnostic := "open proofkit/ghp_secretvalue/input.json:\n" + strings.Repeat("x", 600) - redacted := RedactDiagnosticValue(diagnostic) - if strings.Contains(redacted, "ghp_secretvalue") { - t.Fatalf("RedactDiagnosticValue leaked secret-shaped token: %q", redacted) - } - if !strings.Contains(redacted, "") { - t.Fatalf("RedactDiagnosticValue(%q) = %q, want secret placeholder", diagnostic, redacted) + const want = "" + for _, fixture := range ReportVisibleRedactionFixtures() { + if got := RedactDiagnosticValue("prefix " + fixture.Input + " suffix"); got != want { + t.Fatalf("RedactDiagnosticValue(%s) = %q, want fixed label", fixture.Name, got) + } } - if !strings.Contains(redacted, "") { - t.Fatalf("RedactDiagnosticValue(%q) = %q, want control placeholder", diagnostic, redacted) + for _, value := range []string{"line\nbreak", "unsafe\u0085value", "unsafe\u200bvalue", "unsafe\u2028value", "unsafe\u2029value", "unsafe\U000e0001value", string([]byte{0xff})} { + if got := RedactDiagnosticValue(value); got != want { + t.Fatalf("RedactDiagnosticValue unsafe class = %q, want fixed label", got) + } } - if !strings.Contains(redacted, "") { - t.Fatalf("RedactDiagnosticValue(%q) = %q, want truncation marker", diagnostic, redacted) + longSafe := strings.Repeat("x", maxDiagnosticRunes+20) + if got := RedactDiagnosticValue(longSafe); !strings.HasSuffix(got, "...") { + t.Fatalf("safe long diagnostic was not bounded: %q", got) } +} - headerDiagnostic := "request failed: Authorization: Basic YWxpY2U6c2VjcmV0" - headerRedacted := RedactDiagnosticValue(headerDiagnostic) - if strings.Contains(headerRedacted, "YWxpY2U6c2VjcmV0") || strings.Contains(headerRedacted, "Basic") { - t.Fatalf("RedactDiagnosticValue leaked authorization header value: %q", headerRedacted) - } +func TestReportVisibleRedactionMatrixRejectsSplitSecretsWithoutDisclosure(t *testing.T) { + t.Parallel() + const want = "" + separators := []string{"\x00", "\x7f", "\u0085", "\u200b", "\U000e0001", "\u2028", "\u2029"} for _, fixture := range ReportVisibleRedactionFixtures() { - redacted := RedactDiagnosticValue(fixture.Input) - for _, needle := range fixture.SensitiveNeedles { - if strings.Contains(redacted, needle) { - t.Fatalf("RedactDiagnosticValue leaked %s needle %q in %q", fixture.Name, needle, redacted) + base := "prefix " + fixture.Input + " suffix" + if got := RedactStructuralText(base); got != want { + t.Fatalf("contiguous %s structural redaction = %q", fixture.Name, got) + } + for _, separator := range separators { + for _, offset := range []int{len(base) / 3, len(base) / 2, len(base) * 2 / 3} { + value := base[:offset] + separator + base[offset:] + if got := RedactDiagnosticValue(value); got != want { + t.Fatalf("split %s diagnostic redaction = %q", fixture.Name, got) + } + if got := RedactStructuralText(value); got != want { + t.Fatalf("split %s structural redaction = %q", fixture.Name, got) + } } } } -} - -func TestRedactStructuralTextPreservesLongStructureAndRedactsSensitiveTokens(t *testing.T) { - t.Parallel() - - longToken := strings.Repeat("x", maxDiagnosticRunes+20) - structural := RedactStructuralText(longToken + "\n" + "Authorization: Bearer abcdefghijklmnop") - if strings.Contains(structural, "") { - t.Fatalf("RedactStructuralText truncated structural token: %q", structural) - } - if !strings.Contains(structural, longToken) { - t.Fatalf("RedactStructuralText lost long structural token: %q", structural) - } - if !strings.Contains(structural, "") { - t.Fatalf("RedactStructuralText(%q) = %q, want control placeholder", longToken, structural) - } - if !strings.Contains(structural, "") { - t.Fatalf("RedactStructuralText(%q) = %q, want secret placeholder", longToken, structural) + longSafe := strings.Repeat("x", maxDiagnosticRunes+20) + if got := RedactStructuralText(longSafe); got != longSafe { + t.Fatalf("safe structural text changed: %q", got) } } diff --git a/internal/kernel/agentenvelope/agentenvelope.go b/internal/kernel/agentenvelope/agentenvelope.go index c97512a..c47c359 100644 --- a/internal/kernel/agentenvelope/agentenvelope.go +++ b/internal/kernel/agentenvelope/agentenvelope.go @@ -45,14 +45,15 @@ type Input struct { } func Build(input Input) map[string]any { - actionPlan := sortMaps(sanitizeMapList(input.ActionPlan), "stepId") - contextRefs := sortMaps(sanitizeMapList(input.ContextRefs), "refId") - routeQuestions := sortMaps(sanitizeMapList(input.RouteQuestions), "questionId") - clarificationQuestions := sortMaps(sanitizeMapList(input.ClarificationQuestion), "questionId") - blockedPreconditions := sortMaps(sanitizeMapList(input.BlockedPreconditions), "preconditionId") - omitted := sortMaps(sanitizeMapList(input.Omitted), "omissionId") - receiptRefs := sortMaps(sanitizeMapList(input.ReceiptRefs), "receiptRefId") - commands := sortMaps(sanitizeMapList(input.Commands), "commandId") + prepared := prepareLocalReferences(input) + actionPlan := sortMaps(sanitizeMapList(prepared.actionPlan), "stepId") + contextRefs := sortMaps(sanitizeMapList(prepared.contextRefs), "refId") + routeQuestions := sortMaps(sanitizeMapList(prepared.routeQuestions), "questionId") + clarificationQuestions := sortMaps(sanitizeMapList(prepared.clarificationQuestions), "questionId") + blockedPreconditions := sortMaps(sanitizeMapList(prepared.blockedPreconditions), "preconditionId") + omitted := sortMaps(sanitizeMapList(prepared.omitted), "omissionId") + receiptRefs := sortMaps(sanitizeMapList(prepared.receiptRefs), "receiptRefId") + commands := sortMaps(sanitizeMapList(prepared.commands), "commandId") omittedCount := 0 for _, item := range omitted { if count, ok := item["omittedCount"].(int); ok { @@ -74,9 +75,9 @@ func Build(input Input) map[string]any { "omittedCount": omittedCount, } } - truncated := false + truncated := prepared.identityDegraded originalTargets := collectLocalReferenceTargets(commands, contextRefs, receiptRefs) - actionPlan, omitted, truncated = limitActionPlan(actionPlan, omitted, bounds) + actionPlan, omitted, truncated = limitActionPlan(actionPlan, omitted, bounds, truncated) blockedPreconditions, omitted, truncated = limitList(blockedPreconditions, "blockedPrecondition", "preconditionId", "maxBlockedPreconditions", hardMaxBlockedPreconditions, omitted, bounds, truncated) clarificationQuestions, omitted, truncated = limitList(clarificationQuestions, "clarificationQuestion", "questionId", "maxClarificationQuestions", hardMaxClarificationQuestions, omitted, bounds, truncated) routeQuestions, omitted, truncated = limitList(routeQuestions, "routeQuestion", "questionId", "maxRouteQuestions", hardMaxRouteQuestions, omitted, bounds, truncated) @@ -86,13 +87,13 @@ func Build(input Input) map[string]any { receiptRefs, omitted, truncated = limitListPrioritized(receiptRefs, "receiptRef", "receiptRefId", "maxReceiptRefs", hardMaxReceiptRefs, priorities.receipts, omitted, bounds, truncated) omitted, truncated = limitOmitted(omitted, bounds, truncated) retainedTargets := collectLocalReferenceTargets(commands, contextRefs, receiptRefs) - removedReferenceCount := 0 + removedReferenceCount := prepared.removedReferenceCount for _, records := range []*[]map[string]any{&actionPlan, &blockedPreconditions, &clarificationQuestions, &routeQuestions, &omitted, &commands, &contextRefs, &receiptRefs} { var removed int *records, removed = pruneDanglingLocalReferences(*records, originalTargets, retainedTargets) removedReferenceCount += removed } - referenceClosurePreserved := localReferenceClosurePreserved(originalTargets, retainedTargets, actionPlan, blockedPreconditions, clarificationQuestions, routeQuestions, omitted, commands, contextRefs, receiptRefs) + referenceClosurePreserved := !prepared.identityDegraded && localReferenceClosurePreserved(originalTargets, retainedTargets, actionPlan, blockedPreconditions, clarificationQuestions, routeQuestions, omitted, commands, contextRefs, receiptRefs) if removedReferenceCount > 0 { bounds["prunedLocalReferenceCount"] = removedReferenceCount truncated = true @@ -309,9 +310,9 @@ func boundViolations(bounds map[string]any, counts envelopeCounts) []string { return violations } -func limitActionPlan(values []map[string]any, omitted []map[string]any, bounds map[string]any) ([]map[string]any, []map[string]any, bool) { +func limitActionPlan(values []map[string]any, omitted []map[string]any, bounds map[string]any, truncated bool) ([]map[string]any, []map[string]any, bool) { limit := itemLimit(bounds, "maxActionItems", hardMaxActionItems) - result, omitted, truncated := limitList(values, "actionItem", "stepId", "maxActionItems", hardMaxActionItems, omitted, bounds, false) + result, omitted, truncated := limitList(values, "actionItem", "stepId", "maxActionItems", hardMaxActionItems, omitted, bounds, truncated) evidenceLimit := itemLimit(bounds, "maxActionEvidenceRefs", hardMaxActionEvidenceRefs) remainingEvidenceRefs := evidenceLimit for index, item := range result { diff --git a/internal/kernel/agentenvelope/agentenvelope_test.go b/internal/kernel/agentenvelope/agentenvelope_test.go index 32fefca..bce2f7f 100644 --- a/internal/kernel/agentenvelope/agentenvelope_test.go +++ b/internal/kernel/agentenvelope/agentenvelope_test.go @@ -276,14 +276,8 @@ func TestInvalidInputEnvelopeRedactsSecretLikeDiagnostics(t *testing.T) { if strings.Contains(text, secret) { t.Fatalf("invalid input envelope leaked secret-shaped diagnostic: %s", text) } - if !strings.Contains(text, "") { - t.Fatalf("invalid input envelope text=%q, want redaction placeholder", text) - } - if !strings.Contains(text, "") { - t.Fatalf("invalid input envelope text=%q, want control placeholder", text) - } - if !strings.Contains(text, "") { - t.Fatalf("invalid input envelope text=%q, want truncation marker", text) + if got := strings.Count(text, ""); got != 2 { + t.Fatalf("invalid input envelope redaction count=%d text=%q, want two whole-value labels", got, text) } } @@ -326,7 +320,7 @@ func TestBuildRedactsReportVisibleCallerTextAndSnapshotsInput(t *testing.T) { sourceReport["debugDump"] = secret assertEnvelopeDoesNotContain(t, envelope, secret) - assertEnvelopeContains(t, envelope, "") + assertEnvelopeContains(t, envelope, "") projected := envelope["sourceReport"].(map[string]any) if _, ok := projected["debugDump"]; ok { t.Fatalf("sourceReport projection leaked debugDump: %#v", projected) @@ -406,7 +400,7 @@ func TestBuildRedactsTypedContainersAndUnsupportedValues(t *testing.T) { defined["nested"] = definedSlice{secret + ".mutated"} assertEnvelopeDoesNotContain(t, envelope, secret) - assertEnvelopeContains(t, envelope, "") + assertEnvelopeContains(t, envelope, "") assertEnvelopeContains(t, envelope, "") serialized, err := stablejson.Marshal(envelope) if err != nil { diff --git a/internal/kernel/agentenvelope/reference_identity.go b/internal/kernel/agentenvelope/reference_identity.go new file mode 100644 index 0000000..f5cb147 --- /dev/null +++ b/internal/kernel/agentenvelope/reference_identity.go @@ -0,0 +1,260 @@ +package agentenvelope + +import "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" + +type preparedLocalReferences struct { + actionPlan []map[string]any + blockedPreconditions []map[string]any + clarificationQuestions []map[string]any + commands []map[string]any + contextRefs []map[string]any + omitted []map[string]any + receiptRefs []map[string]any + routeQuestions []map[string]any + removedReferenceCount int + identityDegraded bool +} + +type localIdentityMultiplicity struct { + commands map[string]int + contexts map[string]int + receipts map[string]int +} + +type localReferenceAdmission struct { + identityDegraded bool + removedCount int + standaloneOmissions int + values []map[string]any +} + +func prepareLocalReferences(input Input) preparedLocalReferences { + prepared := preparedLocalReferences{ + actionPlan: cloneMapList(input.ActionPlan), + blockedPreconditions: cloneMapList(input.BlockedPreconditions), + clarificationQuestions: cloneMapList(input.ClarificationQuestion), + commands: cloneMapList(input.Commands), + contextRefs: cloneMapList(input.ContextRefs), + omitted: cloneMapList(input.Omitted), + receiptRefs: cloneMapList(input.ReceiptRefs), + routeQuestions: cloneMapList(input.RouteQuestions), + } + + rejectedTargets := map[string]struct{}{} + var unsafeTargets int + prepared.commands, unsafeTargets = admitLocalTargetRecords(prepared.commands, "commandId", rejectedTargets) + var count int + prepared.contextRefs, count = admitLocalTargetRecords(prepared.contextRefs, "refId", rejectedTargets) + unsafeTargets += count + prepared.receiptRefs, count = admitLocalTargetRecords(prepared.receiptRefs, "receiptRefId", rejectedTargets) + unsafeTargets += count + + multiplicity := localIdentityMultiplicity{ + commands: localIdentityCounts(prepared.commands, "commandId"), + contexts: localIdentityCounts(prepared.contextRefs, "refId"), + receipts: localIdentityCounts(prepared.receiptRefs, "receiptRefId"), + } + ambiguous := ambiguousLocalIdentities(multiplicity) + prepared.commands = removeAmbiguousTargets(prepared.commands, "commandId", ambiguous) + prepared.contextRefs = removeAmbiguousTargets(prepared.contextRefs, "refId", ambiguous) + prepared.receiptRefs = removeAmbiguousTargets(prepared.receiptRefs, "receiptRefId", ambiguous) + + if unsafeTargets > 0 { + prepared.omitted = append(prepared.omitted, omissionRecord("unsafeLocalIdentity", "localTargetIds", unsafeTargets)) + prepared.identityDegraded = true + } + if len(ambiguous) > 0 { + prepared.omitted = append(prepared.omitted, omissionRecord("ambiguousLocalIdentity", "localTargetIds", len(ambiguous))) + prepared.identityDegraded = true + } + + uniqueTargets := collectLocalReferenceTargets(prepared.commands, prepared.contextRefs, prepared.receiptRefs) + standaloneReferenceOmissions := 0 + for _, records := range []*[]map[string]any{ + &prepared.actionPlan, + &prepared.blockedPreconditions, + &prepared.clarificationQuestions, + &prepared.routeQuestions, + &prepared.omitted, + &prepared.commands, + &prepared.contextRefs, + &prepared.receiptRefs, + } { + admission := admitRecordLocalReferences(*records, uniqueTargets, rejectedTargets, ambiguous) + *records = admission.values + prepared.removedReferenceCount += admission.removedCount + standaloneReferenceOmissions += admission.standaloneOmissions + prepared.identityDegraded = prepared.identityDegraded || admission.identityDegraded + } + if standaloneReferenceOmissions > 0 { + prepared.omitted = append(prepared.omitted, omissionRecord("unsafeLocalReference", "localReferences", standaloneReferenceOmissions)) + prepared.identityDegraded = true + } + return prepared +} + +func admitLocalTargetRecords(values []map[string]any, key string, rejected map[string]struct{}) ([]map[string]any, int) { + result := make([]map[string]any, 0, len(values)) + omitted := 0 + for _, value := range values { + if _, ok := losslessLocalIdentity(value[key]); !ok { + if identity, isText := value[key].(string); isText { + rejected[identity] = struct{}{} + } + omitted++ + continue + } + result = append(result, value) + } + return result, omitted +} + +func losslessLocalIdentity(raw any) (string, bool) { + value, ok := raw.(string) + if !ok { + return "", false + } + admitted, err := admit.RuleID(value, "agent envelope local identity") + return admitted, err == nil && admitted == value +} + +func losslessExternalReference(raw any) (string, bool) { + value, ok := raw.(string) + if !ok || value == "" { + return "", false + } + return value, admit.RedactStructuralText(value) == value +} + +func localIdentityCounts(values []map[string]any, key string) map[string]int { + result := map[string]int{} + for _, value := range values { + identity, _ := losslessLocalIdentity(value[key]) + result[identity]++ + } + return result +} + +func ambiguousLocalIdentities(multiplicity localIdentityMultiplicity) map[string]struct{} { + result := map[string]struct{}{} + domainCount := map[string]int{} + for _, domain := range []map[string]int{multiplicity.commands, multiplicity.contexts, multiplicity.receipts} { + for identity, count := range domain { + if count != 1 { + result[identity] = struct{}{} + } + domainCount[identity]++ + } + } + for identity, count := range domainCount { + if count != 1 { + result[identity] = struct{}{} + } + } + return result +} + +func removeAmbiguousTargets(values []map[string]any, key string, ambiguous map[string]struct{}) []map[string]any { + result := make([]map[string]any, 0, len(values)) + for _, value := range values { + identity, _ := losslessLocalIdentity(value[key]) + if _, found := ambiguous[identity]; found { + continue + } + result = append(result, value) + } + return result +} + +func admitRecordLocalReferences(values []map[string]any, targets localReferenceTargets, rejected map[string]struct{}, ambiguous map[string]struct{}) localReferenceAdmission { + admission := localReferenceAdmission{} + result := make([]map[string]any, len(values)) + for index, value := range values { + cloned := copyMap(value) + if raw, exists := cloned["evidenceRefs"]; exists { + kept := []any{} + for _, item := range referenceItems(raw) { + identity, ok := losslessExternalReference(item) + if !ok { + admission.removedCount++ + admission.identityDegraded = true + if identity, isText := item.(string); !isText || !isRejectedIdentity(identity, rejected, ambiguous) { + admission.standaloneOmissions++ + } + continue + } + if _, found := rejected[identity]; found { + admission.removedCount++ + admission.identityDegraded = true + continue + } + if _, found := ambiguous[identity]; found { + admission.removedCount++ + admission.identityDegraded = true + continue + } + kept = append(kept, identity) + } + cloned["evidenceRefs"] = kept + } + if raw, exists := cloned["commandIds"]; exists { + kept := []any{} + for _, item := range referenceItems(raw) { + identity, ok := losslessLocalIdentity(item) + if !ok { + admission.removedCount++ + admission.identityDegraded = true + if identity, isText := item.(string); !isText || !isRejectedIdentity(identity, rejected, ambiguous) { + admission.standaloneOmissions++ + } + continue + } + if _, found := ambiguous[identity]; found { + admission.removedCount++ + admission.identityDegraded = true + continue + } + if _, found := targets.commands[identity]; !found { + admission.removedCount++ + continue + } + kept = append(kept, identity) + } + cloned["commandIds"] = kept + } + result[index] = cloned + } + admission.values = result + return admission +} + +func isRejectedIdentity(identity string, rejected map[string]struct{}, ambiguous map[string]struct{}) bool { + if _, found := rejected[identity]; found { + return true + } + _, found := ambiguous[identity] + return found +} + +func referenceItems(raw any) []any { + switch values := raw.(type) { + case []any: + return values + case []string: + result := make([]any, len(values)) + for index, value := range values { + result[index] = value + } + return result + default: + return []any{raw} + } +} + +func cloneMapList(values []map[string]any) []map[string]any { + result := make([]map[string]any, len(values)) + for index, value := range values { + result[index] = copyMap(value) + } + return result +} diff --git a/internal/kernel/agentenvelope/reference_identity_test.go b/internal/kernel/agentenvelope/reference_identity_test.go new file mode 100644 index 0000000..e9201f7 --- /dev/null +++ b/internal/kernel/agentenvelope/reference_identity_test.go @@ -0,0 +1,222 @@ +package agentenvelope + +import ( + "strings" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" +) + +func TestBuildRejectsDuplicateAndCrossDomainLocalIdentities(t *testing.T) { + sharedID := "proofkit.shared.identity" + tests := []struct { + name string + commands []map[string]any + contexts []map[string]any + receipts []map[string]any + }{ + {name: "duplicate commands", commands: []map[string]any{{"commandId": sharedID}, {"commandId": sharedID}}}, + {name: "duplicate contexts", contexts: []map[string]any{{"refId": sharedID}, {"refId": sharedID}}}, + {name: "duplicate receipts", receipts: []map[string]any{{"receiptRefId": sharedID}, {"receiptRefId": sharedID}}}, + {name: "command context collision", commands: []map[string]any{{"commandId": sharedID}}, contexts: []map[string]any{{"refId": sharedID}}}, + {name: "command receipt collision", commands: []map[string]any{{"commandId": sharedID}}, receipts: []map[string]any{{"receiptRefId": sharedID}}}, + {name: "context receipt collision", contexts: []map[string]any{{"refId": sharedID}}, receipts: []map[string]any{{"receiptRefId": sharedID}}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + envelope := Build(referenceIdentityInput(test.commands, test.contexts, test.receipts, []any{sharedID})) + + assertLocalIdentityAbsent(t, envelope, sharedID) + assertIdentityDegraded(t, envelope, 1, 1) + assertOmission(t, envelope, "proofkit.agent-envelope.omitted.ambiguousLocalIdentity", 1) + }) + } +} + +func TestBuildPrunesAmbiguousIdentityFromEveryReferenceContainer(t *testing.T) { + sharedID := "proofkit.shared.identity" + referenced := func(idKey string, id string) map[string]any { + return map[string]any{ + idKey: id, + "commandIds": []any{sharedID}, + "evidenceRefs": []any{sharedID}, + } + } + input := referenceIdentityInput( + []map[string]any{referenced("commandId", "proofkit.command.unique")}, + []map[string]any{ + {"refId": sharedID}, + {"refId": sharedID}, + referenced("refId", "proofkit.context.unique"), + }, + []map[string]any{referenced("receiptRefId", "proofkit.receipt.unique")}, + nil, + ) + input.ActionPlan = []map[string]any{referenced("stepId", "proofkit.step.unique")} + input.BlockedPreconditions = []map[string]any{referenced("preconditionId", "proofkit.precondition.unique")} + input.ClarificationQuestion = []map[string]any{referenced("questionId", "proofkit.clarification.unique")} + input.RouteQuestions = []map[string]any{referenced("questionId", "proofkit.route.unique")} + input.Omitted = []map[string]any{ + { + "commandIds": []any{sharedID}, + "evidenceRefs": []any{sharedID}, + "omissionId": "proofkit.omission.unique", + "omittedCount": 0, + }, + } + + envelope := Build(input) + assertLocalIdentityAbsent(t, envelope, sharedID) + assertIdentityDegraded(t, envelope, 1, 16) + assertOmission(t, envelope, "proofkit.agent-envelope.omitted.ambiguousLocalIdentity", 1) +} + +func TestBuildOmitsUnsafeTargetsAndReferencesBeforeDisplaySanitization(t *testing.T) { + secret := "ghp_FAKEFAKE1234567890" + control := "proofkit.control\u202evalue" + invalidShape := "proofkit invalid identity" + input := referenceIdentityInput( + []map[string]any{{"commandId": secret}}, + []map[string]any{{"refId": control}}, + []map[string]any{{"receiptRefId": invalidShape}}, + []any{secret, control, invalidShape, "proofkit.external.safe"}, + ) + input.ActionPlan[0]["commandIds"] = []any{secret, control, invalidShape, "proofkit.command.missing"} + + envelope := Build(input) + encoded, err := stablejson.Marshal(envelope) + if err != nil { + t.Fatalf("marshal envelope: %v", err) + } + for _, forbidden := range []string{secret, control, invalidShape, ""} { + if strings.Contains(string(encoded), forbidden) { + t.Fatalf("envelope retained unsafe identity %q:\n%s", forbidden, encoded) + } + } + action := envelope["actionPlan"].([]any)[0].(map[string]any) + assertAnyStrings(t, action["evidenceRefs"], []string{"proofkit.external.safe"}) + assertAnyStrings(t, action["commandIds"], nil) + assertIdentityDegraded(t, envelope, 3, 7) + assertOmission(t, envelope, "proofkit.agent-envelope.omitted.unsafeLocalIdentity", 3) +} + +func TestBuildCountsStandaloneUnsafeReferenceOmissions(t *testing.T) { + secret := "ghp_FAKEFAKE1234567890" + input := referenceIdentityInput(nil, nil, nil, []any{secret}) + input.ActionPlan[0]["commandIds"] = []any{secret} + + envelope := Build(input) + assertIdentityDegraded(t, envelope, 2, 2) + assertOmission(t, envelope, "proofkit.agent-envelope.omitted.unsafeLocalReference", 2) +} + +func TestBuildPreservesUniqueLocalAndSafeExternalReferences(t *testing.T) { + input := referenceIdentityInput( + []map[string]any{{"commandId": "proofkit.command.unique"}}, + []map[string]any{{"refId": "proofkit.context.unique"}}, + []map[string]any{{"receiptRefId": "proofkit.receipt.unique"}}, + []any{"proofkit.command.unique", "proofkit.context.unique", "proofkit.receipt.unique", "https://example.test/evidence"}, + ) + input.ActionPlan[0]["commandIds"] = []any{"proofkit.command.unique"} + + envelope := Build(input) + action := envelope["actionPlan"].([]any)[0].(map[string]any) + assertAnyStrings(t, action["evidenceRefs"], []string{ + "proofkit.command.unique", + "proofkit.context.unique", + "proofkit.receipt.unique", + "https://example.test/evidence", + }) + assertAnyStrings(t, action["commandIds"], []string{"proofkit.command.unique"}) + cost := envelope["costContract"].(map[string]any) + if cost["referenceClosurePreserved"] != true || cost["prunedLocalReferenceCount"] != 0 || cost["omittedEdgeCount"] != 0 { + t.Fatalf("unique reference cost contract = %#v", cost) + } + if bounds := envelope["bounds"].(map[string]any); bounds["truncated"] == true { + t.Fatalf("unique reference envelope was marked truncated: %#v", bounds) + } +} + +func referenceIdentityInput(commands []map[string]any, contexts []map[string]any, receipts []map[string]any, evidenceRefs []any) Input { + return Input{ + ActionPlan: []map[string]any{ + { + "commandIds": []any{}, + "evidenceRefs": evidenceRefs, + "stepId": "proofkit.step.reference-identity", + }, + }, + Commands: commands, + ContextRefs: contexts, + EnvelopeID: "proofkit.envelope.reference-identity", + ReceiptRefs: receipts, + SourceReport: map[string]any{ + "reportId": "proofkit.report.reference-identity", + "reportKind": "proofkit.test", + "state": "passed", + }, + } +} + +func assertLocalIdentityAbsent(t *testing.T, envelope map[string]any, identity string) { + t.Helper() + for _, field := range []struct { + name string + key string + }{{"commands", "commandId"}, {"contextRefs", "refId"}, {"receiptRefs", "receiptRefId"}} { + for _, raw := range envelope[field.name].([]any) { + if raw.(map[string]any)[field.key] == identity { + t.Fatalf("%s retained ambiguous identity %q: %#v", field.name, identity, envelope[field.name]) + } + } + } + encoded, err := stablejson.Marshal(envelope) + if err != nil { + t.Fatalf("marshal envelope: %v", err) + } + if strings.Contains(string(encoded), identity) { + t.Fatalf("envelope retained ambiguous identity %q:\n%s", identity, encoded) + } +} + +func assertIdentityDegraded(t *testing.T, envelope map[string]any, omittedEdges int, prunedReferences int) { + t.Helper() + cost := envelope["costContract"].(map[string]any) + if cost["referenceClosurePreserved"] != false || cost["omittedEdgeCount"] != omittedEdges || cost["prunedLocalReferenceCount"] != prunedReferences { + t.Fatalf("identity-degraded cost contract = %#v, want omitted=%d pruned=%d", cost, omittedEdges, prunedReferences) + } + bounds := envelope["bounds"].(map[string]any) + if bounds["truncated"] != true || bounds["referenceClosurePreserved"] != false { + t.Fatalf("identity-degraded bounds = %#v", bounds) + } +} + +func assertOmission(t *testing.T, envelope map[string]any, omissionID string, count int) { + t.Helper() + for _, raw := range envelope["omitted"].([]any) { + omission := raw.(map[string]any) + if omission["omissionId"] == omissionID { + if omission["omittedCount"] != count { + t.Fatalf("%s count = %v, want %d", omissionID, omission["omittedCount"], count) + } + return + } + } + t.Fatalf("omission %s is absent: %#v", omissionID, envelope["omitted"]) +} + +func assertAnyStrings(t *testing.T, raw any, expected []string) { + t.Helper() + values, ok := raw.([]any) + if !ok { + t.Fatalf("value type = %T, want []any", raw) + } + if len(values) != len(expected) { + t.Fatalf("values = %#v, want %#v", values, expected) + } + for index, expectedValue := range expected { + if values[index] != expectedValue { + t.Fatalf("values[%d] = %v, want %s", index, values[index], expectedValue) + } + } +} diff --git a/internal/kernel/digest/digest.go b/internal/kernel/digest/digest.go index 95d7b6b..e57583b 100644 --- a/internal/kernel/digest/digest.go +++ b/internal/kernel/digest/digest.go @@ -8,7 +8,11 @@ import ( ) func SHA256TextRef(text string) string { - sum := sha256.Sum256([]byte(text)) + return SHA256BytesRef([]byte(text)) +} + +func SHA256BytesRef(value []byte) string { + sum := sha256.Sum256(value) return "sha256:" + hex.EncodeToString(sum[:]) } diff --git a/internal/kernel/jsonpointer/jsonpointer.go b/internal/kernel/jsonpointer/jsonpointer.go index 50f23c2..ea02729 100644 --- a/internal/kernel/jsonpointer/jsonpointer.go +++ b/internal/kernel/jsonpointer/jsonpointer.go @@ -8,19 +8,43 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" ) -func Select(input any, pointer string) (any, error) { - if pointer == "" { - return input, nil +// Pointer is an admitted RFC 6901 pointer. Its tokens are immutable outside +// this package, so syntax admission can precede input I/O without reparsing. +type Pointer struct { + tokens []string +} + +// Parse admits pointer syntax without consulting a JSON document. +func Parse(value string) (Pointer, error) { + if value == "" { + return Pointer{}, nil } - if !strings.HasPrefix(pointer, "/") { - return nil, fmt.Errorf("JSON pointer must be an RFC 6901 pointer") + if !strings.HasPrefix(value, "/") { + return Pointer{}, fmt.Errorf("JSON pointer must be an RFC 6901 pointer") } - current := input - for position, rawPart := range strings.Split(pointer[1:], "/") { + tokens := make([]string, 0, strings.Count(value, "/")) + for _, rawPart := range strings.Split(value[1:], "/") { part, err := decodePointerToken(rawPart) if err != nil { - return nil, err + return Pointer{}, err } + tokens = append(tokens, part) + } + return Pointer{tokens: tokens}, nil +} + +func Select(input any, pointer string) (any, error) { + parsed, err := Parse(pointer) + if err != nil { + return nil, err + } + return SelectParsed(input, parsed) +} + +// SelectParsed resolves an already-admitted pointer against input. +func SelectParsed(input any, pointer Pointer) (any, error) { + current := input + for position, part := range pointer.tokens { switch typed := current.(type) { case []any: index, err := arrayIndex(part) diff --git a/internal/kernel/jsonpointer/jsonpointer_test.go b/internal/kernel/jsonpointer/jsonpointer_test.go index 3a28385..b8d2369 100644 --- a/internal/kernel/jsonpointer/jsonpointer_test.go +++ b/internal/kernel/jsonpointer/jsonpointer_test.go @@ -39,6 +39,31 @@ func TestSelectRFC6901Pointers(t *testing.T) { } } +func TestParseAndSelectParsedPreserveSelectionSemantics(t *testing.T) { + input := map[string]any{"a/b": []any{map[string]any{"a~b": "selected"}}} + pointer, err := Parse("/a~1b/0/a~0b") + if err != nil { + t.Fatalf("Parse returned error: %v", err) + } + got, err := SelectParsed(input, pointer) + if err != nil { + t.Fatalf("SelectParsed returned error: %v", err) + } + if got != "selected" { + t.Fatalf("SelectParsed returned %v, want selected", got) + } +} + +func TestParseRejectsInvalidSyntaxWithoutDocumentAccess(t *testing.T) { + for _, value := range []string{"not-a-pointer", "/bad~", "/bad~2escape"} { + t.Run(value, func(t *testing.T) { + if _, err := Parse(value); err == nil { + t.Fatal("expected syntax error") + } + }) + } +} + func TestSelectRejectsInvalidPointers(t *testing.T) { input := map[string]any{"items": []any{"first"}} for _, pointer := range []string{ diff --git a/internal/kernel/stablejson/stablejson.go b/internal/kernel/stablejson/stablejson.go index ec4a2f7..3148c28 100644 --- a/internal/kernel/stablejson/stablejson.go +++ b/internal/kernel/stablejson/stablejson.go @@ -1,13 +1,15 @@ package stablejson import ( - "bytes" "encoding/json" "fmt" "regexp" "sort" "strconv" "strings" + "unicode/utf16" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/unicodepolicy" ) var jsonNumberPattern = regexp.MustCompile(`^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$`) @@ -46,7 +48,7 @@ func writeValue(builder *strings.Builder, value any, depth int, layout Layout) e builder.WriteString("false") } case string: - builder.WriteString(quote(typed)) + return writeQuotedString(builder, typed) case json.Number: if !isJSONNumberToken(typed.String()) { return fmt.Errorf("invalid JSON number: %s", typed.String()) @@ -106,6 +108,9 @@ func writeObject(builder *strings.Builder, values map[string]any, depth int, lay } keys := make([]string, 0, len(values)) for key := range values { + if !unicodepolicy.ValidScalarString(key) { + return fmt.Errorf("stable JSON object key is not valid UTF-8") + } keys = append(keys, key) } sort.Strings(keys) @@ -123,7 +128,9 @@ func writeObject(builder *strings.Builder, values map[string]any, depth int, lay if layout == LayoutPretty { writeIndent(builder, depth+1) } - builder.WriteString(quote(key)) + if err := writeQuotedString(builder, key); err != nil { + return err + } builder.WriteByte(':') if layout == LayoutPretty { builder.WriteByte(' ') @@ -146,25 +153,44 @@ func writeIndent(builder *strings.Builder, depth int) { } } -func quote(value string) string { - if isUnescapedASCII(value) { - return `"` + value + `"` - } - var buffer bytes.Buffer - encoder := json.NewEncoder(&buffer) - encoder.SetEscapeHTML(false) - if err := encoder.Encode(value); err != nil { - panic(err) +func writeQuotedString(builder *strings.Builder, value string) error { + if !unicodepolicy.ValidScalarString(value) { + return fmt.Errorf("stable JSON string is not valid UTF-8") + } + builder.WriteByte('"') + for _, character := range value { + switch character { + case '"': + builder.WriteString(`\"`) + case '\\': + builder.WriteString(`\\`) + case '\b': + builder.WriteString(`\b`) + case '\t': + builder.WriteString(`\t`) + case '\n': + builder.WriteString(`\n`) + case '\f': + builder.WriteString(`\f`) + case '\r': + builder.WriteString(`\r`) + default: + if unicodepolicy.IsUnsafeScalar(character) { + writeUnicodeEscape(builder, character) + } else { + builder.WriteRune(character) + } + } } - return strings.TrimSuffix(buffer.String(), "\n") + builder.WriteByte('"') + return nil } -func isUnescapedASCII(value string) bool { - for index := 0; index < len(value); index++ { - character := value[index] - if character < 0x20 || character >= 0x80 || character == '"' || character == '\\' { - return false - } +func writeUnicodeEscape(builder *strings.Builder, value rune) { + if value <= 0xffff { + _, _ = fmt.Fprintf(builder, `\u%04x`, value) + return } - return true + high, low := utf16.EncodeRune(value) + _, _ = fmt.Fprintf(builder, `\u%04x\u%04x`, high, low) } diff --git a/internal/kernel/stablejson/stablejson_test.go b/internal/kernel/stablejson/stablejson_test.go index c9f430a..936d8ce 100644 --- a/internal/kernel/stablejson/stablejson_test.go +++ b/internal/kernel/stablejson/stablejson_test.go @@ -93,7 +93,6 @@ func TestQuoteFastPathMatchesCanonicalEncoder(t *testing.T) { "line\nbreak", "cafe\u0301", "\u2028\u2029", - string([]byte{0xff}), } { var buffer strings.Builder if err := writeValue(&buffer, value, 0, LayoutCompact); err != nil { @@ -111,3 +110,122 @@ func TestQuoteFastPathMatchesCanonicalEncoder(t *testing.T) { } } } + +func TestMarshalRejectsMalformedUTF8(t *testing.T) { + for _, value := range []string{ + string([]byte{0xff}), + string([]byte{0xc3, 0x28}), + string([]byte{0xed, 0xa0, 0x80}), + } { + if _, err := Marshal(map[string]any{"value": value}); err == nil { + t.Fatalf("Marshal accepted malformed UTF-8 %x", []byte(value)) + } + if _, err := Marshal(map[string]any{value: "key"}); err == nil { + t.Fatalf("Marshal accepted malformed UTF-8 key %x", []byte(value)) + } + } +} + +func TestMarshalEscapesUnsafeUnicodeAndPreservesScalarOrder(t *testing.T) { + value := map[string]any{ + "value": "\u007f\u0085\u200b\u2028\u2029\U000e0001", + "\U00010000": "supplementary", + "\ue000": "bmp", + } + output, err := MarshalLayout(value, LayoutCompact) + if err != nil { + t.Fatalf("MarshalLayout failed: %v", err) + } + const expected = "{\"value\":\"\\u007f\\u0085\\u200b\\u2028\\u2029\\udb40\\udc01\",\"\ue000\":\"bmp\",\"\U00010000\":\"supplementary\"}\n" + if string(output) != expected { + t.Fatalf("compact output = %q, want %q", output, expected) + } +} + +func TestUnicodePolicyCorpus(t *testing.T) { + valueCases := []struct { + name string + value string + want string + }{ + {name: "ascii", value: "alpha", want: "{\"value\":\"alpha\"}\n"}, + {name: "html", value: "<&>", want: "{\"value\":\"<&>\"}\n"}, + {name: "emoji", value: "\U0001f600", want: "{\"value\":\"\U0001f600\"}\n"}, + {name: "combining", value: "e\u0301", want: "{\"value\":\"e\u0301\"}\n"}, + {name: "short_controls", value: "\b\t\n\f\r", want: "{\"value\":\"\\b\\t\\n\\f\\r\"}\n"}, + {name: "c0_nul", value: "\x00", want: "{\"value\":\"\\u0000\"}\n"}, + {name: "del", value: "\x7f", want: "{\"value\":\"\\u007f\"}\n"}, + {name: "c1_next_line", value: "\u0085", want: "{\"value\":\"\\u0085\"}\n"}, + {name: "cf_zero_width_space", value: "\u200b", want: "{\"value\":\"\\u200b\"}\n"}, + {name: "cf_supplementary_language_tag", value: "\U000e0001", want: "{\"value\":\"\\udb40\\udc01\"}\n"}, + {name: "line_separator", value: "\u2028", want: "{\"value\":\"\\u2028\"}\n"}, + {name: "paragraph_separator", value: "\u2029", want: "{\"value\":\"\\u2029\"}\n"}, + } + for _, test := range valueCases { + t.Run(test.name, func(t *testing.T) { + output, err := MarshalLayout(map[string]any{"value": test.value}, LayoutCompact) + if err != nil { + t.Fatalf("MarshalLayout failed: %v", err) + } + if got := string(output); got != test.want { + t.Fatalf("compact output = %q, want %q", got, test.want) + } + var decoded map[string]string + if err := json.Unmarshal(output, &decoded); err != nil { + t.Fatalf("decode output: %v", err) + } + if got := decoded["value"]; got != test.value { + t.Fatalf("decoded value = %q, want %q", got, test.value) + } + }) + } + + keyCases := []struct { + name string + value map[string]any + want string + }{ + {name: "cf_key", value: map[string]any{"\u200b": "value"}, want: "{\"\\u200b\":\"value\"}\n"}, + { + name: "bmp_before_supplementary_scalar_order", + value: map[string]any{"\U00010000": "supplementary", "\ue000": "bmp"}, + want: "{\"\ue000\":\"bmp\",\"\U00010000\":\"supplementary\"}\n", + }, + } + for _, test := range keyCases { + t.Run(test.name, func(t *testing.T) { + output, err := MarshalLayout(test.value, LayoutCompact) + if err != nil { + t.Fatalf("MarshalLayout failed: %v", err) + } + if got := string(output); got != test.want { + t.Fatalf("compact output = %q, want %q", got, test.want) + } + }) + } +} + +func TestMarshalRejectsEveryMalformedUTF8Class(t *testing.T) { + malformed := []struct { + name string + value []byte + }{ + {name: "isolated_continuation", value: []byte{0x80}}, + {name: "invalid_lead", value: []byte{0xff}}, + {name: "overlong", value: []byte{0xc0, 0x80}}, + {name: "truncated", value: []byte{0xe2, 0x82}}, + {name: "surrogate", value: []byte{0xed, 0xa0, 0x80}}, + {name: "out_of_range", value: []byte{0xf4, 0x90, 0x80, 0x80}}, + } + for _, test := range malformed { + t.Run(test.name, func(t *testing.T) { + value := string(test.value) + if _, err := MarshalLayout(map[string]any{"value": value}, LayoutCompact); err == nil { + t.Fatal("MarshalLayout accepted malformed UTF-8 value") + } + if _, err := MarshalLayout(map[string]any{value: true}, LayoutCompact); err == nil { + t.Fatal("MarshalLayout accepted malformed UTF-8 key") + } + }) + } +} diff --git a/internal/kernel/unicodepolicy/unicodepolicy.go b/internal/kernel/unicodepolicy/unicodepolicy.go new file mode 100644 index 0000000..cb98d8c --- /dev/null +++ b/internal/kernel/unicodepolicy/unicodepolicy.go @@ -0,0 +1,80 @@ +package unicodepolicy + +import ( + "errors" + "sort" + "unicode/utf8" +) + +const ( + UnicodeVersion = "15.0.0" + TableSHA256 = "16873769cae138cd0129781932e487ed558510e52a0237dcfcb558081994cfd9" +) + +type scalarRange struct { + category string + start rune + end rune + step rune +} + +var unsafeScalarRanges = [...]scalarRange{ + {category: "Cc", start: 0x000000, end: 0x00001f, step: 1}, + {category: "Cc", start: 0x00007f, end: 0x00009f, step: 1}, + {category: "Cf", start: 0x0000ad, end: 0x000600, step: 1363}, + {category: "Cf", start: 0x000601, end: 0x000605, step: 1}, + {category: "Cf", start: 0x00061c, end: 0x0006dd, step: 193}, + {category: "Cf", start: 0x00070f, end: 0x000890, step: 385}, + {category: "Cf", start: 0x000891, end: 0x0008e2, step: 81}, + {category: "Cf", start: 0x00180e, end: 0x00200b, step: 2045}, + {category: "Cf", start: 0x00200c, end: 0x00200f, step: 1}, + {category: "Zl", start: 0x002028, end: 0x002028, step: 1}, + {category: "Zp", start: 0x002029, end: 0x002029, step: 1}, + {category: "Cf", start: 0x00202a, end: 0x00202e, step: 1}, + {category: "Cf", start: 0x002060, end: 0x002064, step: 1}, + {category: "Cf", start: 0x002066, end: 0x00206f, step: 1}, + {category: "Cf", start: 0x00feff, end: 0x00fff9, step: 250}, + {category: "Cf", start: 0x00fffa, end: 0x00fffb, step: 1}, + {category: "Cf", start: 0x0110bd, end: 0x0110cd, step: 16}, + {category: "Cf", start: 0x013430, end: 0x01343f, step: 1}, + {category: "Cf", start: 0x01bca0, end: 0x01bca3, step: 1}, + {category: "Cf", start: 0x01d173, end: 0x01d17a, step: 1}, + {category: "Cf", start: 0x0e0001, end: 0x0e0020, step: 31}, + {category: "Cf", start: 0x0e0021, end: 0x0e007f, step: 1}, +} + +var ErrInvalidUTF8 = errors.New("value is not valid UTF-8") + +func DecodeUTF8(value []byte) (string, error) { + if !utf8.Valid(value) { + return "", ErrInvalidUTF8 + } + return string(value), nil +} + +func ValidScalarString(value string) bool { + return utf8.ValidString(value) +} + +func IsUnsafeScalar(value rune) bool { + index := sort.Search(len(unsafeScalarRanges), func(index int) bool { + return unsafeScalarRanges[index].end >= value + }) + if index == len(unsafeScalarRanges) { + return false + } + candidate := unsafeScalarRanges[index] + return value >= candidate.start && (value-candidate.start)%candidate.step == 0 +} + +func ContainsUnsafeScalar(value string) bool { + if !ValidScalarString(value) { + return true + } + for _, character := range value { + if IsUnsafeScalar(character) { + return true + } + } + return false +} diff --git a/internal/kernel/unicodepolicy/unicodepolicy_test.go b/internal/kernel/unicodepolicy/unicodepolicy_test.go new file mode 100644 index 0000000..a8b4b8c --- /dev/null +++ b/internal/kernel/unicodepolicy/unicodepolicy_test.go @@ -0,0 +1,92 @@ +package unicodepolicy + +import ( + "crypto/sha256" + "errors" + "fmt" + "slices" + "strings" + "testing" + "unicode" +) + +func TestUnsafeScalarTableIdentityAndCardinality(t *testing.T) { + var source strings.Builder + source.WriteString(UnicodeVersion) + source.WriteByte('\n') + unsafeCount := 0 + entries := unsafeScalarRanges[:] + entries = slices.Clone(entries) + categoryOrder := map[string]int{"Cc": 0, "Cf": 1, "Zl": 2, "Zp": 3} + slices.SortFunc(entries, func(left scalarRange, right scalarRange) int { + if categoryOrder[left.category] != categoryOrder[right.category] { + return categoryOrder[left.category] - categoryOrder[right.category] + } + return int(left.start - right.start) + }) + for _, entry := range entries { + _, _ = fmt.Fprintf(&source, "%s:%06x-%06x/%d\n", entry.category, entry.start, entry.end, entry.step) + unsafeCount += int((entry.end-entry.start)/entry.step) + 1 + } + if got := fmt.Sprintf("%x", sha256.Sum256([]byte(source.String()))); got != TableSHA256 { + t.Fatalf("table digest = %s, want %s", got, TableSHA256) + } + if unsafeCount != 237 { + t.Fatalf("unsafe scalar count = %d, want 237", unsafeCount) + } +} + +func TestUnsafeScalarClassification(t *testing.T) { + for _, value := range []rune{0x0000, 0x007f, 0x0085, 0x00ad, 0x061c, 0x200b, 0x2028, 0x2029, 0xe0001} { + if !IsUnsafeScalar(value) { + t.Fatalf("U+%04X was not classified unsafe", value) + } + } + for _, value := range []rune{' ', 'a', 0x00ae, 0x0606, 0x2027, 0x1f600, 0x10ffff} { + if IsUnsafeScalar(value) { + t.Fatalf("U+%04X was classified unsafe", value) + } + } +} + +func TestUnsafeScalarTableMatchesEveryUnicode15Scalar(t *testing.T) { + if unicode.Version != UnicodeVersion { + t.Fatalf("Go Unicode version = %s, want %s", unicode.Version, UnicodeVersion) + } + for value := rune(0); value <= unicode.MaxRune; value++ { + if value >= 0xd800 && value <= 0xdfff { + continue + } + want := unicode.Is(unicode.Cc, value) || + unicode.Is(unicode.Cf, value) || + unicode.Is(unicode.Zl, value) || + unicode.Is(unicode.Zp, value) + if got := IsUnsafeScalar(value); got != want { + t.Fatalf("IsUnsafeScalar(U+%04X) = %t, want %t", value, got, want) + } + } +} + +func TestValidScalarStringRejectsMalformedUTF8(t *testing.T) { + for _, value := range []string{string([]byte{0xff}), string([]byte{0xc3, 0x28}), string([]byte{0xed, 0xa0, 0x80})} { + if ValidScalarString(value) { + t.Fatalf("malformed UTF-8 was accepted: %x", []byte(value)) + } + } + if !ValidScalarString("e\u0301 \U0001f600") { + t.Fatal("valid scalar string was rejected") + } +} + +func TestDecodeUTF8RejectsMalformedBytesWithoutDisclosure(t *testing.T) { + secret := []byte{'g', 'h', 'p', '_', 0xff} + if _, err := DecodeUTF8(secret); !errors.Is(err, ErrInvalidUTF8) { + t.Fatalf("DecodeUTF8 error = %v, want ErrInvalidUTF8", err) + } else if strings.Contains(err.Error(), "ghp_") { + t.Fatal("DecodeUTF8 disclosed rejected bytes") + } + decoded, err := DecodeUTF8([]byte("e\u0301 \U0001f600")) + if err != nil || decoded != "e\u0301 \U0001f600" { + t.Fatalf("DecodeUTF8 valid value = %q, %v", decoded, err) + } +} diff --git a/internal/testsupport/nodetestselector/selector.go b/internal/testsupport/nodetestselector/selector.go new file mode 100644 index 0000000..d1bf243 --- /dev/null +++ b/internal/testsupport/nodetestselector/selector.go @@ -0,0 +1,86 @@ +package nodetestselector + +import ( + "bytes" + "context" + "errors" + "os/exec" + "regexp" + "strconv" + "strings" +) + +const maximumTAPBytes = 256 << 10 + +var errTAPLimit = errors.New("node test selector output exceeds the byte limit") + +// Run executes one exact top-level Node test and rejects zero-match success. +func Run(ctx context.Context, nodePath string, workdir string, file string, name string) error { + if nodePath == "" || workdir == "" || file == "" || name == "" || strings.ContainsAny(file+name, "\r\n") { + return errors.New("node test selector coordinates are invalid") + } + pattern := "^" + regexp.QuoteMeta(name) + "$" + command := exec.CommandContext(ctx, nodePath, "--test", "--test-reporter=tap", "--test-name-pattern", pattern, file) + command.Dir = workdir + var stdout boundedBuffer + var stderr boundedBuffer + command.Stdout = &stdout + command.Stderr = &stderr + if err := command.Run(); err != nil { + return errors.New("node test selector execution failed") + } + return admitTAP(stdout.String(), name) +} + +func admitTAP(value string, name string) error { + if value == "" || strings.Contains(value, "\r") || strings.ContainsAny(name, "\r\n") { + return errors.New("node test selector emitted invalid TAP") + } + subtestLine := "# Subtest: " + name + selectedSubtests := 0 + selectedPasses := 0 + summary := map[string]int{} + for _, line := range strings.Split(strings.TrimSuffix(value, "\n"), "\n") { + switch { + case strings.HasPrefix(line, "# Subtest: "): + if line == subtestLine { + selectedSubtests++ + } + case strings.HasPrefix(line, "ok "): + parts := strings.SplitN(line, " - ", 2) + if len(parts) == 2 && parts[1] == name { + selectedPasses++ + } + case strings.HasPrefix(line, "not ok "): + return errors.New("node test selector reported a failure") + case strings.HasPrefix(line, "# "): + parts := strings.Fields(line) + if len(parts) != 3 { + continue + } + count, err := strconv.Atoi(parts[2]) + if err == nil { + summary[parts[1]] = count + } + } + } + if selectedSubtests != 1 || selectedPasses != 1 || summary["tests"] != 1 || summary["pass"] != 1 || summary["fail"] != 0 || summary["cancelled"] != 0 || summary["skipped"] != 0 || summary["todo"] != 0 { + return errors.New("node test selector did not execute exactly one passing test") + } + return nil +} + +type boundedBuffer struct { + buffer bytes.Buffer +} + +func (buffer *boundedBuffer) Write(value []byte) (int, error) { + if buffer.buffer.Len()+len(value) > maximumTAPBytes { + return 0, errTAPLimit + } + return buffer.buffer.Write(value) +} + +func (buffer *boundedBuffer) String() string { + return buffer.buffer.String() +} diff --git a/internal/testsupport/nodetestselector/selector_test.go b/internal/testsupport/nodetestselector/selector_test.go new file mode 100644 index 0000000..c6f275a --- /dev/null +++ b/internal/testsupport/nodetestselector/selector_test.go @@ -0,0 +1,35 @@ +package nodetestselector + +import ( + "strings" + "testing" +) + +const selectedTestName = "selected test" + +func TestAdmitTAPRequiresExactlyOneSelectedPass(t *testing.T) { + valid := "TAP version 13\n# Subtest: selected test\nok 1 - selected test\n1..1\n# tests 1\n# pass 1\n# fail 0\n# cancelled 0\n# skipped 0\n# todo 0\n" + if err := admitTAP(valid, selectedTestName); err != nil { + t.Fatalf("admitTAP(valid) error = %v", err) + } + for _, mutant := range []string{ + "TAP version 13\n1..0\n# Subtest: fixture.mjs\nok 1 - fixture.mjs\n1..1\n# tests 1\n# pass 1\n# fail 0\n# cancelled 0\n# skipped 0\n# todo 0\n", + strings.Replace(valid, "ok 1", "not ok 1", 1), + strings.Replace(valid, "# tests 1", "# tests 2", 1), + valid + "# Subtest: selected test\nok 2 - selected test\n", + } { + if err := admitTAP(mutant, selectedTestName); err == nil { + t.Fatalf("admitTAP accepted mutant %q", mutant) + } + } +} + +func TestBoundedBufferRejectsOneByteOverLimit(t *testing.T) { + var buffer boundedBuffer + if _, err := buffer.Write(make([]byte, maximumTAPBytes)); err != nil { + t.Fatalf("boundary write failed: %v", err) + } + if _, err := buffer.Write([]byte{'x'}); err == nil { + t.Fatal("one-byte-over write succeeded") + } +} diff --git a/internal/tools/browserproofverify/main.go b/internal/tools/browserproofverify/main.go index 0ea5c86..e81d3ad 100644 --- a/internal/tools/browserproofverify/main.go +++ b/internal/tools/browserproofverify/main.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "io" "os" "os/exec" "path/filepath" @@ -17,6 +18,7 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" "github.com/research-engineering/agentic-proofkit/internal/kernel/secretjson" "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" + "github.com/research-engineering/agentic-proofkit/internal/kernel/unicodepolicy" ) const proofPath = "artifacts/proofkit/browser-runtime-proof.json" @@ -421,10 +423,18 @@ func gitOutput(root string, args ...string) (string, error) { if err != nil { return "", err } - return strings.TrimSpace(string(output)), nil + decoded, err := unicodepolicy.DecodeUTF8(output) + if err != nil { + return "", fmt.Errorf("git output is not valid UTF-8") + } + return strings.TrimSpace(decoded), nil } func exit(err error) { - fmt.Fprintln(os.Stderr, "browser runtime proof verification failed:", admit.RedactStructuralText(err.Error())) + writeBrowserProofFailure(os.Stderr, err) os.Exit(1) } + +func writeBrowserProofFailure(writer io.Writer, err error) { + _, _ = fmt.Fprintln(writer, "browser runtime proof verification failed:", admit.RedactStructuralText(err.Error())) +} diff --git a/internal/tools/browserproofverify/main_test.go b/internal/tools/browserproofverify/main_test.go index d4ab75f..fc51cc7 100644 --- a/internal/tools/browserproofverify/main_test.go +++ b/internal/tools/browserproofverify/main_test.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "errors" "os" "path/filepath" "slices" @@ -16,6 +17,21 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" ) +func TestDiagnosticsDoNotLeakArtifactValues(t *testing.T) { + for _, value := range []string{ + "artifact api_key=abc123456789 failed", + "artifact line\nbreak failed", + "artifact unsafe\u200bvalue failed", + string([]byte{'p', 'a', 't', 'h', 0xff}), + } { + var output bytes.Buffer + writeBrowserProofFailure(&output, errors.New(value)) + if got, want := output.String(), "browser runtime proof verification failed: \n"; got != want { + t.Fatalf("visible browser proof diagnostic = %q, want %q", got, want) + } + } +} + func TestInputManifestClosesGoDependenciesAndWitnessPolicy(t *testing.T) { root, err := repositoryRoot() if err != nil { diff --git a/internal/tools/commandcontractgen/main.go b/internal/tools/commandcontractgen/main.go index 37f97c1..11dadbd 100644 --- a/internal/tools/commandcontractgen/main.go +++ b/internal/tools/commandcontractgen/main.go @@ -23,6 +23,7 @@ import ( "strings" "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" ) const ( @@ -877,7 +878,11 @@ func renderPresets(sourceDigest string, presets []string) ([]byte, error) { } func canonicalJSON(value any) ([]byte, error) { - return json.Marshal(value) + encoded, err := stablejson.MarshalLayout(value, stablejson.LayoutCompact) + if err != nil { + return nil, err + } + return bytes.TrimSuffix(encoded, []byte{'\n'}), nil } func sha256Digest(content []byte) string { diff --git a/internal/tools/commandcontractgen/main_test.go b/internal/tools/commandcontractgen/main_test.go index c843344..a7c81de 100644 --- a/internal/tools/commandcontractgen/main_test.go +++ b/internal/tools/commandcontractgen/main_test.go @@ -738,7 +738,10 @@ func refreshDefinitionDigests(contract map[string]any) { for _, raw := range contract["contractDefinitions"].([]any) { definition := raw.(map[string]any) delete(definition, "canonicalDigest") - encoded, _ := canonicalJSON(definition) + encoded, err := canonicalJSON(definition) + if err != nil { + panic(err) + } sum := sha256.Sum256(encoded) definition["canonicalDigest"] = "sha256:" + hex.EncodeToString(sum[:]) } @@ -751,7 +754,9 @@ func readFixtureContract(t *testing.T, root string) map[string]any { t.Fatal(err) } var value map[string]any - if err := json.Unmarshal(content, &value); err != nil { + decoder := json.NewDecoder(bytes.NewReader(content)) + decoder.UseNumber() + if err := decoder.Decode(&value); err != nil { t.Fatal(err) } return value diff --git a/internal/tools/commandoracle/corpus.go b/internal/tools/commandoracle/corpus.go index 1f021d3..97b7b53 100644 --- a/internal/tools/commandoracle/corpus.go +++ b/internal/tools/commandoracle/corpus.go @@ -1,6 +1,7 @@ package commandoracle import ( + "bytes" "crypto/sha256" "encoding/hex" "encoding/json" @@ -98,7 +99,7 @@ func ValidateCounterfeitCorpus(root string) (string, error) { } func admitCorpus(content []byte) (counterfeitCorpus, error) { - raw, err := admission.DecodeJSON(strings.NewReader(string(content)), maxCorpusBytes) + raw, err := admission.DecodeJSON(bytes.NewReader(content), maxCorpusBytes) if err != nil { return counterfeitCorpus{}, decision("corpus.json_invalid") } diff --git a/internal/tools/commandoracle/events.go b/internal/tools/commandoracle/events.go index 92b2889..4ab176c 100644 --- a/internal/tools/commandoracle/events.go +++ b/internal/tools/commandoracle/events.go @@ -2,6 +2,7 @@ package commandoracle import ( "bufio" + "bytes" "encoding/json" "io" "sort" @@ -116,7 +117,7 @@ func parseEvents(reader io.Reader, ledger *eventLedger) error { } func admitEvent(line []byte) (testEvent, error) { - raw, err := admission.DecodeJSON(strings.NewReader(string(line)), maxEventLineBytes) + raw, err := admission.DecodeJSON(bytes.NewReader(line), maxEventLineBytes) if err != nil { return testEvent{}, decision("event.json_invalid") } diff --git a/internal/tools/coveragemetrics/main.go b/internal/tools/coveragemetrics/main.go index 708e935..91741f0 100644 --- a/internal/tools/coveragemetrics/main.go +++ b/internal/tools/coveragemetrics/main.go @@ -682,9 +682,9 @@ func activePackageFunctions(activeFiles map[string]struct{}, packageName string) func activeGoTestFiles(root, packagePath string) (map[string]struct{}, error) { command := exec.Command("go", "list", "-json", packagePath) command.Dir = root - output, err := command.CombinedOutput() + output, err := command.Output() if err != nil { - return nil, fmt.Errorf("go list %s: %w: %s", packagePath, err, strings.TrimSpace(string(output))) + return nil, fmt.Errorf("go list %s: %w", packagePath, err) } var listed struct { Dir string diff --git a/internal/tools/mermaidcheck/main.go b/internal/tools/mermaidcheck/main.go index 71e4b88..6c1f0ce 100644 --- a/internal/tools/mermaidcheck/main.go +++ b/internal/tools/mermaidcheck/main.go @@ -11,6 +11,8 @@ import ( "regexp" "sort" "strings" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/unicodepolicy" ) var ( @@ -81,10 +83,14 @@ func markdownFilesFromGitAt(dir string) ([]string, error) { if err != nil { return nil, fmt.Errorf("list candidate Markdown files: %w", err) } + decoded, err := unicodepolicy.DecodeUTF8(out) + if err != nil { + return nil, fmt.Errorf("candidate Markdown file inventory is not valid UTF-8") + } var files []string seen := make(map[string]struct{}) - for _, path := range strings.Split(string(out), "\x00") { + for _, path := range strings.Split(decoded, "\x00") { if path == "" { continue } diff --git a/internal/tools/npmregistry/main.go b/internal/tools/npmregistry/main.go index 56b61fe..b947e54 100644 --- a/internal/tools/npmregistry/main.go +++ b/internal/tools/npmregistry/main.go @@ -12,6 +12,7 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" "github.com/research-engineering/agentic-proofkit/internal/kernel/releasechannel" "github.com/research-engineering/agentic-proofkit/internal/kernel/trustedpublisher" + "github.com/research-engineering/agentic-proofkit/internal/kernel/unicodepolicy" ) const ( @@ -66,7 +67,11 @@ func run(root string) error { if len(modeBytes) > 64 { return fmt.Errorf("npm publication mode exceeds byte limit") } - mode, err := trustedpublisher.AdmitPublicationMode(strings.TrimSpace(string(modeBytes)), "npm registry evidence publicationMode") + modeText, err := unicodepolicy.DecodeUTF8(modeBytes) + if err != nil { + return fmt.Errorf("npm publication mode is not valid UTF-8") + } + mode, err := trustedpublisher.AdmitPublicationMode(strings.TrimSpace(modeText), "npm registry evidence publicationMode") if err != nil { return err } diff --git a/internal/tools/packageverify/main.go b/internal/tools/packageverify/main.go index 3099926..b7526fe 100644 --- a/internal/tools/packageverify/main.go +++ b/internal/tools/packageverify/main.go @@ -23,8 +23,11 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/command/jsonreportcliadaptersource" "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" "github.com/research-engineering/agentic-proofkit/internal/kernel/releaseplatform" + "github.com/research-engineering/agentic-proofkit/internal/kernel/unicodepolicy" + "github.com/research-engineering/agentic-proofkit/internal/tools/workflowsmoke" ) const rootPackageName = "@research-engineering/agentic-proofkit" @@ -172,11 +175,15 @@ type jsonAdapterSourceSmokeSummary struct { func main() { if err := runVerifier(); err != nil { - _, _ = fmt.Fprintln(os.Stderr, err.Error()) + writeVerificationFailure(os.Stderr, err) os.Exit(1) } } +func writeVerificationFailure(writer io.Writer, err error) { + _, _ = fmt.Fprintln(writer, admit.RedactStructuralText(err.Error())) +} + func runVerifier() error { records, err := readPackRecords(filepath.Join("artifacts", "package", "npm-pack.json")) if err != nil { @@ -548,6 +555,8 @@ func allowedRootEntry(path string) bool { "package/proofkit/receipt-producer-policy.json": {}, "package/proofkit/requirement-bindings.json": {}, "package/proofkit/witness-plan.json": {}, + "package/docs/specs/proofkit-agent-workflow/overview.md": {}, + "package/docs/specs/proofkit-agent-workflow/requirements.v1.json": {}, "package/docs/specs/proofkit-consumer-infra-retirement/overview.md": {}, "package/docs/specs/proofkit-consumer-infra-retirement/requirements.v1.json": {}, "package/docs/specs/proofkit-package-boundary/overview.md": {}, @@ -804,7 +813,11 @@ func readTarTextEntriesFromGzip(gzipReader io.Reader) (map[string]string, error) if err != nil { return nil, err } - entries[header.Name] = string(content) + decoded, err := unicodepolicy.DecodeUTF8(content) + if err != nil { + return nil, fmt.Errorf("package text entry is not valid UTF-8") + } + entries[header.Name] = decoded } return entries, nil } @@ -1776,9 +1789,13 @@ func verifyInstalledOnboardingTrace(consumer string, execute installedCommandOpe func installedRootHelpFamilyArgv(content []byte) ([]string, error) { const routeFragment = "agentic-proofkit help families" + decoded, err := unicodepolicy.DecodeUTF8(content) + if err != nil { + return nil, fmt.Errorf("installed README is not valid UTF-8") + } var command string matchCount := 0 - for _, line := range strings.Split(string(content), "\n") { + for _, line := range strings.Split(decoded, "\n") { if !strings.Contains(line, routeFragment) { continue } @@ -1895,7 +1912,11 @@ func parseInstalledLeafHelpRoutes(help string) ([]installedHelpRoute, error) { } func requireInstalledInvocationSyntax(content []byte, command string) error { - lines := strings.Split(string(content), "\n") + decoded, err := unicodepolicy.DecodeUTF8(content) + if err != nil { + return fmt.Errorf("installed README is not valid UTF-8") + } + lines := strings.Split(decoded, "\n") var usage string var installed string usageCount := 0 @@ -1976,7 +1997,11 @@ func installedREADMEPath(content []byte) (string, error) { const expected = "node_modules/@research-engineering/agentic-proofkit/README.md" var discovered string matchCount := 0 - for _, line := range strings.Split(string(content), "\n") { + decoded, err := unicodepolicy.DecodeUTF8(content) + if err != nil { + return "", fmt.Errorf("installed README is not valid UTF-8") + } + for _, line := range strings.Split(decoded, "\n") { line = strings.TrimLeft(line, " \t") if !strings.HasPrefix(line, prefix) { continue @@ -2167,7 +2192,10 @@ func installedContractPresetIDs(content []byte) ([]string, error) { } func installedREADMEFirstInput(content []byte) ([]string, []byte, error) { - text := string(content) + text, err := unicodepolicy.DecodeUTF8(content) + if err != nil { + return nil, nil, fmt.Errorf("installed README is not valid UTF-8") + } const startMarker = "" const endMarker = "" if strings.Count(text, startMarker) != 1 || strings.Count(text, endMarker) != 1 { @@ -2386,6 +2414,12 @@ func verifyInstalledJSONABI(consumer string) error { if err := verifyJSONAdapterSourceSmoke(consumer); err != nil { return err } + if err := workflowsmoke.Verify(func(input []byte, args ...string) (workflowsmoke.Result, error) { + result, err := runInstalledWithInput(consumer, input, args...) + return workflowsmoke.Result{ExitCode: result.ExitCode, Stdout: result.Stdout, Stderr: result.Stderr}, err + }); err != nil { + return fmt.Errorf("outside consumer agent-workflow smoke failed: %w", err) + } return nil } @@ -2423,8 +2457,8 @@ func verifyJSONAdapterSourceSmokeReport(result installedCommandResult, expectedS if report.SourceFileName != "proofkit-json-report-cli-adapter.ts" { return fmt.Errorf("sourceFileName=%s, want proofkit-json-report-cli-adapter.ts", report.SourceFileName) } - if report.GeneratorID != "proofkit.json-report-cli-adapter-source.typescript.v1" { - return fmt.Errorf("generatorId=%s, want proofkit.json-report-cli-adapter-source.typescript.v1", report.GeneratorID) + if report.GeneratorID != "proofkit.json-report-cli-adapter-source.typescript.v2" { + return fmt.Errorf("generatorId=%s, want proofkit.json-report-cli-adapter-source.typescript.v2", report.GeneratorID) } if report.Source != expectedSource { return fmt.Errorf("json adapter source does not match current owner source") @@ -2553,7 +2587,7 @@ func verifyOutsideConsumerImports(consumer string) error { import { createRequire } from "node:module"; import { readFileSync } from "node:fs"; const require = createRequire(import.meta.url); -const payload = JSON.parse(readFileSync("proofkit-import-boundary.json", "utf8")); +const payload = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(readFileSync("proofkit-import-boundary.json"))); const manifest = require(payload.manifestSpecifier); if (manifest.name !== payload.expectedManifestName) { throw new Error("package.json export did not resolve package manifest"); diff --git a/internal/tools/packageverify/main_test.go b/internal/tools/packageverify/main_test.go index c3c53fb..cd259a8 100644 --- a/internal/tools/packageverify/main_test.go +++ b/internal/tools/packageverify/main_test.go @@ -2,12 +2,14 @@ package main import ( "archive/tar" + "bytes" "compress/gzip" "crypto/sha1" "crypto/sha512" "encoding/base64" "encoding/hex" "encoding/json" + "errors" "os" "os/exec" "path/filepath" @@ -299,6 +301,21 @@ func TestReadManifestFromTarRejectsUnknownPackageManifestFields(t *testing.T) { } } +func TestPackageVerifierDiagnosticNondisclosure(t *testing.T) { + for _, value := range []string{ + "package verification failed for api_key=abc123456789", + "package verification failed for line\nbreak", + "package verification failed for unsafe\u200bvalue", + string([]byte{'p', 'a', 't', 'h', 0xff}), + } { + var output bytes.Buffer + writeVerificationFailure(&output, errors.New(value)) + if got, want := output.String(), "\n"; got != want { + t.Fatalf("visible package diagnostic = %q, want %q", got, want) + } + } +} + func TestVerifyRootManifestBoundaryRejectsLifecycleScripts(t *testing.T) { root := t.TempDir() withWorkingDirectory(t, root) @@ -1592,7 +1609,7 @@ export function runProofkitNoInputJsonCommand(): void { } func jsonAdapterSmokeStdout(source string, sourceHash string, artifactKind string) []byte { - return []byte(`{"schemaVersion":1,"artifactKind":` + quotedJSON(artifactKind) + `,"format":"json","generatorId":"proofkit.json-report-cli-adapter-source.typescript.v1","language":"typescript","source":` + quotedJSON(source) + `,"sourceFileName":"proofkit-json-report-cli-adapter.ts","sourceSha256":` + quotedJSON(sourceHash) + `,"summary":{"exportedSymbolCount":24,"lineCount":600}}`) + return []byte(`{"schemaVersion":1,"artifactKind":` + quotedJSON(artifactKind) + `,"format":"json","generatorId":"proofkit.json-report-cli-adapter-source.typescript.v2","language":"typescript","source":` + quotedJSON(source) + `,"sourceFileName":"proofkit-json-report-cli-adapter.ts","sourceSha256":` + quotedJSON(sourceHash) + `,"summary":{"exportedSymbolCount":24,"lineCount":600}}`) } func quotedJSON(value string) string { diff --git a/internal/tools/pypiregistry/main.go b/internal/tools/pypiregistry/main.go index d2be298..8d1e23c 100644 --- a/internal/tools/pypiregistry/main.go +++ b/internal/tools/pypiregistry/main.go @@ -80,11 +80,10 @@ type pypiFile struct { type pypiHTTPStatusError struct { StatusCode int Status string - Body string } func (err pypiHTTPStatusError) Error() string { - return fmt.Sprintf("pypi returned %s: %s", err.Status, err.Body) + return fmt.Sprintf("pypi returned %s", err.Status) } type pypiMissingWheelError struct { @@ -238,11 +237,10 @@ func fetchPyPIRelease(name string, version string) (pypiResponse, error) { } defer response.Body.Close() if response.StatusCode != http.StatusOK { - body, _ := io.ReadAll(io.LimitReader(response.Body, 4096)) + _, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 4096)) return pypiResponse{}, pypiHTTPStatusError{ StatusCode: response.StatusCode, Status: response.Status, - Body: strings.TrimSpace(string(body)), } } out, err := decodePyPIResponse(response.Body) diff --git a/internal/tools/pythonpackage/verify.go b/internal/tools/pythonpackage/verify.go index 5309f12..a74091d 100644 --- a/internal/tools/pythonpackage/verify.go +++ b/internal/tools/pythonpackage/verify.go @@ -9,6 +9,7 @@ import ( "encoding/base64" "encoding/csv" "encoding/json" + "errors" "fmt" "io" "os" @@ -21,6 +22,8 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" + "github.com/research-engineering/agentic-proofkit/internal/kernel/unicodepolicy" + "github.com/research-engineering/agentic-proofkit/internal/tools/workflowsmoke" ) const ( @@ -445,9 +448,38 @@ 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, venvPython, "-m", "agentic_proofkit"); err != nil { + return fmt.Errorf("python module agent-workflow smoke failed: %w", err) + } + if err := verifyInstalledWorkflowSmoke(consumer, binPath); err != nil { + return fmt.Errorf("python console script agent-workflow smoke failed: %w", err) + } return verifyInstalledPythonPresetContinuation(consumer, venvPython) } +func verifyInstalledWorkflowSmoke(dir string, executable string, prefix ...string) error { + return workflowsmoke.Verify(func(input []byte, args ...string) (workflowsmoke.Result, error) { + argv := append(append([]string(nil), prefix...), args...) + command := exec.Command(executable, argv...) + command.Dir = dir + command.Stdin = bytes.NewReader(input) + var stdout bytes.Buffer + var stderr bytes.Buffer + command.Stdout = &stdout + command.Stderr = &stderr + err := command.Run() + exitCode := 0 + if err != nil { + var exitErr *exec.ExitError + if !errors.As(err, &exitErr) { + return workflowsmoke.Result{}, err + } + exitCode = exitErr.ExitCode() + } + return workflowsmoke.Result{ExitCode: exitCode, Stdout: stdout.Bytes(), Stderr: stderr.Bytes()}, nil + }) +} + func verifyInstalledPythonPresetContinuation(consumer string, venvPython string) error { emptyPath := filepath.Join(consumer, "empty-path") if err := os.Mkdir(emptyPath, 0o700); err != nil && !os.IsExist(err) { @@ -620,9 +652,13 @@ func verifyInstalledPythonHelpAndAgentRouteContinuity(consumer string, environme } func exactDisplayedRouteOperands(output []byte, prefix string, context string) ([]string, error) { + decoded, err := unicodepolicy.DecodeUTF8(output) + if err != nil { + return nil, fmt.Errorf("installed Python wheel route output is not valid UTF-8") + } operands := []string{} seen := map[string]struct{}{} - for _, line := range strings.Split(strings.TrimSuffix(string(output), "\n"), "\n") { + for _, line := range strings.Split(strings.TrimSuffix(decoded, "\n"), "\n") { route, admitted := strings.CutPrefix(line, " ") if !admitted { continue diff --git a/internal/tools/releasechange/record_test.go b/internal/tools/releasechange/record_test.go index 6961cde..c7e8ec6 100644 --- a/internal/tools/releasechange/record_test.go +++ b/internal/tools/releasechange/record_test.go @@ -195,22 +195,18 @@ func TestCurrentChangeRecordNamesReviewedSemanticChanges(t *testing.T) { } var currentBreakingChanges = []Change{ - {ChangeID: "proofkit.compact.consumer-contract-v2", Summary: "Compact-aware adoption, pilot, conformance, impact, proof-view, coverage, inventory, context composition, context slicing, semantic diff, traceability graph, browser, source-set, and resolver command contracts advance atomically and reject legacy compact payloads."}, - {ChangeID: "proofkit.compact.declaration-contract-v2", Summary: "Compact requirement proof inputs now use declaration-only v2 discriminators, rename mutation metadata as a caller claim, and remove assurance-shaped proof state and synthetic checked or finding counts."}, - {ChangeID: "proofkit.compact.identity-role-closure", Summary: "Compact bindings now preserve requirement, surface, and scenario identity while witness routes preserve binding, role, selector, and JSON-safe resolution order across every decision-bearing projection."}, - {ChangeID: "proofkit.compact.source-set-v2", Summary: "Compact source-set normalization advances to wrapper, envelope, and source-set schema v2 with declaration-specific roles and fragment contract v3."}, + {ChangeID: "proofkit.agent-envelope.local-identity-closure", Summary: "Agent envelopes now require unique, pairwise-disjoint command, context, and receipt identities and fail closed by pruning unsafe or ambiguous local targets and references with bounded omissions."}, + {ChangeID: "proofkit.stable-json.unicode-scalar-v2", Summary: "Go, JavaScript, and generated TypeScript stable JSON now reject non-scalar strings and deterministically escape the pinned Unicode 15 control and format policy; the generated TypeScript adapter identity advances to v2."}, } var currentAdditions = []Change{ - {ChangeID: "proofkit.requirement-source.typed-model", Summary: "Add a bounded representation-neutral internal typed model for requirement-source v2 experiments without adding a source codec, persistence format, public CLI, or source cutover."}, + {ChangeID: "proofkit.agent-workflow.change-planner", Summary: "Add a bounded change-workflow-plan command with canonical JSON, derived agent-envelope, plain-text, and capability-gated terminal-color projections."}, + {ChangeID: "proofkit.agent-workflow.native-evidence-guidance", Summary: "Add a no-input native-evidence-guidance command that exposes the repository-owned decision template as canonical JSON or plain text without executing or approving consumer witnesses."}, } var currentMigrationSteps = []string{ - "Replace compact root schema_version 1, authority_state canonical, contract_kind requirement_proof_binding, and normalization_profile proofkit.compact.v1 with schema_version 2, authority_state caller_owned_declaration, contract_kind requirement_proof_route_declaration, and normalization_profile proofkit.compact.declaration.v2.", - "Remove proof_contract_state; replace mutation_resistance_state with declared_mutation_resistance_claim_id; and stop consuming proofContractState, mutationResistanceContext, checkedWitnessSelectorCount, and findingCount from compact resolver projections.", - "Preserve each compact binding as requirementId, surfaceId, and scenarioId plus bindingRecordId, and preserve each witness route as bindingRecordId, role, selector, JSON-safe resolutionOrderIndex from 0 through 9007199254740991, and witnessRouteId; do not collapse equal selectors across positive and falsification roles.", - "Replace changedRecordIds with changedRequirementIds, replace resolver projectionKind proofkit.requirement-proof-resolver with proofkit.requirement-proof-route-resolver, and consume compact-aware adoption, pilot, conformance, impact, proof-view, coverage, inventory, context-compose, context-slice, semantic-diff, traceability-graph, browser, source-set, and resolver parent contracts at their 0.4.0 versions.", - "Regenerate requirement proof source sets with wrapper, canonical envelope, canonical contract, and source-set schema version 2, declaration-specific source roles, and fragment contract requirement-proof-route-declarations/fragment/v3.", + "Ensure every agent-envelope command, context, and receipt ID is individually safe and unique, keep the three local identity domains pairwise disjoint, and remove or rename references to ambiguous local targets before adopting 0.5.0.", + "Ensure every stable-JSON string is a Unicode scalar sequence, accept deterministic escaping of pinned Unicode 15 control and format code points, and regenerate TypeScript adapters against generator proofkit.json-report-cli-adapter-source.typescript.v2.", } func validateCurrentChangeRecord(record Record, notes string) error { @@ -231,7 +227,7 @@ func validateCurrentChangeRecord(record Record, notes string) error { func currentExpectedReleaseNotes() string { lines := []string{ - "# @research-engineering/agentic-proofkit 0.4.0", + "# @research-engineering/agentic-proofkit 0.5.0", "", "## Breaking Contract Changes", "", @@ -264,6 +260,7 @@ func currentExpectedReleaseNotes() string { "", "## Known Limitations", "", + "- 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.", "- Complete nested public structural contracts remain blocked under SCHEMA-01; current CLI contracts own exact root variants only.", "- The requirement-source v2 codec remains unselected until SOURCE-CODEC-01 passes; the typed model is internal and no source cutover is claimed.", "- TSX source parsing remains unsupported.", @@ -273,7 +270,7 @@ func currentExpectedReleaseNotes() string { "Primary npm channel:", "", "```bash", - "npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.4.0", + "npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.5.0", "```", "", "Pre-1.0 npm consumers must keep this dependency exact-pinned.", @@ -284,7 +281,7 @@ func currentExpectedReleaseNotes() string { "", "## Rollback", "", - "- Pin npm consumers to the previous admitted version 0.3.0 with `npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.3.0`.", + "- Pin npm consumers to the previous admitted version 0.4.0 with `npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.4.0`.", "- Treat local package artifacts as candidates until registry identity is proven.", ) return strings.Join(lines, "\n") + "\n" diff --git a/internal/tools/releasecloseoutinput/main.go b/internal/tools/releasecloseoutinput/main.go index 2f9eb17..fc43ef3 100644 --- a/internal/tools/releasecloseoutinput/main.go +++ b/internal/tools/releasecloseoutinput/main.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "context" "crypto/sha1" "crypto/sha256" @@ -1676,7 +1677,7 @@ func checksumFileMatches(root string, checksumPath string, targetPaths []string) if err != nil { return false } - return string(content) == strings.Join(expected, "\n")+"\n" + return bytes.Equal(content, []byte(strings.Join(expected, "\n")+"\n")) } func checksumLines(root string, targetPaths []string) ([]string, error) { @@ -1729,7 +1730,7 @@ func releaseNotesMatchChangeRecord(root string, path string, manifest packageJSO } pypiPublished := fileExists(root, "artifacts/pypi-registry/pypi-release.json") expected := releasechange.RenderMarkdown(record, manifest.Name, pythonPackageName, pypiPublished) - return string(content) == expected + return bytes.Equal(content, []byte(expected)) } func hasChannelStatus(manifest releaseManifest, authority string, allowed ...string) bool { diff --git a/internal/tools/releasemanifest/main.go b/internal/tools/releasemanifest/main.go index 5a8213d..1101f66 100644 --- a/internal/tools/releasemanifest/main.go +++ b/internal/tools/releasemanifest/main.go @@ -17,6 +17,7 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/kernel/releasechannel" "github.com/research-engineering/agentic-proofkit/internal/kernel/releasepublisher" "github.com/research-engineering/agentic-proofkit/internal/kernel/trustedpublisher" + "github.com/research-engineering/agentic-proofkit/internal/kernel/unicodepolicy" "github.com/research-engineering/agentic-proofkit/internal/tools/releasechange" ) @@ -281,6 +282,14 @@ func run() error { if err != nil { return err } + nodeVersion, err := optionalCommandOutput("node", "--version") + if err != nil { + return err + } + npmVersion, err := optionalCommandOutput("npm", "--version") + if err != nil { + return err + } releaseDir := filepath.Join("artifacts", "release") if err := os.MkdirAll(releaseDir, 0o755); err != nil { return err @@ -316,8 +325,8 @@ func run() error { }, Toolchain: toolchainIdentity{ Go: goVersion, - Node: optionalCommandOutput("node", "--version"), - Npm: optionalCommandOutput("npm", "--version"), + Node: nodeVersion, + Npm: npmVersion, }, Channels: releaseChannels(localRecords, registryRecords, npmPublicationMode, pythonPackages, pypiRegistry, pypiPublicationMode, assets, trustedPublishers), LocalPackEvidence: packageEvidenceSet(localRecords, assetShaByFilename(assets)), @@ -393,7 +402,11 @@ func optionalPublicationMode(path string, label string) (string, error) { } return "", err } - mode := strings.TrimSpace(string(content)) + mode, err := unicodepolicy.DecodeUTF8(content) + if err != nil { + return "", fmt.Errorf("publication mode is not valid UTF-8") + } + mode = strings.TrimSpace(mode) mode, err = trustedpublisher.AdmitPublicationMode(mode, fmt.Sprintf("%s publication mode", label)) if err != nil { return "", err @@ -1086,7 +1099,11 @@ func goModule() (string, error) { if err != nil { return "", err } - for _, line := range strings.Split(string(content), "\n") { + decoded, err := unicodepolicy.DecodeUTF8(content) + if err != nil { + return "", fmt.Errorf("go.mod is not valid UTF-8") + } + for _, line := range strings.Split(decoded, "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "module ") { moduleName := strings.TrimSpace(strings.TrimPrefix(line, "module ")) @@ -1104,13 +1121,21 @@ func commandOutput(name string, args ...string) (string, error) { if err != nil { return "", fmt.Errorf("%s %s: %w", name, strings.Join(args, " "), err) } - return strings.TrimSpace(string(output)), nil + decoded, err := unicodepolicy.DecodeUTF8(output) + if err != nil { + return "", fmt.Errorf("toolchain command output is not valid UTF-8") + } + return strings.TrimSpace(decoded), nil } -func optionalCommandOutput(name string, args ...string) string { +func optionalCommandOutput(name string, args ...string) (string, error) { output, err := exec.Command(name, args...).Output() if err != nil { - return "" + return "", nil + } + decoded, err := unicodepolicy.DecodeUTF8(output) + if err != nil { + return "", fmt.Errorf("optional toolchain command output is not valid UTF-8") } - return strings.TrimSpace(string(output)) + return strings.TrimSpace(decoded), nil } diff --git a/internal/tools/releasepreflight/main.go b/internal/tools/releasepreflight/main.go index df071dd..0cd9a05 100644 --- a/internal/tools/releasepreflight/main.go +++ b/internal/tools/releasepreflight/main.go @@ -15,6 +15,7 @@ import ( "strings" "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/unicodepolicy" "github.com/research-engineering/agentic-proofkit/internal/tools/releasechange" "github.com/research-engineering/agentic-proofkit/internal/tools/retainedevidence" ) @@ -205,7 +206,11 @@ func run(args []string) error { if err != nil { return fmt.Errorf("read release notes: %w", err) } - if err := validateGitHubReleaseMetadata(release, options["tag"], string(notes)); err != nil { + notesText, err := unicodepolicy.DecodeUTF8(notes) + if err != nil { + return fmt.Errorf("release notes are not valid UTF-8") + } + if err := validateGitHubReleaseMetadata(release, options["tag"], notesText); err != nil { return err } if args[0] == "github-metadata" { @@ -615,8 +620,12 @@ func readLineSet(path string) ([]string, error) { if err != nil { return nil, fmt.Errorf("read %s: %w", path, err) } + decoded, err := unicodepolicy.DecodeUTF8(content) + if err != nil { + return nil, fmt.Errorf("line-set input is not valid UTF-8") + } lines := []string{} - for _, line := range strings.Split(string(content), "\n") { + for _, line := range strings.Split(decoded, "\n") { line = strings.TrimSpace(line) if line != "" { lines = append(lines, line) diff --git a/internal/tools/releasesbom/main.go b/internal/tools/releasesbom/main.go index 263c87c..e1ed257 100644 --- a/internal/tools/releasesbom/main.go +++ b/internal/tools/releasesbom/main.go @@ -188,7 +188,7 @@ func goModuleInventory() ([]goModuleRecord, error) { if err != nil { return nil, fmt.Errorf("go list modules: %w", err) } - decoder := json.NewDecoder(strings.NewReader(string(output))) + decoder := json.NewDecoder(bytes.NewReader(output)) out := []goModuleRecord{} for { var module goModuleRecord diff --git a/internal/tools/textpolicyinput/main.go b/internal/tools/textpolicyinput/main.go index b5e2682..ef68710 100644 --- a/internal/tools/textpolicyinput/main.go +++ b/internal/tools/textpolicyinput/main.go @@ -9,6 +9,8 @@ import ( "path/filepath" "sort" "strings" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/unicodepolicy" ) var binarySuffixes = []string{ @@ -105,7 +107,11 @@ func repoRoot() (string, error) { if err != nil { return "", fmt.Errorf("resolve git repository root: %w", err) } - return strings.TrimSpace(string(output)), nil + decoded, err := unicodepolicy.DecodeUTF8(output) + if err != nil { + return "", fmt.Errorf("git repository root output is not valid UTF-8") + } + return strings.TrimSpace(decoded), nil } func repoFiles(repoRoot string) ([]string, error) { @@ -115,9 +121,13 @@ func repoFiles(repoRoot string) ([]string, error) { if err != nil { return nil, fmt.Errorf("list git files: %w", err) } + decoded, err := unicodepolicy.DecodeUTF8(output) + if err != nil { + return nil, fmt.Errorf("git file inventory is not valid UTF-8") + } seen := map[string]struct{}{} files := []string{} - for _, path := range strings.Split(string(output), "\x00") { + for _, path := range strings.Split(decoded, "\x00") { if path == "" { continue } diff --git a/internal/tools/workflowsmoke/workflow_smoke.go b/internal/tools/workflowsmoke/workflow_smoke.go new file mode 100644 index 0000000..9b37b40 --- /dev/null +++ b/internal/tools/workflowsmoke/workflow_smoke.go @@ -0,0 +1,220 @@ +package workflowsmoke + +import ( + "bytes" + "fmt" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" +) + +const workflowInput = `{"checkpoint":{"state":"not_started"},"completedStageIds":[],"contextRefs":[],"governingAuthorityRefId":null,"requiredContextRefIds":[],"schemaVersion":1}` + +// Result is the observable process contract for one installed CLI invocation. +type Result struct { + ExitCode int + Stdout []byte + Stderr []byte +} + +// Runner invokes one installed carrier with the supplied stdin and argv. +type Runner func(input []byte, args ...string) (Result, error) + +// Verify exercises the public agent-workflow relation through one installed +// CLI carrier. It owns expectations; carrier adapters own process execution. +func Verify(run Runner) error { + input := []byte(workflowInput) + + plan, err := invoke(run, "planner JSON", input, "change-workflow-plan", "--input", "-") + if err != nil { + return err + } + planValue, err := verifyJSONObject(plan, "planner JSON") + if err != nil { + return err + } + if err := requireString(planValue, "reportKind", "proofkit.change-workflow-plan"); err != nil { + return fmt.Errorf("planner JSON: %w", err) + } + if err := requireString(planValue, "reportId", "proofkit.change-workflow-plan"); err != nil { + return fmt.Errorf("planner JSON: %w", err) + } + if err := requireString(planValue, "outputKind", "next_action"); err != nil { + return fmt.Errorf("planner JSON: %w", err) + } + if err := requireString(planValue, "activeStageId", "architecture"); err != nil { + return fmt.Errorf("planner JSON: %w", err) + } + if err := requireString(planValue, "action", "author"); err != nil { + return fmt.Errorf("planner JSON: %w", err) + } + if err := requireString(planValue, "checkpointState", "not_started"); err != nil { + return fmt.Errorf("planner JSON: %w", err) + } + + compact, err := invoke(run, "planner compact JSON", input, "--json-layout", "compact", "change-workflow-plan", "--input", "-") + if err != nil { + return err + } + compactValue, err := verifyJSONObject(compact, "planner compact JSON") + if err != nil { + return err + } + if err := verifyCanonicalCompactJSON(compact.Stdout, compactValue); err != nil { + return fmt.Errorf("planner compact JSON: %w", err) + } + + envelope, err := invoke(run, "planner agent envelope", input, "change-workflow-plan", "--input", "-", "--agent-envelope") + if err != nil { + return err + } + envelopeValue, err := verifyJSONObject(envelope, "planner agent envelope") + if err != nil { + return err + } + if err := requireString(envelopeValue, "envelopeId", "proofkit.change-workflow-plan.agent-envelope"); err != nil { + return fmt.Errorf("planner agent envelope: %w", err) + } + sourceReport, ok := envelopeValue["sourceReport"].(map[string]any) + if !ok { + return fmt.Errorf("planner agent envelope: sourceReport must be an object") + } + for field, want := range map[string]string{"reportId": "proofkit.change-workflow-plan", "reportKind": "proofkit.change-workflow-plan", "state": "passed"} { + if err := requireString(sourceReport, field, want); err != nil { + return fmt.Errorf("planner agent envelope sourceReport: %w", err) + } + } + + text, err := invoke(run, "planner text", input, "change-workflow-plan", "--input", "-", "--format", "text", "--color", "never") + if err != nil { + return err + } + if !bytes.HasPrefix(text.Stdout, []byte("Change workflow plan\n")) || bytes.Contains(text.Stdout, []byte("\x1b[")) { + return fmt.Errorf("planner text must be plain text with the canonical heading") + } + + if err := verifyFailure(run, "required input", nil, "requires --input ", "change-workflow-plan"); err != nil { + return err + } + if err := verifyFailure(run, "pre-read input pointer", nil, "JSON pointer", "change-workflow-plan", "--input", "proofkit-smoke-missing-input.json", "--input-pointer", "invalid"); err != nil { + return err + } + if err := verifyFailure(run, "JSON color denial", input, "--color is valid only with --format text", "change-workflow-plan", "--input", "-", "--color", "never"); err != nil { + return err + } + if err := verifyFailure(run, "exclusive help", input, "help accepts no additional arguments", "change-workflow-plan", "--help", "--format", "text"); err != nil { + return err + } + if err := verifyFailure(run, "surplus positional operand", input, "unsupported argument", "change-workflow-plan", "--input", "-", "surplus"); err != nil { + return err + } + + help, err := invoke(run, "exclusive help success", []byte("must remain unread"), "change-workflow-plan", "--help") + if err != nil { + return err + } + if !bytes.HasPrefix(help.Stdout, []byte("Usage:\n")) || bytes.Contains(help.Stdout, []byte("\x1b[")) { + return fmt.Errorf("exclusive help success must emit plain command help") + } + + guidance, err := invoke(run, "no-input guidance JSON", []byte("must remain unread"), "native-evidence-guidance") + if err != nil { + return err + } + guidanceValue, err := verifyJSONObject(guidance, "no-input guidance JSON") + if err != nil { + return err + } + if err := requireString(guidanceValue, "guidanceId", "proofkit.native-evidence-guidance.v1"); err != nil { + return fmt.Errorf("no-input guidance JSON: %w", err) + } + slots, ok := guidanceValue["slots"].([]any) + if !ok || len(slots) != 22 { + return fmt.Errorf("no-input guidance JSON: slots must contain 22 records") + } + if err := requireStringObject(slots[0], "slotId", "semantic_owner"); err != nil { + return fmt.Errorf("no-input guidance JSON: first slot: %w", err) + } + if err := requireStringObject(slots[len(slots)-1], "slotId", "non_claims"); err != nil { + return fmt.Errorf("no-input guidance JSON: last slot: %w", err) + } + + guidanceText, err := invoke(run, "no-input guidance text", []byte("must remain unread"), "native-evidence-guidance", "--format", "text", "--color", "never") + if err != nil { + return err + } + if !bytes.HasPrefix(guidanceText.Stdout, []byte("semantic_owner: decision:")) || bytes.Contains(guidanceText.Stdout, []byte("\x1b[")) { + return fmt.Errorf("no-input guidance text must be plain text with the canonical first slot") + } + return nil +} + +func invoke(run Runner, label string, input []byte, args ...string) (Result, error) { + result, err := run(input, args...) + if err != nil { + return Result{}, fmt.Errorf("%s carrier invocation: %w", label, err) + } + if result.ExitCode != 0 { + return Result{}, fmt.Errorf("%s exit code=%d, want 0", label, result.ExitCode) + } + if len(result.Stderr) != 0 { + return Result{}, fmt.Errorf("%s stderr must be empty", label) + } + if len(result.Stdout) == 0 { + return Result{}, fmt.Errorf("%s stdout must not be empty", label) + } + return result, nil +} + +func verifyFailure(run Runner, label string, input []byte, diagnosticFragment string, args ...string) error { + result, err := run(input, args...) + if err != nil { + return fmt.Errorf("%s carrier invocation: %w", label, err) + } + if result.ExitCode != 1 || len(result.Stdout) != 0 || len(result.Stderr) == 0 { + return fmt.Errorf("%s must fail with exit 1, empty stdout, and nonempty stderr", label) + } + if bytes.Contains(result.Stderr, []byte("\x1b[")) || !bytes.Contains(result.Stderr, []byte(diagnosticFragment)) { + return fmt.Errorf("%s diagnostic does not contain the required safe class", label) + } + return nil +} + +func verifyJSONObject(result Result, label string) (map[string]any, error) { + value, err := admission.DecodeJSON(bytes.NewReader(result.Stdout), int64(len(result.Stdout))) + if err != nil { + return nil, fmt.Errorf("%s stdout must contain exactly one strict JSON value: %w", label, err) + } + record, ok := value.(map[string]any) + if !ok { + return nil, fmt.Errorf("%s stdout JSON must be an object", label) + } + return record, nil +} + +func verifyCanonicalCompactJSON(output []byte, value any) error { + canonical, err := stablejson.MarshalLayout(value, stablejson.LayoutCompact) + if err != nil { + return err + } + if !bytes.Equal(output, canonical) || bytes.Count(output, []byte{'\n'}) != 1 { + return fmt.Errorf("stdout is not one newline-terminated canonical compact JSON value") + } + return nil +} + +func requireString(record map[string]any, field string, want string) error { + got, ok := record[field].(string) + if !ok || got != want { + return fmt.Errorf("%s=%v, want %q", field, record[field], want) + } + return nil +} + +func requireStringObject(value any, field string, want string) error { + record, ok := value.(map[string]any) + if !ok { + return fmt.Errorf("value must be an object") + } + return requireString(record, field, want) +} diff --git a/internal/tools/workflowsmoke/workflow_smoke_test.go b/internal/tools/workflowsmoke/workflow_smoke_test.go new file mode 100644 index 0000000..489234b --- /dev/null +++ b/internal/tools/workflowsmoke/workflow_smoke_test.go @@ -0,0 +1,68 @@ +package workflowsmoke_test + +import ( + "bytes" + "context" + "strings" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/app" + "github.com/research-engineering/agentic-proofkit/internal/tools/workflowsmoke" +) + +func TestVerifyAcceptsApplicationCLI(t *testing.T) { + if err := workflowsmoke.Verify(applicationRunner); err != nil { + t.Fatal(err) + } +} + +func TestVerifyRejectsCarrierContractMutations(t *testing.T) { + mutations := []struct { + name string + match string + apply func(workflowsmoke.Result) workflowsmoke.Result + }{ + {name: "planner identity", match: "change-workflow-plan --input -", apply: replaceStdout(`{"reportKind":"wrong"}\n`)}, + {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: "text styling", match: "change-workflow-plan --input - --format text --color never", apply: replaceStdout("\x1b[31mChange workflow plan\x1b[0m\n")}, + {name: "failure stdout", match: "change-workflow-plan", apply: func(result workflowsmoke.Result) workflowsmoke.Result { + result.Stdout = []byte("unexpected") + return result + }}, + {name: "guidance slots", match: "native-evidence-guidance", apply: replaceStdout(`{"guidanceId":"proofkit.native-evidence-guidance.v1","slots":[]}\n`)}, + } + for _, mutation := range mutations { + t.Run(mutation.name, func(t *testing.T) { + applied := false + runner := func(input []byte, args ...string) (workflowsmoke.Result, error) { + result, err := applicationRunner(input, args...) + if err == nil && !applied && strings.Join(args, " ") == mutation.match { + result = mutation.apply(result) + applied = true + } + return result, err + } + if err := workflowsmoke.Verify(runner); err == nil { + t.Fatal("carrier mutation was accepted") + } + if !applied { + t.Fatalf("mutation route %q was not exercised", mutation.match) + } + }) + } +} + +func applicationRunner(input []byte, args ...string) (workflowsmoke.Result, error) { + var stdout bytes.Buffer + var stderr bytes.Buffer + exitCode := app.Run(context.Background(), args, bytes.NewReader(input), &stdout, &stderr) + return workflowsmoke.Result{ExitCode: exitCode, Stdout: stdout.Bytes(), Stderr: stderr.Bytes()}, nil +} + +func replaceStdout(value string) func(workflowsmoke.Result) workflowsmoke.Result { + return func(result workflowsmoke.Result) workflowsmoke.Result { + result.Stdout = []byte(value) + return result + } +} diff --git a/package-lock.json b/package-lock.json index 00a704a..cf3da45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@research-engineering/agentic-proofkit", - "version": "0.4.0", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@research-engineering/agentic-proofkit", - "version": "0.4.0", + "version": "0.5.0", "cpu": [ "arm64", "x64" diff --git a/package.json b/package.json index 9e32617..9d29761 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.4.0", + "version": "0.5.0", "type": "module", "license": "MIT", "sideEffects": false, diff --git a/proofkit/cli-contract.v2.json b/proofkit/cli-contract.v2.json index d3cab46..483d7cc 100644 --- a/proofkit/cli-contract.v2.json +++ b/proofkit/cli-contract.v2.json @@ -28,6 +28,7 @@ "--help", "-h" ], + "commandHelpExclusive": true, "helpCommandPositionalTarget": "optional_supported_command", "helpCatalogFormsSource": "proofkit/command-families.v1.json", "helpReadsCommandInput": false @@ -206,7 +207,7 @@ "rootDefinitionDigest": "sha256:104d1e8bef615582ead1c7c8c64b7e1643a76c2bc2a1f224189c372cf8cc4aa1", "nativeSource": { "path": "internal/command/adoptiondoctor", - "canonicalDigest": "sha256:cec92b209d2fa36cf5da43a7049d3c24212f6762a085b065e75b46f1c5fda625", + "canonicalDigest": "sha256:6ec8b4f324c102c1059c051f99209c91b4f7fe5a7218a03d8717c2791b8dd189", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -237,7 +238,7 @@ "rootDefinitionDigest": "sha256:90583a47b9bdd605b2d3f24dcb1a5dbb2b4d9da20d7be097da381c9c77229e62", "nativeSource": { "path": "internal/command/adoptiondoctor", - "canonicalDigest": "sha256:cec92b209d2fa36cf5da43a7049d3c24212f6762a085b065e75b46f1c5fda625", + "canonicalDigest": "sha256:6ec8b4f324c102c1059c051f99209c91b4f7fe5a7218a03d8717c2791b8dd189", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { @@ -510,7 +511,7 @@ "rootDefinitionDigest": "sha256:fd1121285d1b357ed848febc248700a75417d06619e22ed36991631d7526375b", "nativeSource": { "path": "internal/command/agentroute", - "canonicalDigest": "sha256:21ffaf8d53060e31f3dec92d7f39cf6e7654cc2268bde47599211d1d914acfeb", + "canonicalDigest": "sha256:2fc5d42c6126069aad7cad9a18acf9683289ce54be3a0d2f3aef6f429e2bcbdf", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -586,7 +587,7 @@ "rootDefinitionDigest": "sha256:db7d5b29083a939eea0f23253632394212b0d73b5f1b6206d0811d236d6a84ca", "nativeSource": { "path": "internal/command/agentroute", - "canonicalDigest": "sha256:21ffaf8d53060e31f3dec92d7f39cf6e7654cc2268bde47599211d1d914acfeb", + "canonicalDigest": "sha256:2fc5d42c6126069aad7cad9a18acf9683289ce54be3a0d2f3aef6f429e2bcbdf", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { @@ -814,6 +815,122 @@ ] } }, + { + "command": "change-workflow-plan", + "input": "required", + "stdin": true, + "inputPointer": true, + "scopeClass": "explicit_caller_input", + "agentEnvelope": true, + "outputModes": [ + "json", + "text" + ], + "allowedFlags": [ + "--agent-envelope", + "--color", + "--format", + "--input", + "--input-pointer" + ], + "singleOccurrenceFlags": [ + "--agent-envelope", + "--color", + "--format", + "--input", + "--input-pointer" + ], + "flagChoices": { + "--color": [ + "auto", + "never" + ], + "--format": [ + "json", + "text" + ] + }, + "inputContract": { + "contractId": "proofkit.change-workflow-plan.input.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.change-workflow-plan.input.v1.root-shape", + "rootDefinitionDigest": "sha256:6632a481380bfb07d754ef622661b291f805b1b168b1824089bdd5cc9bdd4b0e", + "nativeSource": { + "path": "internal/command/changeworkflowplan", + "canonicalDigest": "sha256:53a35103619a6f992e670e165dd01e6277a32b04faaa26ea6fd799017ebb7dd4", + "evidenceClass": "source_checkout" + }, + "nativeAdmissionWitnessSelector": { + "path": "internal/command/changeworkflowplan/state_test.go", + "test": "TestWorkflowStatePredicates", + "command": "go test ./internal/command/changeworkflowplan -run '^TestWorkflowStatePredicates$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "root-shape-only definition proofkit.change-workflow-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-WORKFLOW-001", + "REQ-PROOFKIT-WORKFLOW-002", + "REQ-PROOFKIT-WORKFLOW-003", + "REQ-PROOFKIT-WORKFLOW-004", + "REQ-PROOFKIT-WORKFLOW-005", + "REQ-PROOFKIT-WORKFLOW-006", + "REQ-PROOFKIT-WORKFLOW-008", + "REQ-PROOFKIT-WORKFLOW-011" + ] + }, + "outputContract": { + "contractId": "proofkit.change-workflow-plan.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.change-workflow-plan.output.v1.root-shape", + "rootDefinitionDigest": "sha256:d0c7c2f1b2d78da4260757c8bdbccda4f9dfe4f69d3ba3a0af24338508f32da9", + "nativeSource": { + "path": "internal/command/changeworkflowplan", + "canonicalDigest": "sha256:53a35103619a6f992e670e165dd01e6277a32b04faaa26ea6fd799017ebb7dd4", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/changeworkflowplan/state_test.go", + "test": "TestWorkflowStatePredicates", + "command": "go test ./internal/command/changeworkflowplan -run '^TestWorkflowStatePredicates$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "JSON plan and agent-envelope variants; text is a bounded derived presentation", + "root-shape-only definition proofkit.change-workflow-plan.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-WORKFLOW-001", + "REQ-PROOFKIT-WORKFLOW-002", + "REQ-PROOFKIT-WORKFLOW-003", + "REQ-PROOFKIT-WORKFLOW-005", + "REQ-PROOFKIT-WORKFLOW-008", + "REQ-PROOFKIT-WORKFLOW-009", + "REQ-PROOFKIT-WORKFLOW-011" + ], + "flagChoices": { + "--color": [ + "auto", + "never" + ], + "--format": [ + "json", + "text" + ] + } + } + }, { "command": "changed-path-set", "input": "required", @@ -1024,7 +1141,7 @@ "rootType": "object", "closed": true, "rootDefinitionRef": "proofkit.conformance-profile.output.v2.root-shape", - "rootDefinitionDigest": "sha256:b3b1ed639969bba5213bb70e0a4ab9f86b69f180ad677ced78e600f87d55fb4f", + "rootDefinitionDigest": "sha256:5817a9d259deb8ff48d8dc6298b46164e7b132304ca1d865967c611d147b1793", "nativeSource": { "path": "internal/command/conformanceprofile", "canonicalDigest": "sha256:fba2ae58bf66cfc43834368dec84ca85abd11e08a541786dc59eafaf7628c322", @@ -1805,7 +1922,7 @@ "rootDefinitionDigest": "sha256:9378c7663485f39089cf6c39a3d2f778e28b5801637229fe9524a9652a7b0737", "nativeSource": { "path": "internal/command/jsonreportcliadaptersource", - "canonicalDigest": "sha256:f806f423a29816180d2fc5d0b8735276d39bd86ec3cbaa8282a1b5a3015b7399", + "canonicalDigest": "sha256:1bacd24b71f570d953dcd878f2edd6563fdace2705042fa81a0cbabb1ecfd7e3", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { @@ -1996,6 +2113,78 @@ ] } }, + { + "command": "native-evidence-guidance", + "input": "none", + "stdin": false, + "inputPointer": false, + "scopeClass": "built_in_package_catalog", + "outputModes": [ + "json", + "text" + ], + "allowedFlags": [ + "--color", + "--format" + ], + "singleOccurrenceFlags": [ + "--color", + "--format" + ], + "flagChoices": { + "--color": [ + "auto", + "never" + ], + "--format": [ + "json", + "text" + ] + }, + "outputContract": { + "contractId": "proofkit.native-evidence-guidance.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.native-evidence-guidance.output.v1.root-shape", + "rootDefinitionDigest": "sha256:218011a133540f57ef74f8748747e00d33b6757c9f77725ecef39f45fb19423f", + "nativeSource": { + "path": "internal/command/nativeevidenceguidance", + "canonicalDigest": "sha256:3b08daa3b023df2a7687999893a5b357c0345a854e2b25ebca84ab3a514db8a1", + "evidenceClass": "source_checkout" + }, + "nativeOutputWitnessSelector": { + "path": "internal/command/nativeevidenceguidance/guidance_test.go", + "test": "TestGuidanceSlotPredicates", + "command": "go test ./internal/command/nativeevidenceguidance -run '^TestGuidanceSlotPredicates$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "JSON guidance report; text is a bounded derived presentation", + "root-shape-only definition proofkit.native-evidence-guidance.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-WORKFLOW-007", + "REQ-PROOFKIT-WORKFLOW-008", + "REQ-PROOFKIT-WORKFLOW-009", + "REQ-PROOFKIT-WORKFLOW-010", + "REQ-PROOFKIT-WORKFLOW-011" + ], + "flagChoices": { + "--color": [ + "auto", + "never" + ], + "--format": [ + "json", + "text" + ] + } + } + }, { "command": "obligation-decision", "input": "required", @@ -2231,7 +2420,7 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:6e888333db0edc8a6eaeb91dad38b876f0bbee22a43c1bd2424e155e5a58dc88", + "canonicalDigest": "sha256:fe81ca3521d27f477bca5fdee31da075348b3835440215c9d1df1ebdbb8c870e", "evidenceClass": "source_checkout" }, { @@ -3566,7 +3755,7 @@ "rootDefinitionDigest": "sha256:41bc233c96bd468bc96eeb0447e037cacdbcf92bb04161052303e8747e4282f2", "nativeSource": { "path": "internal/command/requirementcontext", - "canonicalDigest": "sha256:c814fcecb510be1b2fbe1d9911825d29874491e42a7e6e69b55e0f678b332492", + "canonicalDigest": "sha256:15b85d9a093c8ce0b30f6428a72d3d4e62baf8e9f51be2b1f12d47f2f3f34509", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -3606,7 +3795,7 @@ "rootDefinitionDigest": "sha256:cdadd7b589d682a122cfe2b801f001a3b22a2269aa4977ebe073932d11e1f816", "nativeSource": { "path": "internal/command/requirementcontext", - "canonicalDigest": "sha256:c814fcecb510be1b2fbe1d9911825d29874491e42a7e6e69b55e0f678b332492", + "canonicalDigest": "sha256:15b85d9a093c8ce0b30f6428a72d3d4e62baf8e9f51be2b1f12d47f2f3f34509", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { @@ -3653,7 +3842,7 @@ "rootDefinitionDigest": "sha256:43852cf1a52b3c1f000ec95e460fbad157a6b6a39958d20f81c9e1e4d31e3c6f", "nativeSource": { "path": "internal/command/requirementcontext", - "canonicalDigest": "sha256:c814fcecb510be1b2fbe1d9911825d29874491e42a7e6e69b55e0f678b332492", + "canonicalDigest": "sha256:15b85d9a093c8ce0b30f6428a72d3d4e62baf8e9f51be2b1f12d47f2f3f34509", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -3691,7 +3880,7 @@ "rootDefinitionDigest": "sha256:61f7fd43e2e9da5bde9b61bb86e02cbf337a43c6891ca888c1d42936e5b16456", "nativeSource": { "path": "internal/command/requirementcontext", - "canonicalDigest": "sha256:c814fcecb510be1b2fbe1d9911825d29874491e42a7e6e69b55e0f678b332492", + "canonicalDigest": "sha256:15b85d9a093c8ce0b30f6428a72d3d4e62baf8e9f51be2b1f12d47f2f3f34509", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { @@ -5473,7 +5662,7 @@ "rootDefinitionDigest": "sha256:3c842174dff5361e7f83166469b832805e05aa314b073c16234b5b64e346281e", "nativeSource": { "path": "internal/app", - "canonicalDigest": "sha256:6e888333db0edc8a6eaeb91dad38b876f0bbee22a43c1bd2424e155e5a58dc88", + "canonicalDigest": "sha256:fe81ca3521d27f477bca5fdee31da075348b3835440215c9d1df1ebdbb8c870e", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -5502,7 +5691,7 @@ "rootDefinitionDigest": "sha256:0ea95e277ebe44cd2de42c29b47c38686ac0b6b390d8965367437b3fe138e209", "nativeSource": { "path": "internal/app", - "canonicalDigest": "sha256:6e888333db0edc8a6eaeb91dad38b876f0bbee22a43c1bd2424e155e5a58dc88", + "canonicalDigest": "sha256:fe81ca3521d27f477bca5fdee31da075348b3835440215c9d1df1ebdbb8c870e", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { @@ -5993,7 +6182,7 @@ "rootDefinitionDigest": "sha256:af1e6196ddee9ee4262ca14c9cca1451c89241ebeef177af4a12be866b088f9a", "nativeSource": { "path": "internal/command/textpolicy", - "canonicalDigest": "sha256:7b46a4a0372f3ecd5a9ae7c42a0ba0200d436834cfcdfa8a2d4860be0d8f78d6", + "canonicalDigest": "sha256:d6a41f1ddc42a71b1d03ae52cad8793c1e96557181f8d1032bb0bcaa3f3d9b7f", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -6020,7 +6209,7 @@ "rootDefinitionDigest": "sha256:a47795a1a72dcece9e3e513c8d33829fed187a959450d1ce007697a434b26df1", "nativeSource": { "path": "internal/command/textpolicy", - "canonicalDigest": "sha256:7b46a4a0372f3ecd5a9ae7c42a0ba0200d436834cfcdfa8a2d4860be0d8f78d6", + "canonicalDigest": "sha256:d6a41f1ddc42a71b1d03ae52cad8793c1e96557181f8d1032bb0bcaa3f3d9b7f", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { @@ -6125,7 +6314,7 @@ "rootDefinitionDigest": "sha256:687efe1fe441443ad7bcab69f05987b47a25672da8032e7471bd7c48ebd95a30", "nativeSource": { "path": "internal/command/publicapi", - "canonicalDigest": "sha256:9f21766bc6d4dd042ff0e3bd3f4f7278448a9822e48399e56f9c5d2d9b02351b", + "canonicalDigest": "sha256:59a2876503e61689f968c462ef98065084c1844db1469e691f144b81d3ae76a9", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -6167,7 +6356,7 @@ "rootDefinitionDigest": "sha256:93da5f4d5efd4c482cb5da37bb60a80f8e47e4eab549428790ddd1a4e680009d", "nativeSource": { "path": "internal/command/publicapi", - "canonicalDigest": "sha256:9f21766bc6d4dd042ff0e3bd3f4f7278448a9822e48399e56f9c5d2d9b02351b", + "canonicalDigest": "sha256:59a2876503e61689f968c462ef98065084c1844db1469e691f144b81d3ae76a9", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { @@ -7919,6 +8108,219 @@ }, "canonicalDigest": "sha256:d20d4d55690f7b98768d8bb1c2c313f78bdf9ac8163fa6d1ff694664b2d7d999" }, + { + "definitionId": "proofkit.change-workflow-plan.input.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "checkpoint", + "completedStageIds", + "contextRefs", + "governingAuthorityRefId", + "requiredContextRefIds", + "schemaVersion" + ], + "requiredFields": [ + "checkpoint", + "completedStageIds", + "contextRefs", + "governingAuthorityRefId", + "requiredContextRefIds", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON input" + ] + } + ] + }, + "canonicalDigest": "sha256:6632a481380bfb07d754ef622661b291f805b1b168b1824089bdd5cc9bdd4b0e" + }, + { + "definitionId": "proofkit.change-workflow-plan.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "requiredFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "rootKind": "object", + "variantId": "01-agent-envelope", + "when": [ + "--agent-envelope" + ] + }, + { + "allowedFields": [ + "action", + "activeStageId", + "authority", + "checkpointState", + "completedStageIds", + "nonClaims", + "omittedContextCount", + "omittedContextRefIds", + "outputKind", + "prompt", + "reportId", + "reportKind", + "retainedContextRefIds", + "retainedContextRefs", + "schemaVersion" + ], + "requiredFields": [ + "action", + "activeStageId", + "authority", + "checkpointState", + "completedStageIds", + "nonClaims", + "omittedContextCount", + "omittedContextRefIds", + "outputKind", + "prompt", + "reportId", + "reportKind", + "retainedContextRefIds", + "retainedContextRefs", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "02-next-action", + "when": [ + "without --agent-envelope; outputKind=next_action; without successorStateDelta" + ] + }, + { + "allowedFields": [ + "action", + "activeStageId", + "authority", + "checkpointState", + "completedStageIds", + "nonClaims", + "omittedContextCount", + "omittedContextRefIds", + "outputKind", + "prompt", + "reportId", + "reportKind", + "retainedContextRefIds", + "retainedContextRefs", + "schemaVersion", + "successorStateDelta" + ], + "requiredFields": [ + "action", + "activeStageId", + "authority", + "checkpointState", + "completedStageIds", + "nonClaims", + "omittedContextCount", + "omittedContextRefIds", + "outputKind", + "prompt", + "reportId", + "reportKind", + "retainedContextRefIds", + "retainedContextRefs", + "schemaVersion", + "successorStateDelta" + ], + "rootKind": "object", + "variantId": "03-accept-stage", + "when": [ + "without --agent-envelope; outputKind=next_action; successorStateDelta" + ] + }, + { + "allowedFields": [ + "authority", + "completedStageIds", + "nonClaims", + "omittedContextCount", + "omittedContextRefIds", + "outputKind", + "reportId", + "reportKind", + "retainedContextRefIds", + "retainedContextRefs", + "schemaVersion" + ], + "requiredFields": [ + "authority", + "completedStageIds", + "nonClaims", + "omittedContextCount", + "omittedContextRefIds", + "outputKind", + "reportId", + "reportKind", + "retainedContextRefIds", + "retainedContextRefs", + "schemaVersion" + ], + "rootKind": "object", + "variantId": "04-workflow-complete", + "when": [ + "without --agent-envelope; outputKind=workflow_complete" + ] + } + ] + }, + "canonicalDigest": "sha256:d0c7c2f1b2d78da4260757c8bdbccda4f9dfe4f69d3ba3a0af24338508f32da9" + }, { "definitionId": "proofkit.changed-path-set.input.v1.root-shape", "schemaVersion": 1, @@ -8274,7 +8676,7 @@ } ] }, - "canonicalDigest": "sha256:b3b1ed639969bba5213bb70e0a4ab9f86b69f180ad677ced78e600f87d55fb4f" + "canonicalDigest": "sha256:5817a9d259deb8ff48d8dc6298b46164e7b132304ca1d865967c611d147b1793" }, { "definitionId": "proofkit.custom-rule-boundary.input.v1.root-shape", @@ -9521,6 +9923,42 @@ }, "canonicalDigest": "sha256:e6a152b5ca41396f25874b1d1ddd1dbc323bd9e927a18c5fe5088ad527fe65cd" }, + { + "definitionId": "proofkit.native-evidence-guidance.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "guidanceId", + "nonClaims", + "schemaVersion", + "slots" + ], + "requiredFields": [ + "guidanceId", + "nonClaims", + "schemaVersion", + "slots" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:218011a133540f57ef74f8748747e00d33b6757c9f77725ecef39f45fb19423f" + }, { "definitionId": "proofkit.obligation-decision.input.v1.root-shape", "schemaVersion": 1, diff --git a/proofkit/command-families.v1.json b/proofkit/command-families.v1.json index f090cc6..0b79830 100644 --- a/proofkit/command-families.v1.json +++ b/proofkit/command-families.v1.json @@ -17,6 +17,15 @@ "pilot-admission" ] }, + { + "familyId": "agent-workflow-planning", + "label": "Agent workflow planning", + "purpose": "Plan bounded engineering-change stages and expose repository-neutral native-evidence guidance.", + "commands": [ + "change-workflow-plan", + "native-evidence-guidance" + ] + }, { "familyId": "cli-metadata-and-conformance", "label": "CLI metadata and conformance", diff --git a/proofkit/requirement-bindings.json b/proofkit/requirement-bindings.json index 21cfb58..5b247b5 100644 --- a/proofkit/requirement-bindings.json +++ b/proofkit/requirement-bindings.json @@ -612,6 +612,22 @@ "proofState": "witness_backed", "nonClaims": ["This requirement does not claim external action safety, tag equivalence, provider execution, branch-protection configuration, release publication, or provider-side asset immutability."] }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-026", + "ownerId": "proofkit.supply-chain-quality", + "specPath": "docs/specs/proofkit-supply-chain-quality/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": ["This bounded encoding policy does not prove terminal-rendering equivalence or safety of arbitrary downstream decoders."] + }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-027", + "ownerId": "proofkit.supply-chain-quality", + "specPath": "docs/specs/proofkit-supply-chain-quality/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": ["This bounded diagnostic taxonomy does not prove confidentiality of values outside its admitted secret and control classes."] + }, { "requirementId": "REQ-PROOFKIT-SPEC-017", "ownerId": "proofkit.spec-proof-core", @@ -679,6 +695,94 @@ "claimLevel": "blocking", "proofState": "witness_backed", "nonClaims": ["This private single-source model does not select or expose a source codec, parse or serialize a persisted source, retain a normalized mirror, establish cross-source requirement identity, authenticate derivation objects, digests, selectors, or freshness, authenticate a caller-declared sourceKind or prove its author's authority or trust class, cut over any current requirement consumer, prove requirement meaning or implementation correctness, execute native witnesses, approve merge or release, or establish rollout or production readiness."] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-001", + "ownerId": "proofkit.agent-workflow", + "specPath": "docs/specs/proofkit-agent-workflow/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": ["The planner does not authenticate caller assertions or prove that a planned action was executed, correct, reviewed, or completed."] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-002", + "ownerId": "proofkit.agent-workflow", + "specPath": "docs/specs/proofkit-agent-workflow/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": ["Stage ordering does not prove the semantic adequacy of any architecture, design, plan, implementation, verification, pull request, or closeout artifact."] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-003", + "ownerId": "proofkit.agent-workflow", + "specPath": "docs/specs/proofkit-agent-workflow/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": ["The snapshot relation does not own pending work, elapsed time, predecessor history, waiver, abort, reopen, or external task-state transitions."] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-004", + "ownerId": "proofkit.agent-workflow", + "specPath": "docs/specs/proofkit-agent-workflow/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": ["Digest equality proves only internal equality of caller-declared identity fields and does not bind actual bytes, authenticate the reviewer, establish currentness or trust, or prove assessment quality."] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-005", + "ownerId": "proofkit.agent-workflow", + "specPath": "docs/specs/proofkit-agent-workflow/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": ["Structural least closure relative to caller-declared seeds and edges does not prove context truth, freshness, semantic sufficiency, global minimality, or provider token usage."] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-006", + "ownerId": "proofkit.agent-workflow", + "specPath": "docs/specs/proofkit-agent-workflow/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": ["Explicit caller input does not establish repository identity, policy, source meaning, authorization to mutate, or the truth of observations made outside Proofkit."] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-007", + "ownerId": "proofkit.agent-workflow", + "specPath": "docs/specs/proofkit-agent-workflow/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": ["The template does not generate, execute, approve, authenticate, or prove adequacy of a consuming repository's script, witness, oracle, receipt, policy, or cleanup implementation."] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-008", + "ownerId": "proofkit.agent-workflow", + "specPath": "docs/specs/proofkit-agent-workflow/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": ["Bounded nondisclosure admission does not prove absence of every possible sensitive value or every terminal interpretation."] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-009", + "ownerId": "proofkit.agent-workflow", + "specPath": "docs/specs/proofkit-agent-workflow/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": ["Terminal styling is presentation only and does not claim identical rendering across terminal emulators."] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-010", + "ownerId": "proofkit.agent-workflow", + "specPath": "docs/specs/proofkit-agent-workflow/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": ["Structural minimality does not prove universal usability, open-world optimality, lower provider token use, or lower cognitive cost for every user and repository."] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-011", + "ownerId": "proofkit.agent-workflow", + "specPath": "docs/specs/proofkit-agent-workflow/requirements.v1.json", + "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."] } ], "bindings": [ @@ -1413,6 +1517,16 @@ "witnessId": "proofkit.agent-envelope.reference-closure", "witnessKind": "contract", "witnessPath": "internal/kernel/agentenvelope/agentenvelope_test.go", + "witnessSelectors": [ + { + "selector": "TestBuildPrunesReferencesToTruncatedLocalTargets", + "command": "go test ./internal/kernel/agentenvelope -run '^TestBuildPrunesReferencesToTruncatedLocalTargets$'" + }, + { + "selector": "TestBuildPrunesCommandIDsWithoutRetainedCommands", + "command": "go test ./internal/kernel/agentenvelope -run '^TestBuildPrunesCommandIDsWithoutRetainedCommands$'" + } + ], "commandIds": [ "proofkit.go-test" ], @@ -1420,6 +1534,37 @@ "local-go" ] }, + { + "requirementId": "REQ-PROOFKIT-SPEC-005", + "scenarioId": "proofkit.spec-proof-core.agent-envelope-local-identity-admission", + "witnessId": "proofkit.agent-envelope.local-identity-admission-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/kernel/agentenvelope/reference_identity_test.go", + "witnessSelectors": [ + { + "selector": "TestBuildRejectsDuplicateAndCrossDomainLocalIdentities", + "command": "go test ./internal/kernel/agentenvelope -run '^TestBuildRejectsDuplicateAndCrossDomainLocalIdentities$'" + }, + { + "selector": "TestBuildPrunesAmbiguousIdentityFromEveryReferenceContainer", + "command": "go test ./internal/kernel/agentenvelope -run '^TestBuildPrunesAmbiguousIdentityFromEveryReferenceContainer$'" + }, + { + "selector": "TestBuildOmitsUnsafeTargetsAndReferencesBeforeDisplaySanitization", + "command": "go test ./internal/kernel/agentenvelope -run '^TestBuildOmitsUnsafeTargetsAndReferencesBeforeDisplaySanitization$'" + }, + { + "selector": "TestBuildCountsStandaloneUnsafeReferenceOmissions", + "command": "go test ./internal/kernel/agentenvelope -run '^TestBuildCountsStandaloneUnsafeReferenceOmissions$'" + }, + { + "selector": "TestBuildPreservesUniqueLocalAndSafeExternalReferences", + "command": "go test ./internal/kernel/agentenvelope -run '^TestBuildPreservesUniqueLocalAndSafeExternalReferences$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, { "requirementId": "REQ-PROOFKIT-SPEC-006", "scenarioId": "proofkit.spec-proof-core.test-inventory-normalized-output", @@ -3633,6 +3778,165 @@ "commandIds": ["proofkit.actionlint", "proofkit.go-test"], "environmentClasses": ["local-go"] }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-026", + "scenarioId": "proofkit.supply-chain-quality.unicode-scalar-policy", + "witnessId": "proofkit.unicode-policy.scalar-table-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/kernel/unicodepolicy/unicodepolicy_test.go", + "witnessSelectors": [ + { + "selector": "TestUnsafeScalarTableMatchesEveryUnicode15Scalar", + "command": "go test ./internal/kernel/unicodepolicy -run '^TestUnsafeScalarTableMatchesEveryUnicode15Scalar$'" + }, + { + "selector": "TestValidScalarStringRejectsMalformedUTF8", + "command": "go test ./internal/kernel/unicodepolicy -run '^TestValidScalarStringRejectsMalformedUTF8$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-026", + "scenarioId": "proofkit.supply-chain-quality.go-stable-json-unicode-parity", + "witnessId": "proofkit.stable-json.go-unicode-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/kernel/stablejson/stablejson_test.go", + "witnessSelectors": [ + { + "selector": "TestUnicodePolicyCorpus", + "command": "go test ./internal/kernel/stablejson -run '^TestUnicodePolicyCorpus$'" + }, + { + "selector": "TestMarshalRejectsEveryMalformedUTF8Class", + "command": "go test ./internal/kernel/stablejson -run '^TestMarshalRejectsEveryMalformedUTF8Class$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-026", + "scenarioId": "proofkit.supply-chain-quality.javascript-stable-json-unicode-parity", + "witnessId": "proofkit.stable-json.javascript-unicode-falsifier", + "witnessKind": "contract", + "witnessPath": "scripts/stable_json_selector_test.go", + "witnessSelectors": [ + { + "selector": "TestStableJSONJavaScriptUnicodeCorpus", + "command": "go test ./scripts -run '^TestStableJSONJavaScriptUnicodeCorpus$'" + }, + { + "selector": "TestStableJSONJavaScriptExhaustiveUnicodeTable", + "command": "go test ./scripts -run '^TestStableJSONJavaScriptExhaustiveUnicodeTable$'" + }, + { + "selector": "TestStableJSONJavaScriptRejectsEverySurrogate", + "command": "go test ./scripts -run '^TestStableJSONJavaScriptRejectsEverySurrogate$'" + }, + { + "selector": "TestStableJSONJavaScriptAdmitsEverySurrogatePair", + "command": "go test ./scripts -run '^TestStableJSONJavaScriptAdmitsEverySurrogatePair$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-026", + "scenarioId": "proofkit.supply-chain-quality.generated-typescript-stable-json-unicode-parity", + "witnessId": "proofkit.generated-typescript-adapter.unicode-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source_test.go", + "witnessSelectors": [ + { + "selector": "TestGeneratedTypeScriptAdapterExecutesCoreSemantics", + "command": "go test ./internal/command/jsonreportcliadaptersource -run '^TestGeneratedTypeScriptAdapterExecutesCoreSemantics$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-027", + "scenarioId": "proofkit.supply-chain-quality.go-report-visible-whole-value-redaction", + "witnessId": "proofkit.admit.whole-value-redaction-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/kernel/admit/fields_test.go", + "witnessSelectors": [ + { + "selector": "TestRedactDiagnosticValueReplacesRejectedValuesAsAWhole", + "command": "go test ./internal/kernel/admit -run '^TestRedactDiagnosticValueReplacesRejectedValuesAsAWhole$'" + }, + { + "selector": "TestReportVisibleRedactionMatrixRejectsSplitSecretsWithoutDisclosure", + "command": "go test ./internal/kernel/admit -run '^TestReportVisibleRedactionMatrixRejectsSplitSecretsWithoutDisclosure$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-027", + "scenarioId": "proofkit.supply-chain-quality.javascript-report-visible-whole-value-redaction", + "witnessId": "proofkit.stable-json.javascript-redaction-falsifier", + "witnessKind": "contract", + "witnessPath": "scripts/stable_json_selector_test.go", + "witnessSelectors": [ + { + "selector": "TestStableJSONJavaScriptDiagnosticWholeValueRedaction", + "command": "go test ./scripts -run '^TestStableJSONJavaScriptDiagnosticWholeValueRedaction$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-027", + "scenarioId": "proofkit.supply-chain-quality.generated-typescript-report-visible-whole-value-redaction", + "witnessId": "proofkit.generated-typescript-adapter.redaction-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source_test.go", + "witnessSelectors": [ + { + "selector": "TestGeneratedTypeScriptAdapterExecutesCoreSemantics", + "command": "go test ./internal/command/jsonreportcliadaptersource -run '^TestGeneratedTypeScriptAdapterExecutesCoreSemantics$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-027", + "scenarioId": "proofkit.supply-chain-quality.package-verifier-diagnostic-nondisclosure", + "witnessId": "proofkit.package-verifier.diagnostic-nondisclosure-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/tools/packageverify/main_test.go", + "witnessSelectors": [ + { + "selector": "TestPackageVerifierDiagnosticNondisclosure", + "command": "go test ./internal/tools/packageverify -run '^TestPackageVerifierDiagnosticNondisclosure$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-027", + "scenarioId": "proofkit.supply-chain-quality.browser-proof-verifier-diagnostic-nondisclosure", + "witnessId": "proofkit.browser-proof-verifier.diagnostic-nondisclosure-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/tools/browserproofverify/main_test.go", + "witnessSelectors": [ + { + "selector": "TestDiagnosticsDoNotLeakArtifactValues", + "command": "go test ./internal/tools/browserproofverify -run '^TestDiagnosticsDoNotLeakArtifactValues$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, { "requirementId": "REQ-PROOFKIT-SPEC-019", "scenarioId": "proofkit.spec-proof-core.requirement-context-composition", @@ -4150,6 +4454,250 @@ "witnessPath": "tests/browser/workspace.spec.mjs", "commandIds": ["proofkit.browser-check"], "environmentClasses": ["local-node-browser"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-001", + "scenarioId": "proofkit.agent-workflow.pure-single-admission-owner", + "witnessId": "proofkit.agent-workflow.pure-single-admission-falsifier", + "witnessKind": "technical", + "witnessPath": "internal/command/changeworkflowplan/change_workflow_plan_test.go", + "witnessSelectors": [ + { + "selector": "TestWorkflowPurityPredicates", + "command": "go test ./internal/command/changeworkflowplan -run '^TestWorkflowPurityPredicates$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-002", + "scenarioId": "proofkit.agent-workflow.stage-prefix-and-terminal-relation", + "witnessId": "proofkit.agent-workflow.stage-prefix-terminal-falsifier", + "witnessKind": "technical", + "witnessPath": "internal/command/changeworkflowplan/state_test.go", + "witnessSelectors": [ + { + "selector": "TestWorkflowStatePredicates", + "command": "go test ./internal/command/changeworkflowplan -run '^TestWorkflowStatePredicates$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-003", + "scenarioId": "proofkit.agent-workflow.total-checkpoint-successor-relation", + "witnessId": "proofkit.agent-workflow.checkpoint-successor-falsifier", + "witnessKind": "technical", + "witnessPath": "internal/command/changeworkflowplan/state_test.go", + "witnessSelectors": [ + { + "selector": "TestWorkflowCheckpointPredicates", + "command": "go test ./internal/command/changeworkflowplan -run '^TestWorkflowCheckpointPredicates$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-004", + "scenarioId": "proofkit.agent-workflow.review-identity-closure", + "witnessId": "proofkit.agent-workflow.review-identity-falsifier", + "witnessKind": "technical", + "witnessPath": "internal/command/changeworkflowplan/admission_test.go", + "witnessSelectors": [ + { + "selector": "TestWorkflowIdentityPredicates", + "command": "go test ./internal/command/changeworkflowplan -run '^TestWorkflowIdentityPredicates$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-005", + "scenarioId": "proofkit.agent-workflow.reference-closed-bounded-context", + "witnessId": "proofkit.agent-workflow.context-closure-falsifier", + "witnessKind": "technical", + "witnessPath": "internal/command/changeworkflowplan/context_closure_test.go", + "witnessSelectors": [ + { + "selector": "TestWorkflowClosurePredicates", + "command": "go test ./internal/command/changeworkflowplan -run '^TestWorkflowClosurePredicates$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-006", + "scenarioId": "proofkit.agent-workflow.no-ambient-authority", + "witnessId": "proofkit.agent-workflow.no-ambient-authority-falsifier", + "witnessKind": "technical", + "witnessPath": "internal/command/changeworkflowplan/dependency_test.go", + "witnessSelectors": [ + { + "selector": "TestWorkflowAmbientAuthorityPredicates", + "command": "go test ./internal/command/changeworkflowplan -run '^TestWorkflowAmbientAuthorityPredicates$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-007", + "scenarioId": "proofkit.agent-workflow.native-evidence-guidance-slot-closure", + "witnessId": "proofkit.agent-workflow.guidance-slot-closure-falsifier", + "witnessKind": "technical", + "witnessPath": "internal/command/nativeevidenceguidance/guidance_test.go", + "witnessSelectors": [ + { + "selector": "TestGuidanceSlotPredicates", + "command": "go test ./internal/command/nativeevidenceguidance -run '^TestGuidanceSlotPredicates$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-007", + "scenarioId": "proofkit.agent-workflow.native-evidence-guidance-purity", + "witnessId": "proofkit.agent-workflow.guidance-purity-falsifier", + "witnessKind": "technical", + "witnessPath": "internal/command/nativeevidenceguidance/dependency_test.go", + "witnessSelectors": [ + { + "selector": "TestGuidanceNoAmbientDependencyPredicates", + "command": "go test ./internal/command/nativeevidenceguidance -run '^TestGuidanceNoAmbientDependencyPredicates$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-008", + "scenarioId": "proofkit.agent-workflow.prompt-coordinate-and-escalation-closure", + "witnessId": "proofkit.agent-workflow.prompt-coordinate-falsifier", + "witnessKind": "technical", + "witnessPath": "internal/command/changeworkflowplan/prompt_test.go", + "witnessSelectors": [ + { + "selector": "TestWorkflowPromptPredicates", + "command": "go test ./internal/command/changeworkflowplan -run '^TestWorkflowPromptPredicates$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-008", + "scenarioId": "proofkit.agent-workflow.bounded-safe-text", + "witnessId": "proofkit.agent-workflow.bounded-safe-text-falsifier", + "witnessKind": "technical", + "witnessPath": "internal/command/changeworkflowplan/text_test.go", + "witnessSelectors": [ + { + "selector": "TestWorkflowTextPredicates", + "command": "go test ./internal/command/changeworkflowplan -run '^TestWorkflowTextPredicates$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-009", + "scenarioId": "proofkit.agent-workflow.cli-presentation-capability-product", + "witnessId": "proofkit.agent-workflow.cli-presentation-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/app/agent_workflow_command_test.go", + "witnessSelectors": [ + { + "selector": "TestAgentWorkflowCLITruthTable", + "command": "go test ./internal/app -run '^TestAgentWorkflowCLITruthTable$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-009", + "scenarioId": "proofkit.agent-workflow.style-strip-parity", + "witnessId": "proofkit.agent-workflow.style-strip-parity-falsifier", + "witnessKind": "technical", + "witnessPath": "internal/command/changeworkflowplan/text_test.go", + "witnessSelectors": [ + { + "selector": "TestWorkflowTextProjectionParity", + "command": "go test ./internal/command/changeworkflowplan -run '^TestWorkflowTextProjectionParity$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-010", + "scenarioId": "proofkit.agent-workflow.semantic-owner-minimality", + "witnessId": "proofkit.agent-workflow.semantic-owner-minimality-falsifier", + "witnessKind": "technical", + "witnessPath": "internal/command/nativeevidenceguidance/guidance_test.go", + "witnessSelectors": [ + { + "selector": "TestGuidancePurityPredicates", + "command": "go test ./internal/command/nativeevidenceguidance -run '^TestGuidancePurityPredicates$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-011", + "scenarioId": "proofkit.agent-workflow.public-cli-relation-closure", + "witnessId": "proofkit.agent-workflow.public-cli-relation-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/app/agent_workflow_command_test.go", + "witnessSelectors": [ + { + "selector": "TestAgentWorkflowCLITruthTable", + "command": "go test ./internal/app -run '^TestAgentWorkflowCLITruthTable$'" + } + ], + "commandIds": ["proofkit.command-contract-check", "proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-011", + "scenarioId": "proofkit.agent-workflow.installed-carrier-smoke-closure", + "witnessId": "proofkit.agent-workflow.installed-carrier-smoke-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-011", + "scenarioId": "proofkit.agent-workflow.version-edge-wire-observation", + "witnessId": "proofkit.agent-workflow.version-edge-wire-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/app/agent_workflow_version_edge_test.go", + "witnessSelectors": [ + { + "selector": "TestAgentWorkflowVersionEdgeClosesPublicWireAdditions", + "command": "go test ./internal/app -run '^TestAgentWorkflowVersionEdgeClosesPublicWireAdditions$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] } ], "witnessCommands": [ diff --git a/proofkit/witness-plan.json b/proofkit/witness-plan.json index 9e2283e..f3efe83 100644 --- a/proofkit/witness-plan.json +++ b/proofkit/witness-plan.json @@ -780,6 +780,7 @@ "internal/kernel/report", "internal/kernel/secretjson", "internal/kernel/stablejson", + "internal/kernel/unicodepolicy", "internal/kernel/witnesscommand", "internal/testsupport/browserfixture", "internal/tools/browserproofverify", diff --git a/release/change-record.v2.json b/release/change-record.v2.json index 71b13a3..f64724c 100644 --- a/release/change-record.v2.json +++ b/release/change-record.v2.json @@ -1,46 +1,40 @@ { "schemaVersion": 2, - "previousVersion": "0.3.0", - "version": "0.4.0", + "previousVersion": "0.4.0", + "version": "0.5.0", "changeClass": "breaking", "breakingChanges": [ { - "changeId": "proofkit.compact.consumer-contract-v2", - "summary": "Compact-aware adoption, pilot, conformance, impact, proof-view, coverage, inventory, context composition, context slicing, semantic diff, traceability graph, browser, source-set, and resolver command contracts advance atomically and reject legacy compact payloads." + "changeId": "proofkit.agent-envelope.local-identity-closure", + "summary": "Agent envelopes now require unique, pairwise-disjoint command, context, and receipt identities and fail closed by pruning unsafe or ambiguous local targets and references with bounded omissions." }, { - "changeId": "proofkit.compact.declaration-contract-v2", - "summary": "Compact requirement proof inputs now use declaration-only v2 discriminators, rename mutation metadata as a caller claim, and remove assurance-shaped proof state and synthetic checked or finding counts." - }, - { - "changeId": "proofkit.compact.identity-role-closure", - "summary": "Compact bindings now preserve requirement, surface, and scenario identity while witness routes preserve binding, role, selector, and JSON-safe resolution order across every decision-bearing projection." - }, - { - "changeId": "proofkit.compact.source-set-v2", - "summary": "Compact source-set normalization advances to wrapper, envelope, and source-set schema v2 with declaration-specific roles and fragment contract v3." + "changeId": "proofkit.stable-json.unicode-scalar-v2", + "summary": "Go, JavaScript, and generated TypeScript stable JSON now reject non-scalar strings and deterministically escape the pinned Unicode 15 control and format policy; the generated TypeScript adapter identity advances to v2." } ], "additions": [ { - "changeId": "proofkit.requirement-source.typed-model", - "summary": "Add a bounded representation-neutral internal typed model for requirement-source v2 experiments without adding a source codec, persistence format, public CLI, or source cutover." + "changeId": "proofkit.agent-workflow.change-planner", + "summary": "Add a bounded change-workflow-plan command with canonical JSON, derived agent-envelope, plain-text, and capability-gated terminal-color projections." + }, + { + "changeId": "proofkit.agent-workflow.native-evidence-guidance", + "summary": "Add a no-input native-evidence-guidance command that exposes the repository-owned decision template as canonical JSON or plain text without executing or approving consumer witnesses." } ], "migration": { "required": true, "steps": [ - "Replace compact root schema_version 1, authority_state canonical, contract_kind requirement_proof_binding, and normalization_profile proofkit.compact.v1 with schema_version 2, authority_state caller_owned_declaration, contract_kind requirement_proof_route_declaration, and normalization_profile proofkit.compact.declaration.v2.", - "Remove proof_contract_state; replace mutation_resistance_state with declared_mutation_resistance_claim_id; and stop consuming proofContractState, mutationResistanceContext, checkedWitnessSelectorCount, and findingCount from compact resolver projections.", - "Preserve each compact binding as requirementId, surfaceId, and scenarioId plus bindingRecordId, and preserve each witness route as bindingRecordId, role, selector, JSON-safe resolutionOrderIndex from 0 through 9007199254740991, and witnessRouteId; do not collapse equal selectors across positive and falsification roles.", - "Replace changedRecordIds with changedRequirementIds, replace resolver projectionKind proofkit.requirement-proof-resolver with proofkit.requirement-proof-route-resolver, and consume compact-aware adoption, pilot, conformance, impact, proof-view, coverage, inventory, context-compose, context-slice, semantic-diff, traceability-graph, browser, source-set, and resolver parent contracts at their 0.4.0 versions.", - "Regenerate requirement proof source sets with wrapper, canonical envelope, canonical contract, and source-set schema version 2, declaration-specific source roles, and fragment contract requirement-proof-route-declarations/fragment/v3." + "Ensure every agent-envelope command, context, and receipt ID is individually safe and unique, keep the three local identity domains pairwise disjoint, and remove or rename references to ambiguous local targets before adopting 0.5.0.", + "Ensure every stable-JSON string is a Unicode scalar sequence, accept deterministic escaping of pinned Unicode 15 control and format code points, and regenerate TypeScript adapters against generator proofkit.json-report-cli-adapter-source.typescript.v2." ] }, "platformRequirements": [ "Published Darwin package binaries require macOS 12.0 or later on arm64 and x86_64." ], "knownLimitations": [ + "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.", "Complete nested public structural contracts remain blocked under SCHEMA-01; current CLI contracts own exact root variants only.", "The requirement-source v2 codec remains unselected until SOURCE-CODEC-01 passes; the typed model is internal and no source cutover is claimed.", "TSX source parsing remains unsupported." diff --git a/scripts/browser-proof-execution.mjs b/scripts/browser-proof-execution.mjs index a34b9f2..4ba2541 100644 --- a/scripts/browser-proof-execution.mjs +++ b/scripts/browser-proof-execution.mjs @@ -3,6 +3,7 @@ import {symlinkSync} from "node:fs"; import {join, resolve} from "node:path"; import {assertInputSnapshotUnchanged, materializeInputSnapshot, snapshotInputAssets} from "./browser-proof-inputs.mjs"; +import {createUTF8StrictStreamDecoder, decodeUTF8Strict} from "./stable-json.mjs"; export async function executeBrowserProof({environment, execFile, inputPaths, nodeExecutable, resolution, runDirectory, spawnProcess, startServer = startBrowserServer, testCommand}) { const sourceDirectory = resolve(runDirectory, "source"); @@ -60,9 +61,10 @@ function admitBrowserProofReport({execFile, reportPath, sourceDirectory}) { const output = execFile( "go", ["run", "./internal/tools/browserproofverify", "--admit-playwright-report", reportPath], - {cwd: sourceDirectory, encoding: "utf8", maxBuffer: 8 << 20}, + {cwd: sourceDirectory, maxBuffer: 8 << 20}, ); - const projects = JSON.parse(String(output)); + const encoded = typeof output === "string" ? output : decodeUTF8Strict(output, "browser proof report admission output"); + const projects = JSON.parse(encoded); if (!Array.isArray(projects)) throw new Error("browser proof report admission returned an invalid projection"); return projects; } @@ -72,41 +74,42 @@ export async function startBrowserServer(binary, options = {}) { const readinessTimeoutMs = options.readinessTimeoutMs ?? 30_000; const stopTimeoutMs = options.stopTimeoutMs ?? 10_000; const child = spawnProcess(binary, [], {stdio: ["ignore", "pipe", "pipe"]}); - let stdout = ""; - let stderr = ""; - child.stdout.setEncoding("utf8"); - child.stderr.setEncoding("utf8"); - const collectStdout = (chunk) => { stdout = boundedDiagnostics(stdout, chunk); }; - const collectStderr = (chunk) => { stderr = boundedDiagnostics(stderr, chunk); }; + const stdoutDecoder = boundedUTF8Stream("browser test server stdout"); + const collectStdout = (chunk) => { stdoutDecoder.write(chunk); }; + const drainStderr = () => {}; child.stdout.on("data", collectStdout); - child.stderr.on("data", collectStderr); + child.stderr.on("data", drainStderr); const lifecycle = observeChildProcess(child); let ready = false; let stopPromise; const stop = () => { const terminalBeforeStop = lifecycle.current(); - stopPromise ??= stopChildProcess(child, lifecycle, stopTimeoutMs, ready && terminalBeforeStop !== null).finally(() => { + stopPromise ??= stopChildProcess(child, lifecycle, stopTimeoutMs, ready && terminalBeforeStop !== null).then(() => { + stdoutDecoder.finish(); + if (stdoutDecoder.error() !== null) throw stdoutDecoder.error(); + }).finally(() => { child.stdout.off("data", collectStdout); - child.stderr.off("data", collectStderr); + child.stderr.off("data", drainStderr); lifecycle.dispose(); }); return stopPromise; }; try { - const url = await waitForBrowserServerReady(child, lifecycle, () => stdout, () => stderr, readinessTimeoutMs); + const url = await waitForBrowserServerReady(child, lifecycle, stdoutDecoder.value, stdoutDecoder.error, stdoutDecoder.finish, readinessTimeoutMs); ready = true; return {url, stop}; } catch (error) { try { await stop(); } catch (cleanupError) { + if (cleanupError === error) throw error; throw new AggregateError([error, cleanupError], "browser test server failed before readiness and cleanup did not complete"); } throw error; } } -function waitForBrowserServerReady(child, lifecycle, stdout, stderr, timeoutMs) { +function waitForBrowserServerReady(child, lifecycle, stdout, decodeError, finishDecode, timeoutMs) { return new Promise((resolve, reject) => { let settled = false; const finish = (callback, value) => { @@ -117,12 +120,21 @@ function waitForBrowserServerReady(child, lifecycle, stdout, stderr, timeoutMs) callback(value); }; const inspect = () => { + const error = decodeError(); + if (error !== null) { + finish(reject, error); + return; + } const match = stdout().match(/Proofkit requirement browser: (http:\/\/127\.0\.0\.1:\d+\/)\n/); if (match) finish(resolve, match[1]); }; - const timeout = setTimeout(() => finish(reject, new Error(`browser test server readiness timed out: ${stderr()}`)), timeoutMs); - child.stdout.on("data", inspect); - lifecycle.promise.then((outcome) => finish(reject, terminalOutcomeError(outcome, "before readiness", stderr()))); + const timeout = setTimeout(() => finish(reject, new Error("browser test server readiness timed out")), timeoutMs); + child.stdout.on("data", inspect); + lifecycle.promise.then((outcome) => { + finishDecode(); + const error = decodeError(); + finish(reject, error ?? terminalOutcomeError(outcome, "before readiness")); + }); inspect(); }); } @@ -154,7 +166,7 @@ function observeChildProcess(child) { async function stopChildProcess(child, lifecycle, timeoutMs, rejectExistingTerminal) { const existing = lifecycle.current(); if (existing !== null) { - if (rejectExistingTerminal) throw terminalOutcomeError(existing, "before requested shutdown", ""); + if (rejectExistingTerminal) throw terminalOutcomeError(existing, "before requested shutdown"); return; } child.kill("SIGTERM"); @@ -186,18 +198,44 @@ function waitForTerminalOutcome(lifecycle, timeoutMs) { } function admitRequestedShutdown(outcome) { - if (outcome.kind === "error") throw terminalOutcomeError(outcome, "during requested shutdown", ""); + if (outcome.kind === "error") throw terminalOutcomeError(outcome, "during requested shutdown"); if (outcome.code === 0 || outcome.signal === "SIGTERM" || outcome.signal === "SIGKILL") return; - throw terminalOutcomeError(outcome, "during requested shutdown", ""); + throw terminalOutcomeError(outcome, "during requested shutdown"); } -function terminalOutcomeError(outcome, phase, diagnostics) { +function terminalOutcomeError(outcome, phase) { if (outcome.kind === "error") return new Error(`browser test server process error ${phase}`); - const suffix = diagnostics === "" ? "" : `: ${diagnostics}`; - return new Error(`browser test server exited ${phase} with code ${outcome.code} and signal ${outcome.signal}${suffix}`); + return new Error(`browser test server exited ${phase} with code ${outcome.code} and signal ${outcome.signal}`); } -function boundedDiagnostics(current, chunk) { - const next = current + String(chunk); - return next.length <= 65_536 ? next : next.slice(-65_536); +function boundedUTF8Stream(label) { + const decoder = createUTF8StrictStreamDecoder(label); + let decodeError = null; + let output = ""; + let finished = false; + const append = (text) => { + const next = output + text; + output = next.length <= 65_536 ? next : next.slice(-65_536); + }; + return { + error: () => decodeError, + finish() { + if (finished || decodeError !== null) return; + finished = true; + try { + append(decoder.finish()); + } catch (error) { + decodeError = error; + } + }, + value: () => output, + write(chunk) { + if (finished || decodeError !== null) return; + try { + append(decoder.write(chunk)); + } catch (error) { + decodeError = error; + } + }, + }; } diff --git a/scripts/browser-proof-execution.test.mjs b/scripts/browser-proof-execution.test.mjs index 54e7f6a..7748de7 100644 --- a/scripts/browser-proof-execution.test.mjs +++ b/scripts/browser-proof-execution.test.mjs @@ -46,6 +46,18 @@ test("browser proof server readiness timeout terminates the child", async () => assert.equal(child.listenerCount("exit"), 0); }); +test("browser proof server rejects malformed UTF-8 without disclosing buffered output", async () => { + const child = new FakeChild((signal) => { + if (signal === "SIGTERM") child.exit(signal); + }); + queueMicrotask(() => child.stdout.write(Buffer.from([0x67, 0x68, 0x70, 0x5f, 0xff]))); + await assert.rejects( + startBrowserServer("server", {spawnProcess: () => child, readinessTimeoutMs: 50, stopTimeoutMs: 50}), + (error) => error.message.includes("not valid UTF-8") && !error.message.includes("ghp_"), + ); + assert.deepEqual(child.signals, ["SIGTERM"]); +}); + test("browser proof server cleanup escalates from SIGTERM to SIGKILL", async () => { const child = new FakeChild((signal) => { if (signal === "SIGKILL") child.exit(signal); diff --git a/scripts/browser-proof-inputs.mjs b/scripts/browser-proof-inputs.mjs index 405081b..dddb95c 100644 --- a/scripts/browser-proof-inputs.mjs +++ b/scripts/browser-proof-inputs.mjs @@ -3,13 +3,14 @@ import {createHash} from "node:crypto"; import {lstatSync, mkdirSync, readFileSync, writeFileSync} from "node:fs"; import {dirname, isAbsolute, join} from "node:path"; +import {decodeUTF8Strict} from "./stable-json.mjs"; + export const browserProofInputManifestPath = "scripts/browser-runtime-proof-inputs.v1.json"; export function loadBrowserProofInputResolution() { - const encoded = process.env.PROOFKIT_BROWSER_INPUT_RESOLUTION ?? execFileSync( - "go", - ["run", "./internal/tools/browserproofverify", "--resolve-inputs"], - {encoding: "utf8"}, + const encoded = process.env.PROOFKIT_BROWSER_INPUT_RESOLUTION ?? decodeUTF8Strict( + execFileSync("go", ["run", "./internal/tools/browserproofverify", "--resolve-inputs"]), + "browser proof input resolution output", ); const resolution = JSON.parse(encoded); if (!resolution || typeof resolution !== "object" || Array.isArray(resolution)) throw new Error("browser proof input resolution must be an object"); diff --git a/scripts/source-hygiene.mjs b/scripts/source-hygiene.mjs index 779edae..5b33c2a 100644 --- a/scripts/source-hygiene.mjs +++ b/scripts/source-hygiene.mjs @@ -2,6 +2,8 @@ import { execFileSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; +import {decodeUTF8Strict} from "./stable-json.mjs"; + const textExtensions = new Set([ ".css", ".go", @@ -39,13 +41,13 @@ function containsOrganizationSpecificToken(text) { } function trackedFiles() { - return execFileSync("git", ["ls-files", "-z"], { encoding: "utf8" }) + return decodeUTF8Strict(execFileSync("git", ["ls-files", "-z"]), "Git file inventory") .split("\0") .filter(Boolean); } function trackedIndexEntries() { - return execFileSync("git", ["ls-files", "-s", "-z"], { encoding: "utf8" }) + return decodeUTF8Strict(execFileSync("git", ["ls-files", "-s", "-z"]), "Git index inventory") .split("\0") .filter(Boolean) .map((entry) => { @@ -69,9 +71,10 @@ for (const entry of trackedIndexEntries()) { continue; } - const lowerText = execFileSync("git", ["cat-file", "-p", entry.object], { - encoding: "utf8", - }).toLowerCase(); + const lowerText = decodeUTF8Strict( + execFileSync("git", ["cat-file", "-p", entry.object]), + "tracked text object", + ).toLowerCase(); if (containsOrganizationSpecificToken(lowerText)) { organizationSpecific.add(entry.file); } @@ -82,7 +85,7 @@ for (const file of trackedFiles()) { continue; } - const lowerText = readFileSync(file, "utf8").toLowerCase(); + const lowerText = decodeUTF8Strict(readFileSync(file), "worktree text file").toLowerCase(); if (containsOrganizationSpecificToken(lowerText)) { organizationSpecific.add(file); } diff --git a/scripts/source_hygiene_test.go b/scripts/source_hygiene_test.go index 8944d35..fd13253 100644 --- a/scripts/source_hygiene_test.go +++ b/scripts/source_hygiene_test.go @@ -82,6 +82,47 @@ func TestSourceHygieneIgnoresTokenSubstringsInsideContentDigests(t *testing.T) { } } +func TestSourceHygieneRejectsMalformedUTF8(t *testing.T) { + for _, test := range []struct { + name string + stageBad bool + wantLabel string + }{ + {name: "staged blob", stageBad: true, wantLabel: "tracked text object is not valid UTF-8"}, + {name: "worktree file", wantLabel: "worktree text file is not valid UTF-8"}, + } { + t.Run(test.name, func(t *testing.T) { + scriptPath := sourceHygieneScriptPath(t) + tempDir := t.TempDir() + runCommand(t, tempDir, "git", "init") + path := filepath.Join(tempDir, "fixture.md") + if err := os.WriteFile(path, []byte("clean\n"), 0o644); err != nil { + t.Fatal(err) + } + runCommand(t, tempDir, "git", "add", "fixture.md") + malformed := []byte{'g', 'h', 'p', '_', 0xff} + if test.stageBad { + if err := os.WriteFile(path, malformed, 0o644); err != nil { + t.Fatal(err) + } + runCommand(t, tempDir, "git", "add", "fixture.md") + } else if err := os.WriteFile(path, malformed, 0o644); err != nil { + t.Fatal(err) + } + + command := exec.Command("node", scriptPath) + command.Dir = tempDir + output, err := command.CombinedOutput() + if err == nil { + t.Fatalf("source hygiene accepted malformed UTF-8: %s", output) + } + if !strings.Contains(string(output), test.wantLabel) || strings.Contains(string(output), "ghp_") { + t.Fatalf("source hygiene diagnostic = %q, want fixed label without input bytes", output) + } + }) + } +} + func assertSourceHygieneRejects(t *testing.T, scriptPath string, repoRoot string, file string, evidenceClass string) { t.Helper() command := exec.Command("node", scriptPath) diff --git a/scripts/stable-json.mjs b/scripts/stable-json.mjs index f92c471..c05fe6a 100644 --- a/scripts/stable-json.mjs +++ b/scripts/stable-json.mjs @@ -10,15 +10,171 @@ export function stableJSONStringify(value) { case "object": if (value === null) return "null"; if (Array.isArray(value)) return `[${value.map(stableJSONStringify).join(",")}]`; - return `{${Object.keys(value).sort().map((key) => + return `{${Object.keys(value).map(assertScalarString).sort(compareScalarStrings).map((key) => `${quote(key)}:${stableJSONStringify(value[key])}`).join(",")}}`; default: throw new Error(`unsupported stable JSON value type: ${typeof value}`); } } +export function decodeUTF8Strict(bytes, fixedLabel = "input") { + try { + return new TextDecoder("utf-8", {fatal: true}).decode(bytes); + } catch { + throw new Error(`${fixedLabel} is not valid UTF-8`); + } +} + +export function createUTF8StrictStreamDecoder(fixedLabel = "input") { + const decoder = new TextDecoder("utf-8", {fatal: true}); + let finished = false; + const decode = (bytes, options) => { + try { + return decoder.decode(bytes, options); + } catch { + throw new Error(`${fixedLabel} is not valid UTF-8`); + } + }; + return { + finish() { + if (finished) return ""; + finished = true; + return decode(); + }, + write(bytes) { + if (finished) throw new Error(`${fixedLabel} UTF-8 decoder is already finished`); + return decode(bytes, {stream: true}); + }, + }; +} + +export function redactDiagnosticValue(value) { + if (!isScalarString(value) || containsUnsafeScalar(value) || containsSecretLikeValue(value)) { + return ""; + } + const scalars = [...value]; + if (scalars.length <= 512) return value; + return `${scalars.slice(0, 512).join("")}...`; +} + +const unsafeScalarRanges = [ + [0x000000, 0x00001f, 1], + [0x00007f, 0x00009f, 1], + [0x0000ad, 0x000600, 1363], + [0x000601, 0x000605, 1], + [0x00061c, 0x0006dd, 193], + [0x00070f, 0x000890, 385], + [0x000891, 0x0008e2, 81], + [0x00180e, 0x00200b, 2045], + [0x00200c, 0x00200f, 1], + [0x002028, 0x002028, 1], + [0x002029, 0x002029, 1], + [0x00202a, 0x00202e, 1], + [0x002060, 0x002064, 1], + [0x002066, 0x00206f, 1], + [0x00feff, 0x00fff9, 250], + [0x00fffa, 0x00fffb, 1], + [0x0110bd, 0x0110cd, 16], + [0x013430, 0x01343f, 1], + [0x01bca0, 0x01bca3, 1], + [0x01d173, 0x01d17a, 1], + [0x0e0001, 0x0e0020, 31], + [0x0e0021, 0x0e007f, 1], +]; + function quote(value) { - return JSON.stringify(value) - .replaceAll("\u2028", "\\u2028") - .replaceAll("\u2029", "\\u2029"); + assertScalarString(value); + let output = '"'; + for (const character of value) { + const scalar = character.codePointAt(0); + switch (scalar) { + case 0x22: output += '\\"'; break; + case 0x5c: output += "\\\\"; break; + case 0x08: output += "\\b"; break; + case 0x09: output += "\\t"; break; + case 0x0a: output += "\\n"; break; + case 0x0c: output += "\\f"; break; + case 0x0d: output += "\\r"; break; + default: + output += isUnsafeScalar(scalar) ? unicodeEscape(scalar) : character; + } + } + return `${output}"`; +} + +function assertScalarString(value) { + if (!isScalarString(value)) { + throw new Error("stable JSON strings must contain only Unicode scalar values"); + } + return value; +} + +function isScalarString(value) { + for (let index = 0; index < value.length; index += 1) { + const unit = value.charCodeAt(index); + if (unit >= 0xd800 && unit <= 0xdbff) { + const low = value.charCodeAt(index + 1); + if (!(low >= 0xdc00 && low <= 0xdfff)) { + return false; + } + index += 1; + } else if (unit >= 0xdc00 && unit <= 0xdfff) { + return false; + } + } + return true; +} + +function compareScalarStrings(left, right) { + const leftScalars = Array.from(left, (character) => character.codePointAt(0)); + const rightScalars = Array.from(right, (character) => character.codePointAt(0)); + const sharedLength = Math.min(leftScalars.length, rightScalars.length); + for (let index = 0; index < sharedLength; index += 1) { + if (leftScalars[index] !== rightScalars[index]) return leftScalars[index] - rightScalars[index]; + } + return leftScalars.length - rightScalars.length; +} + +export function isUnsafeScalar(value) { + let low = 0; + let high = unsafeScalarRanges.length; + while (low < high) { + const middle = Math.floor((low + high) / 2); + if (unsafeScalarRanges[middle][1] < value) low = middle + 1; + else high = middle; + } + if (low === unsafeScalarRanges.length) return false; + const [start, end, step] = unsafeScalarRanges[low]; + return value >= start && value <= end && (value - start) % step === 0; +} + +function containsUnsafeScalar(value) { + for (const character of value) { + if (isUnsafeScalar(character.codePointAt(0))) return true; + } + return false; +} + +function containsSecretLikeValue(value) { + return [ + /authorization\s*:\s*[^\r\n]+/iu, + /bearer\s+[A-Za-z0-9._~+/=-]{8,}/iu, + /(?:access[-_]?token|api[-_]?key|pass(?:word|wd)|secret|token)\s*[=:]\s*\S+/iu, + /github_pat_[A-Za-z0-9_]+/iu, + /gh[pousr]_[A-Za-z0-9_]+/iu, + /sk-(?:proj-)?[A-Za-z0-9_-]{10,}/iu, + /xox[abprs]-[A-Za-z0-9-]+/iu, + /glpat-[A-Za-z0-9_-]+/iu, + /[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/iu, + /-----BEGIN [A-Z ]*PRIVATE KEY-----/iu, + /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/u, + ].some((pattern) => pattern.test(value)); +} + +function unicodeEscape(value) { + if (value <= 0xffff) return `\\u${value.toString(16).padStart(4, "0")}`; + const adjusted = value - 0x10000; + const high = 0xd800 + (adjusted >> 10); + const low = 0xdc00 + (adjusted & 0x3ff); + return `\\u${high.toString(16)}\\u${low.toString(16)}`; } diff --git a/scripts/stable-json.test.mjs b/scripts/stable-json.test.mjs index 8a5c82d..0c65a25 100644 --- a/scripts/stable-json.test.mjs +++ b/scripts/stable-json.test.mjs @@ -1,7 +1,13 @@ import assert from "node:assert/strict"; import test from "node:test"; -import {stableJSONStringify} from "./stable-json.mjs"; +import { + createUTF8StrictStreamDecoder, + decodeUTF8Strict, + isUnsafeScalar, + redactDiagnosticValue, + stableJSONStringify, +} from "./stable-json.mjs"; test("stableJSONStringify sorts object keys without HTML escaping", () => { assert.equal( @@ -14,3 +20,137 @@ test("stableJSONStringify rejects unsupported values", () => { assert.throws(() => stableJSONStringify({missing: undefined}), /unsupported stable JSON value type/); assert.throws(() => stableJSONStringify(Number.NaN), /must be finite/); }); + +test("stableJSONStringify escapes unsafe scalars and preserves scalar key order", () => { + assert.equal( + stableJSONStringify({value: "\u007f\u0085\u200b\u2028\u2029\u{e0001}", "\u{10000}": "supplementary", "\ue000": "bmp"}), + `{"value":"\\u007f\\u0085\\u200b\\u2028\\u2029\\udb40\\udc01","\ue000":"bmp","\u{10000}":"supplementary"}`, + ); +}); + +test("stable JSON Unicode corpus", () => { + const cases = [ + ["alpha", `{"value":"alpha"}`], + ["<&>", `{"value":"<&>"}`], + ["\u{1f600}", `{"value":"\u{1f600}"}`], + ["e\u0301", `{"value":"e\u0301"}`], + ["\b\t\n\f\r", `{"value":"\\b\\t\\n\\f\\r"}`], + ["\0", `{"value":"\\u0000"}`], + ["\u007f", `{"value":"\\u007f"}`], + ["\u0085", `{"value":"\\u0085"}`], + ["\u200b", `{"value":"\\u200b"}`], + ["\u{e0001}", `{"value":"\\udb40\\udc01"}`], + ["\u2028", `{"value":"\\u2028"}`], + ["\u2029", `{"value":"\\u2029"}`], + ]; + for (const [value, expected] of cases) { + const encoded = stableJSONStringify({value}); + assert.equal(encoded, expected); + assert.equal(JSON.parse(encoded).value, value); + } + assert.equal(stableJSONStringify({"\u200b": "value"}), `{"\\u200b":"value"}`); + assert.equal( + stableJSONStringify({"\u{10000}": "supplementary", "\ue000": "bmp"}), + `{"\ue000":"bmp","\u{10000}":"supplementary"}`, + ); +}); + +test("stable JSON Unicode table classifies every scalar and complement", () => { + const ranges = [ + [0x000000, 0x00001f, 1], [0x00007f, 0x00009f, 1], [0x0000ad, 0x000600, 1363], + [0x000601, 0x000605, 1], [0x00061c, 0x0006dd, 193], [0x00070f, 0x000890, 385], + [0x000891, 0x0008e2, 81], [0x00180e, 0x00200b, 2045], [0x00200c, 0x00200f, 1], + [0x002028, 0x002028, 1], [0x002029, 0x002029, 1], [0x00202a, 0x00202e, 1], + [0x002060, 0x002064, 1], [0x002066, 0x00206f, 1], [0x00feff, 0x00fff9, 250], + [0x00fffa, 0x00fffb, 1], [0x0110bd, 0x0110cd, 16], [0x013430, 0x01343f, 1], + [0x01bca0, 0x01bca3, 1], [0x01d173, 0x01d17a, 1], [0x0e0001, 0x0e0020, 31], + [0x0e0021, 0x0e007f, 1], + ]; + const expected = new Set(); + for (const [start, end, step] of ranges) { + for (let value = start; value <= end; value += step) expected.add(value); + } + for (let value = 0; value <= 0x10ffff; value += 1) { + if (value >= 0xd800 && value <= 0xdfff) continue; + assert.equal(isUnsafeScalar(value), expected.has(value), `U+${value.toString(16).padStart(4, "0")}`); + } +}); + +test("stableJSONStringify rejects every unpaired surrogate class", () => { + for (const value of ["\ud800", "\udc00", "\ud800a"]) { + assert.throws(() => stableJSONStringify({value}), /Unicode scalar values/); + assert.throws(() => stableJSONStringify({[value]: "key"}), /Unicode scalar values/); + } +}); + +test("stableJSONStringify rejects every unpaired surrogate code unit in keys and values", () => { + for (let unit = 0xd800; unit <= 0xdfff; unit += 1) { + const value = String.fromCharCode(unit); + assert.throws(() => stableJSONStringify({value}), /Unicode scalar values/); + assert.throws(() => stableJSONStringify({[value]: "key"}), /Unicode scalar values/); + } +}); + +test("stableJSONStringify admits every valid UTF-16 surrogate pair in keys and values", () => { + for (let high = 0xd800; high <= 0xdbff; high += 1) { + for (let low = 0xdc00; low <= 0xdfff; low += 1) { + const value = String.fromCharCode(high, low); + stableJSONStringify({value}); + stableJSONStringify({[value]: "key"}); + } + } +}); + +test("diagnostic whole-value redaction", () => { + const fixed = ""; + const families = [ + ["authorization_header", ["request failed: Authorization: Basic ", "YWxpY2U6c2VjcmV0"].join("")], + ["bearer_token", ["Bearer ", "abcdefghijklmnopqrstuvwxyz"].join("")], + ["api_key_label", ["api", "_key=", "abc123456789"].join("")], + ["access_token_label", ["access", "-token=", "abcdefghijklmnopqrstuvwxyz"].join("")], + ["password_label", ["pass", "wd=", "abcdefghijklmnopqrstuvwxyz"].join("")], + ["github_pat", ["github", "_pat_", "abcdefghijklmnopqrstuvwxyz"].join("")], + ["github_ghp", ["gh", "p_", "123456789012345678901234567890123456"].join("")], + ["openai_key", ["sk", "-proj-", "abcdefghijklmnop"].join("")], + ["slack_token", ["xox", "b-", "1234567890-", "abcdefghijklmnop"].join("")], + ["gitlab_token", ["gl", "pat-", "abcdefghijklmnop"].join("")], + ["url_credentials", ["https://user:", "password", "@example.test/repo.git"].join("")], + ["private_key_header", ["-----BEGIN OPENSSH ", "PRIVATE KEY-----"].join("")], + ["jwt_like", ["eyJhbGciOiJIUzI1NiJ9", ".", "eyJzdWIiOiIxMjMifQ", ".", "signature"].join("")], + ]; + const separators = ["\0", "\u007f", "\u0085", "\u200b", "\u{e0001}", "\u2028", "\u2029"]; + for (const [name, secret] of families) { + const wrapped = `prefix ${secret} suffix`; + assert.equal(redactDiagnosticValue(wrapped), fixed, `${name} contiguous`); + for (const separator of separators) { + for (const offset of [Math.floor(wrapped.length / 3), Math.floor(wrapped.length / 2), Math.floor(wrapped.length * 2 / 3)]) { + assert.equal(redactDiagnosticValue(wrapped.slice(0, offset) + separator + wrapped.slice(offset)), fixed, `${name} split`); + } + } + } + assert.equal(redactDiagnosticValue("safe diagnostic"), "safe diagnostic"); + assert.equal(redactDiagnosticValue("x".repeat(520)), "x".repeat(512) + "..."); + assert.equal(redactDiagnosticValue("\ud800"), fixed); +}); + +test("decodeUTF8Strict rejects malformed bytes without exposing them", () => { + assert.equal(decodeUTF8Strict(Buffer.from("valid"), "fixture"), "valid"); + assert.throws( + () => decodeUTF8Strict(Buffer.from([0x67, 0x68, 0x70, 0x5f, 0xff]), "fixture"), + (error) => error.message === "fixture is not valid UTF-8" && !error.message.includes("ghp_"), + ); +}); + +test("createUTF8StrictStreamDecoder preserves split scalars and rejects malformed tails", () => { + const valid = createUTF8StrictStreamDecoder("stream fixture"); + assert.equal(valid.write(Buffer.from([0xf0, 0x9f])), ""); + assert.equal(valid.write(Buffer.from([0x98, 0x80])), "\u{1f600}"); + assert.equal(valid.finish(), ""); + + const malformed = createUTF8StrictStreamDecoder("stream fixture"); + assert.equal(malformed.write(Buffer.from([0x67, 0x68, 0x70, 0x5f, 0xc3])), "ghp_"); + assert.throws( + () => malformed.finish(), + (error) => error.message === "stream fixture is not valid UTF-8" && !error.message.includes("ghp_"), + ); +}); diff --git a/scripts/stable_json_selector_test.go b/scripts/stable_json_selector_test.go new file mode 100644 index 0000000..cce9b5f --- /dev/null +++ b/scripts/stable_json_selector_test.go @@ -0,0 +1,47 @@ +package main + +import ( + "context" + "os/exec" + "testing" + "time" + + "github.com/research-engineering/agentic-proofkit/internal/testsupport/nodetestselector" +) + +func TestStableJSONJavaScriptUnicodePolicy(t *testing.T) { + runStableJSONJavaScriptSelector(t, "stableJSONStringify escapes unsafe scalars and preserves scalar key order") +} + +func TestStableJSONJavaScriptUnicodeCorpus(t *testing.T) { + runStableJSONJavaScriptSelector(t, "stable JSON Unicode corpus") +} + +func TestStableJSONJavaScriptExhaustiveUnicodeTable(t *testing.T) { + runStableJSONJavaScriptSelector(t, "stable JSON Unicode table classifies every scalar and complement") +} + +func TestStableJSONJavaScriptRejectsEverySurrogate(t *testing.T) { + runStableJSONJavaScriptSelector(t, "stableJSONStringify rejects every unpaired surrogate code unit in keys and values") +} + +func TestStableJSONJavaScriptAdmitsEverySurrogatePair(t *testing.T) { + runStableJSONJavaScriptSelector(t, "stableJSONStringify admits every valid UTF-16 surrogate pair in keys and values") +} + +func TestStableJSONJavaScriptDiagnosticWholeValueRedaction(t *testing.T) { + runStableJSONJavaScriptSelector(t, "diagnostic whole-value redaction") +} + +func runStableJSONJavaScriptSelector(t *testing.T, name string) { + t.Helper() + nodePath, err := exec.LookPath("node") + if err != nil { + t.Fatalf("locate node: %v", err) + } + ctx, cancel := context.WithTimeout(t.Context(), 15*time.Second) + defer cancel() + if err := nodetestselector.Run(ctx, nodePath, ".", "stable-json.test.mjs", name); err != nil { + t.Fatalf("run exact JavaScript selector: %v", err) + } +} diff --git a/scripts/validate-self-hosting-receipts.go b/scripts/validate-self-hosting-receipts.go index 598b2e3..5c4445e 100644 --- a/scripts/validate-self-hosting-receipts.go +++ b/scripts/validate-self-hosting-receipts.go @@ -316,25 +316,25 @@ func runProofkit(command string, inputPath string, outputPath string) error { result := exec.Command(binaryPath, command, "--input", inputPath) output, err := result.CombinedOutput() if err != nil { - return proofkitVerdict(command, err, output) + return proofkitVerdict(err, output) } if err := os.WriteFile(outputPath, output, 0o644); err != nil { return err } - return proofkitVerdict(command, nil, output) + return proofkitVerdict(nil, output) } -func proofkitVerdict(command string, processErr error, output []byte) error { +func proofkitVerdict(processErr error, output []byte) error { if processErr != nil { - return fmt.Errorf("%s failed: %w\n%s", command, processErr, string(output)) + return fmt.Errorf("proofkit command failed: %w", processErr) } reportOutput, err := admission.DecodeJSON(bytes.NewReader(output), maxJSONBytes) if err != nil { - return fmt.Errorf("%s emitted invalid JSON: %w", command, err) + return fmt.Errorf("proofkit command emitted invalid JSON: %w", err) } record, ok := reportOutput.(map[string]any) if !ok || record["state"] != "passed" { - return fmt.Errorf("%s did not pass", command) + return fmt.Errorf("proofkit command did not pass") } return nil } diff --git a/scripts/validate-self-hosting-receipts_test.go b/scripts/validate-self-hosting-receipts_test.go index f594f83..1afcc46 100644 --- a/scripts/validate-self-hosting-receipts_test.go +++ b/scripts/validate-self-hosting-receipts_test.go @@ -36,7 +36,7 @@ func TestRunProofkitVerdictCases(t *testing.T) { } for _, test := range cases { t.Run(test.name, func(t *testing.T) { - err := proofkitVerdict("fixture", test.processErr, []byte(test.output)) + err := proofkitVerdict(test.processErr, []byte(test.output)) if test.wantError == "" { if err != nil { t.Fatalf("proofkitVerdict() error=%v, want nil", err) @@ -50,6 +50,17 @@ func TestRunProofkitVerdictCases(t *testing.T) { } } +func TestProofkitVerdictDoesNotExposeChildOutput(t *testing.T) { + const sentinel = "Bearer child-output-secret-abcdefghijklmnop" + err := proofkitVerdict(errors.New("exit status 7"), []byte(sentinel)) + if err == nil { + t.Fatal("proofkitVerdict accepted a failed child process") + } + if strings.Contains(err.Error(), sentinel) || strings.Contains(err.Error(), "abcdefghijklmnop") { + t.Fatalf("proofkitVerdict exposed child output: %q", err) + } +} + func TestRunInvokesEveryRequiredSelfHostingAdmissionBoundary(t *testing.T) { parsed, err := parser.ParseFile(token.NewFileSet(), "validate-self-hosting-receipts.go", nil, 0) if err != nil { diff --git a/scripts/write-browser-proof.mjs b/scripts/write-browser-proof.mjs index 8056c27..857f91a 100644 --- a/scripts/write-browser-proof.mjs +++ b/scripts/write-browser-proof.mjs @@ -4,7 +4,7 @@ import {writeFileSync} from "node:fs"; import {executeBrowserProof} from "./browser-proof-execution.mjs"; import {isSafeRepoPath, loadBrowserProofInputResolution} from "./browser-proof-inputs.mjs"; -import {stableJSONStringify} from "./stable-json.mjs"; +import {decodeUTF8Strict, stableJSONStringify} from "./stable-json.mjs"; const testCommand = ["node_modules/@playwright/test/cli.js", "test"]; const resolution = loadBrowserProofInputResolution(); @@ -28,8 +28,11 @@ const {assets, projects, testResult} = execution; const inputResolution = {serverTarget: resolution.serverTarget, writerPath: resolution.writerPath}; const inputDigest = createHash("sha256").update(stableJSONStringify({assets, inputResolution})).digest("hex"); const engines = projects.map(({browserName, browserVersion}) => ({name: browserName, version: browserVersion})); -const sourceRevision = execFileSync("git", ["rev-parse", "HEAD"], {encoding: "utf8"}).trim(); -const sourceTreeState = execFileSync("git", ["status", "--porcelain=v1", "--untracked-files=all"], {encoding: "utf8"}).trim() === "" ? "clean" : "dirty"; +const sourceRevision = decodeUTF8Strict(execFileSync("git", ["rev-parse", "HEAD"]), "git source revision output").trim(); +const sourceTreeState = decodeUTF8Strict( + execFileSync("git", ["status", "--porcelain=v1", "--untracked-files=all"]), + "git source tree status output", +).trim() === "" ? "clean" : "dirty"; const record = { assets, command: {argv: testCommand, exitCode: testResult.status, inputMode: "materialized_snapshot", runner: "node"}, From c6b5fb6027a6df20114730ec8a9e8faa5c3fb56e Mon Sep 17 00:00:00 2001 From: iperev Date: Mon, 31 Aug 2026 13:24:02 +0200 Subject: [PATCH 02/11] fix: refresh workflow contract identity --- internal/app/cli_contract_test.go | 2 +- internal/app/command_contract_generated.go | 4 ++-- internal/app/testdata/v0.5-wire-observations.json | 6 +++--- internal/command/stackpreset/preset_ids_generated.go | 2 +- proofkit/cli-contract.v2.json | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index 25fb172..cbe057c 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -23,7 +23,7 @@ import ( ) const ( - cliContractPublicABISHA256 = "2af9b2e3b77b8309f295a2ffeeec70cb4de4d51cb6fbfa45e7733da810ddc1df" + cliContractPublicABISHA256 = "b6a9ca9e0e9f2cf4df12282d1d1d95d9412984a78a98976b72efb8bcaedcc299" 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 c9ecd5b..acad430 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 = "cbac189e838e846c66616ff6219e495c640cf9918cb831e0a4892c5dc38934ee" +const commandContractSourceSHA256 = "1cd9ae8ab6f5997d741fb95dd8846b1fdd66d0daaedec6630ee6ebf40e78f6d4" type generatedCommandContractMetadata struct { InputContractSHA256 string @@ -19,7 +19,7 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "binding-partition": {InputContractSHA256: "sha256:366ad082045af52b2ac6604f18626d0f285b2db73b45d9a82687b8d3b0d2b3fd", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.binding-partition.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:52840879e13a00ef9a4abaad6cdb33000511674d5f9003fb56f387fdf58fadc8", FlagChoices: map[string][]string{}}, "branch-authority": {InputContractSHA256: "sha256:8a3ed74978898593fbdbf1f7fa684dae450fbd9019edcd60d07f818d63363ed4", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.branch-authority.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3c7dc74842299b92cd5baf57cc8666e9415963091359e5faf654e28da89561f1", FlagChoices: map[string][]string{}}, "capability-map-admission": {InputContractSHA256: "sha256:efe87497cf6a32cfd1f9c9c191393cac8781d1e9211914a8a7b6aaa44e7f371d", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.capability-map-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:bfb32535fd51bed168111912acb1651527897fe59a592119dc0032ce9286c9db", FlagChoices: map[string][]string{}}, - "change-workflow-plan": {InputContractSHA256: "sha256:0851ad1ad3990009ca769e6f7e7e4402f289c35484123ccb88097ab21c3b9f18", 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:1a9f6fd253fa01c55f29990d67f1e46ead8068c7f16880df45a645021c26dae1", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}}, + "change-workflow-plan": {InputContractSHA256: "sha256:9ee0780f92de00257b389ae7f04c8a92e2ef52614139f56600332c9cc823919e", 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:e851ab050ce8623957c25bac08ab8d05e45918f763410310ba432c80e05ffd9d", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}}, "changed-path-set": {InputContractSHA256: "sha256:8fe97426a58969e3e8dcbd52ed44540666b4de6be0487e8a3bc5088ae9c0f933", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.changed-path-set.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:abccdbf78e67f633ce49c34e8849c03f08ce42fa934a4c68969720c5045bf593", FlagChoices: map[string][]string{}}, "completion-criteria": {InputContractSHA256: "sha256:99c49c44b001e40383787e4c55f66621b8a8315f09635f1baf2326dc09bec4e6", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.completion-criteria.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:c90bb9605c7a22914104701a068dded510534bdeb60f4d601555d46c2d3d8a6d", FlagChoices: map[string][]string{}}, "conformance-profile": {InputContractSHA256: "sha256: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{}}, diff --git a/internal/app/testdata/v0.5-wire-observations.json b/internal/app/testdata/v0.5-wire-observations.json index 8b847eb..9f4a4aa 100644 --- a/internal/app/testdata/v0.5-wire-observations.json +++ b/internal/app/testdata/v0.5-wire-observations.json @@ -5,12 +5,12 @@ "version": "0.5.0", "evidenceClass": "owner_authored_frozen_version_edge_observation", "previousPublicAbiSha256": "sha256:fc03740aea9e7f525a4388e5d7f557cde07e11b0db0c05101fe937c28a1129d9", - "currentPublicAbiSha256": "sha256:2af9b2e3b77b8309f295a2ffeeec70cb4de4d51cb6fbfa45e7733da810ddc1df", + "currentPublicAbiSha256": "sha256:b6a9ca9e0e9f2cf4df12282d1d1d95d9412984a78a98976b72efb8bcaedcc299", "addedCommandContracts": [ { "command": "change-workflow-plan", - "inputContractSha256": "sha256:0851ad1ad3990009ca769e6f7e7e4402f289c35484123ccb88097ab21c3b9f18", - "outputContractSha256": "sha256:1a9f6fd253fa01c55f29990d67f1e46ead8068c7f16880df45a645021c26dae1" + "inputContractSha256": "sha256:9ee0780f92de00257b389ae7f04c8a92e2ef52614139f56600332c9cc823919e", + "outputContractSha256": "sha256:e851ab050ce8623957c25bac08ab8d05e45918f763410310ba432c80e05ffd9d" }, { "command": "native-evidence-guidance", diff --git a/internal/command/stackpreset/preset_ids_generated.go b/internal/command/stackpreset/preset_ids_generated.go index 8682538..33f7a2e 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 = "cbac189e838e846c66616ff6219e495c640cf9918cb831e0a4892c5dc38934ee" +const presetContractSourceSHA256 = "1cd9ae8ab6f5997d741fb95dd8846b1fdd66d0daaedec6630ee6ebf40e78f6d4" var presetIDs = []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"} diff --git a/proofkit/cli-contract.v2.json b/proofkit/cli-contract.v2.json index 483d7cc..6bcd88e 100644 --- a/proofkit/cli-contract.v2.json +++ b/proofkit/cli-contract.v2.json @@ -859,7 +859,7 @@ "rootDefinitionDigest": "sha256:6632a481380bfb07d754ef622661b291f805b1b168b1824089bdd5cc9bdd4b0e", "nativeSource": { "path": "internal/command/changeworkflowplan", - "canonicalDigest": "sha256:53a35103619a6f992e670e165dd01e6277a32b04faaa26ea6fd799017ebb7dd4", + "canonicalDigest": "sha256:990cf34a04fb15f2a60339c0713e1644f26f1e81462a4daf171e3613acc87bce", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -894,7 +894,7 @@ "rootDefinitionDigest": "sha256:d0c7c2f1b2d78da4260757c8bdbccda4f9dfe4f69d3ba3a0af24338508f32da9", "nativeSource": { "path": "internal/command/changeworkflowplan", - "canonicalDigest": "sha256:53a35103619a6f992e670e165dd01e6277a32b04faaa26ea6fd799017ebb7dd4", + "canonicalDigest": "sha256:990cf34a04fb15f2a60339c0713e1644f26f1e81462a4daf171e3613acc87bce", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { From 134ea70a560c7a6d380ad900fc19b05c18b95b3d Mon Sep 17 00:00:00 2001 From: iperev Date: Mon, 31 Aug 2026 14:55:31 +0200 Subject: [PATCH 03/11] fix: close agent workflow review findings --- docs/proofkit-contract-map.md | 4 +- .../specs/proofkit-agent-workflow/overview.md | 23 +- .../requirements.v1.json | 8 +- internal/app/agent_workflow_command_test.go | 9 + internal/app/agent_workflow_topology_test.go | 131 +++++++ internal/app/app.go | 4 +- internal/app/app_test.go | 24 ++ internal/app/cli_contract_test.go | 2 +- internal/app/command_contract_generated.go | 10 +- internal/app/command_flag_constraints.go | 5 + internal/app/command_help.go | 4 - ...contract_current_wire_observations_test.go | 334 ++++++++++++++++++ .../compact_contract_release_closure_test.go | 40 ++- ...compact_contract_wire_observations_test.go | 20 ++ .../compact-v2-wire-observations.json | 1 - .../app/testdata/v0.5-wire-observations.json | 8 +- .../command/changeworkflowplan/admission.go | 6 +- .../command/changeworkflowplan/catalog.go | 91 +++++ .../change_workflow_plan_test.go | 3 + .../command/changeworkflowplan/decision.go | 23 +- internal/command/changeworkflowplan/model.go | 16 +- internal/command/changeworkflowplan/prompt.go | 51 +-- .../command/changeworkflowplan/prompt_test.go | 61 +++- internal/command/changeworkflowplan/report.go | 1 + .../command/changeworkflowplan/state_test.go | 94 ++++- .../changeworkflowplan/test_helpers_test.go | 6 +- .../nativeevidenceguidance/guidance.go | 65 ++-- .../nativeevidenceguidance/guidance_test.go | 75 ++-- .../stackpreset/preset_ids_generated.go | 2 +- .../testsupport/nodetestselector/selector.go | 74 +++- .../nodetestselector/selector_test.go | 24 ++ internal/tools/releasechange/record_test.go | 4 +- .../tools/workflowsmoke/workflow_smoke.go | 8 +- .../workflowsmoke/workflow_smoke_test.go | 9 + proofkit/cli-contract.v2.json | 35 +- proofkit/requirement-bindings.json | 31 +- release/change-record.v2.json | 4 +- scripts/stable-json.mjs | 16 +- scripts/stable_json_selector_test.go | 34 +- 39 files changed, 1090 insertions(+), 270 deletions(-) create mode 100644 internal/app/agent_workflow_topology_test.go create mode 100644 internal/app/compact_contract_current_wire_observations_test.go create mode 100644 internal/command/changeworkflowplan/catalog.go diff --git a/docs/proofkit-contract-map.md b/docs/proofkit-contract-map.md index 25f96cc..e321005 100644 --- a/docs/proofkit-contract-map.md +++ b/docs/proofkit-contract-map.md @@ -39,7 +39,7 @@ 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 | total checkpoint relation, reference-closed next-stage context, deterministic agent prompts, bounded text/JSON/envelope projections, and repository-neutral native-evidence guidance | 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 | +| 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 | | Adoption and scaffolding | `init`, `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` | adoption intent, aggregate adoption contract envelope, checklist facts, target paths, owner routes, caller-extracted stale authority vocabulary facts, explicit pre-spec capability observations, pilot records, stack preset id, optional init preset id | dry-run route selection, 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, pilot shape admission | final files, final requirements, rollout policy, text extraction from files, code observation extraction, pilot truth | selected child output, plan, 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 | @@ -123,7 +123,7 @@ Semantic context routes are `requirement-context-compose`, | State or goal | Next Proofkit route | Stop or escalation condition | |---|---|---| | The agent does not know where to start. | `init`, or `init --preset` with `fresh`, `code-baseline`, `code-audit`, `legacy`, or `change-set` | Treat output as dry-run route guidance only. Stop before scanning, writing files, or making requirements authoritative. | -| An agent needs a bounded, deterministic stage transition for an engineering change. | `change-workflow-plan`; 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. Stop before treating the plan or guidance 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-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. | | No admitted spec/profile exists and the caller has explicit capability observations. | `capability-map-admission`; use `trustMode: "code_baseline"` only when maintainers intentionally freeze current code, otherwise use `trustMode: "audit_from_code"`. | Stop before treating seeds as stable requirements. The consumer owns observation extraction, materialization, requirement meaning, and proof adequacy. | | No admitted spec/profile exists and no capability observations exist. | `scaffold-project-structure`, `adoption-workflow-plan`, or `stack-preset` | Stop before writing files; the consumer owns materialization, overwrite policy, and final requirement text. | | Candidate boundary is uncertain. | `adoption-doctor` or `gradual-adoption-guidance --agent-envelope` | Escalate to owner review when the boundary is advisory, ambiguous, or missing native witnesses. | diff --git a/docs/specs/proofkit-agent-workflow/overview.md b/docs/specs/proofkit-agent-workflow/overview.md index a415e06..c09a045 100644 --- a/docs/specs/proofkit-agent-workflow/overview.md +++ b/docs/specs/proofkit-agent-workflow/overview.md @@ -8,10 +8,15 @@ 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 - the ordered stages `architecture`, `design`, `implementation_plan`, + the optional built-in profile `proofkit.reviewed-change.v1`, whose ordered + stages are `architecture`, `design`, `implementation_plan`, `implementation`, `verification`, `pull_request`, and `closeout`. 2. `native-evidence-guidance` exposes one versioned 22-slot template for repository-owned falsifiers, oracles, bounds, lifecycle, and non-claims. + Every slot names one closed applicability class: `always`, + `declared_input_channels`, `environment_or_network_access`, + `external_process`, or `mutable_artifacts`. The latter four apply only when + 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. @@ -25,8 +30,9 @@ introduced. - `REQ-PROOFKIT-WORKFLOW-001`: one-time typed admission, pure deterministic projection, and derived-output authority boundary. -- `REQ-PROOFKIT-WORKFLOW-002`: exact seven-stage order, prefix/checkpoint - biconditional, and a disjoint terminal output. +- `REQ-PROOFKIT-WORKFLOW-002`: named optional built-in profile, exact + seven-stage order, prefix/checkpoint biconditional, and a disjoint terminal + output. - `REQ-PROOFKIT-WORKFLOW-003`: 28 active state rows, one terminal row, and an exact successor-state delta that preserves governing authority and context. - `REQ-PROOFKIT-WORKFLOW-004`: typed subject/finding resolution and internally @@ -39,15 +45,16 @@ introduced. capabilities outside explicit input, with no ambient repository authority, setup facade, hidden policy, or agent-route extension. - `REQ-PROOFKIT-WORKFLOW-007`: pure deterministic repository-neutral - native-evidence guidance from one versioned typed table. + native-evidence guidance from one versioned typed table with five closed + applicability classes. - `REQ-PROOFKIT-WORKFLOW-008`: display-safe caller admission, no-leak denial, bounded actionable text with required coordinates, and no styling of caller values. - `REQ-PROOFKIT-WORKFLOW-009`: explicit text/color selection and confinement of ANSI to eligible terminal stdout. -- `REQ-PROOFKIT-WORKFLOW-010`: one owner per semantic relation and the - factor-proven dedicated two-command carrier topology, with no runtime - experiment or external template residue. +- `REQ-PROOFKIT-WORKFLOW-010`: one production owner per semantic relation, + independently authored semantic oracles, and an AST-checked two-command + carrier topology with no runtime experiment or external template residue. - `REQ-PROOFKIT-WORKFLOW-011`: a finite factorized CLI relation closed across descriptor/dispatcher/family/help/root-contract/witness/generated/package surfaces, with npm-only non-runtime specification docs and cross-channel @@ -60,6 +67,8 @@ requirement. This workflow spec consumes those owners without duplicating them. ## Non-Claims - Generated prompts are not authenticated and need not be obeyed or adequate. +- The built-in reviewed-change profile is optional guidance, not universal + consuming-repository process policy or a custom workflow-graph engine. - Static prompt bytes do not prove provider token use or model reasoning. - Least dependency closure does not prove context truth, freshness, semantic sufficiency, or global minimality. diff --git a/docs/specs/proofkit-agent-workflow/requirements.v1.json b/docs/specs/proofkit-agent-workflow/requirements.v1.json index 48d9b6b..82cff2e 100644 --- a/docs/specs/proofkit-agent-workflow/requirements.v1.json +++ b/docs/specs/proofkit-agent-workflow/requirements.v1.json @@ -21,7 +21,7 @@ { "requirementId": "REQ-PROOFKIT-WORKFLOW-002", "ownerId": "proofkit.agent-workflow", - "invariant": "The workflow owns exactly the ordered stages architecture, design, implementation_plan, implementation, verification, pull_request, and closeout; completedStageIds is exactly one prefix of that order, every incomplete prefix has a non-null checkpoint and an active stage equal to the first stage outside the prefix, and the complete prefix has a null checkpoint if and only if the output is the disjoint workflow_complete variant with no active-stage, action, checkpoint-state, or prompt field.", + "invariant": "The optional built-in workflow profile proofkit.reviewed-change.v1 owns exactly the ordered stages architecture, design, implementation_plan, implementation, verification, pull_request, and closeout; every non-envelope plan names that profile, completedStageIds is exactly one prefix of that order, every incomplete prefix has a non-null checkpoint and an active stage equal to the first stage outside the prefix, and the complete prefix has a null checkpoint if and only if the output is the disjoint workflow_complete variant with no active-stage, action, checkpoint-state, or prompt field.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -86,7 +86,7 @@ { "requirementId": "REQ-PROOFKIT-WORKFLOW-007", "ownerId": "proofkit.agent-workflow", - "invariant": "Native-evidence guidance has exactly one versioned repository-neutral semantic owner that requires exactly the declared semantic owner, subject/currentness, execution root/source snapshot, exact argv, stdin/input bytes, toolchain/platform/locale/timezone/dependencies, positive control, independent near-miss falsifier, executable oracle, input/work bounds, environment/network policy, CPU/memory/disk/file-descriptor/PID bounds, stdout/stderr bounds, timeout/process-group/detached-child policy, exit mapping, nondisclosure, cleanup/crash inventory, receipt identity, currentness/trust, escalation, retirement, and non-claim slots; every admitted static-resource or CLI delivery is deterministically bound to that owner, and JSON and text are derived without input, mutable shared state, filesystem, Git, process, environment, clock, random, network, container, provider, or other ambient dependency.", + "invariant": "Native-evidence guidance has exactly one versioned repository-neutral semantic owner that emits exactly the declared semantic owner, subject/currentness, execution root/source snapshot, exact argv, stdin/input bytes, toolchain/platform/locale/timezone/dependencies, positive control, independent near-miss falsifier, executable oracle, input/work bounds, environment/network policy, CPU/memory/disk/file-descriptor/PID bounds, stdout/stderr bounds, timeout/process-group/detached-child policy, exit mapping, nondisclosure, cleanup/crash inventory, receipt identity, currentness/trust, escalation, retirement, and non-claim slots; every slot carries exactly one closed applicability class from always, declared_input_channels, environment_or_network_access, external_process, and mutable_artifacts so mechanism-specific decisions are conditional on the corresponding declared consumer mechanism; every admitted CLI delivery is deterministically bound to that owner, and JSON and text are derived without input, mutable shared state, filesystem, Git, process, environment, clock, random, network, container, provider, or other ambient dependency.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -99,7 +99,7 @@ { "requirementId": "REQ-PROOFKIT-WORKFLOW-008", "ownerId": "proofkit.agent-workflow", - "invariant": "Every workflow caller-visible value is admitted through its typed owner; display-safe values reject malformed UTF-8, secret-shaped content, ASCII controls, DEL, Unicode Cc, Cf, Zl, and Zp without echo; every active prompt packet projects observed fact, uncertainty, the resolved governingAuthorityRefId or a closed missing-owner escalation target, exact admitted coordinates, candidate action, a missing-consumer-witness state, non-claim, stop condition, and expected successor from command-owned tables, and when governingAuthorityRefId is null the packet forbids repository mutation until the caller supplies it; planner text contains the state, active stage and action when present, checkpoint, owner or escalation, stop condition, expected successor, omission count, and one non-claim within 24 lines and 8192 bytes; guidance text contains every slot ID, required decision, and completion criterion within 48 lines and 16384 bytes; caller-owned values are never terminal-styled.", + "invariant": "Every workflow caller-visible value is admitted through its typed owner; display-safe values reject malformed UTF-8, secret-shaped content, ASCII controls, DEL, Unicode Cc, Cf, Zl, and Zp without echo; every active prompt packet projects observed fact, uncertainty, the resolved governingAuthorityRefId or a closed missing-owner escalation target, exact admitted coordinates, candidate action, a missing-consumer-witness state, non-claim, stop condition, and expected successor from command-owned tables, and when governingAuthorityRefId is null the packet forbids repository mutation until the caller supplies it; planner text contains the state, active stage and action when present, checkpoint, owner or escalation, stop condition, expected successor, omission count, and one non-claim within 24 lines and 8192 bytes; guidance text contains every slot ID, applicability class, required decision, and completion criterion within 48 lines and 16384 bytes; caller-owned values are never terminal-styled.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -125,7 +125,7 @@ { "requirementId": "REQ-PROOFKIT-WORKFLOW-010", "ownerId": "proofkit.agent-workflow", - "invariant": "The capability has exactly one stage/action semantic owner and exactly one evidence-guidance semantic owner; an independently frozen factorized comparison over planner carriers, guidance owners, and guidance carriers selects exactly one dedicated planner CLI carrier and one dedicated guidance CLI carrier without duplicating guidance meaning, while the implementation retains one coherent workflow command family and no persisted experiment matrix, candidate receipt, source-codec variant, external prompt duplicate, runtime guidance resource, repository adapter, setup state, or generic agent executor; changing one stage or guidance slot changes one semantic owner before ordinary generated projections are refreshed.", + "invariant": "The production capability has exactly one workflow catalog declaration owning the stage, checkpoint-action, and action-profile relations and exactly one guidance-table declaration owning evidence slots; descriptor closure exposes exactly one required-input planner carrier and one no-input guidance carrier in one coherent command family, a source-topology oracle rejects duplicate or retired production owners and runtime guidance resources, and independent literal state, action-profile, and slot oracles falsify semantic drift without becoming production owners; the capability adds no persisted experiment matrix, candidate receipt, source-codec variant, external prompt duplicate, repository adapter, setup state, or generic agent executor.", "claimLevel": "blocking", "riskClass": "medium", "proofBindingRefs": ["proofkit/requirement-bindings.json"], diff --git a/internal/app/agent_workflow_command_test.go b/internal/app/agent_workflow_command_test.go index d584f1d..48b0739 100644 --- a/internal/app/agent_workflow_command_test.go +++ b/internal/app/agent_workflow_command_test.go @@ -221,6 +221,15 @@ func testAgentWorkflowHelpClasses(t *testing.T) { t.Fatalf("args=%v status=%d stdout=%q stderr=%q", item.args, status, stdout, stderr) } } + + 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}) + 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) + } + } + }) } func testAgentWorkflowTerminalCapabilityProduct(t *testing.T) { diff --git a/internal/app/agent_workflow_topology_test.go b/internal/app/agent_workflow_topology_test.go new file mode 100644 index 0000000..be65813 --- /dev/null +++ b/internal/app/agent_workflow_topology_test.go @@ -0,0 +1,131 @@ +package app + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "slices" + "sort" + "strings" + "testing" +) + +func TestAgentWorkflowSemanticOwnerTopology(t *testing.T) { + expectedCommands := []string{"change-workflow-plan", "native-evidence-guidance"} + actualCommands := []string{} + for _, descriptor := range commandDescriptors { + if descriptor.runner != commandRunnerAgentWorkflow { + continue + } + actualCommands = append(actualCommands, descriptor.name) + switch descriptor.name { + case "change-workflow-plan": + if descriptor.input != commandInputRequired || !slices.Equal(descriptor.semanticOwnerDirs, []string{"changeworkflowplan"}) { + t.Fatalf("planner carrier topology is invalid: %#v", descriptor) + } + case "native-evidence-guidance": + if descriptor.input != commandInputNone || !slices.Equal(descriptor.semanticOwnerDirs, []string{"nativeevidenceguidance"}) { + t.Fatalf("guidance carrier topology is invalid: %#v", descriptor) + } + default: + t.Fatalf("agent workflow runner has surplus carrier %s", descriptor.name) + } + } + sort.Strings(actualCommands) + if !slices.Equal(actualCommands, expectedCommands) { + t.Fatalf("agent workflow carriers=%v want %v", actualCommands, expectedCommands) + } + + familyCount := 0 + for _, family := range generatedCommandFamilyCatalog().Families { + if family.ID != "agent-workflow-planning" { + continue + } + familyCount++ + if !slices.Equal(family.Commands, expectedCommands) { + t.Fatalf("agent workflow family carriers=%v want %v", family.Commands, expectedCommands) + } + } + if familyCount != 1 { + t.Fatalf("agent workflow family count=%d want 1", familyCount) + } + + root := repoRoot(t) + assertSingleSemanticTableOwner(t, filepath.Join(root, "internal/command/changeworkflowplan"), "workflowCatalog", "catalog.go") + assertSingleSemanticTableOwner(t, filepath.Join(root, "internal/command/nativeevidenceguidance"), "guidanceTable", "guidance.go") + for _, retiredOwner := range []struct { + dir string + name string + }{ + {filepath.Join(root, "internal/command/changeworkflowplan"), "stageTable"}, + {filepath.Join(root, "internal/command/changeworkflowplan"), "promptProfiles"}, + } { + if files := topLevelVariableOwners(t, retiredOwner.dir, retiredOwner.name); len(files) != 0 { + t.Fatalf("retired semantic owner %s remains in %v", retiredOwner.name, files) + } + } + assertNoRuntimeGuidanceResources(t, filepath.Join(root, "internal/command/changeworkflowplan")) + assertNoRuntimeGuidanceResources(t, filepath.Join(root, "internal/command/nativeevidenceguidance")) +} + +func assertSingleSemanticTableOwner(t *testing.T, dir, variable, expectedFile string) { + t.Helper() + files := topLevelVariableOwners(t, dir, variable) + if !slices.Equal(files, []string{expectedFile}) { + t.Fatalf("semantic owner %s files=%v want [%s]", variable, files, expectedFile) + } +} + +func topLevelVariableOwners(t *testing.T, dir, variable string) []string { + t.Helper() + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatal(err) + } + owners := []string{} + for _, entry := range entries { + if entry.IsDir() || filepath.Ext(entry.Name()) != ".go" || strings.HasSuffix(entry.Name(), "_test.go") { + continue + } + path := filepath.Join(dir, entry.Name()) + parsed, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.SkipObjectResolution) + if err != nil { + t.Fatalf("parse %s: %v", path, err) + } + for _, declaration := range parsed.Decls { + general, ok := declaration.(*ast.GenDecl) + if !ok || general.Tok != token.VAR { + continue + } + for _, specification := range general.Specs { + value := specification.(*ast.ValueSpec) + for _, name := range value.Names { + if name.Name == variable { + owners = append(owners, entry.Name()) + } + } + } + } + } + sort.Strings(owners) + return owners +} + +func assertNoRuntimeGuidanceResources(t *testing.T, dir string) { + t.Helper() + err := filepath.WalkDir(dir, func(path string, entry os.DirEntry, err error) error { + if err != nil { + return err + } + if entry.IsDir() || filepath.Ext(entry.Name()) == ".go" { + return nil + } + return fmt.Errorf("agent workflow owner contains runtime resource %s", path) + }) + if err != nil { + t.Fatal(err) + } +} diff --git a/internal/app/app.go b/internal/app/app.go index 9070606..b81a15b 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -52,11 +52,11 @@ func RunWithRendererAndCapabilities(ctx context.Context, args []string, stdin io } return writeText(commandUsageWithRenderer(descriptor, renderer), 0, nil, stdout, stderr) } - if descriptor.runner != commandRunnerHelp && hasCommandHelpFlag(args[1:]) { + parsedArguments := classifyDescriptorArguments(descriptor, args[1:]) + if descriptor.runner != commandRunnerHelp && (parsedArguments.present["--help"] || parsedArguments.present["-h"]) { writeDiagnosticf(stderr, "%s help accepts no additional arguments", descriptor.name) return 1 } - parsedArguments := classifyDescriptorArguments(descriptor, args[1:]) if err := validateFlagConstraints(descriptor, parsedArguments); err != nil { writeDiagnostic(stderr, err) return 1 diff --git a/internal/app/app_test.go b/internal/app/app_test.go index d271bfc..3ad378e 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -180,6 +180,30 @@ func TestJSONLayoutUsesDescriptorParsedFlags(t *testing.T) { } } +func TestCommandHelpUsesDescriptorParsedTokenRoles(t *testing.T) { + for _, descriptor := range commandDescriptors { + valueFlags := make([]string, 0) + for _, flag := range descriptor.allowedFlags { + if flagRequiresValue(flag) { + valueFlags = append(valueFlags, flag) + } + } + for _, valueFlag := range valueFlags { + for _, helpLikeValue := range []string{"--help", "-h"} { + t.Run(descriptor.name+"/"+valueFlag+"/"+helpLikeValue, func(t *testing.T) { + parsed := classifyDescriptorArguments(descriptor, []string{valueFlag, helpLikeValue}) + if parsed.present["--help"] || parsed.present["-h"] { + t.Fatalf("descriptor parser reclassified %s value %s as command help: %#v", valueFlag, helpLikeValue, parsed) + } + if !slices.Equal(parsed.values[valueFlag], []string{helpLikeValue}) { + t.Fatalf("descriptor parser value for %s = %v, want %s", valueFlag, parsed.values[valueFlag], helpLikeValue) + } + }) + } + } + } +} + func TestJSONLayoutPreservesFlagShapedInputPathAtProcessBoundary(t *testing.T) { t.Chdir(t.TempDir()) treeInput := `{"schemaVersion":2,"treeId":"consumer.tree","rootNodeId":"spec.root","callerAnnotations":[],"edges":[],"overlays":[],"nodes":[{"nodeId":"spec.root","nodeKind":"meta_spec","label":"Root","displayOrder":1,"callerAnnotations":[],"sourceRefs":[{"sourceRefId":"spec.root.requirements","sourceRefKind":"source_id","sourceRole":"requirements","sourceId":"consumer.requirements"}]}]}` diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index cbe057c..70977a9 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -23,7 +23,7 @@ import ( ) const ( - cliContractPublicABISHA256 = "b6a9ca9e0e9f2cf4df12282d1d1d95d9412984a78a98976b72efb8bcaedcc299" + cliContractPublicABISHA256 = "d0f923fe01da20aabbb1ea7c7fa972ad350f3486560488284e128051b0125d8e" 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 acad430..454626a 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 = "1cd9ae8ab6f5997d741fb95dd8846b1fdd66d0daaedec6630ee6ebf40e78f6d4" +const commandContractSourceSHA256 = "4065a82beed882107eff06781cec597db04ec2680b2981526ca14a39e5c431ad" type generatedCommandContractMetadata struct { InputContractSHA256 string @@ -19,7 +19,7 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "binding-partition": {InputContractSHA256: "sha256:366ad082045af52b2ac6604f18626d0f285b2db73b45d9a82687b8d3b0d2b3fd", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.binding-partition.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:52840879e13a00ef9a4abaad6cdb33000511674d5f9003fb56f387fdf58fadc8", FlagChoices: map[string][]string{}}, "branch-authority": {InputContractSHA256: "sha256:8a3ed74978898593fbdbf1f7fa684dae450fbd9019edcd60d07f818d63363ed4", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.branch-authority.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3c7dc74842299b92cd5baf57cc8666e9415963091359e5faf654e28da89561f1", FlagChoices: map[string][]string{}}, "capability-map-admission": {InputContractSHA256: "sha256:efe87497cf6a32cfd1f9c9c191393cac8781d1e9211914a8a7b6aaa44e7f371d", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.capability-map-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:bfb32535fd51bed168111912acb1651527897fe59a592119dc0032ce9286c9db", FlagChoices: map[string][]string{}}, - "change-workflow-plan": {InputContractSHA256: "sha256:9ee0780f92de00257b389ae7f04c8a92e2ef52614139f56600332c9cc823919e", 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:e851ab050ce8623957c25bac08ab8d05e45918f763410310ba432c80e05ffd9d", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}}, + "change-workflow-plan": {InputContractSHA256: "sha256:85fa3562d37a532ffbb8a2f7e2282af3d2b0d24c5b341fcb582f7adb2cb7ed50", 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:bcb22b1f930d1f63c24dd73681d9758eaaadfd41dde70360b432c21a55150803", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}}, "changed-path-set": {InputContractSHA256: "sha256:8fe97426a58969e3e8dcbd52ed44540666b4de6be0487e8a3bc5088ae9c0f933", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.changed-path-set.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:abccdbf78e67f633ce49c34e8849c03f08ce42fa934a4c68969720c5045bf593", FlagChoices: map[string][]string{}}, "completion-criteria": {InputContractSHA256: "sha256:99c49c44b001e40383787e4c55f66621b8a8315f09635f1baf2326dc09bec4e6", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.completion-criteria.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:c90bb9605c7a22914104701a068dded510534bdeb60f4d601555d46c2d3d8a6d", FlagChoices: map[string][]string{}}, "conformance-profile": {InputContractSHA256: "sha256: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{}}, @@ -37,10 +37,10 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "json-report-cli-adapter-source": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:dbde6809a1702e1161be48a85da1d0971f55be96c0f8432778dd5d5ce757c3b0", FlagChoices: map[string][]string{}}, "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{}}, "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{}}, - "native-evidence-guidance": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:ed40668acec3a8e2ae097b0ce011994e7facf6feda23e0057c4dcbe660a1a1b7", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}}, + "native-evidence-guidance": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:70d892de1ef24e113bed39bb648e2b51ef2dae812e985a9bee186cac20821ef4", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}}, "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{}}, "package-runtime-dependency-admission": {InputContractSHA256: "sha256:fc85887af9b8fcd899d245f0db30b2f2f68609822fc268126bf999082bb4115f", InputSchemaSummary: []string{"schemaVersion=1", "reportId", "expectedDependencySpec", "expectedLockfileIntegrity", "expectedPackageName", "expectedPackageVersion", "admissibleLocations{}", "packageResolution{}", "nonClaims[]", "root-shape-only definition proofkit.package-runtime-dependency-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:c012032e8c8212fd50bc2e85669cc610609ca2124ebc992c9e88f44a1ad2d5fc", FlagChoices: map[string][]string{}}, - "pilot-admission": {InputContractSHA256: "sha256: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:c6f47e00793881556f05739099b9a082f82961f876d343152761474fae4016c2", FlagChoices: map[string][]string{}}, + "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:91b93de58cb4ff03e3bd88eaddad4afaeef743d5ab37a44a6170382543f50638", FlagChoices: map[string][]string{}}, "producer-policy-self-proof": {InputContractSHA256: "sha256:d48e18826000c8d415f3c44b6c686e1da6ed962ef7ca36c9f705de8c68d034f9", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.producer-policy-self-proof.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:e82a3989a743f8babc6069f7af82b1dd1ea62bad8dbb18d95e105b36f74e4276", FlagChoices: map[string][]string{}}, "proof-obligation-algebra": {InputContractSHA256: "sha256: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{}}, "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{}}, @@ -78,7 +78,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{}}, "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{}}, "selective-gate-plan": {InputContractSHA256: "sha256:5293a5a4c7d8426cf637e6f8d252095ca0eb1714365bb89bec83307b778c678a", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.selective-gate-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:d7bffed853af5595af08b03859be01c283a3bdff1b3502d94ddc190889977647", FlagChoices: map[string][]string{}}, - "self-check": {InputContractSHA256: "sha256:768c1fc1d1aea0ab27e73e5b25927975153f5eac9887fb96ca07280640a39c46", 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:f5a0a7f7d177080ff2726b1827383c4bb8bace291c5af83d1d6a93bfb20b41c5", FlagChoices: map[string][]string{}}, + "self-check": {InputContractSHA256: "sha256:77332ca52a451d4f072de049f0a3e1e9f70ebaf447a144650ca1afafeb514dc1", 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:ce17ce8ca3da695924bf16aaa06ffde3d5505cb1a8d6828cc1dfaa55f3bb7d47", FlagChoices: map[string][]string{}}, "spec-overview-claims": {InputContractSHA256: "sha256:2490dcd34ba7485e13f8f33e8a288a0463c4c52cc6b0d82c57777466927e49a4", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.spec-overview-claims.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:554f3a7020e9820ccb90672629fd769c52b2f298f356040aa3b0a817666cbfbf", FlagChoices: map[string][]string{}}, "spec-proof-bundle-admission": {InputContractSHA256: "sha256:6b6c2875b6476e63a1911e7d6112d9999df2babbee969f84abc4c9e4b470c933", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.spec-proof-bundle-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:e9e0eb66cebca3b99fe5036fb2e7327a9284934ed76f58818d18094d0546fc52", FlagChoices: map[string][]string{}}, "stack-preset": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:f495e9ade4e1e7af7a8f8b2059f7611cc016e6080363afccf76d8dfc2dbc6d2d", FlagChoices: map[string][]string{"--preset": []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"}}}, diff --git a/internal/app/command_flag_constraints.go b/internal/app/command_flag_constraints.go index c83b869..ed0a117 100644 --- a/internal/app/command_flag_constraints.go +++ b/internal/app/command_flag_constraints.go @@ -16,6 +16,11 @@ func classifyDescriptorArguments(descriptor commandDescriptor, args []string) de parsed := descriptorArguments{counts: map[string]int{}, present: map[string]bool{}, values: map[string][]string{}} for index := 0; index < len(args); index++ { argument := args[index] + if argument == "--help" || argument == "-h" { + parsed.present[argument] = true + parsed.counts[argument]++ + continue + } if !slices.Contains(descriptor.allowedFlags, argument) { continue } diff --git a/internal/app/command_help.go b/internal/app/command_help.go index 0007f56..cd641b9 100644 --- a/internal/app/command_help.go +++ b/internal/app/command_help.go @@ -12,10 +12,6 @@ func isCommandHelpRequest(args []string) bool { return len(args) == 2 && args[0] != "help" && (args[1] == "--help" || args[1] == "-h") } -func hasCommandHelpFlag(args []string) bool { - return slices.Contains(args, "--help") || slices.Contains(args, "-h") -} - func commandUsage(descriptor commandDescriptor) string { return commandUsageWithRenderer(descriptor, cliexec.PathRenderer()) } diff --git a/internal/app/compact_contract_current_wire_observations_test.go b/internal/app/compact_contract_current_wire_observations_test.go new file mode 100644 index 0000000..b25e252 --- /dev/null +++ b/internal/app/compact_contract_current_wire_observations_test.go @@ -0,0 +1,334 @@ +package app + +import ( + "bytes" + "context" + "encoding/json" + "strings" + "testing" + "time" + + "github.com/research-engineering/agentic-proofkit/internal/command/requirementbrowser" + "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/compactproofcontract" +) + +func currentCompactV2WireObservations(t *testing.T) map[string]any { + t.Helper() + compactInput := compactV2WireContract() + contract, err := compactproofcontract.Admit(compactInput) + if err != nil { + t.Fatalf("admit current compact wire contract: %v", err) + } + resolver, err := contract.ResolverProjection(compactproofcontract.ResolverOptions{LocalEnvironmentClasses: []string{"local-go"}}) + if err != nil { + t.Fatalf("build current compact resolver observation: %v", err) + } + adoptionInput := strictJSONObjectFromText(t, cliAdoptionContractEnvelopeInput(), "current adoption aggregate input") + pilotDirect := cliPilotInput("proofkit.cli.pilot.first", false) + pilotEnvelope := strictJSONObjectFromText(t, cliPilotContractEnvelopeInput("all"), "current pilot envelope input") + conformanceInput := strictJSONObjectFromText(t, cliConformanceProfileInput(), "current conformance input") + conformanceOutput := runCLIForJSON(t, []string{"conformance-profile", "--input", "-", "--profile", "local"}, cliJSON(conformanceInput)) + + impactFixture := strings.Replace( + cliImpactInputComposeInput(), + `"paths":["docs/specs/proofkit-cli-impact/requirements.v1.json"]`, + `"paths":["docs/specs/proofkit-cli-impact/requirements.v1.json","internal/app/cli_abi_test.go"]`, + 1, + ) + impactComposeInput := strictJSONObjectFromText(t, impactFixture, "current impact compose input") + impactInput := runCLIForJSON(t, []string{"requirement-impact-input-compose", "--input", "-"}, cliJSON(impactComposeInput)) + impactOutput := runCLIForJSON(t, []string{"impact", "--input", "-"}, cliJSON(impactInput)) + + sourceInput := cliProofSourceSetInput(t, "canonical_contract") + sourceOutput := runAppJSON(t, []string{"requirement-proof-source-set", "--input", "-"}, sourceInput) + sourceFragment := strictJSONObjectFromText(t, sourceInput["sources"].([]any)[0].(map[string]any)["text"].(string), "current compact source fragment") + sourceRow := compactSourceRoleObservation(t, sourceInput["sourceSet"].(map[string]any)) + + coverageComposeInput := strictJSONObjectFromText(t, cliCoverageInputComposeInput(), "current coverage compose input") + coverageInput := runCLIForJSON(t, []string{"requirement-coverage-input-compose", "--input", "-"}, cliJSON(coverageComposeInput)) + coverageOutput := runCLIForJSON(t, []string{"requirement-coverage-view", "--input", "-", "--format", "json"}, cliJSON(coverageInput)) + coverageScenario := firstCompactCoverageScenario(t, coverageOutput) + + inventoryInput := strictJSONObjectFromText(t, cliProofBindingDerivedInventoryInput(), "current proof-binding inventory input") + inventoryOutput := runCLIForJSON(t, []string{"test-evidence-inventory", "--input", "-", "--projection", "proof-binding-derived", "--normalized-inventory"}, cliJSON(inventoryInput)) + proofViewOutput := runCLIForJSON(t, []string{"requirement-proof-view", "--input", "-", "--format", "json", "--local-environment-class", "local-go"}, cliJSON(compactInput)) + + result := map[string]any{ + compactWireObservationKey("adoption-contract-envelope", "input", "aggregate"): adoptionInput, + compactWireObservationKey("compact-contract", "input", "direct"): compactInput, + compactWireObservationKey("compact-resolver", "output", "resolver"): resolver, + compactWireObservationKey("conformance-profile", "input", "direct"): conformanceInput, + compactWireObservationKey("conformance-profile", "output", "profile"): conformanceOutput, + compactWireObservationKey("impact", "input", "direct"): impactInput, + compactWireObservationKey("impact", "output", "direct"): impactOutput, + compactWireObservationKey("pilot-admission", "input", "direct"): pilotDirect, + compactWireObservationKey("pilot-admission", "input", "envelope"): pilotEnvelope, + compactWireObservationKey("requirement-coverage-input-compose", "input", "wrapper"): coverageComposeInput, + compactWireObservationKey("requirement-coverage-input-compose", "output", "wrapper"): coverageInput, + compactWireObservationKey("requirement-coverage-view", "input", "wrapper"): coverageInput, + compactWireObservationKey("requirement-coverage-view", "output", "compact-scenario"): coverageScenario, + compactWireObservationKey("requirement-coverage-view", "output", "root"): coverageOutput, + compactWireObservationKey("requirement-impact-input-compose", "input", "wrapper"): impactComposeInput, + compactWireObservationKey("requirement-impact-input-compose", "output", "wrapper"): impactInput, + compactWireObservationKey("requirement-proof-resolver", "output", "resolver"): resolver, + compactWireObservationKey("requirement-proof-source-set", "input", "canonical-envelope"): sourceInput["canonicalEnvelope"], + compactWireObservationKey("requirement-proof-source-set", "input", "fragment"): sourceFragment, + compactWireObservationKey("requirement-proof-source-set", "input", "source-row"): sourceRow, + compactWireObservationKey("requirement-proof-source-set", "input", "source-set"): sourceInput["sourceSet"], + compactWireObservationKey("requirement-proof-source-set", "input", "wrapper"): sourceInput, + compactWireObservationKey("requirement-proof-source-set", "output", "wrapper"): sourceOutput, + compactWireObservationKey("requirement-proof-view", "input", "compact"): compactInput, + compactWireObservationKey("requirement-proof-view", "output", "compact"): proofViewOutput, + compactWireObservationKey("test-evidence-inventory", "input", "proof-binding"): inventoryInput, + compactWireObservationKey("test-evidence-inventory", "output", "normalized"): inventoryOutput, + } + for key, observation := range currentCompactParentContractObservations(t) { + result[key] = observation + } + for key, observation := range currentCompactContextChainObservations(t) { + result[key] = observation + } + return result +} + +func currentCompactContextChainObservations(t *testing.T) map[string]any { + t.Helper() + root := t.TempDir() + coverageCompose := strictJSONObjectFromText(t, cliCoverageInputComposeInput(), "compact context coverage compose input") + coverageInput := runCLIForJSON(t, []string{"requirement-coverage-input-compose", "--input", "-"}, cliJSON(coverageCompose)) + requirementSource := coverageCompose["requirementSource"].(map[string]any) + sourceID := requirementSource["sourceId"].(string) + tree := map[string]any{ + "schemaVersion": json.Number("2"), "treeId": "proofkit.compact.context-tree", "rootNodeId": "proofkit.compact.context-root", + "callerAnnotations": []any{}, "edges": []any{}, "overlays": []any{}, + "nodes": []any{map[string]any{ + "nodeId": "proofkit.compact.context-root", "nodeKind": "meta_spec", "label": "Compact context fixture", "displayOrder": json.Number("1"), + "callerAnnotations": []any{}, + "sourceRefs": []any{map[string]any{ + "sourceRefId": "proofkit.compact.context-root.requirements", "sourceRefKind": "source_id", + "sourceRole": "requirements", "sourceId": sourceID, + }}, + }}, + } + writeCLIJSONFixture(t, root, "proofkit/spec-tree.json", tree) + writeCLIJSONFixture(t, root, "docs/specs/compact/requirements.v1.json", requirementSource) + writeCLIJSONFixture(t, root, "proofkit/coverage-input.json", coverageInput) + catalog := map[string]any{ + "schemaVersion": json.Number("1"), "catalogId": "proofkit.compact.context", + "specTree": map[string]any{"path": "proofkit/spec-tree.json"}, + "requirementSources": []any{map[string]any{ + "nodeId": "proofkit.compact.context-root", "path": "docs/specs/compact/requirements.v1.json", + }}, + "coverage": map[string]any{"path": "proofkit/coverage-input.json"}, + } + base := runAppJSON(t, []string{"requirement-context-compose", "--input", "-", "--repo-root", root}, catalog) + sliceInput := map[string]any{ + "schemaVersion": json.Number("1"), "sliceId": "proofkit.compact.context.slice", "context": base, + "query": map[string]any{"profile": "coverage", "requirementIds": []any{"REQ-PROOFKIT-CLI-COVERAGE-001"}}, + } + sliceOutput := runAppJSON(t, []string{"requirement-context-slice", "--input", "-"}, sliceInput) + + requirements := requirementSource["requirements"].([]any) + requirements[0].(map[string]any)["invariant"] = "Compact context fixture changed invariant." + writeCLIJSONFixture(t, root, "docs/specs/compact/requirements.v1.json", requirementSource) + current := runAppJSON(t, []string{"requirement-context-compose", "--input", "-", "--repo-root", root}, catalog) + diffInput := map[string]any{ + "schemaVersion": json.Number("2"), "diffId": "proofkit.compact.context.diff", + "baseContext": base, "currentContext": current, + } + diffOutput := runAppJSON(t, []string{"requirement-semantic-diff", "--input", "-"}, diffInput) + graphInput := map[string]any{ + "schemaVersion": json.Number("2"), "graphId": "proofkit.compact.context.graph", "context": current, + } + graphOutput := runAppJSON(t, []string{"requirement-traceability-graph", "--input", "-"}, graphInput) + workspaceInput := map[string]any{ + "schemaVersion": json.Number("2"), "workspaceId": "proofkit.compact.context.workspace", "context": current, + "diffInput": diffInput, "graphInput": graphInput, + } + browserHandle, err := requirementbrowser.StartServer(workspaceInput, requirementbrowser.Options{Host: "127.0.0.1", Port: 0, PortSet: true, View: "workspace"}) + if err != nil { + t.Fatalf("start compact context workspace server: %v", err) + } + closeCompactObservationServer(t, browserHandle) + + baseCoverage := jsonObjectField(t, jsonObjectField(t, base, "projections"), "coverage") + currentCoverage := jsonObjectField(t, jsonObjectField(t, current, "projections"), "coverage") + sliceCoverage := jsonObjectField(t, jsonObjectField(t, sliceOutput, "projections"), "coverage") + baseIdentity := compactCoverageIdentityObservation(t, baseCoverage) + assertCompactIdentityObservation(t, sliceCoverage, baseIdentity, "context slice output") + assertCompactIdentityObservation(t, currentCoverage, baseIdentity, "current context output") + if diffOutput["baseSnapshotId"] != base["snapshotId"] || diffOutput["currentSnapshotId"] != current["snapshotId"] { + t.Fatalf("semantic diff output lost context identity: base=%v current=%v", diffOutput["baseSnapshotId"], diffOutput["currentSnapshotId"]) + } + if graphOutput["snapshotId"] != current["snapshotId"] { + t.Fatalf("traceability graph output snapshotId=%v want %v", graphOutput["snapshotId"], current["snapshotId"]) + } + if browserHandle.SnapshotID != current["snapshotId"] { + t.Fatalf("browser runtime snapshotId=%v want %v", browserHandle.SnapshotID, current["snapshotId"]) + } + baseCoverageDigest := compactWireValueDigest(t, baseCoverage) + return map[string]any{ + compactWireObservationKey("requirement-browser-server", "output", "workspace-runtime"): map[string]any{ + "snapshotId": browserHandle.SnapshotID, + }, + compactWireObservationKey("requirement-context-compose", "output", "coverage-runtime"): map[string]any{ + "coverageSha256": baseCoverageDigest, "identities": baseIdentity, + }, + compactWireObservationKey("requirement-context-slice", "output", "coverage-runtime"): map[string]any{ + "coverageSha256": compactWireValueDigest(t, sliceCoverage), "identities": compactCoverageIdentityObservation(t, sliceCoverage), + }, + compactWireObservationKey("requirement-semantic-diff", "output", "coverage-runtime"): map[string]any{ + "baseSnapshotId": diffOutput["baseSnapshotId"], "currentSnapshotId": diffOutput["currentSnapshotId"], + "outputSha256": compactWireValueDigest(t, diffOutput), + }, + compactWireObservationKey("requirement-traceability-graph", "output", "coverage-runtime"): map[string]any{ + "outputSha256": compactWireValueDigest(t, graphOutput), "snapshotId": graphOutput["snapshotId"], + }, + } +} + +func compactCoverageIdentityObservation(t *testing.T, value any) map[string]any { + t.Helper() + bindingIDs := map[string]struct{}{} + routeIDs := map[string]struct{}{} + collectCompactIdentityValues(value, bindingIDs, routeIDs) + if len(bindingIDs) == 0 || len(routeIDs) == 0 { + t.Fatalf("compact coverage projection has no stable identities: bindings=%v routes=%v", sortedSetKeys(bindingIDs), sortedSetKeys(routeIDs)) + } + return map[string]any{ + "bindingRecordIds": stringsToAny(sortedSetKeys(bindingIDs)), + "witnessRouteIds": stringsToAny(sortedSetKeys(routeIDs)), + } +} + +func collectCompactIdentityValues(value any, bindingIDs, routeIDs map[string]struct{}) { + switch typed := value.(type) { + case map[string]any: + for key, child := range typed { + if text, ok := child.(string); ok { + switch key { + case "bindingRecordId": + bindingIDs[text] = struct{}{} + case "witnessRouteId": + routeIDs[text] = struct{}{} + } + } + collectCompactIdentityValues(child, bindingIDs, routeIDs) + } + case []any: + for _, child := range typed { + collectCompactIdentityValues(child, bindingIDs, routeIDs) + } + } +} + +func assertCompactIdentityObservation(t *testing.T, value any, want map[string]any, context string) { + t.Helper() + got := compactCoverageIdentityObservation(t, value) + if !compactJSONEqual(got, want) { + t.Fatalf("%s identity projection=%v want %v", context, got, want) + } +} + +func stringsToAny(values []string) []any { + result := make([]any, len(values)) + for index, value := range values { + result[index] = value + } + return result +} + +func closeCompactObservationServer(t *testing.T, handle requirementbrowser.ServerHandle) { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := handle.Close(ctx); err != nil { + t.Fatalf("close compact context workspace server: %v", err) + } + select { + case err := <-handle.Done(): + if err != nil { + t.Fatalf("wait for compact context workspace server: %v", err) + } + case <-ctx.Done(): + t.Fatalf("wait for compact context workspace server: %v", ctx.Err()) + } +} + +func compactWireValueDigest(t *testing.T, value any) string { + t.Helper() + content, err := json.Marshal(value) + if err != nil { + t.Fatalf("encode compact nested wire value: %v", err) + } + return sha256Text(string(content)) +} + +func currentCompactParentContractObservations(t *testing.T) map[string]any { + t.Helper() + contract := readCLIContract(t) + commands := make(map[string]cliContractCommand, len(contract.Commands)) + for _, command := range contract.Commands { + commands[command.Command] = command + } + result := map[string]any{} + for _, key := range expectedCompactWireObservationKeys { + parts := strings.Split(key, "|") + if len(parts) != 3 || parts[2] != "union" { + continue + } + command, ok := commands[parts[0]] + if !ok { + t.Fatalf("expected parent observation references unknown command %s", parts[0]) + } + raw := command.InputContract + if parts[1] == "output" { + raw = command.OutputContract + } + if raw == nil { + t.Fatalf("expected parent observation %s has no contract", key) + } + content, err := json.Marshal(raw) + if err != nil { + t.Fatalf("encode expected parent observation %s: %v", key, err) + } + value, err := admission.DecodeJSON(bytes.NewReader(content), int64(len(content))) + if err != nil { + t.Fatalf("admit expected parent observation %s: %v", key, err) + } + record := value.(map[string]any) + result[key] = map[string]any{"contract": record} + } + return result +} + +func compactSourceRoleObservation(t *testing.T, sourceSet map[string]any) map[string]any { + t.Helper() + columns := sourceSet["source_columns"].([]any) + rows := sourceSet["sources"].([]any) + if len(rows) == 0 { + t.Fatal("current source set has no source rows") + } + row := rows[0].([]any) + for index, raw := range columns { + if raw == "role" { + return map[string]any{"role": row[index]} + } + } + t.Fatal("current source set has no role column") + return nil +} + +func firstCompactCoverageScenario(t *testing.T, report map[string]any) map[string]any { + t.Helper() + requirements := report["requirementCoverage"].([]any) + if len(requirements) == 0 { + t.Fatal("current compact coverage report has no requirements") + } + scenarios := requirements[0].(map[string]any)["scenarios"].([]any) + if len(scenarios) == 0 { + t.Fatal("current compact coverage report has no scenarios") + } + return scenarios[0].(map[string]any) +} diff --git a/internal/app/compact_contract_release_closure_test.go b/internal/app/compact_contract_release_closure_test.go index 78a2a2a..8eb56ca 100644 --- a/internal/app/compact_contract_release_closure_test.go +++ b/internal/app/compact_contract_release_closure_test.go @@ -436,7 +436,7 @@ func assertCompactParentContractChangeSetClosure(t *testing.T, manifest compactW func assertCompactParentContractSemanticClassification(t *testing.T, manifest compactWireManifest) { t.Helper() oldObservations := readCompactV1WireObservations(t) - currentObservations := readCompactV2WireObservations(t) + currentObservations := currentCompactV2WireObservations(t) metadataOnly := []string{} for _, delta := range manifest.Deltas { if delta.Class != "parent_contract" && delta.Class != "metadata_freshness" { @@ -457,7 +457,7 @@ func assertCompactParentContractSemanticClassification(t *testing.T, manifest co assertExactStringSet(t, metadataOnly, expectedCompactMetadataFreshnessDirections, "metadata-only CLI parent-contract direction closure") } -func compactWithoutNativeSourceDigest(t *testing.T, value any, context string) any { +func compactWithoutContractFreshnessDigests(t *testing.T, value any, context string) any { t.Helper() content, err := json.Marshal(value) if err != nil { @@ -475,6 +475,16 @@ func compactWithoutNativeSourceDigest(t *testing.T, value any, context string) a if !ok { t.Fatalf("%s must contain a contract object", context) } + delete(contract, "rootDefinitionDigest") + if nativeSources, ok := contract["nativeSources"].([]any); ok { + for index, raw := range nativeSources { + nativeSource, ok := raw.(map[string]any) + if !ok { + t.Fatalf("%s nativeSources[%d] must be an object", context, index) + } + delete(nativeSource, "canonicalDigest") + } + } nativeSource, ok := contract["nativeSource"].(map[string]any) if !ok { return record @@ -486,6 +496,32 @@ func compactWithoutNativeSourceDigest(t *testing.T, value any, context string) a return record } +func compactWithoutNativeSourceDigest(t *testing.T, value any, context string) any { + t.Helper() + content, err := json.Marshal(value) + if err != nil { + t.Fatalf("encode %s: %v", context, err) + } + clone, err := admission.DecodeJSON(bytes.NewReader(content), int64(len(content))) + if err != nil { + t.Fatalf("clone %s: %v", context, err) + } + record, ok := clone.(map[string]any) + if !ok { + t.Fatalf("%s must be an object", context) + } + contract, ok := record["contract"].(map[string]any) + if !ok { + t.Fatalf("%s must contain a contract object", context) + } + nativeSource, ok := contract["nativeSource"].(map[string]any) + if !ok { + return record + } + delete(nativeSource, "canonicalDigest") + return record +} + func validSHA256Digest(value string) bool { if len(value) != len("sha256:")+sha256.Size*2 || !strings.HasPrefix(value, "sha256:") { return false diff --git a/internal/app/compact_contract_wire_observations_test.go b/internal/app/compact_contract_wire_observations_test.go index 6c37541..76e2892 100644 --- a/internal/app/compact_contract_wire_observations_test.go +++ b/internal/app/compact_contract_wire_observations_test.go @@ -133,6 +133,26 @@ func TestCompactV2WireDeltasResolveAgainstFrozenVersionEdgeObservations(t *testi assertCompactWireDeltaFrontierClosure(t, manifest.Deltas, oldObservations, currentObservations) } +func TestCurrentCompactV2WireSemanticsMatchFrozenVersionEdge(t *testing.T) { + frozen := readCompactV2WireObservations(t) + current := currentCompactV2WireObservations(t) + assertExactStringSet(t, sortedMapKeys(frozen), expectedCompactWireObservationKeys, "frozen compact v2 observation closure") + assertExactStringSet(t, sortedMapKeys(current), expectedCompactWireObservationKeys, "current compact v2 observation closure") + for _, key := range expectedCompactWireObservationKeys { + frozenValue := frozen[key] + currentValue := current[key] + if strings.HasSuffix(key, "|union") { + frozenValue = compactWithoutContractFreshnessDigests(t, frozenValue, key+" frozen") + currentValue = compactWithoutContractFreshnessDigests(t, currentValue, key+" current") + } + if !compactJSONEqual(frozenValue, currentValue) { + pointers := []string{} + collectUncoveredCompactWireDiffs(frozenValue, true, currentValue, true, "", map[string]string{}, map[string]struct{}{}, &pointers) + t.Fatalf("current compact v2 observation %s drifted from the frozen version-edge semantics at %v", key, pointers) + } + } +} + func readCompactV1WireObservations(t *testing.T) map[string]any { t.Helper() return compactWireObservationMap(t, readCompactV1WireObservationDocument(t), "compact v1") diff --git a/internal/app/testdata/compact-v2-wire-observations.json b/internal/app/testdata/compact-v2-wire-observations.json index 7c472cd..c9fa5f1 100644 --- a/internal/app/testdata/compact-v2-wire-observations.json +++ b/internal/app/testdata/compact-v2-wire-observations.json @@ -5864,4 +5864,3 @@ ], "schemaVersion": 2 } - diff --git a/internal/app/testdata/v0.5-wire-observations.json b/internal/app/testdata/v0.5-wire-observations.json index 9f4a4aa..dff6994 100644 --- a/internal/app/testdata/v0.5-wire-observations.json +++ b/internal/app/testdata/v0.5-wire-observations.json @@ -5,17 +5,17 @@ "version": "0.5.0", "evidenceClass": "owner_authored_frozen_version_edge_observation", "previousPublicAbiSha256": "sha256:fc03740aea9e7f525a4388e5d7f557cde07e11b0db0c05101fe937c28a1129d9", - "currentPublicAbiSha256": "sha256:b6a9ca9e0e9f2cf4df12282d1d1d95d9412984a78a98976b72efb8bcaedcc299", + "currentPublicAbiSha256": "sha256:d0f923fe01da20aabbb1ea7c7fa972ad350f3486560488284e128051b0125d8e", "addedCommandContracts": [ { "command": "change-workflow-plan", - "inputContractSha256": "sha256:9ee0780f92de00257b389ae7f04c8a92e2ef52614139f56600332c9cc823919e", - "outputContractSha256": "sha256:e851ab050ce8623957c25bac08ab8d05e45918f763410310ba432c80e05ffd9d" + "inputContractSha256": "sha256:85fa3562d37a532ffbb8a2f7e2282af3d2b0d24c5b341fcb582f7adb2cb7ed50", + "outputContractSha256": "sha256:bcb22b1f930d1f63c24dd73681d9758eaaadfd41dde70360b432c21a55150803" }, { "command": "native-evidence-guidance", "inputContractSha256": "", - "outputContractSha256": "sha256:ed40668acec3a8e2ae097b0ce011994e7facf6feda23e0057c4dcbe660a1a1b7" + "outputContractSha256": "sha256:70d892de1ef24e113bed39bb648e2b51ef2dae812e985a9bee186cac20821ef4" } ], "breakingChangeIds": [ diff --git a/internal/command/changeworkflowplan/admission.go b/internal/command/changeworkflowplan/admission.go index 55b7796..81ec2e9 100644 --- a/internal/command/changeworkflowplan/admission.go +++ b/internal/command/changeworkflowplan/admission.go @@ -42,7 +42,7 @@ func admitInput(raw any) (admittedInput, error) { if err != nil { return admittedInput{}, err } - checkpointValue, err := admitCheckpoint(record["checkpoint"], len(completed) == len(stageTable)) + checkpointValue, err := admitCheckpoint(record["checkpoint"], len(completed) == len(workflowCatalog.Stages)) if err != nil { return admittedInput{}, err } @@ -74,13 +74,13 @@ func admitInput(raw any) (admittedInput, error) { func admitCompletedStages(raw any) ([]string, error) { values, ok := raw.([]any) - if !ok || len(values) > len(stageTable) { + if !ok || len(values) > len(workflowCatalog.Stages) { return nil, reject("proofkit.workflow.stage_prefix", "completedStageIds must be a stage prefix") } result := make([]string, len(values)) for index, rawValue := range values { value, ok := rawValue.(string) - if !ok || value != stageTable[index].ID { + if !ok || value != workflowCatalog.Stages[index].ID { return nil, reject("proofkit.workflow.stage_prefix", "completedStageIds must be a stage prefix") } result[index] = value diff --git a/internal/command/changeworkflowplan/catalog.go b/internal/command/changeworkflowplan/catalog.go new file mode 100644 index 0000000..2af4673 --- /dev/null +++ b/internal/command/changeworkflowplan/catalog.go @@ -0,0 +1,91 @@ +package changeworkflowplan + +const workflowProfileID = "proofkit.reviewed-change.v1" + +type stageDefinition struct { + ID string + FirstAction string +} + +type checkpointDefinition struct { + State string + Action string + UseStageAction bool +} + +type actionProfile struct { + CandidateAction string + ExpectedNextCheckpoint string + StopCondition string +} + +type workflowCatalogDefinition struct { + Stages []stageDefinition + CheckpointActions []checkpointDefinition + ActionProfiles map[string]actionProfile +} + +var workflowCatalog = workflowCatalogDefinition{ + Stages: []stageDefinition{ + {ID: "architecture", FirstAction: "author"}, + {ID: "design", FirstAction: "author"}, + {ID: "implementation_plan", FirstAction: "author"}, + {ID: "implementation", FirstAction: "implement"}, + {ID: "verification", FirstAction: "verify"}, + {ID: "pull_request", FirstAction: "open_pull_request"}, + {ID: "closeout", FirstAction: "closeout"}, + }, + CheckpointActions: []checkpointDefinition{ + {State: "not_started", UseStageAction: true}, + {State: "ready_for_review", Action: "review"}, + {State: "review_findings", Action: "repair"}, + {State: "review_passed", Action: "accept_stage"}, + }, + ActionProfiles: map[string]actionProfile{ + "author": { + CandidateAction: "Author only the active-stage artifact under the consuming repository's semantic owner and bind the result to a caller-owned artifact reference and digest.", + StopCondition: "Stop after the artifact and digest are ready for independent review.", + ExpectedNextCheckpoint: "ready_for_review", + }, + "implement": { + CandidateAction: "Implement only the accepted owner-scoped plan, preserving its non-claims and adding its native positive and negative proof paths.", + StopCondition: "Stop after the implementation artifact and digest are ready for independent review.", + ExpectedNextCheckpoint: "ready_for_review", + }, + "verify": { + CandidateAction: "Run the consuming repository's positive controls and independent near-miss falsifiers against the exact caller-declared subject.", + StopCondition: "Stop after bounded verification evidence and its subject digest are ready for independent review.", + ExpectedNextCheckpoint: "ready_for_review", + }, + "open_pull_request": { + CandidateAction: "Open a pull request only under consuming-repository policy for the exact reviewed head and expose its native checks without claiming merge authority.", + StopCondition: "Stop after the pull-request artifact and exact head digest are ready for independent review.", + ExpectedNextCheckpoint: "ready_for_review", + }, + "closeout": { + CandidateAction: "Assemble bounded closeout evidence for the exact reviewed subject without merging, releasing, rolling out, or declaring production readiness.", + StopCondition: "Stop after the closeout artifact and digest are ready for independent review.", + ExpectedNextCheckpoint: "ready_for_review", + }, + "review": { + CandidateAction: "Independently attempt to falsify the active-stage artifact under its declared owner, bounds, non-claims, and exact subject digest.", + StopCondition: "Stop with review_findings bound to every admitted finding, or review_passed bound to the unchanged assessed subject digest.", + ExpectedNextCheckpoint: "review_findings_or_review_passed", + }, + "repair": { + CandidateAction: "Repair every referenced finding under the same semantic owner, produce a new subject digest, and do not emit a pass claim.", + StopCondition: "Stop when the repaired artifact and new digest are ready for another independent review.", + ExpectedNextCheckpoint: "ready_for_review", + }, + "accept_stage": { + CandidateAction: "Apply only the reported successorStateDelta to the prior snapshot, preserve all context fields byte-for-byte, and submit the merged snapshot for ordinary admission.", + StopCondition: "Stop after constructing the merged immutable snapshot; do not infer execution, approval, merge, or release from stage acceptance.", + ExpectedNextCheckpoint: "successor_state_delta", + }, + }, +} + +func actionProfileFor(action string) (actionProfile, bool) { + profile, ok := workflowCatalog.ActionProfiles[action] + return profile, ok +} diff --git a/internal/command/changeworkflowplan/change_workflow_plan_test.go b/internal/command/changeworkflowplan/change_workflow_plan_test.go index 9cb065a..a06cf25 100644 --- a/internal/command/changeworkflowplan/change_workflow_plan_test.go +++ b/internal/command/changeworkflowplan/change_workflow_plan_test.go @@ -19,6 +19,9 @@ func TestWorkflowPurityPredicates(t *testing.T) { if plan["authority"] != "derived_non_authoritative_plan" { t.Fatal("plan does not deny semantic authority") } + if plan["workflowProfileId"] != workflowProfileID { + t.Fatal("plan does not name its optional built-in workflow profile") + } if len(plan["nonClaims"].([]any)) != len(boundaryNonClaims) { t.Fatal("plan lost boundary non-claims") } diff --git a/internal/command/changeworkflowplan/decision.go b/internal/command/changeworkflowplan/decision.go index e7881b3..fca2422 100644 --- a/internal/command/changeworkflowplan/decision.go +++ b/internal/command/changeworkflowplan/decision.go @@ -3,28 +3,23 @@ package changeworkflowplan var checkpointRelation = buildCheckpointRelation() func buildCheckpointRelation() []stateRow { - rows := make([]stateRow, 0, len(stageTable)*len(checkpointStates)+1) - for stageIndex, stage := range stageTable { - for _, state := range []string{"not_started", "ready_for_review", "review_findings", "review_passed"} { - action := stage.FirstAction - switch state { - case "ready_for_review": - action = "review" - case "review_findings": - action = "repair" - case "review_passed": - action = "accept_stage" + rows := make([]stateRow, 0, len(workflowCatalog.Stages)*len(workflowCatalog.CheckpointActions)+1) + for stageIndex, stage := range workflowCatalog.Stages { + for _, checkpoint := range workflowCatalog.CheckpointActions { + action := checkpoint.Action + if checkpoint.UseStageAction { + action = stage.FirstAction } rows = append(rows, stateRow{ Action: action, ActiveStageID: stage.ID, - CheckpointState: state, + CheckpointState: checkpoint.State, CompletedCount: stageIndex, OutputKind: "next_action", }) } } - return append(rows, stateRow{CompletedCount: len(stageTable), OutputKind: "workflow_complete", Terminal: true}) + return append(rows, stateRow{CompletedCount: len(workflowCatalog.Stages), OutputKind: "workflow_complete", Terminal: true}) } func decide(input admittedInput) (stateDecision, error) { @@ -50,7 +45,7 @@ func decide(input admittedInput) (stateDecision, error) { if row.Action == "accept_stage" { completed := append(cloneStrings(input.CompletedStageIDs), row.ActiveStageID) var nextCheckpoint *checkpoint - if len(completed) < len(stageTable) { + if len(completed) < len(workflowCatalog.Stages) { nextCheckpoint = &checkpoint{State: "not_started", FindingRefs: []string{}} } decision.SuccessorStateDelta = &successorStateDelta{Checkpoint: nextCheckpoint, CompletedStageIDs: completed} diff --git a/internal/command/changeworkflowplan/model.go b/internal/command/changeworkflowplan/model.go index 8f89576..4e60bfa 100644 --- a/internal/command/changeworkflowplan/model.go +++ b/internal/command/changeworkflowplan/model.go @@ -17,26 +17,12 @@ const ( missingConsumerWitness = "missing_consumer_witness" ) -var stageTable = []stageDefinition{ - {ID: "architecture", FirstAction: "author"}, - {ID: "design", FirstAction: "author"}, - {ID: "implementation_plan", FirstAction: "author"}, - {ID: "implementation", FirstAction: "implement"}, - {ID: "verification", FirstAction: "verify"}, - {ID: "pull_request", FirstAction: "open_pull_request"}, - {ID: "closeout", FirstAction: "closeout"}, -} - var boundaryNonClaims = []string{ "Change workflow plans do not authenticate caller assertions, context, digests, checkpoint state, or external state.", "Change workflow plans do not approve repository edits, merge, release, rollout, or production readiness.", "Change workflow plans do not execute or supervise agents, native witnesses, repository operations, providers, or CI.", "Change workflow prompts, reports, text, and envelopes are derived projections and are not semantic authority.", -} - -type stageDefinition struct { - ID string - FirstAction string + "The reviewed-change workflow is an optional built-in profile and is not a universal consuming-repository process policy.", } type contextRef struct { diff --git a/internal/command/changeworkflowplan/prompt.go b/internal/command/changeworkflowplan/prompt.go index f09935f..df00f58 100644 --- a/internal/command/changeworkflowplan/prompt.go +++ b/internal/command/changeworkflowplan/prompt.go @@ -6,57 +6,8 @@ const ( missingOwnerStop = "Do not mutate the repository until the caller supplies the consuming-repository semantic owner and governing authority coordinates." ) -type actionProfile struct { - CandidateAction string - ExpectedNextCheckpoint string - StopCondition string -} - -var promptProfiles = map[string]actionProfile{ - "author": { - CandidateAction: "Author only the active-stage artifact under the consuming repository's semantic owner and bind the result to a caller-owned artifact reference and digest.", - StopCondition: "Stop after the artifact and digest are ready for independent review.", - ExpectedNextCheckpoint: "ready_for_review", - }, - "implement": { - CandidateAction: "Implement only the accepted owner-scoped plan, preserving its non-claims and adding its native positive and negative proof paths.", - StopCondition: "Stop after the implementation artifact and digest are ready for independent review.", - ExpectedNextCheckpoint: "ready_for_review", - }, - "verify": { - CandidateAction: "Run the consuming repository's positive controls and independent near-miss falsifiers against the exact caller-declared subject.", - StopCondition: "Stop after bounded verification evidence and its subject digest are ready for independent review.", - ExpectedNextCheckpoint: "ready_for_review", - }, - "open_pull_request": { - CandidateAction: "Open a pull request only under consuming-repository policy for the exact reviewed head and expose its native checks without claiming merge authority.", - StopCondition: "Stop after the pull-request artifact and exact head digest are ready for independent review.", - ExpectedNextCheckpoint: "ready_for_review", - }, - "closeout": { - CandidateAction: "Assemble bounded closeout evidence for the exact reviewed subject without merging, releasing, rolling out, or declaring production readiness.", - StopCondition: "Stop after the closeout artifact and digest are ready for independent review.", - ExpectedNextCheckpoint: "ready_for_review", - }, - "review": { - CandidateAction: "Independently attempt to falsify the active-stage artifact under its declared owner, bounds, non-claims, and exact subject digest.", - StopCondition: "Stop with review_findings bound to every admitted finding, or review_passed bound to the unchanged assessed subject digest.", - ExpectedNextCheckpoint: "review_findings_or_review_passed", - }, - "repair": { - CandidateAction: "Repair every referenced finding under the same semantic owner, produce a new subject digest, and do not emit a pass claim.", - StopCondition: "Stop when the repaired artifact and new digest are ready for another independent review.", - ExpectedNextCheckpoint: "ready_for_review", - }, - "accept_stage": { - CandidateAction: "Apply only the reported successorStateDelta to the prior snapshot, preserve all context fields byte-for-byte, and submit the merged snapshot for ordinary admission.", - StopCondition: "Stop after constructing the merged immutable snapshot; do not infer execution, approval, merge, or release from stage acceptance.", - ExpectedNextCheckpoint: "successor_state_delta", - }, -} - func buildPrompt(input admittedInput, decision stateDecision, closure closureResult) (map[string]any, error) { - profile, ok := promptProfiles[decision.Action] + profile, ok := actionProfileFor(decision.Action) if !ok { return nil, invalidState("action has no prompt profile") } diff --git a/internal/command/changeworkflowplan/prompt_test.go b/internal/command/changeworkflowplan/prompt_test.go index 5bfc6eb..2191b9b 100644 --- a/internal/command/changeworkflowplan/prompt_test.go +++ b/internal/command/changeworkflowplan/prompt_test.go @@ -5,6 +5,49 @@ import ( "testing" ) +var expectedWorkflowActionProfiles = map[string]actionProfile{ + "author": { + CandidateAction: "Author only the active-stage artifact under the consuming repository's semantic owner and bind the result to a caller-owned artifact reference and digest.", + ExpectedNextCheckpoint: "ready_for_review", + StopCondition: "Stop after the artifact and digest are ready for independent review.", + }, + "implement": { + CandidateAction: "Implement only the accepted owner-scoped plan, preserving its non-claims and adding its native positive and negative proof paths.", + ExpectedNextCheckpoint: "ready_for_review", + StopCondition: "Stop after the implementation artifact and digest are ready for independent review.", + }, + "verify": { + CandidateAction: "Run the consuming repository's positive controls and independent near-miss falsifiers against the exact caller-declared subject.", + ExpectedNextCheckpoint: "ready_for_review", + StopCondition: "Stop after bounded verification evidence and its subject digest are ready for independent review.", + }, + "open_pull_request": { + CandidateAction: "Open a pull request only under consuming-repository policy for the exact reviewed head and expose its native checks without claiming merge authority.", + ExpectedNextCheckpoint: "ready_for_review", + StopCondition: "Stop after the pull-request artifact and exact head digest are ready for independent review.", + }, + "closeout": { + CandidateAction: "Assemble bounded closeout evidence for the exact reviewed subject without merging, releasing, rolling out, or declaring production readiness.", + ExpectedNextCheckpoint: "ready_for_review", + StopCondition: "Stop after the closeout artifact and digest are ready for independent review.", + }, + "review": { + CandidateAction: "Independently attempt to falsify the active-stage artifact under its declared owner, bounds, non-claims, and exact subject digest.", + ExpectedNextCheckpoint: "review_findings_or_review_passed", + StopCondition: "Stop with review_findings bound to every admitted finding, or review_passed bound to the unchanged assessed subject digest.", + }, + "repair": { + CandidateAction: "Repair every referenced finding under the same semantic owner, produce a new subject digest, and do not emit a pass claim.", + ExpectedNextCheckpoint: "ready_for_review", + StopCondition: "Stop when the repaired artifact and new digest are ready for another independent review.", + }, + "accept_stage": { + CandidateAction: "Apply only the reported successorStateDelta to the prior snapshot, preserve all context fields byte-for-byte, and submit the merged snapshot for ordinary admission.", + ExpectedNextCheckpoint: "successor_state_delta", + StopCondition: "Stop after constructing the merged immutable snapshot; do not infer execution, approval, merge, or release from stage acceptance.", + }, +} + func TestWorkflowPromptPredicates(t *testing.T) { t.Run("caller_not_styled", func(t *testing.T) { input := initialInput() @@ -19,15 +62,15 @@ func TestWorkflowPromptPredicates(t *testing.T) { } }) t.Run("candidate_action", func(t *testing.T) { - for index := range stageTable { + for index := range workflowCatalog.Stages { for _, state := range []string{"not_started", "ready_for_review", "review_findings", "review_passed"} { plan := requireBuild(t, inputForStage(index, state)) prompt := plan["prompt"].(map[string]any) if len(prompt) != 9 { t.Fatalf("prompt has %d fields", len(prompt)) } - profile := promptProfiles[plan["action"].(string)] - if prompt["candidateAction"] != profile.CandidateAction { + profile, exists := expectedWorkflowActionProfiles[plan["action"].(string)] + if !exists || prompt["candidateAction"] != profile.CandidateAction || prompt["expectedNextCheckpoint"] != profile.ExpectedNextCheckpoint { t.Fatal("candidate action drifted from action owner") } } @@ -49,9 +92,13 @@ func TestWorkflowPromptPredicates(t *testing.T) { } }) t.Run("expected_checkpoint", func(t *testing.T) { - for action, profile := range promptProfiles { - if profile.ExpectedNextCheckpoint == "" { - t.Fatalf("action %s has no expected checkpoint", action) + if len(workflowCatalog.ActionProfiles) != len(expectedWorkflowActionProfiles) { + t.Fatalf("action profile count=%d want %d", len(workflowCatalog.ActionProfiles), len(expectedWorkflowActionProfiles)) + } + for action, expected := range expectedWorkflowActionProfiles { + profile, exists := workflowCatalog.ActionProfiles[action] + if !exists || profile != expected { + t.Fatalf("action profile %s=%#v want %#v", action, profile, expected) } } }) @@ -105,7 +152,7 @@ func TestWorkflowPromptPredicates(t *testing.T) { } }) t.Run("stop_condition", func(t *testing.T) { - for action, profile := range promptProfiles { + for action, profile := range expectedWorkflowActionProfiles { if !strings.HasPrefix(profile.StopCondition, "Stop ") || strings.Contains(strings.ToLower(profile.StopCondition), "wait") { t.Fatalf("action %s has a non-terminal stop condition", action) } diff --git a/internal/command/changeworkflowplan/report.go b/internal/command/changeworkflowplan/report.go index 14c3b0c..cb8a44b 100644 --- a/internal/command/changeworkflowplan/report.go +++ b/internal/command/changeworkflowplan/report.go @@ -103,6 +103,7 @@ func planValue(value projection) map[string]any { "retainedContextRefIds": stringsValue(contextRefIDs(value.Closure.Retained)), "retainedContextRefs": contextRefsValue(value.Closure.Retained), "schemaVersion": json.Number("1"), + "workflowProfileId": workflowProfileID, } if value.Decision.OutputKind == "workflow_complete" { return plan diff --git a/internal/command/changeworkflowplan/state_test.go b/internal/command/changeworkflowplan/state_test.go index 18544fe..296ac57 100644 --- a/internal/command/changeworkflowplan/state_test.go +++ b/internal/command/changeworkflowplan/state_test.go @@ -1,11 +1,52 @@ package changeworkflowplan import ( + "fmt" "testing" "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" ) +type expectedWorkflowRow struct { + StageIndex int + StageID string + CheckpointState string + Action string + OutputKind string +} + +var expectedWorkflowStateMatrix = []expectedWorkflowRow{ + {0, "architecture", "not_started", "author", "next_action"}, + {0, "architecture", "ready_for_review", "review", "next_action"}, + {0, "architecture", "review_findings", "repair", "next_action"}, + {0, "architecture", "review_passed", "accept_stage", "next_action"}, + {1, "design", "not_started", "author", "next_action"}, + {1, "design", "ready_for_review", "review", "next_action"}, + {1, "design", "review_findings", "repair", "next_action"}, + {1, "design", "review_passed", "accept_stage", "next_action"}, + {2, "implementation_plan", "not_started", "author", "next_action"}, + {2, "implementation_plan", "ready_for_review", "review", "next_action"}, + {2, "implementation_plan", "review_findings", "repair", "next_action"}, + {2, "implementation_plan", "review_passed", "accept_stage", "next_action"}, + {3, "implementation", "not_started", "implement", "next_action"}, + {3, "implementation", "ready_for_review", "review", "next_action"}, + {3, "implementation", "review_findings", "repair", "next_action"}, + {3, "implementation", "review_passed", "accept_stage", "next_action"}, + {4, "verification", "not_started", "verify", "next_action"}, + {4, "verification", "ready_for_review", "review", "next_action"}, + {4, "verification", "review_findings", "repair", "next_action"}, + {4, "verification", "review_passed", "accept_stage", "next_action"}, + {5, "pull_request", "not_started", "open_pull_request", "next_action"}, + {5, "pull_request", "ready_for_review", "review", "next_action"}, + {5, "pull_request", "review_findings", "repair", "next_action"}, + {5, "pull_request", "review_passed", "accept_stage", "next_action"}, + {6, "closeout", "not_started", "closeout", "next_action"}, + {6, "closeout", "ready_for_review", "review", "next_action"}, + {6, "closeout", "review_findings", "repair", "next_action"}, + {6, "closeout", "review_passed", "accept_stage", "next_action"}, + {-1, "", "", "", "workflow_complete"}, +} + func TestWorkflowStatePredicates(t *testing.T) { commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.046999593271228979057731418772803126658652333387186990019354835268016126566224") t.Run("checkpoint_requires_incomplete", func(t *testing.T) { @@ -20,7 +61,7 @@ func TestWorkflowStatePredicates(t *testing.T) { } }) t.Run("first_incomplete_active", func(t *testing.T) { - for index, stage := range stageTable { + for index, stage := range workflowCatalog.Stages { plan := requireBuild(t, inputForStage(index, "not_started")) if plan["activeStageId"] != stage.ID { t.Fatalf("stage %d selected %v", index, plan["activeStageId"]) @@ -94,7 +135,7 @@ func TestWorkflowCheckpointPredicates(t *testing.T) { } }) t.Run("merged_successor_admitted", func(t *testing.T) { - for index := range stageTable { + for index := range workflowCatalog.Stages { prior := inputForStage(index, "review_passed") plan := requireBuild(t, prior) merged, err := MergeSuccessor(prior, plan["successorStateDelta"].(map[string]any)) @@ -107,7 +148,7 @@ func TestWorkflowCheckpointPredicates(t *testing.T) { } }) t.Run("review_passed_accepts", func(t *testing.T) { - for index := range stageTable { + for index := range workflowCatalog.Stages { plan := requireBuild(t, inputForStage(index, "review_passed")) if plan["action"] != "accept_stage" { t.Fatalf("stage %d action is %v", index, plan["action"]) @@ -116,7 +157,7 @@ func TestWorkflowCheckpointPredicates(t *testing.T) { }) t.Run("terminal_disjoint", func(t *testing.T) { terminal := requireBuild(t, terminalInput()) - for index := range stageTable { + for index := range workflowCatalog.Stages { for _, state := range []string{"not_started", "ready_for_review", "review_findings", "review_passed"} { active := requireBuild(t, inputForStage(index, state)) if active["outputKind"] == terminal["outputKind"] { @@ -125,22 +166,41 @@ func TestWorkflowCheckpointPredicates(t *testing.T) { } } }) - t.Run("twenty_eight_actions", func(t *testing.T) { - if len(checkpointRelation) != 29 { - t.Fatalf("relation has %d rows", len(checkpointRelation)) + t.Run("independent_twenty_nine_row_matrix", func(t *testing.T) { + if len(checkpointRelation) != len(expectedWorkflowStateMatrix) { + t.Fatalf("relation has %d rows, want %d", len(checkpointRelation), len(expectedWorkflowStateMatrix)) } - seen := 0 - for index := range stageTable { - for _, state := range []string{"not_started", "ready_for_review", "review_findings", "review_passed"} { - plan := requireBuild(t, inputForStage(index, state)) - if _, ok := plan["action"].(string); !ok { - t.Fatal("active row has no action") - } - seen++ + seen := map[string]struct{}{} + for _, expected := range expectedWorkflowStateMatrix { + name := expected.OutputKind + input := terminalInput() + if expected.StageIndex >= 0 { + name = fmt.Sprintf("%02d/%s", expected.StageIndex, expected.CheckpointState) + input = inputForStage(expected.StageIndex, expected.CheckpointState) } + t.Run(name, func(t *testing.T) { + plan := requireBuild(t, input) + if plan["outputKind"] != expected.OutputKind { + t.Fatalf("outputKind=%v want %s", plan["outputKind"], expected.OutputKind) + } + if expected.StageIndex < 0 { + if _, present := plan["action"]; present { + t.Fatalf("terminal plan contains action: %v", plan) + } + return + } + key := expected.StageID + "|" + expected.CheckpointState + if _, duplicate := seen[key]; duplicate { + t.Fatalf("duplicate expected matrix row %s", key) + } + seen[key] = struct{}{} + if plan["activeStageId"] != expected.StageID || plan["checkpointState"] != expected.CheckpointState || plan["action"] != expected.Action { + t.Fatalf("plan coordinates=%v/%v/%v want %s/%s/%s", plan["activeStageId"], plan["checkpointState"], plan["action"], expected.StageID, expected.CheckpointState, expected.Action) + } + }) } - if seen != 28 { - t.Fatalf("got %d active rows", seen) + if len(seen) != 28 { + t.Fatalf("matrix has %d unique active rows, want 28", len(seen)) } }) } diff --git a/internal/command/changeworkflowplan/test_helpers_test.go b/internal/command/changeworkflowplan/test_helpers_test.go index fcedec2..f57eda1 100644 --- a/internal/command/changeworkflowplan/test_helpers_test.go +++ b/internal/command/changeworkflowplan/test_helpers_test.go @@ -55,7 +55,7 @@ func inputForStage(stageIndex int, checkpointState string) map[string]any { input := initialInput() completed := make([]any, stageIndex) for index := 0; index < stageIndex; index++ { - completed[index] = stageTable[index].ID + completed[index] = workflowCatalog.Stages[index].ID } input["completedStageIds"] = completed if checkpointState != "not_started" { @@ -73,8 +73,8 @@ func terminalInput() map[string]any { } func stageIDs() []string { - result := make([]string, len(stageTable)) - for index, stage := range stageTable { + result := make([]string, len(workflowCatalog.Stages)) + for index, stage := range workflowCatalog.Stages { result[index] = stage.ID } return result diff --git a/internal/command/nativeevidenceguidance/guidance.go b/internal/command/nativeevidenceguidance/guidance.go index 571e48d..a4ffc7a 100644 --- a/internal/command/nativeevidenceguidance/guidance.go +++ b/internal/command/nativeevidenceguidance/guidance.go @@ -13,6 +13,12 @@ const ( SlotCount = 22 MaximumTextBytes = 16 * 1024 MaximumTextLines = 48 + + ApplicabilityAlways = "always" + ApplicabilityDeclaredInputChannels = "declared_input_channels" + ApplicabilityEnvironmentOrNetwork = "environment_or_network_access" + ApplicabilityExternalProcess = "external_process" + ApplicabilityMutableArtifacts = "mutable_artifacts" ) var errInvalidGuidanceTable = errors.New("native evidence guidance table is invalid") @@ -21,6 +27,7 @@ var errInvalidGuidanceTable = errors.New("native evidence guidance table is inva type Slot struct { Order int `json:"order"` SlotID string `json:"slotId"` + ApplicabilityClass string `json:"applicabilityClass"` Question string `json:"question"` RequiredConsumerDecision string `json:"requiredConsumerDecision"` CompletionCriterion string `json:"completionCriterion"` @@ -39,6 +46,7 @@ func (guidance Guidance) JSONValue() map[string]any { slots := make([]any, 0, len(guidance.Slots)) for _, slot := range guidance.Slots { slots = append(slots, map[string]any{ + "applicabilityClass": slot.ApplicabilityClass, "completionCriterion": slot.CompletionCriterion, "order": slot.Order, "question": slot.Question, @@ -67,28 +75,28 @@ type TextLine struct { // guidanceTable is the sole semantic owner of native-evidence guidance. var guidanceTable = [...]Slot{ - {Order: 1, SlotID: "semantic_owner", Question: "Who owns the invariant and its change authority?", RequiredConsumerDecision: "Select exactly one stable semantic owner and one change authority.", CompletionCriterion: "Both stable IDs are recorded and no second owner can independently change the same meaning."}, - {Order: 2, SlotID: "subject_identity_currentness", Question: "Which exact subject is proved and when is it current?", RequiredConsumerDecision: "Select the subject identity, digest algorithm, and freshness rule.", CompletionCriterion: "The exact subject digest is bound and the freshness rule deterministically rejects a stale subject."}, - {Order: 3, SlotID: "execution_root_source_snapshot", Question: "Which execution root and immutable source snapshot are admitted?", RequiredConsumerDecision: "Select one root and one immutable revision or complete source-digest set.", CompletionCriterion: "The witness binds the admitted root and revision or digests and rejects execution over a different snapshot."}, - {Order: 4, SlotID: "exact_argv", Question: "Which process invocation is admitted?", RequiredConsumerDecision: "Select one argv array without shell interpolation.", CompletionCriterion: "The executed argv equals the admitted array element-for-element and no shell parses it."}, - {Order: 5, SlotID: "stdin_input_bytes", Question: "Which stdin and input bytes are admitted?", RequiredConsumerDecision: "Select exact bytes or digests and byte bounds for every input channel.", CompletionCriterion: "The witness binds each input channel and rejects missing, substituted, or over-bound bytes."}, - {Order: 6, SlotID: "toolchain_environment", Question: "Which toolchain and deterministic environment are fixed?", RequiredConsumerDecision: "Select exact tool versions, platform, locale, timezone, and dependency identities.", CompletionCriterion: "The witness records and validates every selected identity before execution."}, - {Order: 7, SlotID: "positive_control", Question: "Which known-good subject proves the witness can succeed?", RequiredConsumerDecision: "Select one independently justified positive fixture and expected oracle result.", CompletionCriterion: "The fixture executes and produces the exact passing oracle result."}, - {Order: 8, SlotID: "near_miss_falsifier", Question: "Which plausible wrong subject must fail?", RequiredConsumerDecision: "Select one independent near-miss mutation and expected failure rule.", CompletionCriterion: "The mutated subject executes and fails under the selected stable rule while the positive control still passes."}, - {Order: 9, SlotID: "oracle_rule", Question: "Which executable oracle decides the outcome?", RequiredConsumerDecision: "Select one stable rule ID and deterministic observation mapping.", CompletionCriterion: "Every admitted observation maps to exactly one verdict and the near-miss activates the intended rule."}, - {Order: 10, SlotID: "input_work_bounds", Question: "Which cardinality and work bounds prevent excess work?", RequiredConsumerDecision: "Select numeric bounds and the point at which they dominate semantic processing.", CompletionCriterion: "Boundary fixtures pass, one-over fixtures fail before expensive per-item work, and no partial output is written."}, - {Order: 11, SlotID: "environment_network_policy", Question: "Which environment variables and network accesses are allowed?", RequiredConsumerDecision: "Select explicit allow and deny inventories.", CompletionCriterion: "Allowed accesses are observed, one denied environment read and one denied network access fail under stable rules, and undeclared access is denied."}, - {Order: 12, SlotID: "resource_bounds", Question: "Which CPU, memory, disk, file-descriptor, and PID limits apply?", RequiredConsumerDecision: "Select numeric limits and cleanup obligations.", CompletionCriterion: "Boundary use succeeds, one-over use fails deterministically, and allocated resources are released."}, - {Order: 13, SlotID: "output_bounds", Question: "Which stdout and stderr limits apply?", RequiredConsumerDecision: "Select exact byte caps and overflow behavior.", CompletionCriterion: "Boundary output succeeds and one-over output fails before any partial write."}, - {Order: 14, SlotID: "process_lifecycle", Question: "How are timeout and child processes contained?", RequiredConsumerDecision: "Select timeout, process-group termination, and detached-child policy.", CompletionCriterion: "Timeout terminates the process group, no detached child survives, and cleanup completion is observed before success."}, - {Order: 15, SlotID: "exit_mapping", Question: "How do native exit classes map to proof states?", RequiredConsumerDecision: "Select an exhaustive disjoint exit-class mapping.", CompletionCriterion: "Every native exit class maps to exactly one declared state and an unknown class fails closed."}, - {Order: 16, SlotID: "nondisclosure", Question: "Which values must never reach visible sinks?", RequiredConsumerDecision: "Select the secret, control, and sentinel corpus and all report-visible sinks.", CompletionCriterion: "No corpus value or fragment appears in stdout, stderr, reports, receipts, logs, or generated records."}, - {Order: 17, SlotID: "cleanup_crash_inventory", Question: "Which outputs may remain after success, failure, or crash?", RequiredConsumerDecision: "Select exact terminal inventories plus rollback or recovery rules.", CompletionCriterion: "Each terminal mode leaves exactly its inventory and interrupted mutation is rolled back or recovered by the declared rule."}, - {Order: 18, SlotID: "receipt_identity", Question: "Which identities must the receipt bind?", RequiredConsumerDecision: "Select command, argv, input, output, subject, and witness identities.", CompletionCriterion: "The receipt binds every selected identity by digest and any one-field substitution is rejected."}, - {Order: 19, SlotID: "currentness_trust", Question: "Which currentness and trust class is admitted?", RequiredConsumerDecision: "Select a closed class and its evidence predicates.", CompletionCriterion: "The class is owner-admitted from evidence, stale or weaker evidence is rejected, and the witness does not escalate the class."}, - {Order: 20, SlotID: "escalation", Question: "Which unknowns block and who resolves them?", RequiredConsumerDecision: "Select an owner, blocking condition, and required evidence for every unresolved state.", CompletionCriterion: "Each unresolved state blocks the pass path and names exactly one owner, condition, and evidence requirement."}, - {Order: 21, SlotID: "retirement", Question: "When may this witness be replaced or removed?", RequiredConsumerDecision: "Select replacement identity, parity predicates, owner approval, and retirement gate.", CompletionCriterion: "Replacement parity passes on the exact subject, the owner approval is bound, and the retirement gate rejects premature removal."}, - {Order: 22, SlotID: "non_claims", Question: "Which conclusions remain explicitly denied?", RequiredConsumerDecision: "Select the denied authority, scope, and readiness claims.", CompletionCriterion: "The exact non-claims remain in the report contract and no positive report language implies a denied claim."}, + {Order: 1, SlotID: "semantic_owner", ApplicabilityClass: ApplicabilityAlways, Question: "Who owns the invariant and its change authority?", RequiredConsumerDecision: "Select exactly one stable semantic owner and one change authority.", CompletionCriterion: "Both stable IDs are recorded and no second owner can independently change the same meaning."}, + {Order: 2, SlotID: "subject_identity_currentness", ApplicabilityClass: ApplicabilityAlways, Question: "Which exact subject is proved and when is it current?", RequiredConsumerDecision: "Select the subject identity, digest algorithm, and freshness rule.", CompletionCriterion: "The exact subject digest is bound and the freshness rule deterministically rejects a stale subject."}, + {Order: 3, SlotID: "execution_root_source_snapshot", ApplicabilityClass: ApplicabilityAlways, Question: "Which execution root and immutable source snapshot are admitted?", RequiredConsumerDecision: "Select one root and one immutable revision or complete source-digest set.", CompletionCriterion: "The witness binds the admitted root and revision or digests and rejects execution over a different snapshot."}, + {Order: 4, SlotID: "exact_argv", ApplicabilityClass: ApplicabilityExternalProcess, Question: "Which process invocation is admitted?", RequiredConsumerDecision: "Select one argv array without shell interpolation.", CompletionCriterion: "The executed argv equals the admitted array element-for-element and no shell parses it."}, + {Order: 5, SlotID: "stdin_input_bytes", ApplicabilityClass: ApplicabilityDeclaredInputChannels, Question: "Which stdin and input bytes are admitted?", RequiredConsumerDecision: "Select exact bytes or digests and byte bounds for every input channel.", CompletionCriterion: "The witness binds each input channel and rejects missing, substituted, or over-bound bytes."}, + {Order: 6, SlotID: "toolchain_environment", ApplicabilityClass: ApplicabilityAlways, Question: "Which toolchain and deterministic environment are fixed?", RequiredConsumerDecision: "Select exact tool versions, platform, locale, timezone, and dependency identities.", CompletionCriterion: "The witness records and validates every selected identity before execution."}, + {Order: 7, SlotID: "positive_control", ApplicabilityClass: ApplicabilityAlways, Question: "Which known-good subject proves the witness can succeed?", RequiredConsumerDecision: "Select one independently justified positive fixture and expected oracle result.", CompletionCriterion: "The fixture executes and produces the exact passing oracle result."}, + {Order: 8, SlotID: "near_miss_falsifier", ApplicabilityClass: ApplicabilityAlways, Question: "Which plausible wrong subject must fail?", RequiredConsumerDecision: "Select one independent near-miss mutation and expected failure rule.", CompletionCriterion: "The mutated subject executes and fails under the selected stable rule while the positive control still passes."}, + {Order: 9, SlotID: "oracle_rule", ApplicabilityClass: ApplicabilityAlways, Question: "Which executable oracle decides the outcome?", RequiredConsumerDecision: "Select one stable rule ID and deterministic observation mapping.", CompletionCriterion: "Every admitted observation maps to exactly one verdict and the near-miss activates the intended rule."}, + {Order: 10, SlotID: "input_work_bounds", ApplicabilityClass: ApplicabilityAlways, Question: "Which cardinality and work bounds prevent excess work?", RequiredConsumerDecision: "Select numeric bounds and the point at which they dominate semantic processing.", CompletionCriterion: "Boundary fixtures pass, one-over fixtures fail before expensive per-item work, and no partial output is written."}, + {Order: 11, SlotID: "environment_network_policy", ApplicabilityClass: ApplicabilityEnvironmentOrNetwork, Question: "Which environment variables and network accesses are allowed?", RequiredConsumerDecision: "Select explicit allow and deny inventories.", CompletionCriterion: "Allowed accesses are observed, one denied environment read and one denied network access fail under stable rules, and undeclared access is denied."}, + {Order: 12, SlotID: "resource_bounds", ApplicabilityClass: ApplicabilityExternalProcess, Question: "Which CPU, memory, disk, file-descriptor, and PID limits apply?", RequiredConsumerDecision: "Select numeric limits and cleanup obligations.", CompletionCriterion: "Boundary use succeeds, one-over use fails deterministically, and allocated resources are released."}, + {Order: 13, SlotID: "output_bounds", ApplicabilityClass: ApplicabilityAlways, Question: "Which stdout and stderr limits apply?", RequiredConsumerDecision: "Select exact byte caps and overflow behavior.", CompletionCriterion: "Boundary output succeeds and one-over output fails before any partial write."}, + {Order: 14, SlotID: "process_lifecycle", ApplicabilityClass: ApplicabilityExternalProcess, Question: "How are timeout and child processes contained?", RequiredConsumerDecision: "Select timeout, process-group termination, and detached-child policy.", CompletionCriterion: "Timeout terminates the process group, no detached child survives, and cleanup completion is observed before success."}, + {Order: 15, SlotID: "exit_mapping", ApplicabilityClass: ApplicabilityExternalProcess, Question: "How do native exit classes map to proof states?", RequiredConsumerDecision: "Select an exhaustive disjoint exit-class mapping.", CompletionCriterion: "Every native exit class maps to exactly one declared state and an unknown class fails closed."}, + {Order: 16, SlotID: "nondisclosure", ApplicabilityClass: ApplicabilityAlways, Question: "Which values must never reach visible sinks?", RequiredConsumerDecision: "Select the secret, control, and sentinel corpus and all report-visible sinks.", CompletionCriterion: "No corpus value or fragment appears in stdout, stderr, reports, receipts, logs, or generated records."}, + {Order: 17, SlotID: "cleanup_crash_inventory", ApplicabilityClass: ApplicabilityMutableArtifacts, Question: "Which outputs may remain after success, failure, or crash?", RequiredConsumerDecision: "Select exact terminal inventories plus rollback or recovery rules.", CompletionCriterion: "Each terminal mode leaves exactly its inventory and interrupted mutation is rolled back or recovered by the declared rule."}, + {Order: 18, SlotID: "receipt_identity", ApplicabilityClass: ApplicabilityAlways, Question: "Which identities must the receipt bind?", RequiredConsumerDecision: "Select command, argv, input, output, subject, and witness identities.", CompletionCriterion: "The receipt binds every selected identity by digest and any one-field substitution is rejected."}, + {Order: 19, SlotID: "currentness_trust", ApplicabilityClass: ApplicabilityAlways, Question: "Which currentness and trust class is admitted?", RequiredConsumerDecision: "Select a closed class and its evidence predicates.", CompletionCriterion: "The class is owner-admitted from evidence, stale or weaker evidence is rejected, and the witness does not escalate the class."}, + {Order: 20, SlotID: "escalation", ApplicabilityClass: ApplicabilityAlways, Question: "Which unknowns block and who resolves them?", RequiredConsumerDecision: "Select an owner, blocking condition, and required evidence for every unresolved state.", CompletionCriterion: "Each unresolved state blocks the pass path and names exactly one owner, condition, and evidence requirement."}, + {Order: 21, SlotID: "retirement", ApplicabilityClass: ApplicabilityAlways, Question: "When may this witness be replaced or removed?", RequiredConsumerDecision: "Select replacement identity, parity predicates, owner approval, and retirement gate.", CompletionCriterion: "Replacement parity passes on the exact subject, the owner approval is bound, and the retirement gate rejects premature removal."}, + {Order: 22, SlotID: "non_claims", ApplicabilityClass: ApplicabilityAlways, Question: "Which conclusions remain explicitly denied?", RequiredConsumerDecision: "Select the denied authority, scope, and readiness claims.", CompletionCriterion: "The exact non-claims remain in the report contract and no positive report language implies a denied claim."}, } var guidanceNonClaims = [...]string{ @@ -137,7 +145,7 @@ func TextProjection() ([]TextLine, error) { lines := make([]TextLine, 0, len(guidance.Slots)*2+1) for _, slot := range guidance.Slots { lines = append(lines, - TextLine{Label: slot.SlotID, Value: "decision: " + slot.RequiredConsumerDecision}, + TextLine{Label: slot.SlotID, Value: "applicability: " + slot.ApplicabilityClass + "; decision: " + slot.RequiredConsumerDecision}, TextLine{Label: "completion", Value: slot.CompletionCriterion}, ) } @@ -151,7 +159,7 @@ func validateGuidanceTable() error { } seen := make(map[string]struct{}, len(guidanceTable)) for index, slot := range guidanceTable { - if slot.Order != index+1 || slot.SlotID == "" || slot.Question == "" || slot.RequiredConsumerDecision == "" || slot.CompletionCriterion == "" { + if slot.Order != index+1 || slot.SlotID == "" || !validApplicabilityClass(slot.ApplicabilityClass) || slot.Question == "" || slot.RequiredConsumerDecision == "" || slot.CompletionCriterion == "" { return errInvalidGuidanceTable } if _, duplicate := seen[slot.SlotID]; duplicate { @@ -161,3 +169,12 @@ func validateGuidanceTable() error { } return nil } + +func validApplicabilityClass(value string) bool { + switch value { + case ApplicabilityAlways, ApplicabilityDeclaredInputChannels, ApplicabilityEnvironmentOrNetwork, ApplicabilityExternalProcess, ApplicabilityMutableArtifacts: + return true + default: + return false + } +} diff --git a/internal/command/nativeevidenceguidance/guidance_test.go b/internal/command/nativeevidenceguidance/guidance_test.go index 398f54c..fa3fa3d 100644 --- a/internal/command/nativeevidenceguidance/guidance_test.go +++ b/internal/command/nativeevidenceguidance/guidance_test.go @@ -3,6 +3,8 @@ package nativeevidenceguidance import ( "crypto/sha256" "fmt" + "maps" + "slices" "strings" "testing" @@ -10,28 +12,28 @@ import ( ) var slotFingerprints = [...]string{ - "4f7d1ea4203b97065c619f024d88f3f1fb1eb3df8c022a7cab69ad8af0b0d251", - "1b6b182ab32c13c303bb31cab396112256daa9d7361ef99f6cc7af6fc088fa81", - "45df453e81d723ccef3cf29eb3ccde7ba90bac72356194e49ba19f26fc252fd1", - "555d5b05c5588974c86b471dfd9af8ee9b4774ee75ff981a5148902fa9974ba8", - "80c46416ff3b4855c832b1c126180e4c0d630265a814f47874ab4a99e752a9fa", - "ecf29279f0527277c0ea62aa5210917659743357d17b88696175cd85a513c6fb", - "fed68d4a0833735bff84ab5509dfc07a233611aa7c3ca2593efdb2fe6de31f46", - "5f76850ecd6571cc081365656f89e4888bf6ce53f6f47efceaebb8e83f271427", - "8adea3ac35394b6bebf904f51a1a488cad8d156713566076c8ba529e36b725d8", - "e7ca629cbe625bb8c342dfadb1ca9ecb3e4456d7dbe394e3bf7e158b5ed8d048", - "ed669cec6efc72612632050c1173395741290639e9bca106352276212e387a0e", - "4f72cdb349f7d87a8a25ce0c6180dd403224792c710b274b9b393d6c3b64cf93", - "d5883acd09e114ca0571af5c845c9770e31b11178d59aa3bcebeead8e8ad7e51", - "d2f65a835de4d04ed0d8d5b322ab694a26ff68f85055d4a13790ea13e45bb6b8", - "d5a74ef4e8e16ee74991962d72871787e92508b32e2460344e99f87c87950aaf", - "df63e39402c6daa6aa078e18e75d741da855f9490c7d4cfad3dbcbef37aa1b55", - "10ef4f5cbc7b81d12479bb1b2c48be1d59349e40ca0859e23fd0376bca6279de", - "6d63c32605c2a21ae866e7edfb1dc6b9c752e5e87987d6cf8d62627e8f0f78d4", - "833a7d9fb8309efce8b1ad5c728fe7c393ae5d90cdfd50a71e88df3acab731e2", - "c6caf7c6e81f0aab3b4261fde6c490d46cc3db0cc3869d79dbad44553e913f7a", - "6c83ba103d8c1b4ad61d196bc90bd8c6033c3b7b35173efbc6b7be8e192cc3bb", - "05ed48165f9ef51ca30d2f0cad31050cd65beca08dca56ced734e27dca35711c", + "8c9fb4466291b8a60aeed04ffe91e684753424bb71cf93ba0eb7c73ae31b5d21", + "f68141d84607e4f343982c753116bb0aeba8e4fcbc65a25cb0284548ac95853c", + "930d361a8fde04b1391f2040254efa3c8ae30506a08776072955645d233f53c6", + "11e011fe5d90085990ef5837ae40b5d1c179ba787f56d544d855c1fd29324844", + "62b191cc9e5752295aa04c16aab4bbbf67be9ba67893ebae7b9a3d47b1e9542c", + "e214e2c6203145b94a43f0f4992fd3b40232f439acbd7a17ec3de22da73c9dd8", + "b9f830de4358ce0976002226a55d3e21479a7a02b8e6a6d28e2cba58d7222095", + "4b34d7c224d13957e333ca1aa7eaac1d5ff19b5b54dfc8fb436f6e20fc735e25", + "f45e880bad5b6f8e14a1d4f321ddcfdbefa83a143732c0b961459edb202c15e8", + "f70d28729dc5589400787d398f868225f010bb159c822e57494ddfcffb3227af", + "04a4843479b418e0f09d45077136bbd07c7b58b8e3f7143d045796051fc0e46f", + "496e80d2cdfc106b0c36d9032ea2d3671c83ab670c363959f7cb30e6ed4fe3cc", + "5c05b800f6579a9f120be7e3563a9d4dd60ed74ff46cb58651df91762b3cfb0a", + "9f186415e33a8899b6ab423c61f9889372d4cb5b5d30dff669152a5100c3d565", + "0a2e2c9f6c4294f51f82fc0f131512e5926784a04f1028db1f744361e7693cf8", + "c1efe0f8cc0aa87bc6c05bf4ce603ceae72acbba4b357b64c588e6d9b6bfc2a0", + "e72db7215f074d0353c635af40bb14263a5f3a8de895af849a5bf679ff3adc04", + "be53fb4f4f32e22557922b20c62b42a5f5b8dc645aba27b7caf9af9e88eda905", + "b55ed27e5e8dc19dfb7ae997d35b0566e55140bf75570f6b2e2a46b3ae84852b", + "fc0065447b033670bb33c6be89d0080aa0ce7f0e60c4e74f7b24f46ec3a49119", + "a7aa8e953573653e9c6f4924ba5205a65c61de6d9ff247d7048f082d0dab4e9d", + "232e97077ee39087aebd1f2bc3baa5abdd1ff848f8f5779b5489be96b5668b50", } func TestGuidanceSlotPredicates(t *testing.T) { @@ -43,13 +45,26 @@ func TestGuidanceSlotPredicates(t *testing.T) { if guidance.SchemaVersion != SchemaVersion || guidance.GuidanceID != GuidanceID || len(guidance.Slots) != SlotCount { t.Fatalf("Build() = %#v, want fixed versioned 22-slot guidance", guidance) } + actualFingerprints := make([]string, len(guidance.Slots)) + for index, slot := range guidance.Slots { + actualFingerprints[index] = slotFingerprint(slot) + } + if !slices.Equal(actualFingerprints, slotFingerprints[:]) { + t.Fatalf("slot fingerprints = %#v", actualFingerprints) + } + classCounts := map[string]int{} for _, slot := range guidance.Slots { - slot := slot - t.Run("slot_"+slot.SlotID, func(t *testing.T) { - if got, want := slotFingerprint(slot), slotFingerprints[slot.Order-1]; got != want { - t.Fatalf("slot %s fingerprint = %s, want %s", slot.SlotID, got, want) - } - }) + classCounts[slot.ApplicabilityClass]++ + } + wantClassCounts := map[string]int{ + ApplicabilityAlways: 15, + ApplicabilityDeclaredInputChannels: 1, + ApplicabilityEnvironmentOrNetwork: 1, + ApplicabilityExternalProcess: 4, + ApplicabilityMutableArtifacts: 1, + } + if !maps.Equal(classCounts, wantClassCounts) { + t.Fatalf("applicability class partition = %#v, want %#v", classCounts, wantClassCounts) } } @@ -97,7 +112,7 @@ func TestGuidancePlainTextIsBoundedAndComplete(t *testing.T) { t.Fatalf("Build() error = %v", err) } for _, slot := range guidance.Slots { - for _, required := range []string{slot.SlotID, slot.RequiredConsumerDecision, slot.CompletionCriterion} { + for _, required := range []string{slot.SlotID, slot.ApplicabilityClass, slot.RequiredConsumerDecision, slot.CompletionCriterion} { if !strings.Contains(text, required) { t.Fatalf("text omits required slot projection %q", required) } @@ -152,6 +167,6 @@ func TestGuidanceJSONProjectionIsFreshAndComplete(t *testing.T) { } func slotFingerprint(slot Slot) string { - value := fmt.Sprintf("%d\x1f%s\x1f%s\x1f%s\x1f%s", slot.Order, slot.SlotID, slot.Question, slot.RequiredConsumerDecision, slot.CompletionCriterion) + value := fmt.Sprintf("%d\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s", slot.Order, slot.SlotID, slot.ApplicabilityClass, slot.Question, slot.RequiredConsumerDecision, slot.CompletionCriterion) return fmt.Sprintf("%x", sha256.Sum256([]byte(value))) } diff --git a/internal/command/stackpreset/preset_ids_generated.go b/internal/command/stackpreset/preset_ids_generated.go index 33f7a2e..329c517 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 = "1cd9ae8ab6f5997d741fb95dd8846b1fdd66d0daaedec6630ee6ebf40e78f6d4" +const presetContractSourceSHA256 = "4065a82beed882107eff06781cec597db04ec2680b2981526ca14a39e5c431ad" var presetIDs = []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"} diff --git a/internal/testsupport/nodetestselector/selector.go b/internal/testsupport/nodetestselector/selector.go index d1bf243..2eea695 100644 --- a/internal/testsupport/nodetestselector/selector.go +++ b/internal/testsupport/nodetestselector/selector.go @@ -6,6 +6,7 @@ import ( "errors" "os/exec" "regexp" + "slices" "strconv" "strings" ) @@ -16,10 +17,23 @@ var errTAPLimit = errors.New("node test selector output exceeds the byte limit") // Run executes one exact top-level Node test and rejects zero-match success. func Run(ctx context.Context, nodePath string, workdir string, file string, name string) error { - if nodePath == "" || workdir == "" || file == "" || name == "" || strings.ContainsAny(file+name, "\r\n") { + return RunSet(ctx, nodePath, workdir, file, []string{name}) +} + +// RunSet executes one exact sorted set of top-level Node tests in one process. +func RunSet(ctx context.Context, nodePath string, workdir string, file string, names []string) error { + if nodePath == "" || workdir == "" || file == "" || len(names) == 0 || strings.ContainsAny(file, "\r\n") { return errors.New("node test selector coordinates are invalid") } - pattern := "^" + regexp.QuoteMeta(name) + "$" + selected := slices.Clone(names) + if !validSelectorNames(selected) { + return errors.New("node test selector names must be sorted and unique") + } + patterns := make([]string, len(selected)) + for index, name := range selected { + patterns[index] = regexp.QuoteMeta(name) + } + pattern := "^(?:" + strings.Join(patterns, "|") + ")$" command := exec.CommandContext(ctx, nodePath, "--test", "--test-reporter=tap", "--test-name-pattern", pattern, file) command.Dir = workdir var stdout boundedBuffer @@ -29,28 +43,45 @@ func Run(ctx context.Context, nodePath string, workdir string, file string, name if err := command.Run(); err != nil { return errors.New("node test selector execution failed") } - return admitTAP(stdout.String(), name) + return admitTAPSet(stdout.String(), selected) } func admitTAP(value string, name string) error { - if value == "" || strings.Contains(value, "\r") || strings.ContainsAny(name, "\r\n") { + return admitTAPSet(value, []string{name}) +} + +func admitTAPSet(value string, names []string) error { + if value == "" || strings.Contains(value, "\r") || !validSelectorNames(names) { return errors.New("node test selector emitted invalid TAP") } - subtestLine := "# Subtest: " + name - selectedSubtests := 0 - selectedPasses := 0 + selectedSubtests := make(map[string]int, len(names)) + selectedPasses := make(map[string]int, len(names)) + selectedNames := make(map[string]struct{}, len(names)) + for _, name := range names { + selectedNames[name] = struct{}{} + } + totalSubtests := 0 + totalPasses := 0 summary := map[string]int{} for _, line := range strings.Split(strings.TrimSuffix(value, "\n"), "\n") { switch { case strings.HasPrefix(line, "# Subtest: "): - if line == subtestLine { - selectedSubtests++ + totalSubtests++ + name := strings.TrimPrefix(line, "# Subtest: ") + if _, selected := selectedNames[name]; !selected { + return errors.New("node test selector reported an unselected test") } + selectedSubtests[name]++ case strings.HasPrefix(line, "ok "): parts := strings.SplitN(line, " - ", 2) - if len(parts) == 2 && parts[1] == name { - selectedPasses++ + if len(parts) != 2 { + return errors.New("node test selector emitted invalid TAP") + } + totalPasses++ + if _, selected := selectedNames[parts[1]]; !selected { + return errors.New("node test selector reported an unselected test") } + selectedPasses[parts[1]]++ case strings.HasPrefix(line, "not ok "): return errors.New("node test selector reported a failure") case strings.HasPrefix(line, "# "): @@ -64,12 +95,29 @@ func admitTAP(value string, name string) error { } } } - if selectedSubtests != 1 || selectedPasses != 1 || summary["tests"] != 1 || summary["pass"] != 1 || summary["fail"] != 0 || summary["cancelled"] != 0 || summary["skipped"] != 0 || summary["todo"] != 0 { - return errors.New("node test selector did not execute exactly one passing test") + for _, name := range names { + if selectedSubtests[name] != 1 || selectedPasses[name] != 1 { + return errors.New("node test selector did not execute exactly the selected passing tests") + } + } + if totalSubtests != len(names) || totalPasses != len(names) || summary["tests"] != len(names) || summary["pass"] != len(names) || summary["fail"] != 0 || summary["cancelled"] != 0 || summary["skipped"] != 0 || summary["todo"] != 0 { + return errors.New("node test selector did not execute exactly the selected passing tests") } return nil } +func validSelectorNames(names []string) bool { + if len(names) == 0 || !slices.IsSorted(names) { + return false + } + for index, name := range names { + if name == "" || strings.ContainsAny(name, "\r\n") || index > 0 && names[index-1] == name { + return false + } + } + return true +} + type boundedBuffer struct { buffer bytes.Buffer } diff --git a/internal/testsupport/nodetestselector/selector_test.go b/internal/testsupport/nodetestselector/selector_test.go index c6f275a..a515bab 100644 --- a/internal/testsupport/nodetestselector/selector_test.go +++ b/internal/testsupport/nodetestselector/selector_test.go @@ -24,6 +24,30 @@ func TestAdmitTAPRequiresExactlyOneSelectedPass(t *testing.T) { } } +func TestAdmitTAPSetRequiresEverySelectedPassExactlyOnce(t *testing.T) { + valid := "TAP version 13\n# Subtest: first test\nok 1 - first test\n# Subtest: second test\nok 2 - second test\n1..2\n# tests 2\n# pass 2\n# fail 0\n# cancelled 0\n# skipped 0\n# todo 0\n" + names := []string{"first test", "second test"} + if err := admitTAPSet(valid, names); err != nil { + t.Fatalf("admitTAPSet(valid) error = %v", err) + } + for _, mutant := range []string{ + strings.Replace(valid, "# Subtest: second test\nok 2 - second test\n", "", 1), + strings.Replace(valid, "ok 2 - second test", "ok 2 - first test", 1), + strings.Replace(valid, "# tests 2", "# tests 1", 1), + valid + "# Subtest: first test\nok 3 - first test\n", + strings.Replace(valid, "1..2\n", "# Subtest: unselected test\nok 3 - unselected test\n1..3\n", 1), + } { + if err := admitTAPSet(mutant, names); err == nil { + t.Fatalf("admitTAPSet accepted mutant %q", mutant) + } + } + for _, invalidNames := range [][]string{{"second test", "first test"}, {"first test", "first test"}, {"first test", ""}} { + if err := admitTAPSet(valid, invalidNames); err == nil { + t.Fatalf("admitTAPSet accepted invalid names %q", invalidNames) + } + } +} + func TestBoundedBufferRejectsOneByteOverLimit(t *testing.T) { var buffer boundedBuffer if _, err := buffer.Write(make([]byte, maximumTAPBytes)); err != nil { diff --git a/internal/tools/releasechange/record_test.go b/internal/tools/releasechange/record_test.go index c7e8ec6..71e9438 100644 --- a/internal/tools/releasechange/record_test.go +++ b/internal/tools/releasechange/record_test.go @@ -200,8 +200,8 @@ var currentBreakingChanges = []Change{ } var currentAdditions = []Change{ - {ChangeID: "proofkit.agent-workflow.change-planner", Summary: "Add a bounded change-workflow-plan command with canonical JSON, derived agent-envelope, plain-text, and capability-gated terminal-color projections."}, - {ChangeID: "proofkit.agent-workflow.native-evidence-guidance", Summary: "Add a no-input native-evidence-guidance command that exposes the repository-owned decision template as canonical JSON or plain text without executing or approving consumer witnesses."}, + {ChangeID: "proofkit.agent-workflow.change-planner", Summary: "Add a bounded change-workflow-plan command for the named optional proofkit.reviewed-change.v1 profile with canonical JSON, derived agent-envelope, plain-text, and capability-gated terminal-color projections."}, + {ChangeID: "proofkit.agent-workflow.native-evidence-guidance", Summary: "Add a no-input native-evidence-guidance command that exposes the repository-owned decision template and closed applicability classes as canonical JSON or plain text without executing or approving consumer witnesses."}, } var currentMigrationSteps = []string{ diff --git a/internal/tools/workflowsmoke/workflow_smoke.go b/internal/tools/workflowsmoke/workflow_smoke.go index 9b37b40..3abb0c9 100644 --- a/internal/tools/workflowsmoke/workflow_smoke.go +++ b/internal/tools/workflowsmoke/workflow_smoke.go @@ -51,6 +51,9 @@ func Verify(run Runner) error { if err := requireString(planValue, "checkpointState", "not_started"); err != nil { return fmt.Errorf("planner JSON: %w", err) } + if err := requireString(planValue, "workflowProfileId", "proofkit.reviewed-change.v1"); err != nil { + return fmt.Errorf("planner JSON: %w", err) + } compact, err := invoke(run, "planner compact JSON", input, "--json-layout", "compact", "change-workflow-plan", "--input", "-") if err != nil { @@ -135,6 +138,9 @@ func Verify(run Runner) error { if err := requireStringObject(slots[0], "slotId", "semantic_owner"); err != nil { return fmt.Errorf("no-input guidance JSON: first slot: %w", err) } + if err := requireStringObject(slots[0], "applicabilityClass", "always"); err != nil { + return fmt.Errorf("no-input guidance JSON: first slot: %w", err) + } if err := requireStringObject(slots[len(slots)-1], "slotId", "non_claims"); err != nil { return fmt.Errorf("no-input guidance JSON: last slot: %w", err) } @@ -143,7 +149,7 @@ func Verify(run Runner) error { if err != nil { return err } - if !bytes.HasPrefix(guidanceText.Stdout, []byte("semantic_owner: decision:")) || bytes.Contains(guidanceText.Stdout, []byte("\x1b[")) { + if !bytes.HasPrefix(guidanceText.Stdout, []byte("semantic_owner: applicability: always; decision:")) || bytes.Contains(guidanceText.Stdout, []byte("\x1b[")) { return fmt.Errorf("no-input guidance text must be plain text with the canonical first slot") } return nil diff --git a/internal/tools/workflowsmoke/workflow_smoke_test.go b/internal/tools/workflowsmoke/workflow_smoke_test.go index 489234b..b02a65b 100644 --- a/internal/tools/workflowsmoke/workflow_smoke_test.go +++ b/internal/tools/workflowsmoke/workflow_smoke_test.go @@ -23,6 +23,7 @@ func TestVerifyRejectsCarrierContractMutations(t *testing.T) { 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: "text styling", match: "change-workflow-plan --input - --format text --color never", apply: replaceStdout("\x1b[31mChange workflow plan\x1b[0m\n")}, @@ -31,6 +32,7 @@ func TestVerifyRejectsCarrierContractMutations(t *testing.T) { return result }}, {name: "guidance slots", match: "native-evidence-guidance", apply: replaceStdout(`{"guidanceId":"proofkit.native-evidence-guidance.v1","slots":[]}\n`)}, + {name: "guidance applicability", match: "native-evidence-guidance", apply: replaceStdoutFragment(`"applicabilityClass": "always"`, `"applicabilityClass": "external_process"`)}, } for _, mutation := range mutations { t.Run(mutation.name, func(t *testing.T) { @@ -66,3 +68,10 @@ func replaceStdout(value string) func(workflowsmoke.Result) workflowsmoke.Result return result } } + +func replaceStdoutFragment(oldValue string, newValue string) func(workflowsmoke.Result) workflowsmoke.Result { + return func(result workflowsmoke.Result) workflowsmoke.Result { + result.Stdout = bytes.Replace(result.Stdout, []byte(oldValue), []byte(newValue), 1) + return result + } +} diff --git a/proofkit/cli-contract.v2.json b/proofkit/cli-contract.v2.json index 6bcd88e..fde27ea 100644 --- a/proofkit/cli-contract.v2.json +++ b/proofkit/cli-contract.v2.json @@ -859,7 +859,7 @@ "rootDefinitionDigest": "sha256:6632a481380bfb07d754ef622661b291f805b1b168b1824089bdd5cc9bdd4b0e", "nativeSource": { "path": "internal/command/changeworkflowplan", - "canonicalDigest": "sha256:990cf34a04fb15f2a60339c0713e1644f26f1e81462a4daf171e3613acc87bce", + "canonicalDigest": "sha256:2453c3d2a15b756153a3e553ed093327efade8a7ee8423f1ef8e405e9beb92a5", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -891,10 +891,10 @@ "rootType": "object", "closed": true, "rootDefinitionRef": "proofkit.change-workflow-plan.output.v1.root-shape", - "rootDefinitionDigest": "sha256:d0c7c2f1b2d78da4260757c8bdbccda4f9dfe4f69d3ba3a0af24338508f32da9", + "rootDefinitionDigest": "sha256:ca7b2acffdcbfd28222eb61f2bd8687f76f20cb789b712ee3ffc2756a3674ff8", "nativeSource": { "path": "internal/command/changeworkflowplan", - "canonicalDigest": "sha256:990cf34a04fb15f2a60339c0713e1644f26f1e81462a4daf171e3613acc87bce", + "canonicalDigest": "sha256:2453c3d2a15b756153a3e553ed093327efade8a7ee8423f1ef8e405e9beb92a5", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { @@ -906,6 +906,7 @@ "compatibilitySummary": [ "schemaVersion=1", "JSON plan and agent-envelope variants; text is a bounded derived presentation", + "non-envelope JSON plan variants identify the optional built-in workflow profile", "root-shape-only definition proofkit.change-workflow-plan.output.v1.root-shape; nested fields, types, and cardinalities are non-claims" ], "ownerRequirementRefs": [ @@ -2150,7 +2151,7 @@ "rootDefinitionDigest": "sha256:218011a133540f57ef74f8748747e00d33b6757c9f77725ecef39f45fb19423f", "nativeSource": { "path": "internal/command/nativeevidenceguidance", - "canonicalDigest": "sha256:3b08daa3b023df2a7687999893a5b357c0345a854e2b25ebca84ab3a514db8a1", + "canonicalDigest": "sha256:c36dc61cc915ef446305eee5d325791da82581d794700ac0bb6adcb69c54e80f", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { @@ -2420,7 +2421,7 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:fe81ca3521d27f477bca5fdee31da075348b3835440215c9d1df1ebdbb8c870e", + "canonicalDigest": "sha256:ceba9fe3379339a2ff137ccaa1ff89bb0136cd306bf70fb3ca8354324d2a1acf", "evidenceClass": "source_checkout" }, { @@ -5662,7 +5663,7 @@ "rootDefinitionDigest": "sha256:3c842174dff5361e7f83166469b832805e05aa314b073c16234b5b64e346281e", "nativeSource": { "path": "internal/app", - "canonicalDigest": "sha256:fe81ca3521d27f477bca5fdee31da075348b3835440215c9d1df1ebdbb8c870e", + "canonicalDigest": "sha256:ceba9fe3379339a2ff137ccaa1ff89bb0136cd306bf70fb3ca8354324d2a1acf", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -5691,7 +5692,7 @@ "rootDefinitionDigest": "sha256:0ea95e277ebe44cd2de42c29b47c38686ac0b6b390d8965367437b3fe138e209", "nativeSource": { "path": "internal/app", - "canonicalDigest": "sha256:fe81ca3521d27f477bca5fdee31da075348b3835440215c9d1df1ebdbb8c870e", + "canonicalDigest": "sha256:ceba9fe3379339a2ff137ccaa1ff89bb0136cd306bf70fb3ca8354324d2a1acf", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { @@ -8216,7 +8217,8 @@ "reportKind", "retainedContextRefIds", "retainedContextRefs", - "schemaVersion" + "schemaVersion", + "workflowProfileId" ], "requiredFields": [ "action", @@ -8233,7 +8235,8 @@ "reportKind", "retainedContextRefIds", "retainedContextRefs", - "schemaVersion" + "schemaVersion", + "workflowProfileId" ], "rootKind": "object", "variantId": "02-next-action", @@ -8258,7 +8261,8 @@ "retainedContextRefIds", "retainedContextRefs", "schemaVersion", - "successorStateDelta" + "successorStateDelta", + "workflowProfileId" ], "requiredFields": [ "action", @@ -8276,7 +8280,8 @@ "retainedContextRefIds", "retainedContextRefs", "schemaVersion", - "successorStateDelta" + "successorStateDelta", + "workflowProfileId" ], "rootKind": "object", "variantId": "03-accept-stage", @@ -8296,7 +8301,8 @@ "reportKind", "retainedContextRefIds", "retainedContextRefs", - "schemaVersion" + "schemaVersion", + "workflowProfileId" ], "requiredFields": [ "authority", @@ -8309,7 +8315,8 @@ "reportKind", "retainedContextRefIds", "retainedContextRefs", - "schemaVersion" + "schemaVersion", + "workflowProfileId" ], "rootKind": "object", "variantId": "04-workflow-complete", @@ -8319,7 +8326,7 @@ } ] }, - "canonicalDigest": "sha256:d0c7c2f1b2d78da4260757c8bdbccda4f9dfe4f69d3ba3a0af24338508f32da9" + "canonicalDigest": "sha256:ca7b2acffdcbfd28222eb61f2bd8687f76f20cb789b712ee3ffc2756a3674ff8" }, { "definitionId": "proofkit.changed-path-set.input.v1.root-shape", diff --git a/proofkit/requirement-bindings.json b/proofkit/requirement-bindings.json index 5b247b5..6813f91 100644 --- a/proofkit/requirement-bindings.json +++ b/proofkit/requirement-bindings.json @@ -3824,20 +3824,8 @@ "witnessPath": "scripts/stable_json_selector_test.go", "witnessSelectors": [ { - "selector": "TestStableJSONJavaScriptUnicodeCorpus", - "command": "go test ./scripts -run '^TestStableJSONJavaScriptUnicodeCorpus$'" - }, - { - "selector": "TestStableJSONJavaScriptExhaustiveUnicodeTable", - "command": "go test ./scripts -run '^TestStableJSONJavaScriptExhaustiveUnicodeTable$'" - }, - { - "selector": "TestStableJSONJavaScriptRejectsEverySurrogate", - "command": "go test ./scripts -run '^TestStableJSONJavaScriptRejectsEverySurrogate$'" - }, - { - "selector": "TestStableJSONJavaScriptAdmitsEverySurrogatePair", - "command": "go test ./scripts -run '^TestStableJSONJavaScriptAdmitsEverySurrogatePair$'" + "selector": "TestStableJSONJavaScriptUnicodePredicates", + "command": "go test ./scripts -run '^TestStableJSONJavaScriptUnicodePredicates$'" } ], "commandIds": ["proofkit.go-test"], @@ -4650,6 +4638,21 @@ "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-010", + "scenarioId": "proofkit.agent-workflow.semantic-owner-topology", + "witnessId": "proofkit.agent-workflow.semantic-owner-topology-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/app/agent_workflow_topology_test.go", + "witnessSelectors": [ + { + "selector": "TestAgentWorkflowSemanticOwnerTopology", + "command": "go test ./internal/app -run '^TestAgentWorkflowSemanticOwnerTopology$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, { "requirementId": "REQ-PROOFKIT-WORKFLOW-011", "scenarioId": "proofkit.agent-workflow.public-cli-relation-closure", diff --git a/release/change-record.v2.json b/release/change-record.v2.json index f64724c..95952a9 100644 --- a/release/change-record.v2.json +++ b/release/change-record.v2.json @@ -16,11 +16,11 @@ "additions": [ { "changeId": "proofkit.agent-workflow.change-planner", - "summary": "Add a bounded change-workflow-plan command with canonical JSON, derived agent-envelope, plain-text, and capability-gated terminal-color projections." + "summary": "Add a bounded change-workflow-plan command for the named optional proofkit.reviewed-change.v1 profile with canonical JSON, derived agent-envelope, plain-text, and capability-gated terminal-color projections." }, { "changeId": "proofkit.agent-workflow.native-evidence-guidance", - "summary": "Add a no-input native-evidence-guidance command that exposes the repository-owned decision template as canonical JSON or plain text without executing or approving consumer witnesses." + "summary": "Add a no-input native-evidence-guidance command that exposes the repository-owned decision template and closed applicability classes as canonical JSON or plain text without executing or approving consumer witnesses." } ], "migration": { diff --git a/scripts/stable-json.mjs b/scripts/stable-json.mjs index c05fe6a..3cb9dcd 100644 --- a/scripts/stable-json.mjs +++ b/scripts/stable-json.mjs @@ -126,13 +126,17 @@ function isScalarString(value) { } function compareScalarStrings(left, right) { - const leftScalars = Array.from(left, (character) => character.codePointAt(0)); - const rightScalars = Array.from(right, (character) => character.codePointAt(0)); - const sharedLength = Math.min(leftScalars.length, rightScalars.length); - for (let index = 0; index < sharedLength; index += 1) { - if (leftScalars[index] !== rightScalars[index]) return leftScalars[index] - rightScalars[index]; + let leftIndex = 0; + let rightIndex = 0; + while (leftIndex < left.length && rightIndex < right.length) { + const leftScalar = left.codePointAt(leftIndex); + const rightScalar = right.codePointAt(rightIndex); + if (leftScalar !== rightScalar) return leftScalar - rightScalar; + leftIndex += leftScalar > 0xffff ? 2 : 1; + rightIndex += rightScalar > 0xffff ? 2 : 1; } - return leftScalars.length - rightScalars.length; + if (leftIndex === left.length && rightIndex === right.length) return 0; + return leftIndex === left.length ? -1 : 1; } export function isUnsafeScalar(value) { diff --git a/scripts/stable_json_selector_test.go b/scripts/stable_json_selector_test.go index cce9b5f..bbc664a 100644 --- a/scripts/stable_json_selector_test.go +++ b/scripts/stable_json_selector_test.go @@ -9,31 +9,21 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/testsupport/nodetestselector" ) -func TestStableJSONJavaScriptUnicodePolicy(t *testing.T) { - runStableJSONJavaScriptSelector(t, "stableJSONStringify escapes unsafe scalars and preserves scalar key order") -} - -func TestStableJSONJavaScriptUnicodeCorpus(t *testing.T) { - runStableJSONJavaScriptSelector(t, "stable JSON Unicode corpus") -} - -func TestStableJSONJavaScriptExhaustiveUnicodeTable(t *testing.T) { - runStableJSONJavaScriptSelector(t, "stable JSON Unicode table classifies every scalar and complement") -} - -func TestStableJSONJavaScriptRejectsEverySurrogate(t *testing.T) { - runStableJSONJavaScriptSelector(t, "stableJSONStringify rejects every unpaired surrogate code unit in keys and values") -} - -func TestStableJSONJavaScriptAdmitsEverySurrogatePair(t *testing.T) { - runStableJSONJavaScriptSelector(t, "stableJSONStringify admits every valid UTF-16 surrogate pair in keys and values") +func TestStableJSONJavaScriptUnicodePredicates(t *testing.T) { + runStableJSONJavaScriptSelectors(t, []string{ + "stable JSON Unicode corpus", + "stable JSON Unicode table classifies every scalar and complement", + "stableJSONStringify admits every valid UTF-16 surrogate pair in keys and values", + "stableJSONStringify escapes unsafe scalars and preserves scalar key order", + "stableJSONStringify rejects every unpaired surrogate code unit in keys and values", + }) } func TestStableJSONJavaScriptDiagnosticWholeValueRedaction(t *testing.T) { - runStableJSONJavaScriptSelector(t, "diagnostic whole-value redaction") + runStableJSONJavaScriptSelectors(t, []string{"diagnostic whole-value redaction"}) } -func runStableJSONJavaScriptSelector(t *testing.T, name string) { +func runStableJSONJavaScriptSelectors(t *testing.T, names []string) { t.Helper() nodePath, err := exec.LookPath("node") if err != nil { @@ -41,7 +31,7 @@ func runStableJSONJavaScriptSelector(t *testing.T, name string) { } ctx, cancel := context.WithTimeout(t.Context(), 15*time.Second) defer cancel() - if err := nodetestselector.Run(ctx, nodePath, ".", "stable-json.test.mjs", name); err != nil { - t.Fatalf("run exact JavaScript selector: %v", err) + if err := nodetestselector.RunSet(ctx, nodePath, ".", "stable-json.test.mjs", names); err != nil { + t.Fatalf("run exact JavaScript selector set: %v", err) } } From 830715027b8d3b17b2683b0421d574f4345d84d2 Mon Sep 17 00:00:00 2001 From: iperev Date: Mon, 31 Aug 2026 16:08:40 +0200 Subject: [PATCH 04/11] fix: harden agent workflow closeout proofs --- internal/app/agent_workflow_command_test.go | 7 +- internal/app/agent_workflow_topology_test.go | 119 ++++++++++ internal/kernel/admit/fields.go | 27 ++- internal/kernel/admit/fields_test.go | 12 + internal/kernel/jsonpointer/jsonpointer.go | 4 + .../kernel/jsonpointer/jsonpointer_test.go | 2 +- internal/kernel/processgroup/process_other.go | 27 +++ internal/kernel/processgroup/process_unix.go | 31 +++ .../kernel/processgroup/process_unix_test.go | 35 +++ .../tools/browserproofverify/main_test.go | 19 +- internal/tools/commandoracle/execute.go | 13 +- internal/tools/commandoracle/process_unix.go | 28 --- internal/tools/packageverify/main.go | 8 +- internal/tools/packageverify/main_test.go | 19 +- internal/tools/pythonpackage/verify.go | 25 +-- .../tools/repositorysnapshot/git_inventory.go | 10 +- .../tools/repositorysnapshot/process_unix.go | 28 --- internal/tools/workflowsmoke/process.go | 139 ++++++++++++ .../tools/workflowsmoke/workflow_smoke.go | 212 +++++++++--------- .../workflowsmoke/workflow_smoke_test.go | 138 +++++++++++- 20 files changed, 680 insertions(+), 223 deletions(-) create mode 100644 internal/kernel/processgroup/process_other.go create mode 100644 internal/kernel/processgroup/process_unix.go create mode 100644 internal/kernel/processgroup/process_unix_test.go delete mode 100644 internal/tools/commandoracle/process_unix.go delete mode 100644 internal/tools/repositorysnapshot/process_unix.go create mode 100644 internal/tools/workflowsmoke/process.go diff --git a/internal/app/agent_workflow_command_test.go b/internal/app/agent_workflow_command_test.go index 48b0739..51462b6 100644 --- a/internal/app/agent_workflow_command_test.go +++ b/internal/app/agent_workflow_command_test.go @@ -21,12 +21,12 @@ func TestAgentWorkflowCLITruthTable(t *testing.T) { commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.008921652518915565373824823596859113418310007148763537766174180247340157842807") const ( semanticOutputClasses = 24 - frozenRejectionClasses = 44 + frozenRejectionClasses = 45 extraRejectionCases = 2 helpClasses = 10 colorClasses = 8 ) - if got, want := semanticOutputClasses+frozenRejectionClasses+extraRejectionCases+helpClasses+colorClasses, 88; got != want { + if got, want := semanticOutputClasses+frozenRejectionClasses+extraRejectionCases+helpClasses+colorClasses, 89; got != want { t.Fatalf("agent workflow CLI truth-table cardinality = %d, want %d", got, want) } t.Run("semantic output classes", testAgentWorkflowSemanticOutputClasses) @@ -149,6 +149,7 @@ func testAgentWorkflowUsageErrorsPrecedeInput(t *testing.T) { "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", "-"}, @@ -177,7 +178,7 @@ func testAgentWorkflowUsageErrorsPrecedeInput(t *testing.T) { "global/duplicate layout planner": {"--json-layout", "pretty", "--json-layout", "compact", "change-workflow-plan", "--input", "-"}, "global/duplicate layout guidance": {"--json-layout", "pretty", "--json-layout", "compact", "native-evidence-guidance"}, } - if got, want := len(cases)-2, 44; got != want { + if got, want := len(cases)-2, 45; got != want { t.Fatalf("frozen rejection class count = %d, want %d", got, want) } for name, args := range cases { diff --git a/internal/app/agent_workflow_topology_test.go b/internal/app/agent_workflow_topology_test.go index be65813..8f95718 100644 --- a/internal/app/agent_workflow_topology_test.go +++ b/internal/app/agent_workflow_topology_test.go @@ -9,6 +9,7 @@ import ( "path/filepath" "slices" "sort" + "strconv" "strings" "testing" ) @@ -69,6 +70,124 @@ func TestAgentWorkflowSemanticOwnerTopology(t *testing.T) { } assertNoRuntimeGuidanceResources(t, filepath.Join(root, "internal/command/changeworkflowplan")) assertNoRuntimeGuidanceResources(t, filepath.Join(root, "internal/command/nativeevidenceguidance")) + assertAgentWorkflowCarrierClosure(t, root) +} + +func assertAgentWorkflowCarrierClosure(t *testing.T, root string) { + t.Helper() + closures := []struct { + file string + imports []string + callees []string + }{ + { + file: "agent_workflow_args.go", + imports: []string{"fmt", "github.com/research-engineering/agentic-proofkit/internal/kernel/jsonpointer"}, + callees: []string{"fmt.Errorf", "jsonpointer.Parse", "len", "missingAgentWorkflowValue"}, + }, + { + file: "agent_workflow_command.go", + imports: []string{ + "github.com/research-engineering/agentic-proofkit/internal/command/changeworkflowplan", + "github.com/research-engineering/agentic-proofkit/internal/kernel/jsonpointer", + "io", + }, + callees: []string{ + "append", "changeWorkflowTerminalText", "changeworkflowplan.Build", "changeworkflowplan.BuildAgentEnvelope", + "changeworkflowplan.BuildTextProjection", "jsonpointer.SelectParsed", "len", "make", "newTerminalText", + "parseAgentWorkflowArgs", "readInput", "renderTerminalText", "runNativeEvidenceGuidance", "writeDiagnostic", + "writeDiagnosticf", "writeJSON", "writeText", + }, + }, + { + file: "native_evidence_guidance_command.go", + imports: []string{"github.com/research-engineering/agentic-proofkit/internal/command/nativeevidenceguidance", "io"}, + callees: []string{ + "append", "guidance.JSONValue", "len", "make", "nativeEvidenceGuidanceTerminalText", "nativeevidenceguidance.Build", + "nativeevidenceguidance.TextProjection", "newTerminalText", "renderTerminalText", "writeJSON", "writeText", + }, + }, + { + file: "terminal_style.go", + imports: []string{"fmt", "strings"}, + callees: []string{"append", "builder.String", "builder.WriteString", "fmt.Errorf", "renderTerminalText", "terminalTokenStyle"}, + }, + } + for _, closure := range closures { + path := filepath.Join(root, "internal/app", closure.file) + parsed, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.SkipObjectResolution) + if err != nil { + t.Fatalf("parse %s: %v", path, err) + } + imports := make([]string, 0, len(parsed.Imports)) + for _, specification := range parsed.Imports { + value, err := strconv.Unquote(specification.Path.Value) + if err != nil { + t.Fatalf("decode import in %s: %v", path, err) + } + imports = append(imports, value) + } + sort.Strings(imports) + expectedImports := append([]string(nil), closure.imports...) + sort.Strings(expectedImports) + if !slices.Equal(imports, expectedImports) { + t.Fatalf("agent workflow carrier imports in %s=%v want %v", closure.file, imports, expectedImports) + } + + calleeSet := map[string]struct{}{} + explicitReadInputCalls := 0 + ast.Inspect(parsed, func(node ast.Node) bool { + call, ok := node.(*ast.CallExpr) + if !ok { + return true + } + callee := "" + switch function := call.Fun.(type) { + case *ast.Ident: + callee = function.Name + if function.Name == "readInput" { + explicitReadInputCalls++ + if len(call.Args) != 2 { + t.Fatalf("agent workflow carrier %s must pass path and stdin to readInput", closure.file) + } + path, pathOK := call.Args[0].(*ast.SelectorExpr) + if !pathOK { + t.Fatalf("agent workflow carrier %s must pass options.inputPath to readInput", closure.file) + } + owner, ownerOK := path.X.(*ast.Ident) + stdin, stdinOK := call.Args[1].(*ast.Ident) + if !ownerOK || owner.Name != "options" || path.Sel.Name != "inputPath" || !stdinOK || stdin.Name != "stdin" { + t.Fatalf("agent workflow carrier %s must read only options.inputPath through the supplied stdin", closure.file) + } + } + case *ast.SelectorExpr: + owner, ok := function.X.(*ast.Ident) + if !ok { + t.Fatalf("agent workflow carrier %s contains a nonlocal selector call", closure.file) + } + callee = owner.Name + "." + function.Sel.Name + case *ast.ArrayType: + return true + default: + t.Fatalf("agent workflow carrier %s contains an indirect call of type %T", closure.file, call.Fun) + } + calleeSet[callee] = struct{}{} + return true + }) + if closure.file == "agent_workflow_command.go" && explicitReadInputCalls != 1 { + t.Fatalf("agent workflow carrier explicit readInput calls=%d want 1", explicitReadInputCalls) + } + callees := make([]string, 0, len(calleeSet)) + for callee := range calleeSet { + callees = append(callees, callee) + } + sort.Strings(callees) + expectedCallees := append([]string(nil), closure.callees...) + sort.Strings(expectedCallees) + if !slices.Equal(callees, expectedCallees) { + t.Fatalf("agent workflow carrier callees in %s=%v want %v", closure.file, callees, expectedCallees) + } + } } func assertSingleSemanticTableOwner(t *testing.T, dir, variable, expectedFile string) { diff --git a/internal/kernel/admit/fields.go b/internal/kernel/admit/fields.go index 52e7cda..62c8571 100644 --- a/internal/kernel/admit/fields.go +++ b/internal/kernel/admit/fields.go @@ -197,10 +197,33 @@ func RedactDiagnosticValue(value string) string { } func RedactStructuralText(value string) string { - if ContainsReportVisibleUnsafeValue(value) { + if !unicodepolicy.ValidScalarString(value) || ContainsSecretLikeValue(value) || ContainsSecretLikeValue(withoutUnsafeScalars(value)) { return redactedValueLabel } - return value + var builder strings.Builder + redacting := false + for _, character := range value { + if unicodepolicy.IsUnsafeScalar(character) { + if !redacting { + builder.WriteString("") + redacting = true + } + continue + } + redacting = false + builder.WriteRune(character) + } + return builder.String() +} + +func withoutUnsafeScalars(value string) string { + var builder strings.Builder + for _, character := range value { + if !unicodepolicy.IsUnsafeScalar(character) { + builder.WriteRune(character) + } + } + return builder.String() } func DisplayOnlyCommandText(raw any, context string) (string, error) { diff --git a/internal/kernel/admit/fields_test.go b/internal/kernel/admit/fields_test.go index aa026be..5afd379 100644 --- a/internal/kernel/admit/fields_test.go +++ b/internal/kernel/admit/fields_test.go @@ -164,6 +164,18 @@ func TestRedactDiagnosticValueReplacesRejectedValuesAsAWhole(t *testing.T) { t.Fatalf("RedactDiagnosticValue unsafe class = %q, want fixed label", got) } } + for _, test := range []struct { + input string + want string + }{ + {input: "line\nbreak", want: "linebreak"}, + {input: "unsafe\u200bvalue", want: "unsafevalue"}, + {input: string([]byte{'p', 'a', 't', 'h', 0xff}), want: redactedValueLabel}, + } { + if got := RedactStructuralText(test.input); got != test.want { + t.Fatalf("RedactStructuralText(%q)=%q want %q", test.input, got, test.want) + } + } longSafe := strings.Repeat("x", maxDiagnosticRunes+20) if got := RedactDiagnosticValue(longSafe); !strings.HasSuffix(got, "...") { t.Fatalf("safe long diagnostic was not bounded: %q", got) diff --git a/internal/kernel/jsonpointer/jsonpointer.go b/internal/kernel/jsonpointer/jsonpointer.go index ea02729..9f638d4 100644 --- a/internal/kernel/jsonpointer/jsonpointer.go +++ b/internal/kernel/jsonpointer/jsonpointer.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" + "github.com/research-engineering/agentic-proofkit/internal/kernel/unicodepolicy" ) // Pointer is an admitted RFC 6901 pointer. Its tokens are immutable outside @@ -16,6 +17,9 @@ type Pointer struct { // Parse admits pointer syntax without consulting a JSON document. func Parse(value string) (Pointer, error) { + if !unicodepolicy.ValidScalarString(value) { + return Pointer{}, fmt.Errorf("JSON pointer must be valid UTF-8") + } if value == "" { return Pointer{}, nil } diff --git a/internal/kernel/jsonpointer/jsonpointer_test.go b/internal/kernel/jsonpointer/jsonpointer_test.go index b8d2369..5195ab3 100644 --- a/internal/kernel/jsonpointer/jsonpointer_test.go +++ b/internal/kernel/jsonpointer/jsonpointer_test.go @@ -55,7 +55,7 @@ func TestParseAndSelectParsedPreserveSelectionSemantics(t *testing.T) { } func TestParseRejectsInvalidSyntaxWithoutDocumentAccess(t *testing.T) { - for _, value := range []string{"not-a-pointer", "/bad~", "/bad~2escape"} { + for _, value := range []string{"not-a-pointer", "/bad~", "/bad~2escape", string([]byte{'/', 0xff})} { t.Run(value, func(t *testing.T) { if _, err := Parse(value); err == nil { t.Fatal("expected syntax error") diff --git a/internal/kernel/processgroup/process_other.go b/internal/kernel/processgroup/process_other.go new file mode 100644 index 0000000..c5f3539 --- /dev/null +++ b/internal/kernel/processgroup/process_other.go @@ -0,0 +1,27 @@ +//go:build !darwin && !linux + +// Package processgroup owns bounded external-process group termination. +package processgroup + +import ( + "errors" + "os" + "os/exec" +) + +// Configure retains exec.CommandContext's direct-process cancellation on +// platforms where this package has no process-group primitive. +func Configure(command *exec.Cmd) {} + +// Terminate kills the direct child on platforms without a process-group +// primitive. +func Terminate(command *exec.Cmd) error { + if command.Process == nil { + return nil + } + err := command.Process.Kill() + if errors.Is(err, os.ErrProcessDone) { + return nil + } + return err +} diff --git a/internal/kernel/processgroup/process_unix.go b/internal/kernel/processgroup/process_unix.go new file mode 100644 index 0000000..d9313b2 --- /dev/null +++ b/internal/kernel/processgroup/process_unix.go @@ -0,0 +1,31 @@ +//go:build darwin || linux + +// Package processgroup owns bounded external-process group termination. +package processgroup + +import ( + "errors" + "os" + "os/exec" + "syscall" +) + +// Configure places a command in a new process group and makes context +// cancellation terminate that group. +func Configure(command *exec.Cmd) { + command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + command.Cancel = func() error { return Terminate(command) } +} + +// Terminate kills the configured command process group. A process that has +// already exited satisfies the termination obligation. +func Terminate(command *exec.Cmd) error { + if command.Process == nil { + return nil + } + err := syscall.Kill(-command.Process.Pid, syscall.SIGKILL) + if errors.Is(err, os.ErrProcessDone) || errors.Is(err, syscall.ESRCH) { + return nil + } + return err +} diff --git a/internal/kernel/processgroup/process_unix_test.go b/internal/kernel/processgroup/process_unix_test.go new file mode 100644 index 0000000..5df8d5b --- /dev/null +++ b/internal/kernel/processgroup/process_unix_test.go @@ -0,0 +1,35 @@ +//go:build darwin || linux + +package processgroup + +import ( + "bytes" + "context" + "os/exec" + "testing" + "time" +) + +func TestConfigureTerminatesDescendantOnContextCancellation(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond) + defer cancel() + command := exec.CommandContext(ctx, "/bin/sh", "-c", "/bin/sleep 2 & wait") + Configure(command) + var stdout bytes.Buffer + var stderr bytes.Buffer + command.Stdout = &stdout + command.Stderr = &stderr + started := time.Now() + if err := command.Run(); err == nil { + t.Fatal("canceled process group returned success") + } + if elapsed := time.Since(started); elapsed >= time.Second { + t.Fatalf("process group termination took %s", elapsed) + } +} + +func TestTerminateBeforeStartIsSatisfied(t *testing.T) { + if err := Terminate(exec.Command("unused")); err != nil { + t.Fatalf("Terminate before start: %v", err) + } +} diff --git a/internal/tools/browserproofverify/main_test.go b/internal/tools/browserproofverify/main_test.go index fc51cc7..5d635cc 100644 --- a/internal/tools/browserproofverify/main_test.go +++ b/internal/tools/browserproofverify/main_test.go @@ -18,16 +18,19 @@ import ( ) func TestDiagnosticsDoNotLeakArtifactValues(t *testing.T) { - for _, value := range []string{ - "artifact api_key=abc123456789 failed", - "artifact line\nbreak failed", - "artifact unsafe\u200bvalue failed", - string([]byte{'p', 'a', 't', 'h', 0xff}), + for _, test := range []struct { + value string + want string + }{ + {value: "artifact api_key=abc123456789 failed", want: "browser runtime proof verification failed: \n"}, + {value: "artifact line\nbreak failed", want: "browser runtime proof verification failed: artifact linebreak failed\n"}, + {value: "artifact unsafe\u200bvalue failed", want: "browser runtime proof verification failed: artifact unsafevalue failed\n"}, + {value: string([]byte{'p', 'a', 't', 'h', 0xff}), want: "browser runtime proof verification failed: \n"}, } { var output bytes.Buffer - writeBrowserProofFailure(&output, errors.New(value)) - if got, want := output.String(), "browser runtime proof verification failed: \n"; got != want { - t.Fatalf("visible browser proof diagnostic = %q, want %q", got, want) + writeBrowserProofFailure(&output, errors.New(test.value)) + if got := output.String(); got != test.want { + t.Fatalf("visible browser proof diagnostic = %q, want %q", got, test.want) } } } diff --git a/internal/tools/commandoracle/execute.go b/internal/tools/commandoracle/execute.go index 2da5c55..540a602 100644 --- a/internal/tools/commandoracle/execute.go +++ b/internal/tools/commandoracle/execute.go @@ -18,6 +18,7 @@ import ( "time" "github.com/research-engineering/agentic-proofkit/internal/app" + "github.com/research-engineering/agentic-proofkit/internal/kernel/processgroup" "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" "github.com/research-engineering/agentic-proofkit/internal/tools/artifactfile" "github.com/research-engineering/agentic-proofkit/internal/tools/repositorysnapshot" @@ -161,7 +162,7 @@ func runGoTestCommand(ctx context.Context, root string, argv []string, ledger *e command := exec.CommandContext(ctx, goExecutable, argv[1:]...) command.Dir = root command.WaitDelay = processWaitDelay - configureProcessGroup(command) + processgroup.Configure(command) stdout, err := command.StdoutPipe() if err != nil { return decision("process.stdout_pipe_failed") @@ -192,15 +193,15 @@ func runGoTestCommand(ctx context.Context, root string, argv []string, ledger *e case parseErr = <-parseDone: parseComplete = true if parseErr != nil { - _ = terminateProcessGroup(command) + _ = processgroup.Terminate(command) } case <-overflowChannel: overflowed = true overflowChannel = nil - _ = terminateProcessGroup(command) + _ = processgroup.Terminate(command) case <-contextChannel: contextChannel = nil - _ = terminateProcessGroup(command) + _ = processgroup.Terminate(command) } } waitDone := make(chan error, 1) @@ -214,10 +215,10 @@ func runGoTestCommand(ctx context.Context, root string, argv []string, ledger *e case <-overflowChannel: overflowed = true overflowChannel = nil - _ = terminateProcessGroup(command) + _ = processgroup.Terminate(command) case <-contextChannel: contextChannel = nil - _ = terminateProcessGroup(command) + _ = processgroup.Terminate(command) } } <-stderrDone diff --git a/internal/tools/commandoracle/process_unix.go b/internal/tools/commandoracle/process_unix.go deleted file mode 100644 index a8e1c36..0000000 --- a/internal/tools/commandoracle/process_unix.go +++ /dev/null @@ -1,28 +0,0 @@ -//go:build darwin || linux - -package commandoracle - -import ( - "errors" - "os" - "os/exec" - "syscall" -) - -func configureProcessGroup(command *exec.Cmd) { - command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} - command.Cancel = func() error { - return terminateProcessGroup(command) - } -} - -func terminateProcessGroup(command *exec.Cmd) error { - if command.Process == nil { - return nil - } - err := syscall.Kill(-command.Process.Pid, syscall.SIGKILL) - if errors.Is(err, os.ErrProcessDone) || errors.Is(err, syscall.ESRCH) { - return nil - } - return err -} diff --git a/internal/tools/packageverify/main.go b/internal/tools/packageverify/main.go index b7526fe..f8d7f71 100644 --- a/internal/tools/packageverify/main.go +++ b/internal/tools/packageverify/main.go @@ -4,6 +4,7 @@ import ( "archive/tar" "bytes" "compress/gzip" + "context" "crypto/sha1" "crypto/sha512" "encoding/base64" @@ -2414,9 +2415,10 @@ func verifyInstalledJSONABI(consumer string) error { if err := verifyJSONAdapterSourceSmoke(consumer); err != nil { return err } - if err := workflowsmoke.Verify(func(input []byte, args ...string) (workflowsmoke.Result, error) { - result, err := runInstalledWithInput(consumer, input, args...) - return workflowsmoke.Result{ExitCode: result.ExitCode, Stdout: result.Stdout, Stderr: result.Stderr}, err + if err := workflowsmoke.VerifyProcess(context.Background(), workflowsmoke.ProcessCarrier{ + Directory: consumer, + Executable: "npm", + Prefix: []string{"exec", "--offline", "--", "agentic-proofkit"}, }); err != nil { return fmt.Errorf("outside consumer agent-workflow smoke failed: %w", err) } diff --git a/internal/tools/packageverify/main_test.go b/internal/tools/packageverify/main_test.go index cd259a8..c9dd469 100644 --- a/internal/tools/packageverify/main_test.go +++ b/internal/tools/packageverify/main_test.go @@ -302,16 +302,19 @@ func TestReadManifestFromTarRejectsUnknownPackageManifestFields(t *testing.T) { } func TestPackageVerifierDiagnosticNondisclosure(t *testing.T) { - for _, value := range []string{ - "package verification failed for api_key=abc123456789", - "package verification failed for line\nbreak", - "package verification failed for unsafe\u200bvalue", - string([]byte{'p', 'a', 't', 'h', 0xff}), + for _, test := range []struct { + value string + want string + }{ + {value: "package verification failed for api_key=abc123456789", want: "\n"}, + {value: "package verification failed for line\nbreak", want: "package verification failed for linebreak\n"}, + {value: "package verification failed for unsafe\u200bvalue", want: "package verification failed for unsafevalue\n"}, + {value: string([]byte{'p', 'a', 't', 'h', 0xff}), want: "\n"}, } { var output bytes.Buffer - writeVerificationFailure(&output, errors.New(value)) - if got, want := output.String(), "\n"; got != want { - t.Fatalf("visible package diagnostic = %q, want %q", got, want) + writeVerificationFailure(&output, errors.New(test.value)) + if got := output.String(); got != test.want { + t.Fatalf("visible package diagnostic = %q, want %q", got, test.want) } } } diff --git a/internal/tools/pythonpackage/verify.go b/internal/tools/pythonpackage/verify.go index a74091d..dd91632 100644 --- a/internal/tools/pythonpackage/verify.go +++ b/internal/tools/pythonpackage/verify.go @@ -3,13 +3,13 @@ package main import ( "archive/zip" "bytes" + "context" "crypto/sha256" "debug/elf" "debug/macho" "encoding/base64" "encoding/csv" "encoding/json" - "errors" "fmt" "io" "os" @@ -458,25 +458,10 @@ func verifyInstalledPythonWheel(consumer string, venvPython string, wheelPath st } func verifyInstalledWorkflowSmoke(dir string, executable string, prefix ...string) error { - return workflowsmoke.Verify(func(input []byte, args ...string) (workflowsmoke.Result, error) { - argv := append(append([]string(nil), prefix...), args...) - command := exec.Command(executable, argv...) - command.Dir = dir - command.Stdin = bytes.NewReader(input) - var stdout bytes.Buffer - var stderr bytes.Buffer - command.Stdout = &stdout - command.Stderr = &stderr - err := command.Run() - exitCode := 0 - if err != nil { - var exitErr *exec.ExitError - if !errors.As(err, &exitErr) { - return workflowsmoke.Result{}, err - } - exitCode = exitErr.ExitCode() - } - return workflowsmoke.Result{ExitCode: exitCode, Stdout: stdout.Bytes(), Stderr: stderr.Bytes()}, nil + return workflowsmoke.VerifyProcess(context.Background(), workflowsmoke.ProcessCarrier{ + Directory: dir, + Executable: executable, + Prefix: append([]string(nil), prefix...), }) } diff --git a/internal/tools/repositorysnapshot/git_inventory.go b/internal/tools/repositorysnapshot/git_inventory.go index bcd6e5a..b041c5a 100644 --- a/internal/tools/repositorysnapshot/git_inventory.go +++ b/internal/tools/repositorysnapshot/git_inventory.go @@ -9,6 +9,8 @@ import ( "strings" "sync" "time" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/processgroup" ) const ( @@ -93,7 +95,7 @@ func gitOutput(ctx context.Context, root string, args ...string) (string, error) command := exec.CommandContext(ctx, "git", args...) command.Dir = root command.WaitDelay = processWaitDelay - configureProcessGroup(command) + processgroup.Configure(command) stdout := newBoundedBuffer() stderr := newBoundedBuffer() command.Stdout = stdout @@ -116,14 +118,14 @@ func gitOutput(ctx context.Context, root string, args ...string) (string, error) case <-stdoutExceeded: overflowed = true stdoutExceeded = nil - _ = terminateProcessGroup(command) + _ = processgroup.Terminate(command) case <-stderrExceeded: overflowed = true stderrExceeded = nil - _ = terminateProcessGroup(command) + _ = processgroup.Terminate(command) case <-contextDone: contextDone = nil - _ = terminateProcessGroup(command) + _ = processgroup.Terminate(command) } } if ctx.Err() != nil { diff --git a/internal/tools/repositorysnapshot/process_unix.go b/internal/tools/repositorysnapshot/process_unix.go deleted file mode 100644 index 1aaa1e1..0000000 --- a/internal/tools/repositorysnapshot/process_unix.go +++ /dev/null @@ -1,28 +0,0 @@ -//go:build darwin || linux - -package repositorysnapshot - -import ( - "errors" - "os" - "os/exec" - "syscall" -) - -func configureProcessGroup(command *exec.Cmd) { - command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} - command.Cancel = func() error { - return terminateProcessGroup(command) - } -} - -func terminateProcessGroup(command *exec.Cmd) error { - if command.Process == nil { - return nil - } - err := syscall.Kill(-command.Process.Pid, syscall.SIGKILL) - if errors.Is(err, os.ErrProcessDone) || errors.Is(err, syscall.ESRCH) { - return nil - } - return err -} diff --git a/internal/tools/workflowsmoke/process.go b/internal/tools/workflowsmoke/process.go new file mode 100644 index 0000000..d60b814 --- /dev/null +++ b/internal/tools/workflowsmoke/process.go @@ -0,0 +1,139 @@ +package workflowsmoke + +import ( + "bytes" + "context" + "errors" + "fmt" + "os" + "os/exec" + "sync" + "time" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/processgroup" +) + +const ( + processWaitDelay = 2 * time.Second + maximumStdoutBytes = 256 * 1024 + maximumStderrBytes = 64 * 1024 +) + +// ProcessCarrier identifies one installed executable and any fixed argv prefix. +type ProcessCarrier struct { + Directory string + Executable string + Prefix []string + Environment []string +} + +// VerifyProcess applies Verify to one installed process carrier. +func VerifyProcess(ctx context.Context, carrier ProcessCarrier) error { + return Verify(ctx, func(ctx context.Context, invocation Invocation) (Result, error) { + return RunProcess(ctx, carrier, invocation) + }) +} + +// RunProcess executes one bounded carrier invocation. Output overflow and +// cancellation terminate the whole process group on supported Unix hosts. +func RunProcess(ctx context.Context, carrier ProcessCarrier, invocation Invocation) (Result, error) { + if ctx == nil || carrier.Executable == "" { + return Result{}, fmt.Errorf("process carrier requires a context and executable") + } + if invocation.StdinClass != StdinBytes && invocation.StdinClass != StdinMustRemainUnread { + return Result{}, fmt.Errorf("process carrier received an unsupported stdin class") + } + processContext, cancel := context.WithCancel(ctx) + defer cancel() + args := append(append([]string(nil), carrier.Prefix...), invocation.Args...) + command := exec.CommandContext(processContext, carrier.Executable, args...) + command.Dir = carrier.Directory + command.WaitDelay = processWaitDelay + if carrier.Environment != nil { + command.Env = append([]string(nil), carrier.Environment...) + } + processgroup.Configure(command) + stdout := newBoundedBuffer(maximumStdoutBytes, cancel) + stderr := newBoundedBuffer(maximumStderrBytes, cancel) + command.Stdout = stdout + command.Stderr = stderr + var stdinRead *os.File + var stdinWrite *os.File + if invocation.StdinClass == StdinMustRemainUnread { + var err error + stdinRead, stdinWrite, err = os.Pipe() + if err != nil { + return Result{}, fmt.Errorf("create unread-stdin oracle: %w", err) + } + defer stdinRead.Close() + defer stdinWrite.Close() + command.Stdin = stdinRead + } else { + command.Stdin = bytes.NewReader(invocation.Input) + } + err := command.Run() + result := Result{Stdout: stdout.Bytes(), Stderr: stderr.Bytes()} + if stdout.Overflowed() { + return Result{}, fmt.Errorf("process carrier stdout exceeds %d bytes", maximumStdoutBytes) + } + if stderr.Overflowed() { + return Result{}, fmt.Errorf("process carrier stderr exceeds %d bytes", maximumStderrBytes) + } + if ctx.Err() != nil { + return Result{}, fmt.Errorf("process carrier invocation canceled: %w", ctx.Err()) + } + if err == nil { + return result, nil + } + var exitError *exec.ExitError + if errors.As(err, &exitError) { + result.ExitCode = exitError.ExitCode() + return result, nil + } + return Result{}, fmt.Errorf("process carrier invocation failed: %w", err) +} + +type boundedBuffer struct { + mu sync.Mutex + content []byte + limit int + overflow bool + cancel context.CancelFunc +} + +func newBoundedBuffer(limit int, cancel context.CancelFunc) *boundedBuffer { + return &boundedBuffer{limit: limit, cancel: cancel} +} + +func (buffer *boundedBuffer) Write(value []byte) (int, error) { + buffer.mu.Lock() + remaining := buffer.limit - len(buffer.content) + if remaining > 0 { + count := len(value) + if count > remaining { + count = remaining + } + buffer.content = append(buffer.content, value[:count]...) + } + firstOverflow := len(value) > remaining && !buffer.overflow + if len(value) > remaining { + buffer.overflow = true + } + buffer.mu.Unlock() + if firstOverflow { + buffer.cancel() + } + return len(value), nil +} + +func (buffer *boundedBuffer) Bytes() []byte { + buffer.mu.Lock() + defer buffer.mu.Unlock() + return append([]byte(nil), buffer.content...) +} + +func (buffer *boundedBuffer) Overflowed() bool { + buffer.mu.Lock() + defer buffer.mu.Unlock() + return buffer.overflow +} diff --git a/internal/tools/workflowsmoke/workflow_smoke.go b/internal/tools/workflowsmoke/workflow_smoke.go index 3abb0c9..b9283cb 100644 --- a/internal/tools/workflowsmoke/workflow_smoke.go +++ b/internal/tools/workflowsmoke/workflow_smoke.go @@ -2,13 +2,20 @@ package workflowsmoke import ( "bytes" + "context" "fmt" + "time" + "github.com/research-engineering/agentic-proofkit/internal/command/changeworkflowplan" + "github.com/research-engineering/agentic-proofkit/internal/command/nativeevidenceguidance" "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" ) -const workflowInput = `{"checkpoint":{"state":"not_started"},"completedStageIds":[],"contextRefs":[],"governingAuthorityRefId":null,"requiredContextRefIds":[],"schemaVersion":1}` +const ( + workflowInput = `{"checkpoint":{"state":"not_started"},"completedStageIds":[],"contextRefs":[],"governingAuthorityRefId":null,"requiredContextRefIds":[],"schemaVersion":1}` + invocationTimeout = 30 * time.Second +) // Result is the observable process contract for one installed CLI invocation. type Result struct { @@ -17,102 +24,110 @@ type Result struct { Stderr []byte } -// Runner invokes one installed carrier with the supplied stdin and argv. -type Runner func(input []byte, args ...string) (Result, error) +// StdinClass declares whether an invocation owns stdin bytes or must not read +// stdin at all. +type StdinClass uint8 + +const ( + StdinBytes StdinClass = iota + StdinMustRemainUnread +) + +// Invocation is one immutable installed-carrier request. +type Invocation struct { + Args []string + Input []byte + StdinClass StdinClass +} + +// Runner invokes one installed carrier under the supplied lifecycle context. +type Runner func(context.Context, Invocation) (Result, error) // Verify exercises the public agent-workflow relation through one installed -// CLI carrier. It owns expectations; carrier adapters own process execution. -func Verify(run Runner) error { +// CLI carrier. Command owners provide exact expectations; adapters own only +// process execution. +func Verify(ctx context.Context, run Runner) error { + if ctx == nil || run == nil { + return fmt.Errorf("workflow smoke requires a context and runner") + } input := []byte(workflowInput) - - plan, err := invoke(run, "planner JSON", input, "change-workflow-plan", "--input", "-") + inputValue, err := admission.DecodeJSON(bytes.NewReader(input), int64(len(input))) if err != nil { - return err + return fmt.Errorf("decode workflow smoke fixture: %w", err) } - planValue, err := verifyJSONObject(plan, "planner JSON") + expectedPlan, err := changeworkflowplan.Build(inputValue) if err != nil { - return err - } - if err := requireString(planValue, "reportKind", "proofkit.change-workflow-plan"); err != nil { - return fmt.Errorf("planner JSON: %w", err) + return fmt.Errorf("build expected planner JSON: %w", err) } - if err := requireString(planValue, "reportId", "proofkit.change-workflow-plan"); err != nil { - return fmt.Errorf("planner JSON: %w", err) + expectedEnvelope, err := changeworkflowplan.BuildAgentEnvelope(inputValue) + if err != nil { + return fmt.Errorf("build expected planner envelope: %w", err) } - if err := requireString(planValue, "outputKind", "next_action"); err != nil { - return fmt.Errorf("planner JSON: %w", err) + expectedText, err := changeworkflowplan.BuildText(inputValue) + if err != nil { + return fmt.Errorf("build expected planner text: %w", err) } - if err := requireString(planValue, "activeStageId", "architecture"); err != nil { - return fmt.Errorf("planner JSON: %w", err) + expectedGuidance, err := nativeevidenceguidance.Build() + if err != nil { + return fmt.Errorf("build expected native evidence guidance: %w", err) } - if err := requireString(planValue, "action", "author"); err != nil { - return fmt.Errorf("planner JSON: %w", err) + expectedGuidanceText, err := nativeevidenceguidance.RenderPlainText() + if err != nil { + return fmt.Errorf("build expected native evidence guidance text: %w", err) } - if err := requireString(planValue, "checkpointState", "not_started"); err != nil { - return fmt.Errorf("planner JSON: %w", err) + + plan, err := invoke(ctx, run, "planner JSON", bytesInvocation(input, "change-workflow-plan", "--input", "-")) + if err != nil { + return err } - if err := requireString(planValue, "workflowProfileId", "proofkit.reviewed-change.v1"); err != nil { - return fmt.Errorf("planner JSON: %w", err) + if err := verifyExactJSONObject(plan, expectedPlan, "planner JSON"); err != nil { + return err } - compact, err := invoke(run, "planner compact JSON", input, "--json-layout", "compact", "change-workflow-plan", "--input", "-") + compact, err := invoke(ctx, run, "planner compact JSON", bytesInvocation(input, "--json-layout", "compact", "change-workflow-plan", "--input", "-")) if err != nil { return err } - compactValue, err := verifyJSONObject(compact, "planner compact JSON") - if err != nil { + if err := verifyExactJSONObject(compact, expectedPlan, "planner compact JSON"); err != nil { return err } - if err := verifyCanonicalCompactJSON(compact.Stdout, compactValue); err != nil { + if err := verifyCanonicalCompactJSON(compact.Stdout, expectedPlan); err != nil { return fmt.Errorf("planner compact JSON: %w", err) } - envelope, err := invoke(run, "planner agent envelope", input, "change-workflow-plan", "--input", "-", "--agent-envelope") + envelope, err := invoke(ctx, run, "planner agent envelope", bytesInvocation(input, "change-workflow-plan", "--input", "-", "--agent-envelope")) if err != nil { return err } - envelopeValue, err := verifyJSONObject(envelope, "planner agent envelope") - if err != nil { + if err := verifyExactJSONObject(envelope, expectedEnvelope, "planner agent envelope"); err != nil { return err } - if err := requireString(envelopeValue, "envelopeId", "proofkit.change-workflow-plan.agent-envelope"); err != nil { - return fmt.Errorf("planner agent envelope: %w", err) - } - sourceReport, ok := envelopeValue["sourceReport"].(map[string]any) - if !ok { - return fmt.Errorf("planner agent envelope: sourceReport must be an object") - } - for field, want := range map[string]string{"reportId": "proofkit.change-workflow-plan", "reportKind": "proofkit.change-workflow-plan", "state": "passed"} { - if err := requireString(sourceReport, field, want); err != nil { - return fmt.Errorf("planner agent envelope sourceReport: %w", err) - } - } - text, err := invoke(run, "planner text", input, "change-workflow-plan", "--input", "-", "--format", "text", "--color", "never") + text, err := invoke(ctx, run, "planner text", bytesInvocation(input, "change-workflow-plan", "--input", "-", "--format", "text", "--color", "never")) if err != nil { return err } - if !bytes.HasPrefix(text.Stdout, []byte("Change workflow plan\n")) || bytes.Contains(text.Stdout, []byte("\x1b[")) { - return fmt.Errorf("planner text must be plain text with the canonical heading") + if !bytes.Equal(text.Stdout, []byte(expectedText)) || bytes.Contains(text.Stdout, []byte("\x1b[")) { + return fmt.Errorf("planner text does not equal the command-owned plain-text projection") } - if err := verifyFailure(run, "required input", nil, "requires --input ", "change-workflow-plan"); err != nil { + if err := verifyFailure(ctx, run, "required input", bytesInvocation(nil, "change-workflow-plan"), "requires --input "); err != nil { return err } - if err := verifyFailure(run, "pre-read input pointer", nil, "JSON pointer", "change-workflow-plan", "--input", "proofkit-smoke-missing-input.json", "--input-pointer", "invalid"); err != nil { + 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 { return err } - if err := verifyFailure(run, "JSON color denial", input, "--color is valid only with --format text", "change-workflow-plan", "--input", "-", "--color", "never"); err != nil { + 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 { return err } - if err := verifyFailure(run, "exclusive help", input, "help accepts no additional arguments", "change-workflow-plan", "--help", "--format", "text"); err != nil { + if err := verifyFailure(ctx, run, "exclusive help", bytesInvocation(input, "change-workflow-plan", "--help", "--format", "text"), "help accepts no additional arguments"); err != nil { return err } - if err := verifyFailure(run, "surplus positional operand", input, "unsupported argument", "change-workflow-plan", "--input", "-", "surplus"); err != nil { + if err := verifyFailure(ctx, run, "surplus positional operand", bytesInvocation(input, "change-workflow-plan", "--input", "-", "surplus"), "unsupported argument"); err != nil { return err } - help, err := invoke(run, "exclusive help success", []byte("must remain unread"), "change-workflow-plan", "--help") + help, err := invoke(ctx, run, "exclusive help success", unreadInvocation("change-workflow-plan", "--help")) if err != nil { return err } @@ -120,43 +135,36 @@ func Verify(run Runner) error { return fmt.Errorf("exclusive help success must emit plain command help") } - guidance, err := invoke(run, "no-input guidance JSON", []byte("must remain unread"), "native-evidence-guidance") + guidance, err := invoke(ctx, run, "no-input guidance JSON", unreadInvocation("native-evidence-guidance")) if err != nil { return err } - guidanceValue, err := verifyJSONObject(guidance, "no-input guidance JSON") - if err != nil { + if err := verifyExactJSONObject(guidance, expectedGuidance.JSONValue(), "no-input guidance JSON"); err != nil { return err } - if err := requireString(guidanceValue, "guidanceId", "proofkit.native-evidence-guidance.v1"); err != nil { - return fmt.Errorf("no-input guidance JSON: %w", err) - } - slots, ok := guidanceValue["slots"].([]any) - if !ok || len(slots) != 22 { - return fmt.Errorf("no-input guidance JSON: slots must contain 22 records") - } - if err := requireStringObject(slots[0], "slotId", "semantic_owner"); err != nil { - return fmt.Errorf("no-input guidance JSON: first slot: %w", err) - } - if err := requireStringObject(slots[0], "applicabilityClass", "always"); err != nil { - return fmt.Errorf("no-input guidance JSON: first slot: %w", err) - } - if err := requireStringObject(slots[len(slots)-1], "slotId", "non_claims"); err != nil { - return fmt.Errorf("no-input guidance JSON: last slot: %w", err) - } - guidanceText, err := invoke(run, "no-input guidance text", []byte("must remain unread"), "native-evidence-guidance", "--format", "text", "--color", "never") + guidanceText, err := invoke(ctx, run, "no-input guidance text", unreadInvocation("native-evidence-guidance", "--format", "text", "--color", "never")) if err != nil { return err } - if !bytes.HasPrefix(guidanceText.Stdout, []byte("semantic_owner: applicability: always; decision:")) || bytes.Contains(guidanceText.Stdout, []byte("\x1b[")) { - return fmt.Errorf("no-input guidance text must be plain text with the canonical first slot") + 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 } -func invoke(run Runner, label string, input []byte, args ...string) (Result, error) { - result, err := run(input, args...) +func bytesInvocation(input []byte, args ...string) Invocation { + return Invocation{Args: append([]string(nil), args...), Input: append([]byte(nil), input...), StdinClass: StdinBytes} +} + +func unreadInvocation(args ...string) Invocation { + return Invocation{Args: append([]string(nil), args...), StdinClass: StdinMustRemainUnread} +} + +func invoke(ctx context.Context, run Runner, label string, invocation Invocation) (Result, error) { + invocationContext, cancel := context.WithTimeout(ctx, invocationTimeout) + defer cancel() + result, err := run(invocationContext, invocation) if err != nil { return Result{}, fmt.Errorf("%s carrier invocation: %w", label, err) } @@ -172,8 +180,10 @@ func invoke(run Runner, label string, input []byte, args ...string) (Result, err return result, nil } -func verifyFailure(run Runner, label string, input []byte, diagnosticFragment string, args ...string) error { - result, err := run(input, args...) +func verifyFailure(ctx context.Context, run Runner, label string, invocation Invocation, diagnosticFragment string) error { + invocationContext, cancel := context.WithTimeout(ctx, invocationTimeout) + defer cancel() + result, err := run(invocationContext, invocation) if err != nil { return fmt.Errorf("%s carrier invocation: %w", label, err) } @@ -186,20 +196,30 @@ func verifyFailure(run Runner, label string, input []byte, diagnosticFragment st return nil } -func verifyJSONObject(result Result, label string) (map[string]any, error) { - value, err := admission.DecodeJSON(bytes.NewReader(result.Stdout), int64(len(result.Stdout))) +func verifyExactJSONObject(result Result, expected map[string]any, label string) error { + actual, err := admission.DecodeJSON(bytes.NewReader(result.Stdout), maximumStdoutBytes) if err != nil { - return nil, fmt.Errorf("%s stdout must contain exactly one strict JSON value: %w", label, err) + return fmt.Errorf("%s stdout must contain exactly one strict JSON value: %w", label, err) + } + if _, ok := actual.(map[string]any); !ok { + return fmt.Errorf("%s stdout JSON must be an object", label) } - record, ok := value.(map[string]any) - if !ok { - return nil, fmt.Errorf("%s stdout JSON must be an object", label) + actualCanonical, err := stablejson.MarshalLayout(actual, stablejson.LayoutCompact) + if err != nil { + return fmt.Errorf("%s actual JSON is not canonicalizable: %w", label, err) } - return record, nil + expectedCanonical, err := stablejson.MarshalLayout(expected, stablejson.LayoutCompact) + if err != nil { + return fmt.Errorf("%s expected JSON is not canonicalizable: %w", label, err) + } + if !bytes.Equal(actualCanonical, expectedCanonical) { + return fmt.Errorf("%s JSON does not equal the command-owned projection", label) + } + return nil } -func verifyCanonicalCompactJSON(output []byte, value any) error { - canonical, err := stablejson.MarshalLayout(value, stablejson.LayoutCompact) +func verifyCanonicalCompactJSON(output []byte, expected map[string]any) error { + canonical, err := stablejson.MarshalLayout(expected, stablejson.LayoutCompact) if err != nil { return err } @@ -208,19 +228,3 @@ func verifyCanonicalCompactJSON(output []byte, value any) error { } return nil } - -func requireString(record map[string]any, field string, want string) error { - got, ok := record[field].(string) - if !ok || got != want { - return fmt.Errorf("%s=%v, want %q", field, record[field], want) - } - return nil -} - -func requireStringObject(value any, field string, want string) error { - record, ok := value.(map[string]any) - if !ok { - return fmt.Errorf("value must be an object") - } - return requireString(record, field, want) -} diff --git a/internal/tools/workflowsmoke/workflow_smoke_test.go b/internal/tools/workflowsmoke/workflow_smoke_test.go index b02a65b..572e543 100644 --- a/internal/tools/workflowsmoke/workflow_smoke_test.go +++ b/internal/tools/workflowsmoke/workflow_smoke_test.go @@ -3,15 +3,22 @@ package workflowsmoke_test import ( "bytes" "context" + "errors" + "fmt" + "io" + "os" "strings" "testing" + "time" "github.com/research-engineering/agentic-proofkit/internal/app" "github.com/research-engineering/agentic-proofkit/internal/tools/workflowsmoke" ) +const processHelperMode = "PROOFKIT_WORKFLOW_SMOKE_HELPER_MODE" + func TestVerifyAcceptsApplicationCLI(t *testing.T) { - if err := workflowsmoke.Verify(applicationRunner); err != nil { + if err := workflowsmoke.Verify(t.Context(), applicationRunner); err != nil { t.Fatal(err) } } @@ -26,26 +33,29 @@ func TestVerifyRejectsCarrierContractMutations(t *testing.T) { {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: "text styling", match: "change-workflow-plan --input - --format text --color never", apply: replaceStdout("\x1b[31mChange workflow plan\x1b[0m\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 { result.Stdout = []byte("unexpected") return result }}, {name: "guidance slots", match: "native-evidence-guidance", apply: replaceStdout(`{"guidanceId":"proofkit.native-evidence-guidance.v1","slots":[]}\n`)}, {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")}, } for _, mutation := range mutations { t.Run(mutation.name, func(t *testing.T) { applied := false - runner := func(input []byte, args ...string) (workflowsmoke.Result, error) { - result, err := applicationRunner(input, args...) - if err == nil && !applied && strings.Join(args, " ") == mutation.match { + 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 { result = mutation.apply(result) applied = true } return result, err } - if err := workflowsmoke.Verify(runner); err == nil { + if err := workflowsmoke.Verify(t.Context(), runner); err == nil { t.Fatal("carrier mutation was accepted") } if !applied { @@ -55,13 +65,118 @@ func TestVerifyRejectsCarrierContractMutations(t *testing.T) { } } -func applicationRunner(input []byte, args ...string) (workflowsmoke.Result, error) { +func TestRunProcessRejectsTimeout(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 500*time.Millisecond) + defer cancel() + _, err := workflowsmoke.RunProcess(ctx, helperCarrier("hang"), workflowsmoke.Invocation{StdinClass: workflowsmoke.StdinBytes}) + if !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("timeout error=%v, want context deadline", err) + } +} + +func TestRunProcessRejectsOutputOverflow(t *testing.T) { + for _, test := range []struct { + mode string + want string + }{ + {mode: "stdout-overflow", want: "stdout exceeds"}, + {mode: "stderr-overflow", want: "stderr exceeds"}, + } { + t.Run(test.mode, func(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel() + _, err := workflowsmoke.RunProcess(ctx, helperCarrier(test.mode), workflowsmoke.Invocation{StdinClass: workflowsmoke.StdinBytes}) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("overflow error=%v, want %s", err, test.want) + } + }) + } +} + +func TestRunProcessAcceptsExactOutputBounds(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel() + result, err := workflowsmoke.RunProcess(ctx, helperCarrier("output-boundary"), workflowsmoke.Invocation{StdinClass: workflowsmoke.StdinBytes}) + if err != nil { + t.Fatal(err) + } + if len(result.Stdout) != 256*1024 || len(result.Stderr) != 64*1024 { + t.Fatalf("output lengths=(%d,%d), want (%d,%d)", len(result.Stdout), len(result.Stderr), 256*1024, 64*1024) + } +} + +func TestRunProcessProvesUnreadStdin(t *testing.T) { + t.Run("non-reader exits", func(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel() + result, err := workflowsmoke.RunProcess(ctx, helperCarrier("no-read"), workflowsmoke.Invocation{StdinClass: workflowsmoke.StdinMustRemainUnread}) + if err != nil { + t.Fatal(err) + } + if result.ExitCode != 0 { + t.Fatalf("exit code=%d want 0", result.ExitCode) + } + }) + t.Run("reader blocks", func(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 500*time.Millisecond) + defer cancel() + _, err := workflowsmoke.RunProcess(ctx, helperCarrier("read-stdin"), workflowsmoke.Invocation{StdinClass: workflowsmoke.StdinMustRemainUnread}) + if !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("stdin-read error=%v, want context deadline", err) + } + }) +} + +func TestWorkflowSmokeProcessHelper(t *testing.T) { + mode := os.Getenv(processHelperMode) + if mode == "" { + return + } + switch mode { + case "hang": + time.Sleep(time.Hour) + case "stdout-overflow": + _, _ = os.Stdout.Write(bytes.Repeat([]byte{'x'}, 512*1024)) + case "stderr-overflow": + _, _ = os.Stderr.Write(bytes.Repeat([]byte{'x'}, 128*1024)) + case "output-boundary": + _, _ = os.Stdout.Write(bytes.Repeat([]byte{'x'}, 256*1024)) + _, _ = os.Stderr.Write(bytes.Repeat([]byte{'x'}, 64*1024)) + case "read-stdin": + _, _ = io.Copy(io.Discard, os.Stdin) + case "no-read": + default: + _, _ = fmt.Fprintf(os.Stderr, "unsupported helper mode") + os.Exit(2) + } + os.Exit(0) +} + +func applicationRunner(ctx context.Context, invocation workflowsmoke.Invocation) (workflowsmoke.Result, error) { + var stdin io.Reader = bytes.NewReader(invocation.Input) + if invocation.StdinClass == workflowsmoke.StdinMustRemainUnread { + stdin = unreadableReader{} + } var stdout bytes.Buffer var stderr bytes.Buffer - exitCode := app.Run(context.Background(), args, bytes.NewReader(input), &stdout, &stderr) + exitCode := app.Run(ctx, invocation.Args, stdin, &stdout, &stderr) return workflowsmoke.Result{ExitCode: exitCode, Stdout: stdout.Bytes(), Stderr: stderr.Bytes()}, nil } +type unreadableReader struct{} + +func (unreadableReader) Read([]byte) (int, error) { + return 0, fmt.Errorf("stdin must remain unread") +} + +func helperCarrier(mode string) workflowsmoke.ProcessCarrier { + return workflowsmoke.ProcessCarrier{ + Executable: os.Args[0], + Prefix: []string{"-test.run=^TestWorkflowSmokeProcessHelper$", "--"}, + Environment: append(os.Environ(), processHelperMode+"="+mode), + } +} + func replaceStdout(value string) func(workflowsmoke.Result) workflowsmoke.Result { return func(result workflowsmoke.Result) workflowsmoke.Result { result.Stdout = []byte(value) @@ -75,3 +190,10 @@ func replaceStdoutFragment(oldValue string, newValue string) func(workflowsmoke. return result } } + +func appendStdout(value string) func(workflowsmoke.Result) workflowsmoke.Result { + return func(result workflowsmoke.Result) workflowsmoke.Result { + result.Stdout = append(result.Stdout, value...) + return result + } +} From 90591d1dbf79b9e42447a302b5dbadf4527a326d Mon Sep 17 00:00:00 2001 From: iperev Date: Mon, 31 Aug 2026 17:37:04 +0200 Subject: [PATCH 05/11] fix: close agent workflow review boundaries --- .../specs/proofkit-agent-workflow/overview.md | 13 ++- .../requirements.v1.json | 6 +- .../proofkit-supply-chain-quality/overview.md | 12 +- .../requirements.v1.json | 4 +- internal/app/agent_workflow_command_test.go | 93 ++++++++++++++- internal/app/cli_contract_test.go | 2 +- internal/app/command_contract_generated.go | 6 +- .../app/testdata/v0.5-wire-observations.json | 8 +- .../changeworkflowplan/agent_envelope.go | 34 ++++-- .../context_closure_test.go | 42 +++++++ .../command/changeworkflowplan/state_test.go | 45 ++++++++ .../nativeevidenceguidance/guidance.go | 6 +- .../nativeevidenceguidance/guidance_test.go | 6 +- .../stackpreset/preset_ids_generated.go | 2 +- internal/kernel/admit/fields.go | 44 +++---- internal/kernel/admit/fields_test.go | 8 +- internal/kernel/diagnostic/diagnostic.go | 25 ++++ internal/kernel/diagnostic/diagnostic_test.go | 41 +++++++ internal/tools/browserproofverify/main.go | 3 +- .../tools/browserproofverify/main_test.go | 8 +- internal/tools/browsertestserver/main.go | 4 +- internal/tools/commandcontractgen/main.go | 3 +- internal/tools/commandfamilygen/main.go | 3 +- internal/tools/coveragemetrics/main.go | 3 +- internal/tools/mermaidcheck/main.go | 3 +- internal/tools/npmregistry/main.go | 3 +- internal/tools/packageartifact/main.go | 3 +- internal/tools/packagebuild/main.go | 3 +- internal/tools/packagepack/main.go | 3 +- internal/tools/packageverify/main.go | 24 +++- internal/tools/packageverify/main_test.go | 35 +++++- internal/tools/pypiregistry/main.go | 3 +- internal/tools/pythonpackage/main.go | 4 +- internal/tools/releasecloseoutinput/main.go | 3 +- internal/tools/releasemanifest/main.go | 3 +- internal/tools/releasepreflight/main.go | 3 +- internal/tools/releasesbom/main.go | 3 +- internal/tools/textpolicyinput/main.go | 3 +- proofkit/cli-contract.v2.json | 6 +- proofkit/requirement-bindings.json | 45 ++++++++ proofkit/witness-plan.json | 1 + scripts/diagnostic_boundary_test.go | 108 ++++++++++++++++++ scripts/validate-self-hosting-receipts.go | 3 +- 43 files changed, 583 insertions(+), 97 deletions(-) create mode 100644 internal/kernel/diagnostic/diagnostic.go create mode 100644 internal/kernel/diagnostic/diagnostic_test.go create mode 100644 scripts/diagnostic_boundary_test.go diff --git a/docs/specs/proofkit-agent-workflow/overview.md b/docs/specs/proofkit-agent-workflow/overview.md index c09a045..bef7de6 100644 --- a/docs/specs/proofkit-agent-workflow/overview.md +++ b/docs/specs/proofkit-agent-workflow/overview.md @@ -33,20 +33,23 @@ introduced. - `REQ-PROOFKIT-WORKFLOW-002`: named optional built-in profile, exact seven-stage order, prefix/checkpoint biconditional, and a disjoint terminal output. -- `REQ-PROOFKIT-WORKFLOW-003`: 28 active state rows, one terminal row, and an - exact successor-state delta that preserves governing authority and context. +- `REQ-PROOFKIT-WORKFLOW-003`: 28 active state rows, one terminal row, an + executable envelope transition for every accepted stage, and an explicit + terminal stop packet. - `REQ-PROOFKIT-WORKFLOW-004`: typed subject/finding resolution and internally consistent caller-declared digests. - `REQ-PROOFKIT-WORKFLOW-005`: shared 256-byte stable-ID admission before work, global candidate-dependency resolution, one explicit nullable - governing-authority reference, bounded least dependency closure over - explicit seeds, exact omission accounting, and output-byte limits. + governing-authority reference, role-preserving bounded least dependency + closure over explicit seeds, exact omission accounting, and output-byte + limits. - `REQ-PROOFKIT-WORKFLOW-006`: only existing launcher and presentation capabilities outside explicit input, with no ambient repository authority, setup facade, hidden policy, or agent-route extension. - `REQ-PROOFKIT-WORKFLOW-007`: pure deterministic repository-neutral native-evidence guidance from one versioned typed table with five closed - applicability classes. + applicability classes, explicit absent-channel decisions, and finite + nondisclosure corpora. - `REQ-PROOFKIT-WORKFLOW-008`: display-safe caller admission, no-leak denial, bounded actionable text with required coordinates, and no styling of caller values. diff --git a/docs/specs/proofkit-agent-workflow/requirements.v1.json b/docs/specs/proofkit-agent-workflow/requirements.v1.json index 82cff2e..fb57830 100644 --- a/docs/specs/proofkit-agent-workflow/requirements.v1.json +++ b/docs/specs/proofkit-agent-workflow/requirements.v1.json @@ -34,7 +34,7 @@ { "requirementId": "REQ-PROOFKIT-WORKFLOW-003", "ownerId": "proofkit.agent-workflow", - "invariant": "The active checkpoint is exactly one closed discriminated variant not_started, ready_for_review, review_findings, or review_passed; one independently enumerable total non-overlapping relation maps the 28 active stage/checkpoint rows to exactly one stage-specific first action, review, repair, or accept_stage, the twenty-ninth complete-prefix row maps only to workflow_complete, every accept_stage packet declares exactly one successorStateDelta containing only completedStageIds and checkpoint, applying that delta by replacing only those two input fields while preserving schemaVersion, governingAuthorityRefId, contextRefs, and requiredContextRefIds produces exactly one legal successor snapshot, and every surplus, missing, or otherwise illegal field combination fails closed.", + "invariant": "The active checkpoint is exactly one closed discriminated variant not_started, ready_for_review, review_findings, or review_passed; one independently enumerable total non-overlapping relation maps the 28 active stage/checkpoint rows to exactly one stage-specific first action, review, repair, or accept_stage, the twenty-ninth complete-prefix row maps only to workflow_complete, every active agent-envelope action names next_action, every accept_stage plan and envelope action declares exactly one equal successorStateDelta containing only completedStageIds and checkpoint, applying that delta by replacing only those two input fields while preserving schemaVersion, governingAuthorityRefId, contextRefs, and requiredContextRefIds produces exactly one legal successor snapshot, the terminal envelope names workflow_complete and an explicit stop action without a successor delta, and every surplus, missing, or otherwise illegal field combination fails closed.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -60,7 +60,7 @@ { "requirementId": "REQ-PROOFKIT-WORKFLOW-005", "ownerId": "proofkit.agent-workflow", - "invariant": "The planner admits 0 to 48 strictly refId-sorted unique candidate context references; every candidate ID, nullable governingAuthorityRefId, required seed ID, dependency ID, subject ID, and finding ID is first admitted by the shared stable RuleID owner with a 256 UTF-8 byte maximum before sorting, indexing, closure, or projection; every dependency of every candidate resolves exactly once before seed selection or closure, including dependencies of candidates that the final closure omits; the governingAuthorityRefId when present resolves exactly one authority reference; it admits 0 to 12 required seed IDs, at most 12 finding references, at most 8 dependencies per candidate, at most 384 UTF-8 bytes per artifact path, and at most 18432 aggregate candidate-path bytes; it returns the unique least dependency-closed subset containing the governing authority, required, subject, and finding seeds, fails closed above 24 retained references or 9216 retained-path bytes, derives the exact sorted candidate-minus-retained omission set, summarizes that set once in an envelope, and fails JSON, envelope, and text outputs above 128 KiB, 128 KiB, and 8192 bytes before partial output.", + "invariant": "The planner admits 0 to 48 strictly refId-sorted unique candidate context references; every candidate ID, nullable governingAuthorityRefId, required seed ID, dependency ID, subject ID, and finding ID is first admitted by the shared stable RuleID owner with a 256 UTF-8 byte maximum before sorting, indexing, closure, or projection; every dependency of every candidate resolves exactly once before seed selection or closure, including dependencies of candidates that the final closure omits; the governingAuthorityRefId when present resolves exactly one authority reference; it admits 0 to 12 required seed IDs, at most 12 finding references, at most 8 dependencies per candidate, at most 384 UTF-8 bytes per artifact path, and at most 18432 aggregate candidate-path bytes; it returns the unique least dependency-closed subset containing the governing authority, required, subject, and finding seeds, fails closed above 24 retained references or 9216 retained-path bytes, derives the exact sorted candidate-minus-retained omission set, summarizes that set once in an envelope, classifies only the selected governing authority as semantic_owner while other authority, artifact, finding, and witness references remain owner_surface, evidence, review_finding, and proof_binding respectively, and fails JSON, envelope, and text outputs above 128 KiB, 128 KiB, and 8192 bytes before partial output.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -86,7 +86,7 @@ { "requirementId": "REQ-PROOFKIT-WORKFLOW-007", "ownerId": "proofkit.agent-workflow", - "invariant": "Native-evidence guidance has exactly one versioned repository-neutral semantic owner that emits exactly the declared semantic owner, subject/currentness, execution root/source snapshot, exact argv, stdin/input bytes, toolchain/platform/locale/timezone/dependencies, positive control, independent near-miss falsifier, executable oracle, input/work bounds, environment/network policy, CPU/memory/disk/file-descriptor/PID bounds, stdout/stderr bounds, timeout/process-group/detached-child policy, exit mapping, nondisclosure, cleanup/crash inventory, receipt identity, currentness/trust, escalation, retirement, and non-claim slots; every slot carries exactly one closed applicability class from always, declared_input_channels, environment_or_network_access, external_process, and mutable_artifacts so mechanism-specific decisions are conditional on the corresponding declared consumer mechanism; every admitted CLI delivery is deterministically bound to that owner, and JSON and text are derived without input, mutable shared state, filesystem, Git, process, environment, clock, random, network, container, provider, or other ambient dependency.", + "invariant": "Native-evidence guidance has exactly one versioned repository-neutral semantic owner that emits exactly the declared semantic owner, subject/currentness, execution root/source snapshot, exact argv, stdin/input bytes, toolchain/platform/locale/timezone/dependencies, positive control, independent near-miss falsifier, executable oracle, input/work bounds, environment/network policy, CPU/memory/disk/file-descriptor/PID bounds, output-channel bounds, timeout/process-group/detached-child policy, exit mapping, nondisclosure, cleanup/crash inventory, receipt identity, currentness/trust, escalation, retirement, and non-claim slots; every slot carries exactly one closed applicability class from always, declared_input_channels, environment_or_network_access, external_process, and mutable_artifacts so mechanism-specific decisions are conditional on the corresponding declared consumer mechanism; always-applicable output and receipt slots require each present channel to be bounded and identity-bound while each absent channel is explicitly not_applicable, nondisclosure selects one finite exact corpus and one finite explicitly derived fragment list rather than an unbounded fragment claim; every admitted CLI delivery is deterministically bound to that owner, and JSON and text are derived without input, mutable shared state, filesystem, Git, process, environment, clock, random, network, container, provider, or other ambient dependency.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], diff --git a/docs/specs/proofkit-supply-chain-quality/overview.md b/docs/specs/proofkit-supply-chain-quality/overview.md index 4f3eb94..9990fad 100644 --- a/docs/specs/proofkit-supply-chain-quality/overview.md +++ b/docs/specs/proofkit-supply-chain-quality/overview.md @@ -160,9 +160,10 @@ vulnerability absence, or consumer rollout safety by itself. failed attempts retain attempt-scoped action, DOM, network, and source traces without continuous screenshot capture plus a bounded best-effort failure screenshot, without creating passed proof. -- `REQ-PROOFKIT-QUALITY-023`: Python wheels independently bind advertised - platform compatibility to decoded executable bytes, and the wheel set rejects - version, identity, presence, and digest drift; wheels carry Core Metadata 2.4 +- `REQ-PROOFKIT-QUALITY-023`: npm platform binaries and Python wheels + independently bind to the same release binary for every target; wheels also + bind advertised platform compatibility to decoded executable bytes, reject + version, identity, presence, and digest drift, and carry Core Metadata 2.4 plus an exact, RECORD-closed repository license payload. - `REQ-PROOFKIT-QUALITY-024`: a closed version-bound machine record binds the exact previous/current SemVer pair and compatible/breaking class, declares the @@ -185,8 +186,9 @@ vulnerability absence, or consumer rollout safety by itself. preimplementation byte corpus for the pinned control, format, and separator table without changing decoded values. - `REQ-PROOFKIT-QUALITY-027`: report-visible diagnostics and structural text - apply the same scalar, control/format, and split-secret taxonomy and replace - rejected caller values as a whole without echoing any fragment. + apply the same scalar, Unicode-whitespace, control/format, and split-secret + taxonomy, replace rejected caller values as a whole without echoing any + fragment, and own dynamic stderr projection for repository tools. ## Non-Claims diff --git a/docs/specs/proofkit-supply-chain-quality/requirements.v1.json b/docs/specs/proofkit-supply-chain-quality/requirements.v1.json index a2e5527..32be0e0 100644 --- a/docs/specs/proofkit-supply-chain-quality/requirements.v1.json +++ b/docs/specs/proofkit-supply-chain-quality/requirements.v1.json @@ -298,7 +298,7 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-023", "ownerId": "proofkit.supply-chain-quality", - "invariant": "Each Python wheel binds its embedded executable to the release binary digest, and the wheel-set verifier independently rejects version drift, duplicate identities, missing files, and SHA drift; each wheel independently decodes Darwin Mach-O bytes so an advertised macOS platform minimum is never lower than the executable minimum, uses Core Metadata 2.4 License-Expression and License-File fields, carries an exact repository LICENSE copy under the wheel licenses directory, and closes every payload entry through RECORD.", + "invariant": "Every npm platform-binary tar entry and every Python wheel independently binds its embedded executable bytes to the same release-platform binary, so npm-to-release and wheel-to-release equality jointly imply cross-carrier byte identity for all shipped targets; the wheel-set verifier independently rejects version drift, duplicate identities, missing files, and SHA drift; each wheel independently decodes Darwin Mach-O bytes so an advertised macOS platform minimum is never lower than the executable minimum, uses Core Metadata 2.4 License-Expression and License-File fields, carries an exact repository LICENSE copy under the wheel licenses directory, and closes every payload entry through RECORD.", "claimLevel": "blocking", "riskClass": "critical", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -350,7 +350,7 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-027", "ownerId": "proofkit.supply-chain-quality", - "invariant": "Every report-visible diagnostic and structural-text sanitizer accepts only Unicode 15.0.0 scalar strings, recognizes the pinned Cc, Cf, Zl, and Zp range table and contiguous or control-split secret-shaped values through one shared taxonomy, and replaces any rejected caller-owned value as a whole with one fixed command-owned label without emitting its bytes, code points, prefix, suffix, or caller-controlled replacement text.", + "invariant": "Every report-visible diagnostic and structural-text sanitizer accepts only Unicode 15.0.0 scalar strings, recognizes one explicit Unicode whitespace alphabet plus the pinned Cc, Cf, Zl, and Zp range table and contiguous or control-split secret-shaped values through one shared taxonomy, and replaces any rejected caller-owned value as a whole with one fixed command-owned label without emitting its bytes, code points, prefix, suffix, or caller-controlled replacement text; every repository tool entrypoint routes dynamic stderr errors through that owner rather than writing raw error values.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], diff --git a/internal/app/agent_workflow_command_test.go b/internal/app/agent_workflow_command_test.go index 51462b6..06747f8 100644 --- a/internal/app/agent_workflow_command_test.go +++ b/internal/app/agent_workflow_command_test.go @@ -2,6 +2,7 @@ package app import ( "bytes" + "encoding/json" "io" "strings" "testing" @@ -20,21 +21,101 @@ func TestAgentWorkflowCLITruthTable(t *testing.T) { commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.110027854485263740098018592752615689790850067351556445289115876582278838003288") commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.008921652518915565373824823596859113418310007148763537766174180247340157842807") const ( - semanticOutputClasses = 24 - frozenRejectionClasses = 45 - extraRejectionCases = 2 - helpClasses = 10 - colorClasses = 8 + semanticOutputClasses = 24 + envelopeTransitionClasses = 8 + frozenRejectionClasses = 45 + extraRejectionCases = 2 + helpClasses = 10 + colorClasses = 8 ) - if got, want := semanticOutputClasses+frozenRejectionClasses+extraRejectionCases+helpClasses+colorClasses, 89; got != want { + if got, want := semanticOutputClasses+envelopeTransitionClasses+frozenRejectionClasses+extraRejectionCases+helpClasses+colorClasses, 97; got != want { t.Fatalf("agent workflow CLI truth-table cardinality = %d, want %d", got, want) } t.Run("semantic output classes", testAgentWorkflowSemanticOutputClasses) + t.Run("envelope transition classes", testAgentWorkflowEnvelopeTransitionClasses) t.Run("usage errors precede input", testAgentWorkflowUsageErrorsPrecedeInput) t.Run("exclusive help classes", testAgentWorkflowHelpClasses) t.Run("terminal capability product", testAgentWorkflowTerminalCapabilityProduct) } +func testAgentWorkflowEnvelopeTransitionClasses(t *testing.T) { + const digest = "sha256:2222222222222222222222222222222222222222222222222222222222222222" + stages := []string{"architecture", "design", "implementation_plan", "implementation", "verification", "pull_request", "closeout"} + for stageIndex := range stages { + t.Run(stages[stageIndex], func(t *testing.T) { + completed := make([]any, stageIndex) + for index := 0; index < stageIndex; index++ { + completed[index] = stages[index] + } + prior := map[string]any{ + "checkpoint": map[string]any{ + "assessmentSubjectDigest": digest, + "state": "review_passed", + "subjectDigest": digest, + "subjectRefId": "ctx.artifact", + }, + "completedStageIds": completed, + "contextRefs": []any{ + map[string]any{ + "artifactPath": "evidence/artifact.json", + "dependencyRefIds": []any{}, + "refId": "ctx.artifact", + "refKind": "artifact", + "subjectDigest": digest, + }, + }, + "governingAuthorityRefId": nil, + "requiredContextRefIds": []any{}, + "schemaVersion": json.Number("1"), + } + payload, err := stablejson.MarshalLayout(prior, stablejson.LayoutCompact) + if err != nil { + t.Fatal(err) + } + status, stdout, stderr := executeAgentWorkflowCLI(t, []string{"change-workflow-plan", "--input", "-", "--agent-envelope"}, bytes.NewReader(payload), PresentationCapabilities{}) + if status != 0 || stderr != "" { + t.Fatalf("status=%d stderr=%q", status, stderr) + } + envelope := decodeCLIJSON(t, stdout).(map[string]any) + action := envelope["actionPlan"].([]any)[0].(map[string]any) + delta, ok := action["successorStateDelta"].(map[string]any) + if action["outputKind"] != "next_action" || !ok { + t.Fatalf("transition envelope is incomplete: %v", action) + } + merged, err := changeworkflowplan.MergeSuccessor(prior, delta) + if err != nil { + t.Fatal(err) + } + if _, err := changeworkflowplan.Build(merged); err != nil { + t.Fatalf("CLI-projected successor is not admitted: %v", err) + } + }) + } + + t.Run("terminal", func(t *testing.T) { + terminal := map[string]any{ + "checkpoint": nil, + "completedStageIds": []any{"architecture", "design", "implementation_plan", "implementation", "verification", "pull_request", "closeout"}, + "contextRefs": []any{}, + "governingAuthorityRefId": nil, + "requiredContextRefIds": []any{}, + "schemaVersion": json.Number("1"), + } + payload, err := stablejson.MarshalLayout(terminal, stablejson.LayoutCompact) + if err != nil { + t.Fatal(err) + } + status, stdout, stderr := executeAgentWorkflowCLI(t, []string{"change-workflow-plan", "--input", "-", "--agent-envelope"}, bytes.NewReader(payload), PresentationCapabilities{}) + if status != 0 || stderr != "" { + t.Fatalf("status=%d stderr=%q", status, stderr) + } + action := decodeCLIJSON(t, stdout).(map[string]any)["actionPlan"].([]any)[0].(map[string]any) + if action["outputKind"] != "workflow_complete" || action["phase"] != "terminal" { + t.Fatalf("terminal envelope is incomplete: %v", action) + } + }) +} + func testAgentWorkflowSemanticOutputClasses(t *testing.T) { workflowValue, err := admission.DecodeJSON(strings.NewReader(validChangeWorkflowInput), 1<<20) if err != nil { diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index 70977a9..000ce36 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -23,7 +23,7 @@ import ( ) const ( - cliContractPublicABISHA256 = "d0f923fe01da20aabbb1ea7c7fa972ad350f3486560488284e128051b0125d8e" + cliContractPublicABISHA256 = "4ab2fdc8c70526ddbc96107b32562cab6d9abba72263a3de20a234895a409ebb" 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 454626a..2b6c60c 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 = "4065a82beed882107eff06781cec597db04ec2680b2981526ca14a39e5c431ad" +const commandContractSourceSHA256 = "da8d8889b86e75d0af9727fd388927e3d6528c540eac447d87d789b040466544" type generatedCommandContractMetadata struct { InputContractSHA256 string @@ -19,7 +19,7 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "binding-partition": {InputContractSHA256: "sha256:366ad082045af52b2ac6604f18626d0f285b2db73b45d9a82687b8d3b0d2b3fd", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.binding-partition.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:52840879e13a00ef9a4abaad6cdb33000511674d5f9003fb56f387fdf58fadc8", FlagChoices: map[string][]string{}}, "branch-authority": {InputContractSHA256: "sha256:8a3ed74978898593fbdbf1f7fa684dae450fbd9019edcd60d07f818d63363ed4", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.branch-authority.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3c7dc74842299b92cd5baf57cc8666e9415963091359e5faf654e28da89561f1", FlagChoices: map[string][]string{}}, "capability-map-admission": {InputContractSHA256: "sha256:efe87497cf6a32cfd1f9c9c191393cac8781d1e9211914a8a7b6aaa44e7f371d", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.capability-map-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:bfb32535fd51bed168111912acb1651527897fe59a592119dc0032ce9286c9db", FlagChoices: map[string][]string{}}, - "change-workflow-plan": {InputContractSHA256: "sha256:85fa3562d37a532ffbb8a2f7e2282af3d2b0d24c5b341fcb582f7adb2cb7ed50", 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:bcb22b1f930d1f63c24dd73681d9758eaaadfd41dde70360b432c21a55150803", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}}, + "change-workflow-plan": {InputContractSHA256: "sha256:82e6c110323dfbd7e743375a3fcce462e4ceab198bd17c9b8bc893446a5849f7", 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:97a0005561846d34efe6e07976b4df99411e1c4eefd62cbc5c14ef58b7493169", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}}, "changed-path-set": {InputContractSHA256: "sha256:8fe97426a58969e3e8dcbd52ed44540666b4de6be0487e8a3bc5088ae9c0f933", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.changed-path-set.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:abccdbf78e67f633ce49c34e8849c03f08ce42fa934a4c68969720c5045bf593", FlagChoices: map[string][]string{}}, "completion-criteria": {InputContractSHA256: "sha256:99c49c44b001e40383787e4c55f66621b8a8315f09635f1baf2326dc09bec4e6", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.completion-criteria.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:c90bb9605c7a22914104701a068dded510534bdeb60f4d601555d46c2d3d8a6d", FlagChoices: map[string][]string{}}, "conformance-profile": {InputContractSHA256: "sha256: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{}}, @@ -37,7 +37,7 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "json-report-cli-adapter-source": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:dbde6809a1702e1161be48a85da1d0971f55be96c0f8432778dd5d5ce757c3b0", FlagChoices: map[string][]string{}}, "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{}}, "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{}}, - "native-evidence-guidance": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:70d892de1ef24e113bed39bb648e2b51ef2dae812e985a9bee186cac20821ef4", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}}, + "native-evidence-guidance": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:c1d23df574e948ea7160931f53790a6d133ae12eeceefe9e5fa15430d653ff7e", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}}, "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{}}, "package-runtime-dependency-admission": {InputContractSHA256: "sha256:fc85887af9b8fcd899d245f0db30b2f2f68609822fc268126bf999082bb4115f", InputSchemaSummary: []string{"schemaVersion=1", "reportId", "expectedDependencySpec", "expectedLockfileIntegrity", "expectedPackageName", "expectedPackageVersion", "admissibleLocations{}", "packageResolution{}", "nonClaims[]", "root-shape-only definition proofkit.package-runtime-dependency-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:c012032e8c8212fd50bc2e85669cc610609ca2124ebc992c9e88f44a1ad2d5fc", FlagChoices: map[string][]string{}}, "pilot-admission": {InputContractSHA256: "sha256: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:91b93de58cb4ff03e3bd88eaddad4afaeef743d5ab37a44a6170382543f50638", FlagChoices: map[string][]string{}}, diff --git a/internal/app/testdata/v0.5-wire-observations.json b/internal/app/testdata/v0.5-wire-observations.json index dff6994..c166c74 100644 --- a/internal/app/testdata/v0.5-wire-observations.json +++ b/internal/app/testdata/v0.5-wire-observations.json @@ -5,17 +5,17 @@ "version": "0.5.0", "evidenceClass": "owner_authored_frozen_version_edge_observation", "previousPublicAbiSha256": "sha256:fc03740aea9e7f525a4388e5d7f557cde07e11b0db0c05101fe937c28a1129d9", - "currentPublicAbiSha256": "sha256:d0f923fe01da20aabbb1ea7c7fa972ad350f3486560488284e128051b0125d8e", + "currentPublicAbiSha256": "sha256:4ab2fdc8c70526ddbc96107b32562cab6d9abba72263a3de20a234895a409ebb", "addedCommandContracts": [ { "command": "change-workflow-plan", - "inputContractSha256": "sha256:85fa3562d37a532ffbb8a2f7e2282af3d2b0d24c5b341fcb582f7adb2cb7ed50", - "outputContractSha256": "sha256:bcb22b1f930d1f63c24dd73681d9758eaaadfd41dde70360b432c21a55150803" + "inputContractSha256": "sha256:82e6c110323dfbd7e743375a3fcce462e4ceab198bd17c9b8bc893446a5849f7", + "outputContractSha256": "sha256:97a0005561846d34efe6e07976b4df99411e1c4eefd62cbc5c14ef58b7493169" }, { "command": "native-evidence-guidance", "inputContractSha256": "", - "outputContractSha256": "sha256:70d892de1ef24e113bed39bb648e2b51ef2dae812e985a9bee186cac20821ef4" + "outputContractSha256": "sha256:c1d23df574e948ea7160931f53790a6d133ae12eeceefe9e5fa15430d653ff7e" } ], "breakingChangeIds": [ diff --git a/internal/command/changeworkflowplan/agent_envelope.go b/internal/command/changeworkflowplan/agent_envelope.go index 11ff036..13a2fbc 100644 --- a/internal/command/changeworkflowplan/agent_envelope.go +++ b/internal/command/changeworkflowplan/agent_envelope.go @@ -18,7 +18,7 @@ func agentEnvelope(value projection) (map[string]any, error) { "purpose": "Admitted change-workflow context selected by least dependency closure.", "ref": ref.ArtifactPath, "refId": ref.RefID, - "role": contextRole(ref.RefKind), + "role": contextRole(ref, value.Input.GoverningAuthorityRefID), "subjectDigest": ref.SubjectDigest, }) } @@ -36,16 +36,21 @@ func agentEnvelope(value projection) (map[string]any, error) { blocked := []map[string]any{} if value.Decision.OutputKind == "next_action" { prompt := value.Prompt - actionPlan = append(actionPlan, map[string]any{ + action := map[string]any{ "commandIds": []any{}, "evidenceRefs": stringsValue(contextRefIDs(value.Closure.Retained)), "instruction": prompt["candidateAction"], "nonClaims": []any{promptNonClaim}, "owner": prompt["ownerOrEscalationTarget"], + "outputKind": value.Decision.OutputKind, "phase": value.Decision.ActiveStageID, "rationale": "The admitted checkpoint relation selects exactly one next action for the active stage.", "stepId": "proofkit.change-workflow-plan.next-action", - }) + } + if value.Decision.SuccessorStateDelta != nil { + action["successorStateDelta"] = successorValue(*value.Decision.SuccessorStateDelta) + } + actionPlan = append(actionPlan, action) if value.Input.GoverningAuthorityRefID == nil { blocked = append(blocked, map[string]any{ "description": missingOwnerStop, @@ -55,6 +60,18 @@ func agentEnvelope(value projection) (map[string]any, error) { "preconditionId": "proofkit.change-workflow-plan.missing-governing-authority", }) } + } else { + actionPlan = append(actionPlan, map[string]any{ + "commandIds": []any{}, + "evidenceRefs": stringsValue(contextRefIDs(value.Closure.Retained)), + "instruction": "Stop because the admitted workflow snapshot is complete; do not infer merge, release, rollout, or readiness.", + "nonClaims": []any{promptNonClaim}, + "outputKind": value.Decision.OutputKind, + "owner": "consumer_repository", + "phase": "terminal", + "rationale": "The complete stage prefix and null checkpoint select the disjoint terminal workflow variant.", + "stepId": "proofkit.change-workflow-plan.workflow-complete", + }) } sourceHash, err := stableHash(value.Plan) if err != nil { @@ -111,16 +128,19 @@ func stableHash(value map[string]any) (string, error) { return "sha256:" + hex.EncodeToString(digest[:]), nil } -func contextRole(kind string) string { - if kind == "authority" { +func contextRole(ref contextRef, governingAuthorityRefID *string) string { + if governingAuthorityRefID != nil && ref.RefID == *governingAuthorityRefID { return "semantic_owner" } - if kind == "finding" { + if ref.RefKind == "finding" { return "review_finding" } - if kind == "witness" { + if ref.RefKind == "witness" { return "proof_binding" } + if ref.RefKind == "artifact" { + return "evidence" + } return "owner_surface" } diff --git a/internal/command/changeworkflowplan/context_closure_test.go b/internal/command/changeworkflowplan/context_closure_test.go index 715de22..7d930dd 100644 --- a/internal/command/changeworkflowplan/context_closure_test.go +++ b/internal/command/changeworkflowplan/context_closure_test.go @@ -202,6 +202,48 @@ func TestWorkflowClosurePredicates(t *testing.T) { t.Fatalf("unexpected omission summary: %v", omitted) } }) + t.Run("envelope_roles_preserve_reference_semantics", func(t *testing.T) { + input := initialInput() + input["contextRefs"] = []any{ + contextValue("ctx.artifact", "artifact", testDigest, nil), + contextValue("ctx.authority", "authority", "sha256:1111111111111111111111111111111111111111111111111111111111111111", nil), + contextValue("ctx.authority.other", "authority", "sha256:2222222222222222222222222222222222222222222222222222222222222222", nil), + contextValue("ctx.finding", "finding", "sha256:3333333333333333333333333333333333333333333333333333333333333333", nil), + contextValue("ctx.witness", "witness", "sha256:4444444444444444444444444444444444444444444444444444444444444444", nil), + } + input["governingAuthorityRefId"] = "ctx.authority" + input["requiredContextRefIds"] = []any{"ctx.artifact", "ctx.authority.other", "ctx.finding", "ctx.witness"} + envelope, err := BuildAgentEnvelope(input) + if err != nil { + t.Fatal(err) + } + roles := map[string]string{} + for _, raw := range envelope["contextRefs"].([]any) { + ref := raw.(map[string]any) + roles[ref["refId"].(string)] = ref["role"].(string) + } + want := map[string]string{ + "ctx.artifact": "evidence", + "ctx.authority": "semantic_owner", + "ctx.authority.other": "owner_surface", + "ctx.finding": "review_finding", + "ctx.witness": "proof_binding", + } + requireEqual(t, roles, want) + + input["governingAuthorityRefId"] = nil + input["requiredContextRefIds"] = []any{"ctx.authority"} + envelope, err = BuildAgentEnvelope(input) + if err != nil { + t.Fatal(err) + } + for _, raw := range envelope["contextRefs"].([]any) { + ref := raw.(map[string]any) + if ref["role"] == "semantic_owner" { + t.Fatalf("unselected authority was laundered into semantic owner: %v", ref) + } + } + }) t.Run("text_cap", func(t *testing.T) { lines, err := BuildTextProjection(initialInput()) if err != nil { diff --git a/internal/command/changeworkflowplan/state_test.go b/internal/command/changeworkflowplan/state_test.go index 296ac57..e8b2123 100644 --- a/internal/command/changeworkflowplan/state_test.go +++ b/internal/command/changeworkflowplan/state_test.go @@ -147,6 +147,51 @@ func TestWorkflowCheckpointPredicates(t *testing.T) { } } }) + t.Run("agent_envelope_transition_is_executable", func(t *testing.T) { + for index := range workflowCatalog.Stages { + prior := inputForStage(index, "review_passed") + envelope, err := BuildAgentEnvelope(prior) + if err != nil { + t.Fatalf("stage %d envelope rejected: %v", index, err) + } + actions := envelope["actionPlan"].([]any) + if len(actions) != 1 { + t.Fatalf("stage %d action count=%d, want 1", index, len(actions)) + } + action := actions[0].(map[string]any) + if action["outputKind"] != "next_action" { + t.Fatalf("stage %d outputKind=%v", index, action["outputKind"]) + } + delta, ok := action["successorStateDelta"].(map[string]any) + if !ok { + t.Fatalf("stage %d envelope omits successorStateDelta: %v", index, action) + } + merged, err := MergeSuccessor(prior, delta) + if err != nil { + t.Fatalf("stage %d envelope delta rejected: %v", index, err) + } + if _, err := Build(merged); err != nil { + t.Fatalf("stage %d envelope successor rejected: %v", index, err) + } + } + }) + t.Run("agent_envelope_terminal_is_explicit", func(t *testing.T) { + envelope, err := BuildAgentEnvelope(terminalInput()) + if err != nil { + t.Fatal(err) + } + actions := envelope["actionPlan"].([]any) + if len(actions) != 1 { + t.Fatalf("terminal action count=%d, want 1", len(actions)) + } + action := actions[0].(map[string]any) + if action["outputKind"] != "workflow_complete" || action["phase"] != "terminal" { + t.Fatalf("terminal action is not explicit: %v", action) + } + if _, present := action["successorStateDelta"]; present { + t.Fatalf("terminal action contains successorStateDelta: %v", action) + } + }) t.Run("review_passed_accepts", func(t *testing.T) { for index := range workflowCatalog.Stages { plan := requireBuild(t, inputForStage(index, "review_passed")) diff --git a/internal/command/nativeevidenceguidance/guidance.go b/internal/command/nativeevidenceguidance/guidance.go index a4ffc7a..aa923c0 100644 --- a/internal/command/nativeevidenceguidance/guidance.go +++ b/internal/command/nativeevidenceguidance/guidance.go @@ -87,12 +87,12 @@ var guidanceTable = [...]Slot{ {Order: 10, SlotID: "input_work_bounds", ApplicabilityClass: ApplicabilityAlways, Question: "Which cardinality and work bounds prevent excess work?", RequiredConsumerDecision: "Select numeric bounds and the point at which they dominate semantic processing.", CompletionCriterion: "Boundary fixtures pass, one-over fixtures fail before expensive per-item work, and no partial output is written."}, {Order: 11, SlotID: "environment_network_policy", ApplicabilityClass: ApplicabilityEnvironmentOrNetwork, Question: "Which environment variables and network accesses are allowed?", RequiredConsumerDecision: "Select explicit allow and deny inventories.", CompletionCriterion: "Allowed accesses are observed, one denied environment read and one denied network access fail under stable rules, and undeclared access is denied."}, {Order: 12, SlotID: "resource_bounds", ApplicabilityClass: ApplicabilityExternalProcess, Question: "Which CPU, memory, disk, file-descriptor, and PID limits apply?", RequiredConsumerDecision: "Select numeric limits and cleanup obligations.", CompletionCriterion: "Boundary use succeeds, one-over use fails deterministically, and allocated resources are released."}, - {Order: 13, SlotID: "output_bounds", ApplicabilityClass: ApplicabilityAlways, Question: "Which stdout and stderr limits apply?", RequiredConsumerDecision: "Select exact byte caps and overflow behavior.", CompletionCriterion: "Boundary output succeeds and one-over output fails before any partial write."}, + {Order: 13, SlotID: "output_bounds", ApplicabilityClass: ApplicabilityAlways, Question: "Which report-visible output channels and bounds apply?", RequiredConsumerDecision: "For each declared channel, select an exact byte cap and overflow behavior; record every absent channel as not_applicable.", CompletionCriterion: "Each declared channel passes at its boundary and fails one-over before partial output; absent channels emit no output."}, {Order: 14, SlotID: "process_lifecycle", ApplicabilityClass: ApplicabilityExternalProcess, Question: "How are timeout and child processes contained?", RequiredConsumerDecision: "Select timeout, process-group termination, and detached-child policy.", CompletionCriterion: "Timeout terminates the process group, no detached child survives, and cleanup completion is observed before success."}, {Order: 15, SlotID: "exit_mapping", ApplicabilityClass: ApplicabilityExternalProcess, Question: "How do native exit classes map to proof states?", RequiredConsumerDecision: "Select an exhaustive disjoint exit-class mapping.", CompletionCriterion: "Every native exit class maps to exactly one declared state and an unknown class fails closed."}, - {Order: 16, SlotID: "nondisclosure", ApplicabilityClass: ApplicabilityAlways, Question: "Which values must never reach visible sinks?", RequiredConsumerDecision: "Select the secret, control, and sentinel corpus and all report-visible sinks.", CompletionCriterion: "No corpus value or fragment appears in stdout, stderr, reports, receipts, logs, or generated records."}, + {Order: 16, SlotID: "nondisclosure", ApplicabilityClass: ApplicabilityAlways, Question: "Which finite values and derived fragments must never reach visible sinks?", RequiredConsumerDecision: "Select a finite exact secret, control, and sentinel corpus, a finite derived-fragment list with derivation and minimum length, and every report-visible sink.", CompletionCriterion: "No exact corpus member or declared derived fragment appears in any selected sink."}, {Order: 17, SlotID: "cleanup_crash_inventory", ApplicabilityClass: ApplicabilityMutableArtifacts, Question: "Which outputs may remain after success, failure, or crash?", RequiredConsumerDecision: "Select exact terminal inventories plus rollback or recovery rules.", CompletionCriterion: "Each terminal mode leaves exactly its inventory and interrupted mutation is rolled back or recovered by the declared rule."}, - {Order: 18, SlotID: "receipt_identity", ApplicabilityClass: ApplicabilityAlways, Question: "Which identities must the receipt bind?", RequiredConsumerDecision: "Select command, argv, input, output, subject, and witness identities.", CompletionCriterion: "The receipt binds every selected identity by digest and any one-field substitution is rejected."}, + {Order: 18, SlotID: "receipt_identity", ApplicabilityClass: ApplicabilityAlways, Question: "Which operation and channel identities must the receipt bind?", RequiredConsumerDecision: "Select operation, subject, and witness identities; bind each declared argv, input, and output channel by digest and mark each absent channel not_applicable.", CompletionCriterion: "The receipt binds every applicable identity, rejects any one-field substitution, and carries no identity for an absent channel."}, {Order: 19, SlotID: "currentness_trust", ApplicabilityClass: ApplicabilityAlways, Question: "Which currentness and trust class is admitted?", RequiredConsumerDecision: "Select a closed class and its evidence predicates.", CompletionCriterion: "The class is owner-admitted from evidence, stale or weaker evidence is rejected, and the witness does not escalate the class."}, {Order: 20, SlotID: "escalation", ApplicabilityClass: ApplicabilityAlways, Question: "Which unknowns block and who resolves them?", RequiredConsumerDecision: "Select an owner, blocking condition, and required evidence for every unresolved state.", CompletionCriterion: "Each unresolved state blocks the pass path and names exactly one owner, condition, and evidence requirement."}, {Order: 21, SlotID: "retirement", ApplicabilityClass: ApplicabilityAlways, Question: "When may this witness be replaced or removed?", RequiredConsumerDecision: "Select replacement identity, parity predicates, owner approval, and retirement gate.", CompletionCriterion: "Replacement parity passes on the exact subject, the owner approval is bound, and the retirement gate rejects premature removal."}, diff --git a/internal/command/nativeevidenceguidance/guidance_test.go b/internal/command/nativeevidenceguidance/guidance_test.go index fa3fa3d..61deaef 100644 --- a/internal/command/nativeevidenceguidance/guidance_test.go +++ b/internal/command/nativeevidenceguidance/guidance_test.go @@ -24,12 +24,12 @@ var slotFingerprints = [...]string{ "f70d28729dc5589400787d398f868225f010bb159c822e57494ddfcffb3227af", "04a4843479b418e0f09d45077136bbd07c7b58b8e3f7143d045796051fc0e46f", "496e80d2cdfc106b0c36d9032ea2d3671c83ab670c363959f7cb30e6ed4fe3cc", - "5c05b800f6579a9f120be7e3563a9d4dd60ed74ff46cb58651df91762b3cfb0a", + "cd3cfb453090488b804cd31c3b6b5fa8b431de422ca1de560e1b6023e160054f", "9f186415e33a8899b6ab423c61f9889372d4cb5b5d30dff669152a5100c3d565", "0a2e2c9f6c4294f51f82fc0f131512e5926784a04f1028db1f744361e7693cf8", - "c1efe0f8cc0aa87bc6c05bf4ce603ceae72acbba4b357b64c588e6d9b6bfc2a0", + "ce3813e0608d886b6c7f2e981b68e01a290afbcffbd4776a8c5b259de2b2e364", "e72db7215f074d0353c635af40bb14263a5f3a8de895af849a5bf679ff3adc04", - "be53fb4f4f32e22557922b20c62b42a5f5b8dc645aba27b7caf9af9e88eda905", + "4483b3807899e504500120a4890ff85b57b63f8959e86ace3b8c7fde3c4cfa1d", "b55ed27e5e8dc19dfb7ae997d35b0566e55140bf75570f6b2e2a46b3ae84852b", "fc0065447b033670bb33c6be89d0080aa0ce7f0e60c4e74f7b24f46ec3a49119", "a7aa8e953573653e9c6f4924ba5205a65c61de6d9ff247d7048f082d0dab4e9d", diff --git a/internal/command/stackpreset/preset_ids_generated.go b/internal/command/stackpreset/preset_ids_generated.go index 329c517..02d60a5 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 = "4065a82beed882107eff06781cec597db04ec2680b2981526ca14a39e5c431ad" +const presetContractSourceSHA256 = "da8d8889b86e75d0af9727fd388927e3d6528c540eac447d87d789b040466544" var presetIDs = []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"} diff --git a/internal/kernel/admit/fields.go b/internal/kernel/admit/fields.go index 62c8571..2c51e9b 100644 --- a/internal/kernel/admit/fields.go +++ b/internal/kernel/admit/fields.go @@ -12,10 +12,13 @@ import ( ) const ( - secretContextPatternSource = `authorization\s*:\s*[^\r\n]+|bearer\s+[A-Za-z0-9._~+/=-]{8,}|(?:access[-_]?token|api[-_]?key|pass(?:word|wd)|secret|token)\s*[=:]\s*\S+|-----BEGIN [A-Z ]*PRIVATE KEY-----` - secretSharedTokenPatternSource = `github_pat_[A-Za-z0-9_]+|gh[pousr]_[A-Za-z0-9_]+|xox[abprs]-[A-Za-z0-9-]+|glpat-[A-Za-z0-9_-]+|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+` - secretScalarTokenPatternSource = secretSharedTokenPatternSource + `|sk-(?:proj-)?[A-Za-z0-9_-]{10,}` - secretPathTokenPatternSource = secretSharedTokenPatternSource + `|sk-(?:proj-[A-Za-z0-9_-]{10,}|[A-Za-z0-9_-]{16,})` + secretWhitespaceClassSource = `\t\n\v\f\r \x{0085}\p{Zs}\p{Zl}\p{Zp}` + secretWhitespacePatternSource = `[` + secretWhitespaceClassSource + `]` + secretNonWhitespacePatternSource = `[^` + secretWhitespaceClassSource + `]` + secretContextPatternSource = `authorization` + secretWhitespacePatternSource + `*:` + secretWhitespacePatternSource + `*[^\r\n]+|bearer` + secretWhitespacePatternSource + `+[A-Za-z0-9._~+/=-]{8,}|(?:access[-_]?token|api[-_]?key|pass(?:word|wd)|secret|token)` + secretWhitespacePatternSource + `*[=:]` + secretWhitespacePatternSource + `*` + secretNonWhitespacePatternSource + `+|-----BEGIN [A-Z ]*PRIVATE KEY-----` + secretSharedTokenPatternSource = `github_pat_[A-Za-z0-9_]+|gh[pousr]_[A-Za-z0-9_]+|xox[abprs]-[A-Za-z0-9-]+|glpat-[A-Za-z0-9_-]+|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+` + secretScalarTokenPatternSource = secretSharedTokenPatternSource + `|sk-(?:proj-)?[A-Za-z0-9_-]{10,}` + secretPathTokenPatternSource = secretSharedTokenPatternSource + `|sk-(?:proj-[A-Za-z0-9_-]{10,}|[A-Za-z0-9_-]{16,})` ) var ( @@ -29,7 +32,7 @@ var ( secretValuePattern = regexp.MustCompile(`(?i)(?:` + secretContextPatternSource + `|` + secretScalarTokenPatternSource + `)`) secretPathContextPattern = regexp.MustCompile(`(?i)(?:` + secretContextPatternSource + `)`) secretPathTokenPattern = regexp.MustCompile(`(?i)(?:^|[^A-Za-z0-9_])(?:` + secretPathTokenPatternSource + `)(?:$|[^A-Za-z0-9_])`) - urlUserInfoPattern = regexp.MustCompile(`[A-Za-z][A-Za-z0-9+.-]*://[^/\s:@]+:[^/\s@]+@`) + urlUserInfoPattern = regexp.MustCompile(`[A-Za-z][A-Za-z0-9+.-]*://[^/` + secretWhitespaceClassSource + `:@]+:[^/` + secretWhitespaceClassSource + `@]+@`) controlRunePattern = regexp.MustCompile(`[\x00-\x1f\x7f]`) shellControlTokenPattern = regexp.MustCompile("(&&|\\|\\||[;&|<>`]|\\$\\(|\\r|\\n)") ) @@ -58,6 +61,8 @@ func ReportVisibleRedactionFixtures() []RedactionFixture { {Name: "authorization_header", Input: "request failed: Authorization: Basic YWxpY2U6c2VjcmV0", SensitiveNeedles: []string{"Authorization", "Basic", "YWxpY2U6c2VjcmV0"}}, {Name: "bearer_token", Input: "Bearer abcdefghijklmnopqrstuvwxyz", SensitiveNeedles: []string{"abcdefghijklmnopqrstuvwxyz"}}, {Name: "api_key_label", Input: "api_key=abc123456789", SensitiveNeedles: []string{"abc123456789"}}, + {Name: "api_key_unicode_whitespace", Input: "api_key\u00a0=abc123456789", SensitiveNeedles: []string{"abc123456789"}}, + {Name: "api_key_control_split", Input: "api_\u200bkey=abc123456789", SensitiveNeedles: []string{"abc123456789"}}, {Name: "access_token_label", Input: "access-token=abcdefghijklmnopqrstuvwxyz", SensitiveNeedles: []string{"abcdefghijklmnopqrstuvwxyz"}}, {Name: "password_label", Input: "passwd=abcdefghijklmnopqrstuvwxyz", SensitiveNeedles: []string{"abcdefghijklmnopqrstuvwxyz"}}, {Name: "github_pat", Input: githubPAT, SensitiveNeedles: []string{githubPAT}}, @@ -159,7 +164,11 @@ func SHA256HexRef(raw any, context string) (string, error) { } func ContainsSecretLikeValue(value string) bool { - return ContainsSecretTokenLikeValue(value) || ContainsURLCredentialValue(value) + if ContainsSecretTokenLikeValue(value) || ContainsURLCredentialValue(value) { + return true + } + withoutUnsafe := withoutUnsafeScalars(value) + return withoutUnsafe != value && (ContainsSecretTokenLikeValue(withoutUnsafe) || ContainsURLCredentialValue(withoutUnsafe)) } func ContainsReportVisibleUnsafeValue(value string) bool { @@ -167,7 +176,11 @@ func ContainsReportVisibleUnsafeValue(value string) bool { } func ContainsSecretLikePathValue(value string) bool { - return ContainsURLCredentialValue(value) || secretPathContextPattern.MatchString(value) || secretPathTokenPattern.MatchString(value) + if ContainsURLCredentialValue(value) || secretPathContextPattern.MatchString(value) || secretPathTokenPattern.MatchString(value) { + return true + } + withoutUnsafe := withoutUnsafeScalars(value) + return withoutUnsafe != value && (ContainsURLCredentialValue(withoutUnsafe) || secretPathContextPattern.MatchString(withoutUnsafe) || secretPathTokenPattern.MatchString(withoutUnsafe)) } func ContainsSecretTokenLikeValue(value string) bool { @@ -197,23 +210,10 @@ func RedactDiagnosticValue(value string) string { } func RedactStructuralText(value string) string { - if !unicodepolicy.ValidScalarString(value) || ContainsSecretLikeValue(value) || ContainsSecretLikeValue(withoutUnsafeScalars(value)) { + if !unicodepolicy.ValidScalarString(value) || unicodepolicy.ContainsUnsafeScalar(value) || ContainsSecretLikeValue(value) { return redactedValueLabel } - var builder strings.Builder - redacting := false - for _, character := range value { - if unicodepolicy.IsUnsafeScalar(character) { - if !redacting { - builder.WriteString("") - redacting = true - } - continue - } - redacting = false - builder.WriteRune(character) - } - return builder.String() + return value } func withoutUnsafeScalars(value string) string { diff --git a/internal/kernel/admit/fields_test.go b/internal/kernel/admit/fields_test.go index 5afd379..2da0c8b 100644 --- a/internal/kernel/admit/fields_test.go +++ b/internal/kernel/admit/fields_test.go @@ -168,8 +168,12 @@ func TestRedactDiagnosticValueReplacesRejectedValuesAsAWhole(t *testing.T) { input string want string }{ - {input: "line\nbreak", want: "linebreak"}, - {input: "unsafe\u200bvalue", want: "unsafevalue"}, + {input: "line\nbreak", want: redactedValueLabel}, + {input: "unsafe\u0085value", want: redactedValueLabel}, + {input: "unsafe\u200bvalue", want: redactedValueLabel}, + {input: "unsafe\u2028value", want: redactedValueLabel}, + {input: "unsafe\u2029value", want: redactedValueLabel}, + {input: "unsafe\u202evalue", want: redactedValueLabel}, {input: string([]byte{'p', 'a', 't', 'h', 0xff}), want: redactedValueLabel}, } { if got := RedactStructuralText(test.input); got != test.want { diff --git a/internal/kernel/diagnostic/diagnostic.go b/internal/kernel/diagnostic/diagnostic.go new file mode 100644 index 0000000..43660a2 --- /dev/null +++ b/internal/kernel/diagnostic/diagnostic.go @@ -0,0 +1,25 @@ +// Package diagnostic owns terminal-safe projection of errors for CLI tools. +package diagnostic + +import ( + "fmt" + "io" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" +) + +// Text returns the fixed-label structural projection of an error. +func Text(err error) string { + if err == nil { + return "" + } + return admit.RedactStructuralText(err.Error()) +} + +// WriteError writes one sanitized diagnostic line when err is non-nil. +func WriteError(writer io.Writer, err error) { + if err == nil { + return + } + _, _ = fmt.Fprintln(writer, Text(err)) +} diff --git a/internal/kernel/diagnostic/diagnostic_test.go b/internal/kernel/diagnostic/diagnostic_test.go new file mode 100644 index 0000000..0c9d458 --- /dev/null +++ b/internal/kernel/diagnostic/diagnostic_test.go @@ -0,0 +1,41 @@ +package diagnostic + +import ( + "bytes" + "errors" + "strings" + "testing" +) + +func TestWriteErrorPreservesSafeTextAndRedactsRejectedValuesAsAWhole(t *testing.T) { + tests := []struct { + name string + text string + want string + }{ + {name: "safe", text: "package verification failed", want: "package verification failed\n"}, + {name: "secret", text: "prefix api_key=abc123456789 suffix", want: "\n"}, + {name: "control", text: "prefix\u202eraw suffix", want: "\n"}, + {name: "split secret", text: "prefix api_\u200bkey=abc123456789 suffix", want: "\n"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var output bytes.Buffer + WriteError(&output, errors.New(test.text)) + if got := output.String(); got != test.want { + t.Fatalf("WriteError()=%q, want %q", got, test.want) + } + if test.want == "\n" && (strings.Contains(output.String(), "prefix") || strings.Contains(output.String(), "suffix") || strings.Contains(output.String(), "abc123456789")) { + t.Fatalf("WriteError() disclosed rejected fragments: %q", output.String()) + } + }) + } +} + +func TestWriteErrorIgnoresNil(t *testing.T) { + var output bytes.Buffer + WriteError(&output, nil) + if output.Len() != 0 || Text(nil) != "" { + t.Fatalf("nil error produced output %q", output.String()) + } +} diff --git a/internal/tools/browserproofverify/main.go b/internal/tools/browserproofverify/main.go index e81d3ad..e6b2fca 100644 --- a/internal/tools/browserproofverify/main.go +++ b/internal/tools/browserproofverify/main.go @@ -15,6 +15,7 @@ import ( "strings" "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" + "github.com/research-engineering/agentic-proofkit/internal/kernel/diagnostic" "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" "github.com/research-engineering/agentic-proofkit/internal/kernel/secretjson" "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" @@ -436,5 +437,5 @@ func exit(err error) { } func writeBrowserProofFailure(writer io.Writer, err error) { - _, _ = fmt.Fprintln(writer, "browser runtime proof verification failed:", admit.RedactStructuralText(err.Error())) + diagnostic.WriteError(writer, err) } diff --git a/internal/tools/browserproofverify/main_test.go b/internal/tools/browserproofverify/main_test.go index 5d635cc..9f084a1 100644 --- a/internal/tools/browserproofverify/main_test.go +++ b/internal/tools/browserproofverify/main_test.go @@ -22,10 +22,10 @@ func TestDiagnosticsDoNotLeakArtifactValues(t *testing.T) { value string want string }{ - {value: "artifact api_key=abc123456789 failed", want: "browser runtime proof verification failed: \n"}, - {value: "artifact line\nbreak failed", want: "browser runtime proof verification failed: artifact linebreak failed\n"}, - {value: "artifact unsafe\u200bvalue failed", want: "browser runtime proof verification failed: artifact unsafevalue failed\n"}, - {value: string([]byte{'p', 'a', 't', 'h', 0xff}), want: "browser runtime proof verification failed: \n"}, + {value: "artifact api_key=abc123456789 failed", want: "\n"}, + {value: "artifact line\nbreak failed", want: "\n"}, + {value: "artifact unsafe\u200bvalue failed", want: "\n"}, + {value: string([]byte{'p', 'a', 't', 'h', 0xff}), want: "\n"}, } { var output bytes.Buffer writeBrowserProofFailure(&output, errors.New(test.value)) diff --git a/internal/tools/browsertestserver/main.go b/internal/tools/browsertestserver/main.go index 024456b..5be2096 100644 --- a/internal/tools/browsertestserver/main.go +++ b/internal/tools/browsertestserver/main.go @@ -2,12 +2,12 @@ package main import ( "context" - "fmt" "os" "os/signal" "syscall" "github.com/research-engineering/agentic-proofkit/internal/command/requirementbrowser" + "github.com/research-engineering/agentic-proofkit/internal/kernel/diagnostic" "github.com/research-engineering/agentic-proofkit/internal/testsupport/browserfixture" ) @@ -24,6 +24,6 @@ func main() { } func fatal(err error) { - _, _ = fmt.Fprintln(os.Stderr, err) + diagnostic.WriteError(os.Stderr, err) os.Exit(1) } diff --git a/internal/tools/commandcontractgen/main.go b/internal/tools/commandcontractgen/main.go index 11dadbd..085acad 100644 --- a/internal/tools/commandcontractgen/main.go +++ b/internal/tools/commandcontractgen/main.go @@ -23,6 +23,7 @@ import ( "strings" "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/diagnostic" "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" ) @@ -64,7 +65,7 @@ func main() { err = run(root, *check) } if err != nil { - fmt.Fprintln(os.Stderr, err) + diagnostic.WriteError(os.Stderr, err) os.Exit(1) } } diff --git a/internal/tools/commandfamilygen/main.go b/internal/tools/commandfamilygen/main.go index a543207..261acfb 100644 --- a/internal/tools/commandfamilygen/main.go +++ b/internal/tools/commandfamilygen/main.go @@ -18,6 +18,7 @@ import ( "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/diagnostic" ) const ( @@ -57,7 +58,7 @@ func main() { err = run(root, *check) } if err != nil { - fmt.Fprintln(os.Stderr, err) + diagnostic.WriteError(os.Stderr, err) os.Exit(1) } } diff --git a/internal/tools/coveragemetrics/main.go b/internal/tools/coveragemetrics/main.go index 91741f0..a68f1ae 100644 --- a/internal/tools/coveragemetrics/main.go +++ b/internal/tools/coveragemetrics/main.go @@ -22,6 +22,7 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/command/requirementsourceadmission" "github.com/research-engineering/agentic-proofkit/internal/command/testevidenceinventory" "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/diagnostic" "github.com/research-engineering/agentic-proofkit/internal/kernel/gotestsource" "github.com/research-engineering/agentic-proofkit/internal/tools/artifactfile" "github.com/research-engineering/agentic-proofkit/internal/tools/commandoracle" @@ -181,7 +182,7 @@ type deadZoneMetrics struct { func main() { if err := run(); err != nil { - _, _ = fmt.Fprintln(os.Stderr, err.Error()) + diagnostic.WriteError(os.Stderr, err) os.Exit(1) } } diff --git a/internal/tools/mermaidcheck/main.go b/internal/tools/mermaidcheck/main.go index 6c1f0ce..42a73ba 100644 --- a/internal/tools/mermaidcheck/main.go +++ b/internal/tools/mermaidcheck/main.go @@ -12,6 +12,7 @@ import ( "sort" "strings" + "github.com/research-engineering/agentic-proofkit/internal/kernel/diagnostic" "github.com/research-engineering/agentic-proofkit/internal/kernel/unicodepolicy" ) @@ -39,7 +40,7 @@ func (e checkError) Error() string { func main() { if err := run(os.Args[1:], os.Stdout); err != nil { - fmt.Fprintln(os.Stderr, err) + diagnostic.WriteError(os.Stderr, err) os.Exit(1) } } diff --git a/internal/tools/npmregistry/main.go b/internal/tools/npmregistry/main.go index b947e54..f907e7d 100644 --- a/internal/tools/npmregistry/main.go +++ b/internal/tools/npmregistry/main.go @@ -10,6 +10,7 @@ import ( "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/diagnostic" "github.com/research-engineering/agentic-proofkit/internal/kernel/releasechannel" "github.com/research-engineering/agentic-proofkit/internal/kernel/trustedpublisher" "github.com/research-engineering/agentic-proofkit/internal/kernel/unicodepolicy" @@ -43,7 +44,7 @@ type registryArtifactSet struct { func main() { if err := run("."); err != nil { - _, _ = fmt.Fprintln(os.Stderr, err.Error()) + diagnostic.WriteError(os.Stderr, err) os.Exit(1) } } diff --git a/internal/tools/packageartifact/main.go b/internal/tools/packageartifact/main.go index 4d77612..c4f7537 100644 --- a/internal/tools/packageartifact/main.go +++ b/internal/tools/packageartifact/main.go @@ -7,12 +7,13 @@ import ( "os/exec" "time" + "github.com/research-engineering/agentic-proofkit/internal/kernel/diagnostic" "github.com/research-engineering/agentic-proofkit/internal/tools/packageartifactrecord" ) func main() { if err := run(); err != nil { - _, _ = fmt.Fprintln(os.Stderr, err.Error()) + diagnostic.WriteError(os.Stderr, err) os.Exit(1) } } diff --git a/internal/tools/packagebuild/main.go b/internal/tools/packagebuild/main.go index 761fdc3..a15ec31 100644 --- a/internal/tools/packagebuild/main.go +++ b/internal/tools/packagebuild/main.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/diagnostic" "github.com/research-engineering/agentic-proofkit/internal/kernel/releaseplatform" ) @@ -24,7 +25,7 @@ type repositoryJSON struct { func main() { if err := run(os.Args[1:]); err != nil { - _, _ = fmt.Fprintln(os.Stderr, err.Error()) + diagnostic.WriteError(os.Stderr, err) os.Exit(1) } } diff --git a/internal/tools/packagepack/main.go b/internal/tools/packagepack/main.go index ce209d0..7ddd376 100644 --- a/internal/tools/packagepack/main.go +++ b/internal/tools/packagepack/main.go @@ -10,6 +10,7 @@ import ( "sort" "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/diagnostic" ) type packRecord struct { @@ -23,7 +24,7 @@ type packRecord struct { func main() { if err := run(); err != nil { - _, _ = fmt.Fprintln(os.Stderr, err.Error()) + diagnostic.WriteError(os.Stderr, err) os.Exit(1) } } diff --git a/internal/tools/packageverify/main.go b/internal/tools/packageverify/main.go index f8d7f71..21e4964 100644 --- a/internal/tools/packageverify/main.go +++ b/internal/tools/packageverify/main.go @@ -24,7 +24,7 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/command/jsonreportcliadaptersource" "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/diagnostic" "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" "github.com/research-engineering/agentic-proofkit/internal/kernel/releaseplatform" "github.com/research-engineering/agentic-proofkit/internal/kernel/unicodepolicy" @@ -182,7 +182,7 @@ func main() { } func writeVerificationFailure(writer io.Writer, err error) { - _, _ = fmt.Fprintln(writer, admit.RedactStructuralText(err.Error())) + diagnostic.WriteError(writer, err) } func runVerifier() error { @@ -223,6 +223,9 @@ func runVerifier() error { if err := verifyPackedOwnerRecordsMatchSource(rootPackage); err != nil { return err } + if err := verifyPackedPlatformBinariesMatchSource(rootPackage); err != nil { + return err + } if err := verifyNoStalePackageDocs(rootPackage); err != nil { return err } @@ -256,6 +259,23 @@ func verifyPackedOwnerRecordsMatchSource(artifact rootPackageArtifact) error { return nil } +func verifyPackedPlatformBinariesMatchSource(artifact rootPackageArtifact) error { + for _, target := range releaseplatform.Targets() { + packed, err := readTarFileFromBytes(artifact.Content, target.PackageTarEntry) + if err != nil { + return err + } + source, err := os.ReadFile(filepath.FromSlash(target.BinaryPath)) + if err != nil { + return fmt.Errorf("read release binary for %s: %w", target.PlatformSuffix, err) + } + if !bytes.Equal(packed, source) { + return fmt.Errorf("packed platform binary does not match release binary for %s", target.PlatformSuffix) + } + } + return nil +} + func sourceOwnedPackageEntry(entry string) bool { switch entry { case "package/LICENSE", "package/package.json": diff --git a/internal/tools/packageverify/main_test.go b/internal/tools/packageverify/main_test.go index c9dd469..a494e50 100644 --- a/internal/tools/packageverify/main_test.go +++ b/internal/tools/packageverify/main_test.go @@ -307,8 +307,8 @@ func TestPackageVerifierDiagnosticNondisclosure(t *testing.T) { want string }{ {value: "package verification failed for api_key=abc123456789", want: "\n"}, - {value: "package verification failed for line\nbreak", want: "package verification failed for linebreak\n"}, - {value: "package verification failed for unsafe\u200bvalue", want: "package verification failed for unsafevalue\n"}, + {value: "package verification failed for line\nbreak", want: "\n"}, + {value: "package verification failed for unsafe\u200bvalue", want: "\n"}, {value: string([]byte{'p', 'a', 't', 'h', 0xff}), want: "\n"}, } { var output bytes.Buffer @@ -1432,6 +1432,37 @@ func TestVerifyRootPackageRejectsEachForbiddenRootEntry(t *testing.T) { } } +func TestPackedPlatformBinariesMatchReleaseBinaryBytes(t *testing.T) { + root := t.TempDir() + withWorkingDirectory(t, root) + entries := map[string]string{} + current, err := releaseplatform.CurrentTarget() + if err != nil { + t.Fatal(err) + } + nonCurrentEntry := "" + for _, target := range releaseplatform.Targets() { + content := []byte("release-binary-" + target.PlatformSuffix) + writeFileBytes(t, filepath.FromSlash(target.BinaryPath), content) + entries[target.PackageTarEntry] = string(content) + if target.PlatformSuffix != current.PlatformSuffix && nonCurrentEntry == "" { + nonCurrentEntry = target.PackageTarEntry + } + } + artifact := tarballArtifact(t, writePackageTarball(t, entries)) + if err := verifyPackedPlatformBinariesMatchSource(artifact); err != nil { + t.Fatalf("matching carrier bytes rejected: %v", err) + } + if nonCurrentEntry == "" { + t.Fatal("test requires at least one non-current release target") + } + entries[nonCurrentEntry] = "mutated-non-current-binary" + artifact = tarballArtifact(t, writePackageTarball(t, entries)) + if err := verifyPackedPlatformBinariesMatchSource(artifact); err == nil || !strings.Contains(err.Error(), "does not match release binary") { + t.Fatalf("mutated non-current carrier error=%v", err) + } +} + func TestVerifyTextPolicySmokeReportRequiresJSONABI(t *testing.T) { wantSummary := textPolicySmokeSummary{ CheckedTextFileCount: 1, diff --git a/internal/tools/pypiregistry/main.go b/internal/tools/pypiregistry/main.go index 8d1e23c..e32b796 100644 --- a/internal/tools/pypiregistry/main.go +++ b/internal/tools/pypiregistry/main.go @@ -14,6 +14,7 @@ import ( "time" "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/diagnostic" "github.com/research-engineering/agentic-proofkit/internal/kernel/releasechannel" "github.com/research-engineering/agentic-proofkit/internal/kernel/releaseplatform" ) @@ -124,7 +125,7 @@ type registryWheelEvidence struct { func main() { if err := run(); err != nil { - _, _ = fmt.Fprintln(os.Stderr, err.Error()) + diagnostic.WriteError(os.Stderr, err) os.Exit(1) } } diff --git a/internal/tools/pythonpackage/main.go b/internal/tools/pythonpackage/main.go index 70bc9e4..a0abc22 100644 --- a/internal/tools/pythonpackage/main.go +++ b/internal/tools/pythonpackage/main.go @@ -3,11 +3,13 @@ package main import ( "fmt" "os" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/diagnostic" ) func main() { if err := run(os.Args[1:]); err != nil { - _, _ = fmt.Fprintln(os.Stderr, err.Error()) + diagnostic.WriteError(os.Stderr, err) os.Exit(1) } } diff --git a/internal/tools/releasecloseoutinput/main.go b/internal/tools/releasecloseoutinput/main.go index fc43ef3..8bcf3bf 100644 --- a/internal/tools/releasecloseoutinput/main.go +++ b/internal/tools/releasecloseoutinput/main.go @@ -23,6 +23,7 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/command/specproofbundleadmission" "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/diagnostic" "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" "github.com/research-engineering/agentic-proofkit/internal/kernel/releasechannel" "github.com/research-engineering/agentic-proofkit/internal/kernel/releasepublisher" @@ -398,7 +399,7 @@ type specBundleVocabulary struct { func main() { if err := run(os.Stdout); err != nil { - _, _ = fmt.Fprintln(os.Stderr, err.Error()) + diagnostic.WriteError(os.Stderr, err) os.Exit(1) } } diff --git a/internal/tools/releasemanifest/main.go b/internal/tools/releasemanifest/main.go index 1101f66..6020cc8 100644 --- a/internal/tools/releasemanifest/main.go +++ b/internal/tools/releasemanifest/main.go @@ -14,6 +14,7 @@ import ( "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/diagnostic" "github.com/research-engineering/agentic-proofkit/internal/kernel/releasechannel" "github.com/research-engineering/agentic-proofkit/internal/kernel/releasepublisher" "github.com/research-engineering/agentic-proofkit/internal/kernel/trustedpublisher" @@ -196,7 +197,7 @@ type trustedPublisherSet struct { func main() { if err := run(); err != nil { - _, _ = fmt.Fprintln(os.Stderr, err.Error()) + diagnostic.WriteError(os.Stderr, err) os.Exit(1) } } diff --git a/internal/tools/releasepreflight/main.go b/internal/tools/releasepreflight/main.go index 0cd9a05..f949389 100644 --- a/internal/tools/releasepreflight/main.go +++ b/internal/tools/releasepreflight/main.go @@ -15,6 +15,7 @@ import ( "strings" "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/diagnostic" "github.com/research-engineering/agentic-proofkit/internal/kernel/unicodepolicy" "github.com/research-engineering/agentic-proofkit/internal/tools/releasechange" "github.com/research-engineering/agentic-proofkit/internal/tools/retainedevidence" @@ -111,7 +112,7 @@ type githubTagVerification struct { func main() { if err := run(os.Args[1:]); err != nil { - _, _ = fmt.Fprintln(os.Stderr, err.Error()) + diagnostic.WriteError(os.Stderr, err) os.Exit(1) } } diff --git a/internal/tools/releasesbom/main.go b/internal/tools/releasesbom/main.go index e1ed257..34d426b 100644 --- a/internal/tools/releasesbom/main.go +++ b/internal/tools/releasesbom/main.go @@ -16,6 +16,7 @@ import ( "strings" "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/diagnostic" "github.com/research-engineering/agentic-proofkit/internal/kernel/releaseplatform" ) @@ -105,7 +106,7 @@ type cyclonedxDependency struct { func main() { if err := run(); err != nil { - _, _ = fmt.Fprintln(os.Stderr, err.Error()) + diagnostic.WriteError(os.Stderr, err) os.Exit(1) } } diff --git a/internal/tools/textpolicyinput/main.go b/internal/tools/textpolicyinput/main.go index ef68710..0351a06 100644 --- a/internal/tools/textpolicyinput/main.go +++ b/internal/tools/textpolicyinput/main.go @@ -10,6 +10,7 @@ import ( "sort" "strings" + "github.com/research-engineering/agentic-proofkit/internal/kernel/diagnostic" "github.com/research-engineering/agentic-proofkit/internal/kernel/unicodepolicy" ) @@ -152,6 +153,6 @@ func isBinaryCandidate(path string) bool { } func exit(err error) { - _, _ = fmt.Fprintln(os.Stderr, err.Error()) + diagnostic.WriteError(os.Stderr, err) os.Exit(1) } diff --git a/proofkit/cli-contract.v2.json b/proofkit/cli-contract.v2.json index fde27ea..0a25fa5 100644 --- a/proofkit/cli-contract.v2.json +++ b/proofkit/cli-contract.v2.json @@ -859,7 +859,7 @@ "rootDefinitionDigest": "sha256:6632a481380bfb07d754ef622661b291f805b1b168b1824089bdd5cc9bdd4b0e", "nativeSource": { "path": "internal/command/changeworkflowplan", - "canonicalDigest": "sha256:2453c3d2a15b756153a3e553ed093327efade8a7ee8423f1ef8e405e9beb92a5", + "canonicalDigest": "sha256:4586cf0286a67200368ff54512185744aaf0ab56acd30e51eba58f65dc816834", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -894,7 +894,7 @@ "rootDefinitionDigest": "sha256:ca7b2acffdcbfd28222eb61f2bd8687f76f20cb789b712ee3ffc2756a3674ff8", "nativeSource": { "path": "internal/command/changeworkflowplan", - "canonicalDigest": "sha256:2453c3d2a15b756153a3e553ed093327efade8a7ee8423f1ef8e405e9beb92a5", + "canonicalDigest": "sha256:4586cf0286a67200368ff54512185744aaf0ab56acd30e51eba58f65dc816834", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { @@ -2151,7 +2151,7 @@ "rootDefinitionDigest": "sha256:218011a133540f57ef74f8748747e00d33b6757c9f77725ecef39f45fb19423f", "nativeSource": { "path": "internal/command/nativeevidenceguidance", - "canonicalDigest": "sha256:c36dc61cc915ef446305eee5d325791da82581d794700ac0bb6adcb69c54e80f", + "canonicalDigest": "sha256:14c609853ffb5a0decd40676f6b9621d983cd58ea4e7946e6c32ff31667221e1", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { diff --git a/proofkit/requirement-bindings.json b/proofkit/requirement-bindings.json index 6813f91..e99e807 100644 --- a/proofkit/requirement-bindings.json +++ b/proofkit/requirement-bindings.json @@ -3632,6 +3632,21 @@ "commandIds": ["proofkit.browser-check"], "environmentClasses": ["local-node-browser"] }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-023", + "scenarioId": "proofkit.supply-chain-quality.npm-python-carrier-binary-byte-identity", + "witnessId": "proofkit.package-verifier.platform-binary-byte-identity-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/tools/packageverify/main_test.go", + "witnessSelectors": [ + { + "selector": "TestPackedPlatformBinariesMatchReleaseBinaryBytes", + "command": "go test ./internal/tools/packageverify -run '^TestPackedPlatformBinariesMatchReleaseBinaryBytes$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, { "requirementId": "REQ-PROOFKIT-QUALITY-023", "scenarioId": "proofkit.supply-chain-quality.python-wheel-platform-byte-compatibility", @@ -3846,6 +3861,21 @@ "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-027", + "scenarioId": "proofkit.supply-chain-quality.dynamic-stderr-diagnostic-owner", + "witnessId": "proofkit.tool-diagnostic.dynamic-stderr-falsifier", + "witnessKind": "contract", + "witnessPath": "scripts/diagnostic_boundary_test.go", + "witnessSelectors": [ + { + "selector": "TestToolDiagnosticBoundaryRejectsDirectDynamicStderrWrites", + "command": "go test ./scripts -run '^TestToolDiagnosticBoundaryRejectsDirectDynamicStderrWrites$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, { "requirementId": "REQ-PROOFKIT-QUALITY-027", "scenarioId": "proofkit.supply-chain-quality.go-report-visible-whole-value-redaction", @@ -3895,6 +3925,21 @@ "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-027", + "scenarioId": "proofkit.supply-chain-quality.shared-diagnostic-writer-whole-value-redaction", + "witnessId": "proofkit.diagnostic.whole-value-redaction-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/kernel/diagnostic/diagnostic_test.go", + "witnessSelectors": [ + { + "selector": "TestWriteErrorPreservesSafeTextAndRedactsRejectedValuesAsAWhole", + "command": "go test ./internal/kernel/diagnostic -run '^TestWriteErrorPreservesSafeTextAndRedactsRejectedValuesAsAWhole$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, { "requirementId": "REQ-PROOFKIT-QUALITY-027", "scenarioId": "proofkit.supply-chain-quality.package-verifier-diagnostic-nondisclosure", diff --git a/proofkit/witness-plan.json b/proofkit/witness-plan.json index f3efe83..1f10407 100644 --- a/proofkit/witness-plan.json +++ b/proofkit/witness-plan.json @@ -774,6 +774,7 @@ "internal/kernel/browserdoc", "internal/kernel/cliexec", "internal/kernel/compactproofcontract", + "internal/kernel/diagnostic", "internal/kernel/digest", "internal/kernel/markdownfmt", "internal/kernel/proofvocab", diff --git a/scripts/diagnostic_boundary_test.go b/scripts/diagnostic_boundary_test.go new file mode 100644 index 0000000..1ccc32e --- /dev/null +++ b/scripts/diagnostic_boundary_test.go @@ -0,0 +1,108 @@ +package main + +import ( + "go/ast" + "go/parser" + "go/token" + "io/fs" + "path" + "path/filepath" + "strconv" + "strings" + "testing" +) + +func TestToolDiagnosticBoundaryRejectsDirectDynamicStderrWrites(t *testing.T) { + repositoryRoot := sourceHygieneRepoRoot(t) + for _, scope := range []string{"internal/tools", "scripts"} { + root := filepath.Join(repositoryRoot, scope) + if err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() || filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") { + return nil + } + parsed, err := parser.ParseFile(token.NewFileSet(), path, nil, 0) + if err != nil { + return err + } + if dynamicStderrWriteCount(parsed) != 0 { + t.Errorf("%s writes dynamic stderr without the diagnostic owner", path) + } + return nil + }); err != nil { + t.Fatal(err) + } + } + + for _, test := range []struct { + name string + source string + want int + }{ + {name: "fixed literal", source: `package main; import ("fmt"; "os"); func main(){ fmt.Fprintln(os.Stderr, "fixed usage") }`, want: 0}, + {name: "raw error", source: `package main; import ("fmt"; "os"); func main(){ var err error; fmt.Fprintln(os.Stderr, err) }`, want: 1}, + {name: "formatted dynamic value", source: `package main; import ("fmt"; "os"); func main(){ value := "caller"; fmt.Fprintf(os.Stderr, "failed: %s", value) }`, want: 1}, + {name: "aliased imports", source: `package main; import (f "fmt"; system "os"); func main(){ var err error; f.Fprintln(system.Stderr, err) }`, want: 1}, + } { + t.Run(test.name, func(t *testing.T) { + parsed, err := parser.ParseFile(token.NewFileSet(), "fixture.go", test.source, 0) + if err != nil { + t.Fatal(err) + } + if got := dynamicStderrWriteCount(parsed); got != test.want { + t.Fatalf("dynamic stderr write count=%d, want %d", got, test.want) + } + }) + } +} + +func dynamicStderrWriteCount(file *ast.File) int { + importNames := map[string]string{} + for _, spec := range file.Imports { + importPath, err := strconv.Unquote(spec.Path.Value) + if err != nil || (importPath != "fmt" && importPath != "os") { + continue + } + name := path.Base(importPath) + if spec.Name != nil { + name = spec.Name.Name + } + importNames[importPath] = name + } + count := 0 + ast.Inspect(file, func(node ast.Node) bool { + call, ok := node.(*ast.CallExpr) + if !ok || len(call.Args) < 2 || !isFmtPrintCall(call.Fun, importNames["fmt"]) || !isOSStderr(call.Args[0], importNames["os"]) { + return true + } + for _, argument := range call.Args[1:] { + literal, ok := argument.(*ast.BasicLit) + if !ok || literal.Kind != token.STRING { + count++ + break + } + } + return true + }) + return count +} + +func isFmtPrintCall(expression ast.Expr, importName string) bool { + selector, ok := expression.(*ast.SelectorExpr) + if !ok || (selector.Sel.Name != "Fprint" && selector.Sel.Name != "Fprintf" && selector.Sel.Name != "Fprintln") { + return false + } + identifier, ok := selector.X.(*ast.Ident) + return ok && importName != "" && identifier.Name == importName +} + +func isOSStderr(expression ast.Expr, importName string) bool { + selector, ok := expression.(*ast.SelectorExpr) + if !ok || selector.Sel.Name != "Stderr" { + return false + } + identifier, ok := selector.X.(*ast.Ident) + return ok && importName != "" && identifier.Name == importName +} diff --git a/scripts/validate-self-hosting-receipts.go b/scripts/validate-self-hosting-receipts.go index 5c4445e..cf673a0 100644 --- a/scripts/validate-self-hosting-receipts.go +++ b/scripts/validate-self-hosting-receipts.go @@ -15,6 +15,7 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/command/proofreceiptadmission" "github.com/research-engineering/agentic-proofkit/internal/command/receiptproduceradmission" "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/diagnostic" "github.com/research-engineering/agentic-proofkit/internal/kernel/releaseplatform" "github.com/research-engineering/agentic-proofkit/internal/kernel/report" "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" @@ -31,7 +32,7 @@ const ( func main() { if err := run(); err != nil { - _, _ = fmt.Fprintln(os.Stderr, err.Error()) + diagnostic.WriteError(os.Stderr, err) os.Exit(1) } } From 532a42b95f8cd2435f5740e1bee60b43c109d7f7 Mon Sep 17 00:00:00 2001 From: iperev Date: Mon, 31 Aug 2026 18:58:43 +0200 Subject: [PATCH 06/11] fix: close agent workflow audit boundaries --- cmd/agentic-proofkit/main.go | 4 +- .../specs/proofkit-agent-workflow/overview.md | 11 +- .../requirements.v1.json | 4 +- .../proofkit-supply-chain-quality/overview.md | 14 +- .../requirements.v1.json | 4 +- internal/app/agent_workflow_command_test.go | 9 +- internal/app/cli_contract_test.go | 2 +- internal/app/command_contract_generated.go | 4 +- .../app/testdata/v0.5-wire-observations.json | 7 +- .../changeworkflowplan/agent_envelope.go | 120 +++++++---- .../changeworkflowplan/context_closure.go | 10 + internal/command/changeworkflowplan/model.go | 1 + internal/command/changeworkflowplan/prompt.go | 14 +- .../command/changeworkflowplan/prompt_test.go | 11 + .../command/changeworkflowplan/state_test.go | 50 +++++ internal/command/changeworkflowplan/text.go | 7 +- .../command/changeworkflowplan/text_test.go | 11 + .../stackpreset/preset_ids_generated.go | 2 +- internal/kernel/admit/fields.go | 2 +- internal/kernel/diagnostic/capture.go | 61 ++++++ internal/kernel/diagnostic/diagnostic.go | 2 +- internal/kernel/diagnostic/diagnostic_test.go | 29 +++ internal/tools/browserproofverify/main.go | 6 +- internal/tools/packageartifact/main.go | 9 +- internal/tools/packagebuild/main.go | 6 +- internal/tools/packageverify/main.go | 47 ++-- internal/tools/releasechange/record_test.go | 5 +- .../tools/releasemanifest/carrier_identity.go | 202 ++++++++++++++++++ .../releasemanifest/carrier_identity_test.go | 130 +++++++++++ internal/tools/releasemanifest/main.go | 11 +- package.json | 2 +- proofkit/cli-contract.v2.json | 4 +- proofkit/requirement-bindings.json | 77 +++++++ proofkit/witness-plan.json | 1 + release/change-record.v2.json | 11 +- scripts/browser-runtime-proof-inputs.v1.json | 1 + scripts/diagnostic-entrypoints.test.mjs | 23 ++ scripts/diagnostic.mjs | 11 + scripts/diagnostic_boundary_test.go | 57 ++++- scripts/source-hygiene.mjs | 55 ++--- scripts/source_hygiene_test.go | 25 +++ scripts/write-browser-proof.mjs | 96 +++++---- 42 files changed, 986 insertions(+), 172 deletions(-) create mode 100644 internal/kernel/diagnostic/capture.go create mode 100644 internal/tools/releasemanifest/carrier_identity.go create mode 100644 internal/tools/releasemanifest/carrier_identity_test.go create mode 100644 scripts/diagnostic-entrypoints.test.mjs create mode 100644 scripts/diagnostic.mjs diff --git a/cmd/agentic-proofkit/main.go b/cmd/agentic-proofkit/main.go index 164c76d..39d9e55 100644 --- a/cmd/agentic-proofkit/main.go +++ b/cmd/agentic-proofkit/main.go @@ -2,12 +2,12 @@ package main import ( "context" - "fmt" "os" "github.com/mattn/go-isatty" "github.com/research-engineering/agentic-proofkit/internal/app" "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" + "github.com/research-engineering/agentic-proofkit/internal/kernel/diagnostic" ) func main() { @@ -16,7 +16,7 @@ func main() { os.Getenv(cliexec.PythonExecutableEnvironment), ) if err != nil { - _, _ = fmt.Fprintln(os.Stderr, err) + diagnostic.WriteError(os.Stderr, err) os.Exit(1) } _, noColorPresent := os.LookupEnv("NO_COLOR") diff --git a/docs/specs/proofkit-agent-workflow/overview.md b/docs/specs/proofkit-agent-workflow/overview.md index bef7de6..1b73caf 100644 --- a/docs/specs/proofkit-agent-workflow/overview.md +++ b/docs/specs/proofkit-agent-workflow/overview.md @@ -33,9 +33,10 @@ introduced. - `REQ-PROOFKIT-WORKFLOW-002`: named optional built-in profile, exact seven-stage order, prefix/checkpoint biconditional, and a disjoint terminal output. -- `REQ-PROOFKIT-WORKFLOW-003`: 28 active state rows, one terminal row, an - executable envelope transition for every accepted stage, and an explicit - terminal stop packet. +- `REQ-PROOFKIT-WORKFLOW-003`: 28 active state rows, one terminal row, + authority- and witness-gated executable envelope transitions, closed + blockers and clarifications when those preconditions are absent, and an + explicit terminal stop packet. - `REQ-PROOFKIT-WORKFLOW-004`: typed subject/finding resolution and internally consistent caller-declared digests. - `REQ-PROOFKIT-WORKFLOW-005`: shared 256-byte stable-ID admission before work, @@ -51,8 +52,8 @@ introduced. applicability classes, explicit absent-channel decisions, and finite nondisclosure corpora. - `REQ-PROOFKIT-WORKFLOW-008`: display-safe caller admission, no-leak denial, - bounded actionable text with required coordinates, and no styling of caller - values. + exact retained-or-missing witness projection, bounded actionable and + terminal text with required coordinates, and no styling of caller values. - `REQ-PROOFKIT-WORKFLOW-009`: explicit text/color selection and confinement of ANSI to eligible terminal stdout. - `REQ-PROOFKIT-WORKFLOW-010`: one production owner per semantic relation, diff --git a/docs/specs/proofkit-agent-workflow/requirements.v1.json b/docs/specs/proofkit-agent-workflow/requirements.v1.json index fb57830..3d7f20c 100644 --- a/docs/specs/proofkit-agent-workflow/requirements.v1.json +++ b/docs/specs/proofkit-agent-workflow/requirements.v1.json @@ -34,7 +34,7 @@ { "requirementId": "REQ-PROOFKIT-WORKFLOW-003", "ownerId": "proofkit.agent-workflow", - "invariant": "The active checkpoint is exactly one closed discriminated variant not_started, ready_for_review, review_findings, or review_passed; one independently enumerable total non-overlapping relation maps the 28 active stage/checkpoint rows to exactly one stage-specific first action, review, repair, or accept_stage, the twenty-ninth complete-prefix row maps only to workflow_complete, every active agent-envelope action names next_action, every accept_stage plan and envelope action declares exactly one equal successorStateDelta containing only completedStageIds and checkpoint, applying that delta by replacing only those two input fields while preserving schemaVersion, governingAuthorityRefId, contextRefs, and requiredContextRefIds produces exactly one legal successor snapshot, the terminal envelope names workflow_complete and an explicit stop action without a successor delta, and every surplus, missing, or otherwise illegal field combination fails closed.", + "invariant": "The active checkpoint is exactly one closed discriminated variant not_started, ready_for_review, review_findings, or review_passed; one independently enumerable total non-overlapping relation maps the 28 active stage/checkpoint rows to exactly one stage-specific first action, review, repair, or accept_stage, the twenty-ninth complete-prefix row maps only to workflow_complete, an active agent envelope emits its single next_action only when a governing authority ref is retained and the verify action additionally retains at least one caller-owned witness ref, otherwise it emits no executable action plus closed blockers and clarification questions, every accept_stage plan and emitted envelope action declares exactly one equal successorStateDelta containing only completedStageIds and checkpoint, applying that delta by replacing only those two input fields while preserving schemaVersion, governingAuthorityRefId, contextRefs, and requiredContextRefIds produces exactly one legal successor snapshot, the terminal envelope names workflow_complete and an explicit stop action without a successor delta, and every surplus, missing, or otherwise illegal field combination fails closed.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -99,7 +99,7 @@ { "requirementId": "REQ-PROOFKIT-WORKFLOW-008", "ownerId": "proofkit.agent-workflow", - "invariant": "Every workflow caller-visible value is admitted through its typed owner; display-safe values reject malformed UTF-8, secret-shaped content, ASCII controls, DEL, Unicode Cc, Cf, Zl, and Zp without echo; every active prompt packet projects observed fact, uncertainty, the resolved governingAuthorityRefId or a closed missing-owner escalation target, exact admitted coordinates, candidate action, a missing-consumer-witness state, non-claim, stop condition, and expected successor from command-owned tables, and when governingAuthorityRefId is null the packet forbids repository mutation until the caller supplies it; planner text contains the state, active stage and action when present, checkpoint, owner or escalation, stop condition, expected successor, omission count, and one non-claim within 24 lines and 8192 bytes; guidance text contains every slot ID, applicability class, required decision, and completion criterion within 48 lines and 16384 bytes; caller-owned values are never terminal-styled.", + "invariant": "Every workflow caller-visible value is admitted through its typed owner; display-safe values reject malformed UTF-8, secret-shaped content, ASCII controls, DEL, Unicode Cc, Cf, Zl, and Zp without echo; every active prompt packet projects observed fact, uncertainty, the resolved governingAuthorityRefId or a closed missing-owner escalation target, exact admitted coordinates, candidate action, one closed proof-availability state distinguishing a missing consumer witness from exact retained witness refs, non-claim, stop condition, and expected successor from command-owned tables, and when governingAuthorityRefId is null the packet forbids repository mutation until the caller supplies it; planner text contains the state, active stage and action when present, checkpoint, owner or escalation, stop condition, expected successor, omission count, and one non-claim within 24 lines and 8192 bytes, including explicit consumer owner, stop condition, and no successor for the terminal variant; guidance text contains every slot ID, applicability class, required decision, and completion criterion within 48 lines and 16384 bytes; caller-owned values are never terminal-styled.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], diff --git a/docs/specs/proofkit-supply-chain-quality/overview.md b/docs/specs/proofkit-supply-chain-quality/overview.md index 9990fad..413993c 100644 --- a/docs/specs/proofkit-supply-chain-quality/overview.md +++ b/docs/specs/proofkit-supply-chain-quality/overview.md @@ -161,10 +161,12 @@ vulnerability absence, or consumer rollout safety by itself. without continuous screenshot capture plus a bounded best-effort failure screenshot, without creating passed proof. - `REQ-PROOFKIT-QUALITY-023`: npm platform binaries and Python wheels - independently bind to the same release binary for every target; wheels also - bind advertised platform compatibility to decoded executable bytes, reject - version, identity, presence, and digest drift, and carry Core Metadata 2.4 - plus an exact, RECORD-closed repository license payload. + independently bind to the same release binary for every target; a separate + bounded decoder also compares the final tar and wheel members directly, + without a mutable intermediate; wheels bind advertised platform + compatibility to decoded executable bytes, reject version, identity, + presence, and digest drift, and carry Core Metadata 2.4 plus an exact, + RECORD-closed repository license payload. - `REQ-PROOFKIT-QUALITY-024`: a closed version-bound machine record binds the exact previous/current SemVer pair and compatible/breaking class, declares the exact complete current breaking, addition, and migration inventories @@ -188,7 +190,9 @@ vulnerability absence, or consumer rollout safety by itself. - `REQ-PROOFKIT-QUALITY-027`: report-visible diagnostics and structural text apply the same scalar, Unicode-whitespace, control/format, and split-secret taxonomy, replace rejected caller values as a whole without echoing any - fragment, and own dynamic stderr projection for repository tools. + fragment, bound diagnostic output, and own dynamic Go/JavaScript error + projection plus failure-only bounded child-process diagnostics for repository + tools. ## Non-Claims diff --git a/docs/specs/proofkit-supply-chain-quality/requirements.v1.json b/docs/specs/proofkit-supply-chain-quality/requirements.v1.json index 32be0e0..c2a3dcd 100644 --- a/docs/specs/proofkit-supply-chain-quality/requirements.v1.json +++ b/docs/specs/proofkit-supply-chain-quality/requirements.v1.json @@ -298,7 +298,7 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-023", "ownerId": "proofkit.supply-chain-quality", - "invariant": "Every npm platform-binary tar entry and every Python wheel independently binds its embedded executable bytes to the same release-platform binary, so npm-to-release and wheel-to-release equality jointly imply cross-carrier byte identity for all shipped targets; the wheel-set verifier independently rejects version drift, duplicate identities, missing files, and SHA drift; each wheel independently decodes Darwin Mach-O bytes so an advertised macOS platform minimum is never lower than the executable minimum, uses Core Metadata 2.4 License-Expression and License-File fields, carries an exact repository LICENSE copy under the wheel licenses directory, and closes every payload entry through RECORD.", + "invariant": "Every npm platform-binary tar entry and every Python wheel independently binds its embedded executable bytes to the same release-platform binary, and the release manifest independently decodes the final npm tarball and every final wheel under bounded archive limits and directly requires byte equality for the exact release-platform closure without relying on a mutable intermediate dist directory or claimed digests; the wheel-set verifier independently rejects version drift, duplicate identities, missing files, and SHA drift; each wheel independently decodes Darwin Mach-O bytes so an advertised macOS platform minimum is never lower than the executable minimum, uses Core Metadata 2.4 License-Expression and License-File fields, carries an exact repository LICENSE copy under the wheel licenses directory, and closes every payload entry through RECORD.", "claimLevel": "blocking", "riskClass": "critical", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -350,7 +350,7 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-027", "ownerId": "proofkit.supply-chain-quality", - "invariant": "Every report-visible diagnostic and structural-text sanitizer accepts only Unicode 15.0.0 scalar strings, recognizes one explicit Unicode whitespace alphabet plus the pinned Cc, Cf, Zl, and Zp range table and contiguous or control-split secret-shaped values through one shared taxonomy, and replaces any rejected caller-owned value as a whole with one fixed command-owned label without emitting its bytes, code points, prefix, suffix, or caller-controlled replacement text; every repository tool entrypoint routes dynamic stderr errors through that owner rather than writing raw error values.", + "invariant": "Every report-visible diagnostic and structural-text sanitizer accepts only Unicode 15.0.0 scalar strings, recognizes one explicit Unicode whitespace alphabet plus the pinned Cc, Cf, Zl, and Zp range table and contiguous or control-split secret-shaped values through one shared taxonomy, replaces any rejected caller-owned value as a whole with one fixed command-owned label without emitting its bytes, code points, prefix, suffix, or caller-controlled replacement text, and bounds admitted diagnostic projection to 512 Unicode scalars; every Go and JavaScript repository tool entrypoint routes dynamic errors through that owner, failed child-process stderr is captured under a fixed byte limit before whole-value sanitization while successful child stderr is not projected as a diagnostic, direct child stderr wiring is rejected, and subprocess adapters bound stdout, stderr, time, and process-group cleanup.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], diff --git a/internal/app/agent_workflow_command_test.go b/internal/app/agent_workflow_command_test.go index 06747f8..f0855e2 100644 --- a/internal/app/agent_workflow_command_test.go +++ b/internal/app/agent_workflow_command_test.go @@ -63,8 +63,15 @@ func testAgentWorkflowEnvelopeTransitionClasses(t *testing.T) { "refKind": "artifact", "subjectDigest": digest, }, + map[string]any{ + "artifactPath": "evidence/authority.json", + "dependencyRefIds": []any{}, + "refId": "ctx.authority", + "refKind": "authority", + "subjectDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + }, }, - "governingAuthorityRefId": nil, + "governingAuthorityRefId": "ctx.authority", "requiredContextRefIds": []any{}, "schemaVersion": json.Number("1"), } diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index 000ce36..010c367 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -23,7 +23,7 @@ import ( ) const ( - cliContractPublicABISHA256 = "4ab2fdc8c70526ddbc96107b32562cab6d9abba72263a3de20a234895a409ebb" + cliContractPublicABISHA256 = "0ca5482ce0300d2e3b4b44d818e066539ebf535771c197800f56403a9d2f1097" 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 2b6c60c..fb0aeca 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 = "da8d8889b86e75d0af9727fd388927e3d6528c540eac447d87d789b040466544" +const commandContractSourceSHA256 = "5d83a366969941d31e7e285abf7e049b9bfd022477fbe7593efdef7d38b5be5e" type generatedCommandContractMetadata struct { InputContractSHA256 string @@ -19,7 +19,7 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "binding-partition": {InputContractSHA256: "sha256:366ad082045af52b2ac6604f18626d0f285b2db73b45d9a82687b8d3b0d2b3fd", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.binding-partition.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:52840879e13a00ef9a4abaad6cdb33000511674d5f9003fb56f387fdf58fadc8", FlagChoices: map[string][]string{}}, "branch-authority": {InputContractSHA256: "sha256:8a3ed74978898593fbdbf1f7fa684dae450fbd9019edcd60d07f818d63363ed4", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.branch-authority.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3c7dc74842299b92cd5baf57cc8666e9415963091359e5faf654e28da89561f1", FlagChoices: map[string][]string{}}, "capability-map-admission": {InputContractSHA256: "sha256:efe87497cf6a32cfd1f9c9c191393cac8781d1e9211914a8a7b6aaa44e7f371d", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.capability-map-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:bfb32535fd51bed168111912acb1651527897fe59a592119dc0032ce9286c9db", FlagChoices: map[string][]string{}}, - "change-workflow-plan": {InputContractSHA256: "sha256:82e6c110323dfbd7e743375a3fcce462e4ceab198bd17c9b8bc893446a5849f7", 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:97a0005561846d34efe6e07976b4df99411e1c4eefd62cbc5c14ef58b7493169", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}}, + "change-workflow-plan": {InputContractSHA256: "sha256:0e6586bb23f5dc9479112d2123bfd1527ade0d20cf7f2bd14783a3ec26f8bb3e", 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:ee5eaadeb593ee592a2d043a8284aae7add80d0d5323b59902b6ebbd0af645f5", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}}, "changed-path-set": {InputContractSHA256: "sha256:8fe97426a58969e3e8dcbd52ed44540666b4de6be0487e8a3bc5088ae9c0f933", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.changed-path-set.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:abccdbf78e67f633ce49c34e8849c03f08ce42fa934a4c68969720c5045bf593", FlagChoices: map[string][]string{}}, "completion-criteria": {InputContractSHA256: "sha256:99c49c44b001e40383787e4c55f66621b8a8315f09635f1baf2326dc09bec4e6", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.completion-criteria.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:c90bb9605c7a22914104701a068dded510534bdeb60f4d601555d46c2d3d8a6d", FlagChoices: map[string][]string{}}, "conformance-profile": {InputContractSHA256: "sha256: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{}}, diff --git a/internal/app/testdata/v0.5-wire-observations.json b/internal/app/testdata/v0.5-wire-observations.json index c166c74..df69d3d 100644 --- a/internal/app/testdata/v0.5-wire-observations.json +++ b/internal/app/testdata/v0.5-wire-observations.json @@ -5,12 +5,12 @@ "version": "0.5.0", "evidenceClass": "owner_authored_frozen_version_edge_observation", "previousPublicAbiSha256": "sha256:fc03740aea9e7f525a4388e5d7f557cde07e11b0db0c05101fe937c28a1129d9", - "currentPublicAbiSha256": "sha256:4ab2fdc8c70526ddbc96107b32562cab6d9abba72263a3de20a234895a409ebb", + "currentPublicAbiSha256": "sha256:0ca5482ce0300d2e3b4b44d818e066539ebf535771c197800f56403a9d2f1097", "addedCommandContracts": [ { "command": "change-workflow-plan", - "inputContractSha256": "sha256:82e6c110323dfbd7e743375a3fcce462e4ceab198bd17c9b8bc893446a5849f7", - "outputContractSha256": "sha256:97a0005561846d34efe6e07976b4df99411e1c4eefd62cbc5c14ef58b7493169" + "inputContractSha256": "sha256:0e6586bb23f5dc9479112d2123bfd1527ade0d20cf7f2bd14783a3ec26f8bb3e", + "outputContractSha256": "sha256:ee5eaadeb593ee592a2d043a8284aae7add80d0d5323b59902b6ebbd0af645f5" }, { "command": "native-evidence-guidance", @@ -20,6 +20,7 @@ ], "breakingChangeIds": [ "proofkit.agent-envelope.local-identity-closure", + "proofkit.diagnostic.bounded-error-boundary", "proofkit.stable-json.unicode-scalar-v2" ], "nonClaims": [ diff --git a/internal/command/changeworkflowplan/agent_envelope.go b/internal/command/changeworkflowplan/agent_envelope.go index 13a2fbc..9b660a9 100644 --- a/internal/command/changeworkflowplan/agent_envelope.go +++ b/internal/command/changeworkflowplan/agent_envelope.go @@ -33,38 +33,31 @@ func agentEnvelope(value projection) (map[string]any, error) { }) } actionPlan := []map[string]any{} - blocked := []map[string]any{} + blocked, clarifications := envelopeBlockers(value) if value.Decision.OutputKind == "next_action" { - prompt := value.Prompt - action := map[string]any{ - "commandIds": []any{}, - "evidenceRefs": stringsValue(contextRefIDs(value.Closure.Retained)), - "instruction": prompt["candidateAction"], - "nonClaims": []any{promptNonClaim}, - "owner": prompt["ownerOrEscalationTarget"], - "outputKind": value.Decision.OutputKind, - "phase": value.Decision.ActiveStageID, - "rationale": "The admitted checkpoint relation selects exactly one next action for the active stage.", - "stepId": "proofkit.change-workflow-plan.next-action", - } - if value.Decision.SuccessorStateDelta != nil { - action["successorStateDelta"] = successorValue(*value.Decision.SuccessorStateDelta) - } - actionPlan = append(actionPlan, action) - if value.Input.GoverningAuthorityRefID == nil { - blocked = append(blocked, map[string]any{ - "description": missingOwnerStop, - "evidenceRefs": []any{}, - "nonClaim": "This blocker does not identify or authorize a consuming-repository owner.", - "owner": "consumer_repository", - "preconditionId": "proofkit.change-workflow-plan.missing-governing-authority", - }) + if len(blocked) == 0 { + prompt := value.Prompt + action := map[string]any{ + "commandIds": []any{}, + "evidenceRefs": stringsValue(contextRefIDs(value.Closure.Retained)), + "instruction": prompt["candidateAction"], + "nonClaims": []any{promptNonClaim}, + "owner": prompt["ownerOrEscalationTarget"], + "outputKind": value.Decision.OutputKind, + "phase": value.Decision.ActiveStageID, + "rationale": "The admitted checkpoint relation selects exactly one next action for the active stage.", + "stepId": "proofkit.change-workflow-plan.next-action", + } + if value.Decision.SuccessorStateDelta != nil { + action["successorStateDelta"] = successorValue(*value.Decision.SuccessorStateDelta) + } + actionPlan = append(actionPlan, action) } } else { actionPlan = append(actionPlan, map[string]any{ "commandIds": []any{}, "evidenceRefs": stringsValue(contextRefIDs(value.Closure.Retained)), - "instruction": "Stop because the admitted workflow snapshot is complete; do not infer merge, release, rollout, or readiness.", + "instruction": terminalStop, "nonClaims": []any{promptNonClaim}, "outputKind": value.Decision.OutputKind, "owner": "consumer_repository", @@ -81,18 +74,22 @@ func agentEnvelope(value projection) (map[string]any, error) { ActionPlan: actionPlan, BlockedPreconditions: blocked, Bounds: map[string]any{ - "escalation": "Caller must inspect omitted context or unresolved governing authority before mutation.", - "fanout": envelopeFanout(len(omitted), len(blocked)), - "maxActionItems": len(actionPlan), - "maxCommandRefs": 0, - "maxContextRefs": len(contextRefs), - "maxOmittedItems": len(omitted), - "maxReceiptRefs": 0, - "maxTokenBudget": nil, - "nonClaim": "Bounds count emitted records only and do not prove tokenizer-specific cost or semantic sufficiency.", - "omittedCount": len(value.Closure.Omitted), + "escalation": "Caller must inspect omitted context or resolve blocked authority and witness preconditions before mutation or verification.", + "fanout": envelopeFanout(len(omitted), len(blocked)), + "maxActionEvidenceRefs": len(value.Closure.Retained), + "maxActionItems": len(actionPlan), + "maxBlockedPreconditions": len(blocked), + "maxClarificationQuestions": len(clarifications), + "maxCommandRefs": 0, + "maxContextRefs": len(contextRefs), + "maxOmittedItems": len(omitted), + "maxReceiptRefs": 0, + "maxRouteQuestions": 3, + "maxTokenBudget": nil, + "nonClaim": "Bounds count emitted records only and do not prove tokenizer-specific cost or semantic sufficiency.", + "omittedCount": len(value.Closure.Omitted), }, - ClarificationQuestion: []map[string]any{}, + ClarificationQuestion: clarifications, Commands: []map[string]any{}, ContextRefs: contextRefs, EnvelopeID: "proofkit.change-workflow-plan.agent-envelope", @@ -119,6 +116,55 @@ func agentEnvelope(value projection) (map[string]any, error) { return envelope, nil } +func envelopeBlockers(value projection) ([]map[string]any, []map[string]any) { + if value.Decision.OutputKind != "next_action" { + return []map[string]any{}, []map[string]any{} + } + blocked := []map[string]any{} + clarifications := []map[string]any{} + if value.Input.GoverningAuthorityRefID == nil { + blocked = append(blocked, map[string]any{ + "description": missingOwnerStop, + "evidenceRefs": []any{}, + "nonClaim": "This blocker does not identify or authorize a consuming-repository owner.", + "owner": "consumer_repository", + "preconditionId": "proofkit.change-workflow-plan.missing-governing-authority", + }) + clarifications = append(clarifications, map[string]any{ + "askWhen": "No governing authority ref is admitted for the active workflow stage.", + "blocking": true, + "evidenceRefs": []any{}, + "expectedAnswerKind": "governing_authority_ref", + "nonClaim": "Naming an authority ref does not prove its content, scope, or authority.", + "owner": "consumer_repository", + "question": "Which admitted caller-owned context ref is the governing semantic authority for this stage?", + "questionId": "proofkit.change-workflow-plan.clarify-governing-authority", + }) + } + witnessRefIDs := contextRefIDsOfKind(value.Closure.Retained, "witness") + if value.Decision.Action == "verify" && len(witnessRefIDs) == 0 { + retainedRefIDs := contextRefIDs(value.Closure.Retained) + blocked = append(blocked, map[string]any{ + "description": "Verification requires a retained caller-owned witness ref; use native-evidence-guidance to define repository-specific positive controls and near-miss falsifiers.", + "evidenceRefs": stringsValue(retainedRefIDs), + "nonClaim": "A declared witness ref does not prove execution, correctness, freshness, or command success.", + "owner": "consumer_repository", + "preconditionId": "proofkit.change-workflow-plan.missing-consumer-witness", + }) + clarifications = append(clarifications, map[string]any{ + "askWhen": "The active verification stage has no retained caller-owned witness ref.", + "blocking": true, + "evidenceRefs": stringsValue(retainedRefIDs), + "expectedAnswerKind": "consumer_witness_ref", + "nonClaim": "Supplying a witness ref does not execute or validate the referenced native evidence.", + "owner": "consumer_repository", + "question": "Which admitted witness ref binds the consuming repository's positive controls and independent near-miss falsifiers?", + "questionId": "proofkit.change-workflow-plan.clarify-consumer-witness", + }) + } + return blocked, clarifications +} + func stableHash(value map[string]any) (string, error) { encoded, err := stablejson.Marshal(value) if err != nil { diff --git a/internal/command/changeworkflowplan/context_closure.go b/internal/command/changeworkflowplan/context_closure.go index b01f322..97eff8a 100644 --- a/internal/command/changeworkflowplan/context_closure.go +++ b/internal/command/changeworkflowplan/context_closure.go @@ -59,6 +59,16 @@ func contextRefIDs(refs []contextRef) []string { return result } +func contextRefIDsOfKind(refs []contextRef, kind string) []string { + result := []string{} + for _, ref := range refs { + if ref.RefKind == kind { + result = append(result, ref.RefID) + } + } + return result +} + func contextRefPaths(refs []contextRef) []string { result := make([]string, len(refs)) for index, ref := range refs { diff --git a/internal/command/changeworkflowplan/model.go b/internal/command/changeworkflowplan/model.go index 4e60bfa..12e5ca5 100644 --- a/internal/command/changeworkflowplan/model.go +++ b/internal/command/changeworkflowplan/model.go @@ -15,6 +15,7 @@ const ( maxTextLines = 24 missingAuthorityTarget = "missing_consuming_repository_semantic_owner" missingConsumerWitness = "missing_consumer_witness" + retainedConsumerWitness = "retained_consumer_witness_refs" ) var boundaryNonClaims = []string{ diff --git a/internal/command/changeworkflowplan/prompt.go b/internal/command/changeworkflowplan/prompt.go index df00f58..366f305 100644 --- a/internal/command/changeworkflowplan/prompt.go +++ b/internal/command/changeworkflowplan/prompt.go @@ -4,6 +4,7 @@ const ( promptUncertainty = "Caller-declared context, digests, checkpoint state, and external state are unauthenticated." promptNonClaim = "This packet does not prove execution, correctness, review quality, merge approval, release approval, rollout, or production readiness." missingOwnerStop = "Do not mutate the repository until the caller supplies the consuming-repository semantic owner and governing authority coordinates." + terminalStop = "Stop because the admitted workflow snapshot is complete; do not infer merge, release, rollout, or readiness." ) func buildPrompt(input admittedInput, decision stateDecision, closure closureResult) (map[string]any, error) { @@ -50,12 +51,23 @@ func buildPrompt(input admittedInput, decision stateDecision, closure closureRes "nonClaim": promptNonClaim, "observedFact": observedFact, "ownerOrEscalationTarget": ownerTarget, - "proofCommandOrMissingWitness": map[string]any{"state": missingConsumerWitness}, + "proofCommandOrMissingWitness": proofAvailability(closure.Retained), "stopCondition": stopCondition, "uncertainty": promptUncertainty, }, nil } +func proofAvailability(refs []contextRef) map[string]any { + witnessRefIDs := contextRefIDsOfKind(refs, "witness") + if len(witnessRefIDs) == 0 { + return map[string]any{"state": missingConsumerWitness} + } + return map[string]any{ + "state": retainedConsumerWitness, + "witnessRefIds": stringsValue(witnessRefIDs), + } +} + func nullableStringValue(value *string) any { if value == nil { return nil diff --git a/internal/command/changeworkflowplan/prompt_test.go b/internal/command/changeworkflowplan/prompt_test.go index 2191b9b..be261df 100644 --- a/internal/command/changeworkflowplan/prompt_test.go +++ b/internal/command/changeworkflowplan/prompt_test.go @@ -119,6 +119,17 @@ func TestWorkflowPromptPredicates(t *testing.T) { t.Fatalf("unexpected proof state: %v", proof) } }) + t.Run("retained_witness", func(t *testing.T) { + input := initialInput() + input["contextRefs"] = []any{contextValue("ctx.witness", "witness", testDigest, nil)} + input["requiredContextRefIds"] = []any{"ctx.witness"} + plan := requireBuild(t, input) + proof := plan["prompt"].(map[string]any)["proofCommandOrMissingWitness"].(map[string]any) + if proof["state"] != retainedConsumerWitness || len(proof) != 2 { + t.Fatalf("unexpected retained proof state: %v", proof) + } + requireEqual(t, proof["witnessRefIds"], []any{"ctx.witness"}) + }) t.Run("nonclaim", func(t *testing.T) { plan := requireBuild(t, initialInput()) value := plan["prompt"].(map[string]any)["nonClaim"].(string) diff --git a/internal/command/changeworkflowplan/state_test.go b/internal/command/changeworkflowplan/state_test.go index e8b2123..8f147cb 100644 --- a/internal/command/changeworkflowplan/state_test.go +++ b/internal/command/changeworkflowplan/state_test.go @@ -192,6 +192,40 @@ func TestWorkflowCheckpointPredicates(t *testing.T) { t.Fatalf("terminal action contains successorStateDelta: %v", action) } }) + t.Run("agent_envelope_blocks_missing_authority", func(t *testing.T) { + envelope, err := BuildAgentEnvelope(initialInput()) + if err != nil { + t.Fatal(err) + } + if len(envelope["actionPlan"].([]any)) != 0 || len(envelope["blockedPreconditions"].([]any)) != 1 || len(envelope["clarificationQuestions"].([]any)) != 1 { + t.Fatalf("missing authority did not suppress execution: %v", envelope) + } + }) + t.Run("agent_envelope_requires_retained_verification_witness", func(t *testing.T) { + withoutWitness := verificationInput(false) + envelope, err := BuildAgentEnvelope(withoutWitness) + if err != nil { + t.Fatal(err) + } + if len(envelope["actionPlan"].([]any)) != 0 || len(envelope["blockedPreconditions"].([]any)) != 1 || len(envelope["clarificationQuestions"].([]any)) != 1 { + t.Fatalf("verification without a witness was executable: %v", envelope) + } + + withWitness := verificationInput(true) + envelope, err = BuildAgentEnvelope(withWitness) + if err != nil { + t.Fatal(err) + } + actions := envelope["actionPlan"].([]any) + if len(actions) != 1 || len(envelope["blockedPreconditions"].([]any)) != 0 || len(envelope["clarificationQuestions"].([]any)) != 0 { + t.Fatalf("verification with a witness remained blocked: %v", envelope) + } + action := actions[0].(map[string]any) + if action["owner"] != "ctx.authority" { + t.Fatalf("verification action lost owner or witness identity: %v", action) + } + requireEqual(t, action["evidenceRefs"], []any{"ctx.authority", "ctx.witness"}) + }) t.Run("review_passed_accepts", func(t *testing.T) { for index := range workflowCatalog.Stages { plan := requireBuild(t, inputForStage(index, "review_passed")) @@ -249,3 +283,19 @@ func TestWorkflowCheckpointPredicates(t *testing.T) { } }) } + +func verificationInput(withWitness bool) map[string]any { + input := inputForStage(4, "not_started") + contextRefs := []any{ + contextValue("ctx.authority", "authority", "sha256:1111111111111111111111111111111111111111111111111111111111111111", nil), + } + required := []any{} + if withWitness { + contextRefs = append(contextRefs, contextValue("ctx.witness", "witness", testDigest, nil)) + required = append(required, "ctx.witness") + } + input["contextRefs"] = contextRefs + input["governingAuthorityRefId"] = "ctx.authority" + input["requiredContextRefIds"] = required + return input +} diff --git a/internal/command/changeworkflowplan/text.go b/internal/command/changeworkflowplan/text.go index 6dd46fb..581553c 100644 --- a/internal/command/changeworkflowplan/text.go +++ b/internal/command/changeworkflowplan/text.go @@ -22,7 +22,12 @@ func textProjection(value projection) []TextLine { TextLine{Label: "Expected next checkpoint", Value: prompt["expectedNextCheckpoint"].(string)}, ) } else { - lines = append(lines, TextLine{Label: "Checkpoint", Value: "none"}) + lines = append(lines, + TextLine{Label: "Checkpoint", Value: "none"}, + TextLine{Label: "Owner or escalation", Value: "consumer_repository"}, + TextLine{Label: "Stop condition", Value: terminalStop}, + TextLine{Label: "Expected next checkpoint", Value: "none"}, + ) } lines = append(lines, TextLine{Label: "Omitted context", Value: integerText(len(value.Closure.Omitted))}, diff --git a/internal/command/changeworkflowplan/text_test.go b/internal/command/changeworkflowplan/text_test.go index 2f19297..fabaae2 100644 --- a/internal/command/changeworkflowplan/text_test.go +++ b/internal/command/changeworkflowplan/text_test.go @@ -41,6 +41,17 @@ func TestWorkflowTextProjectionParity(t *testing.T) { } } +func TestWorkflowTerminalTextIsOperationallyComplete(t *testing.T) { + result, err := Project(terminalInput()) + if err != nil { + t.Fatal(err) + } + want := "Change workflow plan\nOutput: workflow_complete\nCompleted stages: architecture, design, implementation_plan, implementation, verification, pull_request, closeout\nCheckpoint: none\nOwner or escalation: consumer_repository\nStop condition: " + terminalStop + "\nExpected next checkpoint: none\nOmitted context: 0\nNon-claim: Change workflow plans do not approve repository edits, merge, release, rollout, or production readiness.\n" + if result.Text != want { + t.Fatalf("terminal text is not operationally complete: %q", result.Text) + } +} + func TestWorkflowTextProjectionDefensiveCopy(t *testing.T) { input := initialInput() first, err := BuildTextProjection(input) diff --git a/internal/command/stackpreset/preset_ids_generated.go b/internal/command/stackpreset/preset_ids_generated.go index 02d60a5..27bb020 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 = "da8d8889b86e75d0af9727fd388927e3d6528c540eac447d87d789b040466544" +const presetContractSourceSHA256 = "5d83a366969941d31e7e285abf7e049b9bfd022477fbe7593efdef7d38b5be5e" var presetIDs = []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"} diff --git a/internal/kernel/admit/fields.go b/internal/kernel/admit/fields.go index 2c51e9b..d8866e0 100644 --- a/internal/kernel/admit/fields.go +++ b/internal/kernel/admit/fields.go @@ -199,7 +199,7 @@ func RedactSecretLikeValue(value string) string { } func RedactDiagnosticValue(value string) string { - if ContainsReportVisibleUnsafeValue(value) { + if !unicodepolicy.ValidScalarString(value) || ContainsReportVisibleUnsafeValue(value) { return redactedValueLabel } runes := []rune(value) diff --git a/internal/kernel/diagnostic/capture.go b/internal/kernel/diagnostic/capture.go new file mode 100644 index 0000000..e20c1e7 --- /dev/null +++ b/internal/kernel/diagnostic/capture.go @@ -0,0 +1,61 @@ +package diagnostic + +import ( + "fmt" + "strings" + "sync" +) + +const maxCapturedStderrBytes = 64 << 10 + +// CaptureStderr retains bounded child-process stderr for whole-value +// sanitization after the child reaches a terminal state. +type CaptureStderr struct { + content []byte + mu sync.Mutex + overflow bool +} + +// NewStderrCapture returns an empty bounded stderr capture. +func NewStderrCapture() *CaptureStderr { + return &CaptureStderr{content: make([]byte, 0, 4096)} +} + +// Write implements io.Writer while retaining at most the admitted byte bound. +func (capture *CaptureStderr) Write(value []byte) (int, error) { + capture.mu.Lock() + defer capture.mu.Unlock() + remaining := maxCapturedStderrBytes - len(capture.content) + if remaining > 0 { + retained := len(value) + if retained > remaining { + retained = remaining + } + capture.content = append(capture.content, value[:retained]...) + } + if len(value) > remaining { + capture.overflow = true + } + return len(value), nil +} + +// Failure returns one bounded, already-sanitized summary for a failed child +// process. Empty child stderr returns nil; successful child stderr is not a +// report-visible diagnostic. +func (capture *CaptureStderr) Failure(label string) error { + capture.mu.Lock() + content := append([]byte(nil), capture.content...) + overflow := capture.overflow + capture.mu.Unlock() + if len(content) == 0 && !overflow { + return nil + } + if overflow { + return fmt.Errorf("%s exceeded the stderr capture limit", label) + } + value := strings.TrimSpace(string(content)) + if value == "" { + return fmt.Errorf("%s emitted empty-or-whitespace stderr", label) + } + return fmt.Errorf("%s: %s", label, Text(fmt.Errorf("%s", value))) +} diff --git a/internal/kernel/diagnostic/diagnostic.go b/internal/kernel/diagnostic/diagnostic.go index 43660a2..b598e28 100644 --- a/internal/kernel/diagnostic/diagnostic.go +++ b/internal/kernel/diagnostic/diagnostic.go @@ -13,7 +13,7 @@ func Text(err error) string { if err == nil { return "" } - return admit.RedactStructuralText(err.Error()) + return admit.RedactDiagnosticValue(err.Error()) } // WriteError writes one sanitized diagnostic line when err is non-nil. diff --git a/internal/kernel/diagnostic/diagnostic_test.go b/internal/kernel/diagnostic/diagnostic_test.go index 0c9d458..49921a6 100644 --- a/internal/kernel/diagnostic/diagnostic_test.go +++ b/internal/kernel/diagnostic/diagnostic_test.go @@ -39,3 +39,32 @@ func TestWriteErrorIgnoresNil(t *testing.T) { t.Fatalf("nil error produced output %q", output.String()) } } + +func TestDiagnosticTextEnforcesWholeValueSafetyAndRuneBound(t *testing.T) { + if got := Text(errors.New(strings.Repeat("a", 512))); got != strings.Repeat("a", 512) { + t.Fatalf("512-rune diagnostic changed: %q", got) + } + if got := Text(errors.New(strings.Repeat("a", 513))); got != strings.Repeat("a", 512)+"..." { + t.Fatalf("513-rune diagnostic was not bounded: %q", got) + } + if got := Text(errors.New(string([]byte{'g', 'h', 'p', '_', 0xff}))); got != "" { + t.Fatalf("malformed UTF-8 diagnostic was not redacted: %q", got) + } +} + +func TestStderrCaptureSanitizesWholeValuesAndBoundsMemory(t *testing.T) { + secret := strings.Join([]string{"api", "_key=", "abc123456789"}, "") + capture := NewStderrCapture() + _, _ = capture.Write([]byte(secret)) + err := capture.Failure("child") + if err == nil || strings.Contains(err.Error(), "abc123456789") || !strings.Contains(err.Error(), "") { + t.Fatalf("secret child stderr was not redacted: %v", err) + } + + overflow := NewStderrCapture() + _, _ = overflow.Write([]byte(strings.Repeat("x", maxCapturedStderrBytes+1))) + err = overflow.Failure("child") + if err == nil || err.Error() != "child exceeded the stderr capture limit" { + t.Fatalf("overflow diagnostic = %v", err) + } +} diff --git a/internal/tools/browserproofverify/main.go b/internal/tools/browserproofverify/main.go index e6b2fca..2be1dca 100644 --- a/internal/tools/browserproofverify/main.go +++ b/internal/tools/browserproofverify/main.go @@ -126,8 +126,12 @@ func runBrowserProof(root string) (resultErr error) { environment = withEnvironmentValue(environment, browserProofCandidateEnvironment, runPaths.CandidatePath) command.Env = environment command.Stdout = os.Stdout - command.Stderr = os.Stderr + stderr := diagnostic.NewStderrCapture() + command.Stderr = stderr if err := command.Run(); err != nil { + if childErr := stderr.Failure("browser proof writer stderr"); childErr != nil { + return fmt.Errorf("run browser proof writer: %w; %s", err, childErr) + } return fmt.Errorf("run browser proof writer: %w", err) } value, err := readRootedJSON(root, runPaths.CandidatePath, 8<<20) diff --git a/internal/tools/packageartifact/main.go b/internal/tools/packageartifact/main.go index c4f7537..e3ffecf 100644 --- a/internal/tools/packageartifact/main.go +++ b/internal/tools/packageartifact/main.go @@ -38,9 +38,14 @@ func (commandRunner) Run(root string, argv []string) (int, error) { command.Env = os.Environ() command.Stdin = os.Stdin command.Stdout = os.Stdout - command.Stderr = os.Stderr + stderr := diagnostic.NewStderrCapture() + command.Stderr = stderr if err := command.Run(); err != nil { - if exitError, ok := err.(*exec.ExitError); ok { + if childErr := stderr.Failure("package artifact child stderr"); childErr != nil { + err = fmt.Errorf("%w; %s", err, childErr) + } + var exitError *exec.ExitError + if errors.As(err, &exitError) { return exitError.ExitCode(), err } return 1, err diff --git a/internal/tools/packagebuild/main.go b/internal/tools/packagebuild/main.go index a15ec31..5d34fb2 100644 --- a/internal/tools/packagebuild/main.go +++ b/internal/tools/packagebuild/main.go @@ -83,8 +83,12 @@ func run(args []string) error { "GOARCH="+target.GOARCH, ) command.Stdout = os.Stdout - command.Stderr = os.Stderr + stderr := diagnostic.NewStderrCapture() + command.Stderr = stderr if err := command.Run(); err != nil { + if childErr := stderr.Failure("go build stderr"); childErr != nil { + return fmt.Errorf("build %s/%s: %w; %s", target.GOOS, target.GOARCH, err, childErr) + } return fmt.Errorf("build %s/%s: %w", target.GOOS, target.GOARCH, err) } } diff --git a/internal/tools/packageverify/main.go b/internal/tools/packageverify/main.go index 21e4964..0876afa 100644 --- a/internal/tools/packageverify/main.go +++ b/internal/tools/packageverify/main.go @@ -15,12 +15,12 @@ import ( "io" "maps" "os" - "os/exec" pathpkg "path" "path/filepath" "regexp" "sort" "strings" + "time" "github.com/research-engineering/agentic-proofkit/internal/command/jsonreportcliadaptersource" "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" @@ -36,6 +36,7 @@ const rootBinaryName = "agentic-proofkit" const installedNPMExecCommandPrefix = "npm exec --offline -- agentic-proofkit " const maxTarEntryBytes = 128 << 20 const maxEmbeddedBinaryBytes = 64 << 20 +const packageVerifyProcessTimeout = 30 * time.Second var ( packageCoordinatePattern = regexp.MustCompile(`(?:@research-engineering/agentic-proofkit|agentic-proofkit)@v?[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?`) @@ -1654,9 +1655,20 @@ func withExactTarballConsumer(artifact rootPackageArtifact, verify func(string) } func run(dir string, name string, args ...string) ([]byte, error) { - command := exec.Command(name, args...) - command.Dir = dir - return command.CombinedOutput() + ctx, cancel := context.WithTimeout(context.Background(), packageVerifyProcessTimeout) + defer cancel() + result, err := workflowsmoke.RunProcess(ctx, workflowsmoke.ProcessCarrier{ + Directory: dir, + Executable: name, + }, workflowsmoke.Invocation{Args: args, StdinClass: workflowsmoke.StdinBytes}) + if err != nil { + return nil, err + } + output := append(append([]byte(nil), result.Stdout...), result.Stderr...) + if result.ExitCode != 0 { + return output, fmt.Errorf("process exited with code %d", result.ExitCode) + } + return output, nil } type installedCommandOperation func(string, []byte, ...string) (installedCommandResult, error) @@ -2517,23 +2529,16 @@ func verifyJSONAdapterSourceSmokeReport(result installedCommandResult, expectedS } func runWithInput(dir string, name string, input []byte, args ...string) (installedCommandResult, error) { - command := exec.Command(name, args...) - command.Dir = dir - command.Stdin = bytes.NewReader(input) - var stdout bytes.Buffer - var stderr bytes.Buffer - command.Stdout = &stdout - command.Stderr = &stderr - err := command.Run() - exitCode := 0 - if err != nil { - var exitErr *exec.ExitError - if !errors.As(err, &exitErr) { - return installedCommandResult{}, err - } - exitCode = exitErr.ExitCode() - } - return installedCommandResult{ExitCode: exitCode, Stdout: stdout.Bytes(), Stderr: stderr.Bytes()}, nil + ctx, cancel := context.WithTimeout(context.Background(), packageVerifyProcessTimeout) + defer cancel() + result, err := workflowsmoke.RunProcess(ctx, workflowsmoke.ProcessCarrier{ + Directory: dir, + Executable: name, + }, workflowsmoke.Invocation{Args: args, Input: input, StdinClass: workflowsmoke.StdinBytes}) + if err != nil { + return installedCommandResult{}, err + } + return installedCommandResult{ExitCode: result.ExitCode, Stdout: result.Stdout, Stderr: result.Stderr}, nil } func runInstalledWithInput(dir string, input []byte, args ...string) (installedCommandResult, error) { diff --git a/internal/tools/releasechange/record_test.go b/internal/tools/releasechange/record_test.go index 71e9438..971f938 100644 --- a/internal/tools/releasechange/record_test.go +++ b/internal/tools/releasechange/record_test.go @@ -196,16 +196,19 @@ func TestCurrentChangeRecordNamesReviewedSemanticChanges(t *testing.T) { var currentBreakingChanges = []Change{ {ChangeID: "proofkit.agent-envelope.local-identity-closure", Summary: "Agent envelopes now require unique, pairwise-disjoint command, context, and receipt identities and fail closed by pruning unsafe or ambiguous local targets and references with bounded omissions."}, + {ChangeID: "proofkit.diagnostic.bounded-error-boundary", Summary: "CLI and repository-tool diagnostics now reject malformed or report-unsafe values as a whole, cap admitted text, and bound child-process stdout, stderr, lifetime, and process-group cleanup instead of forwarding raw child errors."}, {ChangeID: "proofkit.stable-json.unicode-scalar-v2", Summary: "Go, JavaScript, and generated TypeScript stable JSON now reject non-scalar strings and deterministically escape the pinned Unicode 15 control and format policy; the generated TypeScript adapter identity advances to v2."}, } var currentAdditions = []Change{ - {ChangeID: "proofkit.agent-workflow.change-planner", Summary: "Add a bounded change-workflow-plan command for the named optional proofkit.reviewed-change.v1 profile with canonical JSON, derived agent-envelope, plain-text, and capability-gated terminal-color projections."}, + {ChangeID: "proofkit.agent-workflow.change-planner", Summary: "Add a bounded change-workflow-plan command for the named optional proofkit.reviewed-change.v1 profile with canonical JSON, derived authority- and witness-gated agent-envelope, plain-text, and capability-gated terminal-color projections."}, {ChangeID: "proofkit.agent-workflow.native-evidence-guidance", Summary: "Add a no-input native-evidence-guidance command that exposes the repository-owned decision template and closed applicability classes as canonical JSON or plain text without executing or approving consumer witnesses."}, + {ChangeID: "proofkit.release.cross-carrier-binary-identity", Summary: "Release assembly now independently decodes final npm and Python archives under fixed bounds and directly requires byte-identical embedded binaries for every release platform."}, } var currentMigrationSteps = []string{ "Ensure every agent-envelope command, context, and receipt ID is individually safe and unique, keep the three local identity domains pairwise disjoint, and remove or rename references to ambiguous local targets before adopting 0.5.0.", + "Treat stderr as a bounded diagnostic class rather than an exact echo of caller or child-process text, and remove consumers that depend on raw forwarded diagnostics.", "Ensure every stable-JSON string is a Unicode scalar sequence, accept deterministic escaping of pinned Unicode 15 control and format code points, and regenerate TypeScript adapters against generator proofkit.json-report-cli-adapter-source.typescript.v2.", } diff --git a/internal/tools/releasemanifest/carrier_identity.go b/internal/tools/releasemanifest/carrier_identity.go new file mode 100644 index 0000000..ac1f795 --- /dev/null +++ b/internal/tools/releasemanifest/carrier_identity.go @@ -0,0 +1,202 @@ +package main + +import ( + "archive/tar" + "archive/zip" + "bytes" + "compress/gzip" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/releaseplatform" +) + +const ( + maxCarrierArchiveBytes = 256 << 20 + maxCarrierArchiveEntries = 4096 + maxCarrierBinaryBytes int64 = 64 << 20 + wheelBinaryEntry = "agentic_proofkit/bin/agentic-proofkit" +) + +func verifyCrossCarrierBinaryIdentity(packageDir string, pythonDir string, localRecords []packRecord, pythonPackages *pythonPackageSet) error { + if pythonPackages == nil { + return nil + } + if len(localRecords) != 1 { + return fmt.Errorf("cross-carrier identity requires exactly one npm package artifact") + } + npmBinaries, err := readNPMCarrierBinaries(filepath.Join(packageDir, localRecords[0].Filename)) + if err != nil { + return fmt.Errorf("decode npm package carrier: %w", err) + } + wheelsBySuffix := make(map[string]pythonWheelRecord, len(pythonPackages.Packages)) + for _, record := range pythonPackages.Packages { + if _, exists := wheelsBySuffix[record.PlatformSuffix]; exists { + return fmt.Errorf("cross-carrier identity contains a duplicate Python platform suffix") + } + wheelsBySuffix[record.PlatformSuffix] = record + } + targets := releaseplatform.Targets() + if len(wheelsBySuffix) != len(targets) { + return fmt.Errorf("cross-carrier identity requires exact release-platform wheel closure") + } + for _, target := range targets { + npmBinary, exists := npmBinaries[target.PlatformSuffix] + if !exists { + return fmt.Errorf("npm package carrier is missing a release-platform binary") + } + wheel, exists := wheelsBySuffix[target.PlatformSuffix] + if !exists { + return fmt.Errorf("python package carrier is missing a release-platform wheel") + } + wheelBinary, err := readWheelCarrierBinary(filepath.Join(pythonDir, wheel.Filename)) + if err != nil { + return fmt.Errorf("decode python wheel carrier: %w", err) + } + if !bytes.Equal(npmBinary, wheelBinary) { + return fmt.Errorf("npm and python package carriers contain different release-platform binary bytes") + } + } + return nil +} + +func readNPMCarrierBinaries(path string) (map[string][]byte, error) { + file, err := openBoundedCarrier(path) + if err != nil { + return nil, err + } + defer file.Close() + gzipReader, err := gzip.NewReader(file) + if err != nil { + return nil, err + } + defer gzipReader.Close() + tarReader := tar.NewReader(gzipReader) + expected := make(map[string]string, len(releaseplatform.Targets())) + for _, target := range releaseplatform.Targets() { + expected[target.PackageTarEntry] = target.PlatformSuffix + } + binaries := make(map[string][]byte, len(expected)) + var totalSize int64 + entryCount := 0 + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + entryCount++ + if entryCount > maxCarrierArchiveEntries { + return nil, fmt.Errorf("npm package carrier exceeds the entry limit") + } + if header.Size < 0 || header.Size > maxCarrierArchiveBytes-totalSize { + return nil, fmt.Errorf("npm package carrier exceeds the uncompressed byte limit") + } + totalSize += header.Size + platformSuffix, wanted := expected[header.Name] + if !wanted { + continue + } + if _, duplicate := binaries[platformSuffix]; duplicate { + return nil, fmt.Errorf("npm package carrier duplicates a release-platform binary") + } + if header.Typeflag != tar.TypeReg { + return nil, fmt.Errorf("npm package carrier release-platform binary is not a regular file") + } + content, err := readBoundedCarrierMember(tarReader, header.Size) + if err != nil { + return nil, err + } + binaries[platformSuffix] = content + } + if len(binaries) != len(expected) { + return nil, fmt.Errorf("npm package carrier lacks exact release-platform binary closure") + } + return binaries, nil +} + +func readWheelCarrierBinary(path string) ([]byte, error) { + info, err := os.Stat(path) + if err != nil { + return nil, err + } + if !info.Mode().IsRegular() || info.Size() > maxCarrierArchiveBytes { + return nil, fmt.Errorf("python wheel carrier is not an admitted regular archive") + } + reader, err := zip.OpenReader(path) + if err != nil { + return nil, err + } + defer reader.Close() + if len(reader.File) > maxCarrierArchiveEntries { + return nil, fmt.Errorf("python wheel carrier exceeds the entry limit") + } + seen := map[string]struct{}{} + var binary []byte + for _, entry := range reader.File { + if _, duplicate := seen[entry.Name]; duplicate { + return nil, fmt.Errorf("python wheel carrier contains duplicate entries") + } + seen[entry.Name] = struct{}{} + if entry.Name != wheelBinaryEntry { + continue + } + if entry.Mode()&os.ModeSymlink != 0 || !entry.FileInfo().Mode().IsRegular() { + return nil, fmt.Errorf("python wheel carrier binary is not a regular file") + } + if entry.UncompressedSize64 > uint64(maxCarrierBinaryBytes) { + return nil, fmt.Errorf("python wheel carrier binary exceeds the byte limit") + } + member, err := entry.Open() + if err != nil { + return nil, err + } + binary, err = readBoundedCarrierMember(member, int64(entry.UncompressedSize64)) + closeErr := member.Close() + if err != nil { + return nil, err + } + if closeErr != nil { + return nil, closeErr + } + } + if binary == nil { + return nil, fmt.Errorf("python wheel carrier lacks its embedded binary") + } + return binary, nil +} + +func openBoundedCarrier(path string) (*os.File, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + info, err := file.Stat() + if err != nil { + file.Close() + return nil, err + } + if !info.Mode().IsRegular() || info.Size() > maxCarrierArchiveBytes { + file.Close() + return nil, fmt.Errorf("package carrier is not an admitted regular archive") + } + return file, nil +} + +func readBoundedCarrierMember(reader io.Reader, declaredSize int64) ([]byte, error) { + if declaredSize < 0 || declaredSize > maxCarrierBinaryBytes { + return nil, fmt.Errorf("package carrier binary exceeds the byte limit") + } + content, err := io.ReadAll(io.LimitReader(reader, maxCarrierBinaryBytes+1)) + if err != nil { + return nil, err + } + if int64(len(content)) != declaredSize { + return nil, fmt.Errorf("package carrier binary size differs from its archive header") + } + return content, nil +} diff --git a/internal/tools/releasemanifest/carrier_identity_test.go b/internal/tools/releasemanifest/carrier_identity_test.go new file mode 100644 index 0000000..a9f7781 --- /dev/null +++ b/internal/tools/releasemanifest/carrier_identity_test.go @@ -0,0 +1,130 @@ +package main + +import ( + "archive/tar" + "archive/zip" + "compress/gzip" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/releaseplatform" +) + +func TestCrossCarrierBinaryIdentityReadsFinalArchives(t *testing.T) { + t.Run("matching_carriers", func(t *testing.T) { + packageDir, pythonDir, records, packages := writeCarrierFixture(t, "") + if err := verifyCrossCarrierBinaryIdentity(packageDir, pythonDir, records, packages); err != nil { + t.Fatal(err) + } + }) + + t.Run("wheel_mutation", func(t *testing.T) { + packageDir, pythonDir, records, packages := writeCarrierFixture(t, "linux-x64") + err := verifyCrossCarrierBinaryIdentity(packageDir, pythonDir, records, packages) + if err == nil || !strings.Contains(err.Error(), "different release-platform binary bytes") { + t.Fatalf("mutated wheel was not rejected: %v", err) + } + }) + + t.Run("duplicate_tar_member", func(t *testing.T) { + packageDir, pythonDir, records, packages := writeCarrierFixture(t, "") + writeNPMCarrier(t, filepath.Join(packageDir, records[0].Filename), true) + err := verifyCrossCarrierBinaryIdentity(packageDir, pythonDir, records, packages) + if err == nil || !strings.Contains(err.Error(), "duplicates a release-platform binary") { + t.Fatalf("duplicate npm binary was not rejected: %v", err) + } + }) +} + +func writeCarrierFixture(t *testing.T, mutatedWheelSuffix string) (string, string, []packRecord, *pythonPackageSet) { + t.Helper() + root := t.TempDir() + packageDir := filepath.Join(root, "package") + pythonDir := filepath.Join(root, "pypi") + if err := os.MkdirAll(packageDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(pythonDir, 0o755); err != nil { + t.Fatal(err) + } + records := []packRecord{{Filename: "proofkit.tgz"}} + writeNPMCarrier(t, filepath.Join(packageDir, records[0].Filename), false) + packages := &pythonPackageSet{Packages: make([]pythonWheelRecord, 0, len(releaseplatform.Targets()))} + for _, target := range releaseplatform.Targets() { + filename := target.PlatformSuffix + ".whl" + content := carrierBinary(target.PlatformSuffix) + if target.PlatformSuffix == mutatedWheelSuffix { + content = append(content, []byte("-mutated")...) + } + writeWheelCarrier(t, filepath.Join(pythonDir, filename), content) + packages.Packages = append(packages.Packages, pythonWheelRecord{ + Filename: filename, + PlatformSuffix: target.PlatformSuffix, + }) + } + return packageDir, pythonDir, records, packages +} + +func writeNPMCarrier(t *testing.T, path string, duplicate bool) { + t.Helper() + file, err := os.Create(path) + if err != nil { + t.Fatal(err) + } + gzipWriter := gzip.NewWriter(file) + tarWriter := tar.NewWriter(gzipWriter) + for index, target := range releaseplatform.Targets() { + copies := 1 + if duplicate && index == 0 { + copies = 2 + } + for range copies { + content := carrierBinary(target.PlatformSuffix) + if err := tarWriter.WriteHeader(&tar.Header{Name: target.PackageTarEntry, Mode: 0o755, Size: int64(len(content)), Typeflag: tar.TypeReg}); err != nil { + t.Fatal(err) + } + if _, err := tarWriter.Write(content); err != nil { + t.Fatal(err) + } + } + } + if err := tarWriter.Close(); err != nil { + t.Fatal(err) + } + if err := gzipWriter.Close(); err != nil { + t.Fatal(err) + } + if err := file.Close(); err != nil { + t.Fatal(err) + } +} + +func writeWheelCarrier(t *testing.T, path string, content []byte) { + t.Helper() + file, err := os.Create(path) + if err != nil { + t.Fatal(err) + } + wheel := zip.NewWriter(file) + header := &zip.FileHeader{Name: wheelBinaryEntry, Method: zip.Store} + header.SetMode(0o755) + member, err := wheel.CreateHeader(header) + if err != nil { + t.Fatal(err) + } + if _, err := member.Write(content); err != nil { + t.Fatal(err) + } + if err := wheel.Close(); err != nil { + t.Fatal(err) + } + if err := file.Close(); err != nil { + t.Fatal(err) + } +} + +func carrierBinary(platformSuffix string) []byte { + return []byte("binary:" + platformSuffix) +} diff --git a/internal/tools/releasemanifest/main.go b/internal/tools/releasemanifest/main.go index 6020cc8..ef62405 100644 --- a/internal/tools/releasemanifest/main.go +++ b/internal/tools/releasemanifest/main.go @@ -223,10 +223,6 @@ func run() error { if err != nil { return err } - assets, checksums, sbomSubjectChecksums, err := releaseAssets(localRecords, pythonPackages) - if err != nil { - return err - } registryRecords, err := optionalPackRecords(filepath.Join("artifacts", "registry", "npm-pack.json")) if err != nil { return err @@ -268,9 +264,16 @@ func run() error { if err := requirePythonPackagesMatchPackage(manifest, pythonPackages, "local Python package evidence"); err != nil { return err } + if err := verifyCrossCarrierBinaryIdentity(filepath.Join("artifacts", "package"), filepath.Join("artifacts", "pypi"), localRecords, pythonPackages); err != nil { + return err + } if err := requirePyPIRegistryMatchesLocal(pypiRegistry, pythonPackages, manifest); err != nil { return err } + assets, checksums, sbomSubjectChecksums, err := releaseAssets(localRecords, pythonPackages) + if err != nil { + return err + } trustedPublishers, err := trustedPublisherSetFromEnv(manifest, npmPublicationMode, pypiPublicationMode, os.Getenv) if err != nil { return err diff --git a/package.json b/package.json index 9d29761..015fe97 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "build": "go run ./internal/tools/packagebuild", "check": "npm run npm:version && npm run source-hygiene && npm run command-contract:check && npm run command-family:check && npm run text-policy && npm run mermaid:check && npm run go:check && npm run browser:check && npm run package:artifact && npm run self:receipt && npm run self:coverage && npm run release:closeout", "browser:check": "npm run browser:static-check && npm run browser:test", - "browser:static-check": "tsc -p tsconfig.browser.json && node --test scripts/browser-proof-execution.test.mjs scripts/browser-proof-inputs.test.mjs scripts/browser-selection-authority.test.mjs scripts/stable-json.test.mjs", + "browser:static-check": "tsc -p tsconfig.browser.json && node --test scripts/browser-proof-execution.test.mjs scripts/browser-proof-inputs.test.mjs scripts/browser-selection-authority.test.mjs scripts/diagnostic-entrypoints.test.mjs scripts/stable-json.test.mjs", "browser:test": "go run ./internal/tools/browserproofverify --run", "command-family:check": "go run ./internal/tools/commandfamilygen --check", "go:actionlint": "go tool actionlint", diff --git a/proofkit/cli-contract.v2.json b/proofkit/cli-contract.v2.json index 0a25fa5..e6782ed 100644 --- a/proofkit/cli-contract.v2.json +++ b/proofkit/cli-contract.v2.json @@ -859,7 +859,7 @@ "rootDefinitionDigest": "sha256:6632a481380bfb07d754ef622661b291f805b1b168b1824089bdd5cc9bdd4b0e", "nativeSource": { "path": "internal/command/changeworkflowplan", - "canonicalDigest": "sha256:4586cf0286a67200368ff54512185744aaf0ab56acd30e51eba58f65dc816834", + "canonicalDigest": "sha256:072cd4568529c5e253f1e5f296ba77fcf013ecd04f85f4c34b72082f7c57f0f5", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -894,7 +894,7 @@ "rootDefinitionDigest": "sha256:ca7b2acffdcbfd28222eb61f2bd8687f76f20cb789b712ee3ffc2756a3674ff8", "nativeSource": { "path": "internal/command/changeworkflowplan", - "canonicalDigest": "sha256:4586cf0286a67200368ff54512185744aaf0ab56acd30e51eba58f65dc816834", + "canonicalDigest": "sha256:072cd4568529c5e253f1e5f296ba77fcf013ecd04f85f4c34b72082f7c57f0f5", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { diff --git a/proofkit/requirement-bindings.json b/proofkit/requirement-bindings.json index e99e807..8b6bef1 100644 --- a/proofkit/requirement-bindings.json +++ b/proofkit/requirement-bindings.json @@ -3647,6 +3647,21 @@ "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-023", + "scenarioId": "proofkit.supply-chain-quality.final-cross-carrier-binary-byte-identity", + "witnessId": "proofkit.release-manifest.cross-carrier-byte-identity-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/tools/releasemanifest/carrier_identity_test.go", + "witnessSelectors": [ + { + "selector": "TestCrossCarrierBinaryIdentityReadsFinalArchives", + "command": "go test ./internal/tools/releasemanifest -run '^TestCrossCarrierBinaryIdentityReadsFinalArchives$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, { "requirementId": "REQ-PROOFKIT-QUALITY-023", "scenarioId": "proofkit.supply-chain-quality.python-wheel-platform-byte-compatibility", @@ -3876,6 +3891,64 @@ "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-027", + "scenarioId": "proofkit.supply-chain-quality.go-bounded-diagnostic-capture", + "witnessId": "proofkit.diagnostic.bounded-capture-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/kernel/diagnostic/diagnostic_test.go", + "witnessSelectors": [ + { + "selector": "TestDiagnosticTextEnforcesWholeValueSafetyAndRuneBound", + "command": "go test ./internal/kernel/diagnostic -run '^TestDiagnosticTextEnforcesWholeValueSafetyAndRuneBound$'" + }, + { + "selector": "TestStderrCaptureSanitizesWholeValuesAndBoundsMemory", + "command": "go test ./internal/kernel/diagnostic -run '^TestStderrCaptureSanitizesWholeValuesAndBoundsMemory$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-027", + "scenarioId": "proofkit.supply-chain-quality.javascript-entrypoint-diagnostic-owner", + "witnessId": "proofkit.javascript-entrypoint.diagnostic-owner-falsifier", + "witnessKind": "contract", + "witnessPath": "scripts/diagnostic-entrypoints.test.mjs", + "commandIds": ["proofkit.browser-check"], + "environmentClasses": ["local-node-browser"] + }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-027", + "scenarioId": "proofkit.supply-chain-quality.browser-proof-writer-diagnostic-nondisclosure", + "witnessId": "proofkit.browser-proof-writer.diagnostic-nondisclosure-falsifier", + "witnessKind": "contract", + "witnessPath": "scripts/diagnostic_boundary_test.go", + "witnessSelectors": [ + { + "selector": "TestBrowserProofWriterRedactsInvalidResolution", + "command": "go test ./scripts -run '^TestBrowserProofWriterRedactsInvalidResolution$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-027", + "scenarioId": "proofkit.supply-chain-quality.source-hygiene-diagnostic-nondisclosure", + "witnessId": "proofkit.source-hygiene.diagnostic-nondisclosure-falsifier", + "witnessKind": "contract", + "witnessPath": "scripts/source_hygiene_test.go", + "witnessSelectors": [ + { + "selector": "TestSourceHygieneRedactsSecretShapedTrackedPath", + "command": "go test ./scripts -run '^TestSourceHygieneRedactsSecretShapedTrackedPath$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, { "requirementId": "REQ-PROOFKIT-QUALITY-027", "scenarioId": "proofkit.supply-chain-quality.go-report-visible-whole-value-redaction", @@ -4630,6 +4703,10 @@ "witnessKind": "technical", "witnessPath": "internal/command/changeworkflowplan/text_test.go", "witnessSelectors": [ + { + "selector": "TestWorkflowTerminalTextIsOperationallyComplete", + "command": "go test ./internal/command/changeworkflowplan -run '^TestWorkflowTerminalTextIsOperationallyComplete$'" + }, { "selector": "TestWorkflowTextPredicates", "command": "go test ./internal/command/changeworkflowplan -run '^TestWorkflowTextPredicates$'" diff --git a/proofkit/witness-plan.json b/proofkit/witness-plan.json index 1f10407..9afd1d6 100644 --- a/proofkit/witness-plan.json +++ b/proofkit/witness-plan.json @@ -796,6 +796,7 @@ "scripts/browser-proof-inputs.test.mjs", "scripts/browser-runtime-proof-inputs.v1.json", "scripts/browser-selection-authority.test.mjs", + "scripts/diagnostic.mjs", "scripts/stable-json.mjs", "scripts/stable-json.test.mjs", "scripts/write-browser-proof.mjs", diff --git a/release/change-record.v2.json b/release/change-record.v2.json index 95952a9..5a199d0 100644 --- a/release/change-record.v2.json +++ b/release/change-record.v2.json @@ -8,6 +8,10 @@ "changeId": "proofkit.agent-envelope.local-identity-closure", "summary": "Agent envelopes now require unique, pairwise-disjoint command, context, and receipt identities and fail closed by pruning unsafe or ambiguous local targets and references with bounded omissions." }, + { + "changeId": "proofkit.diagnostic.bounded-error-boundary", + "summary": "CLI and repository-tool diagnostics now reject malformed or report-unsafe values as a whole, cap admitted text, and bound child-process stdout, stderr, lifetime, and process-group cleanup instead of forwarding raw child errors." + }, { "changeId": "proofkit.stable-json.unicode-scalar-v2", "summary": "Go, JavaScript, and generated TypeScript stable JSON now reject non-scalar strings and deterministically escape the pinned Unicode 15 control and format policy; the generated TypeScript adapter identity advances to v2." @@ -16,17 +20,22 @@ "additions": [ { "changeId": "proofkit.agent-workflow.change-planner", - "summary": "Add a bounded change-workflow-plan command for the named optional proofkit.reviewed-change.v1 profile with canonical JSON, derived agent-envelope, plain-text, and capability-gated terminal-color projections." + "summary": "Add a bounded change-workflow-plan command for the named optional proofkit.reviewed-change.v1 profile with canonical JSON, derived authority- and witness-gated agent-envelope, plain-text, and capability-gated terminal-color projections." }, { "changeId": "proofkit.agent-workflow.native-evidence-guidance", "summary": "Add a no-input native-evidence-guidance command that exposes the repository-owned decision template and closed applicability classes as canonical JSON or plain text without executing or approving consumer witnesses." + }, + { + "changeId": "proofkit.release.cross-carrier-binary-identity", + "summary": "Release assembly now independently decodes final npm and Python archives under fixed bounds and directly requires byte-identical embedded binaries for every release platform." } ], "migration": { "required": true, "steps": [ "Ensure every agent-envelope command, context, and receipt ID is individually safe and unique, keep the three local identity domains pairwise disjoint, and remove or rename references to ambiguous local targets before adopting 0.5.0.", + "Treat stderr as a bounded diagnostic class rather than an exact echo of caller or child-process text, and remove consumers that depend on raw forwarded diagnostics.", "Ensure every stable-JSON string is a Unicode scalar sequence, accept deterministic escaping of pinned Unicode 15 control and format code points, and regenerate TypeScript adapters against generator proofkit.json-report-cli-adapter-source.typescript.v2." ] }, diff --git a/scripts/browser-runtime-proof-inputs.v1.json b/scripts/browser-runtime-proof-inputs.v1.json index 04ad94f..c1e266d 100644 --- a/scripts/browser-runtime-proof-inputs.v1.json +++ b/scripts/browser-runtime-proof-inputs.v1.json @@ -16,6 +16,7 @@ "scripts/browser-proof-inputs.mjs", "scripts/browser-proof-inputs.test.mjs", "scripts/browser-selection-authority.test.mjs", + "scripts/diagnostic.mjs", "scripts/stable-json.mjs", "scripts/stable-json.test.mjs", "tsconfig.browser.json" diff --git a/scripts/diagnostic-entrypoints.test.mjs b/scripts/diagnostic-entrypoints.test.mjs new file mode 100644 index 0000000..a85f688 --- /dev/null +++ b/scripts/diagnostic-entrypoints.test.mjs @@ -0,0 +1,23 @@ +import assert from "node:assert/strict"; +import {readFileSync} from "node:fs"; +import test from "node:test"; + +test("repository-owned JavaScript entrypoints use the diagnostic boundary", () => { + const manifest = JSON.parse(readFileSync("package.json", "utf8")); + const browserInputs = JSON.parse(readFileSync("scripts/browser-runtime-proof-inputs.v1.json", "utf8")); + const entrypoints = new Set([browserInputs.writerPath]); + for (const command of Object.values(manifest.scripts)) { + for (const match of command.matchAll(/(?:^|&&|\|\|)\s*node\s+(scripts\/[^\s]+\.mjs)(?:\s|$)/gu)) { + entrypoints.add(match[1]); + } + } + assert.deepEqual([...entrypoints].sort(), [ + "scripts/source-hygiene.mjs", + "scripts/write-browser-proof.mjs", + ]); + for (const path of entrypoints) { + const source = readFileSync(path, "utf8"); + assert.match(source, /import \{runDiagnosticEntrypoint\} from "\.\/diagnostic\.mjs";/u, path); + assert.match(source, /await runDiagnosticEntrypoint\(/u, path); + } +}); diff --git a/scripts/diagnostic.mjs b/scripts/diagnostic.mjs new file mode 100644 index 0000000..d41507d --- /dev/null +++ b/scripts/diagnostic.mjs @@ -0,0 +1,11 @@ +import {redactDiagnosticValue} from "./stable-json.mjs"; + +export async function runDiagnosticEntrypoint(operation, stderr = process.stderr) { + try { + await operation(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + stderr.write(`${redactDiagnosticValue(message)}\n`); + process.exitCode = 1; + } +} diff --git a/scripts/diagnostic_boundary_test.go b/scripts/diagnostic_boundary_test.go index 1ccc32e..7c3d2d6 100644 --- a/scripts/diagnostic_boundary_test.go +++ b/scripts/diagnostic_boundary_test.go @@ -1,10 +1,13 @@ package main import ( + "bytes" "go/ast" "go/parser" "go/token" "io/fs" + "os" + "os/exec" "path" "path/filepath" "strconv" @@ -14,7 +17,7 @@ import ( func TestToolDiagnosticBoundaryRejectsDirectDynamicStderrWrites(t *testing.T) { repositoryRoot := sourceHygieneRepoRoot(t) - for _, scope := range []string{"internal/tools", "scripts"} { + for _, scope := range []string{"cmd/agentic-proofkit", "internal/tools", "scripts"} { root := filepath.Join(repositoryRoot, scope) if err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error { if walkErr != nil { @@ -45,6 +48,9 @@ func TestToolDiagnosticBoundaryRejectsDirectDynamicStderrWrites(t *testing.T) { {name: "raw error", source: `package main; import ("fmt"; "os"); func main(){ var err error; fmt.Fprintln(os.Stderr, err) }`, want: 1}, {name: "formatted dynamic value", source: `package main; import ("fmt"; "os"); func main(){ value := "caller"; fmt.Fprintf(os.Stderr, "failed: %s", value) }`, want: 1}, {name: "aliased imports", source: `package main; import (f "fmt"; system "os"); func main(){ var err error; f.Fprintln(system.Stderr, err) }`, want: 1}, + {name: "direct child stderr", source: `package main; import ("os"; "os/exec"); func main(){ command := exec.Command("tool"); command.Stderr = os.Stderr }`, want: 1}, + {name: "nested direct child stderr", source: `package main; import ("io"; "os"; "os/exec"); func main(){ command := exec.Command("tool"); command.Stderr = io.MultiWriter(os.Stderr) }`, want: 1}, + {name: "composite child stderr", source: `package main; import ("os"; "os/exec"); var command = exec.Cmd{Stderr: os.Stderr}`, want: 1}, } { t.Run(test.name, func(t *testing.T) { parsed, err := parser.ParseFile(token.NewFileSet(), "fixture.go", test.source, 0) @@ -58,6 +64,24 @@ func TestToolDiagnosticBoundaryRejectsDirectDynamicStderrWrites(t *testing.T) { } } +func TestBrowserProofWriterRedactsInvalidResolution(t *testing.T) { + repositoryRoot := sourceHygieneRepoRoot(t) + secret := strings.Join([]string{"api", "_key=", "abc123456789"}, "") + command := exec.Command("node", filepath.Join(repositoryRoot, "scripts", "write-browser-proof.mjs")) + command.Dir = repositoryRoot + command.Env = append(os.Environ(), "PROOFKIT_BROWSER_INPUT_RESOLUTION="+secret) + var stdout bytes.Buffer + var stderr bytes.Buffer + command.Stdout = &stdout + command.Stderr = &stderr + if err := command.Run(); err == nil { + t.Fatal("browser proof writer accepted invalid input resolution") + } + if stdout.Len() != 0 || stderr.String() != "\n" || strings.Contains(stderr.String(), "abc123456789") { + t.Fatalf("stdout=%q stderr=%q", stdout.String(), stderr.String()) + } +} + func dynamicStderrWriteCount(file *ast.File) int { importNames := map[string]string{} for _, spec := range file.Imports { @@ -73,6 +97,19 @@ func dynamicStderrWriteCount(file *ast.File) int { } count := 0 ast.Inspect(file, func(node ast.Node) bool { + if assignment, ok := node.(*ast.AssignStmt); ok { + for index, left := range assignment.Lhs { + if index < len(assignment.Rhs) && isStderrField(left) && containsOSStderr(assignment.Rhs[index], importNames["os"]) { + count++ + } + } + } + if field, ok := node.(*ast.KeyValueExpr); ok { + identifier, keyOK := field.Key.(*ast.Ident) + if keyOK && identifier.Name == "Stderr" && containsOSStderr(field.Value, importNames["os"]) { + count++ + } + } call, ok := node.(*ast.CallExpr) if !ok || len(call.Args) < 2 || !isFmtPrintCall(call.Fun, importNames["fmt"]) || !isOSStderr(call.Args[0], importNames["os"]) { return true @@ -89,6 +126,24 @@ func dynamicStderrWriteCount(file *ast.File) int { return count } +func isStderrField(expression ast.Expr) bool { + selector, ok := expression.(*ast.SelectorExpr) + return ok && selector.Sel.Name == "Stderr" +} + +func containsOSStderr(expression ast.Expr, importName string) bool { + found := false + ast.Inspect(expression, func(node ast.Node) bool { + candidate, ok := node.(ast.Expr) + if ok && isOSStderr(candidate, importName) { + found = true + return false + } + return true + }) + return found +} + func isFmtPrintCall(expression ast.Expr, importName string) bool { selector, ok := expression.(*ast.SelectorExpr) if !ok || (selector.Sel.Name != "Fprint" && selector.Sel.Name != "Fprintf" && selector.Sel.Name != "Fprintln") { diff --git a/scripts/source-hygiene.mjs b/scripts/source-hygiene.mjs index 5b33c2a..4e3c637 100644 --- a/scripts/source-hygiene.mjs +++ b/scripts/source-hygiene.mjs @@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; +import {runDiagnosticEntrypoint} from "./diagnostic.mjs"; import {decodeUTF8Strict} from "./stable-json.mjs"; const textExtensions = new Set([ @@ -64,35 +65,37 @@ function isTextFile(file) { return textExtensions.has(extension); } -const organizationSpecific = new Set(); - -for (const entry of trackedIndexEntries()) { - if (!isTextFile(entry.file)) { - continue; +await runDiagnosticEntrypoint(() => { + const organizationSpecific = new Set(); + + for (const entry of trackedIndexEntries()) { + if (!isTextFile(entry.file)) { + continue; + } + + const lowerText = decodeUTF8Strict( + execFileSync("git", ["cat-file", "-p", entry.object]), + "tracked text object", + ).toLowerCase(); + if (containsOrganizationSpecificToken(lowerText)) { + organizationSpecific.add(entry.file); + } } - const lowerText = decodeUTF8Strict( - execFileSync("git", ["cat-file", "-p", entry.object]), - "tracked text object", - ).toLowerCase(); - if (containsOrganizationSpecificToken(lowerText)) { - organizationSpecific.add(entry.file); - } -} + for (const file of trackedFiles()) { + if (!isTextFile(file) || !existsSync(file)) { + continue; + } -for (const file of trackedFiles()) { - if (!isTextFile(file) || !existsSync(file)) { - continue; + const lowerText = decodeUTF8Strict(readFileSync(file), "worktree text file").toLowerCase(); + if (containsOrganizationSpecificToken(lowerText)) { + organizationSpecific.add(file); + } } - const lowerText = decodeUTF8Strict(readFileSync(file), "worktree text file").toLowerCase(); - if (containsOrganizationSpecificToken(lowerText)) { - organizationSpecific.add(file); + if (organizationSpecific.size > 0) { + throw new Error( + `organization-specific text leaked into Proofkit: ${[...organizationSpecific].sort().join(", ")}`, + ); } -} - -if (organizationSpecific.size > 0) { - throw new Error( - `organization-specific text leaked into Proofkit: ${[...organizationSpecific].sort().join(", ")}`, - ); -} +}); diff --git a/scripts/source_hygiene_test.go b/scripts/source_hygiene_test.go index fd13253..7a1334a 100644 --- a/scripts/source_hygiene_test.go +++ b/scripts/source_hygiene_test.go @@ -123,6 +123,31 @@ func TestSourceHygieneRejectsMalformedUTF8(t *testing.T) { } } +func TestSourceHygieneRedactsSecretShapedTrackedPath(t *testing.T) { + scriptPath := sourceHygieneScriptPath(t) + tempDir := t.TempDir() + runCommand(t, tempDir, "git", "init") + secretPath := strings.Join([]string{"api", "_key=", "abc123456789.md"}, "") + bannedToken := strings.Join([]string{"a", "fc"}, "") + if err := os.WriteFile(filepath.Join(tempDir, secretPath), []byte("leaked "+bannedToken+"\n"), 0o644); err != nil { + t.Fatal(err) + } + runCommand(t, tempDir, "git", "add", secretPath) + + command := exec.Command("node", scriptPath) + command.Dir = tempDir + var stdout strings.Builder + var stderr strings.Builder + command.Stdout = &stdout + command.Stderr = &stderr + if err := command.Run(); err == nil { + t.Fatal("source hygiene accepted organization-specific content") + } + if stdout.Len() != 0 || stderr.String() != "\n" || strings.Contains(stderr.String(), "abc123456789") { + t.Fatalf("stdout=%q stderr=%q", stdout.String(), stderr.String()) + } +} + func assertSourceHygieneRejects(t *testing.T, scriptPath string, repoRoot string, file string, evidenceClass string) { t.Helper() command := exec.Command("node", scriptPath) diff --git a/scripts/write-browser-proof.mjs b/scripts/write-browser-proof.mjs index 857f91a..77770bb 100644 --- a/scripts/write-browser-proof.mjs +++ b/scripts/write-browser-proof.mjs @@ -4,56 +4,60 @@ import {writeFileSync} from "node:fs"; import {executeBrowserProof} from "./browser-proof-execution.mjs"; import {isSafeRepoPath, loadBrowserProofInputResolution} from "./browser-proof-inputs.mjs"; +import {runDiagnosticEntrypoint} from "./diagnostic.mjs"; import {decodeUTF8Strict, stableJSONStringify} from "./stable-json.mjs"; const testCommand = ["node_modules/@playwright/test/cli.js", "test"]; -const resolution = loadBrowserProofInputResolution(); -const inputPaths = resolution.inputPaths; -const runDirectory = requiredSafePath("PROOFKIT_BROWSER_RUN_DIRECTORY"); -const proofCandidatePath = requiredSafePath("PROOFKIT_BROWSER_PROOF_CANDIDATE_PATH"); -if (proofCandidatePath !== `${runDirectory}/browser-runtime-proof.candidate.json`) { - throw new Error("browser proof candidate path must belong to the admitted run directory"); -} -const execution = await executeBrowserProof({ - environment: process.env, - execFile: execFileSync, - inputPaths, - nodeExecutable: process.execPath, - resolution, - runDirectory, - spawnProcess: spawnSync, - testCommand, + +await runDiagnosticEntrypoint(async () => { + const resolution = loadBrowserProofInputResolution(); + const inputPaths = resolution.inputPaths; + const runDirectory = requiredSafePath("PROOFKIT_BROWSER_RUN_DIRECTORY"); + const proofCandidatePath = requiredSafePath("PROOFKIT_BROWSER_PROOF_CANDIDATE_PATH"); + if (proofCandidatePath !== `${runDirectory}/browser-runtime-proof.candidate.json`) { + throw new Error("browser proof candidate path must belong to the admitted run directory"); + } + const execution = await executeBrowserProof({ + environment: process.env, + execFile: execFileSync, + inputPaths, + nodeExecutable: process.execPath, + resolution, + runDirectory, + spawnProcess: spawnSync, + testCommand, + }); + const {assets, projects, testResult} = execution; + const inputResolution = {serverTarget: resolution.serverTarget, writerPath: resolution.writerPath}; + const inputDigest = createHash("sha256").update(stableJSONStringify({assets, inputResolution})).digest("hex"); + const engines = projects.map(({browserName, browserVersion}) => ({name: browserName, version: browserVersion})); + const sourceRevision = decodeUTF8Strict(execFileSync("git", ["rev-parse", "HEAD"]), "git source revision output").trim(); + const sourceTreeState = decodeUTF8Strict( + execFileSync("git", ["status", "--porcelain=v1", "--untracked-files=all"]), + "git source tree status output", + ).trim() === "" ? "clean" : "dirty"; + const record = { + assets, + command: {argv: testCommand, exitCode: testResult.status, inputMode: "materialized_snapshot", runner: "node"}, + engines, + inputDigest: `sha256:${inputDigest}`, + inputResolution, + nonClaims: [ + "This record proves only that the listed content-bound Playwright tests passed in each recorded project before record creation.", + "Playwright WebKit is not branded Safari compatibility proof.", + "This record does not prove registry publication, rollout, or production readiness.", + "A dirty-tree record is content-bound only to its listed assets; sourceRevision is contextual.", + "This record does not independently authenticate or sandbox the same-user proof runner, external toolchain packages, or browser binaries.", + ], + projects, + proofKind: "proofkit.browser-runtime-proof", + schemaVersion: 2, + sourceRevision, + sourceTreeState, + state: "passed", + }; + writeFileSync(proofCandidatePath, `${JSON.stringify(record, null, 2)}\n`, {flag: "wx", mode: 0o600}); }); -const {assets, projects, testResult} = execution; -const inputResolution = {serverTarget: resolution.serverTarget, writerPath: resolution.writerPath}; -const inputDigest = createHash("sha256").update(stableJSONStringify({assets, inputResolution})).digest("hex"); -const engines = projects.map(({browserName, browserVersion}) => ({name: browserName, version: browserVersion})); -const sourceRevision = decodeUTF8Strict(execFileSync("git", ["rev-parse", "HEAD"]), "git source revision output").trim(); -const sourceTreeState = decodeUTF8Strict( - execFileSync("git", ["status", "--porcelain=v1", "--untracked-files=all"]), - "git source tree status output", -).trim() === "" ? "clean" : "dirty"; -const record = { - assets, - command: {argv: testCommand, exitCode: testResult.status, inputMode: "materialized_snapshot", runner: "node"}, - engines, - inputDigest: `sha256:${inputDigest}`, - inputResolution, - nonClaims: [ - "This record proves only that the listed content-bound Playwright tests passed in each recorded project before record creation.", - "Playwright WebKit is not branded Safari compatibility proof.", - "This record does not prove registry publication, rollout, or production readiness.", - "A dirty-tree record is content-bound only to its listed assets; sourceRevision is contextual.", - "This record does not independently authenticate or sandbox the same-user proof runner, external toolchain packages, or browser binaries.", - ], - projects, - proofKind: "proofkit.browser-runtime-proof", - schemaVersion: 2, - sourceRevision, - sourceTreeState, - state: "passed", -}; -writeFileSync(proofCandidatePath, `${JSON.stringify(record, null, 2)}\n`, {flag: "wx", mode: 0o600}); function requiredSafePath(name) { const value = process.env[name]; From bfbc9eeb0e77bb1a5d92341ca34bfde1160a65fb Mon Sep 17 00:00:00 2001 From: iperev Date: Mon, 31 Aug 2026 20:28:09 +0200 Subject: [PATCH 07/11] fix: close final workflow audit findings --- .github/workflows/release.yml | 2 +- .../requirements.v1.json | 8 +- internal/app/agent_workflow_topology_test.go | 35 +-- .../app/agent_workflow_version_edge_test.go | 19 +- internal/app/cli_contract_test.go | 2 +- internal/app/command_contract_generated.go | 4 +- .../app/testdata/v0.5-wire-observations.json | 7 +- .../json_report_cli_adapter_source.go | 26 +- .../json_report_cli_adapter_source_test.go | 52 +++- .../stackpreset/preset_ids_generated.go | 2 +- .../testdata/unsafe-scalar-ranges.v1.json | 29 ++ .../unicodepolicy/unicodepolicy_test.go | 39 +++ internal/tools/packageverify/main.go | 4 +- internal/tools/releasechange/record_test.go | 4 +- .../releasemanifest/artifact_snapshot.go | 277 ++++++++++++++++++ .../releasemanifest/artifact_snapshot_test.go | 89 ++++++ .../tools/releasemanifest/carrier_identity.go | 139 ++++++--- .../releasemanifest/carrier_identity_test.go | 30 +- internal/tools/releasemanifest/main.go | 62 +--- internal/tools/releasemanifest/main_test.go | 5 +- internal/tools/workflowsmoke/process.go | 16 +- .../tools/workflowsmoke/process_unix_test.go | 48 +++ .../workflowsmoke/workflow_smoke_test.go | 10 + proofkit/cli-contract.v2.json | 2 +- proofkit/requirement-bindings.json | 55 +++- release/change-record.v2.json | 4 +- scripts/stable-json.test.mjs | 19 +- scripts/workflow_package_gate_oracle_test.go | 15 + 28 files changed, 832 insertions(+), 172 deletions(-) create mode 100644 internal/kernel/unicodepolicy/testdata/unsafe-scalar-ranges.v1.json create mode 100644 internal/tools/releasemanifest/artifact_snapshot.go create mode 100644 internal/tools/releasemanifest/artifact_snapshot_test.go create mode 100644 internal/tools/workflowsmoke/process_unix_test.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 58ab25e..93d2a9f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -467,7 +467,7 @@ jobs: ["schemaVersion", 1], ["artifactKind", "proofkit.json-report-cli-adapter-source"], ["format", "json"], - ["generatorId", "proofkit.json-report-cli-adapter-source.typescript.v1"], + ["generatorId", "proofkit.json-report-cli-adapter-source.typescript.v2"], ["language", "typescript"], ["sourceFileName", "proofkit-json-report-cli-adapter.ts"] ]) { diff --git a/docs/specs/proofkit-supply-chain-quality/requirements.v1.json b/docs/specs/proofkit-supply-chain-quality/requirements.v1.json index c2a3dcd..891d4db 100644 --- a/docs/specs/proofkit-supply-chain-quality/requirements.v1.json +++ b/docs/specs/proofkit-supply-chain-quality/requirements.v1.json @@ -298,7 +298,7 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-023", "ownerId": "proofkit.supply-chain-quality", - "invariant": "Every npm platform-binary tar entry and every Python wheel independently binds its embedded executable bytes to the same release-platform binary, and the release manifest independently decodes the final npm tarball and every final wheel under bounded archive limits and directly requires byte equality for the exact release-platform closure without relying on a mutable intermediate dist directory or claimed digests; the wheel-set verifier independently rejects version drift, duplicate identities, missing files, and SHA drift; each wheel independently decodes Darwin Mach-O bytes so an advertised macOS platform minimum is never lower than the executable minimum, uses Core Metadata 2.4 License-Expression and License-File fields, carries an exact repository LICENSE copy under the wheel licenses directory, and closes every payload entry through RECORD.", + "invariant": "Every npm platform-binary tar entry and every Python wheel independently binds its embedded executable bytes to the same release-platform binary, and release metadata generation admits every final npm tarball, wheel, and SBOM exactly once into one bounded regular non-symlink immutable snapshot, derives final carrier decoding, direct chunked binary equality, release checksums, and SBOM-subject checksums only from that epoch, and revalidates every source identity before metadata persistence without relying on a mutable intermediate dist directory or claimed digests; both tar and wheel decoders reject duplicate entry names and require exact release-platform closure; the wheel-set verifier independently rejects version drift, duplicate identities, missing files, and SHA drift; each wheel independently decodes Darwin Mach-O bytes so an advertised macOS platform minimum is never lower than the executable minimum, uses Core Metadata 2.4 License-Expression and License-File fields, carries an exact repository LICENSE copy under the wheel licenses directory, and closes every payload entry through RECORD.", "claimLevel": "blocking", "riskClass": "critical", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -337,7 +337,7 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-026", "ownerId": "proofkit.supply-chain-quality", - "invariant": "Every repository-owned stable JSON implementation used by a public or retained-evidence path accepts only Unicode 15.0.0 scalar strings, rejecting malformed UTF-8 byte strings and unpaired UTF-16 surrogates as applicable, emits the same deterministic semantic-preserving JSON escapes for the pinned Cc, Cf, Zl, and Zp range table, preserves ordinary decoded values, and remains byte-compatible with one independently authored literal corpus frozen before implementation.", + "invariant": "Every repository-owned stable JSON implementation used by a public or retained-evidence path accepts only Unicode 15.0.0 scalar strings, rejecting malformed UTF-8 byte strings and unpaired UTF-16 surrogates as applicable, emits the same deterministic semantic-preserving JSON escapes for the pinned Cc, Cf, Zl, and Zp range table, preserves ordinary decoded values, and remains byte-compatible with one versioned owner-authored literal range corpus whose Go projection is exhaustively checked against the Unicode 15.0.0 category oracle and whose JavaScript and generated TypeScript projections are exhaustively checked over the scalar domain.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -350,12 +350,12 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-027", "ownerId": "proofkit.supply-chain-quality", - "invariant": "Every report-visible diagnostic and structural-text sanitizer accepts only Unicode 15.0.0 scalar strings, recognizes one explicit Unicode whitespace alphabet plus the pinned Cc, Cf, Zl, and Zp range table and contiguous or control-split secret-shaped values through one shared taxonomy, replaces any rejected caller-owned value as a whole with one fixed command-owned label without emitting its bytes, code points, prefix, suffix, or caller-controlled replacement text, and bounds admitted diagnostic projection to 512 Unicode scalars; every Go and JavaScript repository tool entrypoint routes dynamic errors through that owner, failed child-process stderr is captured under a fixed byte limit before whole-value sanitization while successful child stderr is not projected as a diagnostic, direct child stderr wiring is rejected, and subprocess adapters bound stdout, stderr, time, and process-group cleanup.", + "invariant": "Every report-visible diagnostic and structural-text sanitizer accepts only Unicode 15.0.0 scalar strings, recognizes one explicit Unicode whitespace alphabet plus the pinned Cc, Cf, Zl, and Zp range table and contiguous or control-split secret-shaped values through one shared taxonomy, replaces any rejected caller-owned value as a whole with one fixed command-owned label without emitting its bytes, code points, prefix, suffix, or caller-controlled replacement text, and bounds admitted diagnostic projection to 512 Unicode scalars; every Go and JavaScript repository tool entrypoint routes dynamic errors through that owner, failed child-process stderr that enters this diagnostic boundary is captured under a fixed byte limit before whole-value sanitization while successful child stderr is not projected, direct child stderr wiring is rejected, and the installed-carrier smoke adapter bounds stdout, stderr, time, and process-group cleanup and terminates process-group descendants before every post-start return on supported Unix hosts.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], "nonClaimRefs": ["NC-PROOFKIT-QUALITY-027"], - "nonClaims": ["This bounded diagnostic taxonomy does not prove confidentiality of values outside its admitted secret and control classes."], + "nonClaims": ["This bounded diagnostic taxonomy does not prove confidentiality of values outside its admitted secret and control classes or claim that every repository-internal tool invocation uses the installed-carrier timeout policy."], "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, "deferral": null, "updatePolicy": {"reviewOwnerId": "proofkit.supply-chain-quality", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} diff --git a/internal/app/agent_workflow_topology_test.go b/internal/app/agent_workflow_topology_test.go index 8f95718..c647f39 100644 --- a/internal/app/agent_workflow_topology_test.go +++ b/internal/app/agent_workflow_topology_test.go @@ -76,14 +76,14 @@ func TestAgentWorkflowSemanticOwnerTopology(t *testing.T) { func assertAgentWorkflowCarrierClosure(t *testing.T, root string) { t.Helper() closures := []struct { - file string - imports []string - callees []string + file string + imports []string + requiredCallees []string }{ { - file: "agent_workflow_args.go", - imports: []string{"fmt", "github.com/research-engineering/agentic-proofkit/internal/kernel/jsonpointer"}, - callees: []string{"fmt.Errorf", "jsonpointer.Parse", "len", "missingAgentWorkflowValue"}, + file: "agent_workflow_args.go", + imports: []string{"fmt", "github.com/research-engineering/agentic-proofkit/internal/kernel/jsonpointer"}, + requiredCallees: []string{"jsonpointer.Parse"}, }, { file: "agent_workflow_command.go", @@ -92,7 +92,7 @@ func assertAgentWorkflowCarrierClosure(t *testing.T, root string) { "github.com/research-engineering/agentic-proofkit/internal/kernel/jsonpointer", "io", }, - callees: []string{ + requiredCallees: []string{ "append", "changeWorkflowTerminalText", "changeworkflowplan.Build", "changeworkflowplan.BuildAgentEnvelope", "changeworkflowplan.BuildTextProjection", "jsonpointer.SelectParsed", "len", "make", "newTerminalText", "parseAgentWorkflowArgs", "readInput", "renderTerminalText", "runNativeEvidenceGuidance", "writeDiagnostic", @@ -102,15 +102,15 @@ func assertAgentWorkflowCarrierClosure(t *testing.T, root string) { { file: "native_evidence_guidance_command.go", imports: []string{"github.com/research-engineering/agentic-proofkit/internal/command/nativeevidenceguidance", "io"}, - callees: []string{ + requiredCallees: []string{ "append", "guidance.JSONValue", "len", "make", "nativeEvidenceGuidanceTerminalText", "nativeevidenceguidance.Build", "nativeevidenceguidance.TextProjection", "newTerminalText", "renderTerminalText", "writeJSON", "writeText", }, }, { - file: "terminal_style.go", - imports: []string{"fmt", "strings"}, - callees: []string{"append", "builder.String", "builder.WriteString", "fmt.Errorf", "renderTerminalText", "terminalTokenStyle"}, + file: "terminal_style.go", + imports: []string{"fmt", "strings"}, + requiredCallees: []string{"renderTerminalText", "terminalTokenStyle"}, }, } for _, closure := range closures { @@ -177,15 +177,10 @@ func assertAgentWorkflowCarrierClosure(t *testing.T, root string) { if closure.file == "agent_workflow_command.go" && explicitReadInputCalls != 1 { t.Fatalf("agent workflow carrier explicit readInput calls=%d want 1", explicitReadInputCalls) } - callees := make([]string, 0, len(calleeSet)) - for callee := range calleeSet { - callees = append(callees, callee) - } - sort.Strings(callees) - expectedCallees := append([]string(nil), closure.callees...) - sort.Strings(expectedCallees) - if !slices.Equal(callees, expectedCallees) { - t.Fatalf("agent workflow carrier callees in %s=%v want %v", closure.file, callees, expectedCallees) + for _, required := range closure.requiredCallees { + if _, ok := calleeSet[required]; !ok { + t.Fatalf("agent workflow carrier %s is missing required owner call %s", closure.file, required) + } } } } diff --git a/internal/app/agent_workflow_version_edge_test.go b/internal/app/agent_workflow_version_edge_test.go index ba7fc35..d07f6cc 100644 --- a/internal/app/agent_workflow_version_edge_test.go +++ b/internal/app/agent_workflow_version_edge_test.go @@ -17,6 +17,7 @@ const agentWorkflowVersionEdgePath = "internal/app/testdata/v0.5-wire-observatio type agentWorkflowVersionEdge struct { AddedCommandContracts []agentWorkflowCommandContract `json:"addedCommandContracts"` + AdditionChangeIDs []string `json:"additionChangeIds"` BreakingChangeIDs []string `json:"breakingChangeIds"` CurrentPublicABISHA256 string `json:"currentPublicAbiSha256"` EdgeID string `json:"edgeId"` @@ -52,6 +53,14 @@ func TestAgentWorkflowVersionEdgeClosesPublicWireAdditions(t *testing.T) { {name: "previous ABI", mutate: func(value *agentWorkflowVersionEdge) { value.PreviousPublicABISHA256 = value.CurrentPublicABISHA256 }}, {name: "command contract", mutate: func(value *agentWorkflowVersionEdge) { value.AddedCommandContracts[0].OutputContractSHA256 += "0" }}, {name: "missing command", mutate: func(value *agentWorkflowVersionEdge) { value.AddedCommandContracts = value.AddedCommandContracts[1:] }}, + {name: "addition owner", mutate: func(value *agentWorkflowVersionEdge) { value.AdditionChangeIDs[0] += ".drift" }}, + {name: "missing addition", mutate: func(value *agentWorkflowVersionEdge) { value.AdditionChangeIDs = value.AdditionChangeIDs[1:] }}, + {name: "reordered additions", mutate: func(value *agentWorkflowVersionEdge) { + value.AdditionChangeIDs[0], value.AdditionChangeIDs[1] = value.AdditionChangeIDs[1], value.AdditionChangeIDs[0] + }}, + {name: "surplus addition", mutate: func(value *agentWorkflowVersionEdge) { + value.AdditionChangeIDs = append(value.AdditionChangeIDs, "proofkit.surplus") + }}, {name: "breaking owner", mutate: func(value *agentWorkflowVersionEdge) { value.BreakingChangeIDs[0] += ".drift" }}, } for _, mutant := range mutants { @@ -79,7 +88,7 @@ func readAgentWorkflowVersionEdge(t *testing.T) agentWorkflowVersionEdge { if !ok { t.Fatal("agent workflow version edge must be an object") } - assertExactObjectKeys(t, root, []string{"addedCommandContracts", "breakingChangeIds", "currentPublicAbiSha256", "edgeId", "evidenceClass", "nonClaims", "previousPublicAbiSha256", "previousVersion", "schemaVersion", "version"}, "agent workflow version edge") + assertExactObjectKeys(t, root, []string{"addedCommandContracts", "additionChangeIds", "breakingChangeIds", "currentPublicAbiSha256", "edgeId", "evidenceClass", "nonClaims", "previousPublicAbiSha256", "previousVersion", "schemaVersion", "version"}, "agent workflow version edge") commandContracts, ok := root["addedCommandContracts"].([]any) if !ok { t.Fatal("agent workflow version edge addedCommandContracts must be an array") @@ -115,6 +124,13 @@ func validateAgentWorkflowVersionEdge(record agentWorkflowVersionEdge, releaseRe if !slices.Equal(record.AddedCommandContracts, expectedCommands) { return fmt.Errorf("version-edge added command contracts are not exact") } + additions := make([]string, 0, len(releaseRecord.Additions)) + for _, change := range releaseRecord.Additions { + additions = append(additions, change.ChangeID) + } + if !slices.Equal(record.AdditionChangeIDs, additions) { + return fmt.Errorf("version-edge addition owners are not exact") + } breaking := make([]string, 0, len(releaseRecord.BreakingChanges)) for _, change := range releaseRecord.BreakingChanges { breaking = append(breaking, change.ChangeID) @@ -130,6 +146,7 @@ func validateAgentWorkflowVersionEdge(record agentWorkflowVersionEdge, releaseRe func cloneAgentWorkflowVersionEdge(record agentWorkflowVersionEdge) agentWorkflowVersionEdge { record.AddedCommandContracts = append([]agentWorkflowCommandContract(nil), record.AddedCommandContracts...) + record.AdditionChangeIDs = append([]string(nil), record.AdditionChangeIDs...) record.BreakingChangeIDs = append([]string(nil), record.BreakingChangeIDs...) record.NonClaims = append([]string(nil), record.NonClaims...) return record diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index 010c367..c41efc6 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -23,7 +23,7 @@ import ( ) const ( - cliContractPublicABISHA256 = "0ca5482ce0300d2e3b4b44d818e066539ebf535771c197800f56403a9d2f1097" + cliContractPublicABISHA256 = "757a547c72d2a5e274f8f00bc58adc665cc4495a268fd3e48ce3a6fa0c06f22c" 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 fb0aeca..0e9ab06 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 = "5d83a366969941d31e7e285abf7e049b9bfd022477fbe7593efdef7d38b5be5e" +const commandContractSourceSHA256 = "5fc85982c751d3a384193e57a59a6eedec5a1b0dce3b58bcb143228a68070427" type generatedCommandContractMetadata struct { InputContractSHA256 string @@ -34,7 +34,7 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "help": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "", FlagChoices: map[string][]string{}}, "impact": {InputContractSHA256: "sha256:41d3107414837955ee408d5ce94949a4c1a6b76f6949e6c1dc224bd06f6b09bc", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.impact.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:73066e9a5ca48f21936111ffb7223900fb629875997f4e7b16d7fef9c4177972", FlagChoices: map[string][]string{}}, "init": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:3e59a3002327c759e5e747f8baacaa63a4d6784e1a1c520f0a54e01af3f2faa0", FlagChoices: map[string][]string{}}, - "json-report-cli-adapter-source": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:dbde6809a1702e1161be48a85da1d0971f55be96c0f8432778dd5d5ce757c3b0", FlagChoices: map[string][]string{}}, + "json-report-cli-adapter-source": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:123ba4fdaa25c23cb3a8225252de29ce136c32a1a05563fb411999bddc2c7e4b", FlagChoices: map[string][]string{}}, "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{}}, "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{}}, "native-evidence-guidance": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:c1d23df574e948ea7160931f53790a6d133ae12eeceefe9e5fa15430d653ff7e", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}}, diff --git a/internal/app/testdata/v0.5-wire-observations.json b/internal/app/testdata/v0.5-wire-observations.json index df69d3d..1bdb75e 100644 --- a/internal/app/testdata/v0.5-wire-observations.json +++ b/internal/app/testdata/v0.5-wire-observations.json @@ -5,7 +5,7 @@ "version": "0.5.0", "evidenceClass": "owner_authored_frozen_version_edge_observation", "previousPublicAbiSha256": "sha256:fc03740aea9e7f525a4388e5d7f557cde07e11b0db0c05101fe937c28a1129d9", - "currentPublicAbiSha256": "sha256:0ca5482ce0300d2e3b4b44d818e066539ebf535771c197800f56403a9d2f1097", + "currentPublicAbiSha256": "sha256:757a547c72d2a5e274f8f00bc58adc665cc4495a268fd3e48ce3a6fa0c06f22c", "addedCommandContracts": [ { "command": "change-workflow-plan", @@ -18,6 +18,11 @@ "outputContractSha256": "sha256:c1d23df574e948ea7160931f53790a6d133ae12eeceefe9e5fa15430d653ff7e" } ], + "additionChangeIds": [ + "proofkit.agent-workflow.change-planner", + "proofkit.agent-workflow.native-evidence-guidance", + "proofkit.release.cross-carrier-binary-identity" + ], "breakingChangeIds": [ "proofkit.agent-envelope.local-identity-closure", "proofkit.diagnostic.bounded-error-boundary", diff --git a/internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source.go b/internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source.go index 28a8718..f9c9375 100644 --- a/internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source.go +++ b/internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source.go @@ -8,6 +8,7 @@ import ( const LanguageTypeScript = "typescript" const FormatJSON = "json" +const TypeScriptGeneratorID = "proofkit.json-report-cli-adapter-source.typescript.v2" var exportedSymbols = []string{ "ProofkitCommandRunOptions", @@ -65,7 +66,7 @@ func (bundle Bundle) JSONValue() map[string]any { return map[string]any{ "schemaVersion": 1, "artifactKind": "proofkit.json-report-cli-adapter-source", - "generatorId": "proofkit.json-report-cli-adapter-source.typescript.v2", + "generatorId": TypeScriptGeneratorID, "language": bundle.Language, "format": bundle.Format, "sourceFileName": "proofkit-json-report-cli-adapter.ts", @@ -344,24 +345,29 @@ function isProofkitScalarString(value: string): boolean { if (unit >= 0xd800 && unit <= 0xdbff) { const low = value.charCodeAt(index + 1); if (!(low >= 0xdc00 && low <= 0xdfff)) { - return false; + return false; } index += 1; } else if (unit >= 0xdc00 && unit <= 0xdfff) { - return false; + return false; } } - return true; + return true; } function compareProofkitScalarStrings(left: string, right: string): number { - const leftScalars = Array.from(left, (character) => character.codePointAt(0) as number); - const rightScalars = Array.from(right, (character) => character.codePointAt(0) as number); - const sharedLength = Math.min(leftScalars.length, rightScalars.length); - for (let index = 0; index < sharedLength; index += 1) { - if (leftScalars[index] !== rightScalars[index]) return (leftScalars[index] as number) - (rightScalars[index] as number); + let leftIndex = 0; + let rightIndex = 0; + while (leftIndex < left.length && rightIndex < right.length) { + const leftScalar = left.codePointAt(leftIndex) as number; + const rightScalar = right.codePointAt(rightIndex) as number; + if (leftScalar !== rightScalar) return leftScalar - rightScalar; + leftIndex += leftScalar > 0xffff ? 2 : 1; + rightIndex += rightScalar > 0xffff ? 2 : 1; } - return leftScalars.length - rightScalars.length; + if (leftIndex < left.length) return 1; + if (rightIndex < right.length) return -1; + return 0; } function isProofkitUnsafeScalar(value: number): boolean { diff --git a/internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source_test.go b/internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source_test.go index f144fdb..80206a6 100644 --- a/internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source_test.go +++ b/internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source_test.go @@ -15,7 +15,7 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" ) -const expectedTypeScriptSourceSha256 = "sha256:c1c0c4608b3dee6520c4de90a24adcad342f0aeeab02fb8face911e55d00f064" +const expectedTypeScriptSourceSha256 = "sha256:a171cc1b95c6078b7190ac50fc9fd298db8f42bfc9b65bbb67fa77d63dc04a93" func TestBuildEmitsDeterministicTypeScriptSourceBundle(t *testing.T) { if !slices.IsSorted(exportedSymbols) { @@ -178,7 +178,7 @@ func TestGeneratedTypeScriptAdapterExecutesCoreSemantics(t *testing.T) { if err := os.WriteFile(fakeProofkitPath, []byte(fakeProofkitBinarySource), 0o755); err != nil { t.Fatalf("write fake proofkit: %v", err) } - if err := os.WriteFile(harnessPath, []byte(generatedAdapterHarnessSource(fakeProofkitPath)), 0o644); err != nil { + if err := os.WriteFile(harnessPath, []byte(generatedAdapterHarnessSource(t, fakeProofkitPath)), 0o644); err != nil { t.Fatalf("write harness: %v", err) } command := exec.Command(nodePath, "--experimental-strip-types", harnessPath) @@ -274,7 +274,8 @@ process.stdin.on("end", () => { }); ` -func generatedAdapterHarnessSource(fakeProofkitPath string) string { +func generatedAdapterHarnessSource(t *testing.T, fakeProofkitPath string) string { + t.Helper() return `import assert from "node:assert/strict"; import { existsSync, mkdirSync, readFileSync, readdirSync, symlinkSync, writeFileSync } from "node:fs"; import { resolve } from "node:path"; @@ -346,16 +347,7 @@ for (const [value, expected] of [ assert.equal(encoded, expected); assert.equal(parseProofkitJsonStrict(encoded).value, value); } -const unsafeScalarRangesForTest = [ - [0x000000, 0x00001f, 1], [0x00007f, 0x00009f, 1], [0x0000ad, 0x000600, 1363], - [0x000601, 0x000605, 1], [0x00061c, 0x0006dd, 193], [0x00070f, 0x000890, 385], - [0x000891, 0x0008e2, 81], [0x00180e, 0x00200b, 2045], [0x00200c, 0x00200f, 1], - [0x002028, 0x002028, 1], [0x002029, 0x002029, 1], [0x00202a, 0x00202e, 1], - [0x002060, 0x002064, 1], [0x002066, 0x00206f, 1], [0x00feff, 0x00fff9, 250], - [0x00fffa, 0x00fffb, 1], [0x0110bd, 0x0110cd, 16], [0x013430, 0x01343f, 1], - [0x01bca0, 0x01bca3, 1], [0x01d173, 0x01d17a, 1], [0x0e0001, 0x0e0020, 31], - [0x0e0021, 0x0e007f, 1], -]; +const unsafeScalarRangesForTest = ` + unsafeScalarRangesLiteral(t) + `; const unsafeScalarsForTest = new Set(); for (const [start, end, step] of unsafeScalarRangesForTest) { for (let value = start; value <= end; value += step) unsafeScalarsForTest.add(value); @@ -663,6 +655,40 @@ console.log("generated adapter semantics ok"); ` } +func unsafeScalarRangesLiteral(t *testing.T) string { + t.Helper() + type corpusRange struct { + Start int `json:"start"` + End int `json:"end"` + Step int `json:"step"` + } + type corpus struct { + SchemaVersion int `json:"schemaVersion"` + Ranges []corpusRange `json:"ranges"` + } + path := filepath.Join("..", "..", "kernel", "unicodepolicy", "testdata", "unsafe-scalar-ranges.v1.json") + content, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + var owner corpus + if err := json.Unmarshal(content, &owner); err != nil { + t.Fatal(err) + } + if owner.SchemaVersion != 1 || len(owner.Ranges) == 0 { + t.Fatal("Unicode owner corpus identity is invalid") + } + projection := make([][3]int, 0, len(owner.Ranges)) + for _, item := range owner.Ranges { + projection = append(projection, [3]int{item.Start, item.End, item.Step}) + } + encoded, err := json.Marshal(projection) + if err != nil { + t.Fatal(err) + } + return string(encoded) +} + func redactionFixturesLiteral() string { type fixture struct { Input string `json:"input"` diff --git a/internal/command/stackpreset/preset_ids_generated.go b/internal/command/stackpreset/preset_ids_generated.go index 27bb020..e4133fe 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 = "5d83a366969941d31e7e285abf7e049b9bfd022477fbe7593efdef7d38b5be5e" +const presetContractSourceSHA256 = "5fc85982c751d3a384193e57a59a6eedec5a1b0dce3b58bcb143228a68070427" var presetIDs = []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"} diff --git a/internal/kernel/unicodepolicy/testdata/unsafe-scalar-ranges.v1.json b/internal/kernel/unicodepolicy/testdata/unsafe-scalar-ranges.v1.json new file mode 100644 index 0000000..9ae4b19 --- /dev/null +++ b/internal/kernel/unicodepolicy/testdata/unsafe-scalar-ranges.v1.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": 1, + "unicodeVersion": "15.0.0", + "tableSha256": "16873769cae138cd0129781932e487ed558510e52a0237dcfcb558081994cfd9", + "ranges": [ + {"category": "Cc", "start": 0, "end": 31, "step": 1}, + {"category": "Cc", "start": 127, "end": 159, "step": 1}, + {"category": "Cf", "start": 173, "end": 1536, "step": 1363}, + {"category": "Cf", "start": 1537, "end": 1541, "step": 1}, + {"category": "Cf", "start": 1564, "end": 1757, "step": 193}, + {"category": "Cf", "start": 1807, "end": 2192, "step": 385}, + {"category": "Cf", "start": 2193, "end": 2274, "step": 81}, + {"category": "Cf", "start": 6158, "end": 8203, "step": 2045}, + {"category": "Cf", "start": 8204, "end": 8207, "step": 1}, + {"category": "Zl", "start": 8232, "end": 8232, "step": 1}, + {"category": "Zp", "start": 8233, "end": 8233, "step": 1}, + {"category": "Cf", "start": 8234, "end": 8238, "step": 1}, + {"category": "Cf", "start": 8288, "end": 8292, "step": 1}, + {"category": "Cf", "start": 8294, "end": 8303, "step": 1}, + {"category": "Cf", "start": 65279, "end": 65529, "step": 250}, + {"category": "Cf", "start": 65530, "end": 65531, "step": 1}, + {"category": "Cf", "start": 69821, "end": 69837, "step": 16}, + {"category": "Cf", "start": 78896, "end": 78911, "step": 1}, + {"category": "Cf", "start": 113824, "end": 113827, "step": 1}, + {"category": "Cf", "start": 119155, "end": 119162, "step": 1}, + {"category": "Cf", "start": 917505, "end": 917536, "step": 31}, + {"category": "Cf", "start": 917537, "end": 917631, "step": 1} + ] +} diff --git a/internal/kernel/unicodepolicy/unicodepolicy_test.go b/internal/kernel/unicodepolicy/unicodepolicy_test.go index a8b4b8c..b5bde41 100644 --- a/internal/kernel/unicodepolicy/unicodepolicy_test.go +++ b/internal/kernel/unicodepolicy/unicodepolicy_test.go @@ -2,14 +2,30 @@ package unicodepolicy import ( "crypto/sha256" + "encoding/json" "errors" "fmt" + "os" "slices" "strings" "testing" "unicode" ) +type unsafeScalarCorpus struct { + SchemaVersion int `json:"schemaVersion"` + UnicodeVersion string `json:"unicodeVersion"` + TableSHA256 string `json:"tableSha256"` + Ranges []unsafeScalarCorpusRange `json:"ranges"` +} + +type unsafeScalarCorpusRange struct { + Category string `json:"category"` + Start rune `json:"start"` + End rune `json:"end"` + Step rune `json:"step"` +} + func TestUnsafeScalarTableIdentityAndCardinality(t *testing.T) { var source strings.Builder source.WriteString(UnicodeVersion) @@ -36,6 +52,29 @@ func TestUnsafeScalarTableIdentityAndCardinality(t *testing.T) { } } +func TestUnsafeScalarTableMatchesVersionedOwnerCorpus(t *testing.T) { + content, err := os.ReadFile("testdata/unsafe-scalar-ranges.v1.json") + if err != nil { + t.Fatal(err) + } + var corpus unsafeScalarCorpus + if err := json.Unmarshal(content, &corpus); err != nil { + t.Fatal(err) + } + if corpus.SchemaVersion != 1 || corpus.UnicodeVersion != UnicodeVersion || corpus.TableSHA256 != TableSHA256 { + t.Fatalf("Unicode owner corpus identity is stale: %#v", corpus) + } + if len(corpus.Ranges) != len(unsafeScalarRanges) { + t.Fatalf("Unicode owner corpus range count=%d want %d", len(corpus.Ranges), len(unsafeScalarRanges)) + } + for index, actual := range corpus.Ranges { + expected := unsafeScalarRanges[index] + if actual.Category != expected.category || actual.Start != expected.start || actual.End != expected.end || actual.Step != expected.step { + t.Fatalf("Unicode owner corpus range %d=%#v want %#v", index, actual, expected) + } + } +} + func TestUnsafeScalarClassification(t *testing.T) { for _, value := range []rune{0x0000, 0x007f, 0x0085, 0x00ad, 0x061c, 0x200b, 0x2028, 0x2029, 0xe0001} { if !IsUnsafeScalar(value) { diff --git a/internal/tools/packageverify/main.go b/internal/tools/packageverify/main.go index 0876afa..33e6fd0 100644 --- a/internal/tools/packageverify/main.go +++ b/internal/tools/packageverify/main.go @@ -2491,8 +2491,8 @@ func verifyJSONAdapterSourceSmokeReport(result installedCommandResult, expectedS if report.SourceFileName != "proofkit-json-report-cli-adapter.ts" { return fmt.Errorf("sourceFileName=%s, want proofkit-json-report-cli-adapter.ts", report.SourceFileName) } - if report.GeneratorID != "proofkit.json-report-cli-adapter-source.typescript.v2" { - return fmt.Errorf("generatorId=%s, want proofkit.json-report-cli-adapter-source.typescript.v2", report.GeneratorID) + if report.GeneratorID != jsonreportcliadaptersource.TypeScriptGeneratorID { + return fmt.Errorf("generatorId=%s, want %s", report.GeneratorID, jsonreportcliadaptersource.TypeScriptGeneratorID) } if report.Source != expectedSource { return fmt.Errorf("json adapter source does not match current owner source") diff --git a/internal/tools/releasechange/record_test.go b/internal/tools/releasechange/record_test.go index 971f938..950be99 100644 --- a/internal/tools/releasechange/record_test.go +++ b/internal/tools/releasechange/record_test.go @@ -196,14 +196,14 @@ func TestCurrentChangeRecordNamesReviewedSemanticChanges(t *testing.T) { var currentBreakingChanges = []Change{ {ChangeID: "proofkit.agent-envelope.local-identity-closure", Summary: "Agent envelopes now require unique, pairwise-disjoint command, context, and receipt identities and fail closed by pruning unsafe or ambiguous local targets and references with bounded omissions."}, - {ChangeID: "proofkit.diagnostic.bounded-error-boundary", Summary: "CLI and repository-tool diagnostics now reject malformed or report-unsafe values as a whole, cap admitted text, and bound child-process stdout, stderr, lifetime, and process-group cleanup instead of forwarding raw child errors."}, + {ChangeID: "proofkit.diagnostic.bounded-error-boundary", Summary: "CLI and repository-tool diagnostics now reject malformed or report-unsafe values as a whole, cap admitted text, capture failed child stderr before whole-value sanitization, and keep successful child stderr out of diagnostics; installed-carrier smoke additionally bounds stdout, stderr, lifetime, and process-group cleanup."}, {ChangeID: "proofkit.stable-json.unicode-scalar-v2", Summary: "Go, JavaScript, and generated TypeScript stable JSON now reject non-scalar strings and deterministically escape the pinned Unicode 15 control and format policy; the generated TypeScript adapter identity advances to v2."}, } var currentAdditions = []Change{ {ChangeID: "proofkit.agent-workflow.change-planner", Summary: "Add a bounded change-workflow-plan command for the named optional proofkit.reviewed-change.v1 profile with canonical JSON, derived authority- and witness-gated agent-envelope, plain-text, and capability-gated terminal-color projections."}, {ChangeID: "proofkit.agent-workflow.native-evidence-guidance", Summary: "Add a no-input native-evidence-guidance command that exposes the repository-owned decision template and closed applicability classes as canonical JSON or plain text without executing or approving consumer witnesses."}, - {ChangeID: "proofkit.release.cross-carrier-binary-identity", Summary: "Release assembly now independently decodes final npm and Python archives under fixed bounds and directly requires byte-identical embedded binaries for every release platform."}, + {ChangeID: "proofkit.release.cross-carrier-binary-identity", Summary: "Release assembly now admits final npm, Python, and SBOM artifacts once into a bounded immutable snapshot, independently rejects duplicate archive entries, directly compares embedded platform binaries in bounded chunks, and derives release and SBOM checksums from the same artifact epoch."}, } var currentMigrationSteps = []string{ diff --git a/internal/tools/releasemanifest/artifact_snapshot.go b/internal/tools/releasemanifest/artifact_snapshot.go new file mode 100644 index 0000000..b34deeb --- /dev/null +++ b/internal/tools/releasemanifest/artifact_snapshot.go @@ -0,0 +1,277 @@ +package main + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "sort" +) + +const maxReleaseArtifactBytes int64 = 512 << 20 + +type releaseArtifactSnapshot struct { + root string + bySource map[string]releaseArtifact +} + +type releaseArtifact struct { + filename string + sha256 string + size int64 +} + +func newReleaseArtifactSnapshot(localRecords []packRecord, pythonPackages *pythonPackageSet) (*releaseArtifactSnapshot, error) { + packagePaths, err := optionalGlob(filepath.Join("artifacts", "package", "*.tgz")) + if err != nil { + return nil, err + } + if err := requireExactPathSet(packagePaths, expectedPackageArtifactPaths(localRecords, nil), "release package artifact"); err != nil { + return nil, err + } + wheelPaths, err := optionalGlob(filepath.Join("artifacts", "pypi", "*.whl")) + if err != nil { + return nil, err + } + if err := requireExactPathSet(wheelPaths, expectedPythonWheelPaths(pythonPackages), "release Python wheel artifact"); err != nil { + return nil, err + } + sourcePaths := expectedPackageArtifactPaths(localRecords, pythonPackages) + if len(sourcePaths) == 0 { + return nil, fmt.Errorf("release assets require at least one package artifact") + } + sourcePaths = append(sourcePaths, filepath.Join("artifacts", "release", "sbom.cdx.json")) + sort.Strings(sourcePaths) + root, err := os.MkdirTemp("", "proofkit-release-artifacts-") + if err != nil { + return nil, fmt.Errorf("create release artifact snapshot: %w", err) + } + snapshot := &releaseArtifactSnapshot{root: root, bySource: make(map[string]releaseArtifact, len(sourcePaths))} + filenames := make(map[string]struct{}, len(sourcePaths)) + for _, sourcePath := range sourcePaths { + filename := filepath.Base(sourcePath) + if _, duplicate := filenames[filename]; duplicate { + return nil, errors.Join(fmt.Errorf("release assets contain duplicate flattened filenames"), snapshot.Close()) + } + filenames[filename] = struct{}{} + artifact, err := snapshotReleaseArtifact(root, sourcePath) + if err != nil { + return nil, errors.Join(err, snapshot.Close()) + } + snapshot.bySource[sourcePath] = artifact + } + return snapshot, nil +} + +func (snapshot *releaseArtifactSnapshot) Close() error { + if snapshot == nil || snapshot.root == "" { + return nil + } + if err := os.RemoveAll(snapshot.root); err != nil { + return fmt.Errorf("remove release artifact snapshot: %w", err) + } + snapshot.root = "" + snapshot.bySource = nil + return nil +} + +func (snapshot *releaseArtifactSnapshot) VerifyCrossCarrierBinaryIdentity(localRecords []packRecord, pythonPackages *pythonPackageSet) error { + if err := snapshot.requireOpen(); err != nil { + return err + } + return verifyCrossCarrierBinaryIdentity( + filepath.Join(snapshot.root, "artifacts", "package"), + filepath.Join(snapshot.root, "artifacts", "pypi"), + localRecords, + pythonPackages, + ) +} + +func (snapshot *releaseArtifactSnapshot) ReleaseEvidence(localRecords []packRecord, pythonPackages *pythonPackageSet) ([]assetEvidence, []string, []string, error) { + if err := snapshot.requireOpen(); err != nil { + return nil, nil, nil, err + } + subjectPaths := expectedPackageArtifactPaths(localRecords, pythonPackages) + assetPaths := append([]string(nil), subjectPaths...) + assetPaths = append(assetPaths, filepath.Join("artifacts", "release", "sbom.cdx.json")) + sort.Strings(assetPaths) + sort.Strings(subjectPaths) + assets := make([]assetEvidence, 0, len(assetPaths)) + checksums := make([]string, 0, len(assetPaths)) + subjectChecksums := make([]string, 0, len(subjectPaths)) + for _, sourcePath := range assetPaths { + artifact, ok := snapshot.bySource[sourcePath] + if !ok { + return nil, nil, nil, fmt.Errorf("release artifact snapshot lacks an expected asset") + } + assets = append(assets, assetEvidence{Filename: artifact.filename, Sha256: artifact.sha256}) + checksums = append(checksums, fmt.Sprintf("%s %s", artifact.sha256, artifact.filename)) + } + for _, sourcePath := range subjectPaths { + artifact, ok := snapshot.bySource[sourcePath] + if !ok { + return nil, nil, nil, fmt.Errorf("release artifact snapshot lacks an expected SBOM subject") + } + subjectChecksums = append(subjectChecksums, fmt.Sprintf("%s %s", artifact.sha256, artifact.filename)) + } + return assets, checksums, subjectChecksums, nil +} + +func (snapshot *releaseArtifactSnapshot) RevalidateSources() error { + if err := snapshot.requireOpen(); err != nil { + return err + } + paths := make([]string, 0, len(snapshot.bySource)) + for path := range snapshot.bySource { + paths = append(paths, path) + } + sort.Strings(paths) + for _, path := range paths { + actual, err := stableReleaseArtifactIdentity(path) + if err != nil { + return err + } + expected := snapshot.bySource[path] + if actual.size != expected.size || actual.sha256 != expected.sha256 { + return fmt.Errorf("release artifact source changed after immutable admission") + } + } + return nil +} + +func (snapshot *releaseArtifactSnapshot) requireOpen() error { + if snapshot == nil || snapshot.root == "" || snapshot.bySource == nil { + return fmt.Errorf("release artifact snapshot is closed") + } + return nil +} + +func snapshotReleaseArtifact(snapshotRoot string, sourcePath string) (releaseArtifact, error) { + clean, err := admittedReleaseArtifactPath(sourcePath) + if err != nil { + return releaseArtifact{}, err + } + source, before, root, err := openStableReleaseArtifact(clean) + if err != nil { + return releaseArtifact{}, err + } + defer root.Close() + defer source.Close() + destination := filepath.Join(snapshotRoot, clean) + if err := os.MkdirAll(filepath.Dir(destination), 0o700); err != nil { + return releaseArtifact{}, fmt.Errorf("create release snapshot directory: %w", err) + } + target, err := os.OpenFile(destination, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) + if err != nil { + return releaseArtifact{}, fmt.Errorf("create release snapshot file: %w", err) + } + hash := sha256.New() + written, copyErr := io.Copy(io.MultiWriter(target, hash), io.LimitReader(source, maxReleaseArtifactBytes+1)) + syncErr := target.Sync() + closeErr := target.Close() + if copyErr != nil { + return releaseArtifact{}, fmt.Errorf("copy release artifact snapshot: %w", copyErr) + } + if syncErr != nil { + return releaseArtifact{}, fmt.Errorf("sync release artifact snapshot: %w", syncErr) + } + if closeErr != nil { + return releaseArtifact{}, fmt.Errorf("close release artifact snapshot: %w", closeErr) + } + if written > maxReleaseArtifactBytes { + return releaseArtifact{}, fmt.Errorf("release artifact exceeds the byte limit") + } + after, err := source.Stat() + pathAfter, pathErr := root.Lstat(clean) + directoryErr := admitReleaseArtifactDirectories(root, clean) + if err != nil || pathErr != nil || directoryErr != nil || pathAfter.Mode()&os.ModeSymlink != 0 || !os.SameFile(before, after) || !os.SameFile(after, pathAfter) || before.Size() != after.Size() || after.Size() != written || !before.ModTime().Equal(after.ModTime()) { + return releaseArtifact{}, fmt.Errorf("release artifact changed during immutable admission") + } + return releaseArtifact{ + filename: filepath.Base(clean), + sha256: hex.EncodeToString(hash.Sum(nil)), + size: written, + }, nil +} + +func stableReleaseArtifactIdentity(sourcePath string) (releaseArtifact, error) { + clean, err := admittedReleaseArtifactPath(sourcePath) + if err != nil { + return releaseArtifact{}, err + } + file, before, root, err := openStableReleaseArtifact(clean) + if err != nil { + return releaseArtifact{}, err + } + defer root.Close() + defer file.Close() + hash := sha256.New() + size, err := io.Copy(hash, io.LimitReader(file, maxReleaseArtifactBytes+1)) + if err != nil { + return releaseArtifact{}, fmt.Errorf("revalidate release artifact: %w", err) + } + if size > maxReleaseArtifactBytes { + return releaseArtifact{}, fmt.Errorf("release artifact exceeds the byte limit") + } + after, err := file.Stat() + pathAfter, pathErr := root.Lstat(clean) + directoryErr := admitReleaseArtifactDirectories(root, clean) + if err != nil || pathErr != nil || directoryErr != nil || pathAfter.Mode()&os.ModeSymlink != 0 || !os.SameFile(before, after) || !os.SameFile(after, pathAfter) || before.Size() != after.Size() || after.Size() != size || !before.ModTime().Equal(after.ModTime()) { + return releaseArtifact{}, fmt.Errorf("release artifact changed during revalidation") + } + return releaseArtifact{sha256: hex.EncodeToString(hash.Sum(nil)), size: size}, nil +} + +func admittedReleaseArtifactPath(path string) (string, error) { + clean := filepath.Clean(path) + if clean == "." || !filepath.IsLocal(clean) || clean != path { + return "", fmt.Errorf("release artifact path must be normalized and repository-relative") + } + return clean, nil +} + +func openStableReleaseArtifact(path string) (*os.File, fs.FileInfo, *os.Root, error) { + root, err := os.OpenRoot(".") + if err != nil { + return nil, nil, nil, fmt.Errorf("open release artifact root: %w", err) + } + if err := admitReleaseArtifactDirectories(root, path); err != nil { + root.Close() + return nil, nil, nil, err + } + before, err := root.Lstat(path) + if err != nil { + root.Close() + return nil, nil, nil, fmt.Errorf("inspect release artifact: %w", err) + } + if before.Mode()&os.ModeSymlink != 0 || !before.Mode().IsRegular() || before.Size() > maxReleaseArtifactBytes { + root.Close() + return nil, nil, nil, fmt.Errorf("release artifact must be a bounded regular non-symlink file") + } + file, err := root.Open(path) + if err != nil { + root.Close() + return nil, nil, nil, fmt.Errorf("open release artifact: %w", err) + } + opened, err := file.Stat() + if err != nil || !os.SameFile(before, opened) || !opened.Mode().IsRegular() { + file.Close() + root.Close() + return nil, nil, nil, fmt.Errorf("release artifact changed before admission") + } + return file, opened, root, nil +} + +func admitReleaseArtifactDirectories(root *os.Root, path string) error { + for current := filepath.Dir(path); current != "."; current = filepath.Dir(current) { + info, err := root.Lstat(current) + if err != nil || info.Mode()&os.ModeSymlink != 0 || !info.IsDir() { + return fmt.Errorf("release artifact path traverses an inadmissible directory") + } + } + return nil +} diff --git a/internal/tools/releasemanifest/artifact_snapshot_test.go b/internal/tools/releasemanifest/artifact_snapshot_test.go new file mode 100644 index 0000000..43a032c --- /dev/null +++ b/internal/tools/releasemanifest/artifact_snapshot_test.go @@ -0,0 +1,89 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/releaseplatform" +) + +func TestReleaseArtifactSnapshotOwnsOneImmutableEpoch(t *testing.T) { + root := t.TempDir() + oldWorkingDirectory, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + if err := os.Chdir(root); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := os.Chdir(oldWorkingDirectory); err != nil { + t.Fatalf("restore working directory: %v", err) + } + }) + for _, directory := range []string{"artifacts/package", "artifacts/pypi", "artifacts/release"} { + if err := os.MkdirAll(directory, 0o700); err != nil { + t.Fatal(err) + } + } + records := []packRecord{{Filename: "proofkit.tgz"}} + writeNPMCarrier(t, filepath.Join("artifacts", "package", records[0].Filename), "") + packages := &pythonPackageSet{Packages: make([]pythonWheelRecord, 0, len(releaseplatform.Targets()))} + for _, target := range releaseplatform.Targets() { + filename := target.PlatformSuffix + ".whl" + writeWheelCarrier(t, filepath.Join("artifacts", "pypi", filename), carrierBinary(target.PlatformSuffix)) + packages.Packages = append(packages.Packages, pythonWheelRecord{Filename: filename, PlatformSuffix: target.PlatformSuffix}) + } + writeFile(t, filepath.Join("artifacts", "release", "sbom.cdx.json"), "{}") + + snapshot, err := newReleaseArtifactSnapshot(records, packages) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := snapshot.Close(); err != nil { + t.Fatal(err) + } + }) + if err := snapshot.VerifyCrossCarrierBinaryIdentity(records, packages); err != nil { + t.Fatal(err) + } + assets, checksums, subjectChecksums, err := snapshot.ReleaseEvidence(records, packages) + if err != nil { + t.Fatal(err) + } + if len(assets) != len(releaseplatform.Targets())+2 || len(checksums) != len(assets) || len(subjectChecksums) != len(assets)-1 { + t.Fatalf("snapshot evidence sizes=(%d,%d,%d)", len(assets), len(checksums), len(subjectChecksums)) + } + for _, subject := range subjectChecksums { + parts := strings.SplitN(subject, " ", 2) + if len(parts) != 2 || !containsChecksumLine(checksums, subject) { + t.Fatalf("SBOM subject checksum does not reuse the admitted asset identity: %q", subject) + } + } + + writeFile(t, filepath.Join("artifacts", "package", records[0].Filename), "replacement") + if err := snapshot.RevalidateSources(); err == nil || !strings.Contains(err.Error(), "changed after immutable admission") { + t.Fatalf("same-path replacement was not rejected: %v", err) + } + if err := snapshot.VerifyCrossCarrierBinaryIdentity(records, packages); err != nil { + t.Fatalf("immutable snapshot changed after source replacement: %v", err) + } + if err := snapshot.Close(); err != nil { + t.Fatal(err) + } + if _, _, _, err := snapshot.ReleaseEvidence(records, packages); err == nil || !strings.Contains(err.Error(), "snapshot is closed") { + t.Fatalf("closed snapshot remained readable: %v", err) + } +} + +func containsChecksumLine(lines []string, expected string) bool { + for _, line := range lines { + if line == expected { + return true + } + } + return false +} diff --git a/internal/tools/releasemanifest/carrier_identity.go b/internal/tools/releasemanifest/carrier_identity.go index ac1f795..bda416b 100644 --- a/internal/tools/releasemanifest/carrier_identity.go +++ b/internal/tools/releasemanifest/carrier_identity.go @@ -5,6 +5,7 @@ import ( "archive/zip" "bytes" "compress/gzip" + "errors" "fmt" "io" "os" @@ -20,14 +21,21 @@ const ( wheelBinaryEntry = "agentic_proofkit/bin/agentic-proofkit" ) -func verifyCrossCarrierBinaryIdentity(packageDir string, pythonDir string, localRecords []packRecord, pythonPackages *pythonPackageSet) error { +func verifyCrossCarrierBinaryIdentity(packageDir string, pythonDir string, localRecords []packRecord, pythonPackages *pythonPackageSet) (returnErr error) { if pythonPackages == nil { return nil } if len(localRecords) != 1 { return fmt.Errorf("cross-carrier identity requires exactly one npm package artifact") } - npmBinaries, err := readNPMCarrierBinaries(filepath.Join(packageDir, localRecords[0].Filename)) + scratch, err := os.MkdirTemp("", "proofkit-carrier-binaries-") + if err != nil { + return fmt.Errorf("create cross-carrier scratch directory: %w", err) + } + defer func() { + returnErr = errors.Join(returnErr, os.RemoveAll(scratch)) + }() + npmBinaries, err := extractNPMCarrierBinaries(filepath.Join(packageDir, localRecords[0].Filename), scratch) if err != nil { return fmt.Errorf("decode npm package carrier: %w", err) } @@ -51,18 +59,23 @@ func verifyCrossCarrierBinaryIdentity(packageDir string, pythonDir string, local if !exists { return fmt.Errorf("python package carrier is missing a release-platform wheel") } - wheelBinary, err := readWheelCarrierBinary(filepath.Join(pythonDir, wheel.Filename)) + equal, err := wheelCarrierBinaryEquals(filepath.Join(pythonDir, wheel.Filename), npmBinary) if err != nil { return fmt.Errorf("decode python wheel carrier: %w", err) } - if !bytes.Equal(npmBinary, wheelBinary) { + if !equal { return fmt.Errorf("npm and python package carriers contain different release-platform binary bytes") } } return nil } -func readNPMCarrierBinaries(path string) (map[string][]byte, error) { +type extractedCarrierBinary struct { + path string + size int64 +} + +func extractNPMCarrierBinaries(path string, scratch string) (map[string]extractedCarrierBinary, error) { file, err := openBoundedCarrier(path) if err != nil { return nil, err @@ -78,7 +91,8 @@ func readNPMCarrierBinaries(path string) (map[string][]byte, error) { for _, target := range releaseplatform.Targets() { expected[target.PackageTarEntry] = target.PlatformSuffix } - binaries := make(map[string][]byte, len(expected)) + binaries := make(map[string]extractedCarrierBinary, len(expected)) + seenNames := make(map[string]struct{}) var totalSize int64 entryCount := 0 for { @@ -93,6 +107,10 @@ func readNPMCarrierBinaries(path string) (map[string][]byte, error) { if entryCount > maxCarrierArchiveEntries { return nil, fmt.Errorf("npm package carrier exceeds the entry limit") } + if _, duplicate := seenNames[header.Name]; duplicate { + return nil, fmt.Errorf("npm package carrier contains duplicate entries") + } + seenNames[header.Name] = struct{}{} if header.Size < 0 || header.Size > maxCarrierArchiveBytes-totalSize { return nil, fmt.Errorf("npm package carrier exceeds the uncompressed byte limit") } @@ -107,11 +125,23 @@ func readNPMCarrierBinaries(path string) (map[string][]byte, error) { if header.Typeflag != tar.TypeReg { return nil, fmt.Errorf("npm package carrier release-platform binary is not a regular file") } - content, err := readBoundedCarrierMember(tarReader, header.Size) + if header.Size > maxCarrierBinaryBytes { + return nil, fmt.Errorf("package carrier binary exceeds the byte limit") + } + destination := filepath.Join(scratch, platformSuffix) + binary, err := os.OpenFile(destination, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) if err != nil { return nil, err } - binaries[platformSuffix] = content + written, copyErr := io.CopyN(binary, tarReader, header.Size) + closeErr := binary.Close() + if err := errors.Join(copyErr, closeErr); err != nil { + return nil, err + } + if written != header.Size { + return nil, fmt.Errorf("package carrier binary size differs from its archive header") + } + binaries[platformSuffix] = extractedCarrierBinary{path: destination, size: written} } if len(binaries) != len(expected) { return nil, fmt.Errorf("npm package carrier lacks exact release-platform binary closure") @@ -119,55 +149,98 @@ func readNPMCarrierBinaries(path string) (map[string][]byte, error) { return binaries, nil } -func readWheelCarrierBinary(path string) ([]byte, error) { +func wheelCarrierBinaryEquals(path string, expected extractedCarrierBinary) (bool, error) { info, err := os.Stat(path) if err != nil { - return nil, err + return false, err } if !info.Mode().IsRegular() || info.Size() > maxCarrierArchiveBytes { - return nil, fmt.Errorf("python wheel carrier is not an admitted regular archive") + return false, fmt.Errorf("python wheel carrier is not an admitted regular archive") } reader, err := zip.OpenReader(path) if err != nil { - return nil, err + return false, err } defer reader.Close() if len(reader.File) > maxCarrierArchiveEntries { - return nil, fmt.Errorf("python wheel carrier exceeds the entry limit") + return false, fmt.Errorf("python wheel carrier exceeds the entry limit") } seen := map[string]struct{}{} - var binary []byte + found := false + equal := false for _, entry := range reader.File { if _, duplicate := seen[entry.Name]; duplicate { - return nil, fmt.Errorf("python wheel carrier contains duplicate entries") + return false, fmt.Errorf("python wheel carrier contains duplicate entries") } seen[entry.Name] = struct{}{} if entry.Name != wheelBinaryEntry { continue } if entry.Mode()&os.ModeSymlink != 0 || !entry.FileInfo().Mode().IsRegular() { - return nil, fmt.Errorf("python wheel carrier binary is not a regular file") + return false, fmt.Errorf("python wheel carrier binary is not a regular file") } if entry.UncompressedSize64 > uint64(maxCarrierBinaryBytes) { - return nil, fmt.Errorf("python wheel carrier binary exceeds the byte limit") + return false, fmt.Errorf("python wheel carrier binary exceeds the byte limit") + } + if found { + return false, fmt.Errorf("python wheel carrier contains duplicate embedded binaries") + } + found = true + if int64(entry.UncompressedSize64) != expected.size { + continue } member, err := entry.Open() if err != nil { - return nil, err + return false, err } - binary, err = readBoundedCarrierMember(member, int64(entry.UncompressedSize64)) - closeErr := member.Close() + expectedFile, err := os.Open(expected.path) if err != nil { - return nil, err + member.Close() + return false, err + } + equal, err = equalCarrierStreams(member, expectedFile) + memberCloseErr := member.Close() + expectedCloseErr := expectedFile.Close() + if err != nil { + return false, err + } + if memberCloseErr != nil { + return false, memberCloseErr } - if closeErr != nil { - return nil, closeErr + if expectedCloseErr != nil { + return false, expectedCloseErr } } - if binary == nil { - return nil, fmt.Errorf("python wheel carrier lacks its embedded binary") + if !found { + return false, fmt.Errorf("python wheel carrier lacks its embedded binary") + } + return equal, nil +} + +func equalCarrierStreams(left io.Reader, right io.Reader) (bool, error) { + leftBuffer := make([]byte, 64*1024) + rightBuffer := make([]byte, len(leftBuffer)) + for { + leftCount, leftErr := io.ReadFull(left, leftBuffer) + rightCount, rightErr := io.ReadFull(right, rightBuffer) + if leftCount != rightCount || !bytes.Equal(leftBuffer[:leftCount], rightBuffer[:rightCount]) { + return false, nil + } + leftDone := leftErr == io.EOF || leftErr == io.ErrUnexpectedEOF + rightDone := rightErr == io.EOF || rightErr == io.ErrUnexpectedEOF + if leftDone || rightDone { + if !leftDone || !rightDone { + return false, nil + } + return true, nil + } + if leftErr != nil { + return false, leftErr + } + if rightErr != nil { + return false, rightErr + } } - return binary, nil } func openBoundedCarrier(path string) (*os.File, error) { @@ -186,17 +259,3 @@ func openBoundedCarrier(path string) (*os.File, error) { } return file, nil } - -func readBoundedCarrierMember(reader io.Reader, declaredSize int64) ([]byte, error) { - if declaredSize < 0 || declaredSize > maxCarrierBinaryBytes { - return nil, fmt.Errorf("package carrier binary exceeds the byte limit") - } - content, err := io.ReadAll(io.LimitReader(reader, maxCarrierBinaryBytes+1)) - if err != nil { - return nil, err - } - if int64(len(content)) != declaredSize { - return nil, fmt.Errorf("package carrier binary size differs from its archive header") - } - return content, nil -} diff --git a/internal/tools/releasemanifest/carrier_identity_test.go b/internal/tools/releasemanifest/carrier_identity_test.go index a9f7781..2b71a88 100644 --- a/internal/tools/releasemanifest/carrier_identity_test.go +++ b/internal/tools/releasemanifest/carrier_identity_test.go @@ -30,12 +30,21 @@ func TestCrossCarrierBinaryIdentityReadsFinalArchives(t *testing.T) { t.Run("duplicate_tar_member", func(t *testing.T) { packageDir, pythonDir, records, packages := writeCarrierFixture(t, "") - writeNPMCarrier(t, filepath.Join(packageDir, records[0].Filename), true) + writeNPMCarrier(t, filepath.Join(packageDir, records[0].Filename), "binary") err := verifyCrossCarrierBinaryIdentity(packageDir, pythonDir, records, packages) - if err == nil || !strings.Contains(err.Error(), "duplicates a release-platform binary") { + if err == nil || !strings.Contains(err.Error(), "contains duplicate entries") { t.Fatalf("duplicate npm binary was not rejected: %v", err) } }) + + t.Run("duplicate_non_binary_tar_member", func(t *testing.T) { + packageDir, pythonDir, records, packages := writeCarrierFixture(t, "") + writeNPMCarrier(t, filepath.Join(packageDir, records[0].Filename), "metadata") + err := verifyCrossCarrierBinaryIdentity(packageDir, pythonDir, records, packages) + if err == nil || !strings.Contains(err.Error(), "contains duplicate entries") { + t.Fatalf("duplicate npm metadata entry was not rejected: %v", err) + } + }) } func writeCarrierFixture(t *testing.T, mutatedWheelSuffix string) (string, string, []packRecord, *pythonPackageSet) { @@ -50,7 +59,7 @@ func writeCarrierFixture(t *testing.T, mutatedWheelSuffix string) (string, strin t.Fatal(err) } records := []packRecord{{Filename: "proofkit.tgz"}} - writeNPMCarrier(t, filepath.Join(packageDir, records[0].Filename), false) + writeNPMCarrier(t, filepath.Join(packageDir, records[0].Filename), "") packages := &pythonPackageSet{Packages: make([]pythonWheelRecord, 0, len(releaseplatform.Targets()))} for _, target := range releaseplatform.Targets() { filename := target.PlatformSuffix + ".whl" @@ -67,7 +76,7 @@ func writeCarrierFixture(t *testing.T, mutatedWheelSuffix string) (string, strin return packageDir, pythonDir, records, packages } -func writeNPMCarrier(t *testing.T, path string, duplicate bool) { +func writeNPMCarrier(t *testing.T, path string, duplicate string) { t.Helper() file, err := os.Create(path) if err != nil { @@ -75,9 +84,20 @@ func writeNPMCarrier(t *testing.T, path string, duplicate bool) { } gzipWriter := gzip.NewWriter(file) tarWriter := tar.NewWriter(gzipWriter) + if duplicate == "metadata" { + for range 2 { + content := []byte("metadata") + if err := tarWriter.WriteHeader(&tar.Header{Name: "package/README.md", Mode: 0o644, Size: int64(len(content)), Typeflag: tar.TypeReg}); err != nil { + t.Fatal(err) + } + if _, err := tarWriter.Write(content); err != nil { + t.Fatal(err) + } + } + } for index, target := range releaseplatform.Targets() { copies := 1 - if duplicate && index == 0 { + if duplicate == "binary" && index == 0 { copies = 2 } for range copies { diff --git a/internal/tools/releasemanifest/main.go b/internal/tools/releasemanifest/main.go index ef62405..6398a4b 100644 --- a/internal/tools/releasemanifest/main.go +++ b/internal/tools/releasemanifest/main.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "errors" "fmt" "io" "os" @@ -202,7 +203,7 @@ func main() { } } -func run() error { +func run() (returnErr error) { manifest, err := readPackageJSON("package.json") if err != nil { return err @@ -264,13 +265,20 @@ func run() error { if err := requirePythonPackagesMatchPackage(manifest, pythonPackages, "local Python package evidence"); err != nil { return err } - if err := verifyCrossCarrierBinaryIdentity(filepath.Join("artifacts", "package"), filepath.Join("artifacts", "pypi"), localRecords, pythonPackages); err != nil { + artifactSnapshot, err := newReleaseArtifactSnapshot(localRecords, pythonPackages) + if err != nil { + return err + } + defer func() { + returnErr = errors.Join(returnErr, artifactSnapshot.Close()) + }() + if err := artifactSnapshot.VerifyCrossCarrierBinaryIdentity(localRecords, pythonPackages); err != nil { return err } if err := requirePyPIRegistryMatchesLocal(pypiRegistry, pythonPackages, manifest); err != nil { return err } - assets, checksums, sbomSubjectChecksums, err := releaseAssets(localRecords, pythonPackages) + assets, checksums, sbomSubjectChecksums, err := artifactSnapshot.ReleaseEvidence(localRecords, pythonPackages) if err != nil { return err } @@ -294,6 +302,9 @@ func run() error { if err != nil { return err } + if err := artifactSnapshot.RevalidateSources(); err != nil { + return err + } releaseDir := filepath.Join("artifacts", "release") if err := os.MkdirAll(releaseDir, 0o755); err != nil { return err @@ -748,51 +759,6 @@ func sortPackRecords(records []packRecord) { }) } -func releaseAssets(localRecords []packRecord, pythonPackages *pythonPackageSet) ([]assetEvidence, []string, []string, error) { - paths := expectedPackageArtifactPaths(localRecords, pythonPackages) - if len(paths) == 0 { - return nil, nil, nil, fmt.Errorf("release assets require at least one package artifact") - } - packagePaths, err := optionalGlob(filepath.Join("artifacts", "package", "*.tgz")) - if err != nil { - return nil, nil, nil, err - } - if err := requireExactPathSet(packagePaths, expectedPackageArtifactPaths(localRecords, nil), "release package artifact"); err != nil { - return nil, nil, nil, err - } - pythonWheelPaths, err := optionalGlob(filepath.Join("artifacts", "pypi", "*.whl")) - if err != nil { - return nil, nil, nil, err - } - if err := requireExactPathSet(pythonWheelPaths, expectedPythonWheelPaths(pythonPackages), "release Python wheel artifact"); err != nil { - return nil, nil, nil, err - } - sbomSubjectPaths := append([]string{}, paths...) - paths = append(paths, filepath.Join("artifacts", "release", "sbom.cdx.json")) - sort.Strings(paths) - sort.Strings(sbomSubjectPaths) - assets := make([]assetEvidence, 0, len(paths)) - checksums := make([]string, 0, len(paths)) - sbomSubjectChecksums := make([]string, 0, len(sbomSubjectPaths)) - for _, path := range paths { - sum, err := fileSHA256(path) - if err != nil { - return nil, nil, nil, err - } - filename := filepath.Base(path) - assets = append(assets, assetEvidence{Filename: filename, Sha256: sum}) - checksums = append(checksums, fmt.Sprintf("%s %s", sum, filename)) - } - for _, path := range sbomSubjectPaths { - sum, err := fileSHA256(path) - if err != nil { - return nil, nil, nil, err - } - sbomSubjectChecksums = append(sbomSubjectChecksums, fmt.Sprintf("%s %s", sum, filepath.Base(path))) - } - return assets, checksums, sbomSubjectChecksums, nil -} - func checksumLines(paths []string) ([]string, error) { sort.Strings(paths) lines := make([]string, 0, len(paths)) diff --git a/internal/tools/releasemanifest/main_test.go b/internal/tools/releasemanifest/main_test.go index fb38c8c..ccc8139 100644 --- a/internal/tools/releasemanifest/main_test.go +++ b/internal/tools/releasemanifest/main_test.go @@ -455,10 +455,13 @@ func TestReleaseAssetsRejectStalePackageArtifacts(t *testing.T) { writeFile(t, "artifacts/pypi/agentic_proofkit-1.2.3-py3-none-any.whl", "wheel") writeFile(t, "artifacts/release/sbom.cdx.json", "{}") - _, _, _, err = releaseAssets( + snapshot, err := newReleaseArtifactSnapshot( []packRecord{{Filename: "agentic-proofkit-1.2.3.tgz", Name: "agentic-proofkit", Version: "1.2.3", Integrity: "sha512-x", Shasum: "abc"}}, &pythonPackageSet{Packages: []pythonWheelRecord{{Filename: "agentic_proofkit-1.2.3-py3-none-any.whl"}}}, ) + if snapshot != nil { + t.Cleanup(func() { _ = snapshot.Close() }) + } if err == nil || !strings.Contains(err.Error(), "unexpected file") { t.Fatalf("releaseAssets() error=%v, want unexpected stale package artifact", err) } diff --git a/internal/tools/workflowsmoke/process.go b/internal/tools/workflowsmoke/process.go index d60b814..ee6513a 100644 --- a/internal/tools/workflowsmoke/process.go +++ b/internal/tools/workflowsmoke/process.go @@ -34,8 +34,8 @@ func VerifyProcess(ctx context.Context, carrier ProcessCarrier) error { }) } -// RunProcess executes one bounded carrier invocation. Output overflow and -// cancellation terminate the whole process group on supported Unix hosts. +// RunProcess executes one bounded carrier invocation and terminates its whole +// process group before returning on every post-start path. func RunProcess(ctx context.Context, carrier ProcessCarrier, invocation Invocation) (Result, error) { if ctx == nil || carrier.Executable == "" { return Result{}, fmt.Errorf("process carrier requires a context and executable") @@ -71,8 +71,12 @@ func RunProcess(ctx context.Context, carrier ProcessCarrier, invocation Invocati } else { command.Stdin = bytes.NewReader(invocation.Input) } - err := command.Run() + runErr := command.Run() + cleanupErr := processgroup.Terminate(command) result := Result{Stdout: stdout.Bytes(), Stderr: stderr.Bytes()} + if cleanupErr != nil { + return Result{}, fmt.Errorf("terminate process carrier group: %w", cleanupErr) + } if stdout.Overflowed() { return Result{}, fmt.Errorf("process carrier stdout exceeds %d bytes", maximumStdoutBytes) } @@ -82,15 +86,15 @@ func RunProcess(ctx context.Context, carrier ProcessCarrier, invocation Invocati if ctx.Err() != nil { return Result{}, fmt.Errorf("process carrier invocation canceled: %w", ctx.Err()) } - if err == nil { + if runErr == nil { return result, nil } var exitError *exec.ExitError - if errors.As(err, &exitError) { + if errors.As(runErr, &exitError) { result.ExitCode = exitError.ExitCode() return result, nil } - return Result{}, fmt.Errorf("process carrier invocation failed: %w", err) + return Result{}, fmt.Errorf("process carrier invocation failed: %w", runErr) } type boundedBuffer struct { diff --git a/internal/tools/workflowsmoke/process_unix_test.go b/internal/tools/workflowsmoke/process_unix_test.go new file mode 100644 index 0000000..e7ea391 --- /dev/null +++ b/internal/tools/workflowsmoke/process_unix_test.go @@ -0,0 +1,48 @@ +//go:build darwin || linux + +package workflowsmoke_test + +import ( + "context" + "errors" + "os" + "strconv" + "strings" + "syscall" + "testing" + "time" + + "github.com/research-engineering/agentic-proofkit/internal/tools/workflowsmoke" +) + +func TestRunProcessTerminatesDescendantsAfterSuccessfulParentExit(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel() + result, err := workflowsmoke.RunProcess(ctx, helperCarrier("spawn-descendant"), workflowsmoke.Invocation{StdinClass: workflowsmoke.StdinBytes}) + if err != nil { + t.Fatal(err) + } + pid, err := strconv.Atoi(strings.TrimSpace(string(result.Stdout))) + if err != nil { + t.Fatalf("decode descendant PID: %v", err) + } + process, err := os.FindProcess(pid) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = process.Kill() }) + deadline := time.Now().Add(2 * time.Second) + for { + err := process.Signal(syscall.Signal(0)) + if errors.Is(err, os.ErrProcessDone) || errors.Is(err, syscall.ESRCH) { + return + } + if err != nil { + t.Fatalf("probe descendant process: %v", err) + } + if time.Now().After(deadline) { + t.Fatalf("descendant process %d remained alive after successful carrier return", pid) + } + time.Sleep(20 * time.Millisecond) + } +} diff --git a/internal/tools/workflowsmoke/workflow_smoke_test.go b/internal/tools/workflowsmoke/workflow_smoke_test.go index 572e543..99cb284 100644 --- a/internal/tools/workflowsmoke/workflow_smoke_test.go +++ b/internal/tools/workflowsmoke/workflow_smoke_test.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "os" + "os/exec" "strings" "testing" "time" @@ -145,6 +146,15 @@ func TestWorkflowSmokeProcessHelper(t *testing.T) { case "read-stdin": _, _ = io.Copy(io.Discard, os.Stdin) case "no-read": + case "spawn-descendant": + child := exec.Command(os.Args[0], "-test.run=^TestWorkflowSmokeProcessHelper$", "--") + child.Env = append(os.Environ(), processHelperMode+"=hang") + if err := child.Start(); err != nil { + _, _ = fmt.Fprintln(os.Stderr, "start descendant failed") + os.Exit(2) + } + _, _ = fmt.Fprintln(os.Stdout, child.Process.Pid) + _ = child.Process.Release() default: _, _ = fmt.Fprintf(os.Stderr, "unsupported helper mode") os.Exit(2) diff --git a/proofkit/cli-contract.v2.json b/proofkit/cli-contract.v2.json index e6782ed..5bf39e1 100644 --- a/proofkit/cli-contract.v2.json +++ b/proofkit/cli-contract.v2.json @@ -1923,7 +1923,7 @@ "rootDefinitionDigest": "sha256:9378c7663485f39089cf6c39a3d2f778e28b5801637229fe9524a9652a7b0737", "nativeSource": { "path": "internal/command/jsonreportcliadaptersource", - "canonicalDigest": "sha256:1bacd24b71f570d953dcd878f2edd6563fdace2705042fa81a0cbabb1ecfd7e3", + "canonicalDigest": "sha256:4844b3312ab79264da9a70cc722df8b02fcda77182f5d1c717c39abee0143a83", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { diff --git a/proofkit/requirement-bindings.json b/proofkit/requirement-bindings.json index 8b6bef1..dcecd3f 100644 --- a/proofkit/requirement-bindings.json +++ b/proofkit/requirement-bindings.json @@ -626,7 +626,7 @@ "specPath": "docs/specs/proofkit-supply-chain-quality/requirements.v1.json", "claimLevel": "blocking", "proofState": "witness_backed", - "nonClaims": ["This bounded diagnostic taxonomy does not prove confidentiality of values outside its admitted secret and control classes."] + "nonClaims": ["This bounded diagnostic taxonomy does not prove confidentiality of values outside its admitted secret and control classes or claim that every repository-internal tool invocation uses the installed-carrier timeout policy."] }, { "requirementId": "REQ-PROOFKIT-SPEC-017", @@ -3662,6 +3662,21 @@ "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-023", + "scenarioId": "proofkit.supply-chain-quality.release-artifact-immutable-snapshot", + "witnessId": "proofkit.release-manifest.immutable-artifact-epoch-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/tools/releasemanifest/artifact_snapshot_test.go", + "witnessSelectors": [ + { + "selector": "TestReleaseArtifactSnapshotOwnsOneImmutableEpoch", + "command": "go test ./internal/tools/releasemanifest -run '^TestReleaseArtifactSnapshotOwnsOneImmutableEpoch$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, { "requirementId": "REQ-PROOFKIT-QUALITY-023", "scenarioId": "proofkit.supply-chain-quality.python-wheel-platform-byte-compatibility", @@ -3815,6 +3830,10 @@ "witnessKind": "contract", "witnessPath": "internal/kernel/unicodepolicy/unicodepolicy_test.go", "witnessSelectors": [ + { + "selector": "TestUnsafeScalarTableMatchesVersionedOwnerCorpus", + "command": "go test ./internal/kernel/unicodepolicy -run '^TestUnsafeScalarTableMatchesVersionedOwnerCorpus$'" + }, { "selector": "TestUnsafeScalarTableMatchesEveryUnicode15Scalar", "command": "go test ./internal/kernel/unicodepolicy -run '^TestUnsafeScalarTableMatchesEveryUnicode15Scalar$'" @@ -4043,6 +4062,40 @@ "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-027", + "scenarioId": "proofkit.supply-chain-quality.installed-carrier-process-bounds", + "witnessId": "proofkit.workflow-smoke.process-bounds-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/tools/workflowsmoke/workflow_smoke_test.go", + "witnessSelectors": [ + { + "selector": "TestRunProcessRejectsTimeout", + "command": "go test ./internal/tools/workflowsmoke -run '^TestRunProcessRejectsTimeout$'" + }, + { + "selector": "TestRunProcessRejectsOutputOverflow", + "command": "go test ./internal/tools/workflowsmoke -run '^TestRunProcessRejectsOutputOverflow$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-027", + "scenarioId": "proofkit.supply-chain-quality.installed-carrier-process-group-cleanup", + "witnessId": "proofkit.workflow-smoke.process-group-cleanup-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/tools/workflowsmoke/process_unix_test.go", + "witnessSelectors": [ + { + "selector": "TestRunProcessTerminatesDescendantsAfterSuccessfulParentExit", + "command": "go test ./internal/tools/workflowsmoke -run '^TestRunProcessTerminatesDescendantsAfterSuccessfulParentExit$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, { "requirementId": "REQ-PROOFKIT-SPEC-019", "scenarioId": "proofkit.spec-proof-core.requirement-context-composition", diff --git a/release/change-record.v2.json b/release/change-record.v2.json index 5a199d0..c36e79b 100644 --- a/release/change-record.v2.json +++ b/release/change-record.v2.json @@ -10,7 +10,7 @@ }, { "changeId": "proofkit.diagnostic.bounded-error-boundary", - "summary": "CLI and repository-tool diagnostics now reject malformed or report-unsafe values as a whole, cap admitted text, and bound child-process stdout, stderr, lifetime, and process-group cleanup instead of forwarding raw child errors." + "summary": "CLI and repository-tool diagnostics now reject malformed or report-unsafe values as a whole, cap admitted text, capture failed child stderr before whole-value sanitization, and keep successful child stderr out of diagnostics; installed-carrier smoke additionally bounds stdout, stderr, lifetime, and process-group cleanup." }, { "changeId": "proofkit.stable-json.unicode-scalar-v2", @@ -28,7 +28,7 @@ }, { "changeId": "proofkit.release.cross-carrier-binary-identity", - "summary": "Release assembly now independently decodes final npm and Python archives under fixed bounds and directly requires byte-identical embedded binaries for every release platform." + "summary": "Release assembly now admits final npm, Python, and SBOM artifacts once into a bounded immutable snapshot, independently rejects duplicate archive entries, directly compares embedded platform binaries in bounded chunks, and derives release and SBOM checksums from the same artifact epoch." } ], "migration": { diff --git a/scripts/stable-json.test.mjs b/scripts/stable-json.test.mjs index 0c65a25..f2bb09d 100644 --- a/scripts/stable-json.test.mjs +++ b/scripts/stable-json.test.mjs @@ -1,4 +1,5 @@ import assert from "node:assert/strict"; +import {readFileSync} from "node:fs"; import test from "node:test"; import { @@ -9,6 +10,11 @@ import { stableJSONStringify, } from "./stable-json.mjs"; +const unicodePolicyCorpus = JSON.parse(readFileSync( + new URL("../internal/kernel/unicodepolicy/testdata/unsafe-scalar-ranges.v1.json", import.meta.url), + "utf8", +)); + test("stableJSONStringify sorts object keys without HTML escaping", () => { assert.equal( stableJSONStringify({z: "<&>", a: [{y: "\u2028", x: 1}]}), @@ -56,16 +62,9 @@ test("stable JSON Unicode corpus", () => { }); test("stable JSON Unicode table classifies every scalar and complement", () => { - const ranges = [ - [0x000000, 0x00001f, 1], [0x00007f, 0x00009f, 1], [0x0000ad, 0x000600, 1363], - [0x000601, 0x000605, 1], [0x00061c, 0x0006dd, 193], [0x00070f, 0x000890, 385], - [0x000891, 0x0008e2, 81], [0x00180e, 0x00200b, 2045], [0x00200c, 0x00200f, 1], - [0x002028, 0x002028, 1], [0x002029, 0x002029, 1], [0x00202a, 0x00202e, 1], - [0x002060, 0x002064, 1], [0x002066, 0x00206f, 1], [0x00feff, 0x00fff9, 250], - [0x00fffa, 0x00fffb, 1], [0x0110bd, 0x0110cd, 16], [0x013430, 0x01343f, 1], - [0x01bca0, 0x01bca3, 1], [0x01d173, 0x01d17a, 1], [0x0e0001, 0x0e0020, 31], - [0x0e0021, 0x0e007f, 1], - ]; + assert.equal(unicodePolicyCorpus.schemaVersion, 1); + assert.equal(unicodePolicyCorpus.unicodeVersion, "15.0.0"); + const ranges = unicodePolicyCorpus.ranges.map(({start, end, step}) => [start, end, step]); const expected = new Set(); for (const [start, end, step] of ranges) { for (let value = start; value <= end; value += step) expected.add(value); diff --git a/scripts/workflow_package_gate_oracle_test.go b/scripts/workflow_package_gate_oracle_test.go index b390fbb..8e392eb 100644 --- a/scripts/workflow_package_gate_oracle_test.go +++ b/scripts/workflow_package_gate_oracle_test.go @@ -12,6 +12,7 @@ import ( "strings" "testing" + "github.com/research-engineering/agentic-proofkit/internal/command/jsonreportcliadaptersource" "go.yaml.in/yaml/v3" ) @@ -1289,6 +1290,20 @@ func TestPackageGateWorkflowOracleAcceptsOwnerCIAndReleaseWorkflows(t *testing.T }) } +func TestReleaseWorkflowInstalledAdapterGeneratorMatchesOwner(t *testing.T) { + raw, err := os.ReadFile(filepath.Join("..", ".github", "workflows", "release.yml")) + if err != nil { + t.Fatal(err) + } + needle := `["generatorId", "` + jsonreportcliadaptersource.TypeScriptGeneratorID + `"]` + if count := strings.Count(string(raw), needle); count != 1 { + t.Fatalf("release workflow installed-adapter owner count=%d want 1", count) + } + if strings.Contains(string(raw), "proofkit.json-report-cli-adapter-source.typescript.v1") { + t.Fatal("release workflow retains the retired TypeScript generator identity") + } +} + func TestWorkflowGuardExpressionsRejectNeutralization(t *testing.T) { workflow := readWorkflowForTest(t, filepath.Join("..", ".github", "workflows", "release.yml")) if err := validateReleaseWorkflowExpressions(workflow); err != nil { From 90e222c578f37405a494c3f43ebf2cbfa4ceb6a6 Mon Sep 17 00:00:00 2001 From: iperev Date: Mon, 31 Aug 2026 22:20:28 +0200 Subject: [PATCH 08/11] fix: close workflow and release proof boundaries --- .../specs/proofkit-agent-workflow/overview.md | 21 ++-- .../requirements.v1.json | 8 +- .../proofkit-supply-chain-quality/overview.md | 12 +- .../requirements.v1.json | 4 +- internal/app/agent_workflow_args.go | 2 - internal/app/agent_workflow_command_test.go | 17 +++ internal/app/agent_workflow_topology_test.go | 68 ++++++++++- .../app/agent_workflow_version_edge_test.go | 34 ++++-- internal/app/cli_contract_test.go | 2 +- internal/app/command_contract_generated.go | 8 +- internal/app/terminal_style.go | 16 +-- internal/app/terminal_style_test.go | 8 +- .../app/testdata/v0.5-wire-observations.json | 8 +- .../command/changeworkflowplan/admission.go | 54 ++++----- .../changeworkflowplan/agent_envelope.go | 26 ++++- .../command/changeworkflowplan/catalog.go | 72 +++++++++--- .../changeworkflowplan/context_closure.go | 2 +- .../command/changeworkflowplan/decision.go | 19 ++- internal/command/changeworkflowplan/model.go | 5 + internal/command/changeworkflowplan/prompt.go | 2 +- .../command/changeworkflowplan/prompt_test.go | 2 + internal/command/changeworkflowplan/report.go | 17 ++- .../command/changeworkflowplan/state_test.go | 50 +++++++- .../changeworkflowplan/test_helpers_test.go | 5 +- internal/command/changeworkflowplan/text.go | 12 ++ .../command/changeworkflowplan/text_test.go | 4 +- .../stackpreset/preset_ids_generated.go | 2 +- internal/kernel/processgroup/process_other.go | 7 ++ internal/kernel/processgroup/process_unix.go | 37 ++++++ internal/tools/coveragemetrics/main.go | 41 ++++++- internal/tools/coveragemetrics/main_test.go | 35 +++++- internal/tools/releasechange/record_test.go | 4 +- internal/tools/releasecloseoutinput/main.go | 109 ++++++++++++++---- .../tools/releasecloseoutinput/main_test.go | 61 +++++++++- .../releasemanifest/artifact_snapshot.go | 59 +++++++++- .../releasemanifest/artifact_snapshot_test.go | 49 +++++++- .../tools/releasemanifest/carrier_identity.go | 74 +++++++----- .../releasemanifest/carrier_identity_test.go | 21 +++- internal/tools/releasemanifest/main.go | 4 + internal/tools/workflowsmoke/process.go | 2 +- .../tools/workflowsmoke/process_unix_test.go | 17 +-- proofkit/cli-contract.v2.json | 10 +- 42 files changed, 810 insertions(+), 200 deletions(-) diff --git a/docs/specs/proofkit-agent-workflow/overview.md b/docs/specs/proofkit-agent-workflow/overview.md index 1b73caf..72037ff 100644 --- a/docs/specs/proofkit-agent-workflow/overview.md +++ b/docs/specs/proofkit-agent-workflow/overview.md @@ -35,10 +35,12 @@ introduced. output. - `REQ-PROOFKIT-WORKFLOW-003`: 28 active state rows, one terminal row, authority- and witness-gated executable envelope transitions, closed - blockers and clarifications when those preconditions are absent, and an - explicit terminal stop packet. -- `REQ-PROOFKIT-WORKFLOW-004`: typed subject/finding resolution and internally - consistent caller-declared digests. + blockers and clarifications when those preconditions are absent, exact + accepted-subject carry into each nonterminal successor, and an explicit + terminal stop packet. +- `REQ-PROOFKIT-WORKFLOW-004`: typed subject/finding resolution, incoming + subject identity on every noninitial stage, and internally consistent + caller-declared digests. - `REQ-PROOFKIT-WORKFLOW-005`: shared 256-byte stable-ID admission before work, global candidate-dependency resolution, one explicit nullable governing-authority reference, role-preserving bounded least dependency @@ -53,12 +55,15 @@ introduced. nondisclosure corpora. - `REQ-PROOFKIT-WORKFLOW-008`: display-safe caller admission, no-leak denial, exact retained-or-missing witness projection, bounded actionable and - terminal text with required coordinates, and no styling of caller values. + terminal text with required coordinates and exact rendered successor delta, + and no styling of caller values. - `REQ-PROOFKIT-WORKFLOW-009`: explicit text/color selection and confinement of ANSI to eligible terminal stdout. -- `REQ-PROOFKIT-WORKFLOW-010`: one production owner per semantic relation, - independently authored semantic oracles, and an AST-checked two-command - carrier topology with no runtime experiment or external template residue. +- `REQ-PROOFKIT-WORKFLOW-010`: one production catalog owner for stage, + checkpoint-schema, action-prerequisite, and successor semantics, + independently authored semantic and literal-duplication oracles, and an + AST-checked two-command carrier topology with no runtime experiment or + external template residue. - `REQ-PROOFKIT-WORKFLOW-011`: a finite factorized CLI relation closed across descriptor/dispatcher/family/help/root-contract/witness/generated/package surfaces, with npm-only non-runtime specification docs and cross-channel diff --git a/docs/specs/proofkit-agent-workflow/requirements.v1.json b/docs/specs/proofkit-agent-workflow/requirements.v1.json index 3d7f20c..356cef9 100644 --- a/docs/specs/proofkit-agent-workflow/requirements.v1.json +++ b/docs/specs/proofkit-agent-workflow/requirements.v1.json @@ -34,7 +34,7 @@ { "requirementId": "REQ-PROOFKIT-WORKFLOW-003", "ownerId": "proofkit.agent-workflow", - "invariant": "The active checkpoint is exactly one closed discriminated variant not_started, ready_for_review, review_findings, or review_passed; one independently enumerable total non-overlapping relation maps the 28 active stage/checkpoint rows to exactly one stage-specific first action, review, repair, or accept_stage, the twenty-ninth complete-prefix row maps only to workflow_complete, an active agent envelope emits its single next_action only when a governing authority ref is retained and the verify action additionally retains at least one caller-owned witness ref, otherwise it emits no executable action plus closed blockers and clarification questions, every accept_stage plan and emitted envelope action declares exactly one equal successorStateDelta containing only completedStageIds and checkpoint, applying that delta by replacing only those two input fields while preserving schemaVersion, governingAuthorityRefId, contextRefs, and requiredContextRefIds produces exactly one legal successor snapshot, the terminal envelope names workflow_complete and an explicit stop action without a successor delta, and every surplus, missing, or otherwise illegal field combination fails closed.", + "invariant": "The active checkpoint is exactly one closed discriminated variant not_started, ready_for_review, review_findings, or review_passed; one independently enumerable total non-overlapping relation maps the 28 active stage/checkpoint rows to exactly one stage-specific first action, review, repair, or accept_stage, the twenty-ninth complete-prefix row maps only to workflow_complete, an active agent envelope emits its single next_action only when a governing authority ref is retained and the verify action additionally retains at least one caller-owned witness ref, otherwise it emits no executable action plus closed blockers and clarification questions, every accept_stage plan and emitted envelope action declares exactly one equal successorStateDelta containing only completedStageIds and checkpoint, a nonterminal delta carries the accepted subjectRefId and subjectDigest into the next stage's not_started checkpoint while the terminal delta has a null checkpoint, applying that delta by replacing only those two input fields while preserving schemaVersion, governingAuthorityRefId, contextRefs, and requiredContextRefIds produces exactly one legal successor snapshot, the terminal envelope names workflow_complete and an explicit stop action without a successor delta, and every surplus, missing, or otherwise illegal field combination fails closed.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -47,7 +47,7 @@ { "requirementId": "REQ-PROOFKIT-WORKFLOW-004", "ownerId": "proofkit.agent-workflow", - "invariant": "Every ready_for_review, review_findings, and review_passed checkpoint binds the caller-declared stage artifact through one subjectRefId that resolves exactly one artifact context reference and one equal lowercase SHA-256 subject digest; assessed variants require an equal assessmentSubjectDigest, review_findings resolves each findingRef exactly once to a finding context reference, and a malformed, absent, surplus, unresolved, wrong-kind, or mismatched identity fails before an action is projected.", + "invariant": "Every noninitial not_started, ready_for_review, review_findings, and review_passed checkpoint binds the caller-declared stage artifact through one subjectRefId that resolves exactly one artifact context reference and one equal lowercase SHA-256 subject digest; the initial architecture/not_started checkpoint alone has no incoming subject, assessed variants require an equal assessmentSubjectDigest, review_findings resolves each findingRef exactly once to a finding context reference, and a malformed, absent, surplus, unresolved, wrong-kind, or mismatched identity fails before an action is projected.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -99,7 +99,7 @@ { "requirementId": "REQ-PROOFKIT-WORKFLOW-008", "ownerId": "proofkit.agent-workflow", - "invariant": "Every workflow caller-visible value is admitted through its typed owner; display-safe values reject malformed UTF-8, secret-shaped content, ASCII controls, DEL, Unicode Cc, Cf, Zl, and Zp without echo; every active prompt packet projects observed fact, uncertainty, the resolved governingAuthorityRefId or a closed missing-owner escalation target, exact admitted coordinates, candidate action, one closed proof-availability state distinguishing a missing consumer witness from exact retained witness refs, non-claim, stop condition, and expected successor from command-owned tables, and when governingAuthorityRefId is null the packet forbids repository mutation until the caller supplies it; planner text contains the state, active stage and action when present, checkpoint, owner or escalation, stop condition, expected successor, omission count, and one non-claim within 24 lines and 8192 bytes, including explicit consumer owner, stop condition, and no successor for the terminal variant; guidance text contains every slot ID, applicability class, required decision, and completion criterion within 48 lines and 16384 bytes; caller-owned values are never terminal-styled.", + "invariant": "Every workflow caller-visible value is admitted through its typed owner; display-safe values reject malformed UTF-8, secret-shaped content, ASCII controls, DEL, Unicode Cc, Cf, Zl, and Zp without echo; every active prompt packet projects observed fact, uncertainty, the resolved governingAuthorityRefId or a closed missing-owner escalation target, exact admitted coordinates, candidate action, one closed proof-availability state distinguishing a missing consumer witness from exact retained witness refs, non-claim, stop condition, and expected successor from command-owned tables, and when governingAuthorityRefId is null the packet forbids repository mutation until the caller supplies it; planner text contains the state, active stage and action when present, checkpoint, owner or escalation, stop condition, expected successor, omission count, and one non-claim within 24 lines and 8192 bytes, and an accept_stage projection additionally renders the exact successor completed-stage set, checkpoint state, subject ref, and subject digest while the terminal variant renders explicit consumer owner, stop condition, and no successor; guidance text contains every slot ID, applicability class, required decision, and completion criterion within 48 lines and 16384 bytes; caller-owned values are never terminal-styled.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -125,7 +125,7 @@ { "requirementId": "REQ-PROOFKIT-WORKFLOW-010", "ownerId": "proofkit.agent-workflow", - "invariant": "The production capability has exactly one workflow catalog declaration owning the stage, checkpoint-action, and action-profile relations and exactly one guidance-table declaration owning evidence slots; descriptor closure exposes exactly one required-input planner carrier and one no-input guidance carrier in one coherent command family, a source-topology oracle rejects duplicate or retired production owners and runtime guidance resources, and independent literal state, action-profile, and slot oracles falsify semantic drift without becoming production owners; the capability adds no persisted experiment matrix, candidate receipt, source-codec variant, external prompt duplicate, repository adapter, setup state, or generic agent executor.", + "invariant": "The production capability has exactly one workflow catalog declaration owning stage order, incoming-subject policy, checkpoint field schemas, checkpoint-action relations, action prerequisites, successor-emission policy, and action profiles, and exactly one guidance-table declaration owning evidence slots; descriptor closure exposes exactly one required-input planner carrier and one no-input guidance carrier in one coherent command family, a source-topology oracle rejects duplicate or retired production owners, duplicated catalog vocabulary, and runtime guidance resources, and independent literal state, action-profile, successor, and slot oracles falsify semantic drift without becoming production owners; the capability adds no persisted experiment matrix, candidate receipt, source-codec variant, external prompt duplicate, repository adapter, setup state, or generic agent executor.", "claimLevel": "blocking", "riskClass": "medium", "proofBindingRefs": ["proofkit/requirement-bindings.json"], diff --git a/docs/specs/proofkit-supply-chain-quality/overview.md b/docs/specs/proofkit-supply-chain-quality/overview.md index 413993c..1d95f72 100644 --- a/docs/specs/proofkit-supply-chain-quality/overview.md +++ b/docs/specs/proofkit-supply-chain-quality/overview.md @@ -163,7 +163,9 @@ vulnerability absence, or consumer rollout safety by itself. - `REQ-PROOFKIT-QUALITY-023`: npm platform binaries and Python wheels independently bind to the same release binary for every target; a separate bounded decoder also compares the final tar and wheel members directly, - without a mutable intermediate; wheels bind advertised platform + without a mutable intermediate; package-set wheel and embedded-binary digest + claims must equal bytes decoded from one immutable snapshot and release + closeout recomputes them independently; wheels bind advertised platform compatibility to decoded executable bytes, reject version, identity, presence, and digest drift, and carry Core Metadata 2.4 plus an exact, RECORD-closed repository license payload. @@ -190,9 +192,11 @@ vulnerability absence, or consumer rollout safety by itself. - `REQ-PROOFKIT-QUALITY-027`: report-visible diagnostics and structural text apply the same scalar, Unicode-whitespace, control/format, and split-secret taxonomy, replace rejected caller values as a whole without echoing any - fragment, bound diagnostic output, and own dynamic Go/JavaScript error - projection plus failure-only bounded child-process diagnostics for repository - tools. + fragment, bound diagnostic output, and own dynamic Go/JavaScript error + projection plus failure-only bounded child-process diagnostics for repository + tools. On supported Unix hosts, an installed-carrier success additionally + requires confirmed process-group absence after the parent is reaped; cleanup + timeout is an explicit failure rather than a successful terminal result. ## Non-Claims diff --git a/docs/specs/proofkit-supply-chain-quality/requirements.v1.json b/docs/specs/proofkit-supply-chain-quality/requirements.v1.json index 891d4db..f1d8046 100644 --- a/docs/specs/proofkit-supply-chain-quality/requirements.v1.json +++ b/docs/specs/proofkit-supply-chain-quality/requirements.v1.json @@ -298,7 +298,7 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-023", "ownerId": "proofkit.supply-chain-quality", - "invariant": "Every npm platform-binary tar entry and every Python wheel independently binds its embedded executable bytes to the same release-platform binary, and release metadata generation admits every final npm tarball, wheel, and SBOM exactly once into one bounded regular non-symlink immutable snapshot, derives final carrier decoding, direct chunked binary equality, release checksums, and SBOM-subject checksums only from that epoch, and revalidates every source identity before metadata persistence without relying on a mutable intermediate dist directory or claimed digests; both tar and wheel decoders reject duplicate entry names and require exact release-platform closure; the wheel-set verifier independently rejects version drift, duplicate identities, missing files, and SHA drift; each wheel independently decodes Darwin Mach-O bytes so an advertised macOS platform minimum is never lower than the executable minimum, uses Core Metadata 2.4 License-Expression and License-File fields, carries an exact repository LICENSE copy under the wheel licenses directory, and closes every payload entry through RECORD.", + "invariant": "Every npm platform-binary tar entry and every Python wheel independently binds its embedded executable bytes to the same release-platform binary, and release metadata generation admits every final npm tarball, wheel, and SBOM exactly once into one bounded regular non-symlink immutable snapshot, derives final carrier decoding, direct chunked binary equality, wheel and embedded-binary digests, release checksums, and SBOM-subject checksums only from that epoch, requires every Python package-set digest claim to equal those decoded bytes, and revalidates every source identity before metadata persistence without relying on a mutable intermediate dist directory or claimed digests; release closeout independently reopens every declared wheel, rejects unsafe or duplicate ZIP topology, and recomputes both wheel and embedded-binary digests before accepting Python artifact evidence; both tar and wheel decoders reject duplicate entry names and require exact release-platform closure; the wheel-set verifier independently rejects version drift, duplicate identities, missing files, and SHA drift; each wheel independently decodes Darwin Mach-O bytes so an advertised macOS platform minimum is never lower than the executable minimum, uses Core Metadata 2.4 License-Expression and License-File fields, carries an exact repository LICENSE copy under the wheel licenses directory, and closes every payload entry through RECORD.", "claimLevel": "blocking", "riskClass": "critical", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -350,7 +350,7 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-027", "ownerId": "proofkit.supply-chain-quality", - "invariant": "Every report-visible diagnostic and structural-text sanitizer accepts only Unicode 15.0.0 scalar strings, recognizes one explicit Unicode whitespace alphabet plus the pinned Cc, Cf, Zl, and Zp range table and contiguous or control-split secret-shaped values through one shared taxonomy, replaces any rejected caller-owned value as a whole with one fixed command-owned label without emitting its bytes, code points, prefix, suffix, or caller-controlled replacement text, and bounds admitted diagnostic projection to 512 Unicode scalars; every Go and JavaScript repository tool entrypoint routes dynamic errors through that owner, failed child-process stderr that enters this diagnostic boundary is captured under a fixed byte limit before whole-value sanitization while successful child stderr is not projected, direct child stderr wiring is rejected, and the installed-carrier smoke adapter bounds stdout, stderr, time, and process-group cleanup and terminates process-group descendants before every post-start return on supported Unix hosts.", + "invariant": "Every report-visible diagnostic and structural-text sanitizer accepts only Unicode 15.0.0 scalar strings, recognizes one explicit Unicode whitespace alphabet plus the pinned Cc, Cf, Zl, and Zp range table and contiguous or control-split secret-shaped values through one shared taxonomy, replaces any rejected caller-owned value as a whole with one fixed command-owned label without emitting its bytes, code points, prefix, suffix, or caller-controlled replacement text, and bounds admitted diagnostic projection to 512 Unicode scalars; every Go and JavaScript repository tool entrypoint routes dynamic errors through that owner, failed child-process stderr that enters this diagnostic boundary is captured under a fixed byte limit before whole-value sanitization while successful child stderr is not projected, direct child stderr wiring is rejected, and the installed-carrier smoke adapter bounds stdout, stderr, time, and cleanup; on supported Unix hosts, a successful installed-carrier result requires confirmed process-group absence after the command parent is reaped, while cleanup timeout or probe failure is an explicit failed operation rather than a successful terminal result.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], diff --git a/internal/app/agent_workflow_args.go b/internal/app/agent_workflow_args.go index b9a315f..e8f4f19 100644 --- a/internal/app/agent_workflow_args.go +++ b/internal/app/agent_workflow_args.go @@ -11,7 +11,6 @@ type agentWorkflowArgs struct { inputPointer jsonpointer.Pointer pointerPresent bool format string - formatExplicit bool color string colorExplicit bool agentEnvelope bool @@ -57,7 +56,6 @@ func parseAgentWorkflowArgs(command string, args []string) (agentWorkflowArgs, e return agentWorkflowArgs{}, fmt.Errorf("--format requires one of: json, text") } options.format = value - options.formatExplicit = true case "--input": options.inputPath = value case "--input-pointer": diff --git a/internal/app/agent_workflow_command_test.go b/internal/app/agent_workflow_command_test.go index f0855e2..02e8b82 100644 --- a/internal/app/agent_workflow_command_test.go +++ b/internal/app/agent_workflow_command_test.go @@ -89,6 +89,23 @@ func testAgentWorkflowEnvelopeTransitionClasses(t *testing.T) { if action["outputKind"] != "next_action" || !ok { t.Fatalf("transition envelope is incomplete: %v", action) } + wantCompleted := make([]any, stageIndex+1) + for index := 0; index <= stageIndex; index++ { + wantCompleted[index] = stages[index] + } + if !equalCLIJSON(t, delta["completedStageIds"], wantCompleted) { + t.Fatalf("transition completedStageIds=%v want %v", delta["completedStageIds"], wantCompleted) + } + if stageIndex+1 == len(stages) { + if delta["checkpoint"] != nil { + t.Fatalf("final transition checkpoint=%v want nil", delta["checkpoint"]) + } + } else { + wantCheckpoint := map[string]any{"state": "not_started", "subjectDigest": digest, "subjectRefId": "ctx.artifact"} + if !equalCLIJSON(t, delta["checkpoint"], wantCheckpoint) { + t.Fatalf("transition checkpoint=%v want %v", delta["checkpoint"], wantCheckpoint) + } + } merged, err := changeworkflowplan.MergeSuccessor(prior, delta) if err != nil { t.Fatal(err) diff --git a/internal/app/agent_workflow_topology_test.go b/internal/app/agent_workflow_topology_test.go index c647f39..364d4fc 100644 --- a/internal/app/agent_workflow_topology_test.go +++ b/internal/app/agent_workflow_topology_test.go @@ -56,6 +56,7 @@ func TestAgentWorkflowSemanticOwnerTopology(t *testing.T) { root := repoRoot(t) assertSingleSemanticTableOwner(t, filepath.Join(root, "internal/command/changeworkflowplan"), "workflowCatalog", "catalog.go") + assertWorkflowVocabularyOwnedByCatalog(t, filepath.Join(root, "internal/command/changeworkflowplan")) assertSingleSemanticTableOwner(t, filepath.Join(root, "internal/command/nativeevidenceguidance"), "guidanceTable", "guidance.go") for _, retiredOwner := range []struct { dir string @@ -73,6 +74,71 @@ func TestAgentWorkflowSemanticOwnerTopology(t *testing.T) { assertAgentWorkflowCarrierClosure(t, root) } +func assertWorkflowVocabularyOwnedByCatalog(t *testing.T, dir string) { + t.Helper() + catalogPath := filepath.Join(dir, "catalog.go") + catalog, err := parser.ParseFile(token.NewFileSet(), catalogPath, nil, parser.SkipObjectResolution) + if err != nil { + t.Fatalf("parse %s: %v", catalogPath, err) + } + vocabulary := map[string]struct{}{} + ast.Inspect(catalog, func(node ast.Node) bool { + literal := (*ast.BasicLit)(nil) + switch value := node.(type) { + case *ast.ValueSpec: + if len(value.Names) == 1 && value.Names[0].Name == "workflowProfileID" && len(value.Values) == 1 { + literal, _ = value.Values[0].(*ast.BasicLit) + } + case *ast.KeyValueExpr: + switch key := value.Key.(type) { + case *ast.Ident: + if key.Name == "ID" || key.Name == "FirstAction" || key.Name == "State" || key.Name == "Action" { + literal, _ = value.Value.(*ast.BasicLit) + } + case *ast.BasicLit: + literal = key + } + } + if literal == nil || literal.Kind != token.STRING { + return true + } + decoded, err := strconv.Unquote(literal.Value) + if err != nil { + t.Fatalf("decode catalog literal: %v", err) + } + vocabulary[decoded] = struct{}{} + return true + }) + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatal(err) + } + for _, entry := range entries { + if entry.IsDir() || entry.Name() == "catalog.go" || filepath.Ext(entry.Name()) != ".go" || strings.HasSuffix(entry.Name(), "_test.go") { + continue + } + path := filepath.Join(dir, entry.Name()) + parsed, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.SkipObjectResolution) + if err != nil { + t.Fatalf("parse %s: %v", path, err) + } + ast.Inspect(parsed, func(node ast.Node) bool { + literal, ok := node.(*ast.BasicLit) + if !ok || literal.Kind != token.STRING { + return true + } + value, err := strconv.Unquote(literal.Value) + if err != nil { + t.Fatalf("decode workflow literal in %s: %v", path, err) + } + if _, duplicate := vocabulary[value]; duplicate { + t.Fatalf("%s duplicates workflow catalog semantic literal %q", entry.Name(), value) + } + return true + }) + } +} + func assertAgentWorkflowCarrierClosure(t *testing.T, root string) { t.Helper() closures := []struct { @@ -110,7 +176,7 @@ func assertAgentWorkflowCarrierClosure(t *testing.T, root string) { { file: "terminal_style.go", imports: []string{"fmt", "strings"}, - requiredCallees: []string{"renderTerminalText", "terminalTokenStyle"}, + requiredCallees: []string{"terminalTokenStyle"}, }, } for _, closure := range closures { diff --git a/internal/app/agent_workflow_version_edge_test.go b/internal/app/agent_workflow_version_edge_test.go index d07f6cc..d489646 100644 --- a/internal/app/agent_workflow_version_edge_test.go +++ b/internal/app/agent_workflow_version_edge_test.go @@ -9,6 +9,7 @@ import ( "slices" "testing" + "github.com/research-engineering/agentic-proofkit/internal/command/jsonreportcliadaptersource" "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" "github.com/research-engineering/agentic-proofkit/internal/tools/releasechange" ) @@ -16,17 +17,19 @@ import ( const agentWorkflowVersionEdgePath = "internal/app/testdata/v0.5-wire-observations.json" type agentWorkflowVersionEdge struct { - AddedCommandContracts []agentWorkflowCommandContract `json:"addedCommandContracts"` - AdditionChangeIDs []string `json:"additionChangeIds"` - BreakingChangeIDs []string `json:"breakingChangeIds"` - 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 []agentWorkflowCommandContract `json:"addedCommandContracts"` + AdditionChangeIDs []string `json:"additionChangeIds"` + BreakingChangeIDs []string `json:"breakingChangeIds"` + CurrentPublicABISHA256 string `json:"currentPublicAbiSha256"` + CurrentTypeScriptGeneratorID string `json:"currentTypeScriptGeneratorId"` + EdgeID string `json:"edgeId"` + EvidenceClass string `json:"evidenceClass"` + NonClaims []string `json:"nonClaims"` + PreviousPublicABISHA256 string `json:"previousPublicAbiSha256"` + PreviousTypeScriptGeneratorID string `json:"previousTypeScriptGeneratorId"` + PreviousVersion string `json:"previousVersion"` + SchemaVersion int `json:"schemaVersion"` + Version string `json:"version"` } type agentWorkflowCommandContract struct { @@ -51,6 +54,10 @@ func TestAgentWorkflowVersionEdgeClosesPublicWireAdditions(t *testing.T) { }{ {name: "current ABI", mutate: func(value *agentWorkflowVersionEdge) { value.CurrentPublicABISHA256 += "0" }}, {name: "previous ABI", mutate: func(value *agentWorkflowVersionEdge) { value.PreviousPublicABISHA256 = value.CurrentPublicABISHA256 }}, + {name: "current TypeScript generator", mutate: func(value *agentWorkflowVersionEdge) { value.CurrentTypeScriptGeneratorID += ".drift" }}, + {name: "previous TypeScript generator", mutate: func(value *agentWorkflowVersionEdge) { + value.PreviousTypeScriptGeneratorID = value.CurrentTypeScriptGeneratorID + }}, {name: "command contract", mutate: func(value *agentWorkflowVersionEdge) { value.AddedCommandContracts[0].OutputContractSHA256 += "0" }}, {name: "missing command", mutate: func(value *agentWorkflowVersionEdge) { value.AddedCommandContracts = value.AddedCommandContracts[1:] }}, {name: "addition owner", mutate: func(value *agentWorkflowVersionEdge) { value.AdditionChangeIDs[0] += ".drift" }}, @@ -88,7 +95,7 @@ func readAgentWorkflowVersionEdge(t *testing.T) agentWorkflowVersionEdge { if !ok { t.Fatal("agent workflow version edge must be an object") } - assertExactObjectKeys(t, root, []string{"addedCommandContracts", "additionChangeIds", "breakingChangeIds", "currentPublicAbiSha256", "edgeId", "evidenceClass", "nonClaims", "previousPublicAbiSha256", "previousVersion", "schemaVersion", "version"}, "agent workflow version edge") + assertExactObjectKeys(t, root, []string{"addedCommandContracts", "additionChangeIds", "breakingChangeIds", "currentPublicAbiSha256", "currentTypeScriptGeneratorId", "edgeId", "evidenceClass", "nonClaims", "previousPublicAbiSha256", "previousTypeScriptGeneratorId", "previousVersion", "schemaVersion", "version"}, "agent workflow version edge") commandContracts, ok := root["addedCommandContracts"].([]any) if !ok { t.Fatal("agent workflow version edge addedCommandContracts must be an array") @@ -117,6 +124,9 @@ func validateAgentWorkflowVersionEdge(record agentWorkflowVersionEdge, releaseRe if record.PreviousPublicABISHA256 != "sha256:fc03740aea9e7f525a4388e5d7f557cde07e11b0db0c05101fe937c28a1129d9" || record.CurrentPublicABISHA256 != "sha256:"+cliContractPublicABISHA256 || record.PreviousPublicABISHA256 == record.CurrentPublicABISHA256 { return fmt.Errorf("version-edge ABI identity is invalid") } + if record.PreviousTypeScriptGeneratorID != "proofkit.json-report-cli-adapter-source.typescript.v1" || record.CurrentTypeScriptGeneratorID != jsonreportcliadaptersource.TypeScriptGeneratorID || record.PreviousTypeScriptGeneratorID == record.CurrentTypeScriptGeneratorID { + return fmt.Errorf("version-edge TypeScript generator identity is invalid") + } expectedCommands := []agentWorkflowCommandContract{ {Command: "change-workflow-plan", InputContractSHA256: generatedCommandContractMetadataByName["change-workflow-plan"].InputContractSHA256, OutputContractSHA256: generatedCommandContractMetadataByName["change-workflow-plan"].OutputContractSHA256}, {Command: "native-evidence-guidance", InputContractSHA256: generatedCommandContractMetadataByName["native-evidence-guidance"].InputContractSHA256, OutputContractSHA256: generatedCommandContractMetadataByName["native-evidence-guidance"].OutputContractSHA256}, diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index c41efc6..63935f0 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -23,7 +23,7 @@ import ( ) const ( - cliContractPublicABISHA256 = "757a547c72d2a5e274f8f00bc58adc665cc4495a268fd3e48ce3a6fa0c06f22c" + cliContractPublicABISHA256 = "dd4be9498e7dece58fd354cf7d0aa625c09ba205c7bc7a287d5b28df868981a4" 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 0e9ab06..b40b701 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 = "5fc85982c751d3a384193e57a59a6eedec5a1b0dce3b58bcb143228a68070427" +const commandContractSourceSHA256 = "be6fef4e32ab624992480f537fd55f3d53f352e712486d34f95ee67fee32b842" type generatedCommandContractMetadata struct { InputContractSHA256 string @@ -19,7 +19,7 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "binding-partition": {InputContractSHA256: "sha256:366ad082045af52b2ac6604f18626d0f285b2db73b45d9a82687b8d3b0d2b3fd", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.binding-partition.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:52840879e13a00ef9a4abaad6cdb33000511674d5f9003fb56f387fdf58fadc8", FlagChoices: map[string][]string{}}, "branch-authority": {InputContractSHA256: "sha256:8a3ed74978898593fbdbf1f7fa684dae450fbd9019edcd60d07f818d63363ed4", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.branch-authority.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3c7dc74842299b92cd5baf57cc8666e9415963091359e5faf654e28da89561f1", FlagChoices: map[string][]string{}}, "capability-map-admission": {InputContractSHA256: "sha256:efe87497cf6a32cfd1f9c9c191393cac8781d1e9211914a8a7b6aaa44e7f371d", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.capability-map-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:bfb32535fd51bed168111912acb1651527897fe59a592119dc0032ce9286c9db", FlagChoices: map[string][]string{}}, - "change-workflow-plan": {InputContractSHA256: "sha256:0e6586bb23f5dc9479112d2123bfd1527ade0d20cf7f2bd14783a3ec26f8bb3e", 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:ee5eaadeb593ee592a2d043a8284aae7add80d0d5323b59902b6ebbd0af645f5", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}}, + "change-workflow-plan": {InputContractSHA256: "sha256:d60c2cd30f64a98c9c512e16dead65cef9457910b7d59ec332fa3be48bcc8994", 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:9ea80c03c46e0ec9a8b51fbff8be32da9d7cc734bb76968fb671bf4bfcae8173", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}}, "changed-path-set": {InputContractSHA256: "sha256:8fe97426a58969e3e8dcbd52ed44540666b4de6be0487e8a3bc5088ae9c0f933", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.changed-path-set.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:abccdbf78e67f633ce49c34e8849c03f08ce42fa934a4c68969720c5045bf593", FlagChoices: map[string][]string{}}, "completion-criteria": {InputContractSHA256: "sha256:99c49c44b001e40383787e4c55f66621b8a8315f09635f1baf2326dc09bec4e6", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.completion-criteria.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:c90bb9605c7a22914104701a068dded510534bdeb60f4d601555d46c2d3d8a6d", FlagChoices: map[string][]string{}}, "conformance-profile": {InputContractSHA256: "sha256: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{}}, @@ -40,7 +40,7 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "native-evidence-guidance": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:c1d23df574e948ea7160931f53790a6d133ae12eeceefe9e5fa15430d653ff7e", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}}, "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{}}, "package-runtime-dependency-admission": {InputContractSHA256: "sha256:fc85887af9b8fcd899d245f0db30b2f2f68609822fc268126bf999082bb4115f", InputSchemaSummary: []string{"schemaVersion=1", "reportId", "expectedDependencySpec", "expectedLockfileIntegrity", "expectedPackageName", "expectedPackageVersion", "admissibleLocations{}", "packageResolution{}", "nonClaims[]", "root-shape-only definition proofkit.package-runtime-dependency-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:c012032e8c8212fd50bc2e85669cc610609ca2124ebc992c9e88f44a1ad2d5fc", FlagChoices: map[string][]string{}}, - "pilot-admission": {InputContractSHA256: "sha256: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:91b93de58cb4ff03e3bd88eaddad4afaeef743d5ab37a44a6170382543f50638", FlagChoices: map[string][]string{}}, + "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:c136592e6921acba1fdf50280a9de6945983fc3cfc7454e32f33b7c894cb1774", FlagChoices: map[string][]string{}}, "producer-policy-self-proof": {InputContractSHA256: "sha256:d48e18826000c8d415f3c44b6c686e1da6ed962ef7ca36c9f705de8c68d034f9", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.producer-policy-self-proof.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:e82a3989a743f8babc6069f7af82b1dd1ea62bad8dbb18d95e105b36f74e4276", FlagChoices: map[string][]string{}}, "proof-obligation-algebra": {InputContractSHA256: "sha256: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{}}, "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{}}, @@ -78,7 +78,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{}}, "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{}}, "selective-gate-plan": {InputContractSHA256: "sha256:5293a5a4c7d8426cf637e6f8d252095ca0eb1714365bb89bec83307b778c678a", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.selective-gate-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:d7bffed853af5595af08b03859be01c283a3bdff1b3502d94ddc190889977647", FlagChoices: map[string][]string{}}, - "self-check": {InputContractSHA256: "sha256:77332ca52a451d4f072de049f0a3e1e9f70ebaf447a144650ca1afafeb514dc1", 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:ce17ce8ca3da695924bf16aaa06ffde3d5505cb1a8d6828cc1dfaa55f3bb7d47", FlagChoices: map[string][]string{}}, + "self-check": {InputContractSHA256: "sha256:a4baba76aa97d331e1a64677f56b659f30510be9a918079e745bb62c54dffe24", 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:cda2f97a369e783782b00b466b7fd0f6d7c31d17cbe69cbc8ac25d7e73d56a8c", FlagChoices: map[string][]string{}}, "spec-overview-claims": {InputContractSHA256: "sha256:2490dcd34ba7485e13f8f33e8a288a0463c4c52cc6b0d82c57777466927e49a4", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.spec-overview-claims.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:554f3a7020e9820ccb90672629fd769c52b2f298f356040aa3b0a817666cbfbf", FlagChoices: map[string][]string{}}, "spec-proof-bundle-admission": {InputContractSHA256: "sha256:6b6c2875b6476e63a1911e7d6112d9999df2babbee969f84abc4c9e4b470c933", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.spec-proof-bundle-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:e9e0eb66cebca3b99fe5036fb2e7327a9284934ed76f58818d18094d0546fc52", FlagChoices: map[string][]string{}}, "stack-preset": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:f495e9ade4e1e7af7a8f8b2059f7611cc016e6080363afccf76d8dfc2dbc6d2d", FlagChoices: map[string][]string{"--preset": []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"}}}, diff --git a/internal/app/terminal_style.go b/internal/app/terminal_style.go index c45ebb6..574c4bf 100644 --- a/internal/app/terminal_style.go +++ b/internal/app/terminal_style.go @@ -6,9 +6,8 @@ import ( ) const ( - ansiReset = "\x1b[0m" - ansiLabel = "\x1b[1;36m" - ansiSecondary = "\x1b[36m" + ansiReset = "\x1b[0m" + ansiLabel = "\x1b[1;36m" ) // PresentationCapabilities are immutable process-boundary facts. Library @@ -21,9 +20,8 @@ type PresentationCapabilities struct { type terminalTokenKind string const ( - terminalTokenPlain terminalTokenKind = "plain" - terminalTokenLabel terminalTokenKind = "label" - terminalTokenSecondary terminalTokenKind = "secondary" + terminalTokenPlain terminalTokenKind = "plain" + terminalTokenLabel terminalTokenKind = "label" ) type terminalTextToken struct { @@ -61,18 +59,12 @@ func renderTerminalText(value terminalText, colorMode string, capabilities Prese return builder.String(), nil } -func plainTerminalText(value terminalText) (string, error) { - return renderTerminalText(value, "never", PresentationCapabilities{}) -} - func terminalTokenStyle(kind terminalTokenKind) (string, error) { switch kind { case terminalTokenPlain: return "", nil case terminalTokenLabel: return ansiLabel, nil - case terminalTokenSecondary: - return ansiSecondary, nil default: return "", fmt.Errorf("unsupported terminal token kind") } diff --git a/internal/app/terminal_style_test.go b/internal/app/terminal_style_test.go index ff749f5..5046618 100644 --- a/internal/app/terminal_style_test.go +++ b/internal/app/terminal_style_test.go @@ -9,12 +9,12 @@ func TestTerminalStyleCapabilityRelation(t *testing.T) { view := newTerminalText( terminalTextToken{kind: terminalTokenLabel, text: "Action"}, terminalTextToken{kind: terminalTokenPlain, text: ": inspect\n"}, - terminalTextToken{kind: terminalTokenSecondary, text: "Next"}, + terminalTextToken{kind: terminalTokenLabel, text: "Next"}, terminalTextToken{kind: terminalTokenPlain, text: ": verify\n"}, ) - plain, err := plainTerminalText(view) + plain, err := renderTerminalText(view, "never", PresentationCapabilities{}) if err != nil { - t.Fatalf("plainTerminalText() error = %v", err) + t.Fatalf("renderTerminalText() plain error = %v", err) } for _, test := range []struct { name string @@ -52,7 +52,7 @@ func TestTerminalStyleRejectsUnknownModesAndTokens(t *testing.T) { } func stripTestANSI(value string) string { - for _, sequence := range []string{ansiLabel, ansiSecondary, ansiReset} { + for _, sequence := range []string{ansiLabel, ansiReset} { value = strings.ReplaceAll(value, sequence, "") } return value diff --git a/internal/app/testdata/v0.5-wire-observations.json b/internal/app/testdata/v0.5-wire-observations.json index 1bdb75e..811d4d1 100644 --- a/internal/app/testdata/v0.5-wire-observations.json +++ b/internal/app/testdata/v0.5-wire-observations.json @@ -5,12 +5,14 @@ "version": "0.5.0", "evidenceClass": "owner_authored_frozen_version_edge_observation", "previousPublicAbiSha256": "sha256:fc03740aea9e7f525a4388e5d7f557cde07e11b0db0c05101fe937c28a1129d9", - "currentPublicAbiSha256": "sha256:757a547c72d2a5e274f8f00bc58adc665cc4495a268fd3e48ce3a6fa0c06f22c", + "currentPublicAbiSha256": "sha256:dd4be9498e7dece58fd354cf7d0aa625c09ba205c7bc7a287d5b28df868981a4", + "previousTypeScriptGeneratorId": "proofkit.json-report-cli-adapter-source.typescript.v1", + "currentTypeScriptGeneratorId": "proofkit.json-report-cli-adapter-source.typescript.v2", "addedCommandContracts": [ { "command": "change-workflow-plan", - "inputContractSha256": "sha256:0e6586bb23f5dc9479112d2123bfd1527ade0d20cf7f2bd14783a3ec26f8bb3e", - "outputContractSha256": "sha256:ee5eaadeb593ee592a2d043a8284aae7add80d0d5323b59902b6ebbd0af645f5" + "inputContractSha256": "sha256:d60c2cd30f64a98c9c512e16dead65cef9457910b7d59ec332fa3be48bcc8994", + "outputContractSha256": "sha256:9ea80c03c46e0ec9a8b51fbff8be32da9d7cc734bb76968fb671bf4bfcae8173" }, { "command": "native-evidence-guidance", diff --git a/internal/command/changeworkflowplan/admission.go b/internal/command/changeworkflowplan/admission.go index 81ec2e9..81ea669 100644 --- a/internal/command/changeworkflowplan/admission.go +++ b/internal/command/changeworkflowplan/admission.go @@ -8,13 +8,6 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/kernel/unicodepolicy" ) -var checkpointStates = map[string]struct{}{ - "not_started": {}, - "ready_for_review": {}, - "review_findings": {}, - "review_passed": {}, -} - var contextRefKinds = map[string]struct{}{ "artifact": {}, "authority": {}, @@ -42,7 +35,7 @@ func admitInput(raw any) (admittedInput, error) { if err != nil { return admittedInput{}, err } - checkpointValue, err := admitCheckpoint(record["checkpoint"], len(completed) == len(workflowCatalog.Stages)) + checkpointValue, err := admitCheckpoint(record["checkpoint"], len(completed)) if err != nil { return admittedInput{}, err } @@ -88,8 +81,8 @@ func admitCompletedStages(raw any) ([]string, error) { return result, nil } -func admitCheckpoint(raw any, complete bool) (*checkpoint, error) { - if complete { +func admitCheckpoint(raw any, completedStageCount int) (*checkpoint, error) { + if completedStageCount == len(workflowCatalog.Stages) { if raw != nil { return nil, reject("proofkit.workflow.complete_checkpoint", "a complete workflow requires a null checkpoint") } @@ -99,16 +92,13 @@ func admitCheckpoint(raw any, complete bool) (*checkpoint, error) { if !ok { return nil, reject("proofkit.workflow.incomplete_checkpoint", "an incomplete workflow requires a checkpoint object") } - state, err := admit.Enum(record["state"], checkpointStates, "change workflow checkpoint state") - if err != nil { + state, ok := record["state"].(string) + definition, known := checkpointDefinitionFor(state) + if !ok || !known { return nil, reject("proofkit.workflow.checkpoint_variant", "checkpoint state is invalid") } - expectedKeys := map[string][]string{ - "not_started": {"state"}, - "ready_for_review": {"state", "subjectDigest", "subjectRefId"}, - "review_findings": {"assessmentSubjectDigest", "findingRefs", "state", "subjectDigest", "subjectRefId"}, - "review_passed": {"assessmentSubjectDigest", "state", "subjectDigest", "subjectRefId"}, - }[state] + stage := workflowCatalog.Stages[completedStageCount] + expectedKeys := checkpointFieldNames(definition, stage) if err := knownKeys(record, expectedKeys, "proofkit.workflow.checkpoint_fields"); err != nil { return nil, err } @@ -118,18 +108,19 @@ func admitCheckpoint(raw any, complete bool) (*checkpoint, error) { } } result := &checkpoint{State: state, FindingRefs: []string{}} - if state == "not_started" { - return result, nil - } - result.SubjectRefID, err = admitRefID(record["subjectRefId"]) - if err != nil { - return nil, err - } - result.SubjectDigest, err = admitDigest(record["subjectDigest"]) - if err != nil { - return nil, err + if checkpointRequiresSubject(definition, stage) { + var err error + result.SubjectRefID, err = admitRefID(record["subjectRefId"]) + if err != nil { + return nil, err + } + result.SubjectDigest, err = admitDigest(record["subjectDigest"]) + if err != nil { + return nil, err + } } - if state == "review_findings" || state == "review_passed" { + if definition.RequiresAssessment { + var err error result.AssessmentSubjectDigest, err = admitDigest(record["assessmentSubjectDigest"]) if err != nil { return nil, err @@ -138,7 +129,8 @@ func admitCheckpoint(raw any, complete bool) (*checkpoint, error) { return nil, reject("proofkit.workflow.assessment_digest_mismatch", "assessmentSubjectDigest must equal subjectDigest") } } - if state == "review_findings" { + if definition.RequiresFindingRefs { + var err error result.FindingRefs, err = admitRefIDArray(record["findingRefs"], "finding refs", maxFindings) if err != nil { return nil, err @@ -290,7 +282,7 @@ func validateGlobalReferences(input admittedInput) error { return reject("proofkit.workflow.governing_authority", "governingAuthorityRefId must resolve an authority ref") } } - if input.Checkpoint == nil || input.Checkpoint.State == "not_started" { + if input.Checkpoint == nil || input.Checkpoint.SubjectRefID == "" { return nil } subject, ok := byID[input.Checkpoint.SubjectRefID] diff --git a/internal/command/changeworkflowplan/agent_envelope.go b/internal/command/changeworkflowplan/agent_envelope.go index 9b660a9..bd451da 100644 --- a/internal/command/changeworkflowplan/agent_envelope.go +++ b/internal/command/changeworkflowplan/agent_envelope.go @@ -51,6 +51,10 @@ func agentEnvelope(value projection) (map[string]any, error) { if value.Decision.SuccessorStateDelta != nil { action["successorStateDelta"] = successorValue(*value.Decision.SuccessorStateDelta) } + if value.Input.Checkpoint != nil && value.Input.Checkpoint.SubjectRefID != "" { + action["subjectDigest"] = value.Input.Checkpoint.SubjectDigest + action["subjectRefId"] = value.Input.Checkpoint.SubjectRefID + } actionPlan = append(actionPlan, action) } } else { @@ -141,8 +145,28 @@ func envelopeBlockers(value projection) ([]map[string]any, []map[string]any) { "questionId": "proofkit.change-workflow-plan.clarify-governing-authority", }) } + if value.Decision.RequiresSubject && (value.Input.Checkpoint == nil || value.Input.Checkpoint.SubjectRefID == "") { + retainedRefIDs := contextRefIDs(value.Closure.Retained) + blocked = append(blocked, map[string]any{ + "description": "The active workflow action requires one admitted artifact subject and equal digest.", + "evidenceRefs": stringsValue(retainedRefIDs), + "nonClaim": "A declared artifact subject does not prove its content, freshness, review state, or suitability.", + "owner": "consumer_repository", + "preconditionId": "proofkit.change-workflow-plan.missing-action-subject", + }) + clarifications = append(clarifications, map[string]any{ + "askWhen": "The selected action requires a subject but the admitted checkpoint has no artifact identity.", + "blocking": true, + "evidenceRefs": stringsValue(retainedRefIDs), + "expectedAnswerKind": "artifact_subject_ref", + "nonClaim": "Supplying an artifact ref does not authenticate or approve the referenced subject.", + "owner": "consumer_repository", + "question": "Which admitted artifact ref and digest identify the exact subject for this action?", + "questionId": "proofkit.change-workflow-plan.clarify-action-subject", + }) + } witnessRefIDs := contextRefIDsOfKind(value.Closure.Retained, "witness") - if value.Decision.Action == "verify" && len(witnessRefIDs) == 0 { + if value.Decision.RequiresWitness && len(witnessRefIDs) == 0 { retainedRefIDs := contextRefIDs(value.Closure.Retained) blocked = append(blocked, map[string]any{ "description": "Verification requires a retained caller-owned witness ref; use native-evidence-guidance to define repository-specific positive controls and near-miss falsifiers.", diff --git a/internal/command/changeworkflowplan/catalog.go b/internal/command/changeworkflowplan/catalog.go index 2af4673..4b1e555 100644 --- a/internal/command/changeworkflowplan/catalog.go +++ b/internal/command/changeworkflowplan/catalog.go @@ -3,19 +3,25 @@ package changeworkflowplan const workflowProfileID = "proofkit.reviewed-change.v1" type stageDefinition struct { - ID string - FirstAction string + ID string + FirstAction string + RequiresIncomingSubject bool } type checkpointDefinition struct { - State string - Action string - UseStageAction bool + State string + Action string + UseStageAction bool + RequiresSubject bool + RequiresAssessment bool + RequiresFindingRefs bool } type actionProfile struct { CandidateAction string + EmitsSuccessorDelta bool ExpectedNextCheckpoint string + RequiresWitness bool StopCondition string } @@ -28,18 +34,18 @@ type workflowCatalogDefinition struct { var workflowCatalog = workflowCatalogDefinition{ Stages: []stageDefinition{ {ID: "architecture", FirstAction: "author"}, - {ID: "design", FirstAction: "author"}, - {ID: "implementation_plan", FirstAction: "author"}, - {ID: "implementation", FirstAction: "implement"}, - {ID: "verification", FirstAction: "verify"}, - {ID: "pull_request", FirstAction: "open_pull_request"}, - {ID: "closeout", FirstAction: "closeout"}, + {ID: "design", FirstAction: "author", RequiresIncomingSubject: true}, + {ID: "implementation_plan", FirstAction: "author", RequiresIncomingSubject: true}, + {ID: "implementation", FirstAction: "implement", RequiresIncomingSubject: true}, + {ID: "verification", FirstAction: "verify", RequiresIncomingSubject: true}, + {ID: "pull_request", FirstAction: "open_pull_request", RequiresIncomingSubject: true}, + {ID: "closeout", FirstAction: "closeout", RequiresIncomingSubject: true}, }, CheckpointActions: []checkpointDefinition{ {State: "not_started", UseStageAction: true}, - {State: "ready_for_review", Action: "review"}, - {State: "review_findings", Action: "repair"}, - {State: "review_passed", Action: "accept_stage"}, + {State: "ready_for_review", Action: "review", RequiresSubject: true}, + {State: "review_findings", Action: "repair", RequiresSubject: true, RequiresAssessment: true, RequiresFindingRefs: true}, + {State: "review_passed", Action: "accept_stage", RequiresSubject: true, RequiresAssessment: true}, }, ActionProfiles: map[string]actionProfile{ "author": { @@ -56,6 +62,7 @@ var workflowCatalog = workflowCatalogDefinition{ CandidateAction: "Run the consuming repository's positive controls and independent near-miss falsifiers against the exact caller-declared subject.", StopCondition: "Stop after bounded verification evidence and its subject digest are ready for independent review.", ExpectedNextCheckpoint: "ready_for_review", + RequiresWitness: true, }, "open_pull_request": { CandidateAction: "Open a pull request only under consuming-repository policy for the exact reviewed head and expose its native checks without claiming merge authority.", @@ -81,6 +88,7 @@ var workflowCatalog = workflowCatalogDefinition{ CandidateAction: "Apply only the reported successorStateDelta to the prior snapshot, preserve all context fields byte-for-byte, and submit the merged snapshot for ordinary admission.", StopCondition: "Stop after constructing the merged immutable snapshot; do not infer execution, approval, merge, or release from stage acceptance.", ExpectedNextCheckpoint: "successor_state_delta", + EmitsSuccessorDelta: true, }, }, } @@ -89,3 +97,39 @@ func actionProfileFor(action string) (actionProfile, bool) { profile, ok := workflowCatalog.ActionProfiles[action] return profile, ok } + +func checkpointDefinitionFor(state string) (checkpointDefinition, bool) { + for _, definition := range workflowCatalog.CheckpointActions { + if definition.State == state { + return definition, true + } + } + return checkpointDefinition{}, false +} + +func checkpointRequiresSubject(definition checkpointDefinition, stage stageDefinition) bool { + return definition.RequiresSubject || definition.UseStageAction && stage.RequiresIncomingSubject +} + +func checkpointFieldNames(definition checkpointDefinition, stage stageDefinition) []string { + fields := []string{"state"} + if checkpointRequiresSubject(definition, stage) { + fields = append(fields, "subjectDigest", "subjectRefId") + } + if definition.RequiresAssessment { + fields = append(fields, "assessmentSubjectDigest") + } + if definition.RequiresFindingRefs { + fields = append(fields, "findingRefs") + } + return fields +} + +func initialCheckpointDefinition() checkpointDefinition { + for _, definition := range workflowCatalog.CheckpointActions { + if definition.UseStageAction { + return definition + } + } + panic("workflow catalog has no initial checkpoint definition") +} diff --git a/internal/command/changeworkflowplan/context_closure.go b/internal/command/changeworkflowplan/context_closure.go index 97eff8a..29f7a04 100644 --- a/internal/command/changeworkflowplan/context_closure.go +++ b/internal/command/changeworkflowplan/context_closure.go @@ -12,7 +12,7 @@ func leastClosure(input admittedInput) (closureResult, error) { if input.GoverningAuthorityRefID != nil { seeds = append(seeds, *input.GoverningAuthorityRefID) } - if input.Checkpoint != nil && input.Checkpoint.State != "not_started" { + if input.Checkpoint != nil && input.Checkpoint.SubjectRefID != "" { seeds = append(seeds, input.Checkpoint.SubjectRefID) seeds = append(seeds, input.Checkpoint.FindingRefs...) } diff --git a/internal/command/changeworkflowplan/decision.go b/internal/command/changeworkflowplan/decision.go index fca2422..1f1d4c2 100644 --- a/internal/command/changeworkflowplan/decision.go +++ b/internal/command/changeworkflowplan/decision.go @@ -10,12 +10,19 @@ func buildCheckpointRelation() []stateRow { if checkpoint.UseStageAction { action = stage.FirstAction } + profile, ok := actionProfileFor(action) + if !ok { + panic("workflow catalog action has no profile") + } rows = append(rows, stateRow{ Action: action, ActiveStageID: stage.ID, CheckpointState: checkpoint.State, CompletedCount: stageIndex, + EmitsSuccessor: profile.EmitsSuccessorDelta, OutputKind: "next_action", + RequiresSubject: checkpointRequiresSubject(checkpoint, stage), + RequiresWitness: profile.RequiresWitness, }) } } @@ -41,12 +48,20 @@ func decide(input admittedInput) (stateDecision, error) { ActiveStageID: row.ActiveStageID, CheckpointState: row.CheckpointState, OutputKind: row.OutputKind, + RequiresSubject: row.RequiresSubject, + RequiresWitness: row.RequiresWitness, } - if row.Action == "accept_stage" { + if row.EmitsSuccessor { completed := append(cloneStrings(input.CompletedStageIDs), row.ActiveStageID) var nextCheckpoint *checkpoint if len(completed) < len(workflowCatalog.Stages) { - nextCheckpoint = &checkpoint{State: "not_started", FindingRefs: []string{}} + initial := initialCheckpointDefinition() + nextCheckpoint = &checkpoint{ + FindingRefs: []string{}, + State: initial.State, + SubjectDigest: input.Checkpoint.SubjectDigest, + SubjectRefID: input.Checkpoint.SubjectRefID, + } } decision.SuccessorStateDelta = &successorStateDelta{Checkpoint: nextCheckpoint, CompletedStageIDs: completed} } diff --git a/internal/command/changeworkflowplan/model.go b/internal/command/changeworkflowplan/model.go index 12e5ca5..1ec41e8 100644 --- a/internal/command/changeworkflowplan/model.go +++ b/internal/command/changeworkflowplan/model.go @@ -55,6 +55,8 @@ type stateDecision struct { Action string ActiveStageID string CheckpointState string + RequiresSubject bool + RequiresWitness bool OutputKind string SuccessorStateDelta *successorStateDelta } @@ -64,7 +66,10 @@ type stateRow struct { ActiveStageID string CheckpointState string CompletedCount int + EmitsSuccessor bool OutputKind string + RequiresSubject bool + RequiresWitness bool Terminal bool } diff --git a/internal/command/changeworkflowplan/prompt.go b/internal/command/changeworkflowplan/prompt.go index 366f305..c42f72b 100644 --- a/internal/command/changeworkflowplan/prompt.go +++ b/internal/command/changeworkflowplan/prompt.go @@ -37,7 +37,7 @@ func buildPrompt(input admittedInput, decision stateDecision, closure closureRes if input.GoverningAuthorityRefID != nil { coordinates["governingAuthorityRefId"] = *input.GoverningAuthorityRefID } - if input.Checkpoint != nil && input.Checkpoint.State != "not_started" { + if input.Checkpoint != nil && input.Checkpoint.SubjectRefID != "" { coordinates["subjectDigest"] = input.Checkpoint.SubjectDigest coordinates["subjectRefId"] = input.Checkpoint.SubjectRefID } diff --git a/internal/command/changeworkflowplan/prompt_test.go b/internal/command/changeworkflowplan/prompt_test.go index be261df..5f5c331 100644 --- a/internal/command/changeworkflowplan/prompt_test.go +++ b/internal/command/changeworkflowplan/prompt_test.go @@ -19,6 +19,7 @@ var expectedWorkflowActionProfiles = map[string]actionProfile{ "verify": { CandidateAction: "Run the consuming repository's positive controls and independent near-miss falsifiers against the exact caller-declared subject.", ExpectedNextCheckpoint: "ready_for_review", + RequiresWitness: true, StopCondition: "Stop after bounded verification evidence and its subject digest are ready for independent review.", }, "open_pull_request": { @@ -43,6 +44,7 @@ var expectedWorkflowActionProfiles = map[string]actionProfile{ }, "accept_stage": { CandidateAction: "Apply only the reported successorStateDelta to the prior snapshot, preserve all context fields byte-for-byte, and submit the merged snapshot for ordinary admission.", + EmitsSuccessorDelta: true, ExpectedNextCheckpoint: "successor_state_delta", StopCondition: "Stop after constructing the merged immutable snapshot; do not infer execution, approval, merge, or release from stage acceptance.", }, diff --git a/internal/command/changeworkflowplan/report.go b/internal/command/changeworkflowplan/report.go index cb8a44b..1773d41 100644 --- a/internal/command/changeworkflowplan/report.go +++ b/internal/command/changeworkflowplan/report.go @@ -124,7 +124,22 @@ func successorValue(delta successorStateDelta) map[string]any { "checkpoint": nil, } if delta.Checkpoint != nil { - result["checkpoint"] = map[string]any{"state": delta.Checkpoint.State} + result["checkpoint"] = checkpointValue(*delta.Checkpoint) + } + return result +} + +func checkpointValue(value checkpoint) map[string]any { + result := map[string]any{"state": value.State} + if value.SubjectRefID != "" { + result["subjectDigest"] = value.SubjectDigest + result["subjectRefId"] = value.SubjectRefID + } + if value.AssessmentSubjectDigest != "" { + result["assessmentSubjectDigest"] = value.AssessmentSubjectDigest + } + if len(value.FindingRefs) > 0 { + result["findingRefs"] = stringsValue(value.FindingRefs) } return result } diff --git a/internal/command/changeworkflowplan/state_test.go b/internal/command/changeworkflowplan/state_test.go index 8f147cb..6d0a2a9 100644 --- a/internal/command/changeworkflowplan/state_test.go +++ b/internal/command/changeworkflowplan/state_test.go @@ -133,22 +133,36 @@ func TestWorkflowCheckpointPredicates(t *testing.T) { input["checkpoint"] = checkpointValue requireReject(t, input) } + missingSubject := inputForStage(1, "not_started") + missingSubject["checkpoint"] = map[string]any{"state": "not_started"} + requireReject(t, missingSubject) }) t.Run("merged_successor_admitted", func(t *testing.T) { - for index := range workflowCatalog.Stages { + stages := []string{"architecture", "design", "implementation_plan", "implementation", "verification", "pull_request", "closeout"} + for index := range stages { prior := inputForStage(index, "review_passed") plan := requireBuild(t, prior) - merged, err := MergeSuccessor(prior, plan["successorStateDelta"].(map[string]any)) + delta := plan["successorStateDelta"].(map[string]any) + assertExactSuccessorDelta(t, delta, stages, index) + merged, err := MergeSuccessor(prior, delta) if err != nil { t.Fatalf("stage %d successor rejected: %v", index, err) } - if _, err := Build(merged); err != nil { + next, err := Build(merged) + if err != nil { t.Fatalf("stage %d merged successor rejected: %v", index, err) } + if index+1 < len(stages) && next["activeStageId"] != stages[index+1] { + t.Fatalf("stage %d successor remained at %v", index, next["activeStageId"]) + } + if index+1 == len(stages) && next["outputKind"] != "workflow_complete" { + t.Fatalf("final successor outputKind=%v", next["outputKind"]) + } } }) t.Run("agent_envelope_transition_is_executable", func(t *testing.T) { - for index := range workflowCatalog.Stages { + stages := []string{"architecture", "design", "implementation_plan", "implementation", "verification", "pull_request", "closeout"} + for index := range stages { prior := inputForStage(index, "review_passed") envelope, err := BuildAgentEnvelope(prior) if err != nil { @@ -166,6 +180,7 @@ func TestWorkflowCheckpointPredicates(t *testing.T) { if !ok { t.Fatalf("stage %d envelope omits successorStateDelta: %v", index, action) } + assertExactSuccessorDelta(t, delta, stages, index) merged, err := MergeSuccessor(prior, delta) if err != nil { t.Fatalf("stage %d envelope delta rejected: %v", index, err) @@ -224,7 +239,10 @@ func TestWorkflowCheckpointPredicates(t *testing.T) { if action["owner"] != "ctx.authority" { t.Fatalf("verification action lost owner or witness identity: %v", action) } - requireEqual(t, action["evidenceRefs"], []any{"ctx.authority", "ctx.witness"}) + requireEqual(t, action["evidenceRefs"], []any{"ctx.artifact", "ctx.authority", "ctx.witness"}) + if action["subjectRefId"] != "ctx.artifact" || action["subjectDigest"] != testDigest { + t.Fatalf("verification action lost exact subject identity: %v", action) + } }) t.Run("review_passed_accepts", func(t *testing.T) { for index := range workflowCatalog.Stages { @@ -284,9 +302,31 @@ func TestWorkflowCheckpointPredicates(t *testing.T) { }) } +func assertExactSuccessorDelta(t *testing.T, delta map[string]any, stages []string, stageIndex int) { + t.Helper() + wantCompleted := make([]any, stageIndex+1) + for index := 0; index <= stageIndex; index++ { + wantCompleted[index] = stages[index] + } + requireEqual(t, delta["completedStageIds"], wantCompleted) + if stageIndex+1 == len(stages) { + if delta["checkpoint"] != nil { + t.Fatalf("final successor checkpoint=%v, want nil", delta["checkpoint"]) + } + return + } + wantCheckpoint := map[string]any{ + "state": "not_started", + "subjectDigest": testDigest, + "subjectRefId": "ctx.artifact", + } + requireEqual(t, delta["checkpoint"], wantCheckpoint) +} + func verificationInput(withWitness bool) map[string]any { input := inputForStage(4, "not_started") contextRefs := []any{ + contextValue("ctx.artifact", "artifact", testDigest, nil), contextValue("ctx.authority", "authority", "sha256:1111111111111111111111111111111111111111111111111111111111111111", nil), } required := []any{} diff --git a/internal/command/changeworkflowplan/test_helpers_test.go b/internal/command/changeworkflowplan/test_helpers_test.go index f57eda1..d7763a5 100644 --- a/internal/command/changeworkflowplan/test_helpers_test.go +++ b/internal/command/changeworkflowplan/test_helpers_test.go @@ -58,9 +58,12 @@ func inputForStage(stageIndex int, checkpointState string) map[string]any { completed[index] = workflowCatalog.Stages[index].ID } input["completedStageIds"] = completed - if checkpointState != "not_started" { + if checkpointState != initialCheckpointDefinition().State { input = reviewInput(checkpointState) input["completedStageIds"] = completed + } else if stageIndex > 0 { + input["checkpoint"] = map[string]any{"state": checkpointState, "subjectDigest": testDigest, "subjectRefId": "ctx.artifact"} + input["contextRefs"] = []any{contextValue("ctx.artifact", "artifact", testDigest, nil)} } return input } diff --git a/internal/command/changeworkflowplan/text.go b/internal/command/changeworkflowplan/text.go index 581553c..c1a993c 100644 --- a/internal/command/changeworkflowplan/text.go +++ b/internal/command/changeworkflowplan/text.go @@ -21,6 +21,18 @@ func textProjection(value projection) []TextLine { TextLine{Label: "Stop condition", Value: prompt["stopCondition"].(string)}, TextLine{Label: "Expected next checkpoint", Value: prompt["expectedNextCheckpoint"].(string)}, ) + if delta := value.Decision.SuccessorStateDelta; delta != nil { + lines = append(lines, TextLine{Label: "Successor completed stages", Value: joinStrings(delta.CompletedStageIDs)}) + if delta.Checkpoint == nil { + lines = append(lines, TextLine{Label: "Successor checkpoint", Value: "none"}) + } else { + lines = append(lines, + TextLine{Label: "Successor checkpoint", Value: delta.Checkpoint.State}, + TextLine{Label: "Successor subject ref", Value: delta.Checkpoint.SubjectRefID}, + TextLine{Label: "Successor subject digest", Value: delta.Checkpoint.SubjectDigest}, + ) + } + } } else { lines = append(lines, TextLine{Label: "Checkpoint", Value: "none"}, diff --git a/internal/command/changeworkflowplan/text_test.go b/internal/command/changeworkflowplan/text_test.go index fabaae2..85356fb 100644 --- a/internal/command/changeworkflowplan/text_test.go +++ b/internal/command/changeworkflowplan/text_test.go @@ -15,7 +15,7 @@ func TestWorkflowTextPredicates(t *testing.T) { if len(lines) > maxTextLines || len(result.Text) > maxTextBytes { t.Fatalf("text exceeds bounds: %d lines, %d bytes", len(lines), len(result.Text)) } - for _, coordinate := range []string{"architecture", "accept_stage", "review_passed", "ctx.authority", "successor_state_delta", "Omitted context: 1", "Non-claim:"} { + for _, coordinate := range []string{"architecture", "accept_stage", "review_passed", "ctx.authority", "successor_state_delta", "Successor completed stages: architecture", "Successor checkpoint: not_started", "Successor subject ref: ctx.artifact", "Successor subject digest: " + testDigest, "Omitted context: 1", "Non-claim:"} { if !strings.Contains(result.Text, coordinate) { t.Fatalf("text omits coordinate %q: %s", coordinate, result.Text) } @@ -36,7 +36,7 @@ func TestWorkflowTextProjectionParity(t *testing.T) { if rendered != result.Text { t.Fatalf("structured projection drifted from canonical text\ngot: %q\nwant: %q", rendered, result.Text) } - if result.Text != "Change workflow plan\nOutput: next_action\nCompleted stages: none\nActive stage: architecture\nAction: accept_stage\nCheckpoint: review_passed\nOwner or escalation: ctx.authority\nStop condition: Stop after constructing the merged immutable snapshot; do not infer execution, approval, merge, or release from stage acceptance.\nExpected next checkpoint: successor_state_delta\nOmitted context: 1\nNon-claim: Change workflow plans do not approve repository edits, merge, release, rollout, or production readiness.\n" { + if result.Text != "Change workflow plan\nOutput: next_action\nCompleted stages: none\nActive stage: architecture\nAction: accept_stage\nCheckpoint: review_passed\nOwner or escalation: ctx.authority\nStop condition: Stop after constructing the merged immutable snapshot; do not infer execution, approval, merge, or release from stage acceptance.\nExpected next checkpoint: successor_state_delta\nSuccessor completed stages: architecture\nSuccessor checkpoint: not_started\nSuccessor subject ref: ctx.artifact\nSuccessor subject digest: "+testDigest+"\nOmitted context: 1\nNon-claim: Change workflow plans do not approve repository edits, merge, release, rollout, or production readiness.\n" { t.Fatalf("canonical plain bytes changed: %q", result.Text) } } diff --git a/internal/command/stackpreset/preset_ids_generated.go b/internal/command/stackpreset/preset_ids_generated.go index e4133fe..5243484 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 = "5fc85982c751d3a384193e57a59a6eedec5a1b0dce3b58bcb143228a68070427" +const presetContractSourceSHA256 = "be6fef4e32ab624992480f537fd55f3d53f352e712486d34f95ee67fee32b842" var presetIDs = []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"} diff --git a/internal/kernel/processgroup/process_other.go b/internal/kernel/processgroup/process_other.go index c5f3539..3a095ed 100644 --- a/internal/kernel/processgroup/process_other.go +++ b/internal/kernel/processgroup/process_other.go @@ -7,6 +7,7 @@ import ( "errors" "os" "os/exec" + "time" ) // Configure retains exec.CommandContext's direct-process cancellation on @@ -25,3 +26,9 @@ func Terminate(command *exec.Cmd) error { } return err } + +// TerminateAndWait terminates the direct child. The caller invokes it after +// exec.Cmd.Run has already waited for that child on unsupported platforms. +func TerminateAndWait(command *exec.Cmd, _ time.Duration) error { + return Terminate(command) +} diff --git a/internal/kernel/processgroup/process_unix.go b/internal/kernel/processgroup/process_unix.go index d9313b2..9ab7024 100644 --- a/internal/kernel/processgroup/process_unix.go +++ b/internal/kernel/processgroup/process_unix.go @@ -5,11 +5,15 @@ package processgroup import ( "errors" + "fmt" "os" "os/exec" "syscall" + "time" ) +const processGroupProbeInterval = 10 * time.Millisecond + // Configure places a command in a new process group and makes context // cancellation terminate that group. func Configure(command *exec.Cmd) { @@ -29,3 +33,36 @@ func Terminate(command *exec.Cmd) error { } return err } + +// TerminateAndWait kills a configured process group after the command parent +// has been reaped and proves group absence before returning success. +func TerminateAndWait(command *exec.Cmd, timeout time.Duration) error { + if command.Process == nil { + return nil + } + if timeout <= 0 { + return fmt.Errorf("process-group cleanup timeout must be positive") + } + processGroupID := command.Process.Pid + if err := Terminate(command); err != nil { + return err + } + deadline := time.Now().Add(timeout) + for { + err := syscall.Kill(-processGroupID, syscall.Signal(0)) + if errors.Is(err, syscall.ESRCH) { + return nil + } + if err != nil && !errors.Is(err, syscall.EPERM) { + return fmt.Errorf("probe process group %d: %w", processGroupID, err) + } + remaining := time.Until(deadline) + if remaining <= 0 { + return fmt.Errorf("process group %d remained present after cleanup timeout", processGroupID) + } + if remaining > processGroupProbeInterval { + remaining = processGroupProbeInterval + } + time.Sleep(remaining) + } +} diff --git a/internal/tools/coveragemetrics/main.go b/internal/tools/coveragemetrics/main.go index a68f1ae..fdce137 100644 --- a/internal/tools/coveragemetrics/main.go +++ b/internal/tools/coveragemetrics/main.go @@ -257,8 +257,25 @@ func validateRequiredBindingWitnessSelectors(bindings bindingFile) error { scenarioID string } required := map[inventoryKey][]string{ - {"REQ-PROOFKIT-PACKAGE-001", "proofkit.package-boundary.root-export-and-deep-import-denial"}: {"TestVerifyRootPackageRejectsEachForbiddenRootEntry"}, - {"REQ-PROOFKIT-PACKAGE-002", "proofkit.package-boundary.launcher-profile-admission"}: {"TestLauncherProfileAdmissionMatrix"}, + {"REQ-PROOFKIT-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"}: {"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.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"}: {"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", @@ -490,6 +507,23 @@ func validateRequiredBindingWitnessSelectors(bindings bindingFile) error { }, } 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.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", @@ -550,6 +584,9 @@ func validateRequiredBindingWitnessSelectors(bindings bindingFile) error { for _, binding := range bindings.Bindings { key := inventoryKey{requirementID: binding.RequirementID, scenarioID: binding.ScenarioID} want, isRequired := required[key] + if strings.HasPrefix(binding.RequirementID, "REQ-PROOFKIT-WORKFLOW-") && !isRequired { + return fmt.Errorf("workflow binding is absent from the exact independent-falsifier inventory: %s/%s", binding.RequirementID, binding.ScenarioID) + } if !isRequired { continue } diff --git a/internal/tools/coveragemetrics/main_test.go b/internal/tools/coveragemetrics/main_test.go index ba5aa41..e621f95 100644 --- a/internal/tools/coveragemetrics/main_test.go +++ b/internal/tools/coveragemetrics/main_test.go @@ -589,6 +589,23 @@ func TestBindingWitnessSelectorsRequireExactCriticalInventories(t *testing.T) { } for _, scenarioID := range []string{ + "proofkit.agent-workflow.bounded-safe-text", + "proofkit.agent-workflow.cli-presentation-capability-product", + "proofkit.agent-workflow.installed-carrier-smoke-closure", + "proofkit.agent-workflow.native-evidence-guidance-purity", + "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.public-cli-relation-closure", + "proofkit.agent-workflow.pure-single-admission-owner", + "proofkit.agent-workflow.reference-closed-bounded-context", + "proofkit.agent-workflow.review-identity-closure", + "proofkit.agent-workflow.semantic-owner-minimality", + "proofkit.agent-workflow.semantic-owner-topology", + "proofkit.agent-workflow.stage-prefix-and-terminal-relation", + "proofkit.agent-workflow.style-strip-parity", + "proofkit.agent-workflow.total-checkpoint-successor-relation", + "proofkit.agent-workflow.version-edge-wire-observation", "proofkit.package-boundary.cli-output-root-witnesses", "proofkit.package-boundary.generated-command-caller-preservation", "proofkit.package-boundary.generated-command-field-inventory", @@ -671,7 +688,11 @@ func TestBindingWitnessSelectorsRequireExactCriticalInventories(t *testing.T) { mutated := cloneBindingFile(bindings) mutated.Bindings[index].ScenarioID += ".transferred" err := validateBindingWitnessSelectorsAtRoot(root, mutated) - if err == nil || !strings.Contains(err.Error(), "required independent-falsifier binding is missing") { + want := "required independent-falsifier binding is missing" + if strings.HasPrefix(bindings.Bindings[index].RequirementID, "REQ-PROOFKIT-WORKFLOW-") { + want = "workflow binding is absent from the exact independent-falsifier inventory" + } + if err == nil || !strings.Contains(err.Error(), want) { t.Fatalf("scenario-transfer error=%v", err) } }) @@ -693,6 +714,18 @@ func TestBindingWitnessSelectorsRequireExactCriticalInventories(t *testing.T) { }) } + t.Run("workflow/surplus-binding", func(t *testing.T) { + mutated := cloneBindingFile(bindings) + mutated.Bindings = append(mutated.Bindings, bindingScenario{ + RequirementID: "REQ-PROOFKIT-WORKFLOW-001", + ScenarioID: "proofkit.agent-workflow.uninventoried-surplus", + }) + err := validateBindingWitnessSelectorsAtRoot(root, mutated) + if err == nil || !strings.Contains(err.Error(), "workflow binding is absent from the exact independent-falsifier inventory") { + t.Fatalf("surplus workflow binding error=%v", err) + } + }) + for _, scenarioID := range []string{ "proofkit.package-boundary.cli-output-root-witnesses", "proofkit.package-boundary.generated-command-caller-preservation", diff --git a/internal/tools/releasechange/record_test.go b/internal/tools/releasechange/record_test.go index 950be99..590a129 100644 --- a/internal/tools/releasechange/record_test.go +++ b/internal/tools/releasechange/record_test.go @@ -8,6 +8,8 @@ import ( "slices" "strings" "testing" + + "github.com/research-engineering/agentic-proofkit/internal/command/jsonreportcliadaptersource" ) func TestAdmitAndRenderVersionBoundChangeRecord(t *testing.T) { @@ -209,7 +211,7 @@ var currentAdditions = []Change{ var currentMigrationSteps = []string{ "Ensure every agent-envelope command, context, and receipt ID is individually safe and unique, keep the three local identity domains pairwise disjoint, and remove or rename references to ambiguous local targets before adopting 0.5.0.", "Treat stderr as a bounded diagnostic class rather than an exact echo of caller or child-process text, and remove consumers that depend on raw forwarded diagnostics.", - "Ensure every stable-JSON string is a Unicode scalar sequence, accept deterministic escaping of pinned Unicode 15 control and format code points, and regenerate TypeScript adapters against generator proofkit.json-report-cli-adapter-source.typescript.v2.", + "Ensure every stable-JSON string is a Unicode scalar sequence, accept deterministic escaping of pinned Unicode 15 control and format code points, and regenerate TypeScript adapters against generator " + jsonreportcliadaptersource.TypeScriptGeneratorID + ".", } func validateCurrentChangeRecord(record Record, notes string) error { diff --git a/internal/tools/releasecloseoutinput/main.go b/internal/tools/releasecloseoutinput/main.go index 8bcf3bf..6ed9c2e 100644 --- a/internal/tools/releasecloseoutinput/main.go +++ b/internal/tools/releasecloseoutinput/main.go @@ -1,6 +1,7 @@ package main import ( + "archive/zip" "bytes" "context" "crypto/sha1" @@ -37,21 +38,25 @@ import ( var commandOracleValidateCurrent = commandoracle.ValidateCurrent const ( - completionID = "proofkit.release_closeout.current_package_gate" - ciProvenancePath = "artifacts/proofkit/ci-provenance.json" - cliContractPath = "proofkit/cli-contract.v2.json" - coverageMetricsPath = "artifacts/proofkit/coverage-metrics.json" - commandOracleRecordPath = commandoracle.RecordPath - proofReceiptReportPath = "artifacts/proofkit/self-hosting-proof-receipt-admission-report.json" - proofReceiptsPath = "artifacts/proofkit/self-hosting-proof-receipts.json" - producerReportPath = "artifacts/proofkit/self-hosting-receipt-producer-admission-report.json" - producerPolicyPath = "artifacts/proofkit/self-hosting-receipt-producer-admission.json" - specProofBundleReportPath = "artifacts/proofkit/self-hosting-spec-proof-bundle-admission-report.json" - specProofBundlePath = "artifacts/proofkit/self-hosting-spec-proof-bundle.json" - npmCandidateNonClaim = "Local npm package artifacts are candidate tarball evidence; they do not prove npm registry publication, registry install authority, or consumer adoption." - pypiPlannedNonClaim = "PyPI is not a dependency authority for this version until PyPI package evidence exists." - packageGateEnvironmentClass = "local-go-python" - pythonPackageName = "agentic-proofkit" + completionID = "proofkit.release_closeout.current_package_gate" + ciProvenancePath = "artifacts/proofkit/ci-provenance.json" + cliContractPath = "proofkit/cli-contract.v2.json" + coverageMetricsPath = "artifacts/proofkit/coverage-metrics.json" + commandOracleRecordPath = commandoracle.RecordPath + proofReceiptReportPath = "artifacts/proofkit/self-hosting-proof-receipt-admission-report.json" + proofReceiptsPath = "artifacts/proofkit/self-hosting-proof-receipts.json" + producerReportPath = "artifacts/proofkit/self-hosting-receipt-producer-admission-report.json" + producerPolicyPath = "artifacts/proofkit/self-hosting-receipt-producer-admission.json" + specProofBundleReportPath = "artifacts/proofkit/self-hosting-spec-proof-bundle-admission-report.json" + specProofBundlePath = "artifacts/proofkit/self-hosting-spec-proof-bundle.json" + npmCandidateNonClaim = "Local npm package artifacts are candidate tarball evidence; they do not prove npm registry publication, registry install authority, or consumer adoption." + pypiPlannedNonClaim = "PyPI is not a dependency authority for this version until PyPI package evidence exists." + packageGateEnvironmentClass = "local-go-python" + pythonPackageName = "agentic-proofkit" + pythonWheelBinaryEntry = "agentic_proofkit/bin/agentic-proofkit" + maxPythonWheelBytes int64 = 256 << 20 + maxPythonBinaryBytes int64 = 64 << 20 + maxPythonWheelEntries = 4096 ) type completionInput struct { @@ -101,9 +106,11 @@ type pythonPackageSet struct { } type pythonWheel struct { - Filename string `json:"filename"` - Name string `json:"name"` - Version string `json:"version"` + BinarySha256 string `json:"binarySha256"` + Filename string `json:"filename"` + Name string `json:"name"` + Sha256 string `json:"sha256"` + Version string `json:"version"` } type releaseManifest struct { @@ -1594,16 +1601,78 @@ func readPythonPackageSet(root string, path string, manifest packageJSON) ([]pyt return nil, false } for _, item := range packages.Packages { - if item.Name != pythonPackageName || item.Version != manifest.Version || item.Filename == "" { + if item.Name != pythonPackageName || item.Version != manifest.Version || item.Filename == "" || !isSHA256(item.Sha256) || !isSHA256(item.BinarySha256) { return packages.Packages, false } - if !fileExists(root, filepath.Join("artifacts", "pypi", item.Filename)) { + if !pythonWheelBytesMatch(root, item) { return packages.Packages, false } } return packages.Packages, true } +func pythonWheelBytesMatch(rootPath string, wheel pythonWheel) bool { + if filepath.Base(wheel.Filename) != wheel.Filename || filepath.Clean(wheel.Filename) != wheel.Filename || !filepath.IsLocal(wheel.Filename) { + return false + } + relativePath := filepath.Join("artifacts", "pypi", wheel.Filename) + root, err := os.OpenRoot(rootPath) + if err != nil { + return false + } + defer root.Close() + before, err := root.Lstat(relativePath) + if err != nil || before.Mode()&os.ModeSymlink != 0 || !before.Mode().IsRegular() || before.Size() <= 0 || before.Size() > maxPythonWheelBytes { + return false + } + file, err := root.Open(relativePath) + if err != nil { + return false + } + defer file.Close() + opened, err := file.Stat() + if err != nil || !os.SameFile(before, opened) || !opened.Mode().IsRegular() { + return false + } + wheelHash := sha256.New() + written, err := io.Copy(wheelHash, io.LimitReader(file, maxPythonWheelBytes+1)) + if err != nil || written != before.Size() || hex.EncodeToString(wheelHash.Sum(nil)) != wheel.Sha256 { + return false + } + reader, err := zip.NewReader(file, before.Size()) + if err != nil || len(reader.File) > maxPythonWheelEntries { + return false + } + seen := make(map[string]struct{}, len(reader.File)) + found := false + for _, entry := range reader.File { + if _, duplicate := seen[entry.Name]; duplicate { + return false + } + seen[entry.Name] = struct{}{} + if entry.Name != pythonWheelBinaryEntry { + continue + } + if found || entry.Mode()&os.ModeSymlink != 0 || !entry.FileInfo().Mode().IsRegular() || entry.UncompressedSize64 > uint64(maxPythonBinaryBytes) { + return false + } + found = true + member, err := entry.Open() + if err != nil { + return false + } + binaryHash := sha256.New() + binarySize, copyErr := io.Copy(binaryHash, io.LimitReader(member, maxPythonBinaryBytes+1)) + closeErr := member.Close() + if copyErr != nil || closeErr != nil || binarySize != int64(entry.UncompressedSize64) || hex.EncodeToString(binaryHash.Sum(nil)) != wheel.BinarySha256 { + return false + } + } + after, statErr := file.Stat() + pathAfter, pathErr := root.Lstat(relativePath) + return found && statErr == nil && pathErr == nil && pathAfter.Mode()&os.ModeSymlink == 0 && os.SameFile(before, after) && os.SameFile(after, pathAfter) && before.Size() == after.Size() && before.ModTime().Equal(after.ModTime()) +} + func releaseChecksumInventoriesMatch(root string, manifest packageJSON) bool { packRecords, err := readTypedJSON[[]packRecord](root, "artifacts/package/npm-pack.json") if err != nil || len(packRecords) != 1 { diff --git a/internal/tools/releasecloseoutinput/main_test.go b/internal/tools/releasecloseoutinput/main_test.go index 83ea650..6270c45 100644 --- a/internal/tools/releasecloseoutinput/main_test.go +++ b/internal/tools/releasecloseoutinput/main_test.go @@ -1,6 +1,7 @@ package main import ( + "archive/zip" "bytes" "context" "crypto/sha1" @@ -258,6 +259,26 @@ func TestBuildInputFailsClosedForEachBlockingEvidenceClass(t *testing.T) { }) }, }, + { + name: "stale Python wheel digest", + criterionID: "proofkit.release_closeout.python_wrappers", + mutate: func(root string) { + path := filepath.Join(root, "artifacts", "pypi", "python-packages.json") + record := readJSONMap(t, path) + record["packages"].([]any)[0].(map[string]any)["sha256"] = strings.Repeat("0", 64) + writeJSON(t, path, record) + }, + }, + { + name: "stale Python binary digest", + criterionID: "proofkit.release_closeout.python_wrappers", + mutate: func(root string) { + path := filepath.Join(root, "artifacts", "pypi", "python-packages.json") + record := readJSONMap(t, path) + record["packages"].([]any)[0].(map[string]any)["binarySha256"] = strings.Repeat("0", 64) + writeJSON(t, path, record) + }, + }, { name: "missing SBOM", criterionID: "proofkit.release_closeout.manifest_and_sbom", @@ -1305,14 +1326,14 @@ func populateCompleteFixture(t *testing.T, root string) { runFixtureGit(t, root, "add", ".gitignore", "go.mod", "go.sum", "package.json", "proofkit", "release", "source.txt") runFixtureGit(t, root, "commit", "-m", "fixture") writeNPMArtifact(t, root, testNPMTarballName, []byte("package")) + wheelSHA256, binarySHA256 := writePythonWheelFixture(t, root) writeJSON(t, filepath.Join(root, "artifacts", "pypi", "python-packages.json"), map[string]any{ "packageName": testPythonPackageName, "packageVersion": "1.2.3", "packages": []any{ - map[string]any{"name": testPythonPackageName, "version": "1.2.3", "filename": testPythonWheelName}, + map[string]any{"binarySha256": binarySHA256, "filename": testPythonWheelName, "name": testPythonPackageName, "sha256": wheelSHA256, "version": "1.2.3"}, }, }) - writeFile(t, filepath.Join(root, "artifacts", "pypi", testPythonWheelName), "wheel") changeRecord, err := releasechange.Read(filepath.Join(root, filepath.FromSlash(releasechange.RecordPath))) if err != nil { t.Fatal(err) @@ -1333,6 +1354,42 @@ func populateCompleteFixture(t *testing.T, root string) { writeLocalSelfEvidence(t, root, execution) } +func writePythonWheelFixture(t *testing.T, root string) (string, string) { + t.Helper() + path := filepath.Join(root, "artifacts", "pypi", testPythonWheelName) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + file, err := os.Create(path) + if err != nil { + t.Fatal(err) + } + wheel := zip.NewWriter(file) + header := &zip.FileHeader{Name: pythonWheelBinaryEntry, Method: zip.Store} + header.SetMode(0o755) + member, err := wheel.CreateHeader(header) + if err != nil { + t.Fatal(err) + } + binary := []byte("fixture-binary") + if _, err := member.Write(binary); err != nil { + t.Fatal(err) + } + if err := wheel.Close(); err != nil { + t.Fatal(err) + } + if err := file.Close(); err != nil { + t.Fatal(err) + } + content, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + wheelDigest := sha256.Sum256(content) + binaryDigest := sha256.Sum256(binary) + return hex.EncodeToString(wheelDigest[:]), hex.EncodeToString(binaryDigest[:]) +} + func releaseChangeRecordFixture() map[string]any { return map[string]any{ "additions": []any{map[string]any{"changeId": "proofkit.release.fixture", "summary": "Exercise release change projection."}}, diff --git a/internal/tools/releasemanifest/artifact_snapshot.go b/internal/tools/releasemanifest/artifact_snapshot.go index b34deeb..8e937a1 100644 --- a/internal/tools/releasemanifest/artifact_snapshot.go +++ b/internal/tools/releasemanifest/artifact_snapshot.go @@ -15,8 +15,9 @@ import ( const maxReleaseArtifactBytes int64 = 512 << 20 type releaseArtifactSnapshot struct { - root string - bySource map[string]releaseArtifact + root string + bySource map[string]releaseArtifact + pythonWheelIdentities map[string]pythonWheelIdentity } type releaseArtifact struct { @@ -76,6 +77,7 @@ func (snapshot *releaseArtifactSnapshot) Close() error { } snapshot.root = "" snapshot.bySource = nil + snapshot.pythonWheelIdentities = nil return nil } @@ -83,12 +85,63 @@ func (snapshot *releaseArtifactSnapshot) VerifyCrossCarrierBinaryIdentity(localR if err := snapshot.requireOpen(); err != nil { return err } - return verifyCrossCarrierBinaryIdentity( + snapshot.pythonWheelIdentities = nil + identities, err := verifyCrossCarrierBinaryIdentity( filepath.Join(snapshot.root, "artifacts", "package"), filepath.Join(snapshot.root, "artifacts", "pypi"), localRecords, pythonPackages, ) + if err != nil { + return err + } + if pythonPackages == nil { + snapshot.pythonWheelIdentities = nil + return nil + } + if len(identities) != len(pythonPackages.Packages) { + return fmt.Errorf("python wheel identity decoder lacks exact package closure") + } + for _, record := range pythonPackages.Packages { + identity, ok := identities[record.Filename] + if !ok { + return fmt.Errorf("python wheel identity decoder lacks an expected package") + } + artifact, ok := snapshot.bySource[filepath.Join("artifacts", "pypi", record.Filename)] + if !ok { + return fmt.Errorf("release artifact snapshot lacks an expected Python wheel") + } + identity.wheelSHA256 = artifact.sha256 + if record.Sha256 != identity.wheelSHA256 || record.BinarySha256 != identity.binarySHA256 { + return fmt.Errorf("python package-set digest claims do not match immutable wheel bytes") + } + identities[record.Filename] = identity + } + snapshot.pythonWheelIdentities = identities + return nil +} + +func (snapshot *releaseArtifactSnapshot) AdmittedPythonPackageSet(pythonPackages *pythonPackageSet) (*pythonPackageSet, error) { + if err := snapshot.requireOpen(); err != nil { + return nil, err + } + if pythonPackages == nil { + return nil, nil + } + if len(snapshot.pythonWheelIdentities) != len(pythonPackages.Packages) { + return nil, fmt.Errorf("python wheel bytes have not passed exact immutable identity admission") + } + admitted := *pythonPackages + admitted.Packages = append([]pythonWheelRecord(nil), pythonPackages.Packages...) + for index := range admitted.Packages { + identity, ok := snapshot.pythonWheelIdentities[admitted.Packages[index].Filename] + if !ok { + return nil, fmt.Errorf("python wheel identity admission lacks an expected package") + } + admitted.Packages[index].Sha256 = identity.wheelSHA256 + admitted.Packages[index].BinarySha256 = identity.binarySHA256 + } + return &admitted, nil } func (snapshot *releaseArtifactSnapshot) ReleaseEvidence(localRecords []packRecord, pythonPackages *pythonPackageSet) ([]assetEvidence, []string, []string, error) { diff --git a/internal/tools/releasemanifest/artifact_snapshot_test.go b/internal/tools/releasemanifest/artifact_snapshot_test.go index 43a032c..7a43898 100644 --- a/internal/tools/releasemanifest/artifact_snapshot_test.go +++ b/internal/tools/releasemanifest/artifact_snapshot_test.go @@ -1,6 +1,8 @@ package main import ( + "crypto/sha256" + "fmt" "os" "path/filepath" "strings" @@ -33,8 +35,20 @@ func TestReleaseArtifactSnapshotOwnsOneImmutableEpoch(t *testing.T) { packages := &pythonPackageSet{Packages: make([]pythonWheelRecord, 0, len(releaseplatform.Targets()))} for _, target := range releaseplatform.Targets() { filename := target.PlatformSuffix + ".whl" - writeWheelCarrier(t, filepath.Join("artifacts", "pypi", filename), carrierBinary(target.PlatformSuffix)) - packages.Packages = append(packages.Packages, pythonWheelRecord{Filename: filename, PlatformSuffix: target.PlatformSuffix}) + binary := carrierBinary(target.PlatformSuffix) + path := filepath.Join("artifacts", "pypi", filename) + writeWheelCarrier(t, path, binary) + wheelSHA256, err := fileSHA256(path) + if err != nil { + t.Fatal(err) + } + binarySHA256 := sha256.Sum256(binary) + packages.Packages = append(packages.Packages, pythonWheelRecord{ + BinarySha256: fmt.Sprintf("%x", binarySHA256), + Filename: filename, + PlatformSuffix: target.PlatformSuffix, + Sha256: wheelSHA256, + }) } writeFile(t, filepath.Join("artifacts", "release", "sbom.cdx.json"), "{}") @@ -50,6 +64,37 @@ func TestReleaseArtifactSnapshotOwnsOneImmutableEpoch(t *testing.T) { if err := snapshot.VerifyCrossCarrierBinaryIdentity(records, packages); err != nil { t.Fatal(err) } + for _, mutant := range []struct { + name string + mutate func(*pythonWheelRecord) + }{ + {name: "wheel digest", mutate: func(record *pythonWheelRecord) { record.Sha256 = strings.Repeat("0", 64) }}, + {name: "binary digest", mutate: func(record *pythonWheelRecord) { record.BinarySha256 = strings.Repeat("0", 64) }}, + } { + t.Run(mutant.name, func(t *testing.T) { + mutated := *packages + mutated.Packages = append([]pythonWheelRecord(nil), packages.Packages...) + mutant.mutate(&mutated.Packages[0]) + if err := snapshot.VerifyCrossCarrierBinaryIdentity(records, &mutated); err == nil || !strings.Contains(err.Error(), "digest claims do not match") { + t.Fatalf("digest mutant was admitted: %v", err) + } + if _, err := snapshot.AdmittedPythonPackageSet(packages); err == nil { + t.Fatal("failed digest verification retained stale admitted identities") + } + if err := snapshot.VerifyCrossCarrierBinaryIdentity(records, packages); err != nil { + t.Fatal(err) + } + }) + } + admittedPackages, err := snapshot.AdmittedPythonPackageSet(packages) + if err != nil { + t.Fatal(err) + } + for index := range admittedPackages.Packages { + if admittedPackages.Packages[index].Sha256 != packages.Packages[index].Sha256 || admittedPackages.Packages[index].BinarySha256 != packages.Packages[index].BinarySha256 { + t.Fatalf("derived package identity %d drifted", index) + } + } assets, checksums, subjectChecksums, err := snapshot.ReleaseEvidence(records, packages) if err != nil { t.Fatal(err) diff --git a/internal/tools/releasemanifest/carrier_identity.go b/internal/tools/releasemanifest/carrier_identity.go index bda416b..c366832 100644 --- a/internal/tools/releasemanifest/carrier_identity.go +++ b/internal/tools/releasemanifest/carrier_identity.go @@ -5,6 +5,8 @@ import ( "archive/zip" "bytes" "compress/gzip" + "crypto/sha256" + "encoding/hex" "errors" "fmt" "io" @@ -21,53 +23,60 @@ const ( wheelBinaryEntry = "agentic_proofkit/bin/agentic-proofkit" ) -func verifyCrossCarrierBinaryIdentity(packageDir string, pythonDir string, localRecords []packRecord, pythonPackages *pythonPackageSet) (returnErr error) { +type pythonWheelIdentity struct { + binarySHA256 string + wheelSHA256 string +} + +func verifyCrossCarrierBinaryIdentity(packageDir string, pythonDir string, localRecords []packRecord, pythonPackages *pythonPackageSet) (identities map[string]pythonWheelIdentity, returnErr error) { if pythonPackages == nil { - return nil + return nil, nil } if len(localRecords) != 1 { - return fmt.Errorf("cross-carrier identity requires exactly one npm package artifact") + return nil, fmt.Errorf("cross-carrier identity requires exactly one npm package artifact") } scratch, err := os.MkdirTemp("", "proofkit-carrier-binaries-") if err != nil { - return fmt.Errorf("create cross-carrier scratch directory: %w", err) + return nil, fmt.Errorf("create cross-carrier scratch directory: %w", err) } defer func() { returnErr = errors.Join(returnErr, os.RemoveAll(scratch)) }() npmBinaries, err := extractNPMCarrierBinaries(filepath.Join(packageDir, localRecords[0].Filename), scratch) if err != nil { - return fmt.Errorf("decode npm package carrier: %w", err) + return nil, fmt.Errorf("decode npm package carrier: %w", err) } wheelsBySuffix := make(map[string]pythonWheelRecord, len(pythonPackages.Packages)) for _, record := range pythonPackages.Packages { if _, exists := wheelsBySuffix[record.PlatformSuffix]; exists { - return fmt.Errorf("cross-carrier identity contains a duplicate Python platform suffix") + return nil, fmt.Errorf("cross-carrier identity contains a duplicate Python platform suffix") } wheelsBySuffix[record.PlatformSuffix] = record } targets := releaseplatform.Targets() if len(wheelsBySuffix) != len(targets) { - return fmt.Errorf("cross-carrier identity requires exact release-platform wheel closure") + return nil, fmt.Errorf("cross-carrier identity requires exact release-platform wheel closure") } + identities = make(map[string]pythonWheelIdentity, len(targets)) for _, target := range targets { npmBinary, exists := npmBinaries[target.PlatformSuffix] if !exists { - return fmt.Errorf("npm package carrier is missing a release-platform binary") + return nil, fmt.Errorf("npm package carrier is missing a release-platform binary") } wheel, exists := wheelsBySuffix[target.PlatformSuffix] if !exists { - return fmt.Errorf("python package carrier is missing a release-platform wheel") + return nil, fmt.Errorf("python package carrier is missing a release-platform wheel") } - equal, err := wheelCarrierBinaryEquals(filepath.Join(pythonDir, wheel.Filename), npmBinary) + equal, binarySHA256, err := wheelCarrierBinaryEquals(filepath.Join(pythonDir, wheel.Filename), npmBinary) if err != nil { - return fmt.Errorf("decode python wheel carrier: %w", err) + return nil, fmt.Errorf("decode python wheel carrier: %w", err) } if !equal { - return fmt.Errorf("npm and python package carriers contain different release-platform binary bytes") + return nil, fmt.Errorf("npm and python package carriers contain different release-platform binary bytes") } + identities[wheel.Filename] = pythonWheelIdentity{binarySHA256: binarySHA256} } - return nil + return identities, nil } type extractedCarrierBinary struct { @@ -149,41 +158,42 @@ func extractNPMCarrierBinaries(path string, scratch string) (map[string]extracte return binaries, nil } -func wheelCarrierBinaryEquals(path string, expected extractedCarrierBinary) (bool, error) { +func wheelCarrierBinaryEquals(path string, expected extractedCarrierBinary) (bool, string, error) { info, err := os.Stat(path) if err != nil { - return false, err + return false, "", err } if !info.Mode().IsRegular() || info.Size() > maxCarrierArchiveBytes { - return false, fmt.Errorf("python wheel carrier is not an admitted regular archive") + return false, "", fmt.Errorf("python wheel carrier is not an admitted regular archive") } reader, err := zip.OpenReader(path) if err != nil { - return false, err + return false, "", err } defer reader.Close() if len(reader.File) > maxCarrierArchiveEntries { - return false, fmt.Errorf("python wheel carrier exceeds the entry limit") + return false, "", fmt.Errorf("python wheel carrier exceeds the entry limit") } seen := map[string]struct{}{} found := false equal := false + binarySHA256 := "" for _, entry := range reader.File { if _, duplicate := seen[entry.Name]; duplicate { - return false, fmt.Errorf("python wheel carrier contains duplicate entries") + return false, "", fmt.Errorf("python wheel carrier contains duplicate entries") } seen[entry.Name] = struct{}{} if entry.Name != wheelBinaryEntry { continue } if entry.Mode()&os.ModeSymlink != 0 || !entry.FileInfo().Mode().IsRegular() { - return false, fmt.Errorf("python wheel carrier binary is not a regular file") + return false, "", fmt.Errorf("python wheel carrier binary is not a regular file") } if entry.UncompressedSize64 > uint64(maxCarrierBinaryBytes) { - return false, fmt.Errorf("python wheel carrier binary exceeds the byte limit") + return false, "", fmt.Errorf("python wheel carrier binary exceeds the byte limit") } if found { - return false, fmt.Errorf("python wheel carrier contains duplicate embedded binaries") + return false, "", fmt.Errorf("python wheel carrier contains duplicate embedded binaries") } found = true if int64(entry.UncompressedSize64) != expected.size { @@ -191,30 +201,34 @@ func wheelCarrierBinaryEquals(path string, expected extractedCarrierBinary) (boo } member, err := entry.Open() if err != nil { - return false, err + return false, "", err } expectedFile, err := os.Open(expected.path) if err != nil { member.Close() - return false, err + return false, "", err } - equal, err = equalCarrierStreams(member, expectedFile) + hash := sha256.New() + equal, err = equalCarrierStreams(io.TeeReader(member, hash), expectedFile) memberCloseErr := member.Close() expectedCloseErr := expectedFile.Close() if err != nil { - return false, err + return false, "", err } if memberCloseErr != nil { - return false, memberCloseErr + return false, "", memberCloseErr } if expectedCloseErr != nil { - return false, expectedCloseErr + return false, "", expectedCloseErr + } + if equal { + binarySHA256 = hex.EncodeToString(hash.Sum(nil)) } } if !found { - return false, fmt.Errorf("python wheel carrier lacks its embedded binary") + return false, "", fmt.Errorf("python wheel carrier lacks its embedded binary") } - return equal, nil + return equal, binarySHA256, nil } func equalCarrierStreams(left io.Reader, right io.Reader) (bool, error) { diff --git a/internal/tools/releasemanifest/carrier_identity_test.go b/internal/tools/releasemanifest/carrier_identity_test.go index 2b71a88..147e917 100644 --- a/internal/tools/releasemanifest/carrier_identity_test.go +++ b/internal/tools/releasemanifest/carrier_identity_test.go @@ -4,6 +4,8 @@ import ( "archive/tar" "archive/zip" "compress/gzip" + "crypto/sha256" + "fmt" "os" "path/filepath" "strings" @@ -15,14 +17,25 @@ import ( func TestCrossCarrierBinaryIdentityReadsFinalArchives(t *testing.T) { t.Run("matching_carriers", func(t *testing.T) { packageDir, pythonDir, records, packages := writeCarrierFixture(t, "") - if err := verifyCrossCarrierBinaryIdentity(packageDir, pythonDir, records, packages); err != nil { + identities, err := verifyCrossCarrierBinaryIdentity(packageDir, pythonDir, records, packages) + if err != nil { t.Fatal(err) } + if len(identities) != len(releaseplatform.Targets()) { + t.Fatalf("decoded identity count=%d", len(identities)) + } + for _, target := range releaseplatform.Targets() { + filename := target.PlatformSuffix + ".whl" + want := sha256.Sum256(carrierBinary(target.PlatformSuffix)) + if got := identities[filename].binarySHA256; got != fmt.Sprintf("%x", want) { + t.Fatalf("binary digest for %s=%s", filename, got) + } + } }) t.Run("wheel_mutation", func(t *testing.T) { packageDir, pythonDir, records, packages := writeCarrierFixture(t, "linux-x64") - err := verifyCrossCarrierBinaryIdentity(packageDir, pythonDir, records, packages) + _, err := verifyCrossCarrierBinaryIdentity(packageDir, pythonDir, records, packages) if err == nil || !strings.Contains(err.Error(), "different release-platform binary bytes") { t.Fatalf("mutated wheel was not rejected: %v", err) } @@ -31,7 +44,7 @@ func TestCrossCarrierBinaryIdentityReadsFinalArchives(t *testing.T) { t.Run("duplicate_tar_member", func(t *testing.T) { packageDir, pythonDir, records, packages := writeCarrierFixture(t, "") writeNPMCarrier(t, filepath.Join(packageDir, records[0].Filename), "binary") - err := verifyCrossCarrierBinaryIdentity(packageDir, pythonDir, records, packages) + _, err := verifyCrossCarrierBinaryIdentity(packageDir, pythonDir, records, packages) if err == nil || !strings.Contains(err.Error(), "contains duplicate entries") { t.Fatalf("duplicate npm binary was not rejected: %v", err) } @@ -40,7 +53,7 @@ func TestCrossCarrierBinaryIdentityReadsFinalArchives(t *testing.T) { t.Run("duplicate_non_binary_tar_member", func(t *testing.T) { packageDir, pythonDir, records, packages := writeCarrierFixture(t, "") writeNPMCarrier(t, filepath.Join(packageDir, records[0].Filename), "metadata") - err := verifyCrossCarrierBinaryIdentity(packageDir, pythonDir, records, packages) + _, err := verifyCrossCarrierBinaryIdentity(packageDir, pythonDir, records, packages) if err == nil || !strings.Contains(err.Error(), "contains duplicate entries") { t.Fatalf("duplicate npm metadata entry was not rejected: %v", err) } diff --git a/internal/tools/releasemanifest/main.go b/internal/tools/releasemanifest/main.go index 6398a4b..1bb8083 100644 --- a/internal/tools/releasemanifest/main.go +++ b/internal/tools/releasemanifest/main.go @@ -275,6 +275,10 @@ func run() (returnErr error) { if err := artifactSnapshot.VerifyCrossCarrierBinaryIdentity(localRecords, pythonPackages); err != nil { return err } + pythonPackages, err = artifactSnapshot.AdmittedPythonPackageSet(pythonPackages) + if err != nil { + return err + } if err := requirePyPIRegistryMatchesLocal(pypiRegistry, pythonPackages, manifest); err != nil { return err } diff --git a/internal/tools/workflowsmoke/process.go b/internal/tools/workflowsmoke/process.go index ee6513a..c9136af 100644 --- a/internal/tools/workflowsmoke/process.go +++ b/internal/tools/workflowsmoke/process.go @@ -72,7 +72,7 @@ func RunProcess(ctx context.Context, carrier ProcessCarrier, invocation Invocati command.Stdin = bytes.NewReader(invocation.Input) } runErr := command.Run() - cleanupErr := processgroup.Terminate(command) + cleanupErr := processgroup.TerminateAndWait(command, processWaitDelay) result := Result{Stdout: stdout.Bytes(), Stderr: stderr.Bytes()} if cleanupErr != nil { return Result{}, fmt.Errorf("terminate process carrier group: %w", cleanupErr) diff --git a/internal/tools/workflowsmoke/process_unix_test.go b/internal/tools/workflowsmoke/process_unix_test.go index e7ea391..ddc8fab 100644 --- a/internal/tools/workflowsmoke/process_unix_test.go +++ b/internal/tools/workflowsmoke/process_unix_test.go @@ -31,18 +31,11 @@ func TestRunProcessTerminatesDescendantsAfterSuccessfulParentExit(t *testing.T) t.Fatal(err) } t.Cleanup(func() { _ = process.Kill() }) - deadline := time.Now().Add(2 * time.Second) - for { - err := process.Signal(syscall.Signal(0)) - if errors.Is(err, os.ErrProcessDone) || errors.Is(err, syscall.ESRCH) { - return + err = process.Signal(syscall.Signal(0)) + if !errors.Is(err, os.ErrProcessDone) && !errors.Is(err, syscall.ESRCH) { + if err == nil { + t.Fatalf("descendant process %d remained present after successful carrier return", pid) } - if err != nil { - t.Fatalf("probe descendant process: %v", err) - } - if time.Now().After(deadline) { - t.Fatalf("descendant process %d remained alive after successful carrier return", pid) - } - time.Sleep(20 * time.Millisecond) + t.Fatalf("probe descendant process: %v", err) } } diff --git a/proofkit/cli-contract.v2.json b/proofkit/cli-contract.v2.json index 5bf39e1..31f87ad 100644 --- a/proofkit/cli-contract.v2.json +++ b/proofkit/cli-contract.v2.json @@ -859,7 +859,7 @@ "rootDefinitionDigest": "sha256:6632a481380bfb07d754ef622661b291f805b1b168b1824089bdd5cc9bdd4b0e", "nativeSource": { "path": "internal/command/changeworkflowplan", - "canonicalDigest": "sha256:072cd4568529c5e253f1e5f296ba77fcf013ecd04f85f4c34b72082f7c57f0f5", + "canonicalDigest": "sha256:fb52771062a3cfdea6d9b97e4b7469be3d62f00e4421646f3c9a47ec079c4d25", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -894,7 +894,7 @@ "rootDefinitionDigest": "sha256:ca7b2acffdcbfd28222eb61f2bd8687f76f20cb789b712ee3ffc2756a3674ff8", "nativeSource": { "path": "internal/command/changeworkflowplan", - "canonicalDigest": "sha256:072cd4568529c5e253f1e5f296ba77fcf013ecd04f85f4c34b72082f7c57f0f5", + "canonicalDigest": "sha256:fb52771062a3cfdea6d9b97e4b7469be3d62f00e4421646f3c9a47ec079c4d25", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { @@ -2421,7 +2421,7 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:ceba9fe3379339a2ff137ccaa1ff89bb0136cd306bf70fb3ca8354324d2a1acf", + "canonicalDigest": "sha256:af43c7dae19e019887fd3c43ddef4d453966b517880dc3fb76be5f3ed048281d", "evidenceClass": "source_checkout" }, { @@ -5663,7 +5663,7 @@ "rootDefinitionDigest": "sha256:3c842174dff5361e7f83166469b832805e05aa314b073c16234b5b64e346281e", "nativeSource": { "path": "internal/app", - "canonicalDigest": "sha256:ceba9fe3379339a2ff137ccaa1ff89bb0136cd306bf70fb3ca8354324d2a1acf", + "canonicalDigest": "sha256:af43c7dae19e019887fd3c43ddef4d453966b517880dc3fb76be5f3ed048281d", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -5692,7 +5692,7 @@ "rootDefinitionDigest": "sha256:0ea95e277ebe44cd2de42c29b47c38686ac0b6b390d8965367437b3fe138e209", "nativeSource": { "path": "internal/app", - "canonicalDigest": "sha256:ceba9fe3379339a2ff137ccaa1ff89bb0136cd306bf70fb3ca8354324d2a1acf", + "canonicalDigest": "sha256:af43c7dae19e019887fd3c43ddef4d453966b517880dc3fb76be5f3ed048281d", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { From 3efce090671c5a7d7fc62a674bf50dce30038ae3 Mon Sep 17 00:00:00 2001 From: iperev Date: Mon, 31 Aug 2026 23:09:12 +0200 Subject: [PATCH 09/11] fix: close final workflow and release findings --- .../proofkit-supply-chain-quality/overview.md | 5 +- .../requirements.v1.json | 2 +- internal/app/cli_contract_test.go | 2 +- internal/app/command_contract_generated.go | 4 +- .../app/testdata/v0.5-wire-observations.json | 6 +- .../changeworkflowplan/agent_envelope.go | 2 +- .../command/changeworkflowplan/catalog.go | 8 +- .../command/changeworkflowplan/state_test.go | 3 + .../stackpreset/preset_ids_generated.go | 2 +- internal/tools/coveragemetrics/main.go | 3 + internal/tools/coveragemetrics/main_test.go | 15 ++++ internal/tools/releasecloseoutinput/main.go | 82 +++++++++++++++---- .../tools/releasecloseoutinput/main_test.go | 47 +++++++++++ .../releasemanifest/artifact_snapshot.go | 44 ++++++++-- .../releasemanifest/artifact_snapshot_test.go | 41 ++++++++++ internal/tools/releasemanifest/main.go | 16 ++-- proofkit/cli-contract.v2.json | 4 +- 17 files changed, 241 insertions(+), 45 deletions(-) diff --git a/docs/specs/proofkit-supply-chain-quality/overview.md b/docs/specs/proofkit-supply-chain-quality/overview.md index 1d95f72..218cae7 100644 --- a/docs/specs/proofkit-supply-chain-quality/overview.md +++ b/docs/specs/proofkit-supply-chain-quality/overview.md @@ -164,8 +164,9 @@ vulnerability absence, or consumer rollout safety by itself. independently bind to the same release binary for every target; a separate bounded decoder also compares the final tar and wheel members directly, without a mutable intermediate; package-set wheel and embedded-binary digest - claims must equal bytes decoded from one immutable snapshot and release - closeout recomputes them independently; wheels bind advertised platform + claims, including npm SHA-1/SRI, must equal bytes decoded from one immutable + snapshot and release closeout recomputes them through repository-confined, + bounded, identity-checked non-symlink handles; wheels bind advertised platform compatibility to decoded executable bytes, reject version, identity, presence, and digest drift, and carry Core Metadata 2.4 plus an exact, RECORD-closed repository license payload. diff --git a/docs/specs/proofkit-supply-chain-quality/requirements.v1.json b/docs/specs/proofkit-supply-chain-quality/requirements.v1.json index f1d8046..96b1df2 100644 --- a/docs/specs/proofkit-supply-chain-quality/requirements.v1.json +++ b/docs/specs/proofkit-supply-chain-quality/requirements.v1.json @@ -298,7 +298,7 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-023", "ownerId": "proofkit.supply-chain-quality", - "invariant": "Every npm platform-binary tar entry and every Python wheel independently binds its embedded executable bytes to the same release-platform binary, and release metadata generation admits every final npm tarball, wheel, and SBOM exactly once into one bounded regular non-symlink immutable snapshot, derives final carrier decoding, direct chunked binary equality, wheel and embedded-binary digests, release checksums, and SBOM-subject checksums only from that epoch, requires every Python package-set digest claim to equal those decoded bytes, and revalidates every source identity before metadata persistence without relying on a mutable intermediate dist directory or claimed digests; release closeout independently reopens every declared wheel, rejects unsafe or duplicate ZIP topology, and recomputes both wheel and embedded-binary digests before accepting Python artifact evidence; both tar and wheel decoders reject duplicate entry names and require exact release-platform closure; the wheel-set verifier independently rejects version drift, duplicate identities, missing files, and SHA drift; each wheel independently decodes Darwin Mach-O bytes so an advertised macOS platform minimum is never lower than the executable minimum, uses Core Metadata 2.4 License-Expression and License-File fields, carries an exact repository LICENSE copy under the wheel licenses directory, and closes every payload entry through RECORD.", + "invariant": "Every npm platform-binary tar entry and every Python wheel independently binds its embedded executable bytes to the same release-platform binary, and release metadata generation admits every final npm tarball, wheel, and SBOM exactly once into one bounded regular non-symlink immutable snapshot, derives npm pack SHA-1 and SRI claims, final carrier decoding, direct chunked binary equality, wheel and embedded-binary digests, release checksums, and SBOM-subject checksums only from that epoch, requires every npm pack and Python package-set digest claim to equal those decoded bytes, and revalidates every source identity before metadata persistence without relying on a mutable intermediate dist directory or claimed digests; release closeout independently reopens every declared repository-confined npm tarball and wheel through bounded identity-checked non-symlink handles, recomputes npm SHA-1 and SRI plus both wheel and embedded-binary digests, and rejects unsafe or duplicate archive topology before accepting package evidence; both tar and wheel decoders reject duplicate entry names and require exact release-platform closure; the wheel-set verifier independently rejects version drift, duplicate identities, missing files, and SHA drift; each wheel independently decodes Darwin Mach-O bytes so an advertised macOS platform minimum is never lower than the executable minimum, uses Core Metadata 2.4 License-Expression and License-File fields, carries an exact repository LICENSE copy under the wheel licenses directory, and closes every payload entry through RECORD.", "claimLevel": "blocking", "riskClass": "critical", "proofBindingRefs": ["proofkit/requirement-bindings.json"], diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index 63935f0..7867ed9 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -23,7 +23,7 @@ import ( ) const ( - cliContractPublicABISHA256 = "dd4be9498e7dece58fd354cf7d0aa625c09ba205c7bc7a287d5b28df868981a4" + cliContractPublicABISHA256 = "626a47493ebc3bffa3652bc56b013bde88e1989557ef2bfae48e6ecc753add5d" 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 b40b701..66d3d6f 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 = "be6fef4e32ab624992480f537fd55f3d53f352e712486d34f95ee67fee32b842" +const commandContractSourceSHA256 = "b3df54243a1621ebf4e796d5a2efb36ff9c3391614709a7cb8d8629991ede0a0" type generatedCommandContractMetadata struct { InputContractSHA256 string @@ -19,7 +19,7 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "binding-partition": {InputContractSHA256: "sha256:366ad082045af52b2ac6604f18626d0f285b2db73b45d9a82687b8d3b0d2b3fd", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.binding-partition.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:52840879e13a00ef9a4abaad6cdb33000511674d5f9003fb56f387fdf58fadc8", FlagChoices: map[string][]string{}}, "branch-authority": {InputContractSHA256: "sha256:8a3ed74978898593fbdbf1f7fa684dae450fbd9019edcd60d07f818d63363ed4", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.branch-authority.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3c7dc74842299b92cd5baf57cc8666e9415963091359e5faf654e28da89561f1", FlagChoices: map[string][]string{}}, "capability-map-admission": {InputContractSHA256: "sha256:efe87497cf6a32cfd1f9c9c191393cac8781d1e9211914a8a7b6aaa44e7f371d", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.capability-map-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:bfb32535fd51bed168111912acb1651527897fe59a592119dc0032ce9286c9db", FlagChoices: map[string][]string{}}, - "change-workflow-plan": {InputContractSHA256: "sha256:d60c2cd30f64a98c9c512e16dead65cef9457910b7d59ec332fa3be48bcc8994", 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:9ea80c03c46e0ec9a8b51fbff8be32da9d7cc734bb76968fb671bf4bfcae8173", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}}, + "change-workflow-plan": {InputContractSHA256: "sha256:76a06617a1538570cd2d068a55645cfb2f570934ac11d89338d55451f3611470", 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:161995e61f16d6026640f4eb7d2c73189aa21afded7c0704a7b561301155a490", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}}, "changed-path-set": {InputContractSHA256: "sha256:8fe97426a58969e3e8dcbd52ed44540666b4de6be0487e8a3bc5088ae9c0f933", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.changed-path-set.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:abccdbf78e67f633ce49c34e8849c03f08ce42fa934a4c68969720c5045bf593", FlagChoices: map[string][]string{}}, "completion-criteria": {InputContractSHA256: "sha256:99c49c44b001e40383787e4c55f66621b8a8315f09635f1baf2326dc09bec4e6", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.completion-criteria.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:c90bb9605c7a22914104701a068dded510534bdeb60f4d601555d46c2d3d8a6d", FlagChoices: map[string][]string{}}, "conformance-profile": {InputContractSHA256: "sha256: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{}}, diff --git a/internal/app/testdata/v0.5-wire-observations.json b/internal/app/testdata/v0.5-wire-observations.json index 811d4d1..1378dff 100644 --- a/internal/app/testdata/v0.5-wire-observations.json +++ b/internal/app/testdata/v0.5-wire-observations.json @@ -5,14 +5,14 @@ "version": "0.5.0", "evidenceClass": "owner_authored_frozen_version_edge_observation", "previousPublicAbiSha256": "sha256:fc03740aea9e7f525a4388e5d7f557cde07e11b0db0c05101fe937c28a1129d9", - "currentPublicAbiSha256": "sha256:dd4be9498e7dece58fd354cf7d0aa625c09ba205c7bc7a287d5b28df868981a4", + "currentPublicAbiSha256": "sha256:626a47493ebc3bffa3652bc56b013bde88e1989557ef2bfae48e6ecc753add5d", "previousTypeScriptGeneratorId": "proofkit.json-report-cli-adapter-source.typescript.v1", "currentTypeScriptGeneratorId": "proofkit.json-report-cli-adapter-source.typescript.v2", "addedCommandContracts": [ { "command": "change-workflow-plan", - "inputContractSha256": "sha256:d60c2cd30f64a98c9c512e16dead65cef9457910b7d59ec332fa3be48bcc8994", - "outputContractSha256": "sha256:9ea80c03c46e0ec9a8b51fbff8be32da9d7cc734bb76968fb671bf4bfcae8173" + "inputContractSha256": "sha256:76a06617a1538570cd2d068a55645cfb2f570934ac11d89338d55451f3611470", + "outputContractSha256": "sha256:161995e61f16d6026640f4eb7d2c73189aa21afded7c0704a7b561301155a490" }, { "command": "native-evidence-guidance", diff --git a/internal/command/changeworkflowplan/agent_envelope.go b/internal/command/changeworkflowplan/agent_envelope.go index bd451da..e0afbf7 100644 --- a/internal/command/changeworkflowplan/agent_envelope.go +++ b/internal/command/changeworkflowplan/agent_envelope.go @@ -126,7 +126,7 @@ func envelopeBlockers(value projection) ([]map[string]any, []map[string]any) { } blocked := []map[string]any{} clarifications := []map[string]any{} - if value.Input.GoverningAuthorityRefID == nil { + if workflowCatalog.ActiveActionsRequireGoverningAuthority && value.Input.GoverningAuthorityRefID == nil { blocked = append(blocked, map[string]any{ "description": missingOwnerStop, "evidenceRefs": []any{}, diff --git a/internal/command/changeworkflowplan/catalog.go b/internal/command/changeworkflowplan/catalog.go index 4b1e555..900d102 100644 --- a/internal/command/changeworkflowplan/catalog.go +++ b/internal/command/changeworkflowplan/catalog.go @@ -26,12 +26,14 @@ type actionProfile struct { } type workflowCatalogDefinition struct { - Stages []stageDefinition - CheckpointActions []checkpointDefinition - ActionProfiles map[string]actionProfile + Stages []stageDefinition + CheckpointActions []checkpointDefinition + ActionProfiles map[string]actionProfile + ActiveActionsRequireGoverningAuthority bool } var workflowCatalog = workflowCatalogDefinition{ + ActiveActionsRequireGoverningAuthority: true, Stages: []stageDefinition{ {ID: "architecture", FirstAction: "author"}, {ID: "design", FirstAction: "author", RequiresIncomingSubject: true}, diff --git a/internal/command/changeworkflowplan/state_test.go b/internal/command/changeworkflowplan/state_test.go index 6d0a2a9..f16e395 100644 --- a/internal/command/changeworkflowplan/state_test.go +++ b/internal/command/changeworkflowplan/state_test.go @@ -49,6 +49,9 @@ var expectedWorkflowStateMatrix = []expectedWorkflowRow{ func TestWorkflowStatePredicates(t *testing.T) { commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.046999593271228979057731418772803126658652333387186990019354835268016126566224") + if !workflowCatalog.ActiveActionsRequireGoverningAuthority { + t.Fatal("workflow catalog does not own the active-action governing-authority prerequisite") + } t.Run("checkpoint_requires_incomplete", func(t *testing.T) { input := terminalInput() input["checkpoint"] = map[string]any{"state": "not_started"} diff --git a/internal/command/stackpreset/preset_ids_generated.go b/internal/command/stackpreset/preset_ids_generated.go index 5243484..619aded 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 = "be6fef4e32ab624992480f537fd55f3d53f352e712486d34f95ee67fee32b842" +const presetContractSourceSHA256 = "b3df54243a1621ebf4e796d5a2efb36ff9c3391614709a7cb8d8629991ede0a0" 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.go b/internal/tools/coveragemetrics/main.go index fdce137..0ad5779 100644 --- a/internal/tools/coveragemetrics/main.go +++ b/internal/tools/coveragemetrics/main.go @@ -590,6 +590,9 @@ func validateRequiredBindingWitnessSelectors(bindings bindingFile) error { if !isRequired { continue } + 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) diff --git a/internal/tools/coveragemetrics/main_test.go b/internal/tools/coveragemetrics/main_test.go index e621f95..5997606 100644 --- a/internal/tools/coveragemetrics/main_test.go +++ b/internal/tools/coveragemetrics/main_test.go @@ -725,6 +725,21 @@ func TestBindingWitnessSelectorsRequireExactCriticalInventories(t *testing.T) { t.Fatalf("surplus workflow binding error=%v", err) } }) + t.Run("workflow/duplicate-binding", func(t *testing.T) { + mutated := cloneBindingFile(bindings) + for _, binding := range bindings.Bindings { + if binding.ScenarioID != "proofkit.agent-workflow.pure-single-admission-owner" { + continue + } + mutated.Bindings = append(mutated.Bindings, binding) + err := validateBindingWitnessSelectorsAtRoot(root, mutated) + if err == nil || !strings.Contains(err.Error(), "required independent-falsifier binding is duplicated") { + t.Fatalf("duplicate workflow binding error=%v", err) + } + return + } + t.Fatal("workflow duplicate-binding seed is missing") + }) for _, scenarioID := range []string{ "proofkit.package-boundary.cli-output-root-witnesses", diff --git a/internal/tools/releasecloseoutinput/main.go b/internal/tools/releasecloseoutinput/main.go index 6ed9c2e..e0d44d0 100644 --- a/internal/tools/releasecloseoutinput/main.go +++ b/internal/tools/releasecloseoutinput/main.go @@ -54,6 +54,7 @@ const ( packageGateEnvironmentClass = "local-go-python" pythonPackageName = "agentic-proofkit" pythonWheelBinaryEntry = "agentic_proofkit/bin/agentic-proofkit" + maxNPMTarballBytes int64 = 256 << 20 maxPythonWheelBytes int64 = 256 << 20 maxPythonBinaryBytes int64 = 64 << 20 maxPythonWheelEntries = 4096 @@ -453,13 +454,17 @@ func buildInput(root string) (completionInput, error) { func packageArtifactCriterion(root string, manifest packageJSON) criterion { packPath := "artifacts/package/npm-pack.json" - tarballPath := filepath.Join("artifacts", "package", npmTarballName(manifest.Name, manifest.Version)) - ok := fileExists(root, tarballPath) && validPackRecords(root, packPath, manifest) + tarballName, nameOK := npmTarballName(manifest.Name, manifest.Version) + evidence := []string{"package.json", packPath} + if nameOK { + evidence = append(evidence, filepath.ToSlash(filepath.Join("artifacts", "package", tarballName))) + } + ok := nameOK && validPackRecords(root, packPath, manifest) return blockingCriterion( "proofkit.release_closeout.package_artifacts", "Local npm package artifact and npm pack metadata must exist for the current package version.", ok, - []string{"package.json", packPath, filepath.ToSlash(tarballPath)}, + evidence, []string{"npm:package:artifact", "internal/tools/packageverify"}, []string{"npm pack metadata is missing, invalid, or does not describe the current package.", "The current package tarball is missing or empty."}, []string{"This criterion does not claim npm registry publication or consumer installation."}, @@ -1564,11 +1569,19 @@ func evidenceRefsIfSatisfied(status string, refs []string) []string { return []string{} } -func npmTarballName(name string, version string) string { - return strings.Replace(strings.Replace(name, "@", "", 1), "/", "-", 1) + "-" + version + ".tgz" +func npmTarballName(name string, version string) (string, bool) { + filename := strings.Replace(strings.Replace(name, "@", "", 1), "/", "-", 1) + "-" + version + ".tgz" + if filename == "" || filepath.Base(filename) != filename || filepath.Clean(filename) != filename || !filepath.IsLocal(filename) { + return "", false + } + return filename, true } func validPackRecords(root string, path string, manifest packageJSON) bool { + expectedFilename, ok := npmTarballName(manifest.Name, manifest.Version) + if !ok { + return false + } records, err := readTypedJSON[[]packRecord](root, path) if err != nil || len(records) != 1 { return false @@ -1576,23 +1589,55 @@ func validPackRecords(root string, path string, manifest packageJSON) bool { record := records[0] return record.Name == manifest.Name && record.Version == manifest.Version && - record.Filename == npmTarballName(manifest.Name, manifest.Version) && + record.Filename == expectedFilename && packRecordBytesMatch(root, record) } func packRecordBytesMatch(root string, record packRecord) bool { - content, err := os.ReadFile(filepath.Join(root, "artifacts", "package", record.Filename)) - if err != nil || len(content) == 0 { + if filepath.Base(record.Filename) != record.Filename || filepath.Clean(record.Filename) != record.Filename || !filepath.IsLocal(record.Filename) { + return false + } + relativePath := filepath.Join("artifacts", "package", record.Filename) + rootHandle, err := os.OpenRoot(root) + if err != nil { return false } - sha1Sum := sha1.Sum(content) - if hex.EncodeToString(sha1Sum[:]) != record.Shasum { + defer rootHandle.Close() + if !artifactDirectoriesAdmitted(rootHandle, relativePath) { return false } - hash := sha512.New() - _, _ = hash.Write(content) - integrity := "sha512-" + base64.StdEncoding.EncodeToString(hash.Sum(nil)) - return integrity == record.Integrity + before, err := rootHandle.Lstat(relativePath) + if err != nil || before.Mode()&os.ModeSymlink != 0 || !before.Mode().IsRegular() || before.Size() <= 0 || before.Size() > maxNPMTarballBytes { + return false + } + file, err := rootHandle.Open(relativePath) + if err != nil { + return false + } + defer file.Close() + opened, err := file.Stat() + if err != nil || !os.SameFile(before, opened) || !opened.Mode().IsRegular() { + return false + } + sha1Hash := sha1.New() + sha512Hash := sha512.New() + written, err := io.Copy(io.MultiWriter(sha1Hash, sha512Hash), io.LimitReader(file, maxNPMTarballBytes+1)) + if err != nil || written != before.Size() || hex.EncodeToString(sha1Hash.Sum(nil)) != record.Shasum || "sha512-"+base64.StdEncoding.EncodeToString(sha512Hash.Sum(nil)) != record.Integrity { + return false + } + after, statErr := file.Stat() + pathAfter, pathErr := rootHandle.Lstat(relativePath) + return statErr == nil && pathErr == nil && pathAfter.Mode()&os.ModeSymlink == 0 && os.SameFile(before, after) && os.SameFile(after, pathAfter) && before.Size() == after.Size() && before.ModTime().Equal(after.ModTime()) +} + +func artifactDirectoriesAdmitted(root *os.Root, path string) bool { + for current := filepath.Dir(path); current != "."; current = filepath.Dir(current) { + info, err := root.Lstat(current) + if err != nil || info.Mode()&os.ModeSymlink != 0 || !info.IsDir() { + return false + } + } + return true } func readPythonPackageSet(root string, path string, manifest packageJSON) ([]pythonWheel, bool) { @@ -1621,6 +1666,9 @@ func pythonWheelBytesMatch(rootPath string, wheel pythonWheel) bool { return false } defer root.Close() + if !artifactDirectoriesAdmitted(root, relativePath) { + return false + } before, err := root.Lstat(relativePath) if err != nil || before.Mode()&os.ModeSymlink != 0 || !before.Mode().IsRegular() || before.Size() <= 0 || before.Size() > maxPythonWheelBytes { return false @@ -1674,12 +1722,16 @@ func pythonWheelBytesMatch(rootPath string, wheel pythonWheel) bool { } func releaseChecksumInventoriesMatch(root string, manifest packageJSON) bool { + expectedFilename, ok := npmTarballName(manifest.Name, manifest.Version) + if !ok { + return false + } packRecords, err := readTypedJSON[[]packRecord](root, "artifacts/package/npm-pack.json") if err != nil || len(packRecords) != 1 { return false } packRecord := packRecords[0] - if packRecord.Name != manifest.Name || packRecord.Version != manifest.Version || packRecord.Filename != npmTarballName(manifest.Name, manifest.Version) { + if packRecord.Name != manifest.Name || packRecord.Version != manifest.Version || packRecord.Filename != expectedFilename { return false } wheels, ok := readPythonPackageSet(root, "artifacts/pypi/python-packages.json", manifest) diff --git a/internal/tools/releasecloseoutinput/main_test.go b/internal/tools/releasecloseoutinput/main_test.go index 6270c45..b5a9dd6 100644 --- a/internal/tools/releasecloseoutinput/main_test.go +++ b/internal/tools/releasecloseoutinput/main_test.go @@ -200,6 +200,35 @@ func TestBuildInputFailsClosedForEachBlockingEvidenceClass(t *testing.T) { mustRemove(t, filepath.Join(root, "artifacts", "package", testNPMTarballName)) }, }, + { + name: "symlinked package tarball", + criterionID: "proofkit.release_closeout.package_artifacts", + mutate: func(root string) { + tarballPath := filepath.Join(root, "artifacts", "package", testNPMTarballName) + mustRemove(t, tarballPath) + outsidePath := filepath.Join(t.TempDir(), "outside.tgz") + writeFile(t, outsidePath, "package") + if err := os.Symlink(outsidePath, tarballPath); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "symlinked package directory", + criterionID: "proofkit.release_closeout.package_artifacts", + mutate: func(root string) { + replaceDirectoryWithSymlink(t, filepath.Join(root, "artifacts", "package")) + }, + }, + { + name: "traversing package identity", + criterionID: "proofkit.release_closeout.package_artifacts", + mutate: func(root string) { + manifest := readJSONMap(t, filepath.Join(root, "package.json")) + manifest["name"] = "scope/../../../../../outside" + writeJSON(t, filepath.Join(root, "package.json"), manifest) + }, + }, { name: "stale npm pack metadata", criterionID: "proofkit.release_closeout.package_artifacts", @@ -246,6 +275,13 @@ func TestBuildInputFailsClosedForEachBlockingEvidenceClass(t *testing.T) { mustRemove(t, filepath.Join(root, "artifacts", "pypi", testPythonWheelName)) }, }, + { + name: "symlinked Python artifact directory", + criterionID: "proofkit.release_closeout.python_wrappers", + mutate: func(root string) { + replaceDirectoryWithSymlink(t, filepath.Join(root, "artifacts", "pypi")) + }, + }, { name: "stale Python package metadata", criterionID: "proofkit.release_closeout.python_wrappers", @@ -2163,6 +2199,17 @@ func mustRemove(t *testing.T, path string) { } } +func replaceDirectoryWithSymlink(t *testing.T, path string) { + t.Helper() + target := filepath.Join(t.TempDir(), filepath.Base(path)) + if err := os.Rename(path, target); err != nil { + t.Fatal(err) + } + if err := os.Symlink(target, path); err != nil { + t.Fatal(err) + } +} + func writeJSON(t *testing.T, path string, value any) { t.Helper() content, err := json.Marshal(value) diff --git a/internal/tools/releasemanifest/artifact_snapshot.go b/internal/tools/releasemanifest/artifact_snapshot.go index 8e937a1..bb67a30 100644 --- a/internal/tools/releasemanifest/artifact_snapshot.go +++ b/internal/tools/releasemanifest/artifact_snapshot.go @@ -1,7 +1,10 @@ package main import ( + "crypto/sha1" "crypto/sha256" + "crypto/sha512" + "encoding/base64" "encoding/hex" "errors" "fmt" @@ -21,9 +24,11 @@ type releaseArtifactSnapshot struct { } type releaseArtifact struct { - filename string - sha256 string - size int64 + filename string + integrity string + sha1 string + sha256 string + size int64 } func newReleaseArtifactSnapshot(localRecords []packRecord, pythonPackages *pythonPackageSet) (*releaseArtifactSnapshot, error) { @@ -144,6 +149,25 @@ func (snapshot *releaseArtifactSnapshot) AdmittedPythonPackageSet(pythonPackages return &admitted, nil } +func (snapshot *releaseArtifactSnapshot) AdmittedNPMRecords(records []packRecord) ([]packRecord, error) { + if err := snapshot.requireOpen(); err != nil { + return nil, err + } + admitted := append([]packRecord(nil), records...) + for index := range admitted { + artifact, ok := snapshot.bySource[filepath.Join("artifacts", "package", admitted[index].Filename)] + if !ok { + return nil, fmt.Errorf("release artifact snapshot lacks an expected npm tarball") + } + if admitted[index].Shasum != artifact.sha1 || admitted[index].Integrity != artifact.integrity { + return nil, fmt.Errorf("npm pack digest claims do not match immutable tarball bytes") + } + admitted[index].Shasum = artifact.sha1 + admitted[index].Integrity = artifact.integrity + } + return admitted, nil +} + func (snapshot *releaseArtifactSnapshot) ReleaseEvidence(localRecords []packRecord, pythonPackages *pythonPackageSet) ([]assetEvidence, []string, []string, error) { if err := snapshot.requireOpen(); err != nil { return nil, nil, nil, err @@ -222,8 +246,10 @@ func snapshotReleaseArtifact(snapshotRoot string, sourcePath string) (releaseArt if err != nil { return releaseArtifact{}, fmt.Errorf("create release snapshot file: %w", err) } - hash := sha256.New() - written, copyErr := io.Copy(io.MultiWriter(target, hash), io.LimitReader(source, maxReleaseArtifactBytes+1)) + sha1Hash := sha1.New() + sha256Hash := sha256.New() + sha512Hash := sha512.New() + written, copyErr := io.Copy(io.MultiWriter(target, sha1Hash, sha256Hash, sha512Hash), io.LimitReader(source, maxReleaseArtifactBytes+1)) syncErr := target.Sync() closeErr := target.Close() if copyErr != nil { @@ -245,9 +271,11 @@ func snapshotReleaseArtifact(snapshotRoot string, sourcePath string) (releaseArt return releaseArtifact{}, fmt.Errorf("release artifact changed during immutable admission") } return releaseArtifact{ - filename: filepath.Base(clean), - sha256: hex.EncodeToString(hash.Sum(nil)), - size: written, + filename: filepath.Base(clean), + integrity: "sha512-" + base64.StdEncoding.EncodeToString(sha512Hash.Sum(nil)), + sha1: hex.EncodeToString(sha1Hash.Sum(nil)), + sha256: hex.EncodeToString(sha256Hash.Sum(nil)), + size: written, }, nil } diff --git a/internal/tools/releasemanifest/artifact_snapshot_test.go b/internal/tools/releasemanifest/artifact_snapshot_test.go index 7a43898..8da5eb7 100644 --- a/internal/tools/releasemanifest/artifact_snapshot_test.go +++ b/internal/tools/releasemanifest/artifact_snapshot_test.go @@ -1,7 +1,11 @@ package main import ( + "crypto/sha1" "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "encoding/hex" "fmt" "os" "path/filepath" @@ -32,6 +36,7 @@ func TestReleaseArtifactSnapshotOwnsOneImmutableEpoch(t *testing.T) { } records := []packRecord{{Filename: "proofkit.tgz"}} writeNPMCarrier(t, filepath.Join("artifacts", "package", records[0].Filename), "") + records[0].Shasum, records[0].Integrity = npmFileClaims(t, filepath.Join("artifacts", "package", records[0].Filename)) packages := &pythonPackageSet{Packages: make([]pythonWheelRecord, 0, len(releaseplatform.Targets()))} for _, target := range releaseplatform.Targets() { filename := target.PlatformSuffix + ".whl" @@ -61,6 +66,31 @@ func TestReleaseArtifactSnapshotOwnsOneImmutableEpoch(t *testing.T) { t.Fatal(err) } }) + admittedRecords, err := snapshot.AdmittedNPMRecords(records) + if err != nil { + t.Fatal(err) + } + if len(admittedRecords) != 1 || admittedRecords[0].Shasum != records[0].Shasum || admittedRecords[0].Integrity != records[0].Integrity { + t.Fatalf("admitted npm records drifted: %#v", admittedRecords) + } + for _, mutant := range []struct { + name string + mutate func(*packRecord) + }{ + {name: "npm shasum", mutate: func(record *packRecord) { record.Shasum = strings.Repeat("0", 40) }}, + {name: "npm integrity", mutate: func(record *packRecord) { record.Integrity = "sha512-" + strings.Repeat("A", 88) }}, + } { + t.Run(mutant.name, func(t *testing.T) { + mutated := append([]packRecord(nil), records...) + mutant.mutate(&mutated[0]) + if err := requireRegistryRecordsMatchLocal(mutated, mutated); err != nil { + t.Fatalf("declaration-only control should agree before byte admission: %v", err) + } + if _, err := snapshot.AdmittedNPMRecords(mutated); err == nil || !strings.Contains(err.Error(), "digest claims do not match") { + t.Fatalf("npm digest mutant was admitted: %v", err) + } + }) + } if err := snapshot.VerifyCrossCarrierBinaryIdentity(records, packages); err != nil { t.Fatal(err) } @@ -124,6 +154,17 @@ func TestReleaseArtifactSnapshotOwnsOneImmutableEpoch(t *testing.T) { } } +func npmFileClaims(t *testing.T, path string) (string, string) { + t.Helper() + content, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + sha1Sum := sha1.Sum(content) + sha512Sum := sha512.Sum512(content) + return hex.EncodeToString(sha1Sum[:]), "sha512-" + base64.StdEncoding.EncodeToString(sha512Sum[:]) +} + func containsChecksumLine(lines []string, expected string) bool { for _, line := range lines { if line == expected { diff --git a/internal/tools/releasemanifest/main.go b/internal/tools/releasemanifest/main.go index 1bb8083..eb0b48c 100644 --- a/internal/tools/releasemanifest/main.go +++ b/internal/tools/releasemanifest/main.go @@ -250,18 +250,12 @@ func run() (returnErr error) { if err := requirePublicationMode(npmPublicationMode, "npm", len(registryRecords) > 0); err != nil { return err } - if err := requireNPMRegistryMatchesLocal(npmRegistry, registryRecords, localRecords, npmPublicationMode); err != nil { - return err - } if err := requirePublicationMode(pypiPublicationMode, "pypi", pypiRegistry != nil); err != nil { return err } if err := requirePackRecordsMatchPackage(manifest, localRecords, "local npm package evidence"); err != nil { return err } - if err := requireRegistryRecordsMatchLocal(registryRecords, localRecords); err != nil { - return err - } if err := requirePythonPackagesMatchPackage(manifest, pythonPackages, "local Python package evidence"); err != nil { return err } @@ -272,6 +266,16 @@ func run() (returnErr error) { defer func() { returnErr = errors.Join(returnErr, artifactSnapshot.Close()) }() + localRecords, err = artifactSnapshot.AdmittedNPMRecords(localRecords) + if err != nil { + return err + } + if err := requireNPMRegistryMatchesLocal(npmRegistry, registryRecords, localRecords, npmPublicationMode); err != nil { + return err + } + if err := requireRegistryRecordsMatchLocal(registryRecords, localRecords); err != nil { + return err + } if err := artifactSnapshot.VerifyCrossCarrierBinaryIdentity(localRecords, pythonPackages); err != nil { return err } diff --git a/proofkit/cli-contract.v2.json b/proofkit/cli-contract.v2.json index 31f87ad..5c307ad 100644 --- a/proofkit/cli-contract.v2.json +++ b/proofkit/cli-contract.v2.json @@ -859,7 +859,7 @@ "rootDefinitionDigest": "sha256:6632a481380bfb07d754ef622661b291f805b1b168b1824089bdd5cc9bdd4b0e", "nativeSource": { "path": "internal/command/changeworkflowplan", - "canonicalDigest": "sha256:fb52771062a3cfdea6d9b97e4b7469be3d62f00e4421646f3c9a47ec079c4d25", + "canonicalDigest": "sha256:0fa5cc1dfc2c76f359809177e147a98a08047cb338ceebe44e0ca35bedd44398", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -894,7 +894,7 @@ "rootDefinitionDigest": "sha256:ca7b2acffdcbfd28222eb61f2bd8687f76f20cb789b712ee3ffc2756a3674ff8", "nativeSource": { "path": "internal/command/changeworkflowplan", - "canonicalDigest": "sha256:fb52771062a3cfdea6d9b97e4b7469be3d62f00e4421646f3c9a47ec079c4d25", + "canonicalDigest": "sha256:0fa5cc1dfc2c76f359809177e147a98a08047cb338ceebe44e0ca35bedd44398", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { From b5fd4d5a9ef4112abbe884d36ac3b1786204314c Mon Sep 17 00:00:00 2001 From: iperev Date: Mon, 31 Aug 2026 23:41:41 +0200 Subject: [PATCH 10/11] test: bind final workflow and release invariants --- internal/app/cli_contract_test.go | 2 +- internal/app/command_contract_generated.go | 4 +- .../app/testdata/v0.5-wire-observations.json | 6 +-- .../changeworkflowplan/agent_envelope.go | 6 +-- .../command/changeworkflowplan/state_test.go | 19 +++++++-- .../stackpreset/preset_ids_generated.go | 2 +- internal/tools/coveragemetrics/main.go | 7 ++++ internal/tools/coveragemetrics/main_test.go | 2 + internal/tools/releasecloseoutinput/main.go | 11 ++++- .../tools/releasecloseoutinput/main_test.go | 19 +++++++++ proofkit/cli-contract.v2.json | 4 +- proofkit/requirement-bindings.json | 42 +++++++++++++++++++ 12 files changed, 107 insertions(+), 17 deletions(-) diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index 7867ed9..2d9ffcf 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -23,7 +23,7 @@ import ( ) const ( - cliContractPublicABISHA256 = "626a47493ebc3bffa3652bc56b013bde88e1989557ef2bfae48e6ecc753add5d" + cliContractPublicABISHA256 = "9ecd2c3d2f3f360088409f7e91cce406fc1d1d6edda1b404fce119985c4fb623" 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 66d3d6f..cc65674 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 = "b3df54243a1621ebf4e796d5a2efb36ff9c3391614709a7cb8d8629991ede0a0" +const commandContractSourceSHA256 = "3e1289cbe709b62aa5cb76dd71d39ad0c40c70f6fcebd2e991544fac43a51d1c" type generatedCommandContractMetadata struct { InputContractSHA256 string @@ -19,7 +19,7 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "binding-partition": {InputContractSHA256: "sha256:366ad082045af52b2ac6604f18626d0f285b2db73b45d9a82687b8d3b0d2b3fd", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.binding-partition.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:52840879e13a00ef9a4abaad6cdb33000511674d5f9003fb56f387fdf58fadc8", FlagChoices: map[string][]string{}}, "branch-authority": {InputContractSHA256: "sha256:8a3ed74978898593fbdbf1f7fa684dae450fbd9019edcd60d07f818d63363ed4", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.branch-authority.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3c7dc74842299b92cd5baf57cc8666e9415963091359e5faf654e28da89561f1", FlagChoices: map[string][]string{}}, "capability-map-admission": {InputContractSHA256: "sha256:efe87497cf6a32cfd1f9c9c191393cac8781d1e9211914a8a7b6aaa44e7f371d", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.capability-map-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:bfb32535fd51bed168111912acb1651527897fe59a592119dc0032ce9286c9db", FlagChoices: map[string][]string{}}, - "change-workflow-plan": {InputContractSHA256: "sha256:76a06617a1538570cd2d068a55645cfb2f570934ac11d89338d55451f3611470", 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:161995e61f16d6026640f4eb7d2c73189aa21afded7c0704a7b561301155a490", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}}, + "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"}}}, "changed-path-set": {InputContractSHA256: "sha256:8fe97426a58969e3e8dcbd52ed44540666b4de6be0487e8a3bc5088ae9c0f933", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.changed-path-set.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:abccdbf78e67f633ce49c34e8849c03f08ce42fa934a4c68969720c5045bf593", FlagChoices: map[string][]string{}}, "completion-criteria": {InputContractSHA256: "sha256:99c49c44b001e40383787e4c55f66621b8a8315f09635f1baf2326dc09bec4e6", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.completion-criteria.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:c90bb9605c7a22914104701a068dded510534bdeb60f4d601555d46c2d3d8a6d", FlagChoices: map[string][]string{}}, "conformance-profile": {InputContractSHA256: "sha256: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{}}, diff --git a/internal/app/testdata/v0.5-wire-observations.json b/internal/app/testdata/v0.5-wire-observations.json index 1378dff..adc591b 100644 --- a/internal/app/testdata/v0.5-wire-observations.json +++ b/internal/app/testdata/v0.5-wire-observations.json @@ -5,14 +5,14 @@ "version": "0.5.0", "evidenceClass": "owner_authored_frozen_version_edge_observation", "previousPublicAbiSha256": "sha256:fc03740aea9e7f525a4388e5d7f557cde07e11b0db0c05101fe937c28a1129d9", - "currentPublicAbiSha256": "sha256:626a47493ebc3bffa3652bc56b013bde88e1989557ef2bfae48e6ecc753add5d", + "currentPublicAbiSha256": "sha256:9ecd2c3d2f3f360088409f7e91cce406fc1d1d6edda1b404fce119985c4fb623", "previousTypeScriptGeneratorId": "proofkit.json-report-cli-adapter-source.typescript.v1", "currentTypeScriptGeneratorId": "proofkit.json-report-cli-adapter-source.typescript.v2", "addedCommandContracts": [ { "command": "change-workflow-plan", - "inputContractSha256": "sha256:76a06617a1538570cd2d068a55645cfb2f570934ac11d89338d55451f3611470", - "outputContractSha256": "sha256:161995e61f16d6026640f4eb7d2c73189aa21afded7c0704a7b561301155a490" + "inputContractSha256": "sha256:e3124fc636b7f66b24daf8e1435cea11da15a741abeabe0cc3d3890b13c71625", + "outputContractSha256": "sha256:cd035e9b71d83c341b1a937a18699fd727cb4b0d694983d715b064292ae4d8bd" }, { "command": "native-evidence-guidance", diff --git a/internal/command/changeworkflowplan/agent_envelope.go b/internal/command/changeworkflowplan/agent_envelope.go index e0afbf7..6faf751 100644 --- a/internal/command/changeworkflowplan/agent_envelope.go +++ b/internal/command/changeworkflowplan/agent_envelope.go @@ -33,7 +33,7 @@ func agentEnvelope(value projection) (map[string]any, error) { }) } actionPlan := []map[string]any{} - blocked, clarifications := envelopeBlockers(value) + blocked, clarifications := envelopeBlockers(value, workflowCatalog) if value.Decision.OutputKind == "next_action" { if len(blocked) == 0 { prompt := value.Prompt @@ -120,13 +120,13 @@ func agentEnvelope(value projection) (map[string]any, error) { return envelope, nil } -func envelopeBlockers(value projection) ([]map[string]any, []map[string]any) { +func envelopeBlockers(value projection, catalog workflowCatalogDefinition) ([]map[string]any, []map[string]any) { if value.Decision.OutputKind != "next_action" { return []map[string]any{}, []map[string]any{} } blocked := []map[string]any{} clarifications := []map[string]any{} - if workflowCatalog.ActiveActionsRequireGoverningAuthority && value.Input.GoverningAuthorityRefID == nil { + if catalog.ActiveActionsRequireGoverningAuthority && value.Input.GoverningAuthorityRefID == nil { blocked = append(blocked, map[string]any{ "description": missingOwnerStop, "evidenceRefs": []any{}, diff --git a/internal/command/changeworkflowplan/state_test.go b/internal/command/changeworkflowplan/state_test.go index f16e395..0ce6d85 100644 --- a/internal/command/changeworkflowplan/state_test.go +++ b/internal/command/changeworkflowplan/state_test.go @@ -49,9 +49,22 @@ var expectedWorkflowStateMatrix = []expectedWorkflowRow{ func TestWorkflowStatePredicates(t *testing.T) { commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.046999593271228979057731418772803126658652333387186990019354835268016126566224") - if !workflowCatalog.ActiveActionsRequireGoverningAuthority { - t.Fatal("workflow catalog does not own the active-action governing-authority prerequisite") - } + t.Run("catalog_owns_active_action_authority_prerequisite", func(t *testing.T) { + value, err := project(initialInput()) + if err != nil { + t.Fatal(err) + } + blocked, clarifications := envelopeBlockers(value, workflowCatalog) + if len(blocked) != 1 || len(clarifications) != 1 { + t.Fatalf("catalog prerequisite did not block missing authority: blocked=%v clarifications=%v", blocked, clarifications) + } + permissive := workflowCatalog + permissive.ActiveActionsRequireGoverningAuthority = false + blocked, clarifications = envelopeBlockers(value, permissive) + if len(blocked) != 0 || len(clarifications) != 0 { + t.Fatalf("disabled catalog prerequisite retained authority blockers: blocked=%v clarifications=%v", blocked, clarifications) + } + }) t.Run("checkpoint_requires_incomplete", func(t *testing.T) { input := terminalInput() input["checkpoint"] = map[string]any{"state": "not_started"} diff --git a/internal/command/stackpreset/preset_ids_generated.go b/internal/command/stackpreset/preset_ids_generated.go index 619aded..7ed4271 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 = "b3df54243a1621ebf4e796d5a2efb36ff9c3391614709a7cb8d8629991ede0a0" +const presetContractSourceSHA256 = "3e1289cbe709b62aa5cb76dd71d39ad0c40c70f6fcebd2e991544fac43a51d1c" 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.go b/internal/tools/coveragemetrics/main.go index 0ad5779..bef9083 100644 --- a/internal/tools/coveragemetrics/main.go +++ b/internal/tools/coveragemetrics/main.go @@ -269,6 +269,7 @@ func validateRequiredBindingWitnessSelectors(bindings bindingFile) error { {"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"}: {"TestVerifyAcceptsApplicationCLI", "TestVerifyRejectsCarrierContractMutations"}, @@ -418,6 +419,10 @@ func validateRequiredBindingWitnessSelectors(bindings bindingFile) error { "TestLiteralShellWordsConsumesLongBackslashRun", "TestOnboardingTraceCoversEveryDiscoveredPresetAndREADMEInput", }, + {"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", @@ -519,6 +524,7 @@ func validateRequiredBindingWitnessSelectors(bindings bindingFile) error { {"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", @@ -555,6 +561,7 @@ func validateRequiredBindingWitnessSelectors(bindings bindingFile) error { {"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", diff --git a/internal/tools/coveragemetrics/main_test.go b/internal/tools/coveragemetrics/main_test.go index 5997606..a38a1c7 100644 --- a/internal/tools/coveragemetrics/main_test.go +++ b/internal/tools/coveragemetrics/main_test.go @@ -590,6 +590,7 @@ func TestBindingWitnessSelectorsRequireExactCriticalInventories(t *testing.T) { for _, scenarioID := range []string{ "proofkit.agent-workflow.bounded-safe-text", + "proofkit.agent-workflow.catalog-prerequisite-causality", "proofkit.agent-workflow.cli-presentation-capability-product", "proofkit.agent-workflow.installed-carrier-smoke-closure", "proofkit.agent-workflow.native-evidence-guidance-purity", @@ -624,6 +625,7 @@ func TestBindingWitnessSelectorsRequireExactCriticalInventories(t *testing.T) { "proofkit.supply-chain-quality.installed-package-json-abi-smoke", "proofkit.supply-chain-quality.osv-permission-separation", "proofkit.supply-chain-quality.python-wheel-platform-byte-compatibility", + "proofkit.supply-chain-quality.release-closeout-npm-byte-admission", "proofkit.supply-chain-quality.release-platform-python-wheels", "proofkit.supply-chain-quality.release-change-record-projection", "proofkit.supply-chain-quality.release-predecessor-lineage", diff --git a/internal/tools/releasecloseoutinput/main.go b/internal/tools/releasecloseoutinput/main.go index e0d44d0..71e5da8 100644 --- a/internal/tools/releasecloseoutinput/main.go +++ b/internal/tools/releasecloseoutinput/main.go @@ -1594,6 +1594,13 @@ func validPackRecords(root string, path string, manifest packageJSON) bool { } func packRecordBytesMatch(root string, record packRecord) bool { + return packRecordBytesMatchWithin(root, record, maxNPMTarballBytes) +} + +func packRecordBytesMatchWithin(root string, record packRecord, maxBytes int64) bool { + if maxBytes <= 0 { + return false + } if filepath.Base(record.Filename) != record.Filename || filepath.Clean(record.Filename) != record.Filename || !filepath.IsLocal(record.Filename) { return false } @@ -1607,7 +1614,7 @@ func packRecordBytesMatch(root string, record packRecord) bool { return false } before, err := rootHandle.Lstat(relativePath) - if err != nil || before.Mode()&os.ModeSymlink != 0 || !before.Mode().IsRegular() || before.Size() <= 0 || before.Size() > maxNPMTarballBytes { + if err != nil || before.Mode()&os.ModeSymlink != 0 || !before.Mode().IsRegular() || before.Size() <= 0 || before.Size() > maxBytes { return false } file, err := rootHandle.Open(relativePath) @@ -1621,7 +1628,7 @@ func packRecordBytesMatch(root string, record packRecord) bool { } sha1Hash := sha1.New() sha512Hash := sha512.New() - written, err := io.Copy(io.MultiWriter(sha1Hash, sha512Hash), io.LimitReader(file, maxNPMTarballBytes+1)) + written, err := io.Copy(io.MultiWriter(sha1Hash, sha512Hash), io.LimitReader(file, maxBytes+1)) if err != nil || written != before.Size() || hex.EncodeToString(sha1Hash.Sum(nil)) != record.Shasum || "sha512-"+base64.StdEncoding.EncodeToString(sha512Hash.Sum(nil)) != record.Integrity { return false } diff --git a/internal/tools/releasecloseoutinput/main_test.go b/internal/tools/releasecloseoutinput/main_test.go index b5a9dd6..589fe0f 100644 --- a/internal/tools/releasecloseoutinput/main_test.go +++ b/internal/tools/releasecloseoutinput/main_test.go @@ -907,6 +907,25 @@ func TestBuildInputFailsClosedForEachBlockingEvidenceClass(t *testing.T) { } } +func TestPackRecordBytesMatchEnforcesByteLimit(t *testing.T) { + root := t.TempDir() + content := []byte("package") + writeNPMArtifact(t, root, testNPMTarballName, content) + record := packRecord{ + Filename: testNPMTarballName, + Integrity: testNPMIntegrity(content), + Name: testNPMPackageName, + Shasum: testSHA1(content), + Version: "1.2.3", + } + if !packRecordBytesMatchWithin(root, record, int64(len(content))) { + t.Fatal("exact byte limit rejected a valid npm tarball") + } + if packRecordBytesMatchWithin(root, record, int64(len(content)-1)) { + t.Fatal("limit-minus-one admitted an oversized npm tarball") + } +} + func TestCommandRouteMetricsProducerReachabilityMatchesExactProducerPartition(t *testing.T) { routes := producerReachableCommandRouteMetricsFixture() expectedCommands := producerReachableCLIContractCommands() diff --git a/proofkit/cli-contract.v2.json b/proofkit/cli-contract.v2.json index 5c307ad..d25de1a 100644 --- a/proofkit/cli-contract.v2.json +++ b/proofkit/cli-contract.v2.json @@ -859,7 +859,7 @@ "rootDefinitionDigest": "sha256:6632a481380bfb07d754ef622661b291f805b1b168b1824089bdd5cc9bdd4b0e", "nativeSource": { "path": "internal/command/changeworkflowplan", - "canonicalDigest": "sha256:0fa5cc1dfc2c76f359809177e147a98a08047cb338ceebe44e0ca35bedd44398", + "canonicalDigest": "sha256:f0f7fce215691b4b52a02010ff12b65605473f586f5d11c428e5d647f405baec", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -894,7 +894,7 @@ "rootDefinitionDigest": "sha256:ca7b2acffdcbfd28222eb61f2bd8687f76f20cb789b712ee3ffc2756a3674ff8", "nativeSource": { "path": "internal/command/changeworkflowplan", - "canonicalDigest": "sha256:0fa5cc1dfc2c76f359809177e147a98a08047cb338ceebe44e0ca35bedd44398", + "canonicalDigest": "sha256:f0f7fce215691b4b52a02010ff12b65605473f586f5d11c428e5d647f405baec", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { diff --git a/proofkit/requirement-bindings.json b/proofkit/requirement-bindings.json index dcecd3f..417120f 100644 --- a/proofkit/requirement-bindings.json +++ b/proofkit/requirement-bindings.json @@ -3662,6 +3662,29 @@ "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-023", + "scenarioId": "proofkit.supply-chain-quality.release-closeout-npm-byte-admission", + "witnessId": "proofkit.release-closeout.npm-byte-admission-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/tools/releasecloseoutinput/main_test.go", + "witnessSelectors": [ + { + "selector": "TestBuildInputFailsClosedForEachBlockingEvidenceClass", + "command": "go test ./internal/tools/releasecloseoutinput -run '^TestBuildInputFailsClosedForEachBlockingEvidenceClass$'" + }, + { + "selector": "TestPackRecordBytesMatchEnforcesByteLimit", + "command": "go test ./internal/tools/releasecloseoutinput -run '^TestPackRecordBytesMatchEnforcesByteLimit$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, { "requirementId": "REQ-PROOFKIT-QUALITY-023", "scenarioId": "proofkit.supply-chain-quality.release-artifact-immutable-snapshot", @@ -4798,6 +4821,25 @@ "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-010", + "scenarioId": "proofkit.agent-workflow.catalog-prerequisite-causality", + "witnessId": "proofkit.agent-workflow.catalog-prerequisite-causality-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/command/changeworkflowplan/state_test.go", + "witnessSelectors": [ + { + "selector": "TestWorkflowStatePredicates", + "command": "go test ./internal/command/changeworkflowplan -run '^TestWorkflowStatePredicates$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, { "requirementId": "REQ-PROOFKIT-WORKFLOW-010", "scenarioId": "proofkit.agent-workflow.semantic-owner-minimality", From a944b3fd45522e5975c4e60879e81b8a17c8b14f Mon Sep 17 00:00:00 2001 From: iperev Date: Tue, 1 Sep 2026 00:12:46 +0200 Subject: [PATCH 11/11] chore: update Go patch toolchain --- go.mod | 2 +- internal/tools/commandoracle/corpus_coordinates.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 8f67345..b2b6c7a 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/research-engineering/agentic-proofkit go 1.26 -toolchain go1.26.5 +toolchain go1.26.7 tool ( github.com/rhysd/actionlint/cmd/actionlint diff --git a/internal/tools/commandoracle/corpus_coordinates.go b/internal/tools/commandoracle/corpus_coordinates.go index ddcf0d7..26a91dd 100644 --- a/internal/tools/commandoracle/corpus_coordinates.go +++ b/internal/tools/commandoracle/corpus_coordinates.go @@ -63,7 +63,7 @@ func syntheticRecordValue() (map[string]any, error) { CounterfeitCorpusDigest: strings.Repeat("2", 64), Entries: entries, ExecutionCommands: executionCommands(candidates), - GoVersion: "go1.26.5", + GoVersion: "go1.26.7", NonClaims: RecordNonClaims(), Platform: "darwin/arm64", SchemaVersion: SchemaVersion,