Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions ADOPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,18 @@ human context in `overview.md` plus machine-admissible `requirements.v1.json`
records. The overview explains context; it does not create uncited durable
truth.

The public source contract still requires `schemaVersion: 1` and the matching
`specPackagePath/overview.md` and `specPackagePath/requirements.v1.json` paths.
The internal grouped v2 model and codec are not an alternative public reader.
Changing a version number or filename does not migrate a source or its bindings.

Canonical field correspondence and accepted input domains are separate checks.
The current public byte limit does not imply that a source fits the private
model's member and expanded-projection budgets. A future source transition must
declare its versioned limits and qualify every affected consumer; it must not
silently narrow current admission or infer scenario bodies from binding IDs.
Keep the public v1 source active until that transition is explicitly admitted.

Proof bindings are verification-route contracts. They answer which scenario,
witness, command, environment class, and receipt policy can falsify or support
a requirement. Native tests and tools own executable verification procedures
Expand Down
118 changes: 118 additions & 0 deletions internal/app/source_compat_domain_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package app

import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"strings"
"testing"

legacy "github.com/research-engineering/agentic-proofkit/internal/command/requirementsourceadmission"
codec "github.com/research-engineering/agentic-proofkit/internal/kernel/requirementsourcecodec"
model "github.com/research-engineering/agentic-proofkit/internal/kernel/requirementsourcemodel"
)

func TestSourceCompatibilityResourceDomainsAreNotInterchangeable(t *testing.T) {
for _, item := range []struct {
count int
code string
}{
{4096, ""}, {16384, "expanded_item_budget_exceeded"}, {16385, "member_budget_exceeded"},
} {
t.Run(fmt.Sprint(item.count), func(t *testing.T) {
data := sourceCompatJSON(t, legacy.SourceValue(sourceCompatSource(item.count)))
if len(data) >= maxInputBytes {
t.Fatal("source no longer fits the public byte limit; re-evaluate the domain witness")
}
output := runCLI(t, []string{"requirement-source-admission", "--input", "-"}, string(data))
report := sourceCompatDecode(t, output).(map[string]any)
if report["reportKind"] != "proofkit.requirement-source-admission" || report["state"] != "passed" || report["reportId"] != "source.compatibility" {
t.Fatal("wrong public admission result")
}
source := sourceCompatAdmit(t, sourceCompatDecode(t, data))
for range 2 {
candidate, err := model.Normalize(sourceCompatDraft(source))
if model.ErrorCode(err) != item.code {
t.Fatalf("resource-domain decision changed: got %v, want %s", err, item.code)
}
if item.code == "" && len(candidate.Atomic().Requirements) != item.count {
t.Fatal("candidate dropped an accepted requirement")
}
}
sourceCompatRoundTrip(t, sourceCompatSource(1))
})
}
}

