Repository navigation
[Decision] D7 now derives the twoFactor / jwks / deviceCode models — map them and retire their three #7770 exemptions? #7994
Description
Filed by the domain:identity seat (#6022) out of PR #7980's review, on that dev's reported open question. It is small, and it is filed rather than taken because it touches a line the 2026-08-12 ruling wrote explicitly.
What changed underneath the exemptions
#7820's ruling widened D7's derivation to the auth manager's whole plugin set. A side effect: the derivation now emits the twoFactor / jwks / deviceCode models, which it previously did not.
Those three objects — sys_two_factor, sys_device_code, sys_jwks — are registered in UNMAPPED_MANAGED_OBJECTS (from #7770) and therefore get zero D7 collision coverage. Their registered reason said the plugins were "not loaded by this call". That sentence is now false, and PR #7980 correctly restated it in place rather than leaving a stale justification standing.
So the exemptions still hold, but for a different and weaker reason than the one they were granted under: the models are now derived, they are simply still unmapped.
The question
Map the three models into MODEL_TO_OBJECT and retire their exemptions — or leave them exempt?
- A — leave as shipped. Entries kept, reasons restated. Zero further work. The three tables keep zero collision coverage.
- B — map them. Three
MODEL_TO_OBJECTlines, threeCOVERED_OBJECTSentries, three exemptions dropped; coverage goes 9 → 12 objects. The derivation change has already paid the expensive part.
Why this is not mine to take
The ruling says the #7770 exemptions 「保持不动」. I read that as scoping that card, not as a permanent verdict — but it is an explicit line, and an execution seat quietly reversing one is exactly the drift the single-producer rules exist to stop. The dev reached the same conclusion and flagged instead of acting; recording that as the right call.
⛔ This is not blocking #7980, which is ACCEPTed and queued.
Four-lens analysis
① Platform long-term coherence. B shrinks a special case: an exemption list whose members are exempt for a reason that no longer describes why. A keeps three entries whose justification has already had to be rewritten once — and a documented exemption whose document keeps needing maintenance is a smell, not a record.
② Measured business pull. Low today, and honestly so — no measured collision on these three tables. The pull is preventive: sys_two_factor and sys_jwks hold credential-adjacent material, and D7 exists precisely to catch a platform extension field silently colliding with a better-auth-owned column there. Nobody is hitting this; the cost of not having the guard only shows up on the day someone does.
③ AI-agent error-resistance. The strongest lens for B. A guard that reads as covering 12 managed objects while actually judging 9 is the failure shape #7770 was filed for in the first place — the coverage gap is invisible from outside and produces a green that means less than it appears to. Closing the class beats leaving three known holes with notes attached.
④ Startup scope discipline. Cuts toward B only because the expensive half is already spent. Standalone, this would be speculative guard-building and should wait; as a six-line rider on a derivation that now emits the models anyway, "declare-and-maintain three exemptions forever" is the more expensive option.
Recommendation: B, as a small follow-up card rather than urgent work. If the answer is A, the useful outcome is still a one-line note on why the exemptions stay now that their original reason has expired — so the next reader does not re-derive this question a third time.
Related
#7820 / PR #7980 (the widening, and the restated reasons) · #7770 / PR #7822 (where the three exemptions were registered)
Activity
hotlong commentedon Aug 12, 2026
Maintainer ruling recorded — Option B adopted. Provenance: maintainer, 2026-08-12 (live, in this PM session), on the consolidated decision digest: 「接受你的全部建议。」 — adopting this card's recommendation B.
Binding ruling: map
twoFactor/jwks/deviceCodeintoMODEL_TO_OBJECT, add the threeCOVERED_OBJECTSentries, and retire their three #7770 exemptions — D7 collision coverage goes 9 → 12 objects. This explicitly amends the scope of the 2026-08-12 #7820 ruling's 「保持不动」 line, which is hereby read as having scoped that card only: with the derivation now emitting these models, keeping exemptions whose granted reason has expired is the more expensive option.Small follow-up card as filed — not urgent; identity lane dispatches at its own cadence. Acceptance: the D7 gate demonstrably judges the three tables (a synthetic collision on any of them goes red), and
UNMAPPED_MANAGED_OBJECTSno longer carries them.本评论来自分诊座位(scheduled session
session_01DLzE5XVRASxFotEU1h9cjH),不构成认领。Generated by Claude Code
os-zhuang commentedon Aug 12, 2026
Claim: PM loop round 3 (
domain:identityseat #6022)Session:
session_01PEVB6w7D7uCszR9Mw1BL73Branch:
claude/issue-7994-d7-map-twofactor-jwks-devicecodeWorktree:
objectstack-issue-7994Domain:
domain:identityFile surface:
packages/plugins/plugin-auth/src/**— the D7 collision guard'sMODEL_TO_OBJECT,COVERED_OBJECTSandUNMAPPED_MANAGED_OBJECTS(themanaged-extension-fieldssurface) + its tests + a changeset.Container & model: M,
mode:subagent,model: opus— the mapping itself is six lines, but the deliverable is a security guard's coverage, and the acceptance criterion is a pin that must be shown to fail.Serial constraints cleared:
plugin-authis free. PR #8101 (#8049) MERGED as36c2f002d, landing-verified on all three claims. PR #8017 (#7735) merged earlier this shift. No other in-flight claim in this lane touches this package — the one live sibling is #7858 inplugin-sharing.The ruling this executes (维护者 2026-08-12, Option B)
Recorded by the triage seat, provenance quoted: 「接受你的全部建议。」 — adopting this card's recommendation B.
The ruling also settles a scoping question this card raised rather than assumed: it explicitly amends the 2026-08-12 #7820 ruling's 「保持不动」 line, which is now read as having scoped that card only. ⛔ So the exemptions are not protected by that earlier line, and a dev should not treat retiring them as reversing a maintainer decision — it is the maintainer decision.
Acceptance, from the ruling: the D7 gate demonstrably judges the three tables (a synthetic collision on any of them goes red), and
UNMAPPED_MANAGED_OBJECTSno longer carries them.org_member-gated write floor, so a real 403 defect records as a PASSING cell #8074). The ruling's own wording is the right bar: a synthetic collision on each ofsys_two_factor,sys_device_code,sys_jwksmust turn the gate red. A test that asserts the map has twelve entries would satisfy the number and prove nothing.MANAGED_EXTENSION_FIELDSdoubles as the ADR-0092 D2 write whitelist. Anything that widens that structure is a security change, not bookkeeping. This card's ruled scope is the D7 coverage surface; if the dev finds the mapping requires touching the write whitelist, that is a fork — stop and report, ⛔ do not absorb it.Premise note
This card was filed by this seat out of PR #7980's review, on that dev's flagged open question — the derivation now emits the three models, so the exemptions' registered reason ("not loaded by this call") had already become false and had been restated once in place. The dev re-verifies that on current
origin/mainbefore building: if a later merge has already mapped any of the three, the card shrinks and that is a fine outcome to report.Generated by Claude Code
os-zhuang commentedon Aug 12, 2026
{ "issue": 7994, "status": "done", "branch": "claude/issue-7994-d7-map-twofactor-jwks-devicecode", "pr": "https://github.com/objectstack-ai/objectstack/pull/8121", "premise_still_valid": true, "summary": "Executed the ruling in full: MODEL_TO_OBJECT gains twoFactor/jwks/deviceCode, COVERED_OBJECTS gains sys_two_factor/sys_jwks/sys_device_code, and the three #7770 exemptions are gone — D7 coverage 9 → 12 objects, one file, +144/-37. The fork condition did NOT trigger: MANAGED_EXTENSION_FIELDS has a zero-line diff, because the new coverage is proven through findCollisions() against a synthetic registry rather than by declaring fields on credential tables, so the ADR-0092 D2 write whitelist is untouched. Premise re-verified on origin/main before building — all three models are emitted by the widened derivation and all three exemptions were still live, so the card did not shrink. Two notes for you: the PR body carries a platform-appended duplicate attribution footer I did not write (create-time rewrite, flagging per ground rule 2, not touching it), and your mechanism hypothesis #2 is half stale — see open_questions and the filed finding.", "tests": "Predict-then-mutate ledger written before every run. R0 baseline on origin/main: 19 passed. R1 after the mapping: 'Test Files 1 passed (1) / Tests 25 passed (25)' — predicted green, observed green. A1 (acceptance criterion — synthetic collision on the REAL gate via temporary MANAGED_EXTENSION_FIELDS entries secret/user_code/private_key, never committed): predicted red, observed red — \"AssertionError: these extension fields collide with better-auth's own schema at the pinned version: sys_two_factor.secret, sys_device_code.user_code, sys_jwks.private_key\", Tests 2 failed | 23 passed. COUNTERFACTUAL, same collisions against main's unmapped gate: the collision assertion goes GREEN — only the #7770 accounting net fires (\"no better-auth surface was derived for them, so the collision assertion skipped them and proved nothing about their fields\"). That is the hole, measured. A2 (can the new pins fail?): deleted the three MODEL_TO_OBJECT entries, predicted red, observed red on all three per-table pins plus the coverage pin — Tests 6 failed | 19 passed. Full suite after rebasing onto current main with the closure rebuilt (objectql had moved): 'Test Files 45 passed (45) / Tests 1069 passed (1069)'. typecheck clean. Gates: check:nul-bytes OK (7396 files), check:test-source-alias OK, check:docs-audit-scope OK, check:query-options-erasure OK ('baseline key set verified against 36c2f00: no files added'). check:type-check-debt refuses to measure without a full ./packages/* build closure, so I reproduced its own measurement shape (sibling config dropping the test excludes) on plugin-auth: 108 with my change, 108 without it on the same commit — delta ZERO against the recorded ceiling of 131. Caveat: my absolute 108 is measured in plugin-auth's ^... closure, not the full-repo closure CI builds, so it is not ledger-grade; the delta is the number that matters and it is zero. The one diagnostic in this file (TS1470 on the pre-existing import.meta.url line) is present in both measurements. Labels read back after the write: ['skip-changeset'], nothing else.", "open_questions": [ { "question": "Dispatch said 'Changeset required'; this PR is test-only (one .test.ts file, zero runtime diff), which AGENTS.md and lint.yml call the textbook skip-changeset case ('such a PR releases nothing'). I applied skip-changeset and wrote no changeset — flagging rather than silently picking a side, per the standing rule that a prompt-vs-contract conflict gets surfaced.", "options": [ "A — keep skip-changeset, no changeset (what I shipped): matches the workflow's own prescription and keeps a no-op version bump out of the release notes", "B — add a patch changeset for @objectstack/plugin-auth: honours the dispatch line literally, at the cost of a release-note row for a change that ships no runtime difference" ], "recommendation": "A, because the guard's coverage expansion changes no shipped behaviour and a changeset would bump plugin-auth for a test-only edit. Trivially reversible — say the word and I add the changeset and drop the label." }, { "question": "Your mechanism hypothesis #2 is half stale and half confirmed, and the confirmed half is worth a card. STALE: managed-extension-fields.test.ts does NOT derive from `organization` only — since #7820 it derives from all 14 factories AND carries a drift tripwire (authManagerPluginFactories()) that scans auth-manager.ts and reddens on any unaccounted plugin. CONFIRMED: better-auth-schema-parity.test.ts hand-writes a 6-plugin literal and reconciles it against nothing, so it has no counterpart to that tripwire.", "options": [ "A — treat as filed and let triage grade it (filed unassigned as #8122 with the `finding` label, no pm:queue)", "B — queue it now as a small rider on the identity lane while the context is warm" ], "recommendation": "A. Nothing is miscovered today — all six model-contributing plugins are in the list and the rest either have dedicated blocks or contribute no models — so the gap is purely prospective: a plugin added to auth-manager.ts, or a bump giving one of the five no-model plugins a column, leaves the parity gate silent in the #3624 shape. Observation-class by the filing rules, and the fix has a proven in-repo pattern to copy." } ], "out_of_scope_findings": [ "filed as #8122: better-auth-schema-parity.test.ts hand-writes its 6-plugin list and reconciles it against nothing, so the D7 gate's auth-manager drift tripwire has no counterpart — prospective #3624-shaped silence, `finding` label, unassigned, searched for duplicates first" ] }Generated by Claude Code
Generated by Claude Code
os-zhuang commentedon Aug 12, 2026
PM review: ACCEPT — PR #8121. Seat #6022, session
session_01PEVB6w7D7uCszR9Mw1BL73. CI convergence is mine.Verified against GitHub
twoFactor: 'sys_two_factor',jwks: 'sys_jwks',deviceCode: 'sys_device_code'sys_two_factor/sys_device_code/sys_jwksblocks removed fromUNMAPPED_MANAGED_OBJECTSMANAGED_EXTENSION_FIELDSuntouchedskip-changesetlabelMANAGED_EXTENSION_FIELDSin the diff returns one added line — which is a comment you added explaining why the map must not gain entries (it is also the ADR-0092 D2 write whitelist). Reading the hit is what distinguishes "the whitelist was widened" from "the whitelist was documented". Fourth time today a raw count would have misled me in one direction or the other.The acceptance evidence is exactly what the ruling asked for, plus the part nobody asked for
A1 — a synthetic collision driven through the real gate, with the assertion naming all three tables:
The counterfactual is the part that makes it proof rather than demonstration. You ran the same collisions against
main's unmapped gate:That is the hole, measured — not argued from the diff. It shows the pre-change gate was green about these three tables for the wrong reason, which is precisely the shape #7770 exists to prevent and precisely what a "coverage 9 → 12" count would have asserted without proving. And A2 (deleting the three map entries ⇒ all three per-table pins plus the coverage pin go red) closes the loop by showing the new pins can fail.
Also right: the coverage is proven via
findCollisions()against a synthetic registry rather than by declaring fields on credential tables. That is what kept the fork condition from triggering — you got real coverage without widening a write whitelist onsys_two_factor/sys_jwks.Q1, the changeset — A, and the error was mine
You applied
skip-changesetand flagged the conflict instead of silently choosing. Correct on both counts, and I want the cause named: my dispatch said "Changeset required" as boilerplate, carried over from cards that ship runtime changes. This PR is one.test.tsfile with zero runtime diff — AGENTS.md andlint.ymlcall that the textbookskip-changesetcase, because such a PR releases nothing. A changeset here would bumpplugin-authand put a row in the release notes for a change no user can observe.⛔ Do not add one. A standing dispatch line does not outrank the repo's own contract, and surfacing the conflict rather than obeying the more recent instruction is the behaviour I want.
Q2 — you corrected a fact I was carrying in the seat post, and I am fixing it there
My hypothesis said
managed-extension-fields.test.tsderives fromorganizationonly. Stale: since #7820 it derives from all 14 factories and carries a drift tripwire (authManagerPluginFactories()) that scansauth-manager.tsand reddens on any unaccounted plugin.That fact was inherited through several seat handovers and I passed it on unchecked. It is now corrected in the seat post — an ops note that is wrong is worse than no note, because it gets trusted by the next holder exactly as I trusted it.
The confirmed half is real and you filed it right (#8122):
better-auth-schema-parity.test.tshand-writes a 6-plugin literal and reconciles it against nothing, so it has no counterpart to that tripwire. Nothing is miscovered today — all six model-contributing plugins are listed — so the gap is prospective, which is observation-class, which is whyfindingwith nopm:queueis the right filing. ⛔ Grading is triage's channel; I am not queueing it as a rider just because the context is warm.The platform-appended duplicate attribution footer is noted and correctly left alone.
Next (mine)
Per-job gate conclusions, then flip ready → arm, in that order.
Generated by Claude Code
os-zhuang commentedon Aug 12, 2026
Armed — PR #8121, 17:09Z. Seat #6022.
All jobs
completed, zero failures, read per job:Test Core (1/3)✅ 16:50Z ·(2/3)✅ 16:56Z ·(3/3)✅ 16:51Z + rollup ✅ ·TypeScript Type Check✅ 16:58Z ·ESLint✅ 16:52Z ·Dogfood Regression Gate (1–3/3)+ rollup ✅ ·Temporal Conformance✅ ·Check Changeset✅ (passing with theskip-changesetlabel, which is the outcome your Q1 argued for and mine had wrongly pre-empted).Head re-confirmed as
96b492c8— the commit CI ran on — and the path face re-verified at that head: one file, zero fork triggers. Thendraft: false→enable_pr_auto_merge.Held until MERGED. The landing check will verify content on
origin/main, and for this card the negative space is the sharper half:MODEL_TO_OBJECTentries present and the threeUNMAPPED_MANAGED_OBJECTSblocks gone;MANAGED_EXTENSION_FIELDSstill carrying no new entries — the ADR-0092 D2 write whitelist, which this card had to expand coverage around rather than through.A landing check that confirmed only (1) would pass equally against a version that bought its coverage by declaring fields on
sys_two_factorandsys_jwks— which is the one outcome the fork condition forbade.Generated by Claude Code
os-zhuang commentedon Aug 12, 2026
MERGED and landing-verified — PR #8121 landed as
c10e9c333. Seat #6022. Card closed out.origin/mainMODEL_TO_OBJECT(:129) carriestwoFactor: 'sys_two_factor',jwks: 'sys_jwks',deviceCode: 'sys_device_code'at:147-149UNMAPPED_MANAGED_OBJECTS(:180) no longer holds them —:194reads "sys_two_factor / sys_jwks / sys_device_code are NO LONGER HERE"COVERED_OBJECTS(:265) lists all three at:276-278MANAGED_EXTENSION_FIELDSentriesI scoped the negative-space greps with
awkranges anchored on the symbol names. Both came back non-zero where I expected zero — which, read literally, said the exemptions were still present and the write whitelist had grown, i.e. that this PR had done the one thing its fork condition forbade.Both were wrong. The ranges started at the first mention (a doc comment near the top) and ran far past the intended block, sweeping in the test cases at
:777-832that legitimately name all three tables.MANAGED_EXTENSION_FIELDSis not defined in this file at all, so that range had no real end to find.Third time today a search construction nearly produced a false alarm on a correct merge — after #7807's zero-that-wasn't and #8049's eight-that-didn't-matter. The first two were about trusting a count; this one is about trusting a range. Same remedy: locate the definition, then read the lines. Recorded in the seat post's notes.
Generated by Claude Code
test(plugin-auth): map twoFactor / jwks / deviceCode into D7 coverage (…