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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,43 @@ import (
"testing"
)

func TestNormalizeOwnsDetachedInputAcrossCalls(t *testing.T) {
for _, owner := range []MetadataOwnerKind{MetadataOwnerMember, MetadataOwnerProfile} {
t.Run(string(owner), func(t *testing.T) {
draft := validDraft()
applyMetadataVariantForOwner(&draft, "claimLevel", string(ClaimDeferred), owner)
model, err := Normalize(draft)
if err != nil {
t.Fatal(err)
}
atomic := detachedTestCopy(model.Atomic())
layout := detachedTestCopy(model.Layout())
references := detachedTestCopy(model.References())
assertUnchanged := func(step string) {
t.Helper()
if !reflect.DeepEqual(model.Atomic(), atomic) || !reflect.DeepEqual(model.Layout(), layout) || !reflect.DeepEqual(model.References(), references) {
t.Fatalf("%s changed an admitted model", step)
}
}
if mutateReferencedState(reflect.ValueOf(&draft).Elem(), false) == 0 {
t.Fatal("draft fixture did not expose mutable references")
}
assertUnchanged("caller mutation")
other := validDraft()
other.SourceID = "source.second"
other.SourceNonClaims = []string{"A separate source boundary."}
other.Groups[0].StatementStem = "The API must"
if _, err := Normalize(other); err != nil {
t.Fatal(err)
}
assertUnchanged("later normalization")
assertAccessorReturnsDetachedState(t, "atomic", model.Atomic)
assertAccessorReturnsDetachedState(t, "layout", model.Layout)
assertAccessorReturnsDetachedState(t, "references", model.References)
})
}
}

