diff --git a/internal/app/authoring_input_guide_test.go b/internal/app/authoring_input_guide_test.go index ad46633..9d131d6 100644 --- a/internal/app/authoring_input_guide_test.go +++ b/internal/app/authoring_input_guide_test.go @@ -32,6 +32,9 @@ func TestAuthoringInputGuideBootstrapCLI(t *testing.T) { } canonical = help } + if !strings.Contains(canonical, "Order authoringRefs by refId, strictly ascending and without duplicates.") { + t.Fatal("authoring help omits multi-reference ordering") + } parts := strings.Split(canonical, "```json\n") if len(parts) != 2 { t.Fatal("authoring help must contain one template, not duplicate source examples") @@ -54,25 +57,57 @@ func TestAuthoringInputGuideBootstrapCLI(t *testing.T) { t.Fatal("authoring template fabricated its source or candidate") } packet["currentRequirementSource"], update["candidateRequirement"] = empty, candidate - for _, mode := range []string{"retrospective_baseline", "pull_request_design"} { - packet["mode"] = mode - report := runAdoptionHelpCLI(t, adoptionHelpJSON(t, packet), args...) - if report["planKind"] != "proofkit.requirement-authoring-plan" || report["state"] != "passed" || report["mode"] != mode { - t.Fatalf("wrong authoring report: %v", report) - } - preview := report["nonAuthoritativeAdmissionPreview"].(map[string]any) - if preview["candidateOnly"] != true || preview["ownerReviewRequired"] != true || preview["authority"] != "candidate_only" { - t.Fatal("admitted bootstrap became product approval") - } - if !reflect.DeepEqual(preview["requirementSourcePreview"], source) { - t.Fatalf("bootstrap source mismatch:\ngot: %s\nwant: %s", adoptionHelpJSON(t, preview["requirementSourcePreview"]), adoptionHelpJSON(t, source)) - } - materialization["requirementSources"] = []any{preview["requirementSourcePreview"]} - plan := runAdoptionHelpCLI(t, adoptionHelpJSON(t, materialization), "adopt", "materialize", "plan", "--input", "-", "--repo-root", root) - if plan["state"] != "ready" || plan["sourceIntent"] != "audit-from-code" { - t.Fatal("bootstrap preview cannot feed the existing materialization route") + firstRef := packet["authoringRefs"].([]any)[0].(map[string]any) + secondRef := decodeCLIJSON(t, string(adoptionHelpJSON(t, firstRef))).(map[string]any) + secondRef["refId"] = "example.observation.z" + secondRef["kind"] = "test_summary" + secondRef["path"] = "tests/request_test.go" + secondRef["digest"] = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + secondRef["nonClaims"] = []any{"Separate synthetic test observation; execution is not authenticated."} + secondRef["summary"] = "A second independent observation remains pending owner review." + refs := []any{firstRef, secondRef} + refIDs := []any{firstRef["refId"], secondRef["refId"]} + for _, count := range []int{1, 2} { + packet["authoringRefs"] = refs[:count] + update["sourceRefIds"] = refIDs[:count] + for _, mode := range []string{"retrospective_baseline", "pull_request_design"} { + packet["mode"] = mode + report := runAdoptionHelpCLI(t, adoptionHelpJSON(t, packet), args...) + if report["planKind"] != "proofkit.requirement-authoring-plan" || report["state"] != "passed" || report["mode"] != mode { + t.Fatalf("wrong authoring report: %v", report) + } + if !reflect.DeepEqual(report["authoringRefs"], packet["authoringRefs"]) { + t.Fatal("authoring projection lost or changed a complete reference record") + } + preview := report["nonAuthoritativeAdmissionPreview"].(map[string]any) + if preview["candidateOnly"] != true || preview["ownerReviewRequired"] != true || preview["authority"] != "candidate_only" { + t.Fatal("admitted bootstrap became product approval") + } + if !reflect.DeepEqual(preview["requirementSourcePreview"], source) { + t.Fatalf("bootstrap source mismatch:\ngot: %s\nwant: %s", adoptionHelpJSON(t, preview["requirementSourcePreview"]), adoptionHelpJSON(t, source)) + } + materialization["requirementSources"] = []any{preview["requirementSourcePreview"]} + plan := runAdoptionHelpCLI(t, adoptionHelpJSON(t, materialization), "adopt", "materialize", "plan", "--input", "-", "--repo-root", root) + if plan["state"] != "ready" || plan["sourceIntent"] != "audit-from-code" { + t.Fatal("bootstrap preview cannot feed the existing materialization route") + } } } + for _, mutation := range []string{"unsorted", "duplicate"} { + t.Run(mutation+" authoring refs", func(t *testing.T) { + invalid := decodeCLIJSON(t, string(adoptionHelpJSON(t, packet))).(map[string]any) + refs := invalid["authoringRefs"].([]any) + if mutation == "unsorted" { + refs[0], refs[1] = refs[1], refs[0] + } else { + refs[1] = refs[0] + } + status, stdout, stderr := executeAgentWorkflowCLI(t, args, bytes.NewReader(adoptionHelpJSON(t, invalid)), PresentationCapabilities{}) + if status != 1 || stdout != "" || !strings.Contains(stderr, "authoringRef ids must be sorted and unique") { + t.Fatalf("invalid ref order: status=%d stdout=%q stderr=%q", status, stdout, stderr) + } + }) + } // A source containing the candidate must not be treated as empty bootstrap. packet["currentRequirementSource"] = source diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index 3821bfd..9d4d47e 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -24,7 +24,7 @@ import ( ) const ( - cliContractPublicABISHA256 = "4c40a20ac5333efddf7e9a4b36991ddf5b732fbd357a1538f60b887d6c73c447" + cliContractPublicABISHA256 = "0969a1577b6e76ede1e6b8c71a12e7d479d2a159a53f999c4580bfe1cf987757" 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 5bac669..f1bfac1 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 = "9b71962f3475f0c986dbbeba7c09877829f41a3707467c5cb411e001c8bfd2f1" +const commandContractSourceSHA256 = "3f582e65dbb4fde85269d2e1980d77f235c9ded12b35e42bfc483611c33da400" type generatedCommandContractMetadata struct { InputContractSHA256 string @@ -65,7 +65,7 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "rendered-artifact-freshness": {InputContractSHA256: "sha256:be4f53ef1307b4c16bb15a945f8021473b5a215961f3d38f6f591a0240da91f3", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.rendered-artifact-freshness.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:c9a763142d11daca913672b0bab517767d35e275cd7c8ccb2bb360e6fc8f7425", FlagChoices: map[string][]string{}, RouteTokens: []string{"rendered-artifact-freshness"}}, "repo-profile-admission": {InputContractSHA256: "sha256:3a7331d66195dbdc9f672d380efe8fdb9d1d2e36a764b8bc912dccdd81b0e965", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.repo-profile-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:36d2116fa144aa86d7b9d0ac59b89ad04fb97c85a11f3f65efb7311506761fbd", FlagChoices: map[string][]string{}, RouteTokens: []string{"repo-profile-admission"}}, "repository-inventory": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:4a6fc5b5ef55090854e70927494d220afdee0ae234de4f61a720a6018865f02f", FlagChoices: map[string][]string{}, RouteTokens: []string{"repository-inventory"}}, - "requirement-authoring-plan": {InputContractSHA256: "sha256:f7a53affaa19700bf8a6b38e93c746455b120caee1c42cb28a0b4c2894d75657", 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:68d0efbbc60482db6c028322c33aa3d644a84ef44553a61d350ab80259b1c35d", FlagChoices: map[string][]string{}, RouteTokens: []string{"requirement-authoring-plan"}}, + "requirement-authoring-plan": {InputContractSHA256: "sha256:dd1269ff4b5dcd70ac38c34e7df27b1ca9d31f0f880d84eaf328cbae71c23906", 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:02377126b5d982362fdf845a9cd12bea4a320965493feb9cd92bc0d4a784b74e", FlagChoices: map[string][]string{}, RouteTokens: []string{"requirement-authoring-plan"}}, "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{}, RouteTokens: []string{"requirement-bindings"}}, "requirement-browser-server": {InputContractSHA256: "sha256:557d9f1e6919a6f40b831fb910340f85cdc0a0619d0c4895098308939a262e6e", 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:40a4ac0312d4fb921d572817331a73c629ed807caed789296f992f1482e0d932", FlagChoices: map[string][]string{}, RouteTokens: []string{"requirement-browser-server"}}, "requirement-context-compose": {InputContractSHA256: "sha256:0b8d5eace6247fd8fa01ad7372f0395ea6fa10e7f0aa9fe5bab2ff4ed8a69384", 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:dd08c6e3e66349019a349049345e64fca3d31459e42fe634e98e9a9ade8101f4", FlagChoices: map[string][]string{}, RouteTokens: []string{"requirement-context-compose"}}, diff --git a/internal/command/requirementauthoringplan/input_guide.go b/internal/command/requirementauthoringplan/input_guide.go index 41413f0..371f498 100644 --- a/internal/command/requirementauthoringplan/input_guide.go +++ b/internal/command/requirementauthoringplan/input_guide.go @@ -37,6 +37,10 @@ const inputGuide = `Requirement authoring input guide: Use retrospective_baseline for reviewed code-derived candidates, or pull_request_design for proposed design changes. Neither mode grants approval. + Order authoringRefs by refId, strictly ascending and without duplicates. + Move each complete reference record with its ID; do not sort IDs separately + from their paths, digests, summaries or nonClaims. + Authoring template (two object operands must be supplied): ` + "```json\n" + `{ "schemaVersion": 1, diff --git a/internal/command/stackpreset/preset_ids_generated.go b/internal/command/stackpreset/preset_ids_generated.go index 98da4ed..778fdd8 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 = "9b71962f3475f0c986dbbeba7c09877829f41a3707467c5cb411e001c8bfd2f1" +const presetContractSourceSHA256 = "3f582e65dbb4fde85269d2e1980d77f235c9ded12b35e42bfc483611c33da400" 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 f829af6..d5b8b69 100644 --- a/internal/tools/releasechange/record_test.go +++ b/internal/tools/releasechange/record_test.go @@ -197,7 +197,7 @@ func TestCurrentChangeRecordNamesReviewedSemanticChanges(t *testing.T) { var currentBreakingChanges = []Change{} var currentAdditions = []Change{ - {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."}, + {ChangeID: "proofkit.authoring.reference-order-guidance", Summary: "Clarify in lazy requirement-authoring help that authoringRefs are strictly sorted by unique refId values while complete reference records stay together. Exercise two distinct references through authoring and materialization planning; preserve their exact fields and reject unsorted or duplicate references. 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{} @@ -220,7 +220,7 @@ func validateCurrentChangeRecord(record Record, notes string) error { func currentExpectedReleaseNotes() string { lines := []string{ - "# @research-engineering/agentic-proofkit 0.14.6", + "# @research-engineering/agentic-proofkit 0.14.7", "", "## Breaking Contract Changes", "", @@ -273,7 +273,7 @@ func currentExpectedReleaseNotes() string { "Primary npm channel:", "", "```bash", - "npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.14.6", + "npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.14.7", "```", "", "Pre-1.0 npm consumers must keep this dependency exact-pinned.", @@ -285,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.5 with `npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.14.5`.", + "- Pin npm consumers to the previous admitted version 0.14.6 with `npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.14.6`.", "- 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 61a8e9a..19d2952 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@research-engineering/agentic-proofkit", - "version": "0.14.6", + "version": "0.14.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@research-engineering/agentic-proofkit", - "version": "0.14.6", + "version": "0.14.7", "cpu": [ "arm64", "x64" diff --git a/package.json b/package.json index 9fd1b60..9de3090 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.6", + "version": "0.14.7", "type": "module", "license": "MIT", "sideEffects": false, diff --git a/proofkit/cli-contract.v2.json b/proofkit/cli-contract.v2.json index 8965b91..a1960fb 100644 --- a/proofkit/cli-contract.v2.json +++ b/proofkit/cli-contract.v2.json @@ -4653,7 +4653,7 @@ "rootDefinitionDigest": "sha256:86f8f4913b5316caa63c40d79125199b282d108cbd8b9711db21c2023d1a4ca4", "nativeSource": { "path": "internal/command/requirementauthoringplan", - "canonicalDigest": "sha256:8f6b7c3cbac7c379f8f0534489961d49a8a0ee5e9c737f0d2d8fc9826b747f19", + "canonicalDigest": "sha256:c9df82eae1d9b8a7dd475b2510616d1d3cd851306f83dd5f1bb7983e7effb792", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -4681,7 +4681,7 @@ "rootDefinitionDigest": "sha256:c2264058757e40789005b7cf7417906461f064b9b9bca0c5ddda86704e3b36cc", "nativeSource": { "path": "internal/command/requirementauthoringplan", - "canonicalDigest": "sha256:8f6b7c3cbac7c379f8f0534489961d49a8a0ee5e9c737f0d2d8fc9826b747f19", + "canonicalDigest": "sha256:c9df82eae1d9b8a7dd475b2510616d1d3cd851306f83dd5f1bb7983e7effb792", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { diff --git a/release/change-record.v2.json b/release/change-record.v2.json index 82877c7..51811c5 100644 --- a/release/change-record.v2.json +++ b/release/change-record.v2.json @@ -1,13 +1,13 @@ { "schemaVersion": 2, - "previousVersion": "0.14.5", - "version": "0.14.6", + "previousVersion": "0.14.6", + "version": "0.14.7", "changeClass": "compatible", "breakingChanges": [], "additions": [ { - "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." + "changeId": "proofkit.authoring.reference-order-guidance", + "summary": "Clarify in lazy requirement-authoring help that authoringRefs are strictly sorted by unique refId values while complete reference records stay together. Exercise two distinct references through authoring and materialization planning; preserve their exact fields and reject unsorted or duplicate references. 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": {