Activate ordinary source collection declarations

This commit is contained in:
2026-10-23 19:12:39 +00:00
parent dfeaf90320
commit 9a7027a04c
9 changed files with 448 additions and 41 deletions

View File

@@ -52,7 +52,8 @@ preserves already validated secret state without rereading the environment.
The current lifecycle is:
```text
validate options → parse the selected source → traverse/compose enabled imports
validate options → parse the selected source → extract source controls and
traverse/strictly compose enabled imports
→ build schema → check the optional application version → apply a selected overlay
→ apply explicit CLI overrides → expand sibling inheritance
→ create and clone defaults → strictly decode
@@ -75,10 +76,11 @@ may register strict sequence composition paths through
at the destination; `sequence-item` explicitly wraps the mapping as one item at a
non-root destination. Unknown modes, invalid destinations, and invalid or
conflicting sequence registrations fail before source acquisition. Overlay and
CLI coordinates remain rooted at the application root. `NoSource` rejects
CLI coordinates remain rooted at the application root. Ordinary `Load` recognizes
source-root collection declarations whether or not imports are enabled; an
`imports` declaration still requires imports to be enabled. `NoSource` rejects
imports, active overlays, and filesystem context as invalid combinations but
accepts overrides. Input containing a reserved `configkit` mapping key is rejected
while imports are disabled.
accepts overrides and remains an absent source rather than an empty document.
Every source, options, schema, version, defaults, decode, resolution, or validation
failure returns a nil result and a `*LoadError`. Successful results contain caller-owned `Config`, detached
@@ -767,8 +769,11 @@ live services as a substitute for configuration validation.
## Strict document composition
ConfigKit strictly composes parsed base documents when `Options.Imports` is true.
Application defaults do not participate in this operation.
Ordinary `Load` applies strict document composition to its parsed source scope;
`Options.Imports` adds imported documents as peers in that scope. Application
defaults do not participate. With imports disabled, the single source still has
its controls extracted and any declared or registered collection identity is
validated, but there are no imported peers to combine.
| Existing node | Incoming node | Result |
| --- | --- | --- |
@@ -779,11 +784,11 @@ Application defaults do not participate in this operation.
| Any other pair of present nodes | Any other pair of present nodes | `compose.conflict` |
Every non-mapping overlap is a conflict, even when values are equal, except for
sequence/sequence contributions at a registered sequence-composition path. Nulls
remain atomic conflicts. Unregistered sequences conflict when repeated and are
never joined by index. Disjoint entries under shared mappings compose, making
stable keyed mappings the natural representation for independently contributed
entries.
sequence/sequence contributions at a registered sequence-composition path or a
source-declared collection destination. Nulls remain atomic conflicts.
Unregistered and undeclared sequences conflict when repeated and are never joined
by index. Disjoint entries under shared mappings compose, making stable keyed
mappings the natural representation for independently contributed entries.
`Definition.SequenceCompositions` registers literal mapping paths and one literal
`IdentityField` for sequences that should compose independently authored records.
@@ -819,6 +824,26 @@ headers are consumed before strict composition and cannot supply a registered
record identity. Final typed decoding and application validation still run on the
completed configuration.
Ordinary source documents may also declare collection behavior under the root
`configkit.sequences` key. Each entry has a required literal `destination` and may
specify `key` as one string or a nonempty list of strings, plus a `mode` of
`replace`, `append`, or `upsert`. These controls are removed before application
decoding. A declaration destination is relative to that source's placement, so a
mounted fragment's declaration is mounted with its values. Declarations from
strict peers must agree on identity fields and on any explicitly supplied mode;
they apply even when the declaring source has no value at that destination.
Singleton sequence contributions are validated as well as peers. A key supplies
the identity used by `append` and `upsert`; without an explicit or retained mode,
a keyed collection uses `upsert`, while an unkeyed collection uses `replace`.
`append` keeps contribution order and rejects repeated identities. `upsert`
replaces a complete matched record at its existing position and appends new
identities in incoming order; it does not merge record fields.
At this boundary, declarations govern ordinary strict source composition,
including loads with imports disabled. They do not change selected-overlay or
layered precedence, inheritance, or CLI behavior. A root `configkit.imports`
declaration still requires `Options.Imports`; `NoSource` has no declaration scope.
An empty mapping contributes no child definitions. Two empty mappings compose, and
an empty mapping neither clears nor conflicts with another mapping's children.
Shared mapping containers retain all contributing value locations as their composite
@@ -915,9 +940,11 @@ zero.
## Explicit imports
Imports are opt-in. With `Options.Imports` enabled, a document may reserve its root
`configkit` mapping for an `imports` sequence. ConfigKit removes that mapping before
application decoding. Each sequence item is a mapping with exactly one `path`,
Imports are opt-in. With `Options.Imports` enabled, a document may use its root
`configkit` mapping for `imports` and `sequences` controls. Ordinary `Load` may use
`sequences` without enabling imports, but an `imports` field still requires
`Options.Imports`. ConfigKit removes recognized root controls before application
decoding. Each imports sequence item is a mapping with exactly one `path`,
`dir`, or `glob` selector. `path` and `dir` accept a string or a list of strings;
`glob` accepts one path pattern string. The optional `required` boolean controls
whether an empty selection or a missing named path is an error. Its default is
@@ -927,8 +954,8 @@ strings. Every destination string is one literal mapping key and follows the
input-destination validation rules above; quote numeric-looking keys so YAML
reads them as strings. Null, scalar destinations, and non-string members are
`import.invalid`, as are all other unsupported shapes and fields. A `configkit`
key below the root is invalid. With imports disabled, any occurrence remains
`import.disabled`.
key below the root is invalid. With imports disabled, root imports and nested
`configkit` keys remain `import.disabled`.
The directive key is `configkit`, not the `_configkit` spelling used in v0.1.0.
There is no legacy alias: `_configkit` is ordinary application data, subject to

View File

@@ -1,6 +1,6 @@
# Sequence Identity And Combination Implementation Plan
Status: implementation in progress; Stages 1–12 complete.
Status: implementation in progress; Stages 1–13 complete.
Audience: a Luna coding agent implementing one numbered stage per prompt, in
order. This plan starts at commit
@@ -493,6 +493,10 @@ selected-overlay policy surface until Stage 14.
Validation: ordinary-load, import, composition, and metadata tests plus
the everyday check. Include one public ordinary-load declaration example test.
**Completion:** Implemented and verified with ordinary-load, import, composition,
metadata, sequence-item, and source-version tests, the public ordinary-load
example, and `./scripts/check.sh`.
## Stage 14 — Activate Selected Overlay Collection Policies
Scope: ordinary loading's selected overlay and shared precedence integration,

View File

@@ -19,6 +19,28 @@ func ExampleLoad() {
// Output: {true 3} <nil>
}
func ExampleLoad_sequenceDeclaration() {
type Item struct {
ID string `yaml:"id"`
}
type Config struct {
Items []Item `yaml:"items"`
}
result, err := configkit.Load[Config](configkit.Bytes("catalog.yml", []byte(`configkit:
sequences:
- destination: [items]
key: id
items:
- id: prompt
`)))
if err != nil {
fmt.Println(err)
return
}
fmt.Println(result.Config.Items[0].ID)
// Output: prompt
}
func ExampleNoSource() {
type Config struct {
Workers int `yaml:"workers"`

View File

@@ -264,3 +264,72 @@ func disabledDirectiveDiagnostics(node *document.Node, path Path) []Diagnostic {
}
return diagnostics
}
func disabledRootImportDiagnostics(node *document.Node) []Diagnostic {
if node == nil || node.Kind != document.Mapping {
return nil
}
var rootDirective *document.Node
var rootLocation document.Location
var foundRootDirective bool
for index := range node.Mapping {
if node.Mapping[index].Key == directiveKey {
rootDirective = node.Mapping[index].Value
rootLocation = node.Mapping[index].KeyLocation
foundRootDirective = true
break
}
}
if foundRootDirective {
if rootDirective == nil || rootDirective.Kind != document.Mapping {
return disabledDirectiveDiagnostic(rootLocation)
}
for _, directive := range rootDirective.Mapping {
if directive.Key == "imports" {
return disabledDirectiveDiagnostic(rootLocation)
}
}
}
for _, entry := range node.Mapping {
if entry.Key == directiveKey {
continue
}
if diagnostic := disabledNestedDirectiveDiagnostics(entry.Value, Path{}.Key(entry.Key)); len(diagnostic) != 0 {
return diagnostic
}
}
return nil
}
func disabledNestedDirectiveDiagnostics(node *document.Node, path Path) []Diagnostic {
if node.Kind == document.Mapping {
for _, entry := range node.Mapping {
childPath := path.Key(entry.Key)
if entry.Key == directiveKey {
location := Location{SourceID: entry.KeyLocation.SourceID, Line: entry.KeyLocation.Line, Column: entry.KeyLocation.Column}
return []Diagnostic{{
Code: CodeImportDisabled, Severity: SeverityError, Path: childPath,
Message: "configuration directives require imports to be enabled", Primary: &location,
}}
}
if diagnostics := disabledNestedDirectiveDiagnostics(entry.Value, childPath); len(diagnostics) != 0 {
return diagnostics
}
}
} else if node.Kind == document.Sequence {
for index, child := range node.Sequence {
if diagnostics := disabledNestedDirectiveDiagnostics(child, path.Index(index)); len(diagnostics) != 0 {
return diagnostics
}
}
}
return nil
}
func disabledDirectiveDiagnostic(location document.Location) []Diagnostic {
public := Location{SourceID: location.SourceID, Line: location.Line, Column: location.Column}
return []Diagnostic{{
Code: CodeImportDisabled, Severity: SeverityError, Path: Keys(directiveKey),
Message: "configuration directives require imports to be enabled", Primary: &public,
}}
}

View File

@@ -46,12 +46,13 @@ type importAccounting struct {
}
type importTraversal struct {
access *filesystemAccess
accounting *importAccounting
limits importLimits
rule *VersionRule
versionState *sourceVersionState
defaults importDefaults
access *filesystemAccess
accounting *importAccounting
limits importLimits
rule *VersionRule
versionState *sourceVersionState
directiveContext rootDirectiveContext
defaults importDefaults
scope acquisitionScope
itemScope bool
@@ -166,12 +167,17 @@ func loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access *filesystem
if versionState != nil {
defaults = snapshotImportDefaults(versionState.importDefaults)
}
directiveContext := rootDirectiveContextImportsOnly
if processing != nil && processing.directiveContext != 0 {
directiveContext = processing.directiveContext
}
state := importTraversal{
access: access, accounting: accounting, limits: limits, rule: rule, versionState: versionState,
defaults: defaults,
scope: acquisitionScope{owner: mainNodeKey, destination: destination.clone()},
sources: []SourceInfo{mainInfo},
active: make(map[resourceIdentity]int), visited: visited,
directiveContext: directiveContext,
defaults: defaults,
scope: acquisitionScope{owner: mainNodeKey, destination: destination.clone()},
sources: []SourceInfo{mainInfo},
active: make(map[resourceIdentity]int), visited: visited,
importGraph: newImportInclusionGraph(),
}
if mainIsItem {
@@ -231,6 +237,7 @@ func loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access *filesystem
source: cloneSourceInfo(mainInfo), payload: body, document: placed,
identity: mainNodeKey, role: importRoleMain, scopeOwner: mainNodeKey,
destination: destination.clone(), processing: processing,
sequenceDeclarations: cloneSequenceDeclarations(sequenceDeclarations),
})
diagnostics = state.walkDeclarations(basePath, declarations, 0, destination)
}
@@ -627,23 +634,34 @@ func (s *importTraversal) visitPath(candidate selectedImportCandidate, spec impo
var sequenceDeclarations []sequenceDeclarationSpec
var diagnostics []Diagnostic
var processing *sourceVersionProcessingResult
directiveContext := s.directiveContext
if directiveContext == 0 {
directiveContext = rootDirectiveContextImportsOnly
}
if s.versionState != nil && s.versionState.mode != sourceVersionModeDefault {
versionDestination := destination
if s.itemScope {
versionDestination = appendDestination(s.itemPrefix.Index(0), destination)
}
processed, failures := s.versionState.process(root, sourceVersionContribution, versionDestination, sourceID, policy.placement)
processed, failures := s.versionState.processWithDirectiveContext(root, sourceVersionContribution, versionDestination, sourceID, policy.placement, directiveContext)
if len(failures) != 0 {
return failures
}
processing = &processed
body, declarations = processed.payload, processed.imports
} else {
body, declarations, diagnostics = splitImportDirectives(root)
if len(diagnostics) != 0 {
return diagnostics
versionDestination := destination
if s.itemScope {
versionDestination = appendDestination(s.itemPrefix.Index(0), destination)
}
processed := processSourceVersionWithDirectiveContext(root, s.rule, sourceVersionContribution, versionDestination, sourceVersionModeDefault, directiveContext)
if len(processed.diagnostics) != 0 {
return processed.diagnostics
}
processing = &processed
body, declarations = processed.payload, processed.imports
}
sequenceDeclarations = cloneSequenceDeclarations(processing.sequences)
placementDestination := destination
mode := placementModeMapping
if policy.placement == placementModeSequenceItem {

View File

@@ -35,6 +35,7 @@ type Options[T any] struct {
placementMode placementMode
sequencePolicy sequencePolicy
deferAssembly bool
directiveContext rootDirectiveContext
importDefaultsValidated bool
}
@@ -174,6 +175,7 @@ func executeLoadRequest[T any](request loadRequest[T], secrets SecretBinding, lo
mainOptions.Overrides = request.overrides
if request.ordinary {
mainOptions.Overlay = request.overlay
mainOptions.directiveContext = rootDirectiveContextSource
}
var schema *schemaDescriptor

View File

@@ -123,13 +123,43 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d
}
return input, diagnostics
}
directiveContext := options.directiveContext
if directiveContext == 0 {
directiveContext = rootDirectiveContextImportsOnly
}
if !options.Imports && parsed.document != nil {
if diagnostics := disabledDirectiveDiagnostics(parsed.document, Path{}); len(diagnostics) != 0 {
return input, diagnostics
var directiveDiagnostics []Diagnostic
if directiveContext == rootDirectiveContextSource {
directiveDiagnostics = disabledRootImportDiagnostics(parsed.document)
} else {
directiveDiagnostics = disabledDirectiveDiagnostics(parsed.document, Path{})
}
if len(directiveDiagnostics) != 0 {
return input, directiveDiagnostics
}
}
var versionProcessing *sourceVersionProcessingResult
if versionState != nil && versionState.mode != sourceVersionModeDefault {
if directiveContext == rootDirectiveContextSource {
var sourceID string
if parsed.source != nil {
sourceID = parsed.source.ID
}
var processed sourceVersionProcessingResult
var failures []Diagnostic
if versionState != nil {
processed, failures = versionState.processWithDirectiveContext(parsed.document, role, destination, sourceID, options.placementMode, directiveContext)
} else {
processed = processSourceVersionWithDirectiveContext(parsed.document, options.Definition.Version, role, destination, sourceVersionModeDefault, directiveContext)
failures = processed.diagnostics
}
if len(failures) != 0 {
return input, failures
}
versionProcessing = &processed
if role == sourceVersionMain && versionState != nil {
versionState.mainProcessing = &processed
}
} else if versionState != nil && versionState.mode != sourceVersionModeDefault {
var sourceID string
if parsed.source != nil {
sourceID = parsed.source.ID
@@ -175,6 +205,10 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d
localPayload := input.document
input.document = placed
identity := importKeyForMainSource(parsed.source)
var sequenceDeclarations []sequenceDeclarationSpec
if versionProcessing != nil {
sequenceDeclarations = cloneSequenceDeclarations(versionProcessing.sequences)
}
input.acquisition = []acquisitionScope{{
owner: identity,
destination: destination.clone(),
@@ -182,6 +216,7 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d
source: cloneSourceInfo(*parsed.source), payload: localPayload,
identity: identity, role: importRoleMain, scopeOwner: identity,
document: placed, destination: destination.clone(), processing: versionProcessing,
sequenceDeclarations: sequenceDeclarations,
}},
}}
rawNodes := countInputNodes(parsed.document)
@@ -273,6 +308,18 @@ func compositionForInputWithPolicy(input assembledInput, policy sequencePolicy)
}
return input.composition, nil
}
if len(input.acquisition) != 0 {
acquired := &composedDocument{acquisition: cloneAcquisitionScopes(input.acquisition)}
if input.accounting != nil {
acquired.inputBytes = input.accounting.bytes
acquired.inputNodes = input.accounting.nodes
} else {
acquired.inputBytes = input.parsed.bytes
acquired.inputNodes = countInputNodes(input.parsed.document)
}
input.composition = acquired
return composeAcquiredInputWithPolicy(input, policy)
}
if input.document == nil {
return &composedDocument{root: &document.Node{Kind: document.Mapping}, origins: make(map[string]Origin)}, nil
}

View File

@@ -0,0 +1,210 @@
package engine
import (
"errors"
"reflect"
"strings"
"testing"
)
type ordinarySequenceItem struct {
ID string `yaml:"id"`
}
type ordinarySequenceRecord struct {
ID string `yaml:"id"`
Children []ordinarySequenceItem `yaml:"children"`
}
type ordinarySequenceConfig struct {
Items []ordinarySequenceItem `yaml:"items"`
Catalog struct {
Items []ordinarySequenceItem `yaml:"items"`
} `yaml:"catalog"`
Records []ordinarySequenceRecord `yaml:"records"`
}
func TestOrdinaryLoadAcceptsDeclarationsWithoutImports(t *testing.T) {
result, err := testLoad[ordinarySequenceConfig](Bytes("catalog.yml", []byte(`configkit:
sequences:
- destination: [items]
key: id
items:
- id: prompt
`)))
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(result.Config.Items, []ordinarySequenceItem{{ID: "prompt"}}) {
t.Fatalf("items = %#v", result.Config.Items)
}
if _, ok := result.Metadata().Lookup(Keys("configkit")); ok {
t.Fatal("collection controls reached application metadata")
}
items, ok := result.Metadata().Lookup(Keys("items"))
if !ok || !items.Explicit || len(items.Origin.Locations) != 1 {
t.Fatalf("items metadata = %#v, %v", items, ok)
}
}
func TestOrdinaryLoadKeepsDisabledImportRejection(t *testing.T) {
_, err := testLoad[ordinarySequenceConfig](Bytes("disabled.yml", []byte(`configkit:
imports: [{path: fragment.yml}]
sequences: [{destination: [items], key: id}]
`)))
if err == nil {
t.Fatal("source with disabled imports succeeded")
}
var loadError *LoadError
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != CodeImportDisabled {
t.Fatalf("error = %#v", err)
}
}
func TestOrdinaryLoadValidatesSingletonCollectionIdentity(t *testing.T) {
_, err := testLoad[ordinarySequenceConfig](Bytes("missing-identity.yml", []byte(`configkit:
sequences:
- destination: [items]
key: id
items: [{}]
`)))
if err == nil {
t.Fatal("singleton sequence without identity succeeded")
}
var loadError *LoadError
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != CodeComposeInvalid {
t.Fatalf("error = %#v", err)
}
}
func TestOrdinaryLoadCombinesImportedCollectionPeers(t *testing.T) {
tests := []struct {
name string
second string
want []ordinarySequenceItem
wantCode Code
}{
{name: "distinct identities", second: "items: [{id: second}]\n", want: []ordinarySequenceItem{{ID: "first"}, {ID: "second"}}},
{name: "duplicate peer identity", second: "items: [{id: first}]\n", wantCode: CodeComposeConflict},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
imports:
- path: first.yml
- path: second.yml
sequences:
- destination: [items]
key: id
mode: append
`)
writeImportFile(t, directory, "first.yml", "items: [{id: first}]\n")
writeImportFile(t, directory, "second.yml", test.second)
result, err := testLoad[ordinarySequenceConfig](File(mainPath), Options[ordinarySequenceConfig]{Imports: true})
if test.wantCode != "" {
if result != nil || err == nil {
t.Fatalf("result, err = %#v, %v", result, err)
}
var loadError *LoadError
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != test.wantCode {
t.Fatalf("error = %#v", err)
}
return
}
if err != nil {
var loadError *LoadError
if errors.As(err, &loadError) {
t.Fatalf("diagnostics = %#v", loadError.Diagnostics())
}
t.Fatal(err)
}
if !reflect.DeepEqual(result.Config.Items, test.want) {
t.Fatalf("items = %#v, want %#v", result.Config.Items, test.want)
}
inline, err := testLoad[ordinarySequenceConfig](Bytes("inline.yml", []byte(`configkit:
sequences:
- destination: [items]
key: id
mode: append
items: [{id: first}, {id: second}]
`)))
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(result.Config.Items, inline.Config.Items) {
t.Fatalf("imported items %#v differ from inline items %#v", result.Config.Items, inline.Config.Items)
}
})
}
}
func TestImportedCollectionDeclarationsUseRelativePlacement(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
imports:
- path: first.yml
destination: [catalog]
- path: second.yml
destination: [catalog]
`)
fragment := `configkit:
sequences:
- destination: [items]
key: id
mode: append
items: [{id: ITEM}]
`
writeImportFile(t, directory, "first.yml", strings.ReplaceAll(fragment, "ITEM", "first"))
writeImportFile(t, directory, "second.yml", strings.ReplaceAll(fragment, "ITEM", "second"))
result, err := testLoad[ordinarySequenceConfig](File(mainPath), Options[ordinarySequenceConfig]{Imports: true})
if err != nil {
t.Fatal(err)
}
want := []ordinarySequenceItem{{ID: "first"}, {ID: "second"}}
if !reflect.DeepEqual(result.Config.Catalog.Items, want) {
t.Fatalf("catalog items = %#v, want %#v", result.Config.Catalog.Items, want)
}
}
func TestSequenceItemSourceAppliesItsCollectionDeclarations(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
imports:
- path: item.yml
destination: [records]
placement: sequence-item
`)
writeImportFile(t, directory, "item.yml", `configkit:
sequences:
- destination: [children]
key: id
mode: append
id: parent
children: [{id: child}]
`)
result, err := testLoad[ordinarySequenceConfig](File(mainPath), Options[ordinarySequenceConfig]{Imports: true})
if err != nil {
t.Fatal(err)
}
want := []ordinarySequenceRecord{{ID: "parent", Children: []ordinarySequenceItem{{ID: "child"}}}}
if !reflect.DeepEqual(result.Config.Records, want) {
t.Fatalf("records = %#v, want %#v", result.Config.Records, want)
}
}
func TestOrdinaryLoadAllowsDeclarationsWithoutValues(t *testing.T) {
result, err := testLoad[ordinarySequenceConfig](Bytes("policy.yml", []byte(`configkit:
sequences:
- destination: [items]
key: id
`)))
if err != nil {
t.Fatal(err)
}
if result.Config.Items != nil {
t.Fatalf("items = %#v, want absent", result.Config.Items)
}
}

