diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 5d0a2f8..700a36d 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -54,6 +54,8 @@ filesystems. [Native link resolver](../../internal/engine/filesystem_links.go#L2 ## Finding 2 — Contextless imports duplicate root contribution assembly +**Status** — Complete. Bound and contextless imports share root charging, placement, and sequence-item scope assembly. The full test suite passed. + **Priority P3.** Stage 4 calls for sharing the root contribution assembly and accounting. The new contextless path has its own source charging, mapping placement, wrapper charging, pending sequence-item scope diff --git a/internal/engine/contextless_imports.go b/internal/engine/contextless_imports.go index 142a516..ce28609 100644 --- a/internal/engine/contextless_imports.go +++ b/internal/engine/contextless_imports.go @@ -48,36 +48,22 @@ func acquireContextlessInput(input assembledInput, request inputAcquisitionReque } return input, diagnostics } - key := importKeyForMainSource(parsed.source) - info := cloneSourceInfo(*parsed.source) - accounting := input.accounting - accounting.sources++ - accounting.bytes += parsed.bytes - accounting.nodes += countInputNodes(parsed.document) - limits := policy.importLimits - if accounting.sources > limits.sources || accounting.bytes > limits.bytes || accounting.nodes > limits.nodes { - return input, []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate input limit", nil)} + accounting, limits := input.accounting, policy.importLimits + if diagnostic := chargeDeclaringImport(parsed, accounting, limits); diagnostic != nil { + return input, []Diagnostic{*diagnostic} } - contribution := acquiredContribution{source: info, payload: body, identity: key, role: importRoleMain, scopeOwner: key, processing: processing, sequenceDeclarations: sequences} - var scope acquisitionScope - if request.input.placementMode == PlacementModeSequenceItem { - contribution.document = body - local := acquisitionScope{owner: key, contributions: []acquiredContribution{contribution}} - scope = acquisitionScope{owner: key, destination: destination.clone(), contributions: []acquiredContribution{{source: info, payload: body, identity: key, role: importRoleMain, scopeOwner: key, destination: destination.clone(), processing: processing, sequenceDeclarations: sequences, pendingItem: &local}}} - if diagnostic := reservePendingItemPlacements(&scope, limits, accounting); diagnostic != nil { - return input, []Diagnostic{*diagnostic} - } - } else { - _, placed, err := placeDocument(body, destination, body.Location, maxDocumentDepth, maxDocumentNodes) - if err != nil { - return input, []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, info.ID, "configuration exceeds the source depth or node limit after destination placement")} - } - accounting.nodes += destinationWrapperNodeCount(destination) - if accounting.nodes > limits.nodes { - return input, []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", nil)} - } - contribution.document, contribution.destination = placed, destination.clone() - scope = acquisitionScope{owner: key, destination: destination.clone(), contributions: []acquiredContribution{contribution}} + key := importKeyForMainSource(parsed.source) + root := acquiredContribution{ + source: cloneSourceInfo(*parsed.source), payload: body, identity: key, + role: importRoleMain, scopeOwner: key, processing: processing, sequenceDeclarations: sequences, + } + scope, diagnostic := prepareDeclaringImportScope(root, accounting, limits) + if diagnostic != nil { + return input, []Diagnostic{*diagnostic} + } + scope, diagnostic = finalizeDeclaringImportScope(scope, root, accounting, limits) + if diagnostic != nil { + return input, []Diagnostic{*diagnostic} } input.document = body input.composition = &composedDocument{acquisition: []acquisitionScope{scope}, importGraph: newImportInclusionGraph(), inputBytes: accounting.bytes, inputNodes: accounting.nodes} diff --git a/internal/engine/import_acquisition.go b/internal/engine/import_acquisition.go index 73d4a9b..8277125 100644 --- a/internal/engine/import_acquisition.go +++ b/internal/engine/import_acquisition.go @@ -112,9 +112,6 @@ func acquireImportedWithAccess(request importAcquisitionRequest, policy *acquisi _ = closeAccess() return nil, sourceInfos(main.source), []Diagnostic{importRepeatedDiagnostic(Location{SourceID: main.source.ID}, previous, importRoute{})} } - accounting.sources++ - accounting.bytes += main.bytes - accounting.nodes += countInputNodes(main.document) directiveContext := request.input.directiveContext if directiveContext == 0 { directiveContext = rootDirectiveContextImportsOnly @@ -125,7 +122,6 @@ func acquireImportedWithAccess(request importAcquisitionRequest, policy *acquisi request.input.directiveContext = directiveContext state := importTraversal{ access: access, request: request, policy: policy, state: loadState, tier: tier, - scope: acquisitionScope{owner: mainNodeKey, destination: destination.clone()}, sources: []SourceInfo{mainInfo}, active: make(map[resourceIdentity]int), importGraph: newImportInclusionGraph(), @@ -134,11 +130,10 @@ func acquireImportedWithAccess(request importAcquisitionRequest, policy *acquisi state.itemScope = true state.itemPrefix = destination.clone() state.itemFallback = destination.clone() - state.scope.destination = Path{} state.sources[0] = cloneSourceInfo(mainInfo) } - if diagnostic := state.checkInitialLimits(); diagnostic != nil { + if diagnostic := chargeDeclaringImport(main, accounting, limits); diagnostic != nil { _ = closeAccess() return nil, cloneSourceInfos(state.sources), []Diagnostic{*diagnostic} } @@ -164,31 +159,18 @@ func acquireImportedWithAccess(request importAcquisitionRequest, policy *acquisi } else { body, declarations, diagnostics = splitImportDirectives(main.document) } + root := acquiredContribution{ + source: cloneSourceInfo(mainInfo), payload: body, identity: mainNodeKey, + role: importRoleMain, scopeOwner: mainNodeKey, processing: processing, + sequenceDeclarations: sequenceDeclarations, + } if len(diagnostics) == 0 { - if mainIsItem { - state.scope.contributions = append(state.scope.contributions, acquiredContribution{ - source: cloneSourceInfo(mainInfo), identity: mainNodeKey, role: importRoleMain, - scopeOwner: mainNodeKey, payload: body, document: body, processing: processing, - sequenceDeclarations: cloneSequenceDeclarations(sequenceDeclarations), - }) - diagnostics = state.walkDeclarations(basePath, declarations, 0, Path{}, 0) + scope, diagnostic := prepareDeclaringImportScope(root, accounting, limits) + if diagnostic != nil { + diagnostics = []Diagnostic{*diagnostic} } else { - _, placed, err := placeDocument(body, destination, body.Location, maxDocumentDepth, maxDocumentNodes) - placementNodes := destinationWrapperNodeCount(destination) - if err != nil { - diagnostics = []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, main.source.ID, "configuration exceeds the source depth or node limit after destination placement")} - } else if accounting.nodes+placementNodes > limits.nodes { - diagnostics = []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", nil)} - } else { - accounting.nodes += placementNodes - state.scope.contributions = append(state.scope.contributions, acquiredContribution{ - 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, 0) - } + state.scope = scope + diagnostics = state.walkDeclarations(basePath, declarations, 0, scope.destination, 0) } } if len(diagnostics) == 0 { @@ -205,19 +187,8 @@ func acquireImportedWithAccess(request importAcquisitionRequest, policy *acquisi if closeErr != nil { return nil, cloneSourceInfos(state.sources), []Diagnostic{importDiagnostic(CodeImportRead, "configuration root could not be closed", nil)} } - acquisition := state.scope - if mainIsItem { - localScope := state.scope - acquisition = acquisitionScope{ - owner: mainNodeKey, destination: destination.clone(), - contributions: []acquiredContribution{{ - source: cloneSourceInfo(mainInfo), payload: body, identity: mainNodeKey, - role: importRoleMain, scopeOwner: mainNodeKey, destination: destination.clone(), - processing: processing, sequenceDeclarations: cloneSequenceDeclarations(sequenceDeclarations), pendingItem: &localScope, - }}, - } - } - if diagnostic := reservePendingItemPlacements(&acquisition, limits, accounting); diagnostic != nil { + acquisition, diagnostic := finalizeDeclaringImportScope(state.scope, root, accounting, limits) + if diagnostic != nil { return nil, cloneSourceInfos(state.sources), []Diagnostic{*diagnostic} } return &composedDocument{ @@ -226,22 +197,6 @@ func acquireImportedWithAccess(request importAcquisitionRequest, policy *acquisi }, cloneSourceInfos(state.sources), nil } -func (s *importTraversal) checkInitialLimits() *Diagnostic { - if s.state.accounting.sources > s.policy.importLimits.sources { - value := importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the source-count limit", nil) - return &value - } - if s.state.accounting.bytes > s.policy.importLimits.bytes { - value := importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate byte limit", nil) - return &value - } - if s.state.accounting.nodes > s.policy.importLimits.nodes { - value := importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", nil) - return &value - } - return nil -} - func prepareImportDeclarations(declarations []importDeclarationSpec, defaults ImportDefaults, parentDestination Path) ([]resolvedImportPolicy, []Diagnostic) { policies := make([]resolvedImportPolicy, len(declarations)) for index, spec := range declarations { diff --git a/internal/engine/import_scope.go b/internal/engine/import_scope.go index 66c6e70..52bf6f1 100644 --- a/internal/engine/import_scope.go +++ b/internal/engine/import_scope.go @@ -285,3 +285,58 @@ func importedVersionClaim(documents []*document.Node, scope acquisitionScope, co } return nil } + +// The declaring payload contributes once even when there are no admitted imports. +func chargeDeclaringImport(parsed parsedSource, accounting *importAccounting, limits importLimits) *Diagnostic { + accounting.sources++ + accounting.bytes += parsed.bytes + accounting.nodes += countInputNodes(parsed.document) + if accounting.sources > limits.sources { + value := importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the source-count limit", nil) + return &value + } + if accounting.bytes > limits.bytes { + value := importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate byte limit", nil) + return &value + } + if accounting.nodes > limits.nodes { + value := importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", nil) + return &value + } + return nil +} + +func prepareDeclaringImportScope(root acquiredContribution, accounting *importAccounting, limits importLimits) (acquisitionScope, *Diagnostic) { + scope := acquisitionScope{owner: root.identity} + if root.source.placementMode == PlacementModeSequenceItem { + root.document = root.payload + } else { + destination := root.source.Destination + _, placed, err := placeDocument(root.payload, destination, root.payload.Location, maxDocumentDepth, maxDocumentNodes) + if err != nil { + diagnostic := sourceDiagnostic(CodeSourceLimitExceeded, root.source.ID, "configuration exceeds the source depth or node limit after destination placement") + return acquisitionScope{}, &diagnostic + } + placementNodes := destinationWrapperNodeCount(destination) + if accounting.nodes+placementNodes > limits.nodes { + diagnostic := importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", nil) + return acquisitionScope{}, &diagnostic + } + accounting.nodes += placementNodes + root.document, root.destination = placed, destination.clone() + scope.destination = destination.clone() + } + scope.contributions = []acquiredContribution{root} + return scope, nil +} + +func finalizeDeclaringImportScope(scope acquisitionScope, root acquiredContribution, accounting *importAccounting, limits importLimits) (acquisitionScope, *Diagnostic) { + if root.source.placementMode == PlacementModeSequenceItem { + root.destination = root.source.Destination.clone() + localScope := scope + root.pendingItem = &localScope + scope = acquisitionScope{owner: root.identity, destination: root.destination.clone(), contributions: []acquiredContribution{root}} + } + diagnostic := reservePendingItemPlacements(&scope, limits, accounting) + return scope, diagnostic +}