diff --git a/internal/app/adoption_input_guide_test.go b/internal/app/adoption_input_guide_test.go index 2b5c15a..68b75cb 100644 --- a/internal/app/adoption_input_guide_test.go +++ b/internal/app/adoption_input_guide_test.go @@ -76,7 +76,7 @@ func TestAdoptionInputGuideCLI(t *testing.T) { if !reflect.DeepEqual(commands, expectedMaterializationGuideCommands()) { t.Fatalf("published carrier command operands drifted: %v", commands) } - for _, denial := range []string{"NOT an additive patch", "binding's specPath must equal its source's requirementsPath", "durability_unknown", "not an execution"} { + for _, denial := range []string{"NOT an additive patch", "binding's specPath must equal its source's requirementsPath", "durability_unknown", "not an execution", "Sort unique ID/path lists and every nonClaims list lexicographically.", "Do not sort argv; its token order is meaningful."} { if !strings.Contains(guide, denial) { t.Fatalf("guide lost required boundary: %s", denial) } @@ -96,6 +96,16 @@ func TestAdoptionInputGuideWholeChain(t *testing.T) { t.Run(intent, func(t *testing.T) { root := t.TempDir() packet := adoptionHelpPacket(t, root, intent) + nonClaims := []any{"Candidate meaning requires owner review.", "Witness execution is not proven."} + packet["nonClaims"] = nonClaims + source := packet["requirementSources"].([]any)[0].(map[string]any) + source["nonClaims"] = nonClaims + adoptionHelpRequirement(packet)["nonClaims"] = nonClaims + binding := packet["requirementProofBinding"].(map[string]any)["record"].(map[string]any) + binding["nonClaims"] = nonClaims + binding["requirements"].([]any)[0].(map[string]any)["nonClaims"] = nonClaims + packet["testEvidenceInventory"].(map[string]any)["record"].(map[string]any)["nonClaims"] = nonClaims + adoptionHelpEntry(packet)["nonClaims"] = nonClaims payload := adoptionHelpJSON(t, packet) _, help, _ := executeAgentWorkflowCLI(t, []string{"adopt", "materialize", "plan", "--help"}, panicReader{}, PresentationCapabilities{}) commands := guideCommands(t, help, "Materialization input guide:", cliexec.PathRenderer()) @@ -137,8 +147,19 @@ func TestAdoptionInputGuideWholeChain(t *testing.T) { if err != nil { t.Fatal(err) } - binding := decodeCLIJSON(t, string(bindingBytes)).(map[string]any) - route := binding["bindings"].([]any)[0].(map[string]any) + materializedBinding := decodeCLIJSON(t, string(bindingBytes)).(map[string]any) + if !reflect.DeepEqual(materializedBinding["requirements"].([]any)[0].(map[string]any)["nonClaims"], nonClaims) { + t.Fatal("materialization lost ordered requirement nonClaims") + } + inventoryBytes, err := os.ReadFile(filepath.Join(root, "proofkit/test-evidence-inventory.json")) + if err != nil { + t.Fatal(err) + } + materializedInventory := decodeCLIJSON(t, string(inventoryBytes)).(map[string]any) + if !reflect.DeepEqual(materializedInventory["entries"].([]any)[0].(map[string]any)["nonClaims"], nonClaims) { + t.Fatal("materialization lost ordered inventory entry nonClaims") + } + route := materializedBinding["bindings"].([]any)[0].(map[string]any) if route["scenarioId"] != "example.requests.empty" || route["witnessId"] != "example.witness.empty" || route["requirementId"] != "REQ-EXAMPLE-001" { t.Fatal("materialization lost the scenario/requirement/witness edge") } @@ -151,6 +172,38 @@ func TestAdoptionInputGuideWholeChain(t *testing.T) { } } +func TestAdoptionInputGuideNonClaimsOrdering(t *testing.T) { + for _, scope := range []string{"packet", "source", "requirement", "inventory", "entry"} { + for _, invalid := range []struct { + name string + values []any + }{ + {"unsorted", []any{"Witness execution is not proven.", "Candidate meaning requires owner review."}}, + {"duplicate", []any{"Witness execution is not proven.", "Witness execution is not proven."}}, + } { + t.Run(scope+"/"+invalid.name, func(t *testing.T) { + root := t.TempDir() + packet := adoptionHelpPacket(t, root, "fresh") + owners := map[string]map[string]any{ + "packet": packet, + "source": packet["requirementSources"].([]any)[0].(map[string]any), + "requirement": adoptionHelpRequirement(packet), + "inventory": packet["testEvidenceInventory"].(map[string]any)["record"].(map[string]any), + "entry": adoptionHelpEntry(packet), + } + owners[scope]["nonClaims"] = invalid.values + status, stdout, stderr := executeAgentWorkflowCLI(t, []string{"adopt", "materialize", "plan", "--input", "-", "--repo-root", root}, bytes.NewReader(adoptionHelpJSON(t, packet)), PresentationCapabilities{}) + if status != 1 || stdout != "" || !strings.Contains(stderr, "nonClaims") || !strings.Contains(stderr, "sorted") || !strings.Contains(stderr, "unique") { + t.Fatalf("wrong rejection: status=%d stdout=%q stderr=%q", status, stdout, stderr) + } + if entries, err := os.ReadDir(root); err != nil || len(entries) != 0 { + t.Fatal("noncanonical nonClaims planning mutated the repository") + } + }) + } + } +} + func TestAdoptionInputGuideRejectsBrokenEdges(t *testing.T) { for _, mutation := range []struct { name string diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index 3bafb4a..3821bfd 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -24,7 +24,7 @@ import ( ) const ( - cliContractPublicABISHA256 = "bf5359d40ce87aa1ad95ba0c8dec8a61413590e2051ca5b534ba8821a29963fc" + cliContractPublicABISHA256 = "4c40a20ac5333efddf7e9a4b36991ddf5b732fbd357a1538f60b887d6c73c447" 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 899f1d5..5bac669 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 = "9022f0da266be6e31300813712124b04cde47b2d74ab6d5ed4ddaa08f1245e92" +const commandContractSourceSHA256 = "9b71962f3475f0c986dbbeba7c09877829f41a3707467c5cb411e001c8bfd2f1" type generatedCommandContractMetadata struct { InputContractSHA256 string @@ -12,9 +12,9 @@ type generatedCommandContractMetadata struct { } var generatedCommandContractMetadataByName = map[string]generatedCommandContractMetadata{ - "adopt-materialize-apply": {InputContractSHA256: "sha256:ac460820567586e2d9a34544c918a802c3bb06a915c62f873d53f4a34b7fa7af", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.apply-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:0b3b50e294f7f03ea14d55c0908620928651a869da43d8e687f79ed514fba09e", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "apply"}}, - "adopt-materialize-plan": {InputContractSHA256: "sha256:adb7a6e5789fa59350603ef93b190b5929fae682405c5396fd148698ee9625ae", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.plan-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:886301d245ed8f8f5c5e74a2b9d82d201e3981b47bee4ba325f418811622da3b", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "plan"}}, - "adopt-materialize-recover": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:52518686fdecd4fb960734154e199fd6f18a03fce2e8d1bc30fc13c5a82dc31e", FlagChoices: map[string][]string{"--action": []string{"resume", "rollback"}, "--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "recover"}}, + "adopt-materialize-apply": {InputContractSHA256: "sha256:ff3aee6b2420c04d19afdf3a72ef2a73b731f25cc1abcb0f3fe1ffd0fc9ed06b", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.apply-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:37683b52f27ceee6e641ac7ac475edd8939ce6112bea79a3d2ac90448d7252c7", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "apply"}}, + "adopt-materialize-plan": {InputContractSHA256: "sha256:484d8d867ff1080d14f14c9b82c9e02ce0bb10ded7713b8098b5a2f5128fefc2", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.plan-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:5ee0769722eacfbd17a27d977f85067d9be4279d48ed7ce7fd3456156d7d5081", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "plan"}}, + "adopt-materialize-recover": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:0e3b6a87b6f3c68a2cc3f62849830343f1ef69bc9fd67bb8bf198349177b001c", FlagChoices: map[string][]string{"--action": []string{"resume", "rollback"}, "--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "recover"}}, "adopt-plan": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:ba2bb3ce147ac37bde035334e0058820339b36de2a0ae270a9e5dbe00e6a3e6d", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}, "--mode": []string{"audit-from-code", "code-baseline", "fresh"}, "--stack": []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"}}, RouteTokens: []string{"adopt", "plan"}}, "adoption-checklist": {InputContractSHA256: "sha256:4e6c4c9b369279837a5894c0b3f842a411dce529b91c91cb2d4ec63eb5ee4c2c", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.adoption-checklist.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:9d0d0e60f0935407fd31007d8502459663eb4c7228dc5e3c7727ae2c9907bdc9", FlagChoices: map[string][]string{}, RouteTokens: []string{"adoption-checklist"}}, "adoption-contract-envelope": {InputContractSHA256: "sha256:c310214676ff4b6f536a5bc9d687f681a7e71f73d7a03ac932707d8cd3905cdf", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.adoption-contract-envelope.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3efb2c5161fee16fd8ac6a40dcb6d9c41fbc23e468f60621436ae9e8076e0950", FlagChoices: map[string][]string{}, RouteTokens: []string{"adoption-contract-envelope"}}, diff --git a/internal/command/adoptionmaterialization/input_guide.go b/internal/command/adoptionmaterialization/input_guide.go index 55f627b..9b0e1ce 100644 --- a/internal/command/adoptionmaterialization/input_guide.go +++ b/internal/command/adoptionmaterialization/input_guide.go @@ -41,8 +41,10 @@ const inputGuide = `Materialization input guide: requirementSources are requirement-source-admission inputs, not source reports. Binding and inventory record fields are raw owner inputs, not passed reports. Paths are repository-relative; stable IDs and ID lists must obey admission. - Sort unique ID/path lists. Preserve identical requirementId, ownerId, - claimLevel and nonClaims across the source and binding projection. + Sort unique ID/path lists and every nonClaims list lexicographically. + Do not sort argv; its token order is meaningful. Preserve identical + requirementId, ownerId, claimLevel and nonClaims across the source and binding + projection. The binding's specPath must equal its source's requirementsPath; the source does not have a specPath field. Each inventory entry's requirementRefs, witnessRefs, commandRefs and sourcePath diff --git a/internal/command/stackpreset/preset_ids_generated.go b/internal/command/stackpreset/preset_ids_generated.go index 644ed2f..98da4ed 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 = "9022f0da266be6e31300813712124b04cde47b2d74ab6d5ed4ddaa08f1245e92" +const presetContractSourceSHA256 = "9b71962f3475f0c986dbbeba7c09877829f41a3707467c5cb411e001c8bfd2f1" var presetIDs = []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"} diff --git a/internal/tools/releasechange/record_test.go b/internal/tools/releasechange/record_test.go index a34e7aa..f829af6 100644 --- a/internal/tools/releasechange/record_test.go +++ b/internal/tools/releasechange/record_test.go @@ -197,8 +197,7 @@ func TestCurrentChangeRecordNamesReviewedSemanticChanges(t *testing.T) { var currentBreakingChanges = []Change{} var currentAdditions = []Change{ - {ChangeID: "proofkit.adoption.cli-input-guidance", Summary: "Expose a connected requirement, scenario, witness and test-inventory example through on-demand CLI help. Source, binding and inventory help route to exact input pointers instead of requiring package documentation. Authoring help explains empty-source bootstrap, candidate-only previews and lifecycle owners without duplicating the source template. Guidance preserves full desired-state materialization, trust declarations, exact write approval and recovery distinctions; no consumer policy or native execution is introduced."}, - {ChangeID: "proofkit.adoption.guide-contract-witnesses", Summary: "Distinguish direct inventory schemaVersion 1 from aggregate contract v2 and projection-specific versions in input help. Check actual published source-plan and continuation operands, original authoring mode, installed launcher rendering and connected examples through native and installed-carrier witnesses. Preserve accepted input semantics and platform requirements; a declared test route still does not prove execution, approval or complete coverage."}, + {ChangeID: "proofkit.adoption.nonclaims-input-guidance", Summary: "Clarify in lazy materialization input help that nonClaims lists are sorted and unique, while argv token order is preserved. Exercise ordered multi-item nonClaims through planning, approved materialization and stored re-admission; reject unsorted and duplicate lists at packet, source, requirement, inventory and entry boundaries. Refresh source-bound contract digests and the ABI fingerprint. Accepted input semantics, request/report kinds, schema versions, authority denials and platform requirements are unchanged."}, } var currentMigrationSteps = []string{} @@ -221,7 +220,7 @@ func validateCurrentChangeRecord(record Record, notes string) error { func currentExpectedReleaseNotes() string { lines := []string{ - "# @research-engineering/agentic-proofkit 0.14.5", + "# @research-engineering/agentic-proofkit 0.14.6", "", "## Breaking Contract Changes", "", @@ -274,7 +273,7 @@ func currentExpectedReleaseNotes() string { "Primary npm channel:", "", "```bash", - "npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.14.5", + "npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.14.6", "```", "", "Pre-1.0 npm consumers must keep this dependency exact-pinned.", @@ -286,7 +285,7 @@ func currentExpectedReleaseNotes() string { "## Rollback", "", "- First follow the migration and persistent-state compatibility restrictions above; changing a package pin does not roll back repository state.", - "- Pin npm consumers to the previous admitted version 0.14.4 with `npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.14.4`.", + "- Pin npm consumers to the previous admitted version 0.14.5 with `npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.14.5`.", "- Treat local package artifacts as candidates until registry identity is proven.", ) return strings.Join(lines, "\n") + "\n" diff --git a/package-lock.json b/package-lock.json index c260c40..61a8e9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@research-engineering/agentic-proofkit", - "version": "0.14.5", + "version": "0.14.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@research-engineering/agentic-proofkit", - "version": "0.14.5", + "version": "0.14.6", "cpu": [ "arm64", "x64" diff --git a/package.json b/package.json index 0d3431c..9fd1b60 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.14.5", + "version": "0.14.6", "type": "module", "license": "MIT", "sideEffects": false, diff --git a/proofkit/cli-contract.v2.json b/proofkit/cli-contract.v2.json index 95f64d9..8965b91 100644 --- a/proofkit/cli-contract.v2.json +++ b/proofkit/cli-contract.v2.json @@ -112,7 +112,7 @@ "rootDefinitionDigest": "sha256:3dbf760a8f8b0a4093c1dd4edc23367c5ef8027b31c18a01490afb657fcc03ad", "nativeSource": { "path": "internal/command/adoptionmaterialization", - "canonicalDigest": "sha256:60d2a22d78bde9016931c03b85f3ccd41f83e6569d44cc9216fb561093846591", + "canonicalDigest": "sha256:a4899baa2f5e2958552aa1046262248bffdedb4edf4a002db7310de9925ac95d", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -148,7 +148,7 @@ }, { "path": "internal/command/adoptionmaterialization", - "canonicalDigest": "sha256:60d2a22d78bde9016931c03b85f3ccd41f83e6569d44cc9216fb561093846591", + "canonicalDigest": "sha256:a4899baa2f5e2958552aa1046262248bffdedb4edf4a002db7310de9925ac95d", "evidenceClass": "source_checkout" }, { @@ -251,7 +251,7 @@ "rootDefinitionDigest": "sha256:c18995fb310dbde102bea231d83ec3756ffdf863199acbc88237471d06278c9e", "nativeSource": { "path": "internal/command/adoptionmaterialization", - "canonicalDigest": "sha256:60d2a22d78bde9016931c03b85f3ccd41f83e6569d44cc9216fb561093846591", + "canonicalDigest": "sha256:a4899baa2f5e2958552aa1046262248bffdedb4edf4a002db7310de9925ac95d", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -287,7 +287,7 @@ }, { "path": "internal/command/adoptionmaterialization", - "canonicalDigest": "sha256:60d2a22d78bde9016931c03b85f3ccd41f83e6569d44cc9216fb561093846591", + "canonicalDigest": "sha256:a4899baa2f5e2958552aa1046262248bffdedb4edf4a002db7310de9925ac95d", "evidenceClass": "source_checkout" }, { @@ -401,7 +401,7 @@ }, { "path": "internal/command/adoptionmaterialization", - "canonicalDigest": "sha256:60d2a22d78bde9016931c03b85f3ccd41f83e6569d44cc9216fb561093846591", + "canonicalDigest": "sha256:a4899baa2f5e2958552aa1046262248bffdedb4edf4a002db7310de9925ac95d", "evidenceClass": "source_checkout" }, { diff --git a/release/change-record.v2.json b/release/change-record.v2.json index 1b6c2a9..82877c7 100644 --- a/release/change-record.v2.json +++ b/release/change-record.v2.json @@ -1,17 +1,13 @@ { "schemaVersion": 2, - "previousVersion": "0.14.4", - "version": "0.14.5", + "previousVersion": "0.14.5", + "version": "0.14.6", "changeClass": "compatible", "breakingChanges": [], "additions": [ { - "changeId": "proofkit.adoption.cli-input-guidance", - "summary": "Expose a connected requirement, scenario, witness and test-inventory example through on-demand CLI help. Source, binding and inventory help route to exact input pointers instead of requiring package documentation. Authoring help explains empty-source bootstrap, candidate-only previews and lifecycle owners without duplicating the source template. Guidance preserves full desired-state materialization, trust declarations, exact write approval and recovery distinctions; no consumer policy or native execution is introduced." - }, - { - "changeId": "proofkit.adoption.guide-contract-witnesses", - "summary": "Distinguish direct inventory schemaVersion 1 from aggregate contract v2 and projection-specific versions in input help. Check actual published source-plan and continuation operands, original authoring mode, installed launcher rendering and connected examples through native and installed-carrier witnesses. Preserve accepted input semantics and platform requirements; a declared test route still does not prove execution, approval or complete coverage." + "changeId": "proofkit.adoption.nonclaims-input-guidance", + "summary": "Clarify in lazy materialization input help that nonClaims lists are sorted and unique, while argv token order is preserved. Exercise ordered multi-item nonClaims through planning, approved materialization and stored re-admission; reject unsorted and duplicate lists at packet, source, requirement, inventory and entry boundaries. Refresh source-bound contract digests and the ABI fingerprint. Accepted input semantics, request/report kinds, schema versions, authority denials and platform requirements are unchanged." } ], "migration": {