Repository navigation
Commit 79a046f
fix(lint): give the tenant-audit census refusals an exit to CI (#18534)
Fixes #18211
Clause-②: no
## What
Two halves, and the second is what makes the first landable.
1. **`scripts/check-tenant-audit-census.mjs` gains check C,
`censusRefusals()`** — it reads
`census.unledgered` and `census.staleLedgerRows` off the census it
already runs and routes
both to the gate's exit code.
2. **The two sites that check C then found are fixed at the receiver** —
`claimOrphanOrgRows`
and `claimOrgSeedOwnership` took `ql: any`, so they are given the narrow
`OrgScopingEngine`
type they actually call through.
## Why check C
`runCensus()` reports two failures about the **tree** rather than about
the artefacts:
`unledgered` (a write call site whose receiver is erased and that none
of the three placement
rules reaches) and `staleLedgerRows` (an `UNTYPED_RECEIVERS` row
matching no call).
The generator's own `main()` prints both and exits 1. But `lint.yml`
(lines 1995/1996 on this
tree) invokes the **gate**, never the generator — and the gate read
neither field. Measured on
this branch's base `1e496f979`:
| file | `unledgered` reads | `staleLedgerRows` reads | control:
`writeCallSites` reads |
|:---|---:|---:|---:|
| `scripts/check-tenant-audit-census.mjs` | **0** | **0** | 17 |
| `scripts/tenant-audit-census.mjs` | 3 | 3 | 5 |
The control is counted in the same file as each zero, so those zeros are
readings, not a grep
that failed to fire.
So the census could find an unplaceable receiver, say so to nobody, and
`Lint & Repo Gates`
stayed green — a defect arriving as **compliance**. This direction is
published:
`content/docs/permissions/tenant-audit-census.mdx` tells readers that a
receiver none of the
three place is an error, never a default.
The fix is the **read**, not a second run of the generator in the
workflow: that would walk
the same corpus and build the same AST twice for one verdict the gate
already holds in hand.
`.github/workflows/**` is untouched.
## Why the receivers are typed, and not ledgered
Check C found exactly two unplaced sites, both `ql: any` seed/back-fill
helpers writing
`schema.name` under `context: SYSTEM_CTX`:
- `packages/plugins/organizations/src/claim-org-seed-ownership.ts`
(`:52` the parameter, `:93` the write)
- `packages/plugins/organizations/src/claim-orphan-org-rows.ts` (`:61`
and `:106`)
Both match the already-ledgered
`plugin-security/src/claim-seed-ownership.ts` row word for
word, so they are engine writes and `engine: false` was never on the
table. What settles the
remaining choice is the ledger's own first line,
`scripts/tenant-audit-census.mjs` at
`origin/main` `1e496f979`, line 803: **"SHRINK-ONLY, and keyed by (file,
receiver) — never by
line"**. Adding two rows to a shrink-only ledger runs against its own
discipline. A typed
receiver needs no row at all, so `UNTYPED_RECEIVERS` is untouched and
`placedByLedger` stays
at **11**.
`OrgScopingEngine` follows `OrphanCleanupEngine` in `plugin-sharing`: a
narrow, locally
declared interface naming only the doors these functions call — `find`,
`update`, and an
optional `registry`. Optional on purpose, because "registry unavailable"
is a real, tested,
logged no-op path that the type has to be able to describe.
It is **package-private**, and that is load-bearing rather than
incidental. The census reads
the type declared at the **receiver**, in this source tree; it never
reads the package's public
entry. Exporting the interface from `index.ts` therefore bought the
placement nothing and only
widened a published surface — so `src/index.ts` exports exactly the nine
names it exported
before, byte for byte:
Taken by diffing the `^export` lines of that file as `git show` prints
them at `origin/main`
against the same lines at `HEAD`: **exit 0, no output, 9 lines on each
side.**
Fire control for that zero: the identical comparison run against the
commit that *did* carry
the export reports one added line — the `export type { OrgScopingEngine
}` re-export — and
exits 1. So the comparison can see an added export, and is reporting
none.
The emitted declarations still carry `interface OrgScopingEngine`
inline, so a consumer's call
resolves without ever naming it; it is simply absent from the shipped
export list.
### The caller had to state it too
Naming the parameter turned an invisible coupling into a type error:
`OrgScopingQuerySlot` in `organizations-plugin.ts` declared the three
members the plugin calls
itself — but the plugin also **forwards** that value to
`claimOrphanOrgRows`, which writes
through it. That is the finding, not an obstacle, and the slot now
extends `OrgScopingEngine`
to say so.
## Measurements
All commands run in a dedicated worktree on `origin/main@1e496f9`
after `pnpm install`.
**Acceptance 1 — the generator, on the day** (`node
scripts/tenant-audit-census.mjs`):
- exit code **1**
- `unledgered` — **2** entries, the two sites above
- `staleLedgerRows` — **0** entries (empty). Fire control for that zero:
the same `--json`
dump reports `unledgered.length = 2` and `unresolved.length = 2`, so the
reader is live.
- Population: 223 write call sites, 569 sources scanned.
**Acceptance 3 — both directions, measured twice.**
First on the gate-only commit, to show check C is real:
| direction | how | gate exit |
|:---|:---|---:|
| red on the day's sites | check C, before the receivers were typed |
**1** (2 x `[untyped-receiver]`) |
| same tree, no check C | `HEAD~1` | **0** |
| green once the sites are placed | one-shot ablation removing them from
the corpus | **0** |
Then again on the finished tree, which is the direction that matters
now:
| leg | gate exit | `[untyped-receiver]` lines |
|:---|---:|---:|
| finished tree | **0** | 0 |
| erase ONE receiver back to `ql: any` | **1** | **1**, naming the file
and line |
| same mutated tree, `censusRefusals()` neutered | 1 (drift/prose only)
| **0** |
The third row is the control: with check C disabled, the
untyped-receiver finding disappears
while the unrelated findings remain — so that red is unambiguously check
C's and nothing
else's. All mutations were one-shot, each proven on disk by counting
both the injected and
the removed string before any result was read, each script carrying
`trap restore EXIT INT TERM` with absolute paths, and each restored to a
blob hash equal to
`git rev-parse HEAD:PATH` with `git diff HEAD --stat` empty afterwards.
**The self-test was made to fail before its green was believed:**
neutering `censusRefusals()`
reds it with 4 of 24 cases failing by name; deleting the whole `census
refusals` battery block
reds it with `self-test battery "census refusals" DID NOT RUN — 0 cases
registered, 5 pinned`.
The new battery carries its own control (a census with neither an
unplaceable site nor a stale
row is **not** a finding), so its four positive cases cannot be passed
by a function that
simply reports everything handed to it.
**What the population did.** 223 to **225**, and both new sites read as
elevated. Worth
recording, because on the gate-only commit the population was **also**
223 with the two sites
unplaced: they were never counted at all — they were the hole in the
certified population, and
nothing on the way to a CI verdict said so. The generated region and the
audit ledger are
regenerated with `--write`; the page's eight hand-written prose figures
are restated by hand,
which `--write` does not do.
**Package verification:** `pnpm --filter @objectstack/organizations
typecheck` and `test` both
exit 0 — 8 test files, 108 tests. No test file changed: both fakes are
declared `const ql: any`,
which the narrowed parameter accepts. No in-repo package depends on
`@objectstack/organizations`,
so the consumer sweep is empty by construction rather than by omission.
**Clause-②:** the whole diff is a **narrowing**. Two exported function
parameters go from `any`
to an interface; `OrgScopingQuerySlot` is declared without `export` and
stays package-private;
and the package entry gains no name, measured above. Nothing relaxes an
accepted set and
nothing widens a published surface.
## Changeset
**A changeset is required and `skip-changeset` has been removed** — the
judgement flipped when
the diff grew past `scripts/`, and it was re-verified rather than
assumed.
`@objectstack/organizations` is not private and ships `files: ["dist",
"README.md",
"CHANGELOG.md"]`. After `pnpm --filter @objectstack/organizations
build`, the shipped
`dist/index.d.ts` declares `claimOrphanOrgRows(ql: OrgScopingEngine,
...)` where it previously
declared `ql: any`, and exports the new `OrgScopingEngine` type. Fire
control for that reading:
the same grep over the same file scores **0** for a symbol that should
not be there.
Bumped `minor`, not `patch`: runtime behaviour is unchanged, but a
consumer passing a value
that does not structurally offer `find` and `update` no longer compiles.
Such a consumer
already got `[]` and a warning from the existing guards, so nothing that
worked stops working
— the failure moves from run time to build time.
## Gates run
Derived from the diff with `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands`
and reconciled with `--ran`, every line recording its exit code: **105
derived, 105 run, 0
NOT-MEASURED, 0 UNRUN**, and the tool confirms that zero is derived from
the recorded codes
rather than claimed. `pnpm check:pm-dispatch-gates` (the bare ~1746-case
battery) exits **0**
in 814.8s, run detached and waited on with `tail --pid`.
Two families needed a built tree and say so themselves rather than
skipping
(`check:skill-examples`, `check:dual-build-cjs-loads` at `exit 3`,
"PREREQUISITE NOT MET");
both were re-run after a full `pnpm build` and both exit **0**.
One family reds locally and is **not** this diff: `pnpm
check:cross-package-test-inputs`
reports that `@objectstack/cli` descends from `packages/spec/dist/`
through a radius no
declared glob reaches, rooted in
`packages/cli/test/init-created-files-summary.e2e.test.ts` —
a file this PR does not touch. It reds only because a local
`packages/spec/dist/` exists.
Proven by moving that directory aside and re-running: **exit 0**, `29
package(s) read outside
themselves, all declared`. The `lint` job that runs this gate does not
build, so CI sees the
unbuilt state. Reported upward as a finding in its own right.
## 验收备注
卡面四条,原样照抄:
1. 先 `pnpm install`,再重跑 `node
scripts/tenant-audit-census.mjs`,读**今天的**退出码与 `unledgered` /
`staleLedgerRows` 的**实际内容**。⛔ 零要有发火对照。
2. 给这两个字段一条**到 CI 的出口**。⛔ 不许用「把 census 也加进 `lint.yml` 跑一遍」糊过去(那会让同一份 AST
走两遍),修法落在门禁文件内。
3. ⭐ **两个方向的对照都要**:门禁对今天这些站点**必须红**;去掉其中一个(或补上 ledger 条目)之后**必须绿**。⛔
只给一个方向不算量过。
4. ⛔ **不许为了让门禁绿而把那些站点写进 ledger 当既成事实** —— 先裁它们该不该被落位,再决定记不记。1 parent 582d3e5 commit 79a046f
8 files changed
Lines changed: 217 additions & 39 deletions
File tree
- .changeset
- content/docs/permissions
- docs/audits
- packages/plugins/organizations/src
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
106 | 106 | | |
107 | 107 | | |
108 | 108 | | |
109 | | - | |
| 109 | + | |
110 | 110 | | |
111 | 111 | | |
112 | 112 | | |
| |||
155 | 155 | | |
156 | 156 | | |
157 | 157 | | |
158 | | - | |
| 158 | + | |
159 | 159 | | |
160 | | - | |
161 | | - | |
| 160 | + | |
| 161 | + | |
162 | 162 | | |
163 | 163 | | |
164 | 164 | | |
| |||
175 | 175 | | |
176 | 176 | | |
177 | 177 | | |
178 | | - | |
| 178 | + | |
179 | 179 | | |
180 | 180 | | |
181 | 181 | | |
182 | | - | |
| 182 | + | |
183 | 183 | | |
184 | 184 | | |
185 | 185 | | |
| |||
191 | 191 | | |
192 | 192 | | |
193 | 193 | | |
194 | | - | |
| 194 | + | |
195 | 195 | | |
196 | | - | |
| 196 | + | |
197 | 197 | | |
198 | 198 | | |
199 | | - | |
| 199 | + | |
200 | 200 | | |
201 | 201 | | |
202 | 202 | | |
203 | 203 | | |
204 | | - | |
| 204 | + | |
205 | 205 | | |
206 | 206 | | |
207 | 207 | | |
208 | 208 | | |
209 | 209 | | |
210 | | - | |
| 210 | + | |
211 | 211 | | |
212 | 212 | | |
213 | 213 | | |
214 | 214 | | |
215 | 215 | | |
216 | 216 | | |
217 | 217 | | |
218 | | - | |
| 218 | + | |
219 | 219 | | |
220 | 220 | | |
221 | 221 | | |
| |||
232 | 232 | | |
233 | 233 | | |
234 | 234 | | |
235 | | - | |
| 235 | + | |
236 | 236 | | |
237 | 237 | | |
238 | 238 | | |
239 | | - | |
240 | | - | |
| 239 | + | |
| 240 | + | |
241 | 241 | | |
242 | | - | |
| 242 | + | |
243 | 243 | | |
244 | 244 | | |
Lines changed: 10 additions & 8 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
32 | | - | |
| 32 | + | |
33 | 33 | | |
34 | | - | |
| 34 | + | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
42 | | - | |
| 42 | + | |
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
| |||
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
55 | | - | |
| 55 | + | |
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
59 | | - | |
60 | | - | |
| 59 | + | |
| 60 | + | |
61 | 61 | | |
62 | | - | |
| 62 | + | |
63 | 63 | | |
64 | 64 | | |
65 | 65 | | |
66 | 66 | | |
67 | 67 | | |
| 68 | + | |
| 69 | + | |
68 | 70 | | |
69 | 71 | | |
70 | 72 | | |
| |||
Lines changed: 4 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
| 26 | + | |
| 27 | + | |
26 | 28 | | |
27 | 29 | | |
28 | 30 | | |
| |||
49 | 51 | | |
50 | 52 | | |
51 | 53 | | |
52 | | - | |
| 54 | + | |
53 | 55 | | |
54 | 56 | | |
55 | 57 | | |
56 | 58 | | |
57 | 59 | | |
58 | 60 | | |
59 | 61 | | |
60 | | - | |
| 62 | + | |
61 | 63 | | |
62 | 64 | | |
63 | 65 | | |
| |||
Lines changed: 4 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| 31 | + | |
| 32 | + | |
31 | 33 | | |
32 | 34 | | |
33 | 35 | | |
| |||
58 | 60 | | |
59 | 61 | | |
60 | 62 | | |
61 | | - | |
| 63 | + | |
62 | 64 | | |
63 | 65 | | |
64 | 66 | | |
65 | 67 | | |
66 | 68 | | |
67 | 69 | | |
68 | 70 | | |
69 | | - | |
| 71 | + | |
70 | 72 | | |
71 | 73 | | |
72 | 74 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
Lines changed: 11 additions & 5 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
5 | 6 | | |
6 | 7 | | |
7 | 8 | | |
| |||
66 | 67 | | |
67 | 68 | | |
68 | 69 | | |
69 | | - | |
70 | | - | |
71 | | - | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
72 | 79 | | |
73 | | - | |
| 80 | + | |
74 | 81 | | |
75 | | - | |
76 | 82 | | |
77 | 83 | | |
78 | 84 | | |
| |||
0 commit comments