docs(spec): the user-level export slot is a real opt-in grant, not "always true this phase" - #19010
Conversation
…lways true this phase" `ResolveApiOptions.userExportAllowed` and the `API_METHOD_DERIVATION` table docblock both asserted, in the present tense, that the slot is always `true` and that no user-level export permission bit exists yet. #3544 wired that bit in: `PermissionSetSchema.allowExport` declares the axis as an opt-in grant (unset or `false` means no export), plugin-security resolves `export` as `list AND userExportAllowed`, plugin-hono-server feeds the resolved bit into `resolveEffectiveApiMethods`, and this package's own suite pins the `false` arm. The two statements could not both be true. Both docblocks now state the axis as it is and name `allowExport` as the authority on its semantics, while keeping the still-true half distinct: an omitted option resolves to `true` because a resolve carrying no permission context must not narrow the object's own exposure. No behaviour change -- the derivation functions are byte-identical and no test assertion moved. `api-surface-declarations/data.txt` is the regenerated baseline, not a hand edit. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
…er-export-always-true-rot
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 136 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 f2c7f528542fbf7a69ba339066455827578303ad && git checkout f2c7f528542fbf7a69ba339066455827578303ad
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 43f4766889e39d7a4590c5787d38e5956d0b4cb6 a95a608623eec5e4a7cebcda5e760adcb07bdcae && git checkout -B drift-repro 43f4766889e39d7a4590c5787d38e5956d0b4cb6 && git merge --no-ff a95a608623eec5e4a7cebcda5e760adcb07bdcae
node scripts/docs-audit/affected-docs.mjs --json 43f4766889e39d7a4590c5787d38e5956d0b4cb6 |
Fixes #18991
Clause-②: no
Two docblocks in
packages/spec/src/data/api-derivation.tsasserted, in the present tense, that the user-level export slot is alwaystrueand that no such permission bit exists yet. #3544 wired that bit in. Both now describe the axis as it is.⛔ Zero behaviour change.
isLegacyDerivable,computeOperationsandresolveEffectiveApiMethodsare byte-identical; the omitted-option default is stilltrue; not one assertion inapi-derivation.test.tsmoved. The diff is two docblocks, the generated baseline that reproduces one of them, and a changeset.The card named ONE carrier. There are TWO, in the same file
The card pointed at the
ResolveApiOptions.userExportAllowedmember docblock. TheAPI_METHOD_DERIVATIONtable docblock, twelve symbols up the same file, made the same claim in its own words — fixing only the first would have left the identical false sentence in the identical file.①
ResolveApiOptions.userExportAllowed— before① after
②
API_METHOD_DERIVATIONtable docblock — before② after
Why this is a contract violation and not a stale comment
The same package declares the opposite, and the two cannot both be true —
packages/spec/src/security/permission.zod.ts, onPermissionSetSchema.allowExport:It is not an aspiration either. Three live readers feed the slot a value that is genuinely
false, found by symbol:packages/plugins/plugin-security/src/permission-evaluator.tsexportaslist ∧ userExportAllowedand returnsfalsefrom that branch — its own comment citesAPI_METHOD_DERIVATION, i.e. carrier ②packages/plugins/plugin-hono-server/src/current-user-endpoints.tsobjects['*']?.allowExport === true,exportBit === true) and hands each toresolveEffectiveApiMethodspackages/spec/src/data/api-derivation.test.tsfalsearm all along:export gated off when userExportAllowed=falseAn author trusting the old text reads the parameter as inert and can legitimately simplify it away as dead weight — reintroducing the defect one level upstream of where it was last found, with no consumer channel left to notice.
The claim is gone — fold-immune probe, both controls in the same run
Carrier ② is broken across two comment lines (one ends
the real, the next beginspermission bit), so a line-orientedgit grepreads 0 for it whether it is there or not. Every reading below strips comment prefixes and collapses newlines into single spaces first, and every run carries a known-present bright needle and a fabricated dark one so a0is a measurement rather than a blind probe.Always \true` this phase` (target ①)the real permission bit is a follow-up(target ②)user-level export slot(BRIGHT)totallyFabricatedNeedleXyz(DARK)The strongest control is in the table itself: in the post-edit run the same target needle read
0in the edited source while still reading1in the not-yet-regenerated baseline — one run, one needle, both answers.The regeneration leg — the baseline follows, and the gate says so
⛔ The baseline is not hand-edited.
check:api-surface-declarationswas run before regenerating and named exactly what moved:pnpm --filter @objectstack/spec gen:api-surface-declarationsthen reportedtouched: data.txt, and the whole diff in that file is the one hunk atResolveApiOptions.Worth recording because it is counter-intuitive: the generator's header states it deliberately does not record a declaration's leading TSDoc. Carrier ① is a member docblock inside the interface body, so it is part of the declaration text and does move the artifact; carrier ② is leading trivia on a
constand correctly moves nothing. That is why the baseline shows one hunk and not two, and it is exactly what the gate reported.Every file in this diff × is it published
@objectstack/specfiles[]currently reads["dist","json-schema","liveness","prompts","llms.txt","README.md","src/**/*.zod.ts","CHANGELOG.md","api-surface","api-surface-declarations","spec-changes.json"].files[]?packages/spec/api-surface-declarations/data.txtapi-surface-declarationsis afiles[]entrypackages/spec/src/data/api-derivation.tsdistsrc/**/*.zod.ts, but its TSDoc compiles into the packeddist/data/index.d.ts, which carries the new text and no longer carries the old — verified by grep on the built tree.changeset/18991-user-export-slot-is-a-real-optin-grant.md⇒ Two published surfaces move, so this is ⛔ not
skip-changeset. It carries apatchchangeset for@objectstack/spec.Verification
All readings at
a95a608623(this branch merged withorigin/main43f4766889), exit codes captured before any pipe.pnpm --filter @objectstack/spec test— 489 files / 14209 tests passed, exit 0. The card's own file named explicitly:vitest run --project local src/data/api-derivation.test.ts— 1 file / 32 tests passed, exit 0.pnpm --filter @objectstack/spec typecheck— exit 0.pnpm --filter @objectstack/spec check:generated— all 16 generated artifacts up to date, exit 0, working tree clean afterwards.pnpm lint(eslint . --no-inline-config, whole repo, ⛔ not narrowed) — exit 0.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 77 derived, 76 run green, 0 unrun. Reconciled with--ran, exit 0.pnpm check:dual-build-cjs-loadsrefused withPREREQUISITE NOT MET/ exit 3 — it reads built output for 87 packages and needs a whole-repopnpm build, which is CI'sBuild Core. Exit 3 is that gate's declared "nothing was measured" code, ⛔ neither a pass nor a failure. The two other exit-3 refusals were cleared by building only their prerequisites and both then ran green (check:doc-formula-expressions,check:lean-entry-closure).Acceptance notes
packages/plugins/**is touched here; those two readers appear above as evidence only.permission.zod.tsis deliberately not edited — it is the half that is already correct, and this change points at it rather than copying it.['list']getsaggregate/export/searchfor free" is about the authored-vs-derived vocabulary, not about the permission axis, and it is true on the default no-user-context path — the same way it is true forsearch, whosesearchableflag it also does not caveat. Not a third carrier; left alone.api-derivation.test.ts:68's title still says "this phase" (export = list (this phase, userExportAllowed defaults true)). It describes the default, which is true today, and touching it would move a test file in a zero-behaviour-change PR. Carrier: the next PR that edits that suite.open_questionsand ⛔ was not built here, to keep this diff at two docblocks.🤖 Generated with Claude Code
https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Generated by Claude Code