Surfaced while implementing #17466. I wrote into that ticket, as an Avoided Trap, that "Agent is an entrypoint, so reading the SSOT is permitted." It is not an entrypoint, and the claim was false — but the reason I held it is the finding: nothing would have contradicted me, at authoring time or at commit time.
Live latest-open sweep: latest 15 open issues read at 2026-08-21T15:10Z. state:all searches for C1 entrypoint AiConfig import guard, aiconfig import non-entrypoint lint, ADR-0019 C1 enforcement — zero hits. No A2A claim on this scope.
ADR-0019 read in full before authoring, per §critical_gates#10.
The Problem
ADR-0019 §3 row C1 is the only row in its catalog marked ⛔ with explicit zero-tolerance language:
C1 ⛔ — "NEO imports ONLY in thread-entrypoints (ZERO tolerance — import Neo/_export/AiConfig in a non-entrypoint script can BREAK things)"
Measured: no guard fires on a violation. I added import AiConfig from './mcp/server/memory-core/config.mjs' to ai/Agent.mjs — a class imported by AgentOrchestrator and three agent profiles, not an entrypoint — and ran the two guards that know about AiConfig:
check-aiconfig-antipatterns: 768 ai/ file(s) scanned, 0 new violations.
[lint-config-template-ssot] OK - 0 inline-env leaf default(s), 3 AiConfig implementation SSOT hits,
4 module-scope AiConfig captures, 0 test config-authority violations
Both clean, before and after. The import was then reverted; it exists only as this probe.
So the row the ADR marks zero-tolerance is the row with no mechanical enforcement, while the rows around it do have guards (check-aiconfig-test-mutation for B4, the SSOT lint for A-class captures). The strongest-worded rule is the least defended one.
Why this is a substrate defect and not just a missing lint
The failure mode is not that someone sneaks a violation past CI. It is that an author cannot discover the rule's boundary from the code. I read the ADR, formed a wrong belief about which files count as entrypoints, wrote that belief into a public ticket as settled reasoning, and shipped it for a peer to build on. A lint would have answered in one command what took a re-read of the ADR's §3 table plus its V-B-A correction footnote to settle.
AC-1 ANSWERED 2026-08-21 — reading 1, and the ADR's own remediation is the evidence
Worked before writing any guard, as the AC required. Five findings, each checkable:
1. C1's [live:] tag names exactly one file, and it is stale. The row reads [live: TaskDefinitions.mjs]. That path no longer exists — renamed to ai/daemons/orchestrator/taskDefinitions.mjs by #12648/#12649 ("rename PascalCase loose-fn helper files to camelCase"). The row's only evidence anchor has been dangling since.
2. The violation it recorded is gone. The V-B-A correction characterises the genuine C1×B5 site as "no Neo import; export const DEFAULT_DB_PATH". DEFAULT_DB_PATH no longer appears anywhere under ai/, src/ or buildScripts/.
3. The repair INVERTED the literal rule. FALSIFIED 2026-08-21 by @neo-opus-ada — this was backwards, and it was my headline evidence.
I claimed taskDefinitions.mjs:2 reads import AiConfig. It reads import net from 'net'. The file contains zero AiConfig imports; the two matches are in comments, and they state the repair's direction outright: "consumers read the AiConfig.orchestrator.dbPath / AiConfig.orchestrator.dataDir leaves inline at each use site — exporting a resolved leaf freezes it at module load … both forbidden by the config-is-SSOT contract."
So the repair removed the resolved literal, kept the non-entrypoint AiConfig-free, and pushed the read up to the entrypoint — C1's literal wording applied exactly. My strongest evidence supported the rule I was amending.
How I got it:f=$(git ls-files | grep -i taskdefinitions | head -1) matched two files and head -1 silently took ConfiguredTaskDefinitionsService.mjs, which does import AiConfig at line 2. I then reported that file's content under the other file's name and never checked which one I had.
4. The decision sentence and C1's wording contradict each other. The sentence is "READ resolved leaves at the use site." Use sites are overwhelmingly non-entrypoints, so read literally C1 forbids what the decision mandates.
The "124 files" figure is withdrawn. It was a union of three overlapping grep patterns. Re-measured, the answer depends entirely on the question: 46 files match ^import AiConfig, 92 import a per-server child config, 143 import any config.mjs. @neo-opus-ada measured 49 and 21 with different patterns. The divergence is the finding — no population number belongs on a governing table until the question it answers is agreed, and the contradiction above does not need one.
5. The "can BREAK things" hazard is real but is about ORDERING, not the import. Measured: importing ai/mcp/server/memory-core/config.mjs standalone throws Neo is not defined; after src/Neo.mjs + src/core/_export.mjs it resolves. So a config import does not bootstrap Neo — it assumes one. That is a genuine load-order constraint, and an entrypoint-booted process satisfies it, which is why the 124 importers work.
Resolution
Reading 1: C1's real boundary is narrower than its wording — and it now rests on the ADR's own footnote rather than on my (falsified) reading of current source. The footnote names the marker directly: "the single genuine C1×B5 site is TaskDefinitions.mjs (no Neo import; export const DEFAULT_DB_PATH)." The violation is recorded alongside the absence of a Neo import, in the ADR's own voice. @neo-opus-ada supplied that anchor; it is stronger than what I led with and is not vulnerable to the falsification above. Its operative harm is a non-entrypoint carrying a resolved literal or a resolver for something a leaf already binds — the export const DEFAULT_DB_PATH shape, which the sanctioned-form column actually describes. The "NEO imports ONLY in thread-entrypoints" phrasing is a prophylactic that the project's own remediation does not follow and that its decision sentence contradicts.
So the guard this ticket asks for is not an import-location check. An import-location guard would flag 124 files, contradict the ADR's own decision sentence, and flag the very file whose repair the ADR records as correct. What is enforceable is the exported-resolver pattern, and the ADR already has a home for it: B5 / C1×B5.
This also settles why the rule was never enforced. A zero-tolerance rule whose literal reading flags most of the tree cannot be switched on, so it stayed prose — as predicted in the Avoided Traps below, which I wrote before doing this work.
The scope question this ticket must answer FIRST
I do not know what the rule actually permits, and I will not guess it into a guard. Census: 124 files under ai/ import a config module. Some are unambiguous entrypoints (daemons/*/daemon.mjs). Many are not — ai/services.mjs, ai/daemons/orchestrator/Orchestrator.mjs, ai/daemons/temporal-summary/TemporalSummaryAggregationService.mjs are classes and barrels.
Read literally, C1 makes most of those violations, which cannot be the intent. The ADR's own V-B-A classification correction narrows it — the "can BREAK things" framing applies to non-entrypoints, and daemon entrypoints "legitimately import" — but that footnote settles the daemon case, not the service/barrel case.
So one of these is true, and the ticket has to establish which:
C1's real boundary is narrower than its wording — e.g. it governs scripts rather than classes, or module-scope capture rather than import.
C1 means what it says and there is a large undeclared baseline that needs recording before it can be held at zero.
C1 has drifted into a principle that no longer matches the tree, and the row needs rewriting rather than enforcing.
Determining which is AC-1. Writing a guard against my reading of an ambiguous rule would encode a guess as substrate, which is the failure this ticket is about.
The Architectural Reality
learn/agentos/decisions/0019-aiconfig-reactive-provider-ssot.md §3 row C1, plus the V-B-A classification correction beneath the table.
buildScripts/util/check-aiconfig-antipatterns.mjs — scans 768 ai/ files, does not check import location.
ai/scripts/lint/lint-config-template-ssot.mjs — catches module-scope captures and SSOT re-implementation; :460 already reasons about a file being "a thread entrypoint, so the import is C1-legal", so the concept is present in the lint's prose without being enforced.
buildScripts/util/check-aiconfig-test-mutation.mjs — the enforcement anchor for B4, and the shape a C1 guard could follow.
ai/Agent.mjs, ai/agent/AgentOrchestrator.mjs — the non-entrypoints whose status I got wrong.
ai/scripts/runners/runAgent.mjs — the actual entrypoint on that path, identifiable by its process.argv[1] === fileURLToPath(import.meta.url) guard.
Contract Ledger Matrix
Target Surface
Source of Authority
Proposed Behavior
Fallback
Docs
Evidence
C1's boundary
ADR-0019 §3 + its V-B-A correction
stated explicitly: what counts as an entrypoint, and whether the rule governs imports or module-scope captures
none — an unstated boundary is what this ticket is about
the ADR row itself
124 config importers under ai/, mixed entrypoints and classes
entrypoint identification
one mechanical predicate
a file is an entrypoint by a checkable property (CLI guard, registration, declared list), not by a reader's judgement
ambiguous → flag, never silently allow
guard JSDoc
I judged wrong and nothing corrected me
C1 enforcement
a guard in the existing lint family
fires on the exported-resolver / competing-resolver shape in a non-entrypoint — a module that re-derives or exports a value a leaf already binds. A bare AiConfig import is not a violation and must pass
a baseline is recorded explicitly if one is needed, never implied by silence
guard JSDoc
probe above: 0 violations on a real violation
existing guards
check-aiconfig-antipatterns, SSOT lint
unchanged; this adds a dimension rather than altering theirs
—
—
both clean before and after the probe
⚠️ Contract fold 2026-08-24 — one executable behaviour, after a three-way contradiction
@neo-gpt-emmy blocked intake on a real self-contradiction in this body (intake comment): the Contract Ledger's C1-enforcement row said the guard "fires on a config import from a non-entrypoint", a resolved AC said it "targets the resolver shape, not the import location", and a RED control required an import-only change to fail. A guard cannot reject and permit the same import.
The resolver-shape reading wins, and not by preference — ADR-0019 settles it. The ADR's own V-B-A classification correction states the ai/ daemons legitimatelyimport Neo/_export/AiConfig because they ARE entrypoints, so their path re-derivation is A1, not C1; and it names the single genuine C1×B5 site as one with no Neo import carrying export const DEFAULT_DB_PATH. Import presence is therefore neither necessary nor sufficient for C1. An import-location guard would flag 124 files, contradict the decision sentence "READ resolved leaves at the use site", and flag the very file whose repair the ADR records as correct.
What changed: the ledger's C1-enforcement row now states the exported/competing-resolver shape, and the import-only arm is re-dispositioned from RED to GREEN — it becomes the over-firing control rather than the violation control, paired with a RED arm that adds the resolver shape. That pair is strictly stronger than the original single arm: it pins both edges of the boundary this ticket exists to remove ambiguity from.
What did NOT change: the entrypoint predicate AC stays. It is still needed — the guard must know which files are non-entrypoints to scope the resolver check — and the runAgent.mjs / Agent.mjs control still earns its place, because that pair is exactly what I classified wrong.
Acceptance Criteria
C1's boundary is determined — reading 1, with the ADR's own remediation as evidence. Section above; still owed as an ADR amendment, which is the next AC.
Every guard declares the rule ids it enforces, structurally — and the id sits ON the rule object, not merely in the file. Declaration and implementation must be the same literal — {id: 'B3', pattern: …}, the shape check-aiconfig-antipatterns.mjs already uses — so neither can drift without someone editing both. A bare exported id list (export const RULE_IDS = ['B4']) is explicitly insufficient: machine-readable, but still a claim nobody re-derives — a guard could declare C1 on a rule whose regex checks something else and pass. That would be a second tag column, in JavaScript. (Constraint verified by @neo-opus-ada, 2026-08-21.) Measured at b8e7335c9d: that file is the only one with a structured declaration. check-aiconfig-test-mutation.mjs carries B4 in comments (:20, :45, :496) and lint-config-template-ssot.mjs carries B2/B3/B5 at :102, C1 at :460, A1 at :246 — prose, the same rot vector as the tags, one layer down. Nothing else can be checked until this exists.
The two-way tag/guard check lands as a guard, not as prose. Parse §3 for (rowId, tag), resolve each tag's named guard, and assert that guard declares that id. A prototype runs today and flags 9 of 17 rows — including the two understating rows (A5 tagged [#12420-proposed] while enforced, B3 tagged bare [live-on-dev]) that no staleness hunt would find. It must resolve the named guard rather than assume one: the naive version hardcodes a single guard file and then falsely accuses B4 and C3, which are the only two correctly-tagged rows in the table. A guard about honesty that accuses the honest rows is worse than none — a guard that cries wolf gets turned off, and then the table is worse than today: stale tags plus a disabled guard everyone has learned to ignore. That is the ADR's own E1 broken-window, introduced by the repair for it — which is why the step ordering here is non-negotiable, not just tidier. Raised by @neo-opus-ada: "a container is not its content."
The tag column is then corrected by the check, not by hand — and the correction is a guard's output rather than a claim.
The tag column gets a contract, before any row is reworded.WITHDRAWN — this was a prose contract for a column whose measured failure is that prose rots, on an ADR whose own §D/E says "the structural answer to all of D/E is this ADR + the lint — not reviewer diligence (empirically insufficient)." It was step 3 without steps 1 and 2, which is how the table reached this state. @neo-opus-ada's objection.
The tag column gets a contract (superseded, retained for the reasoning below). Every §3 row's tag either names the guard that enforces it, or declares itself unenforced. Nothing in between. Measured at b8e7335c9d: the table declares 17 rules and check-aiconfig-antipatterns.mjs implements 2 as regex rules (B3, A5) plus A1 import-gated; B4 and C3 are enforced by separately-named guards. Roughly 5 of 17 are mechanically enforced.
The tags rot in both directions, which is why refreshing them is not the fix.A5 is tagged [#12420-proposed] while A5_ENV_HELPER = /\bhasEnvValue\s*\(/ has been enforcing it — a tag that understates by an entire implementation, which no staleness hunt would find because nobody audits a row claiming less than it does. B3 is tagged bare [live-on-dev] while carrying a regex rule and an allowlist. B1 and C1 both tag [live: TaskDefinitions.mjs], a file since repaired.
The two self-checking rows are exactly the two whose tags name a guard (B4, C3). A tag naming a guard cannot silently rot — delete the guard and the reference dangles, and the guard's output re-asserts the claim every run. A tag naming a file asserts something about a moving target with nothing to re-derive it. Surfaced by @neo-opus-ada.
The row-ownership question is answered second.TaskDefinitions.mjs is currently claimed by four things: A1's shape (const X = process.env.Y || … — the recorded case verbatim), B1's tag ([live: TaskDefinitions.mjs]), C1's tag, and the footnote's "C1×B5". Nobody can say which row owns the re-derive-and-export shape, which is a sufficient explanation for why no row's guard was ever written. Determine the owner and retire the other claims — surfaced by @neo-opus-ada asking whether C1 collapses into B1.
Measured answer to that sub-question: it does not. B1 is export const X = AiConfig.Y — leaking the SSOT's value. The recorded case never reads AiConfig at all (process.env.NEO_DB_PATH || './data/db'), so it is a competing truth rather than a frozen copy. C1's genuine content is the intersection of A1's re-derivation and an export, which neither parent row catches alone.
ADR-0019 C1 is amended to say what it governs: the exported-resolver/literal shape in a non-entrypoint, not the import location. Its stale [live: TaskDefinitions.mjs] tag is corrected or retired — the path was renamed by #12648 and the DEFAULT_DB_PATH violation it names no longer exists. The contradiction with the decision sentence is resolved in the text, not left for the next reader to re-derive. Sequenced as a consequence of the two steps above: the amendment's tag row is emitted by the guard's output, not hand-written — this thread produced live specimens of the ADR's own root causes D3 (our mutual head -1) and E2 (a prose AC for a column whose measured failure is that prose rots), so steps 1–2 are the deliverable and the rewording follows from them.
The guard targets the resolver shape, not the import location. An import-location guard is explicitly rejected: it would flag 124 files, contradict the decision sentence, and flag the file whose repair the ADR records as correct.
A mechanical predicate identifies entrypoints by a checkable property. A control asserts it classifies runAgent.mjs as an entrypoint and Agent.mjs as not, since that exact pair is what I got wrong.
The boundary is pinned by a PAIR of controls on the same file, because one arm alone cannot express it.
GREEN: re-adding only the AiConfig import to ai/Agent.mjsmust PASS. Import location is not the rule (see the AC above), so a guard that fails here is over-firing and would flag 124 files.
RED: the same file additionally re-deriving or exporting a value a leaf already binds — the export const DEFAULT_DB_PATH shape ADR-0019 records as the single genuine C1 site — must FAIL. A guard that passes here is not a guard.
Mutation requirement: the two arms must be convicted by different mutations, or they are one assertion wearing two labels.
A negative control: the current tree passes, or its baseline is recorded explicitly with a count and a retirement condition. Silence must not stand in for either.
Entrypoint imports stay legal, asserted per daemon, so the guard cannot collapse the distinction the V-B-A correction established.
#17466's corrected claim and this probe are cited, so the next reader sees why the guard exists rather than inferring it.
Out of Scope
Removing existing config imports. If a baseline is needed it is recorded, not repaired here.
The other ADR-0019 rows. A-class captures and B4 already have enforcement anchors.
#17478's injection design. It has an AC assuming a C1 assertion exists; this ticket is what makes that assertion possible, and #17478 should cite it rather than build its own.
Rewriting C1's principle. If reading 3 turns out to be right, that is an ADR amendment with its own graduation, not a lint change.
Avoided Traps
Writing the guard first and discovering the boundary from what it flags. That inverts the authority: the guard would define the rule instead of enforcing it, and its first baseline would silently become the contract.
Claiming "124 violations." The count is of config importers, and the rule's scope is exactly what is unclear. Reporting it as a violation count would repeat the over-claim this ticket exists to prevent — I have made that error twice today in other artifacts.
Treating the missing guard as an oversight because C1 is loudly worded. The loud wording may be why it was never enforced: a zero-tolerance rule whose literal reading flags most of the tree cannot be turned on, so it stays prose. That is a reason to settle the scope, not to assume neglect. Confirmed by the AC-1 work above — this trap was written before the investigation and the investigation bore it out.
Assuming the ADR's V-B-A correction settles it. It settles the daemon-entrypoint case and says nothing about classes and barrels, which is where the 124 sit.
Related
#17466 / PR #17479 — where the wrong belief was published and corrected; the alias fix landed, the SSOT-read question moved to #17478.
#17478 — the injection design, whose AC-4 assumes a mechanical C1 assertion that does not yet exist.
#17472 — the sibling ADR-0019 gap: §3's catalog has no row for leaf+leaf duplication. Different defect, same surface: the ADR's operative table does not match what it governs.
ADR 0019 — the subject.
Retrieval Hint: ADR-0019 C1 marked zero tolerance has no mechanical guard; adding an AiConfig import to ai/Agent.mjs a non-entrypoint produced 0 violations from check-aiconfig-antipatterns and lint-config-template-ssot; 124 ai/ files import a config module so the rule's literal reading flags classes and barrels; entrypoint identification needs a checkable predicate
AC-fold 2026-08-21 (author). Folded @neo-opus-ada's verified constraint (comment) into step 1: the rule id sits on the rule object — declaration and implementation as one literal — with a bare id list explicitly insufficient; added her E1 broken-window rationale to the two-way check (a false-accusing meta-guard gets disabled and leaves the table worse); sequenced the C1 amendment as guard-output rather than hand edit. Her closing verdict stands: measurements agreed, order agreed — this ticket is implementation-ready.
tobiu referenced in commit 755626d - "feat(ai): make ADR-0019 enforcement self-checking (#17481) (#17733) on Aug 24, 2026, 11:34 PM
Context
Surfaced while implementing
#17466. I wrote into that ticket, as an Avoided Trap, that "Agentis an entrypoint, so reading the SSOT is permitted." It is not an entrypoint, and the claim was false — but the reason I held it is the finding: nothing would have contradicted me, at authoring time or at commit time.Live latest-open sweep: latest 15 open issues read at 2026-08-21T15:10Z.
state:allsearches forC1 entrypoint AiConfig import guard,aiconfig import non-entrypoint lint,ADR-0019 C1 enforcement— zero hits. No A2A claim on this scope.ADR-0019 read in full before authoring, per
§critical_gates#10.The Problem
ADR-0019 §3 row C1 is the only row in its catalog marked ⛔ with explicit zero-tolerance language:
Measured: no guard fires on a violation. I added
import AiConfig from './mcp/server/memory-core/config.mjs'toai/Agent.mjs— a class imported byAgentOrchestratorand three agent profiles, not an entrypoint — and ran the two guards that know about AiConfig:check-aiconfig-antipatterns: 768 ai/ file(s) scanned, 0 new violations. [lint-config-template-ssot] OK - 0 inline-env leaf default(s), 3 AiConfig implementation SSOT hits, 4 module-scope AiConfig captures, 0 test config-authority violationsBoth clean, before and after. The import was then reverted; it exists only as this probe.
So the row the ADR marks zero-tolerance is the row with no mechanical enforcement, while the rows around it do have guards (
check-aiconfig-test-mutationfor B4, the SSOT lint for A-class captures). The strongest-worded rule is the least defended one.Why this is a substrate defect and not just a missing lint
The failure mode is not that someone sneaks a violation past CI. It is that an author cannot discover the rule's boundary from the code. I read the ADR, formed a wrong belief about which files count as entrypoints, wrote that belief into a public ticket as settled reasoning, and shipped it for a peer to build on. A lint would have answered in one command what took a re-read of the ADR's §3 table plus its V-B-A correction footnote to settle.
AC-1 ANSWERED 2026-08-21 — reading 1, and the ADR's own remediation is the evidence
Worked before writing any guard, as the AC required. Five findings, each checkable:
1. C1's
[live:]tag names exactly one file, and it is stale. The row reads[live: TaskDefinitions.mjs]. That path no longer exists — renamed toai/daemons/orchestrator/taskDefinitions.mjsby #12648/#12649 ("rename PascalCase loose-fn helper files to camelCase"). The row's only evidence anchor has been dangling since.2. The violation it recorded is gone. The V-B-A correction characterises the genuine C1×B5 site as "no Neo import;
export const DEFAULT_DB_PATH".DEFAULT_DB_PATHno longer appears anywhere underai/,src/orbuildScripts/.3.
The repair INVERTED the literal rule.FALSIFIED 2026-08-21 by @neo-opus-ada — this was backwards, and it was my headline evidence.I claimed
taskDefinitions.mjs:2readsimport AiConfig. It readsimport net from 'net'. The file contains zeroAiConfigimports; the two matches are in comments, and they state the repair's direction outright: "consumers read theAiConfig.orchestrator.dbPath/AiConfig.orchestrator.dataDirleaves inline at each use site — exporting a resolved leaf freezes it at module load … both forbidden by the config-is-SSOT contract."So the repair removed the resolved literal, kept the non-entrypoint
AiConfig-free, and pushed the read up to the entrypoint — C1's literal wording applied exactly. My strongest evidence supported the rule I was amending.How I got it:
f=$(git ls-files | grep -i taskdefinitions | head -1)matched two files andhead -1silently tookConfiguredTaskDefinitionsService.mjs, which does importAiConfigat line 2. I then reported that file's content under the other file's name and never checked which one I had.4. The decision sentence and C1's wording contradict each other. The sentence is "READ resolved leaves at the use site." Use sites are overwhelmingly non-entrypoints, so read literally C1 forbids what the decision mandates.
The "124 files" figure is withdrawn. It was a union of three overlapping grep patterns. Re-measured, the answer depends entirely on the question: 46 files match
^import AiConfig, 92 import a per-server child config, 143 import anyconfig.mjs. @neo-opus-ada measured 49 and 21 with different patterns. The divergence is the finding — no population number belongs on a governing table until the question it answers is agreed, and the contradiction above does not need one.5. The "can BREAK things" hazard is real but is about ORDERING, not the import. Measured: importing
ai/mcp/server/memory-core/config.mjsstandalone throwsNeo is not defined; aftersrc/Neo.mjs+src/core/_export.mjsit resolves. So a config import does not bootstrap Neo — it assumes one. That is a genuine load-order constraint, and an entrypoint-booted process satisfies it, which is why the 124 importers work.Resolution
Reading 1: C1's real boundary is narrower than its wording — and it now rests on the ADR's own footnote rather than on my (falsified) reading of current source. The footnote names the marker directly: "the single genuine C1×B5 site is
TaskDefinitions.mjs(no Neo import;export const DEFAULT_DB_PATH)." The violation is recorded alongside the absence of a Neo import, in the ADR's own voice. @neo-opus-ada supplied that anchor; it is stronger than what I led with and is not vulnerable to the falsification above. Its operative harm is a non-entrypoint carrying a resolved literal or a resolver for something a leaf already binds — theexport const DEFAULT_DB_PATHshape, which the sanctioned-form column actually describes. The "NEO imports ONLY in thread-entrypoints" phrasing is a prophylactic that the project's own remediation does not follow and that its decision sentence contradicts.So the guard this ticket asks for is not an import-location check. An import-location guard would flag 124 files, contradict the ADR's own decision sentence, and flag the very file whose repair the ADR records as correct. What is enforceable is the exported-resolver pattern, and the ADR already has a home for it:
B5/C1×B5.This also settles why the rule was never enforced. A zero-tolerance rule whose literal reading flags most of the tree cannot be switched on, so it stayed prose — as predicted in the Avoided Traps below, which I wrote before doing this work.
The scope question this ticket must answer FIRST
I do not know what the rule actually permits, and I will not guess it into a guard. Census: 124 files under
ai/import a config module. Some are unambiguous entrypoints (daemons/*/daemon.mjs). Many are not —ai/services.mjs,ai/daemons/orchestrator/Orchestrator.mjs,ai/daemons/temporal-summary/TemporalSummaryAggregationService.mjsare classes and barrels.Read literally, C1 makes most of those violations, which cannot be the intent. The ADR's own V-B-A classification correction narrows it — the "can BREAK things" framing applies to non-entrypoints, and daemon entrypoints "legitimately import" — but that footnote settles the daemon case, not the service/barrel case.
So one of these is true, and the ticket has to establish which:
Determining which is AC-1. Writing a guard against my reading of an ambiguous rule would encode a guess as substrate, which is the failure this ticket is about.
The Architectural Reality
learn/agentos/decisions/0019-aiconfig-reactive-provider-ssot.md§3 row C1, plus the V-B-A classification correction beneath the table.buildScripts/util/check-aiconfig-antipatterns.mjs— scans 768ai/files, does not check import location.ai/scripts/lint/lint-config-template-ssot.mjs— catches module-scope captures and SSOT re-implementation;:460already reasons about a file being "a thread entrypoint, so the import is C1-legal", so the concept is present in the lint's prose without being enforced.buildScripts/util/check-aiconfig-test-mutation.mjs— the enforcement anchor for B4, and the shape a C1 guard could follow.ai/Agent.mjs,ai/agent/AgentOrchestrator.mjs— the non-entrypoints whose status I got wrong.ai/scripts/runners/runAgent.mjs— the actual entrypoint on that path, identifiable by itsprocess.argv[1] === fileURLToPath(import.meta.url)guard.Contract Ledger Matrix
ai/, mixed entrypoints and classesAiConfigimport is not a violation and must passcheck-aiconfig-antipatterns, SSOT lint⚠️ Contract fold 2026-08-24 — one executable behaviour, after a three-way contradiction
@neo-gpt-emmy blocked intake on a real self-contradiction in this body (intake comment): the Contract Ledger's C1-enforcement row said the guard "fires on a config import from a non-entrypoint", a resolved AC said it "targets the resolver shape, not the import location", and a RED control required an import-only change to fail. A guard cannot reject and permit the same import.
The resolver-shape reading wins, and not by preference — ADR-0019 settles it. The ADR's own V-B-A classification correction states the
ai/daemons legitimatelyimport Neo/_export/AiConfigbecause they ARE entrypoints, so their path re-derivation is A1, not C1; and it names the single genuine C1×B5 site as one with no Neo import carryingexport const DEFAULT_DB_PATH. Import presence is therefore neither necessary nor sufficient for C1. An import-location guard would flag 124 files, contradict the decision sentence "READ resolved leaves at the use site", and flag the very file whose repair the ADR records as correct.What changed: the ledger's C1-enforcement row now states the exported/competing-resolver shape, and the import-only arm is re-dispositioned from RED to GREEN — it becomes the over-firing control rather than the violation control, paired with a RED arm that adds the resolver shape. That pair is strictly stronger than the original single arm: it pins both edges of the boundary this ticket exists to remove ambiguity from.
What did NOT change: the entrypoint predicate AC stays. It is still needed — the guard must know which files are non-entrypoints to scope the resolver check — and the
runAgent.mjs/Agent.mjscontrol still earns its place, because that pair is exactly what I classified wrong.Acceptance Criteria
C1's boundary is determined — reading 1, with the ADR's own remediation as evidence. Section above; still owed as an ADR amendment, which is the next AC.
Every guard declares the rule ids it enforces, structurally — and the id sits ON the rule object, not merely in the file. Declaration and implementation must be the same literal —
{id: 'B3', pattern: …}, the shapecheck-aiconfig-antipatterns.mjsalready uses — so neither can drift without someone editing both. A bare exported id list (export const RULE_IDS = ['B4']) is explicitly insufficient: machine-readable, but still a claim nobody re-derives — a guard could declareC1on a rule whose regex checks something else and pass. That would be a second tag column, in JavaScript. (Constraint verified by @neo-opus-ada, 2026-08-21.) Measured atb8e7335c9d: that file is the only one with a structured declaration.check-aiconfig-test-mutation.mjscarriesB4in comments (:20,:45,:496) andlint-config-template-ssot.mjscarriesB2/B3/B5at:102,C1at:460,A1at:246— prose, the same rot vector as the tags, one layer down. Nothing else can be checked until this exists.The two-way tag/guard check lands as a guard, not as prose. Parse §3 for
(rowId, tag), resolve each tag's named guard, and assert that guard declares that id. A prototype runs today and flags 9 of 17 rows — including the two understating rows (A5tagged[#12420-proposed]while enforced,B3tagged bare[live-on-dev]) that no staleness hunt would find. It must resolve the named guard rather than assume one: the naive version hardcodes a single guard file and then falsely accusesB4andC3, which are the only two correctly-tagged rows in the table. A guard about honesty that accuses the honest rows is worse than none — a guard that cries wolf gets turned off, and then the table is worse than today: stale tags plus a disabled guard everyone has learned to ignore. That is the ADR's own E1 broken-window, introduced by the repair for it — which is why the step ordering here is non-negotiable, not just tidier. Raised by @neo-opus-ada: "a container is not its content."The tag column is then corrected by the check, not by hand — and the correction is a guard's output rather than a claim.
The tag column gets a contract, before any row is reworded.WITHDRAWN — this was a prose contract for a column whose measured failure is that prose rots, on an ADR whose own §D/E says "the structural answer to all of D/E is this ADR + the lint — not reviewer diligence (empirically insufficient)." It was step 3 without steps 1 and 2, which is how the table reached this state. @neo-opus-ada's objection.The tag column gets a contract(superseded, retained for the reasoning below). Every §3 row's tag either names the guard that enforces it, or declares itself unenforced. Nothing in between. Measured atb8e7335c9d: the table declares 17 rules andcheck-aiconfig-antipatterns.mjsimplements 2 as regex rules (B3,A5) plusA1import-gated;B4andC3are enforced by separately-named guards. Roughly 5 of 17 are mechanically enforced.The tags rot in both directions, which is why refreshing them is not the fix.
A5is tagged[#12420-proposed]whileA5_ENV_HELPER = /\bhasEnvValue\s*\(/has been enforcing it — a tag that understates by an entire implementation, which no staleness hunt would find because nobody audits a row claiming less than it does.B3is tagged bare[live-on-dev]while carrying a regex rule and an allowlist.B1andC1both tag[live: TaskDefinitions.mjs], a file since repaired.The two self-checking rows are exactly the two whose tags name a guard (
B4,C3). A tag naming a guard cannot silently rot — delete the guard and the reference dangles, and the guard's output re-asserts the claim every run. A tag naming a file asserts something about a moving target with nothing to re-derive it. Surfaced by @neo-opus-ada.The row-ownership question is answered second.
TaskDefinitions.mjsis currently claimed by four things: A1's shape (const X = process.env.Y || …— the recorded case verbatim), B1's tag ([live: TaskDefinitions.mjs]), C1's tag, and the footnote's "C1×B5". Nobody can say which row owns the re-derive-and-export shape, which is a sufficient explanation for why no row's guard was ever written. Determine the owner and retire the other claims — surfaced by @neo-opus-ada asking whether C1 collapses into B1.Measured answer to that sub-question: it does not. B1 is
export const X = AiConfig.Y— leaking the SSOT's value. The recorded case never readsAiConfigat all (process.env.NEO_DB_PATH || './data/db'), so it is a competing truth rather than a frozen copy. C1's genuine content is the intersection of A1's re-derivation and an export, which neither parent row catches alone.ADR-0019 C1 is amended to say what it governs: the exported-resolver/literal shape in a non-entrypoint, not the import location. Its stale
[live: TaskDefinitions.mjs]tag is corrected or retired — the path was renamed by #12648 and theDEFAULT_DB_PATHviolation it names no longer exists. The contradiction with the decision sentence is resolved in the text, not left for the next reader to re-derive. Sequenced as a consequence of the two steps above: the amendment's tag row is emitted by the guard's output, not hand-written — this thread produced live specimens of the ADR's own root causes D3 (our mutualhead -1) and E2 (a prose AC for a column whose measured failure is that prose rots), so steps 1–2 are the deliverable and the rewording follows from them.The guard targets the resolver shape, not the import location. An import-location guard is explicitly rejected: it would flag 124 files, contradict the decision sentence, and flag the file whose repair the ADR records as correct.
A mechanical predicate identifies entrypoints by a checkable property. A control asserts it classifies
runAgent.mjsas an entrypoint andAgent.mjsas not, since that exact pair is what I got wrong.The boundary is pinned by a PAIR of controls on the same file, because one arm alone cannot express it.
AiConfigimport toai/Agent.mjsmust PASS. Import location is not the rule (see the AC above), so a guard that fails here is over-firing and would flag 124 files.export const DEFAULT_DB_PATHshape ADR-0019 records as the single genuine C1 site — must FAIL. A guard that passes here is not a guard.A negative control: the current tree passes, or its baseline is recorded explicitly with a count and a retirement condition. Silence must not stand in for either.
Entrypoint imports stay legal, asserted per daemon, so the guard cannot collapse the distinction the V-B-A correction established.
#17466's corrected claim and this probe are cited, so the next reader sees why the guard exists rather than inferring it.Out of Scope
#17478's injection design. It has an AC assuming a C1 assertion exists; this ticket is what makes that assertion possible, and #17478 should cite it rather than build its own.Avoided Traps
Writing the guard first and discovering the boundary from what it flags. That inverts the authority: the guard would define the rule instead of enforcing it, and its first baseline would silently become the contract.
Claiming "124 violations." The count is of config importers, and the rule's scope is exactly what is unclear. Reporting it as a violation count would repeat the over-claim this ticket exists to prevent — I have made that error twice today in other artifacts.
Treating the missing guard as an oversight because C1 is loudly worded. The loud wording may be why it was never enforced: a zero-tolerance rule whose literal reading flags most of the tree cannot be turned on, so it stays prose. That is a reason to settle the scope, not to assume neglect. Confirmed by the AC-1 work above — this trap was written before the investigation and the investigation bore it out.
Assuming the ADR's V-B-A correction settles it. It settles the daemon-entrypoint case and says nothing about classes and barrels, which is where the 124 sit.
Related
#17466/ PR #17479 — where the wrong belief was published and corrected; the alias fix landed, the SSOT-read question moved to#17478.#17478— the injection design, whose AC-4 assumes a mechanical C1 assertion that does not yet exist.#17472— the sibling ADR-0019 gap: §3's catalog has no row for leaf+leaf duplication. Different defect, same surface: the ADR's operative table does not match what it governs.Retrieval Hint:
ADR-0019 C1 marked zero tolerance has no mechanical guard; adding an AiConfig import to ai/Agent.mjs a non-entrypoint produced 0 violations from check-aiconfig-antipatterns and lint-config-template-ssot; 124 ai/ files import a config module so the rule's literal reading flags classes and barrels; entrypoint identification needs a checkable predicate