Repository navigation
v2.2.0
Minor Changes
-
4caf8fd: ESLint now owns formatting in the emitted config, and
maxLinesfinally has a unit. Five new gate ids, all riding a caller-injected plugin so the library keeps zero runtime dependencies. All land aterrorin every preset.The load-bearing one is
statementsPerLine→@stylistic/max-statements-per-lineat a hard-wired{ max: 1 }.maxLinescounts code lines with blanks and comments skipped, so a line budget with no cap on line content is satisfiable by collapsing statements onto one line instead of splitting the file — the evasion an agent under budget pressure reaches for first.codeStylecloses the rest of that route:@stylistic's owncustomize()set (read from the factory, not hand-listed — a hand-picked subset is what left zero-indentation legal) plusmax-lenat 90,linebreak-style: unix, and corecurly, which matters because without itif (x) return;counts as one statement and slips past the line gate. Knobs:indent,quotes,semi,maxLen.Also
explicitAny→@typescript-eslint/no-explicit-any(the mechanical half of the narrow-interfaces principle —anyis the hole that lets illegal states be expressed),statementPadding→@stylistic/padding-line-between-statementswith a fixed 17-entry list, andimportBlock→import-lite/first+import-lite/no-duplicates, which catches the two import mistakes an incrementally-editing agent makes routinely and which no formatter can fix.emitLintgrowsoptions.stylisticandoptions.importsbesideoptions.typescript; the generated config passes all three,initinstalls@stylistic/eslint-pluginandeslint-plugin-import-lite, andimpactloads both from the project so its per-rule counts cover the new gates instead of reporting a silent zero. A gate whose carrier plugin is absent emits nothing while lint still passes — indistinguishable from a clean merge — so the merge snippets, the gate catalog, the playbook and the generated config each state that the options object travels whole.codeStylethrows rather than emitting an empty bundle when handed a plugin withoutconfigs.customize().Three behaviors worth knowing before adopting. The shape family is the one gate family test files are not exempt from — a metric threshold screams on a test, but a duplicate import or a collapsed line is no easier to read there.
codeStyleis nearly all auto-fixable (about 5 of ~68 rules are not), soeslint --fixclears most of a first run: the playbook asks for that pass as its own commit, since it rewrites whitespace repo-wide, and notes it cannot push a file overmaxLines. What--fixcannot clear is named too —max-lenhas no fixer and deliberately does not exempt plain strings, because a cap a line escapes by containing a string is not a cap; and alinebreak-stylered usually means git'sautocrlfor a missing.gitattributes, not the file, so fixing the source just gets undone by the next checkout. Two rules stay rejected on their own merits, and the carrier that ships does not carry them anyway:import/no-cycle(a per-file re-check of a graphinspectalready walks, measured at 92s on 850 files) andimport/no-unused-modules(cannot run under flat config). Both need a resolver, which is exactly what the shipped plugin leaves out. On a JavaScript projectexplicitAnyleaves inspect's coverage denominator entirely —anyis a TypeScript construct, and a gate nobody can open is not a gate, the ruledeepWatchalready follows on React.
Patch Changes
-
cbd8ae8: Three field-run corrections to what the tool says about itself. No behavior changes — but each of these sent an adopting agent somewhere it did not need to go.
The playbook no longer invites
impactinto the drafting loop. "Let the tools correct you:inspectandimpactare read-only and cheap" sat above the Method as a licence to run both while drafting, and both brownfield agents of the run took it —impactlints with the emitted config, so it needs the pluginsinitinstalls and greeted them with a load error instead. One agent filed it as a self-contradiction, the other blamed itself; the same wall twice is the playbook's, not theirs. The paragraph now namesinspectalone as the config-only loop tool and states whereimpactjoins (Method step 9, after init) and why it cannot come earlier. The load error's own pointer toblueprint init— the thing that got both agents unstuck in one hop — is now asserted, not incidental.A deletion in
init's output no longer wears the writes'✓.− rm:marks it instead. The note always carried the cause (pristine preset scaffold — removed; the playbook authors the real one), but a destructive line stamped with the success mark and buried among writes skims past as one more thing created: a field agent filtered init's output forwriteand missed its own config being reclaimed, then spent commands hunting the disappearance. The mark says which direction the effect went; the note still says why.And the install line stops stuttering its own kind —
✓ install: install eslint …is now✓ install: eslint …. -
c420e2a: A finding now names the rule that carries it, and the
--agentflag says which path launches anything. Two field observations, both cases of a correct state reading as a broken one.One violation went by three names.
inspectreported[deep-import],impactand the lint run called itno-restricted-imports, and an adopter verifying the merge searched the resolved config forblueprint/deep-importand found nothing — because that ban, likeflow-violationandpackage-ownership, folds into the singleno-restricted-importsentry rather than standing alone. Inspect's migration steps now carry both halves: each names its finding and the ESLint rule that enforces it, and the findings no lint run will ever show —cycle,undeclared-folder,no-entry— say so instead of leaving the reader to wonder where they went.relative-escapeis called out as the one structural ban that is a standalone rule, since a../escape cannot be written as a literal pattern.The playbook's
--print-configguidance gains this as its fourth "know this before reading the output": finding names are not rule ids. It sits beside the three added last release — prefixed keys, rules absent because their layer holds no files, and selfOnly resolving on the importer layer.Separately, the README's security bullet was imprecise about
--agentand two field runs stopped to check. It presented the flag as the one explicit opt-in that runs an agent, without saying that this is true only on the authoring path; on the preset path (init --preset --agent claude) nothing is launched and the flag only narrows which contract file is written. Both meanings are stated now, withinit --helpand--dry-runnamed as the authoritative check — which is what both agents reached for. -
21eeb2f: The generated handbook stops promising a gate that does not exist. Its Rules table printed a tier beside every declared rule under one legend — "
errorfails lint" — which is true of most rows and false of two kinds.cyclesisinspect's finding and deliberately emits no ESLint line;deadCodeis documentation-only and asks for knip. A preset declares both aterror, so a stock handbook told its reader that two rules gate their lint run when neither does.A field agent caught it by cross-checking against
blueprint rulesand trusted the catalog — at no cost to itself, but it named the real problem: the handbook is the artifact meant to outlive the adoption, read later by people who will not have the catalog open beside it. Two generated artifacts from one source disagreeing is the thing the README claims cannot happen.The table gains an Enforced by column —
lint,`blueprint inspect`, ordocumentation only— and the legend now separates the tier (what the enforcing machine does with a violation) from which machine that is. Unknown ids resolve to documentation, matching how the agent contract already treats them.The classification comes from the catalog rather than a second hand-kept list:
GateSpecgains an optionalruntimemarker,cyclescarries it, andenforcedBy()derives the three-way answer. Adding a runtime-backed gate needs no edit in the handbook, and a test asserts that correspondence rather than the current membership. -
c423c4e: The last carrier stops constraining the adopter's tree, and doctor's merge check stops over-promising. Two field findings, both about a green that meant less than it said.
importBlocknow rideseslint-plugin-import-lite(import-lite/first,import-lite/no-duplicates). The two predecessors each walled a real repo:eslint-plugin-importcaps its eslint peer at 9, andeslint-plugin-import-xpeers on@typescript-eslint/utils@^8.56for resolvers these two rules never use — an optional peer is still version-checked when the package is present, and no nested copy can satisfy a peer, so a repo pinned at 8.47 could not install it at all. Since npm resolves the required-deps list as a unit, either one failed the whole install. import-lite is those same rules ported without the resolvers: zero dependencies,eslintas its only peer, and behaviour verified identical across eleven cases including the TypeScript one that matters (import typebeside a value import from the same module stays legal). No release ever carried the old ids.The peer guard widened with it. It already asserted every carrier admits each supported ESLint major; it now also asserts no carrier peers on anything else the adopter owns, since that is the constraint that actually failed.
typescript-eslint↔typescriptis allowed by name; a new peer is a test failure to review, not a surprise in someone's adoption.Doctor's merge-survival check no longer walks past the failure its own playbook warns hardest about. It verified structural rules only — bans, selfOnly selectors, globals — while printing an unqualified
✓ emitted rules survive the merged eslint config. A field agent removed thestylisticargument from a merged config, exactly the "dropped plugin argument" the docs spend the most words on, and watchednpm run lintpass, doctor pass, and the entire ~68-rulecodeStylefamily disappear. The check now also resolves one representative rule per declared carrier-backed gate (codeStyle,statementsPerLine,statementPadding,importBlock, andexplicitAnyon TypeScript projects) and reds when a gate is on but its rule resolved to nothing, naming the missingemitLintargument. A gate turned off in the config is not expected — which makes "we keep our own formatter" a declared decision rather than a silent drop.The ✓ also states its scope now (
structural bans + each active gate's carrier rule; thresholds and package-ownership entries are not compared), and the playbook names the step two separate field runs invented on their own:npx eslint --print-config <a file inside a layer>. A passing lint proves the config loads, not that a rule reached a file — and on a repo whose layers hold no files yet it proves only that the config parses. -
052586f:
initno longer claims effects it did not produce, and its dependency list installs on ESLint 10. One field report filed these as two findings; they are one causal chain, and either half alone still ships a broken repo.importBlockmoved offeslint-plugin-import. That package's peer range stops at ESLint 9 while the current ESLint is 10, andnpm installresolves the required-deps list as a unit — so on any ESLint 10 project the whole install failed and not one plugin landed. This release settled oneslint-plugin-import-liteas the replacement (see the entry below for why the first attempt,eslint-plugin-import-x, was itself walled);importBlockhas never appeared in a release, so no ledger carries any of the earlier ids.A new guard keeps the class closed rather than the instance: every package in the required-deps list is a devDependency here, and a test reads its actual peer range and fails if it excludes any supported ESLint major. The capped dependency that ships next fails in this repo instead of in someone's adoption.
The narration is the other half.
initused to print its entire plan with✓and apply it afterwards, so a step that threw left an output vouching for every effect below it. The install sits mid-plan with the alias writes beneath it: an adopter on ESLint 10 read✓ write: vite.config.ts (import alias added — existing content preserved)over a file nothing had touched, and the agent contract it shipped promised a~appalias that resolved nowhere.doctorcaught it — but only after the lie, and only for someone who thought to ask. Effects are now announced as they land, the step that fails wears✗, and the error names the planned effects that did not happen plus two ways to finish: re-runinit(idempotent — applied effects stay), orinit --no-installto complete the file plan and install the printed deps by hand. -
a7ff535: The merge instruction stops walking into the trap it opens with. A field run on a Vue repo with a
selfOnlylayer declaring two importers found that "combine both option sets into ONE entry" is written for a single collision and misleads on several.emitLintscopes its entries per layer, so one rule key can own more than one. AselfOnlylayer with two importers emitsno-restricted-syntaxon both importer layers, and a house rule may overlap only one of them. Read literally, the instruction leaves two exits and both are wrong: widen the combined entry to cover the other layers and your rule now governs files it never did (a visible new red — the run's repo had hard-coded date literals waiting in the second layer), or narrow it to exclude them and flat-config replacement deletes their emitted ban while lint stays green. That second one is the exact failure the same paragraph warns about two sentences earlier."ONE entry" now says it means one per collision, not one per rule key: combine with the entry you actually collide with and leave the others exactly as emitted.
npx blueprint rules --jsonis named as the way to see the emit points before merging rather than after — two importers show up as two. And since a warning that only says "be careful" is not much of one, the paragraph also names the gate behind it: doctor's survival check probes every layer separately and names the layer that lost its selectors, so the silent branch is not silent at the acceptance gate. -
12cc5b8: The
--print-configstep earns its place, instead of duplicating a gate and surprising the reader. A clean field batch — no blockers in six reports — whose only friction was the instruction added one release earlier.That step arrived in the same change that widened doctor to verify carrier survival, so the playbook was asking for a hand sweep the gate already ran. Two runs noticed the overlap and said so. It now points at the remainder doctor's ✓ explicitly does not compare — thresholds, package-ownership entries, and the survival of the rules you brought to the merge — and says outright that printing configs by hand is not needed on the early-exit path.
It also names the three ways a correct config reads as broken through that command, each of which cost a field agent a detour: resolved keys carry their plugin prefix (
@stylistic/max-len, never baremax-len); a rule scoped to a layer holding no files does not appear at all, which is inspect'sdeclaratory-self-onlynote rather than a loss; andselfOnly's re-export ban resolves on the importer layer inspect names, not on the layer being protected.The gate catalog stops disagreeing with that output.
codeStylelisted its extras bare —max-len,linebreak-style— while the resolved ids carry the@stylistic/prefix and onlycurlyis core. An agent cross-checking the catalog against--print-configread the mismatch as two missing rules. Every other catalog row already printed resolved ids; this one now does too.Finally, the playbook says why the carriers install when no gate from their families is declared — three separate runs paused on it. They sit inert until a gate names them, and installing now makes enabling one a single-line config edit instead of a config edit plus an install plus a second pass over the merge, months later, by someone who was not there.