func TestSourceCompatibilityPublicIdentityAndPathsStayV1(t *testing.T) {
source := sourceCompatSource(1)
for _, item := range []struct{ name, field, path string }{
{"overview/directory", "overviewPath", "docs/specs/other/overview.md"},
{"overview/basename", "overviewPath", source.SpecPackagePath + "/summary.md"},
{"overview/both", "overviewPath", "docs/specs/other/summary.md"},
{"requirements/directory", "requirementsPath", "docs/specs/other/requirements.v1.json"},
{"requirements/basename", "requirementsPath", source.SpecPackagePath + "/requirements.v2.json"},
{"requirements/both", "requirementsPath", "docs/specs/other/requirements.v2.json"},
} {
t.Run(item.name, func(t *testing.T) {
input := legacy.SourceValue(source)
input[item.field] = item.path
var stdout, stderr bytes.Buffer
exit := Run(t.Context(), []string{"requirement-source-admission", "--input", "-"}, bytes.NewReader(sourceCompatJSON(t, input)), &stdout, &stderr)
if exit != 1 || stderr.Len() != 0 {
t.Fatalf("path contradiction exit=%d stderr=%q", exit, stderr.String())
}
report := sourceCompatDecode(t, stdout.Bytes()).(map[string]any)
if report["state"] != "failed" || report["reportKind"] != "proofkit.requirement-source-admission" {
t.Fatal("path contradiction did not produce the expected failed report")
}
if report["summary"].(map[string]any)["failureCount"] != json.Number("1") {
t.Fatal("path case triggered an unrelated additional failure")
}
})
}
v1 := sourceCompatJSON(t, legacy.SourceValue(source))
if _, err := codec.Parse(v1); err == nil {
t.Fatal("private v2 reader accepted public v1 bytes")
}
candidate := sourceCompatRoundTrip(t, source)
v2, err := codec.Format(candidate)
if err != nil {
t.Fatal(err)
}
for _, input := range []any{sourceCompatDecode(t, v2), legacy.SourceValue(source)} {
input.(map[string]any)["schemaVersion"] = json.Number("2")
var stdout, stderr bytes.Buffer
exit := Run(t.Context(), []string{"requirement-source-admission", "--input", "-"}, bytes.NewReader(sourceCompatJSON(t, input)), &stdout, &stderr)
if exit != 1 || stdout.Len() != 0 || stderr.Len() == 0 {
t.Fatal("private/version-substituted input crossed the public v1 boundary")
}
}
wrongVersion := sourceCompatDecode(t, v2).(map[string]any)
wrongVersion["schemaVersion"] = json.Number("1")
if _, err := codec.Parse(sourceCompatJSON(t, wrongVersion)); err == nil {
t.Fatal("private grammar accepted the old identity")
}
runCLI(t, []string{"requirement-source-admission", "--input", "-"}, string(v1))
}

func TestSourceCompatibilityCanonicalNullIsNotInventedPresence(t *testing.T) {
input := legacy.SourceValue(sourceCompatSource(1))
row := input["requirements"].([]any)[0].(map[string]any)
without := sourceCompatAdmit(t, input)
row["deferral"] = nil
withNull := sourceCompatAdmit(t, input)
if !reflect.DeepEqual(without, withNull) {
t.Fatal("canonical legacy deferral presence changed")
}
candidate := sourceCompatRoundTrip(t, withNull)
fields := candidate.Layout().Groups[0].Members[0].Fields
if !fields.Deferral.Present || fields.Deferral.Value != nil || !fields.NonClaimRefs.Present || len(fields.NonClaimRefs.Value) != 0 {
t.Fatal("explicit candidate null/empty mapping was not retained")
}
row["nonClaims"] = nil
if _, err := legacy.Evaluate(input); err == nil || !strings.Contains(err.Error(), "array") {
t.Fatal("null collection incorrectly treated as empty")
}
}
181 changes: 181 additions & 0 deletions internal/app/source_compat_fields_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package app

import (
"reflect"
"slices"
"strings"
"testing"

legacy "github.com/research-engineering/agentic-proofkit/internal/command/requirementsourceadmission"
model "github.com/research-engineering/agentic-proofkit/internal/kernel/requirementsourcemodel"
)