View File

@@ -118,11 +118,18 @@ func (state *sourceVersionState) process(root *document.Node, role sourceVersion
if state == nil || state.mode == sourceVersionModeDefault {
return sourceVersionProcessingResult{}, nil
}
return state.processWithDirectiveContext(root, role, destination, sourceID, placement, rootDirectiveContextImportsOnly)
}
func (state *sourceVersionState) processWithDirectiveContext(root *document.Node, role sourceVersionRole, destination Path, sourceID string, placement placementMode, directiveContext rootDirectiveContext) (sourceVersionProcessingResult, []Diagnostic) {
if state == nil {
return sourceVersionProcessingResult{}, nil
}
projectedDestination := destination.clone()
if placement == placementModeSequenceItem {
projectedDestination = projectedDestination.Index(0)
}
result := processSourceVersion(root, state.localRule, role, projectedDestination, state.mode)
result := processSourceVersionWithDirectiveContext(root, state.localRule, role, projectedDestination, state.mode, directiveContext)
if result.evidence != nil {
result.evidence.sourceID = sourceID
state.evidence = append(state.evidence, result.evidence)
@@ -139,11 +146,12 @@ func (state *sourceVersionState) process(root *document.Node, role sourceVersion
}
type sourceVersionProcessingResult struct {
payload *document.Node
imports []importDeclarationSpec
sequences []sequenceDeclarationSpec
evidence *sourceVersionEvidence
diagnostics []Diagnostic
payload *document.Node
imports []importDeclarationSpec
sequences []sequenceDeclarationSpec
directiveContext rootDirectiveContext
evidence *sourceVersionEvidence
diagnostics []Diagnostic
}
func effectiveSourceVersionRule(local *VersionRule, destination Path, mode sourceVersionMode) *VersionRule {
@@ -214,7 +222,7 @@ func processSourceVersionWithDirectiveContext(root *document.Node, rule *Version
return sourceVersionProcessingResult{diagnostics: diagnostics}
}
result := sourceVersionProcessingResult{
payload: extracted.body, imports: extracted.imports,
payload: extracted.body, imports: extracted.imports, directiveContext: directiveContext,
sequences: cloneSequenceDeclarations(extracted.sequences),
}
if mode == sourceVersionModeDefault {