Problem
The canonical provider-lane compose templates project some embed clocks (NEO_OPENAI_COMPATIBLE_BATCH_EMBEDDING_TIMEOUT_MS, probe timeouts) but not others — the contention ladder leaves (NEO_OPENAI_COMPATIBLE_CONTENTION_TIMEOUT_MS, NEO_OPENAI_COMPATIBLE_CONTENTION_RETRY_COUNT) bind deadline-free interactive embedText calls yet appear in no template. An operator reading the deployment file sees 300s/900s deadlines and cannot see the ~47s ladder that actually binds behavior. That invisibility shipped on two successive plane generations and was only found by live provider-activity trace.
Projection = the leaf is present in the template with its default, a plane-class guidance comment (CPU-constrained vs GPU), and parity-checked against the config source.
Scope wording corrected post-merge (see #17125 debt 1). This sentence originally read "govern every single-input embed call", then "the interactive embedText path". Both overstated it: bulk work routes through embedTexts under the batch ceiling, and deadline-bearing probe calls bypass the ladder entirely — #17122 landed caller-owned probe deadlines, so a probe carries its own deadline rather than inheriting the ladder. Three successive narrowings of one sentence is itself the finding: a scope claim in operator-facing prose attracts overstatement, because the writer knows the mechanism and the reader only gets the sentence.
Acceptance Criteria
- Canonical templates enumerate and project every clock in the declared
namespaces × clockSuffixes profile, each with a plane-class guidance comment.
- The config-leaf-parity gate (or a successor template lint) fails when a behavior-binding clock leaf exists in
ai/configBase.mjs without template projection — mechanically, not by review discipline.
- Existing planes' template deltas are enumerated in the PR body (which leaves become newly visible, with their effective current values unchanged).
- ADR-0019 compliance throughout (no re-derivation, no hidden defaults in templates).
AC-1 wording corrected post-merge (see #17125 debt 2). It originally read "every clock named by the ordering contract (the #17114-class derivation table)". #17114 closed NOT_PLANNED on 2026-08-14 without ever producing that table, so the criterion was anchored to an artifact that will never exist. PR #17117's body was corrected to own its declared set; this ticket was not, and a closed ticket stays discoverable — so the two disagreed with the more findable one being wrong. Coverage is what the declared profile says, and widening it is a reviewable policy edit rather than an appeal to a missing derivation.
Contract Ledger
Added post-merge per #17125 debt 2. Read from the shipped artifact ($behaviorBindingProjection in ai/scripts/lint/config-leaf-parity.json at origin/dev), not from the PR narrative.
| Target Surface |
Source of Authority |
Behavior |
Failure Posture |
Evidence |
$behaviorBindingProjection.clockSuffixes |
config-leaf-parity.json, reviewed as a policy edit |
Selects timing/retry leaves mechanically by suffix — no semantic judgment at lint time. Shipped set: _CHUNK_SIZE, _KEEP_ALIVE, _RETRY_COUNT, _RETRY_DELAY_MS, _TIMEOUT_MS, _YIELD_MS (6). |
A leaf outside the suffix set is simply not in scope — silent by design, which is why widening is a deliberate edit and not a heuristic |
lint-config-template-ssot suffix matching |
$behaviorBindingProjection.profiles[file].namespaces |
same |
Bounds which env prefixes a given template is responsible for. Shipped: NEO_OPENAI_COMPATIBLE_ (1 namespace, 1 profile). |
Out-of-namespace env names are not demanded of that file |
per-profile loop in detectUnprojectedBehaviorBindingClocksFromSources |
| Per-line projection shape |
classifyProjection |
A projected line must carry the canonical # ENV: "value" # guidance shape, both token boundaries, a value equal to the resolved config default, and non-empty trailing guidance. |
Four distinct violation codes rather than one: unprojected-behavior-binding-clock, projection-default-mismatch, projection-missing-guidance, plus the guidance-block count |
mutation-tested in PR #17117; a stale value, prose, or an OLD_-prefixed token each fail |
| Declared guidance-block count |
profiles[file].guidanceBlocks |
Prose blocks are counted, not inferred. Shipped: 3. |
Deleting a guidance block is a policy edit someone reviews, not a silent deletion — the per-line annotations alone once left this green |
the count check, added after exactly that false green |
| Lint scan surface |
SCAN_SURFACE |
Declares ai/deploy/** alongside the .mjs roots, so the sibling parity spec demands the CI workflow watch it. |
An undeclared read surface would let the spec report a satisfied invariant over an incomplete picture — the same shape as the unprojected clock these rules exist to catch, one layer up |
lint-guard-ci-parity |
Successor note. PR #17135 (#17121) widens this policy to 8 suffixes (_PERCENT, _WINDOW_MS), 3 namespaces (NEO_MEMORY_SATURATION_, NEO_STORE_MEMORY_SATURATION_) and 4 guidance blocks. The table above is the state this ticket shipped; the widening is recorded in that PR's own ledger.
Evidence class
Template-vs-configBase diff (contention leaves absent from all templates) + the two-generation blind-ship history, 2026-08-14.
Part of epic #17072.
Problem
The canonical provider-lane compose templates project some embed clocks (
NEO_OPENAI_COMPATIBLE_BATCH_EMBEDDING_TIMEOUT_MS, probe timeouts) but not others — the contention ladder leaves (NEO_OPENAI_COMPATIBLE_CONTENTION_TIMEOUT_MS,NEO_OPENAI_COMPATIBLE_CONTENTION_RETRY_COUNT) bind deadline-free interactiveembedTextcalls yet appear in no template. An operator reading the deployment file sees 300s/900s deadlines and cannot see the ~47s ladder that actually binds behavior. That invisibility shipped on two successive plane generations and was only found by live provider-activity trace.Projection = the leaf is present in the template with its default, a plane-class guidance comment (CPU-constrained vs GPU), and parity-checked against the config source.
Acceptance Criteria
namespaces × clockSuffixesprofile, each with a plane-class guidance comment.ai/configBase.mjswithout template projection — mechanically, not by review discipline.Contract Ledger
Added post-merge per #17125 debt 2. Read from the shipped artifact (
$behaviorBindingProjectioninai/scripts/lint/config-leaf-parity.jsonatorigin/dev), not from the PR narrative.$behaviorBindingProjection.clockSuffixesconfig-leaf-parity.json, reviewed as a policy edit_CHUNK_SIZE,_KEEP_ALIVE,_RETRY_COUNT,_RETRY_DELAY_MS,_TIMEOUT_MS,_YIELD_MS(6).lint-config-template-ssotsuffix matching$behaviorBindingProjection.profiles[file].namespacesNEO_OPENAI_COMPATIBLE_(1 namespace, 1 profile).detectUnprojectedBehaviorBindingClocksFromSourcesclassifyProjection# ENV: "value" # guidanceshape, both token boundaries, a value equal to the resolved config default, and non-empty trailing guidance.unprojected-behavior-binding-clock,projection-default-mismatch,projection-missing-guidance, plus the guidance-block countOLD_-prefixed token each failprofiles[file].guidanceBlocks3.SCAN_SURFACEai/deploy/**alongside the.mjsroots, so the sibling parity spec demands the CI workflow watch it.lint-guard-ci-paritySuccessor note. PR #17135 (#17121) widens this policy to 8 suffixes (
_PERCENT,_WINDOW_MS), 3 namespaces (NEO_MEMORY_SATURATION_,NEO_STORE_MEMORY_SATURATION_) and 4 guidance blocks. The table above is the state this ticket shipped; the widening is recorded in that PR's own ledger.Evidence class
Template-vs-configBase diff (contention leaves absent from all templates) + the two-generation blind-ship history, 2026-08-14.
Part of epic #17072.