func TestSourceCompatibilityCanonicalFieldInventory(t *testing.T) {
for _, item := range []struct {
value any
fields string
}{
{legacy.Source{}, "NonClaims OverviewPath Requirements RequirementsPath SourceID SpecPackagePath"},
{legacy.Requirement{}, "ClaimLevel Deferral Invariant Lifecycle NonClaimRefs NonClaims OwnerID ProofBindingRefs RequirementID RiskClass UpdatePolicy"},
{legacy.Lifecycle{}, "EvidenceRefs ReplacementRequirementIDs State"},
{legacy.Deferral{}, "EvidenceRefs ExpiryRef MergePolicy OwnerID ReviewCondition RiskAcceptedBy"},
{legacy.UpdatePolicy{}, "RequiresImpactDeclaration RequiresProofBindingReview ReviewOwnerID"},
} {
value := reflect.TypeOf(item.value)
actual := []string{}
for i := range value.NumField() {
actual = append(actual, value.Field(i).Name)
}
slices.Sort(actual)
if !slices.Equal(actual, strings.Fields(item.fields)) {
t.Fatalf("%s fields changed; revise the complete mapping: %v", value.Name(), actual)
}
}
source := sourceCompatSource(1)
source.Requirements[0].ClaimLevel = "deferred"
source.Requirements[0].Deferral = &legacy.Deferral{OwnerID: "owner.deferral", RiskAcceptedBy: "owner.risk", ReviewCondition: "Review the input.", ExpiryRef: "expiry.ref", MergePolicy: "merge.policy", EvidenceRefs: []string{"evidence/deferred.json"}}
wire := legacy.SourceValue(sourceCompatAdmit(t, legacy.SourceValue(source)))
requirement := wire["requirements"].([]any)[0].(map[string]any)
count := 0
for _, row := range []struct {
value map[string]any
fields string
}{
{wire, "nonClaims overviewPath requirements requirementsPath schemaVersion sourceId specPackagePath"},
{requirement, "claimLevel deferral invariant lifecycle nonClaimRefs nonClaims ownerId proofBindingRefs requirementId riskClass updatePolicy"},
{requirement["lifecycle"].(map[string]any), "evidenceRefs replacementRequirementIds state"},
{requirement["deferral"].(map[string]any), "evidenceRefs expiryRef mergePolicy ownerId reviewCondition riskAcceptedBy"},
{requirement["updatePolicy"].(map[string]any), "requiresImpactDeclaration requiresProofBindingReview reviewOwnerId"},
} {
keys := []string{}
for key := range row.value {
keys = append(keys, key)
}
slices.Sort(keys)
if !slices.Equal(keys, strings.Fields(row.fields)) {
t.Fatalf("canonical wire fields changed: %v", keys)
}
count += len(keys)
}
if count != 30 {
t.Fatalf("whole-source field inventory=%d, want30 including parent records", count)
}
}

func TestSourceCompatibilityAllFieldsAndVariants(t *testing.T) {
for _, lifecycle := range []string{"active", "deprecated", "removed", "superseded"} {
for _, claim := range []string{"advisory", "blocking", "deferred"} {
if lifecycle != "active" && claim == "blocking" {
continue // This combination is independently forbidden by both owners.
}
for _, risk := range []string{"critical", "high", "low", "medium"} {
t.Run(lifecycle+"/"+claim+"/"+risk, func(t *testing.T) {
source := sourceCompatSource(2)
r := &source.Requirements[0]
r.ClaimLevel, r.RiskClass, r.Lifecycle.State = claim, risk, lifecycle
r.Invariant = "The value preserves\nUnicode \U0001f9ed and literal < & > delimiters."
r.NonClaimRefs = []string{"external.alpha", "external.beta"}
r.NonClaims = []string{"A direct denial remains independent.", "No execution is inferred."}
r.ProofBindingRefs = []string{"proofkit/alpha.json", "proofkit/beta.json"}
r.UpdatePolicy.RequiresImpactDeclaration, r.UpdatePolicy.RequiresProofBindingReview = true, true
if lifecycle != "active" {
r.Lifecycle.EvidenceRefs = []string{"evidence/lifecycle.json"}
}
if lifecycle == "superseded" {
r.Lifecycle.ReplacementRequirementIDs = []string{source.Requirements[1].RequirementID}
}
if claim == "deferred" {
r.Deferral = &legacy.Deferral{OwnerID: "owner.deferral", RiskAcceptedBy: "owner.risk", ReviewCondition: "Review when the declared input changes.",
ExpiryRef: "expiry.condition", MergePolicy: "policy.merge", EvidenceRefs: []string{"evidence/deferral.json"}}
}
sourceCompatRoundTrip(t, source)
})
}
}
}
for _, impact := range []bool{false, true} {
for _, binding := range []bool{false, true} {
source := sourceCompatSource(1)
source.Requirements[0].UpdatePolicy.RequiresImpactDeclaration = impact
source.Requirements[0].UpdatePolicy.RequiresProofBindingReview = binding
candidate := sourceCompatRoundTrip(t, source)
got := candidate.Atomic().Requirements[0].UpdatePolicy
if got.RequiresImpactDeclaration != impact || got.RequiresProofBindingReview != binding {
t.Fatal("independent update-policy operands collapsed")
}
}
}
for _, count := range []int{0, 1, 4096, 4097} {
sourceCompatRoundTrip(t, sourceCompatSource(count))
}
}

