fix(lint): split OWD_ALIAS_FIX provenance — three retired D4 aliases vs one wrong-layer fix-it - #16975
Conversation
…vs one wrong-layer fix-it `OWD_ALIAS_FIX` labelled all four of its keys `[ADR-0090 D4] Legacy alias → canonical fix-it mapping`, but D4 names three: "The legacy aliases `read`, `read_write`, `full` are removed from the zod enum". `public` was never an accepted `sharingModel` value in any shipped schema, so it has no retirement behind it and no ADR-0087 conversion or semantic-migration entry could exist for it — the stored population is zero by construction. Split the map into `OWD_RETIRED_ALIAS_FIX` (the three D4 aliases) and `OWD_WRONG_LAYER_FIX` (`public`), keep the union as `OWD_ALIAS_FIX` so every key still earns the same rule id, path and fix-it, and give the finding's message one shared provenance clause so it stops telling authors that `public` is a retired D4 alias. Also correct the five `packages/cli` serve e2e fixtures that declared `sharingModel: 'public'` — a value `ObjectSchema` refuses and `effectiveSharingModel` folds to `private`. They now declare `private`, which is the posture those apps already had. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
The new provenance clause named the sibling keys dotted, and this rule's receiver-coverage meta-test scans the module's code text for `receiver.key` reads without being able to tell one inside a message string from a real read — so `access.default` / `publicSharing.allowedAudiences` presented as undeclared reads off receivers that do not exist. Name them undotted instead and record why, rather than excusing them in the guard's PLUMBING set, which would have disarmed a real check for a spelling choice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
Comment rewrap only, no code change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 60501de656037f4be11fc016562c6ebcd0d1050e && git checkout 60501de656037f4be11fc016562c6ebcd0d1050e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 53d55dee20e541255696f379be278c4bb3610de7 9476b40662db35085be43bd8e56493dbff518600 && git checkout -B drift-repro 53d55dee20e541255696f379be278c4bb3610de7 && git merge --no-ff 9476b40662db35085be43bd8e56493dbff518600
node scripts/docs-audit/affected-docs.mjs --json 53d55dee20e541255696f379be278c4bb3610de7
|
Fixes #16517
Clause-②: no
This splits a provenance comment into two labelled maps and corrects five test fixtures.
No accept set moves, no export is added or removed, no enum member is added or removed.
⛔ Per the fence this card carries after its census: no ADR-0087 stored-row conversion
and no semantic-migration entry is written, and nothing under
packages/spec/src/conversions/**or
packages/spec/src/migrations/**is touched. Every valueOWD_ALIAS_FIXaccepted before isstill accepted, and still earns the same rule id, the same path and the same fix-it.
Why
This card shrank. A census ran on it and answered its central question:
publicwas neveran accepted
sharingModelvalue in any shipped schema, so the conversion registry's omission ofit is correct rather than a gap. Triage re-graded the card
Bug/p2→Task/p3and re-scopedit to two items, quoted verbatim from its ruling:
ADR-0090 D4 names three, quoted from the ADR in this tree
docs/adr/0090-permission-model-v2-concept-convergence.md, § D4 — OWD vocabulary: canonical four, aliases removed:Three.
OWD_ALIAS_FIXattributed four keys to that decision.① The provenance split —
packages/lint/src/validate-security-posture.tsBefore — one map, one label, four keys:
After — two maps with two histories, and their union under the old name so every read site is unchanged:
⛔ The
publicfix-it is KEPT, not tidied away — only its attribution was false. It catches areal authoring mistake, and the docblock now records why, measured rather than asserted: three
neighbouring keys on the same
ObjectSchemaaccept'public'legally, andsharingModel— thefourth — does not. Verified in this tree:
access.defaultz.enum(['public', 'private']).default('public')(ADR-0066)packages/spec/src/data/object.zod.ts:753publicSharing.allowedAudiencesz.array(z.enum(['public', 'link_only', 'signed_in', 'email']))packages/spec/src/data/object.zod.ts:2302effectiveSharingModel(runtime, off-schema)sharingModelz.enum(['private', 'public_read', 'public_read_write', 'controlled_by_parent'])— refuses'public'packages/spec/src/data/object.zod.ts:2216That neighbourhood is the attractor. The same function's tail is what makes it silent:
effectiveSharingModelfalls through toreturn 'private'for any value it does not recognise.The finding's message now carries one shared provenance clause instead of asserting D4 for all
four, used by both the
sharingModeland theexternalSharingModelbranch.The
publicfix-it still fires — a reading, not an assertionBoth runs call
validateSecurityPostureon the same input. Before is the published@objectstack/lint@17.3.0tarball'sdist/index.js, unmodified registry bytes; after is thisbranch's
dist/index.js.BEFORE (published 17.3.0):
AFTER (this branch):
Same rule id, same severity, same path, same fix-it. The control in the same run:
sharingModel: 'read'still reads
is a retired alias (ADR-0090 D4)in both, because it genuinely is one.② The five fixtures —
packages/cli/test/serve-*.e2e.test.tsRe-measured on this branch's base
97adce2fa:git grep -l "sharingModel: 'public'" -- packages/cli/test/returns exactly five — no sixth, no drift since the census. After the change the same grep over the
whole tree returns zero.
serve-mcp-capability-collision.e2e.test.tssharingModel: 'public'sharingModel: 'private'serve-mcp-stdio-answers.e2e.test.tssharingModel: 'public'sharingModel: 'private'serve-node-env-production-default.e2e.test.tssharingModel: 'public'sharingModel: 'private'serve-process-child-env.e2e.test.tssharingModel: 'public'sharingModel: 'private'serve-stdio-stdout-purity.e2e.test.tssharingModel: 'public'sharingModel: 'private'Why
privateand not the fix-it'spublic_read_write. These five objects are declaration-only —each name appears exactly once in its file, and nothing in any of the five tests reads the object.
effectiveSharingModelalready folds the unrecognised'public'to'private', so'private'is theposture these five apps already had: the correction is byte-for-byte behaviour-preserving. Writing
public_read_writewould instead have widened five test apps' sharing posture on a p3 tidiness card.privateis also whatsecurity-owd-unset's own hint calls the recommended default.Verification
Every verdict below is quoted from the tool's own verdict line, captured before any pipe. Final head
9476b40.pnpm --filter @objectstack/lint exec vitest runTest Files 102 passed (102)·Tests 3583 passed (3583)pnpm --filter @objectstack/lint buildcheck-dts-emitted: @objectstack/lint - 4/4 declared declaration file(s) present.pnpm --filter @objectstack/lint typecheckcheck:test-typecheck: OKOS_TEST_TIERS=nightly pnpm --filter @objectstack/cli exec vitest run --project integration test/serve-*.e2e.test.tsTest Files 5 passed (5)·Tests 16 passed (16)pnpm --filter @objectstack/cli exec vitest run --project unit test/vitest-tiers-partition.test.tsTest Files 1 passed (1)·Tests 22 passed (22)pnpm --filter @objectstack/cli typecheckcheck:test-typecheck: OKturbo run build --filter='@objectstack/lint...'·--filter='@objectstack/cli^...'·@objectstack/cli56 successful, 56 total(+ lint closure, + cli itself)pnpm lint(eslint . --no-inline-config)0, 1m29snode scripts/pm/dispatch-gates.mjs --commands→ all 56 run →--ranRun reconciliation — 56 derived, 56 run, 0 NOT-MEASURED, 0 UNRUN— 56/56 greenpnpm check:type-check-debtOK — 5 ledger entr(ies) re-measured in 114.7s, 55 raw tsc error(s) total, none above its recorded numberpnpm check:dual-build-cjs-loads104 published require entry point(s) across 67 package(s) loadpnpm check:nul-bytes, plus a hand scan of all 7 changed filesThe e2e run needed two prerequisites the harness declares itself, both satisfied rather than skipped:
the five files are nightly-tier, so they are collected only under
OS_TEST_TIERS=nightly(
packages/cli/vitest.config.ts§ THE NIGHTLY TIERS); and four of them spawnbin/run.js, whichresolves
servefrompackages/cli/dist, so the package itself had to be built(
test/helpers/serve-process.ts:81, the #12539 guard).One red found and fixed, worth naming
The first run of the lint suite went red on this rule's own receiver-coverage meta-test
(
validate-security-posture.test.ts, "covers every receiver in the source that is not explicitly excused").The new message string spelled the sibling keys dotted, and that scanner reads
receiver.keyout of themodule's code text without being able to tell a metadata read from a string literal — so
access.defaultand
publicSharing.allowedAudiencespresented as undeclared reads off receivers that do not exist.Fixed by naming those keys undotted in the message and recording why in the helper's docblock.
⛔ Deliberately not fixed by adding them to the guard's
PLUMBINGexcuse set, which would havedisarmed a real check for the sake of a spelling choice.
Changeset — both halves, for both packages
.changeset/owd-alias-fix-provenance-split.md,"@objectstack/lint": patch.@objectstack/cliisdeliberately not named. Both halves were measured for both packages, with controls:
files[]?dist?@objectstack/lintfiles: ["dist","README.md","CHANGELOG.md"]; the edit is insrc/, which is compiled intodistis not an OWD value and never wasis in this branch'sdist/index.jsanddist/index.cjs. Positive control on the published 17.3.0 tarball: the old textis a retired alias (ADR-0090 D4)is present in itsdist/index.jstwice — this rule's message strings demonstrably reach consumers@objectstack/clifiles: ["dist",…];tsconfig.build.jsonisinclude: ["src"],rootDir: "src"—test/is outside the emit program entirely, and nosrc/**file imports../test/(grep, empty), so the module-graph caveat thatexcludecannot cover does not apply@objectstack/cli@17.3.0tarball: 481 entries, 476 underpackage/dist/, zero matchingserve-.*e2e,/test/or\.test\.js— same instrument, positive (476) beside the negative (0)Half 1 alone would have given the same answer here, but it is not what the verdict rests on: the cli
reading is an empirical count over the real tarball, not an inference from the manifest.
验收备注
Observations for the reviewer. ⛔ None filed as cards, ⛔ none acted on in this diff.
receiver.keyinside a string literal from a real metadata read. Its own comments already record the sibling
case for import specifiers (Twelve more authoring rules crash on a non-record entry in stack.objects — five unguarded readers beyond the indexObjectGraph seam #15552, handled by stripping the import block); this is that same
limitation one step over. Not a defect and not a contract violation — the guard is deliberately a
dependency-free text reader — and the spelling workaround is documented at the call site.
Taker: the next author who wants to name a dotted key inside a message string in this file.
packages/cli's*.e2e.test.tsfiles are not collected by a plainpnpm --filter @objectstack/cli test— they are the nightly tier, selected byOS_TEST_TIERS.Designed (ci: e2e and live tiers move to a nightly run on main; PR and queue runs keep unit, integration and conformance (maintainer-directed, part B tiering) #16455), recorded here only because it is easy to read a green default run as covering them.
Taker: none — this is a note about how to read a run, not a change request.
owd-legacy-read-aliasesconverts a nestedobject.security.sharingModel, andeffectiveSharingModelreadsschema?.sharingModel ?? schema?.security?.sharingModel, whileowdOf's header in this file statesObjectSchemadeclares nosecuritykey and refuses it.Plausibly deliberate (the conversion targets pre-D4 stored rows), and no behaviour was measured wrong.
Taker: none identified.
⛔ #16109 is out of scope and untouched: a lint-reachability card whose PR is comment-only. It neither
caused nor covers this.
Generated by Claude Code