LearnNewsExamplesServices
Frontmatter
id15929
titleADR-0019 §5 and configBase JSDoc describe a retired declaration shape
stateClosed
labels
bugdocumentationaiarchitecture
assigneesneo-opus-grace
createdAtJul 25, 2026, 8:01 PM
updatedAtJul 25, 2026, 9:00 PM
githubUrlhttps://github.com/neomjs/neo/issues/15929
authorneo-opus-grace
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJul 25, 2026, 9:00 PM

ADR-0019 §5 and configBase JSDoc describe a retired declaration shape

Closed Backlog/active-chunk-9 bugdocumentationaiarchitecture
neo-opus-grace
neo-opus-grace commented on Jul 25, 2026, 8:01 PM

Context

Two merged PRs changed how ai/ config declarations are written, and neither swept the documentation that describes them. Both gaps are mine: PR #15896 deleted the plane twin, and I authored the ADR §10.1 rewrite that retired its sanction; PR #15914 established a new declaration form, and I claimed the corresponding ADR clause in my review of it rather than requiring the author to route my documentation debt through their branch.

This is one failure class, not two tickets: the declaration-form documentation lags the declaration form. A future author reading either surface is told to write a shape the substrate no longer accepts.

The Problem

Surface 1 — ai/configBase.mjs describes a deleted architecture. Five JSDoc lines still narrate the twin module that #15896 removed:

13: // const (env-free twin resolution — the leaf machinery owns all env binding), so no member
57:  * twin `ai/planeConfig.mjs` (ticket-ref-ok: ADR 0019 §5.5 names that module shape):
58:  * the leaf imports the twin's frozen literals + env names, so leaf↔twin drift is
71:  * The env layer routes through the twin's `parsePlaneIdEnv`, so a path-shaped
93:  * 10s budget, so no twin indirection is warranted.

Line 57 is the worst of them, and its citation is worse than a dead link. §5.5 (ADR §5, list item 5) still resolves — it names the module-scope anchor as the one mechanical reason a literal may live outside the leaf. But its own parenthetical now reads "retires ... the pure-defaults-twin shape it sanctioned". So the comment cites, as its authority, the exact passage that abolished it.

Line 71 compounds it from a second direction: PR #15914 converted that declaration from a raw descriptor object to leaf(CANONICAL_PLANE_ID, 'NEO_PLANE_ID', 'string', {parse: parsePlaneIdEnv}). The sentence is now wrong about the twin and about the shape, from two PRs neither of which touched the sentence.

Surface 2 — ADR §5 item 2 documents a signature that can no longer express a sanctioned case. It reads:

  1. Leaves are declarative: leaf(default, env, type). No inline env-ternaries (A4), no hasEnvValue (A5).

PR #15914 added a fourth parameter and made metadata.parse the override for the type-derived parser — and that was not cosmetic. It was the enabler for the parity-collector fix: before it, a leaf needing a custom env parser had to be written as a raw descriptor object literal, which the config-path collector classifies as a namespace rather than a leaf. Four live declarations were in that state and were converted in that PR (plane.id, two logLevels, memory.sharing.defaultPolicy).

Nothing now records that the raw-descriptor form is non-canonical. The ADR is the SSOT for declaration form; a future author reading §5 item 2 sees a 3-arg signature, needs a custom parser, and reaches for the descriptor object — reintroducing the exact shape #15914 removed, in a form the parity collector silently misreads.

The Architectural Reality

  • ai/configBase.mjs:13,57,58,71,93 — Tier-1 config, sibling to the five per-server configBase.mjs files (structure-map confirms the placement).
  • learn/agentos/decisions/0019-aiconfig-reactive-provider-ssot.md:99 — §5 item 2, the sanctioned-declaration line.
  • learn/agentos/decisions/0019-aiconfig-reactive-provider-ssot.md:107 — §5 item 5's parenthetical, which retires the twin shape line 57 cites as authority.
  • ai/ConfigProvider.mjs:67parse: env ? (metadata?.parse ?? typeParsers[resolvedType] ?? Env.parseString) : null, the resolution order §5 item 2 must now describe.
  • ai/scripts/lint/lint-config-template-ssot.mjs — the collector whose isDescriptor test ('default' in v && 'env' in v && 'type' in v) is why a raw descriptor missing type classifies as a namespace. Relevant beyond parity: the module-scope capture rule allows namespace-proxy captures and forbids primitive-leaf captures, so the misclassification can let a real leaf capture pass.

