@geonosis/oxlint-plugin-biological-architecture 2.3.1 → 2.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +903 -0
- package/package.json +3 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
# @geonosis/oxlint-plugin-biological-architecture
|
|
2
|
+
|
|
3
|
+
## 2.4.2
|
|
4
|
+
|
|
5
|
+
## 2.4.1
|
|
6
|
+
|
|
7
|
+
## 2.4.0
|
|
8
|
+
|
|
9
|
+
## 2.3.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 204d24c: `stepsOf(queries, { undos, bind, execution, budget })` turns a module of bound queries into steps whose compensations travel with them, read as plain data from an undo registry — the shape `@geonosis/db`'s `undosOf` hands over (#249). A name in `undos` is a write and is always a step; a name that is not is a read, and a read is a step only under `execution: 'durable'`, where the body is invoked from the top and a read that answered differently the second time would divert the replay. `bind` re-derives the callable set from the step's OWN context, because a durable instance hibernates between steps. A call made from inside another step's invoke, or where no run is in flight at all, passes through to the plain query (#260) — a step within a step is a second record of one unit of work, and under a durable platform the nested call has no run context to be recorded in. Four added names: `stepsOf`, `BoundQuery`, `StepsOptions`, `StepUndo`; 121 → 125 on the door, nothing removed. `durable-body-reads-through-steps` takes `stepProxies` (default `[]`), so a repo that reaches the outside world through the proxy is not told every one of its own steps is a free read.
|
|
14
|
+
- 9e2bc64: `ssot-no-inline-facts` accepts a literal first argument to `createWorkflow`, the kit's own workflow factory — a workflow's name is what a durable platform memoises by and what a run record is dispatched from, it is declared at the call, and there is no owner module to import it from. It read 34 of them as second copies of a fact on one clean migration (#264). The list is now an option, `declarationCallees`, which ADDS to the kit's own rather than replacing it, so a repo that names `defineFlow` does not lose `createStep` with it. No rule added or removed; the rule's schema gains one optional array.
|
|
15
|
+
|
|
16
|
+
## 2.3.0
|
|
17
|
+
|
|
18
|
+
### Minor Changes
|
|
19
|
+
|
|
20
|
+
- 3dac9a7: **`no-runtime-dead-affordance`: a control that renders and answers nothing (#5).** A control atom
|
|
21
|
+
with no `onClick`, no other `on…` handler, no `asChild`, no `type="submit"`/`"reset"`, no
|
|
22
|
+
`form`/`formAction` and no `disabled` looks pressable and does nothing; `type="button"` is the
|
|
23
|
+
default and arms nothing. It reads the same element→atom map `no-raw-html-atoms` takes, and the map
|
|
24
|
+
is EMPTY by default (law 6) — with none the rule judges nothing, so joining the `frontend` preset
|
|
25
|
+
(which `recommended` is) breaks no config on install. A spread is a prop bag the rule cannot read
|
|
26
|
+
and is left alone.
|
|
27
|
+
- 3dac9a7: **A path grant is enumerated, and what it exempts is counted (#139).** Measured from the option
|
|
28
|
+
table, the plugin has exactly three path GRANTS — `time-through-the-door`'s `exempt` and the
|
|
29
|
+
`allow` of `font-roles-only` and `tokens-only-colors`; `within`, `paths`, `roots` and `routers` are
|
|
30
|
+
scopes, not exemptions. Each now takes `exemptModules` beside its pattern: modules named in full,
|
|
31
|
+
matched by path or by the tail a repo writes them as, never as a pattern — the `boundary-time-is-primitive`
|
|
32
|
+
model. The patterns still work; what changed is that they are counted. Each such rule declares
|
|
33
|
+
`meta.grants` (`{ paths, modules }`), so the doctor keeps no list of another package's options, and
|
|
34
|
+
`geonosis-doctor`'s `exercised` line WARNs with the number of files the pattern exempts in THIS tree
|
|
35
|
+
beside the number the enumeration names.
|
|
36
|
+
- 3dac9a7: **`no-swallowed-catch`: a rejection on a declared fetch or refresh path is answered, never dropped
|
|
37
|
+
(#3).** An empty `.catch(() => {})`, an empty `catch {}` and a handler that only logs all turn a
|
|
38
|
+
failed refresh into a successful one for everything downstream; the fix is to write the failure
|
|
39
|
+
where the caller reads it, or rethrow. The surface is the `paths` option and it is EMPTY by default
|
|
40
|
+
(law 6): with no surface named the rule says nothing at all, so a repo that has not opted in carries
|
|
41
|
+
no findings and no refusal. It ships in its own `failure-paths` preset.
|
|
42
|
+
- 3dac9a7: **`no-generic-utility-package`: a package is named for what it does (#171).** A workspace whose
|
|
43
|
+
manifest name ends in `utils`, `common`, `helpers`, `shared` or `misc` is named for nothing, so
|
|
44
|
+
nothing can be refused entry and everything arrives — web helpers, a constants object, a time door.
|
|
45
|
+
The rule reads the name out of the package's own manifest and reports ONCE, at the entry, so a
|
|
46
|
+
generic package is one finding rather than one per file in it. `names` and `entries` are options;
|
|
47
|
+
the default anti-names are universal, never a repo's vocabulary. Preset `tooling`.
|
|
48
|
+
- 3dac9a7: **`list-take-without-order`: a page taken in no order (#21).** A `list*()` or `query.graph` call
|
|
49
|
+
carrying `take` and no `order` gets whatever rows the database finds cheapest, which is stable in
|
|
50
|
+
development and different under load — so `[0]` answers a question about the page, not about the
|
|
51
|
+
table, and the row a previous stage just wrote is not in it. Both spellings are read: the graph
|
|
52
|
+
call's nested `pagination`, and a module service's second argument. A spread the rule cannot read is
|
|
53
|
+
left alone. `callees` and `orderKeys` default to the framework's spellings. Preset `backend-medusa`.
|
|
54
|
+
- 3dac9a7: **`store-route-scopes-tenant-data` reads options, follows the read one hop, and accepts declared
|
|
55
|
+
evidence (#2).** The route surface, the customer-owned entities and the names that count as scoping
|
|
56
|
+
are `routes` / `entities` / `scopingCalls`, each defaulting to what the rule hardcoded; `evidenceCalls`
|
|
57
|
+
(default none) names the predicates that verify caller-supplied evidence — a reference plus an email —
|
|
58
|
+
so a public tracking route no longer has to move its read out of the file to stay quiet. A relative
|
|
59
|
+
sibling is followed one hop, with its comments blanked, so moving the read sideways no longer
|
|
60
|
+
silences the rule. The file's text scan became an AST read, which drops the loose `customer:` marker:
|
|
61
|
+
the rule is stricter there, not weaker.
|
|
62
|
+
- 3dac9a7: **`no-unregistered-plugin-dir` reaches a flat-file seam (#36).** A `roots` entry may now be written
|
|
63
|
+
`{ path, files: true }`, which makes each FILE under that root a registrant named by its own
|
|
64
|
+
basename — a seam whose providers are files beside their registry was out of scope by construction,
|
|
65
|
+
so a channel nothing reached read as coverage. A bare string root is unchanged and still ignores a
|
|
66
|
+
one-segment path, because a repo that did not ask for the flat reading must not get it.
|
|
67
|
+
- 3dac9a7: **`test-no-clock-polling-for-promise-flips`: a test that already holds a promise awaits it (#4).**
|
|
68
|
+
A `waitFor` whose callback is nothing but `expect` assertions, in a scope that created a promise and
|
|
69
|
+
proves it is one by awaiting or chaining it, polls the scheduler for a flip the promise decides
|
|
70
|
+
exactly — a pass that was slower than the microtask, or a timeout with no cause. `testGlobs` and
|
|
71
|
+
`pollers` default to `.test.`/`.spec.`/`__tests__` and `waitFor`, so the rule runs bare; a scope with
|
|
72
|
+
no promise in it, and a `waitFor` whose body clicks or types, are both left alone. Preset `testbed`.
|
|
73
|
+
- 3dac9a7: **`no-date-string-compare`: a timestamp ordered by its text (#20).** `String(row.created_at)` on a
|
|
74
|
+
Date is `"Fri Aug 30 2026 …"`, so a `localeCompare` or a `<` over it sorts by weekday name — stable,
|
|
75
|
+
plausible and wrong, with no symptom. The rule reports a `*_at`/`*At` field turned into text by
|
|
76
|
+
`String()` or a bare template and then compared; a raw timestamp under `<`, an equality, and a
|
|
77
|
+
template with copy in it are all left alone, because an ISO string does order lexicographically.
|
|
78
|
+
`fieldPattern` names which fields hold a time, defaulting to both spellings. Preset
|
|
79
|
+
`backend-workflows`.
|
|
80
|
+
|
|
81
|
+
## 2.2.0
|
|
82
|
+
|
|
83
|
+
### Minor Changes
|
|
84
|
+
|
|
85
|
+
- d15a654: **A rule names the floor its pattern belongs to, and the doctor says when it is not shipped (#290).**
|
|
86
|
+
`document-sagas-are-generic` and `documents-share-one-table` lint a pattern this kit ships no floor
|
|
87
|
+
for: the rule is the requirement and there is nothing to install behind it, so a second consumer
|
|
88
|
+
hand-writes the floor to satisfy the lint. A rule's `meta` carries `floor` — `{ name, pending }` —
|
|
89
|
+
and the doctor's `exercised` line says "names a pattern the kit does not ship yet (<floor>)" for a
|
|
90
|
+
pending one a config enables. RULES.md carries the generated table, so the inventory says so where
|
|
91
|
+
somebody reads a rule before enabling it, and the entry clears when the floor ships.
|
|
92
|
+
|
|
93
|
+
## 2.1.0
|
|
94
|
+
|
|
95
|
+
### Minor Changes
|
|
96
|
+
|
|
97
|
+
- c94d02e: **`mutating-step-declares-retry` reads a nested budget's limit (#191).** Under `retryKey: 'retry'` — how `@geonosis/workflows` spells it — a step declaring `retry: { limit: 0 }` PASSED: the rule evaluated the budget only when it was a bare number, so on the engine whose budget IS an object the "default spelled out" half of the rule never ran. It now reads the number one level down. `{ limit: 3 }` still says nothing, an object naming no limit still says nothing, and a limit that is a constant is still unevaluable. The key inside that object is the new `retryLimitKey` option, default `limit`, refused when emptied out. Corpus case: `engine/bad-nested-zero-retry.ts`, a finding under this build and silent under the last.
|
|
98
|
+
- 40710b3: **The plugin ships its rule docs (#235).** The tarball carried `corpus`, `dist` and a README, so the option shape of `door` / `within` / `wrapper` / `exempt` was readable only out of the minified bundle's `ACCEPTS` strings. `RULES.md` is now in the published files — asked of `npm pack`, not of the manifest — and it carries a generated line per rule naming every option that rule accepts, read from the rule's own JSON schema. `pnpm generate:rules-doc` writes it and the doc gate fails when the committed section is not what the rules generate.
|
|
99
|
+
|
|
100
|
+
### Patch Changes
|
|
101
|
+
|
|
102
|
+
- e932127: **A rule that starts refusing a bare run is a break, and now has to say so.** At 2.0.0 twelve rules refused a bare run that had not refused one at 1.4.0; the major changeset named `door` alone, and the first consumer read under it had its WHOLE oxlint config refused over `queryStepCalls` — a rule nothing had told anyone needed an option. A `tooling/` check now reads the bare-run table at the last published group tag (in the module it lives in today and in the suite it lived in at 1.4.0 — a tag with neither is a refusal, never a pass) and requires every rule that started refusing since, and every preset that started carrying one, to be named in a `major` changeset for this package. `RULES.md`'s inventory gains a **Requires** column carrying each rule's option, held against the table by a test so it cannot go stale.
|
|
103
|
+
|
|
104
|
+
## 2.0.0
|
|
105
|
+
|
|
106
|
+
### Minor Changes
|
|
107
|
+
|
|
108
|
+
- d22ae0f: **New rule `durable-body-reads-through-steps`** (`backend-workflows`). A durable platform memoises a step by NAME and journals no reads, so a free port read that gates a branch takes the other branch on a replay: the traced case completes, crashes before the run closes, replays, re-reads the status the first invocation wrote, throws its refusal and unwinds — refunding a charge for an invoice that is still sent, with the record saying `compensated`. Inside a `createWorkflow` body the options mark durable, a call reaching one of the repo's `ports` outside a `createStep` is a finding. `ports` is required (D-031); `durableKey` / `durableValue` cover both `durable: true` and `execution: 'durable'`, and the factories are options. An inline body is invoked once and is exempt.
|
|
109
|
+
- 75eed83: **D-031 refined: a rule scoped to a TREE refuses at the first file it would judge, and names it (#205).** `document-sagas-are-generic` refused in every package the preset reached — during.day's `packages/db` among them, which holds no saga and about which the rule has nothing to say either way. It now says nothing over a file that declares no flow, refuses over the first one that does with the option AND the file in the message, and is silent outside a `within` a config did name. A refusal about a tree with nothing in it is noise a consumer learns to route around, which is D-031's own failure mode pointed the other way.
|
|
110
|
+
- a5e2341: **A bare-run refusal names the rule and its REQUIRED option in a fixed first position (#199).** `biological-architecture/<rule> requires option \`<option>\`.` opens the message, and the thrown error is an `OptionMissing` carrying `rule` and `option` as fields — the sentence after it still quotes every option the rule accepts, which is what made a substring assertion unable to tell a required option from a mentioned one. Two wrong entries fell out of the stronger test the same hour: `worker-handles-are-scoped` refuses on `within` first, not `wrapper`, and `no-raw-html-atoms` names its positional `element→atom map`. The doctor reads the reason by any thrown class rather than by `Error:`.
|
|
111
|
+
- 6b5b3db: **BREAKING for a durable workflow: the definition declares its execution, and each door refuses the other's (#210).** `createWorkflow` hardcoded `execution: 'inline'` and `WorkflowOptions` had no way to say otherwise, while `durable-body-reads-through-steps` read a `durable`/`execution` key off the call — a declaration a consumer could not write without an excess-property error, so the engine and its own rule disagreed about what a durable workflow looks like. `WorkflowOptions.execution` (default `'inline'`) is carried on the definition; `executeDurable` refuses a workflow declared inline and `.run()` refuses one declared durable, both by name — a body written for one request is replayed by the durable door, and that is exactly the free read the rule exists for. **REACH CHANGED:** the rule's defaults are now `durableKey: 'execution'`, `durableValue: 'durable'`, the engine's own spelling; a repo whose factory says it another way names `durableKey`/`durableValue`. A durable workflow must add `{ execution: 'durable' }`.
|
|
112
|
+
- c6761f6: **`backend-workflows` carries the step rules that reach both engines.** `mutating-step-declares-retry` and `step-declares-compensation-or-none` are in it as well as in `backend-medusa` — the retry rule only since its key became an option — so a repo on `@geonosis/workflows` gets them from the preset that describes it. Neither leaves `backend-medusa`: taking a live rule out of the preset it was written for is a downgrade wearing a preset change. `unique-query-step-name` stays Medusa's alone, because the collision it reports is that framework's map last-winning. The list of rules that refuse a bare run is now held against the plugin's own rules by a test rather than by hand, which found seven refusals nobody had proved through the binary.
|
|
113
|
+
- 24e4ba4: **New rule `subpath-over-barrel`, and a `package-subpaths` preset for it.** A root import whose names ALL live under one of the package's declared subpaths is a finding naming the subpath to use instead: a subpath exists because its dependency footprint differs from the root's, so the root entry is the union of them all and a file that names it to reach one pays for every other. Asked for by a consumer with 64 such sites, who was about to forbid the root with `no-restricted-imports` — which cannot say which subpath to use. The package→subpath map is a required option (D-031); names spanning two subpaths are what the root is for and are exempt, as are type-only imports, which erase before any of the cost happens.
|
|
114
|
+
- 5de0fba: **New rule `boundary-time-is-primitive` (backlog #145).** A date-library type — `Date`, `Dayjs`, a `Temporal` member — in an exported parameter, return or shape hands every caller the library and its configuration, and no database row or wire message can carry one; time crosses a boundary as epoch milliseconds or an ISO date string. The types are the `types` option (required — enabled without it the rule refuses the run) and the exemption is `doors`: a NAMED, enumerable list of the modules allowed to say the library's type out loud, never a path pattern, so the count of doors a repo has is a number somebody can read. Only signatures are judged; a `Date` a module builds and keeps inside a function body is its own business.
|
|
115
|
+
- 5d9aadb: **One door, two rules, one path grammar (#204).** A repo writes its time module down once and enables both `time-through-the-door` and `boundary-time-is-primitive` with it — but `doors` compared a plain relative path while `door` took a regex over the filename, so the spelling that worked for one made the other report the door's OWN signatures. Both now read a door through `isDoor`: the exact path, a path the filename ends with (the specifier callers import it by is one), or a regex over the normalised filename.
|
|
116
|
+
- c43551a: **A missing-option refusal is raised once per run, and the doctor names it before lint runs (#202).** oxlint constructs a rule per FILE, so a bare option-gated rule printed "Error running JS plugin" once per file — during.day's first sight of a version bump was hundreds of unparseable problems and a counter answering CANNOT MEASURE. The refusal now speaks for the first file and the rule is inert for the rest; a tool that asks a rule what it needs (no filename, or the same file twice) still hears it every time. Beside it, `geonosis-doctor` constructs every rule a config enables with the options that config gives it and FAILs by name — `<config> → <rule>`, the option missing — which is the same defect said before any lint starts.
|
|
117
|
+
- 8349abc: **`time-through-the-door` polices the library the repo names.** `dayjs` was compiled in, so a repo on date-fns or luxon got only the raw-`Date` half of the rule — and lost the half that catches a SECOND configured formatter, which is the defect that half exists for. `libraries` defaults to `['dayjs']`, so nothing changes for a repo that says nothing, and the diagnostic and the probe both name the library that repo declared. Non-breaking.
|
|
118
|
+
- abd2f2d: **REACH CHANGED: `time-through-the-door`'s `exempt` defaults to the test paths — `__tests__/`, `.test.`, `.spec.` (#203).** 1.4.0 had `__tests__` compiled into the rule; making every path an option emptied it, so the rule's reach GREW into test files nobody had asked it to read — during.day's first RC run reported six findings there. Test paths are not consumer vocabulary the way a door is. A repo that names its own `exempt` list replaces the default, and gets exactly the list it wrote.
|
|
119
|
+
- 38889b1: **`step-opens-its-own-cell` reads the scope where the engine declares it.** The rule's premise was a consumer's `step` wrapper, which only a repo without a scope seam needs. `@geonosis/workflows` has one — `scope.perStep` on the runtime a durable host builds — so the finding is now a host (`createRuntime`, `entrypointFor`, both configurable through `hosts`) that declares none, read anywhere inside the call so the adapter's nested runtime counts, and not judged at all when the config cannot be read whole. `scopeKey` names the key. `engine` and `wrapper` keep the `sagaflow-js` spelling reachable and are OFF unless set; `within` is what the rule now refuses without.
|
|
120
|
+
- adebb0c: **`document-sagas-are-generic` reads both engines' factories.** The callee was `saga` alone, so a per-entity `createWorkflow('invoice.create', …)` produced no finding at all — measured through the binary in 026-W2. `factories` defaults to `['saga', 'createWorkflow']` and takes the repo's own names. A flow whose name is ASSEMBLED from the entity is a template literal the rule cannot read, and that is deliberate: it is what the generic mint the rule sends people to looks like, so the remedy can never be the finding.
|
|
121
|
+
- 6883b18: **BREAKING (one release of grace): the preset `backend-sagaflow-cf` is now `backend-workflows-cf` (#197).** It was named after `sagaflow-js`, which the org's own engine replaced; the fact it carries — `step-opens-its-own-cell` — is about a durable run on Cloudflare Workflows, not about one library. A config naming the old preset resolves to the same rule set and says so once per process, whatever door read it; the old name goes at the next major. The rule stays OUT of `backend-workflows`: its defaults are the org engine's spelling (`createRuntime`/`entrypointFor`, `scope.perStep`), a Medusa repo has no `createRuntime`, and a rule enabled where it can find nothing reads as compliance — which is what the engine-presumption line (#159) exists to flag.
|
|
122
|
+
- 3e586a5: **`unique-query-step-name` names its query steps or refuses the run.** `queryStepCalls` loses its default: the two names it shipped with are Medusa framework calls with no counterpart, so on `@geonosis/workflows` the rule had zero reach while reading as enforced — and that engine has no query-step primitive to inherit instead, since a repeated step name there is defined rather than lost (`reserve`, `reserve#2` in call order). Enabled bare it now refuses and names the option, D-031's shape. Breaking for anyone who had it enabled: add `queryStepCalls: ["useQueryGraphStep", "useRemoteQueryStep"]`.
|
|
123
|
+
- 2fb94d5: **`mutating-step-declares-retry` reads the key the repo's engine writes.** The budget's key is a `retryKey` option, default `maxRetries` (Medusa's); `@geonosis/workflows` spells the same budget `retry: { limit, backoff }`, and with the key compiled in a correctly budgeted step on that engine was a finding for ever. Every diagnostic names the configured key, so the remedy is the one this repo writes. A nested budget is one the rule does not evaluate, like a constant or a spread. Non-breaking: a repo that says nothing keeps `maxRetries`.
|
|
124
|
+
- 7533b00: **`document-sagas-are-generic` guards the tree the repo names.** `/packages/workflows/src/` was compiled in, so the same per-kind saga one directory over was silent and no other layout could enable the rule at all. The tree is a `within` option — a regex over the normalised filename, like every other path option here — and enabled without it the rule refuses the run and names it, including when only `perKindPrefixes` was given. The probe is built back out of `within`, so the rule is exercisable under a repo's own layout. Breaking for anyone who had it enabled: add `within`.
|
|
125
|
+
- 1499635: **`worker-handles-are-scoped` names its wrapper and its tree.** `withCells` and `/apps/api/src/` were compiled in, so the rule served exactly one repo's layout and one repo's vocabulary — a scope opener called anything else (`withConnection`) read as unscoped for ever, and a cell worker one directory over read as clean. Both are options now, `wrapper` (a list, since a repo may open more than one kind of handle) and `within` (a regex over the normalised filename); the diagnostic names the configured wrapper, and the probe is built back out of `within`. Breaking for anyone who had it enabled: add both.
|
|
126
|
+
- b979c78: **`time-through-the-door` stops naming one repo's module.** The door is a `door` option (a regex over the normalised filename) and the four exemption directories that were compiled in are an `exempt` list with no default; the diagnostic names the configured module rather than `utils/time`. Enabled with no `door` the rule refuses the run and names the option, D-031's shape, so a repo that half-configures it cannot read as compliance. The rule declares a `probe(options)` that plants outside the configured door — checked against `door` and `exempt` rather than assumed clear — so `geonosis-ratchet --prove` and the doctor's `exercised` line hold under the new shape. Breaking for anyone who had it enabled: add `door`.
|
|
127
|
+
- 97bc8e9: **A debt marker names the plan that owns it.** New rule `todo-names-its-plan` (preset `tooling`): a marker must cite a plan — `TODO(025):` — or it is work nobody scheduled. The marker words are an option and the plan-id shape is an option, both empty-by-default, and enabled with no markers the rule refuses the run. Comments come from the PARSER, so the word in a string literal or after the `//` of a URL is not a finding. New counter `orphanTodos` counts what a repo already has — markers naming no plan, and markers naming a plan that is not on disk — so the law can be adopted as a ratchet rather than as a sweep. The kit enables the rule on itself; every example in the rule's own prose cites a real plan, because exempting a rule's source from the rule is the exemption that never comes back.
|
|
128
|
+
|
|
129
|
+
### Patch Changes
|
|
130
|
+
|
|
131
|
+
- 36eafc4: **The plugin's atom discovery names its scratch directory `geonosis-atoms-` (#201).** One glob has to reach every directory the kit makes: 32,032 stale ones took a machine to `ENOSPC` on 2026-09-02, and 6,145 of the 10,502 still there an hour later — `atoms-`, `bio-rules-`, `parity-`, `unregistered-` — were outside the sweep everyone runs.
|
|
132
|
+
- e0aa82f: **`durable-body-reads-through-steps` says what it cannot see (#212).** It reads a call on a name `ports` lists, so a port read moved one function out — `readStatus(id)`, with `store.read` inside it — is invisible, and no rule that reads one file can follow it. The limit is in the fixShape now, with the way out (name the helper in `ports`, or make it the step the run needs anyway), and a corpus fixture holds the evasion so the silence is MEASURED rather than assumed.
|
|
133
|
+
- c1ee50d: **D-050's time discipline reaches the generated skill.** The two time rules' `fixShape` now carry the whole of it — the three kinds never mixed (an instant is epoch milliseconds, a business date is `YYYY-MM-DD` parsed strictly in UTC, display is a rendering produced at the edge), the clock taken as a `nowMs` parameter rather than read where it is needed, and the primitive as the thing that crosses a boundary. D-055 struck `@geonosis/time`, so the rules' own text is the only place the discipline ships; `skills/geonosis-code/SKILL.md` is regenerated and a test fails if a fixShape stops saying it.
|
|
134
|
+
|
|
135
|
+
## 1.4.0
|
|
136
|
+
|
|
137
|
+
### Minor Changes
|
|
138
|
+
|
|
139
|
+
- d5ee3d1: **`no-unregistered-plugin-dir` reads a SPLIT registry (#123).** `registry` now takes `string | string[]`. A consumer whose `medusa-config.ts` imports `medusa-config.fulfillment.ts` registers its carrier directories in the imported half, and reading only the named file made two REGISTERED directories a standing FAIL there. The halves are listed rather than followed — chasing imports out of a registry would need the repo's module resolution inside a linter — and one unreadable half still buys the silence a single unreadable file always did. The diagnostic names every half. Refusals from this rule now name the accepted shape as well as the missing key (#75), because a repo that reached for a list got `should be string` back and could not tell from the refusal whether a list was a shape the rule had.
|
|
140
|
+
|
|
141
|
+
### Patch Changes
|
|
142
|
+
|
|
143
|
+
- 1a2f36c: **The reach floor is now proven it can fail (#124).** The corpus test that demands at least one firing file per rule had never been seen red, which is the same thing a counter with no probe is: a gate nobody has watched measure. A planted hole — the one directory that fires `no-unregistered-plugin-dir`, deleted from a corpus copy — must make the floor name that rule and no other, and the complete corpus must not name it at all.
|
|
144
|
+
|
|
145
|
+
## 1.3.0
|
|
146
|
+
|
|
147
|
+
### Minor Changes
|
|
148
|
+
|
|
149
|
+
- c9b63c0: **A probe declares FILES, not a file (#101).** A rule whose reach is an option declares
|
|
150
|
+
`probe(options)` so the doctor can exercise it under the repo's OWN values. One file was not enough
|
|
151
|
+
for a rule that reads a SIBLING off disk: `no-unregistered-plugin-dir` returns silently when the
|
|
152
|
+
registry its options name is not there, so its probe fired nothing — and dielime, enforcing that
|
|
153
|
+
rule at error over four registries, read as a permanent `exercised` FAIL for a gate that was
|
|
154
|
+
working. Their call, that the rule stays and the doctor is expected-red, was right, and it was the
|
|
155
|
+
indictment.
|
|
156
|
+
|
|
157
|
+
`probe` may now answer `{ files: [{ path, source }, …] }`, and `exercised` mounts every file it
|
|
158
|
+
declares into the copy of the corpus. `no-unregistered-plugin-dir` declares the directory under
|
|
159
|
+
`roots` AND the registry at the configured path — a registry carrying no string literal at all, so
|
|
160
|
+
nothing in it can name the directory beside it. Its own test moved from "fires nothing, and says so"
|
|
161
|
+
to the one finding every other probe has to produce, through the real binary.
|
|
162
|
+
|
|
163
|
+
The bare `{ path, source }` is the one-file case of the same contract and is read unchanged. That is
|
|
164
|
+
not tidiness: the doctor reads probes off the plugin build a consumer has INSTALLED, and every build
|
|
165
|
+
published before this shape existed answers in the bare form — refusing it would turn every probe in
|
|
166
|
+
an older install into no probe at all, which reads as an accusation against the repo.
|
|
167
|
+
|
|
168
|
+
The FAIL branch — a probe PLACED under the repo's options that still fires nothing — is now held by
|
|
169
|
+
a planted plugin whose one rule declares a probe and reports nothing, ever. It was held by
|
|
170
|
+
`no-unregistered-plugin-dir`, so fixing that rule would have quietly deleted the coverage.
|
|
171
|
+
- 12a942f: **`emit-declares-attempts`: an options budget built by a call never passes silently (#84).** dielime
|
|
172
|
+
ran the rule at ERROR for eight commits reporting nothing — every site's budget came through
|
|
173
|
+
`retryableEvent(…)` and no `budgetHelpers` were declared, so the rule returned on the exact shape it
|
|
174
|
+
exists to judge. Now: with no helpers declared, a call-built budget reports `undeclaredHelpers`
|
|
175
|
+
("this repo declares no budget helpers — name them or pass `attempts` where it can be read"); with
|
|
176
|
+
helpers declared, a call the rule cannot even name reports `opaqueBudget`. A repo whose every emit
|
|
177
|
+
goes through a declared helper is unchanged. This is a judgement change and the release check claims
|
|
178
|
+
it (`--expect-changed emit-declares-attempts`).
|
|
179
|
+
|
|
180
|
+
## 1.2.0
|
|
181
|
+
|
|
182
|
+
## 1.1.0
|
|
183
|
+
|
|
184
|
+
### Minor Changes
|
|
185
|
+
|
|
186
|
+
- 8464846: An option-gated rule can be exercised, and the `runner` line names the runner it read.
|
|
187
|
+
|
|
188
|
+
Both source repos adopted 1.0.0 and both filed the same two findings on 2026-08-30. Both were right
|
|
189
|
+
about the fact and wrong about whose defect it was.
|
|
190
|
+
|
|
191
|
+
**Probes.** A rule whose REACH is an option cannot fire on a fixed corpus: the shipped fixtures say
|
|
192
|
+
`acme` and `corpus/workflows/`, a repo says its own brand and `packages/workflows/src/`, and the
|
|
193
|
+
rule reads as firing nowhere in a tree that enforces it on every file. Each of the twenty
|
|
194
|
+
`requireOption` rules now declares a `probe(options)` beside its `fixShape` — the smallest source
|
|
195
|
+
that must fire it under the options it was handed, at a path that lands where it looks. The
|
|
196
|
+
doctor's `exercised` check writes that probe into a copy of the corpus and runs it again, and says
|
|
197
|
+
where the evidence came from. The PATH is half of it: `step-opens-its-own-cell` was silent because
|
|
198
|
+
its fixture sits at `corpus/workflows/` while the option says `packages/workflows/src/` — the same
|
|
199
|
+
bytes at the second path fire — so a probe derives its path from the option, never from a fixed
|
|
200
|
+
corpus location.
|
|
201
|
+
|
|
202
|
+
It does not soften the check. A probe that was PLACED under this repo's options and fired nothing is
|
|
203
|
+
a FAIL naming the option values. A rule with no probe, or one whose options admit no constructible
|
|
204
|
+
path, is a new fifth verdict — **UNJUDGED**: the question could not be ASKED. It does not pass (the
|
|
205
|
+
run still exits 1) and it does not accuse a repo enforcing the rule on every file, which a FAIL
|
|
206
|
+
there does — one consumer names such a rule as live enforcement in its own law.
|
|
207
|
+
`no-unregistered-plugin-dir` reads its registry off disk and cannot be probed by one file, which it
|
|
208
|
+
says rather than widening. The probes are read off the plugin the config loads, so a plugin too old
|
|
209
|
+
to declare any behaves exactly as before.
|
|
210
|
+
|
|
211
|
+
**`bun test`.** Measured on bun 1.4.0: a planted failing assertion exits 1, so does an awaited one,
|
|
212
|
+
so does a throw in `afterAll`. Its exit code is a verdict, and a bun workspace is now OK naming bun
|
|
213
|
+
and the measurement. The WARN is for vitest, whose `@cloudflare/vitest-pool-workers` really did
|
|
214
|
+
exit 0 over failing suites for weeks — eight lines of one consumer's report blamed it over scripts
|
|
215
|
+
running bun, which is how a reader learns to skim the section and miss the true lines beside them.
|
|
216
|
+
|
|
217
|
+
## 1.0.1
|
|
218
|
+
|
|
219
|
+
### Patch Changes
|
|
220
|
+
|
|
221
|
+
- 1c2ee0b: **Every package ships its declarations and says where they are.** `@geonosis/visual-diff` and
|
|
222
|
+
`@geonosis/verify` emitted no `.d.ts` at all (TS7016 in a consumer: the comparator every visual test
|
|
223
|
+
calls became `any` at the one seam that decides whether a screenshot regressed), and no package
|
|
224
|
+
declared `types` — the nineteen that worked did so through the sibling-file fallback, which ends the
|
|
225
|
+
day `exports` gains a condition. Every export now carries a `types` condition beside `default`, every
|
|
226
|
+
manifest a `types` field, and `tooling/types.test.ts` holds all twenty-one to it after the build.
|
|
227
|
+
|
|
228
|
+
`geonosis-verify-arch` prints a check once when its label is its id ("direction, orphan", not
|
|
229
|
+
"direction direction").
|
|
230
|
+
|
|
231
|
+
## 1.0.0
|
|
232
|
+
|
|
233
|
+
### Major Changes
|
|
234
|
+
|
|
235
|
+
- 94a2376: **`backend-medusa` grows from two rules to nine** — Medusa's own silences, turned into gates. Every
|
|
236
|
+
one exists because the framework INFERS something instead of making you say it. None throws; all are
|
|
237
|
+
found in production, far from the cause.
|
|
238
|
+
|
|
239
|
+
- **`mutating-step-declares-retry`** — `maxRetries` defaults to 0, so a step that writes and
|
|
240
|
+
declares no retry class reverts real state on the first transient failure. Read-only steps are
|
|
241
|
+
exempt; a budget the rule cannot evaluate passes.
|
|
242
|
+
- **`step-declares-compensation-or-none`** — `noCompensation = !compensateFn` is inferred from an
|
|
243
|
+
absent third argument, so a deliberate non-compensation and a forgotten one are the same source.
|
|
244
|
+
Write the function or declare `noCompensation: true`.
|
|
245
|
+
- **`named-when-block`** — an unnamed `when()` derives a `when-then-{ulid}` step name, so a run
|
|
246
|
+
resumed after a restart cannot find its checkpoint. The overloads are told apart by ARITY.
|
|
247
|
+
- **`subscriber-declares-id`** — the loader infers a subscriber id from a function or file name, and
|
|
248
|
+
two that infer alike dedupe each other with no error.
|
|
249
|
+
- **`no-loader-side-effect-in-index-file`** — `ResourceLoader` filters files named `index`, so a
|
|
250
|
+
resource declared in one registers only if something else happens to import it. Pure re-export
|
|
251
|
+
barrels pass. **Defaults to `subscribers/` and `jobs/` only**: measured in the installed 2.19,
|
|
252
|
+
`resource-loader.js:51` still filters and `workflows/workflow-loader.js:34` is the only caller in
|
|
253
|
+
the framework passing `allowIndex: true`, so `workflows/index.ts` IS loaded now.
|
|
254
|
+
- **`service-name-not-reserved`** — a module key matching Medusa's `Modules` enum silently REPLACES
|
|
255
|
+
the built-in at resolution. The 33 keys of 2.19 ship as the `reserved` option's default; the rule
|
|
256
|
+
follows `Module(SERVICE_NAME, …)` to the constant, declared before or after the call.
|
|
257
|
+
- **`no-index-count-as-exact`** — the index engine's count is an EXPLAIN plan-row estimate and reads
|
|
258
|
+
as an exact total to every consumer. Requires the label in the same payload.
|
|
259
|
+
|
|
260
|
+
Every framework name is an option with the framework's own value as its default — `stepFactories`,
|
|
261
|
+
`blocks`, `moduleFactories`, `resourceFactories`, `mutationPrefixes`, `estimateFields`, `labelKey`,
|
|
262
|
+
`paths`, `directories`, `reserved` — and each refuses the run when emptied out.
|
|
263
|
+
- c575f44: Five counters, `tolerance`, and a baseline rewrite that only ever goes down.
|
|
264
|
+
|
|
265
|
+
- `disabledCiJobs` — lines of `if: false` across the workflow files (`dir`). A job switched off to
|
|
266
|
+
get a release through is debt exactly like a lint error: invisible, because the pipeline goes
|
|
267
|
+
green. A condition that merely mentions `false` is not one, and a repo with no workflows
|
|
268
|
+
directory reads 0 rather than refusing; a workflow that is there and cannot be read refuses.
|
|
269
|
+
- `bundleBytes` — one integer out of whatever the repo's sizing command printed (`command`,
|
|
270
|
+
`match`), separators and all. With `match` it takes the group that pattern names and refuses when
|
|
271
|
+
it matches nothing, rather than falling back to any integer and reporting the wrong number as
|
|
272
|
+
confidently as the right one.
|
|
273
|
+
- `fastTierMs` — `finishedAt − startedAt` from the gate report `geonosis-verify` wrote (`report`,
|
|
274
|
+
`tier`). It refuses a report of another tier: the full tier takes minutes, and timing it against
|
|
275
|
+
the fast tier's baseline would report a regression of the wrong thing.
|
|
276
|
+
- `testsWithoutRunner` — workspaces holding tests with no `test` script. Unmeasured and passing are
|
|
277
|
+
indistinguishable from outside, so the count of unmeasured things has to be a number of its own.
|
|
278
|
+
- `packagesWithoutTypecheck` — workspaces with no `typecheck` script (`script`).
|
|
279
|
+
|
|
280
|
+
`tolerance` is new on the verdict: the fraction of the baseline a number may drift UP before the
|
|
281
|
+
ratchet calls it growth. Bytes and milliseconds move on a dependency patch nobody chose, and a gate
|
|
282
|
+
that fails on +40 bytes is a gate that gets switched off. It forgives noise upward only — a shrink
|
|
283
|
+
of any size still lowers the baseline — and a `tolerance` that is not a non-negative number stops
|
|
284
|
+
the run naming the entry, because one that silently became `NaN` reads exactly like a counter that
|
|
285
|
+
can never grow.
|
|
286
|
+
|
|
287
|
+
**Fix:** a baseline rewrite now lowers only the numbers that SHRANK. Writing every measured key back
|
|
288
|
+
was a harmless no-op while `held` implied `now === baseline`. Tolerance breaks that implication, and
|
|
289
|
+
the blanket rewrite would have laundered growth-within-tolerance into the new floor on the first
|
|
290
|
+
unrelated win — a baseline going up on a pass.
|
|
291
|
+
|
|
292
|
+
`examples/dielime.ratchet.json` is dielime's Phase 3 config: 36 keys, one `testFailures` entry per
|
|
293
|
+
workspace. It keeps `--format=unix` and `expectFormat` on the two oxlint counters, which dielime's
|
|
294
|
+
own config drops — the summary cross-check can tell a misparse from a clean run only when the run
|
|
295
|
+
printed a summary, and a warnings-only run in an unreadable shape prints one that says nothing
|
|
296
|
+
about errors. Pinning the format is what closes that window.
|
|
297
|
+
- c418f91: The `geonosis` Claude Code plugin — the method as enforcement rather than advice.
|
|
298
|
+
|
|
299
|
+
`plugin/` ships five hooks, Node ≥ 22, zero dependencies, every tool resolved from the CONSUMER's
|
|
300
|
+
`node_modules/.bin` so a repo that has not adopted a tool is never blocked on a tool it lacks:
|
|
301
|
+
|
|
302
|
+
- **SessionStart** runs `geonosis-doctor` if it is installed and prints a one-line digest of
|
|
303
|
+
`.oxlintrc.json` — which plugins are loaded, how many architecture rules are at `error`. It never
|
|
304
|
+
fails a session.
|
|
305
|
+
- **PreToolUse / Bash** refuses `prettier` (law 4) and `--no-verify` (law 7), and hands a
|
|
306
|
+
`git commit` to `geonosis-ledger` — the message its `-m` flags compose, and whether the staged
|
|
307
|
+
diff delivers anything but comments. The detectors are the ledger's, never reimplemented here; a
|
|
308
|
+
repo without it gets a note and no opinion.
|
|
309
|
+
- **PreToolUse / Edit|Write|MultiEdit** refuses a write that ADDS `eslint-disable`,
|
|
310
|
+
`oxlint-disable`, `@ts-ignore` or `@ts-expect-error` (law 3), any write to `gate-baseline.json` or
|
|
311
|
+
under `.geonosis/` (D-007 — the scored agent never writes the scoreboard), and a lint config that
|
|
312
|
+
sets a rule to `"warn"`, `"off"`, `0` or `1` (law 2). It reads what the call would ADD, never what
|
|
313
|
+
it removes, so the edit that DELETES a suppression is not refused by the guard against them.
|
|
314
|
+
- **PostToolUse / Edit|Write** runs oxfmt on the file just written. It never blocks.
|
|
315
|
+
- **Stop** runs `geonosis-verify fast` and will not let the turn end on a red one, capped at five
|
|
316
|
+
blocks per session — an agent retrying the same broken thing five times has a problem it will not
|
|
317
|
+
solve on the sixth, so the sixth prints the report and lets go.
|
|
318
|
+
|
|
319
|
+
`.claude-plugin/marketplace.json` offers it from `./plugin`. `pnpm generate:skill` now writes the
|
|
320
|
+
plugin's copy of the mechanical skill, the engineer agent with the machine-specific path taken out,
|
|
321
|
+
and `plugin.json` carrying the kit's own version; staleness tests fail when any of the four drift.
|
|
322
|
+
- 45407ec: **The generic articles as glob-scoped rule files, and a law you can start from.** Four
|
|
323
|
+
`.claude/rules/`-shaped files with `paths:` frontmatter, so three of them cost nothing until a
|
|
324
|
+
matching file is opened: `tiers.md` (`**/features/**`, `**/ui/src/**`), `gates.md` (`**` — the one
|
|
325
|
+
that loads with the law), `backend.md` (`**/workflows/**`, `**/api/**`, `**/db/**`) and `process.md`
|
|
326
|
+
(`plans/**`, `proofs/**`). The two area files are POINTERS at `geonosis-backend`, `geonosis-plan`
|
|
327
|
+
and `geonosis-process` rather than second copies of them.
|
|
328
|
+
|
|
329
|
+
`tiers.md` is **generated** from the `frontend` preset by `pnpm generate:skill`: the classification
|
|
330
|
+
cascade and the state matrix as prose, then every rule of the preset filed under the tier its name
|
|
331
|
+
is about, with the first sentence of its own description. It cannot name a rule the plugin does not
|
|
332
|
+
ship and cannot miss one it does, and a staleness test fails the build if the committed file is not
|
|
333
|
+
what the generator writes.
|
|
334
|
+
|
|
335
|
+
`docs/law-template.md` is the 45-line `CLAUDE.md` a repo copies: `@AGENTS.md` on the first line
|
|
336
|
+
(Claude Code does not read `AGENTS.md` natively, so the import is what makes one file serve every
|
|
337
|
+
other agent), the three presets as one-line choices — `state: controlled | zustand`,
|
|
338
|
+
`comments: rich | ceiling`, `tests: colocated | central` — the six laws, the gate, and a table
|
|
339
|
+
pointing at where the depth actually lives. Pinned at 60 lines by a test that measures the block a
|
|
340
|
+
repo copies, not the page explaining it.
|
|
341
|
+
|
|
342
|
+
**Measured, and worth saying: Claude Code has no `rules` plugin component.** A plugin contributes
|
|
343
|
+
skills, agents, hooks and MCP servers; `.claude/rules/` is a project directory and a plugin cannot
|
|
344
|
+
write into it. So the manifest lists the four under `metadata`, the free-form key — an inventory of
|
|
345
|
+
what the tree ships, never a wiring that loads them — and `plugin/rules/README.md` gives the one
|
|
346
|
+
symlink that installs them. A key nothing reads, in the shape of one that does, is the failure this
|
|
347
|
+
kit is about. `claude plugin validate ./plugin --strict` stays ✔ with the key.
|
|
348
|
+
- 5f09ddc: Every hook is driven end to end, as Claude Code drives it.
|
|
349
|
+
|
|
350
|
+
Each script is SPAWNED with real stdin JSON over a throwaway repo carrying a `geonosis.json`, a
|
|
351
|
+
`gate-baseline.json`, an `.oxlintrc.json` and a `node_modules/.bin` linked to the BUILT
|
|
352
|
+
`geonosis-verify` and `geonosis-ratchet`. A hook tested by calling its policy function proves the
|
|
353
|
+
policy; only a spawn proves the exit code, the JSON on stdout and the tool resolution — which is
|
|
354
|
+
the whole of what Claude Code actually reads.
|
|
355
|
+
|
|
356
|
+
The Stop hook is exercised against a real fast tier that really goes red: a law file one line over
|
|
357
|
+
its baseline, so the ratchet grows and the hook blocks with exit 2, the failing step and its tail on
|
|
358
|
+
stderr. Then the sixth block prints the report and exits 0; a green run clears the session's count;
|
|
359
|
+
`stop_hook_active` short-circuits; and a repo with a BACKLOG that did not regress is not blocked at
|
|
360
|
+
all — which is the whole adoption argument, tested rather than asserted.
|
|
361
|
+
|
|
362
|
+
The suite was falsified before it was trusted: making Stop exit 0 instead of 2, dropping the
|
|
363
|
+
delivery check, and raising the cap to 50 each turned exactly the tests that should have gone red,
|
|
364
|
+
red.
|
|
365
|
+
- 2732831: **The `/goal` command, in the plugin.** `plugin/commands/goal.md` unifies the two `/goal` commands
|
|
366
|
+
the source repos each grew separately: during.day's dispatch (`status`, `item <id>`, `rederive`), its
|
|
367
|
+
caps and its stop conditions, plus dielime's derive, value rubric, tiered verification and G4
|
|
368
|
+
meta-gate.
|
|
369
|
+
|
|
370
|
+
Boot is `geonosis-ledger status` and nothing else — never from memory, never from a summary. Pick is
|
|
371
|
+
**the pain the user actually stated**, above revenue path, operability, debt and capability; it is
|
|
372
|
+
the tier most often skipped because it is the least comfortable. Then EARS → RED → GREEN →
|
|
373
|
+
`geonosis-verify fast` → G2 review → G3 walk → proof → tick → commit, ledger row in the same commit
|
|
374
|
+
as the work it records.
|
|
375
|
+
|
|
376
|
+
**Where the two differed, the choice is recorded rather than averaged.** during.day's stop conditions
|
|
377
|
+
win over dielime's "NEVER STOP", which is a human directive to one repo and exactly what law 6 says
|
|
378
|
+
cannot be shared. dielime's `rederive` wins over `replenish`, because parallel read-only agents with
|
|
379
|
+
uncited candidates discarded is a mechanism and the other is a line. The caps are the kit's own
|
|
380
|
+
numbers — 2 revisions, 2 CI-fix attempts, 5 Stop blocks, draft PR — so the command does not put a
|
|
381
|
+
second bound on what the Stop hook already enforces.
|
|
382
|
+
|
|
383
|
+
**⚠AUTH reads `geonosis.json → caps.auth` (D-019), empty until a repo fills it.** during.day's list
|
|
384
|
+
is three specific backlog item numbers; hardcoding those would violate law 6.
|
|
385
|
+
|
|
386
|
+
**The G4 meta-gate carries all three rotation dimensions**, because each was earned by a failure that
|
|
387
|
+
passed every other gate: rotating *aspect* alone let five consecutive same-surface ticks through;
|
|
388
|
+
rotating *surface* alone let design rot for ten ticks; a consecutive counter alone was reset by one
|
|
389
|
+
off-aspect tick, so the distribution over the last six is checked too; and healthy rotation still
|
|
390
|
+
drifted to ever-smaller fixes, so there is an impact-tier floor.
|
|
391
|
+
|
|
392
|
+
`plugin/goal-command.test.ts` pins every `geonosis-*` command and every skill the file names against
|
|
393
|
+
what this workspace actually ships — a slash command naming a renamed tool fails halfway through a
|
|
394
|
+
tick with a shell error. Validated with `claude plugin validate . --strict` @ 2.1.251.
|
|
395
|
+
- 89c6769: The ratchet's own layers are walled, and the walls have a door that is proved open.
|
|
396
|
+
|
|
397
|
+
The three ratchet layers named FILE paths only, so `packages/ratchet/src/index.ts` — the published
|
|
398
|
+
entry — sat in no layer and could import anything, the CLI included, with lint green. It is
|
|
399
|
+
`ratchet-api` now, and the formatter it shares with the CLI has moved into a `ratchet-report` layer
|
|
400
|
+
of its own rather than being counted as part of the CLI it is not.
|
|
401
|
+
|
|
402
|
+
The dogfood suite gains a second kind of plant. A wall that refused everything would be as useless
|
|
403
|
+
as one that refused nothing, so each layer's permitted import — `verify` reaching the lock through
|
|
404
|
+
`@geonosis/ratchet` — is planted and asserted to stay CLEAN. Without that, the next engineer widens
|
|
405
|
+
`mayImport` to get their work through and the wall goes with it.
|
|
406
|
+
- b2d915f: **Fix:** a counter's command no longer loses what it wrote to stderr.
|
|
407
|
+
|
|
408
|
+
`<command> 2>&1` binds the redirect to the LAST command of the string. `echo boom >&2; echo fine`
|
|
409
|
+
therefore merged nothing, and on a ZERO exit `execSync` hands back stdout alone — so a tool that
|
|
410
|
+
prints its findings to stderr and exits 0 read as a tool that found nothing. That is the exact shape
|
|
411
|
+
of a gate that has quietly stopped gating, and it is the one direction in which this kind of bug is
|
|
412
|
+
dangerous: the number comes back lower than the truth and the baseline banks it.
|
|
413
|
+
|
|
414
|
+
On a NON-ZERO exit the line survived, because the failure path concatenates both captured buffers —
|
|
415
|
+
but concatenation put every stderr line after every stdout line, moving each diagnostic away from
|
|
416
|
+
the line it was about, which is what the evidence tail on a regression is for.
|
|
417
|
+
|
|
418
|
+
The command now runs in a subshell, so the shell itself merges the streams in the order they were
|
|
419
|
+
written. Measured before and after: `echo boom >&2` on a zero exit read `""` and now reads `boom`;
|
|
420
|
+
`echo one; echo two >&2; echo three; exit 4` read `one three two` and now reads `one two three`.
|
|
421
|
+
- ff9874b: `tokens-only-colors` and `font-roles-only`, in a new `themekit` preset.
|
|
422
|
+
|
|
423
|
+
A themed kit is only as themed as its least disciplined component, and the defect this catches is
|
|
424
|
+
the one nothing else does: a hardcoded colour or family keeps rendering, plausibly, in the brand it
|
|
425
|
+
was written for. Nothing throws, no page 404s, and a review sees a component that looks right.
|
|
426
|
+
|
|
427
|
+
`tokens-only-colors` fires on a colour written into a `className` or an inline `style` — hex,
|
|
428
|
+
`rgb()`, `hsl()`, `oklch()`, `lab()`, `color()`, and the 148 CSS named colours — including inside
|
|
429
|
+
the shorthands (`border: '1px solid #d8dadb'`, a `boxShadow`'s `rgba()`). It also fires on a `var()`
|
|
430
|
+
naming a custom property **outside** the configured token prefixes: `var(--legacy-blue)` is not a
|
|
431
|
+
literal and reaches around the theme exactly as completely as one. That second half is what makes
|
|
432
|
+
`tokenPrefix` load-bearing rather than decorative, and enabled without it the rule refuses the run
|
|
433
|
+
(D-031) — it cannot tell a token from a bypass, and a rule that cannot tell reads like a clean tree.
|
|
434
|
+
|
|
435
|
+
`font-roles-only` fires on a family literal and on a `var(--font-…)` naming a role the repo never
|
|
436
|
+
declared. Role names are that repo's data (law 6), so `roles` is required and the rule refuses
|
|
437
|
+
without it. It deliberately says **nothing** about bare `font-*` utility classes, because Tailwind
|
|
438
|
+
puts weight and style under the same prefix — a rule that fired on `font-bold` is a rule people
|
|
439
|
+
switch off, and the family violations go off with it.
|
|
440
|
+
|
|
441
|
+
Both take `allow` paths for the token layer, where a literal is the point. Both ship hand-built AST
|
|
442
|
+
cases and real fixtures through the real binary, including **an unedited component copied out of a
|
|
443
|
+
consumer's kit that passes** — a rule that has only ever run on source written to exercise it has
|
|
444
|
+
not been shown to leave real code alone.
|
|
445
|
+
- 063f928: Three defects in the two themekit rules, found by running them over both consumers.
|
|
446
|
+
|
|
447
|
+
Two false positives — a rule that fires on an accepted convention is a rule people switch off:
|
|
448
|
+
|
|
449
|
+
- **`font-[500]` is a WEIGHT, not a family.** Tailwind overloads `font-` in the arbitrary spelling
|
|
450
|
+
too, deciding family-or-weight by the value being a bare number. `font-roles-only` reported it in
|
|
451
|
+
three of a consumer's files. `font-[8bitoperator]` still fires — only a value that is *entirely* a
|
|
452
|
+
number is a weight.
|
|
453
|
+
- **A `var()` the element declares on itself is a local alias.** A component that writes
|
|
454
|
+
`style={{ '--pb-text': fromAProp }}` and reads `text-[var(--pb-text)]` back is not reaching around
|
|
455
|
+
the theme; the variable is set and consumed on one element and never leaves it.
|
|
456
|
+
|
|
457
|
+
And one **under**-reach that only the real binary could have shown, which is why law 8 requires both
|
|
458
|
+
test layers:
|
|
459
|
+
|
|
460
|
+
- **`style={{ … } as CSSProperties}` was invisible.** It parses as a `TSAsExpression`, and the style
|
|
461
|
+
reader only accepted a bare `ObjectExpression` — so every colour literal inside an asserted inline
|
|
462
|
+
style went unreported. That is how every repo writes a custom property in a style, because React's
|
|
463
|
+
`CSSProperties` has no index signature for one. The hand-built AST node was a bare object and
|
|
464
|
+
passed; the fixture through oxlint failed. Type assertions, `satisfies`, non-null and parentheses
|
|
465
|
+
are all unwrapped now.
|
|
466
|
+
|
|
467
|
+
Measured after the fixes: 219 findings across both consumer trees, read-only, in
|
|
468
|
+
`proofs/022-W6-consumers`.
|
|
469
|
+
- c3675c6: **`backend-medusa` grows from nine rules to twelve** — the per-file halves of three scans a consumer
|
|
470
|
+
runs repo-wide. Each was a fact about ONE file all along: one workflow body's steps, one route's own
|
|
471
|
+
path, one handler's own writes.
|
|
472
|
+
|
|
473
|
+
- **`unique-query-step-name`** (scan R4.3) — two `useQueryGraphStep`/`useRemoteQueryStep` calls in
|
|
474
|
+
one workflow body where either has no `.config({ name })`, or both have the same one. The
|
|
475
|
+
step-handler map is a plain assignment and the orchestrator appends a duplicate action with no
|
|
476
|
+
dedupe, so one of the two reads silently never happens and the workflow carries on with the
|
|
477
|
+
other's data. A SINGLE un-named step is exempt — it collides with nothing. A name held in a
|
|
478
|
+
constant passes: unreadable is not absent. Options `workflowFactories`, `queryStepCalls`.
|
|
479
|
+
- **`plugin-route-namespaced`** (scan R3.8's per-file half) — a route owned by a declared plugin
|
|
480
|
+
with no namespace segment under `/store` or `/admin`, or outside those scopes, or with a dynamic
|
|
481
|
+
`:param` where its namespace belongs. The routes-loader assigns last-write-wins, so a bare
|
|
482
|
+
`/store/<leaf>` is claimable by any other package with no error. `packages` mirrors the scanner's
|
|
483
|
+
`[{ under, name, namespaced }]` and is REQUIRED — an enabled rule with nobody declared a plugin
|
|
484
|
+
can never fire, and that reads exactly like compliance. An optional `namespace` per entry pins
|
|
485
|
+
WHICH segment it must be; left off, only presence is checked, because measured on a real tree
|
|
486
|
+
three of nineteen plugin namespaces deliberately differ from the directory name.
|
|
487
|
+
- **`route-no-inline-mutation`** (scan R5.1) — a service write in a route handler, which skips the
|
|
488
|
+
compensation and retry class behind the workflow door. Reads are exempt. The escape-hatch
|
|
489
|
+
annotation is read from the **statement**, and that is why this is an AST rule: the equivalent
|
|
490
|
+
text scan looks on the matching line or the one above, and a consumer's own annotated write is
|
|
491
|
+
spread over six lines by a cast, so the annotation lands five lines from the call and its gate is
|
|
492
|
+
red on a write a person already signed off. Options `paths`, `mutationPrefixes`,
|
|
493
|
+
`inlineMutationOk`.
|
|
494
|
+
|
|
495
|
+
`writesWith` — what counts as a service write — moves to `src/rules/lib/mutations.ts` so this rule
|
|
496
|
+
and `mutating-step-declares-retry` cannot drift into disagreeing about it, the same reason
|
|
497
|
+
`lib/steps.ts` exists. Proved inert: under `--expect-changed` over the corpus,
|
|
498
|
+
`mutating-step-declares-retry` has identical reach and an identical count on both sides.
|
|
499
|
+
|
|
500
|
+
Proved by the corpus rather than asserted: `--expect-changed` over the shipped corpus, old build and
|
|
501
|
+
old rule set against new, returns **HELD** for exactly these three and **no UNCLAIMED rule** — they
|
|
502
|
+
are the only behaviour change in this release.
|
|
503
|
+
- 48bce0c: **Fix:** only a counter that measures a quantity accepts a `tolerance`.
|
|
504
|
+
|
|
505
|
+
Any counter took one. `{"counter":"lawLineCount","tolerance":0.5}` over a two-line law with a
|
|
506
|
+
baseline of 2 printed `ratchet PASS` at three lines — a 50 % growth, waved through. The same edit is
|
|
507
|
+
`"tolerance": 100` on `oxlintErrors`: the gate switched off from the config file, with the run still
|
|
508
|
+
reporting green. That is law 2 — never downgrade a rule — wearing a friendlier name, and it was the
|
|
509
|
+
easiest edit in the repo to get past a reviewer.
|
|
510
|
+
|
|
511
|
+
`Counter` gains `tolerates?: true`, declared only by `bundleBytes` and `fastTierMs`. Whether a
|
|
512
|
+
number is a measured quantity or a count of findings is the counter's to know, not the config's.
|
|
513
|
+
Anything else now stops the run, naming the entry:
|
|
514
|
+
|
|
515
|
+
```
|
|
516
|
+
geonosis-ratchet: "oxlintErrors" does not accept a tolerance — only a counter that measures a
|
|
517
|
+
quantity declares one
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
The plugin's write guard gains its first **ask**. A write to `geonosis.ratchet.json` that introduces
|
|
521
|
+
or raises a `"tolerance"` is neither refused nor allowed: it goes in front of the human as
|
|
522
|
+
`permissionDecision: "ask"`. Widening a band is legitimate and is not the agent's call — a band
|
|
523
|
+
somebody chose and a band somebody's agent chose are not the same thing. A tolerance carried through
|
|
524
|
+
an edit unchanged is not a change and says nothing.
|
|
525
|
+
- 461233c: Docs for the two new things: `docs/plugin.md` and `packages/verify/README.md`.
|
|
526
|
+
|
|
527
|
+
`docs/plugin.md` covers installing the plugin from a local path and from the remote once it is
|
|
528
|
+
pushed, what each of the five hooks does and the reason it does it that way, the five-block cap,
|
|
529
|
+
which files the runner owns and why the agent may not write them, what to put in `geonosis.json`,
|
|
530
|
+
and — the section every README skips — what the plugin CANNOT do. A `fast` tier of `["true"]` passes
|
|
531
|
+
for ever; only `geonosis-ratchet --prove` shows a counter can go red.
|
|
532
|
+
|
|
533
|
+
`packages/verify/README.md` is the runner's own: `--prove` first, the config with no default tier,
|
|
534
|
+
the three exit codes and why a report that could not be written is exit 2, the report shape with the
|
|
535
|
+
`<tier>/<n>` id rule, and `--exclusive`.
|
|
536
|
+
|
|
537
|
+
`docs/README.md` indexes both. The ratchet README gains rows for the five new counters, a section on
|
|
538
|
+
`tolerance` and the shrank-only rewrite it forces, and the landmine that cost an afternoon: a
|
|
539
|
+
loader or shim named RELATIVELY in a counter's command resolves to nothing under `--prove`, whose
|
|
540
|
+
probes run in a scratch directory. Write it `$PWD/…` and it works in both.
|
|
541
|
+
- e48501b: `@geonosis/verify` — one gate runner, with a report a hook can read.
|
|
542
|
+
|
|
543
|
+
`geonosis-verify <tier>` runs the shell steps a repo declared under `verify` in `geonosis.json`,
|
|
544
|
+
serially, stopping at the first red, and writes `.geonosis/gate-report.json`:
|
|
545
|
+
`{ tier, startedAt, finishedAt, ok, steps: [{ id, command, ok, exitCode, ms, tail, skipped? }] }`.
|
|
546
|
+
A step whose text is another tier's name is that tier, inlined in place; the inlined steps keep the
|
|
547
|
+
tier that declared them, so `fast/2` is the same command whether the operator ran `fast` or ran the
|
|
548
|
+
`full` tier that inlines it.
|
|
549
|
+
|
|
550
|
+
There is no default tier and there never will be one: no config, no `verify` object, or an unknown
|
|
551
|
+
tier is refused by name with exit 2, and so is a green run whose report could not be written — a
|
|
552
|
+
gate that cannot record has not passed. A failed step is exit 1.
|
|
553
|
+
|
|
554
|
+
`--exclusive` takes the ratchet's machine-wide lock through `@geonosis/ratchet`'s published entry
|
|
555
|
+
(`--exclusive-timeout <seconds>` bounds the wait), so a verify and a ratchet on one laptop serialise
|
|
556
|
+
against each other rather than each against itself. `--prove` plants a tier with one passing step
|
|
557
|
+
and one that exits 3, runs the real binary over it in a temp directory, and checks the failure
|
|
558
|
+
reached the report — a runner that reports green is worth nothing until it has been watched going
|
|
559
|
+
red on purpose.
|
|
560
|
+
|
|
561
|
+
The kit walls the new package with its own `layer-walls` rule: `verify` may import `ratchet-api`
|
|
562
|
+
and nothing behind it, planted against in the dogfood suite like the other walls.
|
|
563
|
+
- 88d7bf6: **`@geonosis/cli` — one declarative command registry, and the `geonosis` bin over it.** Fifteen
|
|
564
|
+
commands: eleven that front a kit bin by SPAWNING it, and four that are the CLI's own. The registry
|
|
565
|
+
is the list every door reads — the MCP server serves these commands, not a second copy of them.
|
|
566
|
+
|
|
567
|
+
A wrapped command passes the child's exit code through unchanged. The nine bins agree that 0 is
|
|
568
|
+
clean, 1 is a finding or a refusal and 2 is a run that could not be made at all, and a wrapper that
|
|
569
|
+
normalised a 2 into a 1 would have invented a finding out of a tool that never ran. The bin is found
|
|
570
|
+
by resolution — the repo's copy first, this package's second, and it prints which one it ran when it
|
|
571
|
+
falls back, because unseen version skew is the failure `geonosis-doctor` exists to catch.
|
|
572
|
+
|
|
573
|
+
- `init` writes the four config files an existing repo needs and refuses to overwrite any of them —
|
|
574
|
+
and when one exists it writes none of the others. It will not enable a rule whose options only the
|
|
575
|
+
repo can answer, because an enabled rule without its options refuses the whole run at load
|
|
576
|
+
(D-031); which rules those are is MEASURED by constructing each rule with no options, never listed.
|
|
577
|
+
- `sync` generates `AGENTS.md` and `.cursor/rules/*.mdc` from `CLAUDE.md` and the repo's skills —
|
|
578
|
+
one law, every tool (D-002) — deterministically, so `--check` is a gate. `--target` is handed to
|
|
579
|
+
the ledger's edges table rather than read a second time.
|
|
580
|
+
- `discover` reads the findings a repo's own config already produces into `fixShape` candidates and
|
|
581
|
+
writes nothing.
|
|
582
|
+
- `skills list|lock|audit` pins skills by content hash in during.day's lock shape, and the audit
|
|
583
|
+
fails on drift, on a skill nobody pinned, and on an executable file inside a skill directory — the
|
|
584
|
+
supply-chain class a hash over `SKILL.md` is blind to.
|
|
585
|
+
|
|
586
|
+
The plugin gains a `cli` layer in the kit's own `.oxlintrc.json`: it may read the presets, the parity
|
|
587
|
+
tool and the doctor's resolver, and no gate it fronts. Three walls and three doors are planted
|
|
588
|
+
against in `dogfood.test.ts`.
|
|
589
|
+
- 2637a99: **`@geonosis/mcp` — the same registry, served over MCP.** `geonosis-mcp` is a stdio server whose
|
|
590
|
+
tools are `@geonosis/cli` commands and nothing else, so an agent and the operator's terminal cannot
|
|
591
|
+
disagree: a test connects an in-memory client to the real server and asserts the `doctor` tool's
|
|
592
|
+
bytes are the bytes of `geonosis --json doctor`.
|
|
593
|
+
|
|
594
|
+
- **Generated `instructions`**, Midday's mechanic: the law's measured line count, the rules the
|
|
595
|
+
repo's `.oxlintrc.json` actually enables (never the ones it set to `off`), the counters the
|
|
596
|
+
baseline holds, and the read/write kind of every tool. A repo missing one of those files is told
|
|
597
|
+
so rather than given an invented number.
|
|
598
|
+
- **The root is the server's.** No tool takes a directory, so no client can point a gate at another
|
|
599
|
+
tree and read the answer as this repo's.
|
|
600
|
+
- **A write is two calls**, dielime's Trench gate, kept stateless: the first answers with a PREVIEW
|
|
601
|
+
and a token that is the hash of the tool id and the arguments; the second must carry `confirm:
|
|
602
|
+
true` and that token. A token for different arguments is refused rather than run, and nothing is
|
|
603
|
+
remembered between calls, so one client's preview is never another's authority.
|
|
604
|
+
|
|
605
|
+
The plugin gains an `mcp` layer in the kit's own `.oxlintrc.json` — it may import `cli` and nothing
|
|
606
|
+
else the kit ships — planted against in `dogfood.test.ts`.
|
|
607
|
+
- 75f613e: **`create-geonosis` — a third repo that passes its own gates on day one.** `create-geonosis <dir>
|
|
608
|
+
--runtime pnpm|bun --backend medusa|sagaflow-cf|none --ui tiered-shadcn|atoms-only|none` writes a
|
|
609
|
+
workspace with one catalog of shared versions, an `exports` map and no barrel in every package,
|
|
610
|
+
`typecheck` + `lint` + `test` in EVERY workspace (each asking vitest for its own JSON report), a law
|
|
611
|
+
under sixty lines, the five config files, a lefthook, a CI workflow with nothing switched off in it,
|
|
612
|
+
an empty skills lock and a `.claude/settings.json` that installs the plugin.
|
|
613
|
+
|
|
614
|
+
There is no default for the three answers that decide the shape of the tree. A default borrowed from
|
|
615
|
+
dielime or during.day would be one repo's stack hardcoded into every repo this ever writes.
|
|
616
|
+
|
|
617
|
+
- **The edges table is the one place the graph is written.** `docs/architecture.md` carries it, and
|
|
618
|
+
the scaffold runs `geonosis-ledger sync --target turbo` and `--target layer-walls` over it, so
|
|
619
|
+
turbo's boundaries and the linter's layers are generated from the same table and cannot drift.
|
|
620
|
+
- **The baseline starts at zero, except where zero would be a lie**: `lawLineCount` starts at the
|
|
621
|
+
measured length of the `CLAUDE.md` just written.
|
|
622
|
+
- **`lint-corpus/`** is a reach corpus in the new repo's own vocabulary, for the three rules whose
|
|
623
|
+
reach is repo-specific — `layer-walls`, `no-brand-names`, `plugin-route-namespaced` — because the
|
|
624
|
+
plugin's shipped corpus says `acme` and `layers/core` and is no evidence about anybody else.
|
|
625
|
+
|
|
626
|
+
`@geonosis/cli`'s `rulesFor` now takes a map of pre-built rule options instead of a brand, so `init`
|
|
627
|
+
and the scaffold answer the same question in one place. The plugin gains a `create` layer, walled
|
|
628
|
+
against every gate it configures; planted in `dogfood.test.ts`.
|
|
629
|
+
- f60a869: **The six defects the 1.0.0 reviews filed, before the release: two of them about being installable
|
|
630
|
+
at all, three about refusing honestly, one about counting a finding twice.**
|
|
631
|
+
|
|
632
|
+
`geonosis-review` read its command line OUTSIDE `main`'s try, so `--definitely-not-a-flag` reached
|
|
633
|
+
node uncaught: a stack trace and exit **1** — the one code the contract reserves for "the review was
|
|
634
|
+
read and REFUSED", so a caller branching on the code read a typo as a rejection. Parsing happens
|
|
635
|
+
inside now, every command-line mistake is 2 on one line, and the README's exit codes are a table
|
|
636
|
+
that says which is which (#50).
|
|
637
|
+
|
|
638
|
+
`@geonosis/testbed`'s `bin` named `dist/cli.js`, the only bin in the kit that pointed at a build
|
|
639
|
+
output. `pnpm install` links bins BEFORE anything is built, so a fresh clone printed
|
|
640
|
+
`Failed to create bin … ENOENT` and had no `geonosis-testbed` — including the copy `@geonosis/cli`
|
|
641
|
+
spawns for its `testbed` door. It has a committed `bin/geonosis-testbed.mjs` shim like every other
|
|
642
|
+
package, and `tooling/bins.test.ts` holds every `bin` entry to a file git carries (#51).
|
|
643
|
+
|
|
644
|
+
Two shims — `create-geonosis` and `geonosis-mcp` — were **committed at mode 644**. pnpm sets the
|
|
645
|
+
executable bit on the link it creates, so the mode was right everywhere except where it is kept, and
|
|
646
|
+
the flip only ever surfaced as an unrelated diff in somebody else's commit. Both are 755, and the
|
|
647
|
+
test reads the mode **git records**, which is the mode a clone gets (#52). Measured on this base:
|
|
648
|
+
`doctor` and `verify-arch`, the two the review named, were already executable.
|
|
649
|
+
|
|
650
|
+
`geonosis-testbed matrix` crashed on a malformed `testbed.matrix`: a block spelling `majors` as
|
|
651
|
+
`postgres` reached the renderer, which read `.length` off `undefined` — a TypeError at exit 1 for a
|
|
652
|
+
config error one line long. `readDbMatrix` checks the block first and refuses with **2**, naming
|
|
653
|
+
every wrong key at once (a missing one and an unknown one are two findings, and being told one at a
|
|
654
|
+
time means editing the file twice). Hand-rolled, not Zod: this package has no runtime dependencies
|
|
655
|
+
and a test that keeps it that way (#45).
|
|
656
|
+
|
|
657
|
+
`no-mock-db-in-integration` reported an imported handle **twice at one position**: an
|
|
658
|
+
ImportSpecifier's `imported` and its `local` are two nodes over the same span when nothing is
|
|
659
|
+
renamed. One report per place now — the corpus carries the fixture that shows it, and a finding
|
|
660
|
+
counted twice is a finding a ratchet counter banks twice (#46).
|
|
661
|
+
|
|
662
|
+
`geonosis-ledger sync --write` refused a config that did not already carry the fragment
|
|
663
|
+
(`"boundaries": no such key`), so the command whose job is to write the config could not write the
|
|
664
|
+
first one and hand-authoring the generated shape was the way in. It creates the key now — in a
|
|
665
|
+
`turbo.json` with no `boundaries`, in a config that has never named `layer-walls`, in a package with
|
|
666
|
+
no `no-restricted-imports`, and the `rules` object itself if that is missing too — and still writes
|
|
667
|
+
only that fragment. A rule is grown as `["error", { … }]`, the severity being the one thing no
|
|
668
|
+
generator can read off the table (#34).
|
|
669
|
+
- 35adf9a: Two rules and two presets. `no-unregistered-plugin-dir` (preset `integrations`) reports a plugin
|
|
670
|
+
directory the registry names nowhere — Midday ships eight such directories, dielime ships zero, and
|
|
671
|
+
a seam whose providers are flat files is out of scope because nothing matches. `router-schema-parity`
|
|
672
|
+
(preset `backend-trpc`) reports a router with no schema and a schema with no router, with
|
|
673
|
+
repo-root-relative directories because during.day's two halves are in different packages, and a
|
|
674
|
+
`mounts` option because its three apparent gaps are one deliberate arrangement.
|
|
675
|
+
- 13bd54d: Two option changes both earned by running the rules over the consumers read-only.
|
|
676
|
+
`no-unregistered-plugin-dir`'s `manifests` is now required with no default: the default matched
|
|
677
|
+
nothing in dielime, whose plugin directories hold `service.ts` and no index, so the rule reported a
|
|
678
|
+
clean tree it had never looked at. `emit-declares-attempts` gains `budgetHelpers` (empty by default,
|
|
679
|
+
so nothing changes until configured): all 54 of dielime's emit sites spell the bag
|
|
680
|
+
`options: retryableEvent(…)`, the one shape the rule declined to judge, so it had zero coverage of
|
|
681
|
+
the workspace whose landmine it was written from.
|
|
682
|
+
- c479255: The database matrix as a rendered workflow, and the rule both consumers already pass.
|
|
683
|
+
`geonosis-testbed matrix` renders `testbed.matrix` from `geonosis.json` into two GitHub Actions jobs
|
|
684
|
+
— because the source repo's embedded leg is real, tested and invisible, running only where the
|
|
685
|
+
connection variable is unset. It refuses majors with an empty connection string, which would run the
|
|
686
|
+
embedded cell under a container's name and go green twice against one database. `geonosis-testbed
|
|
687
|
+
strict` is the second door onto the report-not-exit-code runner. New rule
|
|
688
|
+
`no-mock-db-in-integration` in the new `testbed` preset: `vi.mock`/`jest.mock`/`mock.module` of a
|
|
689
|
+
configured database module inside a configured integration directory, plus `mockNames` for the
|
|
690
|
+
handle-through-a-helper shape no mock call can be seen in. `integrationDirs` and `dbModules` are
|
|
691
|
+
required with no defaults, because dielime's 106 integration files and during.day's whole tree
|
|
692
|
+
contain zero mocked databases — they are the green fixtures, and a default drawn from them would
|
|
693
|
+
report a clean tree it had never looked at.
|
|
694
|
+
|
|
695
|
+
## 0.6.0
|
|
696
|
+
|
|
697
|
+
### Minor Changes
|
|
698
|
+
|
|
699
|
+
- **No rule changed.** The version moves because the packages release as one fixed group. Measured,
|
|
700
|
+
not assumed: `geonosis-lint-parity --corpus` 0.5.0 vs 0.6.0 under both consumers' own configs
|
|
701
|
+
reports the identical finding count for every in-scope rule on both sides (38 rules under
|
|
702
|
+
dielime's config, 24 under during.day's), and the tree parity is 135 = 135 and 0 = 0 with nothing
|
|
703
|
+
lost, gained or reworded. `--expect-changed atom-no-deps` comes back UNPROVEN, which is the same
|
|
704
|
+
statement from the other direction. `proofs/022-parity-0.5.0-vs-0.6.0-dielime/`.
|
|
705
|
+
|
|
706
|
+
- **The last step of every bump is now `npx geonosis-doctor`.** The README's hand-run
|
|
707
|
+
`node --input-type=module -e '… p.meta.version'` stays as the explanation of what the tool does;
|
|
708
|
+
the tool asks it for every config in the tree, from each config's own directory, and lists every
|
|
709
|
+
copy resolution reaches. See [`@geonosis/doctor`](https://www.npmjs.com/package/@geonosis/doctor).
|
|
710
|
+
|
|
711
|
+
## 0.5.0
|
|
712
|
+
|
|
713
|
+
### Minor Changes
|
|
714
|
+
|
|
715
|
+
- **New rule `cell-no-cross-feature-organelles` (preset `frontend`).** `organelle-dependency` is scoped
|
|
716
|
+
to `organelles/` and `no-cross-feature-stores` only judges stores, so a CELL importing another
|
|
717
|
+
feature's ORGANELLE passed both while both stayed green. Option `sharedFeatures`, default
|
|
718
|
+
`['shared']` — the same option and the same default as `organelle-dependency`'s, because the two
|
|
719
|
+
rules draw one boundary from two sides. Measured on a consumer: three findings in two files.
|
|
720
|
+
|
|
721
|
+
**The corpus carries both spellings of every direction rule.** A direction rule judges where an
|
|
722
|
+
import lands, so `@/features/billing/x` and `../../billing/x` are one breach; the corpus previously
|
|
723
|
+
carried whichever spelling each rule's author happened to write. Fifteen fixtures added. No rule
|
|
724
|
+
changed.
|
|
725
|
+
|
|
726
|
+
**`geonosis-lint-parity --corpus --expect-changed <rule,…>`.** A changelog line that names a rule is
|
|
727
|
+
a claim; this makes it data the corpus judges, and exits 1 on a claim the corpus is silent about or a
|
|
728
|
+
change nobody claimed. Mandatory for any release whose notes name a rule.
|
|
729
|
+
|
|
730
|
+
**`testFailures` reads the runner's JSON report** with `report: "vitest-json"`, refusing when the
|
|
731
|
+
report is absent, unparsable, missing `numFailedTests`, or says `success: false` while naming none.
|
|
732
|
+
`Counter.probe` takes a list, so `--prove` proves each reading mode by name.
|
|
733
|
+
|
|
734
|
+
**`geonosis-ratchet --prove` proves the lock**, by running two children of the CLI over a slow
|
|
735
|
+
`--hold` and requiring the second to start after the first finished.
|
|
736
|
+
|
|
737
|
+
`@geonosis/ratchet`'s source is layered — `core/`, `counters/`, `report.ts`, `cli.ts` — with no
|
|
738
|
+
change to what `@geonosis/ratchet` exports or to a single line the CLI prints.
|
|
739
|
+
|
|
740
|
+
## 0.4.0
|
|
741
|
+
|
|
742
|
+
### Minor Changes
|
|
743
|
+
|
|
744
|
+
- The rule core, and the one widening that came with it (plan 018, K1.2).
|
|
745
|
+
|
|
746
|
+
Measured before any of it existed: **37 of 54 rules normalised the filename themselves**, in three
|
|
747
|
+
spellings, one of which fixed only the first separator; **21 parsed an import source themselves**,
|
|
748
|
+
each covering a different subset of the five ways a file can name another; **10 hand-wrote the same
|
|
749
|
+
re-export visitor**. None of that was decided — it was the order the rules were written in, and a
|
|
750
|
+
single fix to how a path is read had to be found rule by rule.
|
|
751
|
+
|
|
752
|
+
**`src/rules/lib/` is now the one door.** `paths.ts` (`normalise`, `fileOf`), `imports.ts`
|
|
753
|
+
(`sourceOf` over `import` · `export … from` · `export * from` · `import()` · `require()`,
|
|
754
|
+
`isTypeOnly`, `resolveRelative`), `visitors.ts` (`onImportSource` — one visitor set over all five
|
|
755
|
+
shapes — and `mergeVisitors`), `tiers.ts` (`tierOf`, `featureOf` beside `inTier`).
|
|
756
|
+
|
|
757
|
+
**The ten direction rules are declarations over one `directionRule()` factory**: a scope, a
|
|
758
|
+
forbidden predicate, the messages, the fixShape. Each file is 17–57 lines and holds its patterns and
|
|
759
|
+
its wording, nothing mechanical. Rule ids, message ids, message text, options and schemas are
|
|
760
|
+
unchanged, and parity over the kit's corpus and over a consumer tree reported lost 0 / gained 0 /
|
|
761
|
+
changed 0 at every step.
|
|
762
|
+
|
|
763
|
+
**Behaviour change — every direction rule now judges an import by where it LANDS.**
|
|
764
|
+
`cell-must-not-compose-cell` and `layer-walls` resolved a relative source against the importing file;
|
|
765
|
+
the other eight read the string, so `../../billing/organelles/totals` — which names no feature and no
|
|
766
|
+
tier until it is resolved — was invisible to them, and the same breach was a finding or not
|
|
767
|
+
depending on which rule looked. One resolver now, for all of them. In practice this reaches
|
|
768
|
+
`organelle-dependency` and `no-cross-feature-stores`: a relative climb into another feature's
|
|
769
|
+
organelle or store is a finding where it used to be silent. Measured on a consumer's tree
|
|
770
|
+
(`apps` + `packages`, its own config): 135 findings before, 135 after — its relative cross-feature
|
|
771
|
+
climbs are into `shared/`, which the `sharedFeatures` option permits.
|
|
772
|
+
|
|
773
|
+
`RULES.md` carries the named decision, the parity artefact path, and a Core column naming the
|
|
774
|
+
factory or helper behind each of the 56 rules.
|
|
775
|
+
|
|
776
|
+
## 0.3.0
|
|
777
|
+
|
|
778
|
+
### Minor Changes
|
|
779
|
+
|
|
780
|
+
- Three things a consumer asked for.
|
|
781
|
+
|
|
782
|
+
**`./discover-atoms` reads the element an atom wraps from its props type.** It used to take the
|
|
783
|
+
first lowercase JSX-looking token in the file, which on a real shadcn tree returns `string`,
|
|
784
|
+
`number` and `typeof` (a TypeScript generic reads as JSX to a regex) and `path` / `g` (SVG
|
|
785
|
+
internals), while missing `button` entirely — a Radix atom writes
|
|
786
|
+
`const Comp = asChild ? Slot.Root : 'button'` and never a `<button>` tag. Now:
|
|
787
|
+
`ComponentProps<'button'>`, `ComponentPropsWithoutRef` / `WithRef`,
|
|
788
|
+
`HTMLAttributes<HTMLSelectElement>` (the DOM interface mapped back to its tag) and
|
|
789
|
+
`React.ButtonHTMLAttributes`, then the Radix slot form, then a JSX scan filtered through an
|
|
790
|
+
allow-list of HTML tags. A `.stories.tsx` is no longer read as an atom. Exports and signatures
|
|
791
|
+
unchanged.
|
|
792
|
+
|
|
793
|
+
**`no-raw-html-atoms` is silent inside the atom tier**, where a raw element is the point of the
|
|
794
|
+
file, and takes a `scope` option to narrow it to named tiers. The default is every file EXCEPT the
|
|
795
|
+
atom tier — not an allow-list of the five upper tiers, which would quietly stop reporting every
|
|
796
|
+
file in no tier folder at all (an admin widget, a route segment). Its diagnostic now says "above
|
|
797
|
+
the atom tier" where it said "in molecules/compounds/cells/tissues".
|
|
798
|
+
|
|
799
|
+
**New rule `emit-declares-attempts`** (preset `backend-medusa`): Medusa's `emitEventStep` defaults
|
|
800
|
+
`attempts` to 1 and the worker treats only `attempts > 1` as a configured retry, so an emit with
|
|
801
|
+
`{ priority }` and no `attempts` is droppable while reading as deliberate. A non-literal `attempts`
|
|
802
|
+
passes — the rule cannot evaluate a shared constant and does not pretend to. `emitters` is an
|
|
803
|
+
option, default `["emitEventStep"]`.
|
|
804
|
+
|
|
805
|
+
## 0.2.1
|
|
806
|
+
|
|
807
|
+
Version only — the three `@geonosis/*` packages move as one fixed group, and nothing in this
|
|
808
|
+
package changed in 0.2.1. The release is `@geonosis/ratchet`'s: `--prove`, `--exclusive`, and
|
|
809
|
+
`typecheckErrors` refusing on an unbuilt workspace.
|
|
810
|
+
|
|
811
|
+
## 0.2.0
|
|
812
|
+
|
|
813
|
+
### Minor Changes
|
|
814
|
+
|
|
815
|
+
- Loose coupling, first slice — and a posture change that will break a config on purpose.
|
|
816
|
+
|
|
817
|
+
**`@geonosis/lint-parity` is a new package.** Findings parity, rule reach, the corpus-manifest
|
|
818
|
+
contract and the `geonosis-lint-parity` bin now live on their own. The tool knows no rule name and
|
|
819
|
+
no plugin: two oxlint configs and a tree are the whole input, and a corpus arrives as a directory
|
|
820
|
+
whose own `manifest.json` says which rules it speaks for. A parity tool welded to one rule plugin
|
|
821
|
+
can only ever check that plugin.
|
|
822
|
+
|
|
823
|
+
**BREAKING (plugin): the `geonosis-lint-parity` bin and the `./parity` export are gone.** Install
|
|
824
|
+
`@geonosis/lint-parity` and pass `--corpus node_modules/@geonosis/oxlint-plugin-biological-architecture/corpus`
|
|
825
|
+
— the plugin still ships the corpus, which is the part that is genuinely its own. `--corpus` now
|
|
826
|
+
requires its directory.
|
|
827
|
+
|
|
828
|
+
**BREAKING (plugin): a rule enabled without the options it needs refuses the run.** `no-brand-names`,
|
|
829
|
+
`no-raw-html-atoms`, `documents-share-one-table`, `document-sagas-are-generic`,
|
|
830
|
+
`step-opens-its-own-cell` and `layer-walls` throw from `create()` naming the missing option, instead
|
|
831
|
+
of returning an empty visitor. A consumer that enabled one bare will fail lint until it configures
|
|
832
|
+
the rule or turns it off — which is the point: one consumer ran `no-raw-html-atoms` at "error" for
|
|
833
|
+
months with no element→atom map, over 136 raw elements, and the gate was green. An enabled rule that
|
|
834
|
+
cannot fire is indistinguishable from compliance. A rule nobody enabled still does nothing.
|
|
835
|
+
|
|
836
|
+
**New rule `layer-walls`, new preset `tooling`.** A repo declares its own layers as data —
|
|
837
|
+
`{ name, paths, mayImport }` — and the rule enforces the direction between them: relative sources
|
|
838
|
+
resolved against the importing file first, `import()`, `require()` and both re-export forms visited,
|
|
839
|
+
externals not walls unless asked. The kit enables it on itself.
|
|
840
|
+
|
|
841
|
+
The plugin also ships type declarations now, for all three entries.
|
|
842
|
+
|
|
843
|
+
## 0.1.2
|
|
844
|
+
|
|
845
|
+
### Patch Changes
|
|
846
|
+
|
|
847
|
+
- Four findings from the 0.1.1 code review, plus a version that disagreed with itself.
|
|
848
|
+
|
|
849
|
+
**`--corpus` scopes its verdict to the rules its corpus can speak for.** It built the list of
|
|
850
|
+
configured rules from every rule id in both configs, but the shipped corpus only exercises this
|
|
851
|
+
plugin's. Any real consumer config also enables `eqeqeq`, `no-throw-literal` and
|
|
852
|
+
`typescript/no-explicit-any`, so `--corpus` reported `FAIL — fired under neither` on every
|
|
853
|
+
adoption. A corpus now declares its own scope in a `manifest.json` generated from the plugin's
|
|
854
|
+
rules export; rules it does not name are listed under "outside this corpus" with no verdict, and
|
|
855
|
+
neither the exit code nor `neither` considers them. `--corpus <dir>` points the run at another
|
|
856
|
+
plugin's corpus. Matching is on full `plugin/rule` ids on both sides, so
|
|
857
|
+
`typescript(no-unused-vars)` can never stand as evidence for somebody else's `no-unused-vars`.
|
|
858
|
+
|
|
859
|
+
**Parity identifies a finding by its place and its rule, not its wording.** `path:line:col` plus
|
|
860
|
+
`plugin(rule)`; message and severity are excluded. Diffing whole lines meant a rule whose message
|
|
861
|
+
was copy-edited read as one finding lost and one gained — exit 1, a stopped adoption over a
|
|
862
|
+
sentence. Those pair up in a new `messageChanged` list, reported under its own heading and written
|
|
863
|
+
to `changed.txt`, and never reach the exit code. Multiset semantics are unchanged.
|
|
864
|
+
|
|
865
|
+
**`oxlintRule` gets the cross-check the error and warning counters already had.** It counted
|
|
866
|
+
`(rule)` anywhere in the output, so a format that printed rule ids differently read as 0 and the
|
|
867
|
+
ratchet banked the "win" by rewriting the baseline down — and a quoted source line containing
|
|
868
|
+
`(a-rule-name)` counted as a finding. The count now comes off recognised finding lines only, and a
|
|
869
|
+
run with findings where none attributes itself readably is refused. The strict-config copy also
|
|
870
|
+
upgrades the `["off", { … }]` array form, which is how every rule with options is parked
|
|
871
|
+
mid-migration.
|
|
872
|
+
|
|
873
|
+
**The warnings-only window is documented and closable.** oxlint exits 0 when a run found only
|
|
874
|
+
warnings, so the counters' non-zero-exit backstop cannot catch an unrecognised format on such a
|
|
875
|
+
run. The README states this plainly and says why `--deny-warnings` is not the fix. New optional
|
|
876
|
+
counter param `expectFormat: "unix" | "agent" | "default"` refuses output in no known shape even on
|
|
877
|
+
a clean exit, once npm's own chatter is stripped. Unset is the default and changes nothing.
|
|
878
|
+
|
|
879
|
+
**The plugin reported `meta.version` 0.1.0 while shipping 0.1.1.** It is read from `package.json`
|
|
880
|
+
now, and a test asserts the two agree.
|
|
881
|
+
|
|
882
|
+
## 0.1.1
|
|
883
|
+
|
|
884
|
+
### Patch Changes
|
|
885
|
+
|
|
886
|
+
- The defects the first real adoption exposed.
|
|
887
|
+
|
|
888
|
+
- **The oxlint counters had never measured anything.** `oxlintErrors` and `oxlintWarnings` matched
|
|
889
|
+
`/: error /`, which is the shape of oxlint's compact `agent` format alone, while every config asks
|
|
890
|
+
for `--format=unix`. They now read all three shapes oxlint prints and cross-check the count
|
|
891
|
+
against the tool's own summary, raising `CounterError` rather than returning a number they cannot
|
|
892
|
+
corroborate — a blind counter reports 0 and lets the ratchet rewrite the baseline DOWN as a win.
|
|
893
|
+
- **`cell-must-not-compose-cell` fired on the barrel `cells-folder-index-is-barrel` demands.**
|
|
894
|
+
dielime's adoption reported 32 errors, every one a `cells/index.ts` re-exporting siblings, which
|
|
895
|
+
is the shape the other rule requires at the same severity. The two rules now read one shared list
|
|
896
|
+
of barrel filenames. A real cell importing or re-exporting another cell still fires.
|
|
897
|
+
- **`geonosis-lint-parity`.** The adoption proof — same tree, two configs, a findings diff — as a
|
|
898
|
+
bin instead of forty tool calls. Exits 1 when a finding the old config caught is missing under the
|
|
899
|
+
new one. `--corpus` answers the prior question first: does each configured rule fire anywhere at
|
|
900
|
+
all? The package now ships the reach corpus that makes that answerable, with a file for every one
|
|
901
|
+
of its 54 rules.
|
|
902
|
+
- **The ratchet shows what grew.** A regressed counter prints the last ten lines of its own command
|
|
903
|
+
output under the `<-- REGRESSED` line, so the report answers the question it used to raise.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geonosis/oxlint-plugin-biological-architecture",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.2",
|
|
4
4
|
"types": "./dist/index.d.ts",
|
|
5
5
|
"description": "Biological tier architecture as lint — the union of the dielime and during.day rule sets, shipped as presets.",
|
|
6
6
|
"keywords": [
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"files": [
|
|
39
|
+
"CHANGELOG.md",
|
|
39
40
|
"RULES.md",
|
|
40
41
|
"corpus",
|
|
41
42
|
"dist"
|
|
@@ -44,7 +45,7 @@
|
|
|
44
45
|
"oxlint": ">=1.77"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
|
-
"@geonosis/lint-parity": "2.
|
|
48
|
+
"@geonosis/lint-parity": "2.4.2"
|
|
48
49
|
},
|
|
49
50
|
"engines": {
|
|
50
51
|
"node": ">=22"
|