func TestSourceCompatibilityRejectsPairedLifecycleContradictions(t *testing.T) {
tests := []struct {
name string
mutate func(*legacy.Source)
}{
{"missing_proof_binding", func(s *legacy.Source) {
r := &s.Requirements[0]
r.ClaimLevel = "blocking"
r.UpdatePolicy.RequiresImpactDeclaration = true
r.UpdatePolicy.RequiresProofBindingReview = true
}},
{"impact_review_required", func(s *legacy.Source) {
r := &s.Requirements[0]
r.ClaimLevel = "blocking"
r.ProofBindingRefs = []string{"proofkit/proof.json"}
r.UpdatePolicy.RequiresProofBindingReview = true
}},
{"proof_binding_review_required", func(s *legacy.Source) {
r := &s.Requirements[0]
r.ClaimLevel = "blocking"
r.ProofBindingRefs = []string{"proofkit/proof.json"}
r.UpdatePolicy.RequiresImpactDeclaration = true
}},
{"missing_deferral", func(s *legacy.Source) { s.Requirements[0].ClaimLevel = "deferred" }},
{"unexpected_deferral", func(s *legacy.Source) {
s.Requirements[0].Deferral = &legacy.Deferral{OwnerID: "owner.deferral", RiskAcceptedBy: "owner.risk", ReviewCondition: "Review the input.", ExpiryRef: "expiry.ref", MergePolicy: "merge.policy", EvidenceRefs: []string{"evidence/deferred.json"}}
}},
{"missing_lifecycle_evidence", func(s *legacy.Source) { s.Requirements[0].Lifecycle.State = "deprecated" }},
{"unexpected_replacement", func(s *legacy.Source) {
s.Requirements[0].Lifecycle.ReplacementRequirementIDs = []string{s.Requirements[1].RequirementID}
}},
{"missing_replacement", func(s *legacy.Source) {
s.Requirements[0].Lifecycle = legacy.Lifecycle{State: "superseded", EvidenceRefs: []string{"evidence/history.json"}}
}},
{"self_replacement", func(s *legacy.Source) {
s.Requirements[0].Lifecycle = legacy.Lifecycle{State: "superseded", EvidenceRefs: []string{"evidence/history.json"}, ReplacementRequirementIDs: []string{s.Requirements[0].RequirementID}}
}},
{"dangling_replacement", func(s *legacy.Source) {
s.Requirements[0].Lifecycle = legacy.Lifecycle{State: "superseded", EvidenceRefs: []string{"evidence/history.json"}, ReplacementRequirementIDs: []string{"REQ-MISSING"}}
}},
{"inactive_replacement", func(s *legacy.Source) {
s.Requirements[0].Lifecycle = legacy.Lifecycle{State: "superseded", EvidenceRefs: []string{"evidence/history.json"}, ReplacementRequirementIDs: []string{s.Requirements[1].RequirementID}}
s.Requirements[1].Lifecycle = legacy.Lifecycle{State: "removed", EvidenceRefs: []string{"evidence/removal.json"}}
}},
{"nonactive_blocking_requirement", func(s *legacy.Source) {
r := &s.Requirements[0]
r.ClaimLevel = "blocking"
r.ProofBindingRefs = []string{"proofkit/proof.json"}
r.UpdatePolicy.RequiresImpactDeclaration = true
r.UpdatePolicy.RequiresProofBindingReview = true
r.Lifecycle = legacy.Lifecycle{State: "removed", EvidenceRefs: []string{"evidence/removal.json"}}
}},
}
for _, item := range tests {
t.Run(item.name, func(t *testing.T) {
source := sourceCompatSource(2)
item.mutate(&source)
raw := sourceCompatDecode(t, sourceCompatJSON(t, legacy.SourceValue(source)))
result, err := legacy.Evaluate(raw)
if err != nil || result.ExitCode != 1 || result.Report.State != "failed" {
t.Fatalf("legacy semantic contradiction was not rejected: %v", err)
}
if _, err := model.Normalize(sourceCompatDraft(result.Source)); model.ErrorCode(err) != item.name {
t.Fatalf("candidate did not reject the intended contradiction: %v", err)
}
sourceCompatRoundTrip(t, sourceCompatSource(2))
})
}
}
Loading
Loading