Record algorithmic efficiency audit findings
This commit is contained in:
245
docs/roadmap/efficiency.md
Normal file
245
docs/roadmap/efficiency.md
Normal file
@@ -0,0 +1,245 @@
|
||||
# Algorithmic Efficiency Audit
|
||||
|
||||
Reviewed revision: `5098b56655bd383e22a57d6cf09d99db3e92090f`.
|
||||
Audit date: 2026-10-25.
|
||||
|
||||
## Scope And Assessment
|
||||
|
||||
This source-level audit reviewed the public facade, acquisition and discovery,
|
||||
strict composition, precedence assembly, collection policy, inheritance, schema
|
||||
and typed-value operations, callbacks, secrets, inspection, and comparison.
|
||||
Knowledge-graph searches and caller traces guided source inspection; the bounds
|
||||
below follow from the actual loops and handoffs, not timing measurements.
|
||||
|
||||
The implementation already uses indexed mapping merges, tuple-indexed sequence
|
||||
matching, sparse item projections, scoped import adjacency, inheritance policy
|
||||
localization, and worklist sensitivity propagation. The findings are remaining
|
||||
batch-processing costs, not a recommendation to replace those mechanisms.
|
||||
|
||||
The review excludes expressly selected algorithms, including deterministic
|
||||
sorting, stable Kahn scheduling, exact numeric identity normalization, and the
|
||||
pairwise registration-overlap check expressly selected in the completed sequence
|
||||
plan at `d0a605a:docs/roadmap/implementation.md`. It also leaves the
|
||||
[deferred performance experiments](future.md#3-performance-experiments) deferred.
|
||||
Required detached copies and genuinely expanding configuration/history output
|
||||
are not treated as avoidable overhead.
|
||||
|
||||
Complexities describe the identified internal work, not an end-to-end runtime
|
||||
bound or a new consumer guarantee. Examples use bounded-depth paths and
|
||||
constant-sized records unless noted; hash-index alternatives use expected lookup
|
||||
costs, with string processing and actual copied payload accounted for separately.
|
||||
Existing resource limits bound realizable inputs but do not remove these growth
|
||||
patterns. No new limits, public caches, dependencies, or release gates are proposed.
|
||||
|
||||
No additional functional defect was established during this pass. The four open
|
||||
findings in the separate [functional correctness audit](correctness.md) remain
|
||||
applicable and are not duplicated here.
|
||||
|
||||
## 1. Policy Retirement Scans Every Collection For Every Replacement
|
||||
|
||||
Status: open. Priority: medium.
|
||||
|
||||
**Relevant code.** `collectionPolicyScope.retireCollectionPoliciesAtOrBelow` in
|
||||
[collection_policy_state.go](../../internal/engine/collection_policy_state.go),
|
||||
lines 163–174, scans the complete policy table. The replacement hook in
|
||||
`mergeOverlayMappingsWithCollectionPolicy`
|
||||
([overlay.go](../../internal/engine/overlay.go), lines 182–185) calls it for each
|
||||
ordinary replaced value. `applyOverlaySequenceEvidence` also calls it for
|
||||
whole-collection replacement.
|
||||
|
||||
**Scaling case.** A lower tier has P independently declared collection policies
|
||||
and Q ordinary scalar settings elsewhere. A higher tier changes those Q scalar
|
||||
settings without touching a collection. Each scalar replacement scans all P
|
||||
policies even though none is affected: O(P × Q) policy visits. Taking P = Q = N
|
||||
gives quadratic work for linear-sized inputs, effective values, and policy state.
|
||||
This occurs within one precedence merge; it does not depend on many tiers,
|
||||
registration-overlap checking, or expanding inheritance output.
|
||||
|
||||
**Potential refactoring.** Prepare an operation-local semantic path index over
|
||||
the policy table and use descendant queries for retirement, maintaining it as
|
||||
policies change. Alternatively, batch retirement against the old policy state
|
||||
where its timing can be preserved. For the disjoint example, preparation plus
|
||||
queries can be O(P + Q); generally charge additional work to affected entries
|
||||
and path lengths. Preserve explicit-mode persistence, record ownership, and the
|
||||
point at which old identity is retired. A delayed blanket retirement must not
|
||||
clear newly established higher-tier policy. Existing inheritance policy indexing
|
||||
provides a related mechanism to evaluate, not a requirement to unify its interface.
|
||||
|
||||
## 2. Small Tiers Recopy Unrelated Accumulated Collection Policy
|
||||
|
||||
Status: open. Priority: medium.
|
||||
|
||||
**Relevant code.** `resolveStrictCollectionPolicies` in
|
||||
[compose.go](../../internal/engine/compose.go), lines 330–334, first obtains
|
||||
`effectiveLower` through `lowerCollectionPoliciesForClaims`, then clones the
|
||||
result. The latter helper
|
||||
([collection_retirement.go](../../internal/engine/collection_retirement.go),
|
||||
lines 65–71) already deep-clones and scans the entire lower scope.
|
||||
`cloneCollectionPolicyScope` / `cloneCollectionPolicy` in
|
||||
[collection_policy_state.go](../../internal/engine/collection_policy_state.go)
|
||||
also copy record-order and nested record-policy state.
|
||||
|
||||
`assembleLayeredInput` in [load_request.go](../../internal/engine/load_request.go)
|
||||
supplies the accumulated lower policy when composing each underlay group and
|
||||
when applying that group to the accumulator. The overlay resolver repeats this
|
||||
whole-table preparation and additionally checks all retained policy destinations
|
||||
against the incoming tree (`overlay.go`, lines 145–150).
|
||||
|
||||
**Scaling case.** An initial underlay establishes P constant-sized collection
|
||||
policies. Each of T later underlays contributes only one unrelated scalar, with
|
||||
no declarations or collection changes. Despite having no relevant collection
|
||||
work, each tier copies/scans all P policies: O(T × P) work and cumulative
|
||||
allocation. P = T = N yields quadratic bookkeeping for O(P + T) input and final
|
||||
policy/value sizes. These are transient copies of unchanged private policy, not
|
||||
T separately published snapshots required by the public ownership contract.
|
||||
|
||||
**Potential refactoring.** Separate read-only lower-policy lookup from the owned
|
||||
policy changes produced by the current tier. Resolve only policies intersecting
|
||||
incoming claims/declarations, and apply those changes to the owned assembly
|
||||
state. A path index or localized view can avoid scanning unrelated entries;
|
||||
retain copies where state actually crosses a mutable ownership boundary. At
|
||||
minimum, avoid the immediate duplicate deep copy, but removing only that copy
|
||||
does not change the O(T × P) growth. Aim for work proportional to initial state,
|
||||
incoming material, and affected policy rather than the complete accumulated
|
||||
table per tier. Preserve pending declarations, replacement eligibility, and
|
||||
independent nested record-policy ownership.
|
||||
|
||||
This is distinct from finding 1: one tier can avoid repeated retirement scans
|
||||
and still recopy the full table at every subsequent tier. It is also distinct
|
||||
from the deferred metadata-copy/public-record handoff experiment.
|
||||
|
||||
## 3. Tier-History Projection Repeatedly Searches Wide Mappings
|
||||
|
||||
Status: open. Priority: medium.
|
||||
|
||||
**Relevant code.** `projectTierHistoryApplications` in
|
||||
[sequence_history_projection.go](../../internal/engine/sequence_history_projection.go),
|
||||
lines 17–30, calls unindexed `documentAtPath` for each mapped sequence in a source
|
||||
application. That helper delegates to `documentAtPathWithLookup` with no lookup;
|
||||
[document_walk.go](../../internal/engine/document_walk.go), lines 51–58, then
|
||||
linearly searches each mapping on the path.
|
||||
|
||||
**Scaling case.** A source has N sibling collections under `collections`, each
|
||||
with a constant number of records. A single wildcard registration can identify
|
||||
all of them. When this tier upserts/appends over lower collections, the source
|
||||
application contains N sequence contributions requiring projection. Locating
|
||||
each `/collections/<key>` searches the same N-entry mapping, costing
|
||||
1 + 2 + ... + N entry comparisons: O(N²), despite linear-sized source material
|
||||
and projection output. This does not require N application registrations.
|
||||
|
||||
**Potential refactoring.** Lazily build one read-only `documentLookup` for each
|
||||
original application value with active mapped contributions and reuse it, or
|
||||
collect the relevant sequence nodes in one traversal. With V input nodes, S
|
||||
contributions, maximum depth D, and I projected items, the lookup/projection
|
||||
portion can be O(V + S × D + I), rather than repeated wide scans. Preserve
|
||||
authored coordinates, noncontiguous item projections, replacement evidence,
|
||||
and independent output storage. This concerns load-time coordinate projection,
|
||||
not the deferred branch-local explanation-replay experiment.
|
||||
|
||||
## 4. Compound Identity Restoration Rescans Both Records Per Field
|
||||
|
||||
Status: open. Priority: medium.
|
||||
|
||||
**Relevant code.** `preserveInheritanceIdentityFields` in
|
||||
[inheritance_scope.go](../../internal/engine/inheritance_scope.go), lines 143–161,
|
||||
searches the child's mapping for each identity field, then searches the merged
|
||||
mapping again. `combineInheritanceTemplate` invokes it after merging the parent
|
||||
payload with the child in
|
||||
[inheritance_combine.go](../../internal/engine/inheritance_combine.go), line 104.
|
||||
|
||||
**Scaling case.** For K identity fields, C child entries, and M merged entries,
|
||||
the searches cost O(K × (C + M)), plus the required identity-value clones.
|
||||
A single parent/child relationship with K = C = M = N direct scalar identity
|
||||
fields performs quadratic comparisons for linear-sized records and identity
|
||||
metadata. There is no declared small bound on compound-identity width. The
|
||||
shared identity validator and parent-identity exclusion already use mapping or
|
||||
membership indexes; this restoration step does not.
|
||||
|
||||
**Potential refactoring.** Index merged entry positions once and walk child
|
||||
entries against an identity-field set, or use equivalent operation-local mapping
|
||||
indexes. This can be O(K + C + M), plus required copies. Preserve child-owned
|
||||
identity values and locations, existing mapping order, and detached nodes; do
|
||||
not change parent lookup or identity equality. If the step proves redundant
|
||||
after the established merge, removal needs a separate equivalence review rather
|
||||
than an assumption that identity preservation is unnecessary.
|
||||
|
||||
## 5. Secret Failure Reporting Repeats Unindexed Location Searches
|
||||
|
||||
Status: open. Priority: low; affects batches of failures.
|
||||
|
||||
**Relevant code.** `resolveSecretValue` reports each failed reference through
|
||||
`secretDiagnostic` in
|
||||
[secret_reference.go](../../internal/engine/secret_reference.go), lines 53–78 and
|
||||
112–117. That helper calls unindexed `nearestDocumentLocation`; the mapping
|
||||
branch of `nearestDocumentLocationWithLookup` in
|
||||
[validation.go](../../internal/engine/validation.go), lines 144–155, falls back
|
||||
to a linear search when no lookup is supplied.
|
||||
|
||||
**Scaling case.** An N-entry mapping of secret references whose environment
|
||||
values are all absent produces N diagnostics. Each location lookup starts at
|
||||
the root and scans the mapping for its key, yielding O(N²) comparisons for
|
||||
O(N) references and diagnostics. Reusing the same environment name still
|
||||
reproduces the lookup pattern even though environment lookup itself is cached.
|
||||
Deterministic traversal and diagnostic sorting add their ordinary O(N log N)
|
||||
costs; they do not require the quadratic location work.
|
||||
|
||||
**Potential refactoring.** Share a lazily initialized read-only `documentLookup`
|
||||
for the duration of secret resolution, using `nearestDocumentLocationWithLookup`
|
||||
for failures, or carry matching document/location state through traversal. For
|
||||
V input nodes and F failed references at depth D, attribution can be
|
||||
O(V + F × D), before existing diagnostic sorting. Keep the success path free
|
||||
of an eager index if it is unnecessary. Preserve all diagnostics, nearest-known
|
||||
ancestor fallback, source coordinates, and safe messages; failing fast instead
|
||||
would change behavior rather than optimize it.
|
||||
|
||||
## 6. Dynamic Values Repeat A Type-Graph Eligibility Walk
|
||||
|
||||
Status: open. Priority: low; most relevant to repeated dynamic container types.
|
||||
|
||||
**Relevant code.** `valueOperation.dynamicDescriptor` in
|
||||
[value_operation.go](../../internal/engine/value_operation.go), lines 36–48,
|
||||
uses the schema descriptor cache but still calls `descriptorContainsSecret`
|
||||
with a fresh visited map on every value. That helper (lines 51–68) traverses
|
||||
the reachable descriptor graph. Dynamic branches in projection and cloning
|
||||
invoke it for every non-nil interface value, before handling the concrete value.
|
||||
|
||||
**Scaling case.** Let N dynamic values have the same concrete pointer type whose
|
||||
descriptor graph has size S (nodes plus member/element edges). Each call rechecks
|
||||
the same graph, costing O(N × S), even after descriptor construction is cached.
|
||||
For example, typed defaults can supply N interface entries holding nil pointers
|
||||
to one wide, supported, secret-free struct type. Actual values and projected
|
||||
output contain only N nulls, while the struct has S fields. Taking N = S makes
|
||||
the repeated eligibility walk quadratic in the combined type/value description,
|
||||
without requiring quadratic value expansion.
|
||||
|
||||
**Potential refactoring.** Remember successfully approved dynamic descriptors
|
||||
by concrete type within `valueOperation`, after complete construction and the
|
||||
secret-semantics check. The repeated-type case becomes O(S + N) eligibility
|
||||
work, with one entry per approved concrete type. Do not treat a traversal-local
|
||||
visited marker as a completed negative result for a cyclic descriptor graph.
|
||||
Do not cache partial descriptors or path-bearing failures; coordinate with
|
||||
finding 2 of the [correctness audit](correctness.md), which identifies failed
|
||||
construction cache poisoning. Preserve rejection of dynamic secret-reference
|
||||
types, including nil pointers to them. This is operation-local reuse, not the
|
||||
deferred cross-load schema cache or a redesign of typed cloning.
|
||||
|
||||
## Validation And Recommended Order
|
||||
|
||||
Existing focused tests passed on Go 1.26.10, Linux amd64:
|
||||
|
||||
```sh
|
||||
go test ./internal/engine -run 'Collection|Sequence|Inheritance|Secret|Dynamic' -count=1
|
||||
go test . -run '^TestLocalDocumentationLinks$' -count=1
|
||||
git diff --check
|
||||
```
|
||||
|
||||
No benchmarks, timing comparisons, or new tests were needed to establish the
|
||||
loop bounds. Passing tests do not establish efficient growth. Production code
|
||||
and existing correctness findings are unchanged by this audit.
|
||||
|
||||
Consider findings 1 and 2 together when selecting a policy-state refactoring,
|
||||
while keeping their acceptance cases distinct. Findings 3 and 4 are narrower
|
||||
independent changes; findings 5 and 6 are lower-priority follow-ups. These are
|
||||
opportunities for consideration, not an implementation sequence or new release
|
||||
blockers. Any later refactoring should preserve the existing contracts and use
|
||||
focused behavioral coverage without adding performance thresholds.
|
||||
Reference in New Issue
Block a user