Skip to content

fix(lint): split OWD_ALIAS_FIX provenance — three retired D4 aliases vs one wrong-layer fix-it - #16975

Merged
zhuangjianguo merged 3 commits into
mainfrom
claude/issue-16517-owd-alias-fix-provenance-split
Sep 8, 2026
Merged

fix(lint): split OWD_ALIAS_FIX provenance — three retired D4 aliases vs one wrong-layer fix-it#16975
zhuangjianguo merged 3 commits into
mainfrom
claude/issue-16517-owd-alias-fix-provenance-split

Conversation

@claude

@claude claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

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 value OWD_ALIAS_FIX accepted before is
still 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: public was never
an accepted sharingModel value in any shipped schema, so the conversion registry's omission of
it is correct rather than a gap. Triage re-graded the card Bug/p2Task/p3 and re-scoped
it to two items, quoted verbatim from its ruling:

① 更正 OWD_ALIAS_FIX 的出处:把「已退役别名(read / read_write / full)」与「写错层/拼错的 fix-it(public)」拆开。保留有用的 fix-it,停止让这张映射表断言一段不存在的历史。

② 更正五个 packages/cli/test/serve-*.e2e.test.ts fixture … ⭐ 五个文件由五个不同的 PR 在 2026-08-12 ~ 08-23 写下,全都在 D4 于 07-09 关闭枚举之后 ⇒ 这是一个复制粘贴吸引子,不是五次笔误。

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:

The legacy aliases read, read_write, full are removed from the zod enum — authoring rejects them with a fix-it message; no lenient parse, no normalization layer.

Three. OWD_ALIAS_FIX attributed four keys to that decision.

① The provenance split — packages/lint/src/validate-security-posture.ts

Before — one map, one label, four keys:

/** [ADR-0090 D4] Legacy alias → canonical fix-it mapping. */
const OWD_ALIAS_FIX = {
  read: 'public_read',
  read_write: 'public_read_write',
  full: 'public_read_write',
  public: 'public_read_write',
};

After — two maps with two histories, and their union under the old name so every read site is unchanged:

const OWD_RETIRED_ALIAS_FIX = { read: , read_write: , full:  };
const OWD_WRONG_LAYER_FIX = { public: 'public_read_write' };
const OWD_ALIAS_FIX = { ...OWD_RETIRED_ALIAS_FIX, ...OWD_WRONG_LAYER_FIX };
// each is annotated `Record of string to string` in the source; that annotation is
// spelled out in words here because an angle-bracket fragment is not safe in a body

The public fix-it is KEPT, not tidied away — only its attribution was false. It catches a
real authoring mistake, and the docblock now records why, measured rather than asserted: three
neighbouring keys on the same ObjectSchema accept 'public' legally
, and sharingModel — the
fourth — does not. Verified in this tree:

