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
46 changes: 23 additions & 23 deletions internal/kernel/requirementsourcecodec/diagnostic_path.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func resolveRequirementPath(wire document, segments []string) diagnosticPath {
if len(tail) == 0 {
return sameDiagnosticPath(base)
}
if isMetadataField(tail[0]) {
if _, known := metadataFieldPresence(memberValue.Fields, tail[0]); known {
base = metadataOwnerPath(wire, groupValue, memberValue, groupIndex, memberIndex, tail[0])
base = joinPointer(base, tail[0])
tail = tail[1:]
Expand All @@ -56,44 +56,41 @@ func resolveRequirementPath(wire document, segments []string) diagnosticPath {

func metadataOwnerPath(wire document, groupValue group, memberValue member, groupIndex int, memberIndex int, field string) string {
memberBase := pointer("groups", groupIndex, "members", memberIndex, "fields")
if metadataFieldPresent(memberValue.Fields, field) || groupValue.ProfileID == "" {
if present, _ := metadataFieldPresence(memberValue.Fields, field); present || groupValue.ProfileID == "" {
return memberBase
}
for profileIndex, profileValue := range wire.Profiles {
if profileValue.ProfileID == groupValue.ProfileID && metadataFieldPresent(profileValue.Fields, field) {
if present, _ := metadataFieldPresence(profileValue.Fields, field); profileValue.ProfileID == groupValue.ProfileID && present {
return pointer("profiles", profileIndex, "fields")
}
}
return memberBase
}

func metadataFieldPresent(fields metadataFields, field string) bool {
func metadataFieldPresence(fields metadataFields, field string) (present bool, known bool) {
switch field {
case "nonClaims":
return fields.NonClaims != nil, true
case "externalNonClaimRefs":
return fields.ExternalNonClaimRefs != nil, true
case "proofBindingRefs":
return fields.ProofBindingRefs != nil, true
case "ownerId":
return fields.OwnerID != nil
return fields.OwnerID != nil, true
case "claimLevel":
return fields.ClaimLevel != nil
return fields.ClaimLevel != nil, true
case "riskClass":
return fields.RiskClass != nil
return fields.RiskClass != nil, true
case "nonClaimRefs":
return fields.NonClaimRefs != nil
return fields.NonClaimRefs != nil, true
case "lifecycle":
return fields.Lifecycle != nil
return fields.Lifecycle != nil, true
case "deferral":
return fields.Deferral != nil
return fields.Deferral != nil, true
case "updatePolicy":
return fields.UpdatePolicy != nil
return fields.UpdatePolicy != nil, true
default:
return false
}
}

func isMetadataField(value string) bool {
switch value {
case "ownerId", "claimLevel", "riskClass", "nonClaimRefs", "lifecycle", "deferral", "updatePolicy":
return true
default:
return false
return false, false
}
}

Expand Down Expand Up @@ -186,8 +183,11 @@ func modelPathSegments(path string) []string {
return segments
}
end += offset
if _, err := strconv.Atoi(path[offset+1 : end]); err == nil {
segments = append(segments, path[offset+1:end])
component := path[offset+1 : end]
if identifier, err := strconv.Unquote(component); err == nil {
segments = append(segments, identifier)
} else if _, err := strconv.Atoi(component); err == nil {
segments = append(segments, component)
}
offset = end + 1
default:
Expand Down
153 changes: 153 additions & 0 deletions internal/kernel/requirementsourcecodec/diagnostics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,163 @@ package requirementsourcecodec

import (
"bytes"
"encoding/json"
"fmt"
"slices"
"strings"
"testing"
)

func TestEffectiveNonClaimDuplicateSelectsParticipatingReferenceOwner(t *testing.T) {
for _, owner := range []string{"source", "member", "profile"} {
for _, mixed := range []bool{false, true} {
for _, reordered := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/mixed=%t/reordered=%t", owner, mixed, reordered), func(t *testing.T) {
payload, path, refs := nonClaimDuplicatePayload(t, owner, mixed, reordered, false)
_, err := Parse(payload)
assertDiagnostic(t, err, "duplicate_effective_nonclaim", path)
expected, encodeErr := json.Marshal(refs)
if encodeErr != nil {
t.Fatal(encodeErr)
}
span := err.(*Error).Diagnostic().Span
if !bytes.Equal(payload[span.Start:span.End], expected) {
t.Fatal("diagnostic did not select the exact participating reference array")
}
distinct, _, _ := nonClaimDuplicatePayload(t, owner, mixed, reordered, true)
if _, err := Parse(distinct); err != nil {
t.Fatalf("distinct statement control was rejected: %v", err)
}
})
}
}
}
}

func nonClaimDuplicatePayload(t *testing.T, owner string, mixed, reordered, distinct bool) ([]byte, string, []any) {
t.Helper()
const statement = "Named boundary references do not execute witnesses."
const different = "A separate boundary statement remains independent."
var refs []any
groupIndex, memberIndex := 1, 0
if reordered {
groupIndex, memberIndex = 0, 1
}
path := fmt.Sprintf("/groups/%d/members/%d/fields/nonClaimRefs", groupIndex, memberIndex)
payload := mutateRoot(t, mustPayload(t), func(root map[string]any) {
groups := root["groups"].([]any)
members := groups[1].(map[string]any)["members"].([]any)
fields := members[0].(map[string]any)["fields"].(map[string]any)
refOwner, directOwner := fields, fields
refKey, directKey := "nonClaimRefs", "nonClaims"
if owner == "source" {
refOwner, directOwner = root, root
refKey, directKey = "sourceNonClaimRefs", "sourceNonClaims"
path = "/sourceNonClaimRefs"
} else if owner == "profile" {
refOwner = root["profiles"].([]any)[0].(map[string]any)["fields"].(map[string]any)
refOwner[refKey] = fields[refKey]
for _, member := range members {
delete(member.(map[string]any)["fields"].(map[string]any), refKey)
}
path = "/profiles/0/fields/nonClaimRefs"
}
refs = append(refOwner[refKey].([]any), "NCL-DUP-A")
definitions := root["nonClaimDefinitions"].([]any)
definitions = append(definitions, map[string]any{"nonClaimId": "NCL-DUP-A", "statement": statement})
directOwner[directKey] = []any{}
if mixed {
text := statement
if distinct {
text = different
}
directOwner[directKey] = []any{text}
} else {
text := statement
if distinct {
text = different
}
definitions = append(definitions, map[string]any{"nonClaimId": "NCL-DUP-B", "statement": text})
refs = append(refs, "NCL-DUP-B")
}
if reordered {
slices.Reverse(definitions)
slices.Reverse(refs)
groups[0], groups[1] = groups[1], groups[0]
members[0], members[1] = members[1], members[0]
}
root["nonClaimDefinitions"] = definitions
refOwner[refKey] = refs
})
return payload, path, refs
}

func TestDottedRequirementDiagnosticsPreserveIdentityAndLexicalOwner(t *testing.T) {
for _, profileOwned := range []bool{false, true} {
for _, reordered := range []bool{false, true} {
t.Run(fmt.Sprintf("profile=%t/reordered=%t", profileOwned, reordered), func(t *testing.T) {
groupIndex, memberIndex := 1, 0
if reordered {
groupIndex, memberIndex = 0, 1
}
payload := mutateRoot(t, mustPayload(t), func(root map[string]any) {
groups := root["groups"].([]any)
group := groups[1].(map[string]any)
members := group["members"].([]any)
first := members[0].(map[string]any)
first["requirementId"] = "REQ-AA.alpha"
members[1].(map[string]any)["requirementId"] = "REQ-AA.alpha.proofBindingRefs"
first["fields"].(map[string]any)["proofBindingRefs"] = []any{}
if profileOwned {
profile := root["profiles"].([]any)[0].(map[string]any)
profile["fields"].(map[string]any)["proofBindingRefs"] = []any{}
for _, member := range members {
delete(member.(map[string]any)["fields"].(map[string]any), "proofBindingRefs")
}
}
if reordered {
groups[0], groups[1] = groups[1], groups[0]
members[0], members[1] = members[1], members[0]
}
})
path := fmt.Sprintf("/groups/%d/members/%d/fields/proofBindingRefs", groupIndex, memberIndex)
if profileOwned {
path = "/profiles/0/fields/proofBindingRefs"
}
_, err := Parse(payload)
assertDiagnostic(t, err, "missing_proof_binding", path)
span := err.(*Error).Diagnostic().Span
if !bytes.Equal(payload[span.Start:span.End], []byte("[]")) {
t.Fatal("diagnostic did not select the complete owner value")
}
if strings.Contains(err.Error(), "REQ-AA") {
t.Fatal("diagnostic echoed the dynamic identifier")
}
})
}
}
}

func TestDottedDefinitionDiagnosticsResolveArrayEntry(t *testing.T) {
for _, item := range []struct {
root string
code string
value map[string]any
}{
{"profiles", "vacuous_profile", map[string]any{"profileId": "RPROF-AA.alpha", "fields": map[string]any{"ownerId": "proofkit.extra"}}},
{"nonClaimDefinitions", "unreferenced_definition", map[string]any{"nonClaimId": "NCL-AA.alpha", "statement": "This statement has an independent owner."}},
{"vocabulary", "unreferenced_vocabulary", map[string]any{"termId": "TERM-AA.alpha", "kind": "subject", "label": "sample", "definition": "An unused sample term."}},
} {
t.Run(item.root, func(t *testing.T) {
payload := mutateRoot(t, mustPayload(t), func(root map[string]any) {
root[item.root] = append([]any{item.value}, root[item.root].([]any)...)
})
_, err := Parse(payload)
assertDiagnostic(t, err, item.code, "/"+item.root+"/0")
})
}
}

func TestInvalidUTF8UsesByteOnlyCoordinates(t *testing.T) {
payload := []byte{'{', '"', 'x', '"', ':', '"', 0xff, '"', '}'}
_, err := Parse(payload)
Expand Down
41 changes: 34 additions & 7 deletions internal/kernel/requirementsourcecodec/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type document struct {
Kind string `json:"kind"`
SourceID string `json:"sourceId"`
SpecPackagePath string `json:"specPackagePath"`
SourceNonClaims []string `json:"sourceNonClaims"`
SourceNonClaimRefs []string `json:"sourceNonClaimRefs"`
NonClaimDefinitions []nonClaimDefinition `json:"nonClaimDefinitions"`
Vocabulary []vocabularyTerm `json:"vocabulary"`
Expand Down Expand Up @@ -74,13 +75,16 @@ type member struct {
}

type metadataFields struct {
OwnerID *string `json:"ownerId,omitempty"`
ClaimLevel *string `json:"claimLevel,omitempty"`
RiskClass *string `json:"riskClass,omitempty"`
NonClaimRefs *[]string `json:"nonClaimRefs,omitempty"`
Lifecycle *lifecycle `json:"lifecycle,omitempty"`
Deferral json.RawMessage `json:"deferral,omitempty"`
UpdatePolicy *updatePolicy `json:"updatePolicy,omitempty"`
NonClaims *[]string `json:"nonClaims,omitempty"`
ExternalNonClaimRefs *[]string `json:"externalNonClaimRefs,omitempty"`
ProofBindingRefs *[]string `json:"proofBindingRefs,omitempty"`
OwnerID *string `json:"ownerId,omitempty"`
ClaimLevel *string `json:"claimLevel,omitempty"`
RiskClass *string `json:"riskClass,omitempty"`
NonClaimRefs *[]string `json:"nonClaimRefs,omitempty"`
Lifecycle *lifecycle `json:"lifecycle,omitempty"`
Deferral json.RawMessage `json:"deferral,omitempty"`
UpdatePolicy *updatePolicy `json:"updatePolicy,omitempty"`
}

type lifecycle struct {
Expand Down Expand Up @@ -178,12 +182,22 @@ func draftFromDocument(value document) (requirementsourcemodel.Draft, error) {
}
return requirementsourcemodel.Draft{
SourceID: value.SourceID, SpecPackagePath: value.SpecPackagePath, SourceNonClaimRefs: cloneStrings(value.SourceNonClaimRefs),
SourceNonClaims: cloneStrings(value.SourceNonClaims),
NonClaimDefinitions: definitions, Vocabulary: vocabulary, Derivations: derivations, Profiles: profiles, Groups: groups, Scenarios: scenarios,
}, nil
}

func modelMetadata(value metadataFields) (requirementsourcemodel.MetadataFields, error) {
result := requirementsourcemodel.MetadataFields{}
if value.NonClaims != nil {
result.NonClaims = requirementsourcemodel.Own(cloneStrings(*value.NonClaims))
}
if value.ExternalNonClaimRefs != nil {
result.ExternalNonClaimRefs = requirementsourcemodel.Own(cloneStrings(*value.ExternalNonClaimRefs))
}
if value.ProofBindingRefs != nil {
result.ProofBindingRefs = requirementsourcemodel.Own(cloneStrings(*value.ProofBindingRefs))
}
if value.OwnerID != nil {
result.OwnerID = requirementsourcemodel.Own(*value.OwnerID)
}
Expand Down Expand Up @@ -222,6 +236,7 @@ func documentFromModel(model requirementsourcemodel.Model) (document, error) {
references := model.References()
value := document{
SchemaVersion: SchemaVersion, Kind: DocumentKind, SourceID: atomic.SourceID, SpecPackagePath: atomic.SpecPackagePath,
SourceNonClaims: nonNilStrings(atomic.SourceNonClaims),
SourceNonClaimRefs: nonNilStrings(atomic.SourceNonClaimRefs), NonClaimDefinitions: make([]nonClaimDefinition, len(atomic.NonClaimDefinitions)),
Vocabulary: make([]vocabularyTerm, len(atomic.Vocabulary)), Derivations: make([]derivation, len(references.Derivations)),
Profiles: make([]profile, len(layout.Profiles)), Groups: make([]group, len(layout.Groups)), Scenarios: make([]scenario, len(atomic.Scenarios)),
Expand Down Expand Up @@ -269,6 +284,18 @@ func documentFromModel(model requirementsourcemodel.Model) (document, error) {

func wireMetadata(value requirementsourcemodel.MetadataFields) (metadataFields, error) {
result := metadataFields{}
if value.NonClaims.Present {
item := nonNilStrings(value.NonClaims.Value)
result.NonClaims = &item
}
if value.ExternalNonClaimRefs.Present {
item := nonNilStrings(value.ExternalNonClaimRefs.Value)
result.ExternalNonClaimRefs = &item
}
if value.ProofBindingRefs.Present {
item := nonNilStrings(value.ProofBindingRefs.Value)
result.ProofBindingRefs = &item
}
if value.OwnerID.Present {
item := value.OwnerID.Value
result.OwnerID = &item
Expand Down
4 changes: 4 additions & 0 deletions internal/kernel/requirementsourcecodec/field_manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ func manifestLimitOwner(recordID string, fieldName string) string {
"derivation.requirementIds": limitCollectionItems,
"derivation.nonClaimRefs": limitCollectionItems,
"document.sourceNonClaimRefs": limitCollectionItems,
"document.sourceNonClaims": limitCollectionItems,
"document.nonClaimDefinitions": limitDefinitions,
"document.vocabulary": limitTerms,
"document.derivations": limitDerivations,
Expand All @@ -220,6 +221,9 @@ func manifestLimitOwner(recordID string, fieldName string) string {
"lifecycle.replacementRequirementIds": limitCollectionItems,
"lifecycle.evidenceRefs": limitCollectionItems,
"metadataFields.nonClaimRefs": limitCollectionItems,
"metadataFields.nonClaims": limitCollectionItems,
"metadataFields.externalNonClaimRefs": limitCollectionItems,
"metadataFields.proofBindingRefs": limitCollectionItems,
"scenario.requirementIds": limitCollectionItems,
"scenario.parameters": limitCollectionItems,
"scenario.preconditions": limitCollectionItems,
Expand Down
25 changes: 16 additions & 9 deletions internal/kernel/requirementsourcecodec/fixture_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,24 @@ func testDraft() requirementsourcemodel.Draft {
},
}
memberFields := requirementsourcemodel.MetadataFields{
NonClaimRefs: requirementsourcemodel.Own([]string{"NCL-CODEC-002"}),
Lifecycle: requirementsourcemodel.Own(active),
Deferral: requirementsourcemodel.Own[*requirementsourcemodel.Deferral](nil),
NonClaims: requirementsourcemodel.Own([]string{"Requirement admission does not execute native witnesses."}),
ExternalNonClaimRefs: requirementsourcemodel.Own([]string{"proofkit.nonclaim.native"}),
ProofBindingRefs: requirementsourcemodel.Own([]string{"proofkit/requirement-bindings.json"}),
NonClaimRefs: requirementsourcemodel.Own([]string{"NCL-CODEC-002"}),
Lifecycle: requirementsourcemodel.Own(active),
Deferral: requirementsourcemodel.Own[*requirementsourcemodel.Deferral](nil),
}
completeFields := func(claim requirementsourcemodel.ClaimLevel, lifecycle requirementsourcemodel.Lifecycle, deferral *requirementsourcemodel.Deferral) requirementsourcemodel.MetadataFields {
return requirementsourcemodel.MetadataFields{
OwnerID: requirementsourcemodel.Own("proofkit.codec"),
ClaimLevel: requirementsourcemodel.Own(claim),
RiskClass: requirementsourcemodel.Own(requirementsourcemodel.RiskMedium),
NonClaimRefs: requirementsourcemodel.Own([]string{"NCL-CODEC-002"}),
Lifecycle: requirementsourcemodel.Own(lifecycle),
Deferral: requirementsourcemodel.Own(deferral),
NonClaims: requirementsourcemodel.Own([]string{"Requirement admission does not execute native witnesses."}),
ExternalNonClaimRefs: requirementsourcemodel.Own([]string{"proofkit.nonclaim.native"}),
ProofBindingRefs: requirementsourcemodel.Own([]string{"proofkit/requirement-bindings.json"}),
OwnerID: requirementsourcemodel.Own("proofkit.codec"),
ClaimLevel: requirementsourcemodel.Own(claim),
RiskClass: requirementsourcemodel.Own(requirementsourcemodel.RiskMedium),
NonClaimRefs: requirementsourcemodel.Own([]string{"NCL-CODEC-002"}),
Lifecycle: requirementsourcemodel.Own(lifecycle),
Deferral: requirementsourcemodel.Own(deferral),
UpdatePolicy: requirementsourcemodel.Own(requirementsourcemodel.UpdatePolicy{
ReviewOwnerID: "proofkit.codec",
RequiresImpactDeclaration: true,
Expand All @@ -53,6 +59,7 @@ func testDraft() requirementsourcemodel.Draft {
return requirementsourcemodel.Draft{
SourceID: "proofkit.codec.source",
SpecPackagePath: "docs/specs/proofkit-codec",
SourceNonClaims: []string{"Source admission does not execute native witnesses."},
SourceNonClaimRefs: []string{"NCL-CODEC-001"},
NonClaimDefinitions: []requirementsourcemodel.NonClaimDefinition{
{NonClaimID: "NCL-CODEC-001", Statement: "The codec does not prove implementation correctness."},
Expand Down
Loading
Loading