v1.11.0 ·
Words that mean what they say, and a gate that actually gates
The problem
Two fixes shipped here, and they share a shape: something that looked like it was working, was not, and nothing complained.
The first was a check. packages/website's lint script was next lint, a command Next 16
removed. Removed commands do not fail politely — Next parses the argument as a project
directory, so the whole repo's pnpm lint died with Invalid project directory provided, no such directory: .../packages/website/lint. Because the root script chains with &&, the
consequence was not one broken package. scripts/check-docs-drift.mjs sat behind the failing
command and had not run since Next 16 landed. It was unreachable, not merely unrun. And 5,891
lines of TypeScript across 50 files were never type-checked at all — Next 16 also stopped
running lint during next build, so nothing anywhere was looking at them.
The reason it survived is the interesting part. It was enforced in no place. release.sh
gated on build and test only; the repository's single CI workflow does the same, and there is
no pull-request workflow. Nothing ever blocked, so nothing was ever scheduled — and the defect
had been written down four separate times across earlier instances' records, once with the
words "Worth its own instance." A gate that fails open produces no pressure to fix itself.
Repairing it turned out to cost one line; the previously unchecked code produced zero errors.
The second was vocabulary. AIDLC talks to you in words it never defines: you are told an
instance is claimed, that a gate blocked on an artifact, that scope is standard so
the entry criteria are met. Measured across the 17 shipped skills, instance appears 161
times, template 46, artifact 44, scope 41, claim 39. There was no glossary anywhere —
not in the project, not on the website.
Difficulty was never the problem. The audience is a working developer, and a developer learns
artifact and phase without help. The expensive words are the three that already mean
something else in software, because the reader does not experience confusion — they
experience false confidence, and find out much later:
- instance — everywhere else an instantiated object or a running server. Here, one unit of work. The highest-traffic term in the framework and the worst mismatch.
- scope — universally "how much is included". Here it selects which phases apply, so
scope: standardreads as "a normal-sized change" and is not that at all. - claim — sounds like an assertion or a support ticket. It is a lock.
Seven internal type names were also leaking into prose written for humans — Guidance_Layers,
Instance_Claim, State_Store and others — fourteen occurrences in all. One of them sat in a
skill's own description, which is the text you see when you list what is available.
How it could be solved
For the lint gate, Next 16's own upgrade guide says to use Biome or ESLint directly, and a
codemod exists to migrate. That was the obvious path and it was the wrong one here. This
repository has no ESLint at all — no config, no dependency — while packages/cli already
lints with exactly tsc --noEmit. Adopting ESLint would have added a second convention, a new
dependency, and an unknown number of new findings across previously unchecked code. The
instance existed because a gate had gone dark; answering that by introducing a different
gate is how a repair becomes a programme. tsc --noEmit matched the convention already there
and delivered the stated goal, which was type-checking. The honest cost: a type-checker is not
a linter, so React-hooks and accessibility rules that eslint-config-next would catch remain
unchecked. Recorded rather than glossed.
Placing the new gate in release.sh needed a measurement, not a preference. Both halves of
pnpm lint read build output — the website's type-check resolves the content package through
its generated declarations, and the docs-drift check compares committed docs against the built
CLI. Parking the content package's build output and re-running proved it: the type-check fails
with Cannot find module plus seven cascading errors. So lint runs after build, and before
the test suite, because it is the cheaper gate and there is no reason to spend the expensive
one to discover a type error.
For the vocabulary, the real fork was where definitions live. Authoring them inline across 17
skill files needs no machinery at all — pure writing discipline — but it creates up to
seventeen copies of each definition, free to drift. That is the same pattern as the lessons
hand-copied into the project's own instructions file, a duplicate already marked for deletion.
One canonical file consumed by every surface won, on the strength of a seam the framework
already had: skill bodies are compiled through a resolver chain that expands
{{context:...}} and {{guidance:...}} placeholders before packaging. Adding a third pass for
{{glossary:...}} cost about twenty lines at a proven call site.
Two smaller decisions went against the obvious answer. Tracking which terms you have already
been shown would be genuinely nicer on your tenth session — and it means new state that can
go stale, to avoid repeating a one-line definition. Declined; a per-skill ceiling gets the same
benefit with nothing to maintain. And renaming instance was costed and deliberately
deferred. It is simultaneously the highest-traffic term and the worst mismatch, which is an
uncomfortable combination, but the name is load-bearing across state files, YAML keys, CLI
flags, and every project already on disk. Renaming it is a migration and a breaking change to a
published tool. The wager is that explaining it well may make the rename unnecessary — and if
it does not, the explanations will say exactly which words still mislead. That question is now
its own costing spike rather than a paragraph in a finished document.
How AIDLC solves it
lint-gate-repair
1. packages/website/package.json — "lint": "next lint" → "lint": "tsc --noEmit"
tsc --noEmit was chosen over introducing ESLint. The deciding facts:
- The repo has no ESLint at all — no
eslint.config.*, no.eslintrc*, noeslintdependency in any package.packages/clilints with exactlytsc --noEmit. This change makes the website match the one convention already in the repo instead of adding a second one. - Adding ESLint would add a gate, not repair one. It means a new dependency, a flat config, and an
unknown number of new findings across previously unchecked code.
CLAUDE.md's "Before You Start" section asks for making an existing gate deterministic or absent rather than adding a new one, and this instance exists precisely because a gate went silently dark. - The stated goal is met. The idea's complaint was that the LOC "are never type-checked";
tsc --noEmittype-checks them.
Honest limitation, recorded rather than glossed: tsc --noEmit is a type-checker, not a linter. It
does not catch what eslint-config-next would — React-hooks rule violations, jsx-a11y findings,
unused imports. Introducing ESLint remains a defensible follow-up; it is deliberately not smuggled
into a micro-task. Also unchanged: packages/website/scripts/*.mjs stay unchecked, because the
tsconfig.json include covers only **/*.ts / **/*.tsx and checkJs is off.
The type-check passed on the first run — 0 errors over 50 files. The previously unchecked code was not hiding a backlog, so repairing the gate cost nothing beyond the script swap.
2. scripts/release.sh — added a pnpm lint gate between build and test
The script previously gated only on pnpm build and pnpm test. Placement is load-bearing and both
halves of the reasoning were measured, not assumed:
-
After
pnpm build. Both halves ofpnpm lintread build output. Verified by parkingpackages/content/distand re-running the website type-check:src/lib/content.ts(17,8): error TS2307: Cannot find module '@rasensio/aidlc-content' or its corresponding type declarations. ... plus 7 cascading TS7006 implicit-any errors exit 2Run before the build on a clean clone, the gate would fail on absent artifacts rather than on real defects — a false red, which is the same class of dishonest signal this instance is repairing.
check-docs-drift.mjslikewise compares the committed CLI reference against the built CLI. -
Before
pnpm test. Lint is the cheap gate; the suite is the expensive and historically flaky one. There is no reason to spend the suite to discover a type error.
The comment block in release.sh records why the gate was absent, so the next reader does not have to
rediscover it.
plain-language-vocabulary
A canonical glossary with two consumers and no copy in between.
packages/content/glossary.yaml holds ten definitions. loadGlossary() reads it. The CLI resolves
{{glossary:<term>}} in skill bodies against it as a third pass in the existing resolver chain; the
website renders /docs/glossary from the same call at build. Nothing is generated, committed, or
installed into a project, so there is no second copy to drift.
| Half | Where |
|---|---|
| Definitions | packages/content/glossary.yaml — 10 terms, 3 with collision corrections |
| Loader | packages/content/src/glossary.ts — loadGlossary(), getGlossaryTerm() |
| Resolution | packages/cli/src/compile/loaders.ts — resolveGlossaryPlaceholders(), chained third |
| Authoring | 37 placeholders across 15 shipped skill bodies |
| Site | packages/website/src/app/docs/glossary/page.tsx + search index + docs-index card |
| Enforcement | 20 new tests, plus a check-docs-drift.mjs single-source assertion |