You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 9bd631f
Browse filesBrowse the repository at this point in the historyBrowse files
fix(cli): os lint runs the per-package author-time rule pass the other two doors already ran (#18813)
Fixes#18778
Clause-②: yes (narrowing)
`os lint --strict` now exits 1 on a project it exited 0 for. A
newly-refused input is **exhibited** below, not reasoned about. ⚠️#18677's `Clause-②: no` rested on "no newly-refused input could be
exhibited" measured on `os validate`'s door; ⛔ it does not transfer, and
on this door it is false. Verified through `readClause2Line` from
`scripts/pm/check-clause2-carriers.mjs`, ⛔ not an ad-hoc regex.
`os build` has run the author-time rule table **twice** since #16611 —
once over the union-folded stack, then once per `artifactPackages(…)`
entry with `packageBodyAsStack(…)` as resolution context, de-duplicated
against the union run. #18769 gave `os validate` the second half. `os
lint` ran the union fold and stopped, so by `compile.ts`' own
description the survivors of that pass — *"exactly the set the union
could not see"* — were findings `os build` reported and `os lint`
**structurally could not**. Same false-clean direction, on the fastest
of the three doors. All three now call the one shared pass.
## ⭐ The trap, confirmed rather than relayed
`lint.ts` **does** import `artifactPackages` and `packageBodyAsStack` —
at `packages/cli/src/commands/lint.ts:18`, used at `:542` and `:546`.
Opening the hits is what settles it: they feed `os lint`'s **own**
intra-package duplicate-name advisory (#17821), never the shared rule
table. ⛔ A count is not a reading. This door is also the one place in
the tree where "the loop is already written here, write the second one
beside it" is the cheap move, so the seam pin ratchets the call
**count** rather than its absence — the sibling doors'
`not.toMatch(/packageBodyAsStack\(/)` assertion ⛔ cannot be transplanted
here.
## Should `os lint` run it? — settled from the repo's own statements, ⛔
not assumed
The dispatch fenced this as a contract question with a STOP arm. It did
not need the stop arm; four statements settle it, and the one statement
that reads the other way is **false on the tree**.
**For, and they are this door's own words:**
1. `packages/lint/src/authoring-rules.ts:38-44` — *"Any rule that can
emit `error` runs on all three commands. A gate is only as strong as the
weakest command an author or CI happens to run, so a gating rule with
partial coverage is not a stricter check — it is a coin flip"*, and
*"the three commands are three doors in ONE wall"*.
2. `packages/cli/src/commands/lint.ts:652-656` — the union fold landed
**here**, for this exact direction: *"the whole table reported nothing
and `os lint` returned no finding of any severity for a project `os
build` refuses"*. That is the same sentence as this card, one layer out;
the fold fixed which COLLECTIONS the table sees, this fixes which STACKS
it is run over.
3. `packages/cli/src/commands/lint.ts:604-611` — `os lint`'s
pre-registry hand-wired subset was removed because *"a pre-flight that
disagrees with the gate in both directions is worse than no
pre-flight"*.
4. `packages/cli/test/validate-build-gate-parity.test.ts` already holds
the INPUT equal across all three doors (`all three authoring commands
hand the rule table the union-folded stack`), and its #12297 note states
the governing rule for exactly this shape: *"A guard that enumerates a
subset of the class it describes reports green for the members it
forgot. The list is the class now, not the card."*
**Against — one statement, and ⚠️ it was already FALSE when it was
written:**
`validate-build-gate-parity.test.ts`' `PARITY_COMMANDS` docblock read
*"`lint.ts` … emits no artifact and **runs no artifact-level gate**, so
it is not part of the parity question."* Measured at `7572329069`:
`lint.ts:13` imports and `lint.ts:881` **calls** `collectAndLintDocs` —
a name in that same file's `SHARED_NON_REGISTRY_GATES` roster, i.e. an
artifact-level gate by the file's own classification. The clause is
corrected in this PR rather than deleted, because it is the one sentence
in this repository that could be read as "`os lint` is exempt from the
artifact-level gates" and this card had to settle exactly that. What
excludes `lint.ts` from `PARITY_COMMANDS` is the ARTIFACT (that file's
question is `os validate` as `os build`'s read-only superset), not the
gates. ⛔ `PARITY_COMMANDS` is deliberately NOT widened — that would
re-open every ledger classification against a third file in one stroke.
⇒ the roster prediction in the dispatch resolved the other way:
`SHARED_NON_REGISTRY_GATES` and its `it.each('both commands run %s')`
did **not** move. They are keyed to `PARITY_COMMANDS`, which this PR
leaves at two files, so nothing on that roster reddened. What moved is
the docblock the roster is read through.
## The measurement
Fixture `CONFIG_FLIP` (shipped as the pin's own fixture): `core` owns
`pp_account`, the sibling `orders` package owns the view that displays
`pp_account.industry`. Judged as one flattened union the field has a
consumer and nothing is raised; judged per package, `core` declares a
field nothing in `core` reads. ⇒ **the union run is clean and the
per-package run is not** — the only shape that can tell "the doors
agree" from "the doors agree because neither looked".
At `origin/main` `7572329069`:
| | `os build --json` | `os lint --json` | `os lint --json --strict` |
|---|---|---|---|
| **before** | warnings **1** (per-package only) | total **0**, exit
**0** | **exit 0**, `failing: 0` |
| **after** | warnings **1**, unchanged | total **1**, exit **0** |
**exit 1**, `failing: 1` |
On the sibling two-package fixture from #18769 (`CONFIG_MULTI`, where
the survivor is the positional-key ECHO): `os build` 3 warnings · `os
validate` 3 · `os lint` **2 before, 3 after** — the three doors now
report one set.
**CONTROL** — a single-package project (no `packages[]`): `packageCount`
0, the pass is skipped, and `os lint` reports zero per-package findings
before and after. Without it "the doors agree" is satisfied by three
commands that all looked at nothing, which is exactly how this gap
survived two cards' worth of parity files.
⛔ **The DEFAULT face is not claimed to move.** No `error`-severity
per-package-only finding was exhibited: two probe shapes were tried — a
cross-package field consumer and a cross-package page reference
(`nav-target-unresolved`) — and both land at `warning`. That half is
**NOT MEASURED**, and the `--strict` pin says so in its own comment
rather than asserting an unmeasured default-face flip.
The severity face is `os lint`'s own and unchanged: one mapping
expression now serves both halves (`info` → `suggestion`, everything
else verbatim), so an `error` fails the run, a `warning` fails it only
under `--strict`. A per-package `error` is one `os build` ALREADY
refuses, so this narrows `os lint` to the bar the command that ships
holds and never past it.
## Red/green, both legs, from the committed state
`packages/cli/src/commands/lint.ts` alone restored to its `7572329069`
blob (`git show 7572329:…`), everything else at HEAD. Marker count on
disk `2 → 0` **verified before running**; restored with `git checkout
HEAD -- <path>`, `git diff HEAD` empty and `git hash-object` back to
`5b2eb1af023348865d06a4ab7355708af801db43` after each leg.
| pin | ablated | at HEAD |
|---|---|---|
| `test/lint-per-package-authoring-seam.test.ts` (unit) | **2 failed**,
4 passed | **6 passed** |
| `test/lint-per-package-authoring-parity.test.ts` (integration) | **2
failed**, 3 passed | **5 passed** |
The 4 and 3 that pass in both legs are deliberately door-independent
(the pass's own three-door equality, the `findings` ∪ split identity,
the single-package control, the `packageBodyAsStack` count ratchet) —
they are not measuring `lint.ts`, and a file where everything reddened
would mean the pins were coupled to the fix rather than to the
behaviour. The ablated integration failure is the narrowing itself:
`expected +0 to be 1` on `failing`.
## Tier, read from the config's own answer both directions
`vitest list --filesOnly` per project, ⛔ not the predicate applied by
hand:
| file | `--project unit` | `--project integration` |
|---|---|---|
| `lint-per-package-authoring-seam.test.ts` | **1** | 0 |
| `lint-per-package-authoring-parity.test.ts` | 0 | **1** |
| `validate-per-package-authoring-seam.test.ts` (control) | **1** | 0 |
| `validate-per-package-authoring-parity.test.ts` (control) | 0 | **1**
|
Populations non-vacuous: 214 unit files / 50 integration files. Neither
new file constructs `new ObjectQL(`, so neither carries the KERNEL
signal, and no existing file changed tier (no existing test file's
source was touched except `validate-build-gate-parity.test.ts`,
comment-only, which stays unit).
## What ran
- `pnpm --filter '@objectstack/cli^...' build` — dependency closure,
exit 0.
- `pnpm --filter @objectstack/cli exec vitest run --project unit` —
**214 files / 3047 tests, all pass**.
- `pnpm --filter @objectstack/cli exec vitest run --project integration`
over the **declared-narrowed** set of 9 files this diff can reach
(`authoring-rule-command-parity`, `union-fold-command-parity`,
`validate-per-package-authoring-parity`,
`lint-per-package-authoring-parity`, `info-detail-package-fold`,
`lint-eval-generator-refusal-separator`, `emit-json-pipe`,
`adr-0048-app-split`, `nav-contribution-groups.package-id`) — **9 files
/ 53 tests, all pass**. The other 41 integration-tier files are
DB/migration/secret/generate suites that never run the authoring rule
table; ⇒ declared to CI.
- `pnpm --filter @objectstack/cli typecheck` — exit 0, including
`check:test-typecheck`. Both new test files are really in that program:
`tsc -p tsconfig.test.json --listFiles` names them (2 of 2), so the
claim is measured, not assumed.
- Gate families derived from the merge base by
`scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (⛔ not
a hand-made path list): **61 derived, 58 run green, 3 NOT MEASURED, 0
UNRUN**, reconciled through `--ran` with an exit code recorded per
family. The three are `check:dual-build-cjs-loads`, `check:i18n`,
`check:i18n-coverage`, each **exit 3 = PREREQUISITE NOT MET** (packages
with no `dist/` in this worktree) — ⛔ read as NOT MEASURED, never as a
pass. `check:i18n-walk-parity` also refused at first for the same reason
and went green after `pnpm --filter @objectstack/cli build`.
- `eslint --no-inline-config` narrowed to the 5 changed source files:
**0 errors, 0 warnings**, file count **5** read from `--format json`,
not guessed. The narrowing is a measurement because the population is
read from eslint's own config (`files:
['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']` plus per-directory overlays)
and because `eslint.config.mjs:326-329` states this repo *"never enables
type-aware linting (no `parserOptions.project`, no typed
`@typescript-eslint` rules) for ANY file"* — so this diff cannot move
the verdict on any file it does not touch. The repo-wide `pnpm lint` run
is CI's.
## Changeset
`minor` on `@objectstack/cli` with a `**BREAKING**` banner and the
ADR-0087 disposition `not-required (no-migration-prescription)` —
`check:adr-0087-registration` reads the arm and prints it back:
`[BREAKING+clause-②-narrowing]`. ⛔ Not `skip-changeset`: `packages/cli`
is released and both edited sources ship in its `files[]`. `major` is
out of the launch window, which `check:changeset-no-major` confirms
green.
## Acceptance notes
Noted, not filed — nothing here is a reproducible defect, a
declared-contract violation, or a metadata trap:
1. ⚠️ **#18769's own `Clause-②: no` is falsified by this card's fixture,
and the falsification is MEASURED.** `os validate --strict` exits 1 when
`warnings > 0` (`validate.ts:715`). On `CONFIG_FLIP`, with `validate.ts`
alone restored to `095c7f60ae^` it exits **0** (0 warnings); at HEAD it
exits **1** (1 per-package warning). Restore verified by blob hash. ⇒ a
newly-refused input **could** be exhibited on that door too; what was
missing was a fixture whose union run is clean, and #18769's fixture (an
ECHO of a union finding) structurally cannot be one. ⛔ Not corrected
here — #18769 is merged, and rewriting a landed declaration is not this
card's act. Carrier: the PM seat. Dedupe words: `18769 clause-②
narrowing falsified` · `validate --strict per-package warning exit` ·
`union-clean per-package fixture`.
2. The `findings` member added to `runPerPackageAuthoringRules` exists
because `os lint` has no severity split to re-join; re-joining `errors`
then `advisories` at that door would put all errors before all
advisories and silently re-order a list the union run above it produces
in rule order. Stated in the member's own docblock so the next door does
not have to re-derive it.
3. `packages/cli/vitest-tiers.ts` treats `new ObjectQL(` as a KERNEL
signal, and a per-package pin is exactly the kind of test that grows one
later. Nothing to file; the tier table above is the reading that would
catch it.
⛔ Untouched, each with its own card: **#18779** (the positional
de-duplication key — changing it changes what `os build` reports) and
**#18780** (`os build`'s text face counting advisories it never prints).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3)_
---
_Generated by [Claude Code](https://claude.ai/code)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
`os lint` runs the per-package author-time rule pass the other two doors already ran
6
+
7
+
`os build` has run the author-time rule table a second time, once per
8
+
`packages[]` entry with that package's body as the stack and the artifact's own
9
+
`packages[]` as resolution context, since #16611; `os validate` joined it in
10
+
#18677. `os lint` ran the union fold and stopped, so every finding that pass
11
+
produces — "exactly the set the union could not see", in the build command's own
12
+
words — was reported by the command that ships and invisible on the fastest of
13
+
the three doors. All three now call the one shared pass.
14
+
15
+
Measured on a two-package project whose union run is clean and whose per-package
16
+
run is not (one package owns an object, a sibling package owns the view that
17
+
displays its field):
18
+
19
+
|| before | after |
20
+
|---|---|---|
21
+
|`os build --json`| warnings 1 | warnings 1 |
22
+
|`os lint --json`| total 0, exit 0 | total 1, exit 0 |
23
+
|`os lint --json --strict`| exit 0 | exit 1 |
24
+
25
+
**BREAKING** — `os lint --strict` can now fail a project it passed before. A
26
+
per-package finding is a finding this door could not see, `--strict` is
27
+
documented as "treat warnings as errors", and the verdict moves with it. The
28
+
default face is unchanged in the measurement above, and the severity mapping is
29
+
`os lint`'s own: an `error` fails the run, a `warning` fails it only under
30
+
`--strict`, an `info` stays a suggestion. Nothing is refused here that `os build`
31
+
does not already refuse, so the pre-flight is narrowed to the bar the command
32
+
that ships already holds and never past it. A run that must keep its old verdict
33
+
drops `--strict`; a project that wants to keep it fixes what the pass reports,
34
+
which is the same thing `os build` has been reporting all along.
35
+
36
+
Clause-②: yes (narrowing)
37
+
38
+
<!-- adr-0087: not-required (no-migration-prescription) Nothing an author writes changes: no spec key, export, config field or payload key is removed, renamed or added. What moved is which stacks one CLI command's existing rule table is run over, so `objectstack migrate meta` has nothing to rewrite and the ledger has nothing to record. -->
0 commit comments