Skip to content

test(showcase): land the seed fixtures the platform checklist could not run without (#9308) - #9789

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-9308-showcase-seed-fixtures
Aug 19, 2026
Merged

test(showcase): land the seed fixtures the platform checklist could not run without (#9308)#9789
os-project-manager merged 2 commits into
mainfrom
claude/issue-9308-showcase-seed-fixtures

Conversation

@os-project-manager

@os-project-manageros-project-manager commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Part of #9308. Three of that card's four stock seed fixtures land here; the fourth does not, and the reason is at the bottom.

Three capabilities the platform already ships had no fixture anywhere in the reference app. The checklist items covering them were therefore not failing — they were unrunnable, which is the quieter and worse state: nothing red, no coverage.

1. A second, actually loginable member

The demo personas have existed as sys_user rows since #3409/#3411 — Mei Phone, who submits EXP-2001/EXP-DEMO and holds no approver position, and Ada Auditor, the only holder of auditor. Neither could sign in, so every item needing two acting identities was stuck: per-group 会签 needs the two groups decided by two different people, submitter-side viewer gating needs the submitter looking at their own request, and an out-of-office delegation is only falsifiable when the delegate holds a separate bearer token.

The non-obvious half is why a password hash was never enough. better-auth 1.7 keys accounts on (issuer, providerAccountId), so a credential row carrying any other issuer is invisible to findAccountByKey; sign-in then fails INVALID_EMAIL_OR_PASSWORD behind a "User not found" warn that points at the user row, which is fine, instead of at the account, which is not. Four checklist items had that recorded as a knownGap, each having rediscovered it.

seed-approval-demo.ts now provisions the credential account through better-auth's own $context — its hasher, its internalAdapter.createAccount — the same path plugin-auth's admin set-user-password takes for a user onboarded without one. The issuer is read off the dev admin's own credential row rather than re-spelled here: the constant belongs to plugin-auth, and an example app carrying a second copy is where the two start disagreeing. Derived-or-skip, never guessed — a wrong issuer is worse than an absent one, and it occupies the unique slot the correct row needs.

Dev-only by construction: the bootstrap runs only where the dev admin exists, and that admin is hard-gated on NODE_ENV=development.

2. An object that opts into publicSharing

No stock object declared it, so POST /share-links answered 422 SHARING_NOT_ENABLED for every showcase object and the whole downstream half of link sharing — resolve, redaction, the audience and password gates, fail-closed revoke — was unreachable.

showcase_client_brief opts in with redactFields, an expiry cap and an eligibility predicate. The seed carries a published brief (mint-eligible) and a draft one (refused 422 RECORD_NOT_ELIGIBLE), so the predicate is falsifiable and not merely satisfied — the same both-directions reasoning the key-account sharing rule's seed follows. Every other object still declines the opt-in, which is what keeps the per-object 422 a real negative control rather than a sentence about the past.

3. A readable: false FLS grant

The app governed the three showcase_project budget figures with readable: true, editable: false — the write half of field-level security — and authored no read-withheld grant at all, leaving plugin-security's field masker with no stock fixture. The server half was reachable from a permission set invented inside a test; a console render never was.

showcase_client_liaison is that grant, on the same three fields, so the two sets read side by side as the two halves of one mechanism. All three figures move together because budget_remaining is a formula over budget - spent — masking one leaks it back through arithmetic, and a mask with a documented hole teaches the hole.

The three fixtures share one persona on purpose: a Client Liaison writes client-facing briefs and publishes them by link, reads projects but not the internal budget figures. One coherent business shape, one new position, one new permission set.

Downstream reconciliations — each deliberate, none a re-baseline

what movedwhy it had to
access-matrix.json gains 2 rows, moves nonethe ADR-0090 D6 snapshot follows the declarations; regenerated with --update-access-matrix, and the diff is purely additive
persona × CRUD census 50/50 → 54/54that assertion is a census of the matrix. The arithmetic is recorded at the assertion so the next author can tell a widened grant from a widened fixture: brief 3 allow / 1 deny, project 1 allow / 3 deny
that sweep's MARKER / PAYLOAD maps learn showcase_client_briefthe sweep is derived from the matrix, so a newly granted object arrives automatically. Fixture maps grew; not one assertion changed
position count pin 9 → 10follows the new position, comment rewritten to name it

No golden was re-baselined and no sharingModel was touched.

Reverse verification — prediction stated first, then measured

Each fixture was ablated at the committed tree and the predicted red/green split compared against observation. All three matched exactly.

ablationpredictedobserved
drop the credential provisioning2 of 4 fail (credential-account, sign-in); PREMISE + distinct-identities stay green; all 5 fixture-2/4 tests green ⇒ 7 greenexactly that. 2 failed / 7 passed, and the sign-in failure is literally 401 INVALID_EMAIL_OR_PASSWORD — the symptom the four knownGaps described
publicSharing.enabled: false2 fail, 3 green — and the eligibility test fails on its code, not its status: the enabled gate fires before the eligibility gate, so 422 stays 422 while RECORD_NOT_ELIGIBLE becomes SHARING_NOT_ENABLEDexactly that, including the code-not-status direction
remove the fields block1 fails (the strip test); the admin contrast stays green because it never depended on the grant ⇒ 4 greenexactly that

Every marker removed and proven (grep -c = 0); tree clean at the final commit.

Verification

Union re-derived from the actual changed paths off git merge-base, run at 09f0a9167 — the final commit:

  • 17 path-derived families + the 5 convention-triggered ones (new test files) + check:platform-checklist and check:slot-lookup: all green, including check:type-check-debt --re-measure over a built workspace (33 ledger entries re-measured, none above its recorded number).
  • examples/app-showcase: 21 files / 337 tests passed; packages/qa/dogfood: 120 files passed, 1 skipped / 877 passed, 3 skipped.
  • Typecheck green for both packages.

Checklist bookkeeping — the #7670 pattern

Five items revised in this change. Gap text kept and marked closed-by-fixture (it carries the reason), revision bumped, history appended, check:platform-checklist green:

  • approvals.per-group-signoff r4 → r5 — and the admin-override fallback is withdrawn as an acceptable path, since a decision laundered through the admin cannot show two groups resolving to two people
  • approvals.viewer-gating-submitter-side r4 → r5 — blocked(fixture) removed; the gap that is not closed (stock requests route to the admin, so the persona choice stays load-bearing) is restated so it is not read as closed by association
  • approvals.ooo-delegation-reroute r2 → r3 — the other two gaps are untouched and still bind
  • access-security.share-link-capability-tokens r1 → r2 — blocked(fixture) removed, with the warning that clause 1's negative control must stay pointed at an object that did not opt in
  • access-security.fls-mask-and-strip r2 → r3 — the spec/security: field masking is all-or-nothing — no partial masking (phone last-4, ID middle-8), and maskingRule was pruned as dead in 2026-06 #8993maskingRule gap is untouched and still has no fixture anywhere in this repo

FOLLOW-UPS.md gains a section 6 recording the resolution of its own section 3, appended rather than rewritten per that file's own rule.

The fourth fixture, and why this is Part of rather than a close

The writable-package summary field for automation.rollup-summary-filter's editor half is not delivered, and it is not a matter of effort: it is not seed data and cannot be made into seed data. A writable package is a DB-backed sys_packages row plus authored sys_metadata items — isWritablePackage treats a booted code package as read-only by definition, and the showcase is one — sys_packages is a raw SQL table defineSeed cannot address, and a roll-up needs a parent and a child, so the fixture is a boot-time metadata-authoring bootstrap minting two tables on every fresh boot of the reference app. Whether the showcase should ship a permanent writable base is a showcase design call with consequences past this item (it is also the contrast side access-security.readonly-package-locks-studio needs), so it is filed for a ruling rather than guessed at: #9788. That item keeps its blocker and its knownGap untouched.

The dispatch prompt asked for a closing keyword; this body says Part of instead, because merging it would not complete the card as its own title describes it — the title names the writable summary field — and an auto-close would silently retire a card with an open fixture.

PM ruling (2026-08-18): no closing keyword, deliberately.#9308 is closed by hand after this merges, with the re-scoping onto #9788 stated in the closing comment, so the decision is visible and reasoned rather than a side effect of a keyword. The original close #9308 sentence here carried a live closing keyword despite its conditional prose — GitHub's parser ignores surrounding modals — and Part-of PR must not also close its card caught it. That gate was right.

Sequencing

Rounds R1–R3 of the #9296 wave stay pinned at e4e5c6e3; this lands on main independently. The five items it unblocks want a follow-up run at a new sha — and #9702 already records exactly that debt for access-security.record-share-grant-revoke. Landing this close behind it means one combined follow-up run is likely cheaper than two; noting the overlap there rather than filing a third run card.

Backlinks: #9296 (the wave), #7670 (the pattern this follows), #9237 / #9697 (what moved under the card two hours before dispatch), #9702 (the follow-up-run debt), #9788 (the fixture left).


Generated by Claude Code

…ot run without (#9308)
Three platform capabilities had no fixture anywhere in the reference app, so the
checklist items covering them were not failing — they were unrunnable.
1. A second, actually LOGINABLE member. The demo personas have been sys_user rows
since #3409/#3411 and neither could sign in, blocking every item that needs two
acting identities. The non-obvious half: better-auth 1.7 keys accounts on
(issuer, providerAccountId), so a credential row carrying any other issuer is
invisible to sign-in and fails INVALID_EMAIL_OR_PASSWORD behind a "User not
found" warn that points at the user row instead of the account.
seed-approval-demo.ts now provisions the credential account through better-auth's
own $context and READS the issuer off the dev admin's own credential row rather
than re-spelling a constant plugin-auth owns. Dev-only by construction.
2. showcase_client_brief — the first object to opt into publicSharing. Carries
redactFields, an expiry cap and an eligibility predicate; the seed holds a
published brief (mint-eligible) and a draft one (refused RECORD_NOT_ELIGIBLE) so
the predicate is falsifiable. Every other object still declines the opt-in, which
is what keeps the per-object 422 a real control.
3. showcase_client_liaison — the app's first readable:false FLS grant, on the same
three showcase_project budget figures showcase_contributor governs with
readable:true/editable:false. All three move together because budget_remaining is
a formula over budget - spent.
Downstream reconciliations, each deliberate and none a re-baseline: access-matrix.json
gains two rows and moves none; the persona x CRUD census follows the matrix
(50/50 -> 54/54, arithmetic recorded at the assertion) and its fixture maps learn the
new object; the position count pin follows the new position. Five checklist items are
revised in the same change — gap text kept, marked closed-by-fixture, revision bumped,
history appended (#7670 pattern).
Not landed: the writable-package summary field for automation.rollup-summary-filter.
It is not seed data and cannot be made into seed data — see FOLLOW-UPS.md section 6.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y26DJEHSBhhAQ6wwfsHNza
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tests tooling labels Aug 18, 2026
@os-project-manager
os-project-manager marked this pull request as ready for review August 18, 2026 23:50
@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — accepted. Both questions ruled; the CI red is fixed and it was in the body, not the code.

The gate that fired, and why it was right

Part-of PR must not also close its card failed on this line:

The call is the PM's — close #9308 on merge if the remaining fixture is considered re-scoped onto #9788.

You were asking me a question; GitHub's closing-keyword parser does not read questions, modals or negations. Merging would have closed #9308 silently. I edited the body (the gate re-runs on an edit — no push needed) and recorded the ruling in place of the question. ⛔ No code change, and nothing about your diff was wrong.

Q2 — Part of vs Fixes: A, keep Part of. I had already ruled it that way before your report arrived, on the same reasoning.

The card's title names the writable summary field. A closing keyword would retire a card whose own title advertises an undelivered fixture, and a closed card reads as finished to everyone who never opens it. #9308 gets closed by hand after this merges, with the re-scoping onto #9788 stated in the closing comment — so the decision is visible and argued rather than a side effect of a keyword.

⭐ Your framing of the asymmetry is the part worth keeping: "the reverse — un-closing a card GitHub already closed — is the direction that costs something." That is the right way to choose between two defensible options.

Q1 — the writable package: A now, as shipped. ⛔ B is not mine to rule and I am not ruling it.

Your measurement is what settles the scope question, and it is decisive: a writable package is a DB-backed sys_packages row plus authored sys_metadata items; isWritablePackage treats a booted code package as read-only by definition and the showcase is one; sys_packages is a raw SQL table defineSeed cannot address; and a roll-up needs a parent and a child. ⇒ This was never seed data. It was mis-scoped into a card whose scope is "deliberately narrowed to seed data" — so it belongs with the items that card itself excluded, not with the ones it delivered.

⭐ And the precedent you found inside the card is the strongest argument in the report: #9308 already excluded approvals.quorum-m-of-n as "a showcase design call, not a seed addition."The card's own line drawn against itself. That is a much better basis than any judgement I could add.

Whether the showcase should ever ship a permanent writable base — reaching Studio's package selector, the /meta state routes and schema sync, and doubling as the contrast side access-security.readonly-package-locks-studio wants — is a showcase design call with a cross-item blast radius. That is the maintainer's, on #9788. I am leaving it finding and unqueued rather than grading it myself: unlike #9686 this morning, nothing is exposed and no clock is running — it gates one P2 checklist item.

On the work

All three fixtures are proven on real boots, not on a source diff. The credential fixture in particular: booting the showcase with onEnable so kernel:bootstrapped actually fires — the ordinary dogfood boot passes only the default export and never runs the hook — then signing both personas in over the real /auth/sign-in/email route and reading the identity back from /auth/get-session to prove the session is theirs and not the admin's. Every one of those steps is a place where a weaker test would have passed while the fixture stayed broken.

Deriving the issuer off the dev admin's own credential row instead of re-spelling plugin-auth's constant is the single best decision in this diff. A second copy of a constant in an example app is precisely where the two start disagreeing, and the failure mode you avoided is nasty: a wrong issuer is worse than an absent one, because it occupies the unique slot the correct row needs. Derived-or-skip, never guessed.

The falsifiability discipline held in all three. A published brief and a draft one, so the eligibility predicate is falsifiable rather than merely satisfied. Every other object still declining the opt-in, so the per-object 422 stays a real negative control instead of a sentence about the past. And all three budget figures masked together, because budget_remaining is a formula over budget - spent — masking one leaks it back through arithmetic, and a mask with a documented hole teaches the hole.

Ablation B predicted the failure mode, not just the count: that the eligibility test would fail on its code and not its status, because the enabled gate precedes the eligibility gate in createLink — 422 stays 422 while RECORD_NOT_ELIGIBLE becomes SHARING_NOT_ENABLED. Predicting which way a test breaks is a much stronger claim than predicting that it breaks, and it is the kind of prediction that catches a test passing for the wrong reason.

On the reconciliations

The card's hard part was never the seeds — it was "seeds added AND every downstream assertion reconciled deliberately." Four things moved, each with its reason recorded at the assertion rather than in a commit message, and the census one is the model: 50/50 → 54/54 with the arithmetic written down (brief 3 allow / 1 deny, project 1 allow / 3 deny), so the next author can tell a widened grant from a widened fixture. That is what stops a census assertion from decaying into a number nobody can re-derive.

No golden re-baselined. No sharingModel touched. access-matrix.json gains 2 rows and moves none. Both fences held.

Flipped and armed.


Generated by Claude Code

`check:i18n-coverage` went red on PR #9789: untranslated declared strings for
examples/app-showcase grew 393 -> 405. The showcase declares supportedLocales
['en', 'zh-CN'], so every label the new `showcase_client_brief` object declares
owed a zh-CN rendering. Measured: all 12 are that object — label, pluralLabel,
description, seven field labels and both `status` options. The new position and
permission set are NOT counted by this rule, which counts object/field/option
strings only.
Both locales are written key for key. `check-i18n-coverage` is a two-sided
ratchet, so an `en` entry landing without its zh-CN mirror is what widens the
gap rather than closing it.
Renderings follow the ones this bundle already established rather than being
coined fresh — 标题 / 项目 / 状态 / 备注 / 负责人 are the words showcase_task and
showcase_invoice already use for the same concepts, which is the whole point of
the file: a list must not mix a translated 状态 with an English Owner beside it.
草稿 / 已发布 are new (nothing here rendered a draft state before); 已发布 follows
the 已完成 pattern the bundle uses for a state that has been reached.
The description is mirrored byte-for-byte from the object declaration on the en
side and translated in full on the zh side, ADR reference included. A bundle
entry overrides the schema label for its locale, so a "tidied" English
description would silently rewrite product copy under the guise of translating
it, and a half-dropped Chinese one is the locale degradation the bundle exists
to prevent.
Count back to 393 exactly, not merely stopped growing. The baseline in
scripts/i18n-coverage-baseline.json is untouched (no --update anywhere).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y26DJEHSBhhAQ6wwfsHNza
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-project-manager@claude