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