The Fix

  1. Rewrite the five ai/configBase.mjs JSDoc lines to describe the shipped shape — one shared constant module, leaves declaring from it, the env layer bound by leaf's own parse hook. Drop the §5.5 citation entirely rather than repointing it: the durable-comment rule (check-ticket-archaeology) wants behavior, and the behavior is statable without any ADR reference.
  2. Amend ADR §5 item 2 to state the 4-arg form, name metadata.parse as the sanctioned way to declare a custom env parser, and record that a raw descriptor object literal is non-canonical — with the collector consequence, so the reason travels with the rule.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
leaf(default, env, type, metadata) — documented signature ADR-0019 §5 item 2 Records 4-arg form; metadata.parse overrides the type-derived parser n/a — documentation of shipped behavior, no code change ADR §5 item 2 ai/ConfigProvider.mjs:67; test/playwright/unit/ai/ConfigProvider.spec.mjs pins the override + the env-free-null case
Raw descriptor object literal as a declaration form ADR-0019 §5 item 2 (new clause) Declared non-canonical; leaf() is the form n/a ADR §5 item 2 PR #15914 converted all four live instances; collector isDescriptor requires type
ai/configBase.mjs plane JSDoc the shipped module + #15896 Describes one shared constant module, no twin n/a in-file JSDoc ai/planeConfig.mjs exports CANONICAL_PLANE_ID + resolvePlaneDataRoot only

Decision Record impact

amends ADR 0019 — additive to §5 item 2 (records a shipped signature change and names a non-canonical form). It does not challenge or supersede: §5 item 5's retirement of the twin already landed, and this only stops the retired shape being reachable through a stale sentence.

Acceptance Criteria

  • No occurrence of twin remains in ai/configBase.mjs; the replacement prose names the shipped shape (shared constant module + leaf-bound env) rather than deleting the explanation.
  • ai/configBase.mjs carries no ADR section citation for the plane declaration; the ticket-ref-ok marker on line 57 is removed with it.
  • ADR §5 item 2 states leaf(default, env, type, metadata) and names metadata.parse as the sanctioned custom-parser declaration.
  • ADR §5 item 2 records the raw descriptor object literal as non-canonical, citing the collector's namespace misclassification as the mechanical reason.
  • npm run --silent ai:lint-config-template-ssot (or the lint entry) stays green — documentation-only change must not move the parity surface.
  • Grep proof in the PR body: zero twin hits in ai/configBase.mjs, and the ADR line quoted before/after.

Out of Scope

  • Any mechanical guard against raw descriptor objects. This ticket documents; it does not enforce. A lint rule rejecting descriptor-shaped literals in config trees is a separate, larger change and would need its own red proof. Named here so the absence is a recorded decision rather than an oversight.
  • Changing leaf() or the collector. Both shipped in #15914 and are correct; this only records them.
  • The remaining isDescriptor sensitivity (a raw descriptor missing type classifying as a namespace, which can let a module-scope leaf capture pass). Pre-existing, no live instances after #15914, and it belongs with the guard above.

Avoided Traps

  • Repointing the §5.5 citation instead of removing it. The tempting minimal edit is to update the section reference. It would re-create the same rot on the next ADR reshape, and the durable-comment rule exists precisely because tracking refs decay. State the behavior; drop the ref.
  • Filing this as two tickets. The JSDoc and the ADR clause are one failure class in one neighbourhood, and both are one PR's worth of work. Splitting would be the over-fragmentation anti-pattern.
  • Treating the ADR gap as §3 (antipattern catalog). My own first framing, and wrong: §3 catalogues what not to do, but the gap is that the sanctioned-patterns list documents a signature that can no longer express a sanctioned case. §5 item 2 is the home.

Related

#15896 (twin removal — the JSDoc residue is its unswept half), #15914 / #15908 (the collector fix that established metadata.parse), ADR-0019 §5, §10.1.

Live latest-open sweep: checked latest 20 open issues at 2026-07-25T18:00Z; no equivalent found. A2A in-flight sweep: 30 most recent messages scanned; active claims are parity-chain (#15803), consumer census (#15879), wake-side (#15919), and film lanes — none overlap config-declaration documentation.

Origin Session ID: 26e73986-66fa-4d28-9b02-6053541a5671

Retrieval Hint: "configBase twin JSDoc residue ADR-0019 leaf metadata.parse declaration form"