key declaration file:line
access.default z.enum(['public', 'private']).default('public') (ADR-0066) packages/spec/src/data/object.zod.ts:753
publicSharing.allowedAudiences z.array(z.enum(['public', 'link_only', 'signed_in', 'email'])) packages/spec/src/data/object.zod.ts:2302
effectiveSharingModel (runtime, off-schema) returns `'private' 'read'
sharingModel z.enum(['private', 'public_read', 'public_read_write', 'controlled_by_parent'])refuses 'public' packages/spec/src/data/object.zod.ts:2216

That neighbourhood is the attractor. The same function's tail is what makes it silent:
effectiveSharingModel falls through to return '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 sharingModel and the externalSharingModel branch.

The public fix-it still fires — a reading, not an assertion

Both runs call validateSecurityPosture on the same input. Before is the published
@objectstack/lint@17.3.0 tarball's dist/index.js, unmodified registry bytes; after is this
branch's dist/index.js.

BEFORE (published 17.3.0):

rule=security-owd-alias severity=error path=objects[0].sharingModel
message: sharingModel 'public' is a retired alias (ADR-0090 D4). The runtime fails CLOSED to 'private' on unknown values, so this object is NOT writable org-wide.
hint:    Replace with the canonical value: sharingModel: 'public_read_write'.

rule=security-owd-alias severity=error path=objects[2].externalSharingModel
message: externalSharingModel 'public' is a retired alias (ADR-0090 D4).
hint:    Replace with the canonical value: externalSharingModel: 'public_read_write'.

AFTER (this branch):

rule=security-owd-alias severity=error path=objects[0].sharingModel
message: sharingModel 'public' is not an OWD value and never was — ADR-0090 D4 retired 'read', 'read_write' and 'full', not this. 'public' is legal on the neighbouring keys 'access' (its 'default') and 'publicSharing' (its 'allowedAudiences'), just not on this one. The runtime fails CLOSED to 'private' on unknown values, so this object is NOT writable org-wide.
hint:    Replace with the canonical value: sharingModel: 'public_read_write'.

rule=security-owd-alias severity=error path=objects[2].externalSharingModel
message: externalSharingModel 'public' is not an OWD value and never was — ADR-0090 D4 retired 'read', 'read_write' and 'full', not this. 'public' is legal on the neighbouring keys 'access' (its 'default') and 'publicSharing' (its 'allowedAudiences'), just not on this one.
hint:    Replace with the canonical value: externalSharingModel: 'public_read_write'.

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.ts

Re-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.

file line before after
serve-mcp-capability-collision.e2e.test.ts 125 sharingModel: 'public' sharingModel: 'private'
serve-mcp-stdio-answers.e2e.test.ts 108 sharingModel: 'public' sharingModel: 'private'
serve-node-env-production-default.e2e.test.ts 273 sharingModel: 'public' sharingModel: 'private'
serve-process-child-env.e2e.test.ts 148 sharingModel: 'public' sharingModel: 'private'
serve-stdio-stdout-purity.e2e.test.ts 105 sharingModel: 'public' sharingModel: 'private'

Why private and not the fix-it's public_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.
effectiveSharingModel already folds the unrecognised 'public' to 'private', so 'private' is the
posture these five apps already had: the correction is byte-for-byte behaviour-preserving. Writing
public_read_write would instead have widened five test apps' sharing posture on a p3 tidiness card.
private is also what security-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.

what command result
lint package suite pnpm --filter @objectstack/lint exec vitest run Test Files 102 passed (102) · Tests 3583 passed (3583)
lint package build pnpm --filter @objectstack/lint build check-dts-emitted: @objectstack/lint - 4/4 declared declaration file(s) present.
lint package typecheck pnpm --filter @objectstack/lint typecheck check:test-typecheck: OK
the five e2e fixtures OS_TEST_TIERS=nightly pnpm --filter @objectstack/cli exec vitest run --project integration test/serve-*.e2e.test.ts Test Files 5 passed (5) · Tests 16 passed (16)
cli tier partition pin pnpm --filter @objectstack/cli exec vitest run --project unit test/vitest-tiers-partition.test.ts Test Files 1 passed (1) · Tests 22 passed (22)
cli typecheck pnpm --filter @objectstack/cli typecheck check:test-typecheck: OK
dependency closures turbo run build --filter='@objectstack/lint...' · --filter='@objectstack/cli^...' · @objectstack/cli 56 successful, 56 total (+ lint closure, + cli itself)
repo eslint, whole repo, not narrowed pnpm lint (eslint . --no-inline-config) exit 0, 1m29s
derived gate families node scripts/pm/dispatch-gates.mjs --commands → all 56 run → --ran Run reconciliation — 56 derived, 56 run, 0 NOT-MEASURED, 0 UNRUN56/56 green
type-check ratchet pnpm check:type-check-debt OK — 5 ledger entr(ies) re-measured in 114.7s, 55 raw tsc error(s) total, none above its recorded number
dual-build ratchet pnpm check:dual-build-cjs-loads 104 published require entry point(s) across 67 package(s) load
control bytes pnpm check:nul-bytes, plus a hand scan of all 7 changed files 0 in every file; instrument verified against a VT/BEL positive control

The 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 spawn bin/run.js, which
resolves serve from packages/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.key out of the
module's code text without being able to tell a metadata read from a string literal — so access.default
and publicSharing.allowedAudiences presented 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 PLUMBING excuse set, which would have
disarmed 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/cli is
deliberately not named. Both halves were measured for both packages, with controls:

package half 1 — path inside files[]? half 2 — changed text in the published dist? verdict
@objectstack/lint files: ["dist","README.md","CHANGELOG.md"]; the edit is in src/, which is compiled into dist YES. The added runtime message text is not an OWD value and never was is in this branch's dist/index.js and dist/index.cjs. Positive control on the published 17.3.0 tarball: the old text is a retired alias (ADR-0090 D4) is present in its dist/index.js twice — this rule's message strings demonstrably reach consumers patch changeset
@objectstack/cli files: ["dist",…]; tsconfig.build.json is include: ["src"], rootDir: "src"test/ is outside the emit program entirely, and no src/** file imports ../test/ (grep, empty), so the module-graph caveat that exclude cannot cover does not apply NO. Read from the actually published @objectstack/cli@17.3.0 tarball: 481 entries, 476 under package/dist/, zero matching serve-.*e2e, /test/ or \.test\.js — same instrument, positive (476) beside the negative (0) publishes nothing; not named

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.

  • noted, not filed: the receiver-coverage meta-test's scanner cannot distinguish a receiver.key
    inside 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.
  • noted, not filed: packages/cli's *.e2e.test.ts files are not collected by a plain
    pnpm --filter @objectstack/cli test — they are the nightly tier, selected by OS_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.
  • noted, not filed: the census's third observation still stands and is untouched here —
    owd-legacy-read-aliases converts a nested object.security.sharingModel, and
    effectiveSharingModel reads schema?.sharingModel ?? schema?.security?.sharingModel, while
    owdOf's header in this file states ObjectSchema declares no security key 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

claude Bot and others added 3 commits September 8, 2026 19:57
…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
@github-actions github-actions Bot added the size/s label Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 10 documentable anchor(s).

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/objects.mdx (via read_write (literal, a string literal in owdAliasProvenance))
  • content/docs/permissions/authorization.mdx (via validateSecurityPosture (symbol, a top-level function))
  • content/docs/permissions/permissions-matrix.mdx (via read_write (literal, a string literal in owdAliasProvenance))
  • content/docs/permissions/sharing-rules.mdx (via read_write (literal, a string literal in owdAliasProvenance))
  • content/docs/permissions/system-context.mdx (via publicSharing (literal, a string literal in owdAliasProvenance))
  • content/docs/protocol/objectql/security.mdx (via allowedAudiences (literal, a string literal in owdAliasProvenance), link_only (literal, a string literal on a changed line), publicSharing (literal, a string literal in owdAliasProvenance), read_write (literal, a string literal in owdAliasProvenance))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via read_write (literal, a string literal in owdAliasProvenance))
  • content/docs/releases/v13.mdx (via validateSecurityPosture (symbol, a top-level function), read_write (literal, a string literal in owdAliasProvenance))
  • content/docs/releases/v17.mdx (via validateSecurityPosture (symbol, a top-level function), publicSharing (literal, a string literal in owdAliasProvenance))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 53d55dee20e541255696f379be278c4bb3610de7packageMentionDocs.

Which tree this was computed on

This run read content/docs from 60501de656037f4be11fc016562c6ebcd0d1050e — the merge of head 9476b40662db35085be43bd8e56493dbff518600 into base 53d55dee20e541255696f379be278c4bb3610de7, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 53d55dee20e541255696f379be278c4bb3610de7 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 8, 2026
@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 8, 2026 21:43
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit eda26ce Sep 8, 2026
35 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-16517-owd-alias-fix-provenance-split branch September 8, 2026 22:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s tests tooling

Projects

None yet

1 participant