Skip to content

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: standard reads 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*, no eslint dependency in any package. packages/cli lints with exactly tsc --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 --noEmit type-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 of pnpm lint read build output. Verified by parking packages/content/dist and 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 2
    

    Run 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.mjs likewise 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.

HalfWhere
Definitionspackages/content/glossary.yaml — 10 terms, 3 with collision corrections
Loaderpackages/content/src/glossary.tsloadGlossary(), getGlossaryTerm()
Resolutionpackages/cli/src/compile/loaders.tsresolveGlossaryPlaceholders(), chained third
Authoring37 placeholders across 15 shipped skill bodies
Sitepackages/website/src/app/docs/glossary/page.tsx + search index + docs-index card
Enforcement20 new tests, plus a check-docs-drift.mjs single-source assertion