Skip to content

v2.2.0

Choose a tag to compare

@taco3064 taco3064 released this 30 Jul 09:01
· 659 commits to main since this release

Minor Changes

  • 4caf8fd: ESLint now owns formatting in the emitted config, and maxLines finally has a unit. Five new gate ids, all riding a caller-injected plugin so the library keeps zero runtime dependencies. All land at error in every preset.

    The load-bearing one is statementsPerLine → @stylistic/max-statements-per-line at a hard-wired { max: 1 }. maxLines counts 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. codeStyle closes the rest of that route: @stylistic's own customize() set (read from the factory, not hand-listed — a hand-picked subset is what left zero-indentation legal) plus max-len at 90, linebreak-style: unix, and core curly, which matters because without it if (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 — any is the hole that lets illegal states be expressed), statementPadding → @stylistic/padding-line-between-statements with a fixed 17-entry list, and importBlock → 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.

    emitLint grows options.stylistic and options.imports beside options.typescript; the generated config passes all three, init installs @stylistic/eslint-plugin and eslint-plugin-import-lite, and impact loads 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. codeStyle throws rather than emitting an empty bundle when handed a plugin without configs.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. codeStyle is nearly all auto-fixable (about 5 of ~68 rules are not), so eslint --fix clears 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 over maxLines. What --fix cannot clear is named too — max-len has no fixer and deliberately does not exempt plain strings, because a cap a line escapes by containing a string is not a cap; and a linebreak-style red usually means git's autocrlf or 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 graph inspect already walks, measured at 92s on 850 files) and import/no-unused-modules (cannot run under flat config). Both need a resolver, which is exactly what the shipped plugin leaves out. On a JavaScript project explicitAny leaves inspect's coverage denominator entirely — any is a TypeScript construct, and a gate nobody can open is not a gate, the rule deepWatch already 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 impact into the drafting loop. "Let the tools correct you: inspect and impact are 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 — impact lints with the emitted config, so it needs the plugins init installs 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 names inspect alone as the config-only loop tool and states where impact joins (Method step 9, after init) and why it cannot come earlier. The load error's own pointer to blueprint 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 for write and 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 --agent flag 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. inspect reported [deep-import], impact and the lint run called it no-restricted-imports, and an adopter verifying the merge searched the resolved config for blueprint/deep-import and found nothing — because that ban, like flow-violation and package-ownership, folds into the single no-restricted-imports entry 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-escape is 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-config guidance 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 --agent and 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, with init --help and --dry-run named 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 — "error fails lint" — which is true of most rows and false of two kinds. cycles is inspect's finding and deliberately emits no ESLint line; deadCode is documentation-only and asks for knip. A preset declares both at error, 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 rules and 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`, or documentation 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: GateSpec gains an optional runtime marker, cycles carries it, and enforcedBy() 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.

    importBlock now rides eslint-plugin-import-lite (import-lite/first, import-lite/no-duplicates). The two predecessors each walled a real repo: eslint-plugin-import caps its eslint peer at 9, and eslint-plugin-import-x peers on @typescript-eslint/utils@^8.56 for 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, eslint as its only peer, and behaviour verified identical across eleven cases including the TypeScript one that matters (import type beside 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 ↔ typescript is 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 the stylistic argument from a merged config, exactly the "dropped plugin argument" the docs spend the most words on, and watched npm run lint pass, doctor pass, and the entire ~68-rule codeStyle family disappear. The check now also resolves one representative rule per declared carrier-backed gate (codeStyle, statementsPerLine, statementPadding, importBlock, and explicitAny on TypeScript projects) and reds when a gate is on but its rule resolved to nothing, naming the missing emitLint argument. 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: init no 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.

    importBlock moved off eslint-plugin-import. That package's peer range stops at ESLint 9 while the current ESLint is 10, and npm install resolves 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 on eslint-plugin-import-lite as the replacement (see the entry below for why the first attempt, eslint-plugin-import-x, was itself walled); importBlock has 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. init used 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 ~app alias that resolved nowhere. doctor caught 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-run init (idempotent — applied effects stay), or init --no-install to 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 selfOnly layer declaring two importers found that "combine both option sets into ONE entry" is written for a single collision and misleads on several.

    emitLint scopes its entries per layer, so one rule key can own more than one. A selfOnly layer with two importers emits no-restricted-syntax on 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 --json is 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-config step 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 bare max-len); a rule scoped to a layer holding no files does not appear at all, which is inspect's declaratory-self-only note rather than a loss; and selfOnly'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. codeStyle listed its extras bare — max-len, linebreak-style — while the resolved ids carry the @stylistic/ prefix and only curly is core. An agent cross-checking the catalog against --print-config read 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.