func assertAccessorReturnsDetachedState[T any](t *testing.T, name string, accessor func() T) {
t.Helper()
baseline := detachedTestCopy(accessor())
Expand Down
9 changes: 7 additions & 2 deletions internal/kernel/requirementsourcemodel/group_normalization.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ func normalizeGroups(values []Group, profiles map[string]Profile) ([]Group, []At
groups := make([]Group, len(values))
groupIDs := make(map[string]struct{}, len(values))
requirementIDs := map[string]struct{}{}
requirements := []AtomicRequirement{}
origins := []Origin{}
// NormalizeWithLimits has already bounded the complete snapshot membership.
memberCount := 0
for _, group := range values {
memberCount += len(group.Members)
}
requirements := make([]AtomicRequirement, 0, memberCount)
origins := make([]Origin, 0, memberCount)
profileUses := map[string]int{}

for groupIndex, value := range values {
Expand Down
77 changes: 77 additions & 0 deletions internal/kernel/requirementsourcemodel/normalization_cost_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package requirementsourcemodel

import (
"fmt"
"testing"
)

func flatRequirementDraft(count int) Draft {
draft := Draft{SourceID: "source.resource-cost", SpecPackagePath: "docs/specs/resource-cost",
SourceNonClaims: []string{"Synthetic resource observation, not execution authority."}}
if count == 0 {
return draft
}
draft.Groups = []Group{{GroupID: "RGRP-RESOURCE", Members: make([]Member, count)}}
for index := range draft.Groups[0].Members {
draft.Groups[0].Members[index] = Member{
RequirementID: fmt.Sprintf("REQ-RESOURCE-%05d", index),
StatementCompletion: "The operation preserves the declared input value.",
Fields: MetadataFields{
OwnerID: Own("owner.resource"), ClaimLevel: Own(ClaimAdvisory), RiskClass: Own(RiskLow),
NonClaims: Own([]string{"No native witness is asserted."}),
ExternalNonClaimRefs: Own([]string{}), NonClaimRefs: Own([]string{}), ProofBindingRefs: Own([]string{}),
Lifecycle: Own(Lifecycle{State: LifecycleActive}), Deferral: Own[*Deferral](nil),
UpdatePolicy: Own(UpdatePolicy{ReviewOwnerID: "owner.resource"}),
},
}
}
return draft
}

func TestNormalizedMemberStorageMatchesAdmittedCardinality(t *testing.T) {
for _, count := range []int{0, 1, 5, 64, 512, 4096} {
t.Run(fmt.Sprint(count), func(t *testing.T) {
draft := flatRequirementDraft(count)
if count == 5 {
group := draft.Groups[0]
draft.Groups = []Group{
{GroupID: "RGRP-FIRST", Members: group.Members[:2]},
{GroupID: "RGRP-SECOND", Members: group.Members[2:]},
}
}
model, err := Normalize(draft)
if err != nil {
t.Fatal(err)
}
if len(model.atomic.Requirements) != count || cap(model.atomic.Requirements) != count || len(model.layout.Origins) != count || cap(model.layout.Origins) != count {
t.Fatal("model retained excess or incomplete member projection storage")
}
compactedEdges := append([]ReferenceEdge(nil), model.references.Edges...)
if cap(model.references.Edges) != cap(compactedEdges) {
t.Fatal("model retained uncompacted reference edge storage")
}
})
}
}

var normalizationBenchmarkSink Model

func BenchmarkNormalizeMemberStorage(b *testing.B) {
for _, count := range []int{64, 512, 4096} {
b.Run(fmt.Sprint(count), func(b *testing.B) {
draft := flatRequirementDraft(count)
if _, err := Normalize(draft); err != nil {
b.Fatal(err)
}
b.ReportAllocs()
b.ResetTimer()
for range b.N {
model, err := Normalize(draft)
if err != nil {
b.Fatal(err)
}
normalizationBenchmarkSink = model
}
})
}
}
6 changes: 4 additions & 2 deletions internal/kernel/requirementsourcemodel/normalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,11 @@ func NormalizeWithLimits(draft Draft, limits Limits) (Model, error) {
}
layout := LayoutProjection{SourceID: sourceID, Profiles: profiles, Groups: groups, Origins: origins}
references := ReferenceProjection{SourceID: sourceID, Derivations: derivations, Edges: edges}
// Atomic and layout values belong to this detached snapshot; accessors copy
// them on exit. Reference cloning also trims its append-built edge storage.
return Model{
atomic: cloneAtomicProjection(atomic),
layout: cloneLayoutProjection(layout),
atomic: atomic,
layout: layout,
references: cloneReferenceProjection(references),
}, nil
}
Expand Down
8 changes: 4 additions & 4 deletions internal/tools/releasechange/record_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ func TestCurrentChangeRecordNamesReviewedSemanticChanges(t *testing.T) {
var currentBreakingChanges = []Change{}

var currentAdditions = []Change{
{ChangeID: "proofkit.traceability.input-guidance", Summary: "Add lazy CLI recipes for source-intake roles, canonical catalog/tree composition and current-subject review of the exact publication plane. Strengthen connected input and normative-guidance regression oracles and explain scenario-storage ownership. Public source formats, command input/output semantics, runtime admission, native execution authority, dependencies and supported platforms remain unchanged."},
{ChangeID: "proofkit.source-model.materialization", Summary: "Remove redundant atomic and layout copies from private requirement-source normalization, size member projections from the admitted snapshot, and retain reference compaction and detached public accessors. Add allocation benchmarks and independent caller-mutation, later-normalization and retained-capacity checks. Public source formats, CLI semantics, resource limits, dependencies and supported platforms remain unchanged; no source-v2 cutover is introduced."},
}

var currentMigrationSteps = []string{}
Expand All @@ -220,7 +220,7 @@ func validateCurrentChangeRecord(record Record, notes string) error {

func currentExpectedReleaseNotes() string {
lines := []string{
"# @research-engineering/agentic-proofkit 0.14.19",
"# @research-engineering/agentic-proofkit 0.14.20",
"",
"## Breaking Contract Changes",
"",
Expand Down Expand Up @@ -273,7 +273,7 @@ func currentExpectedReleaseNotes() string {
"Primary npm channel:",
"",
"```bash",
"npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.14.19",
"npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.14.20",
"```",
"",
"Pre-1.0 npm consumers must keep this dependency exact-pinned.",
Expand All @@ -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.18 with `npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.14.18`.",
"- Pin npm consumers to the previous admitted version 0.14.19 with `npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.14.19`.",
"- Treat local package artifacts as candidates until registry identity is proven.",
)
return strings.Join(lines, "\n") + "\n"
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@research-engineering/agentic-proofkit",
"description": "Reusable proof profile, report, graph, and witness-planning primitives.",
"version": "0.14.19",
"version": "0.14.20",
"type": "module",
"license": "MIT",
"sideEffects": false,
Expand Down
8 changes: 4 additions & 4 deletions release/change-record.v2.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"schemaVersion": 2,
"previousVersion": "0.14.18",
"version": "0.14.19",
"previousVersion": "0.14.19",
"version": "0.14.20",
"changeClass": "compatible",
"breakingChanges": [],
"additions": [
{
"changeId": "proofkit.traceability.input-guidance",
"summary": "Add lazy CLI recipes for source-intake roles, canonical catalog/tree composition and current-subject review of the exact publication plane. Strengthen connected input and normative-guidance regression oracles and explain scenario-storage ownership. Public source formats, command input/output semantics, runtime admission, native execution authority, dependencies and supported platforms remain unchanged."
"changeId": "proofkit.source-model.materialization",
"summary": "Remove redundant atomic and layout copies from private requirement-source normalization, size member projections from the admitted snapshot, and retain reference compaction and detached public accessors. Add allocation benchmarks and independent caller-mutation, later-normalization and retained-capacity checks. Public source formats, CLI semantics, resource limits, dependencies and supported platforms remain unchanged; no source-v2 cutover is introduced."
}
],
"migration": {
Expand Down
Loading