Repository navigation
Refactor: Separate authentication configuration from identity data models - #55
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…ity models Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…ion) Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
|
This PR is very large. Consider breaking it into smaller PRs for easier review. |
|
@copilot 这是一段可以直接发给 AI (Copilot) 的提示词。这段提示词明确指出了模型字段与 您可以直接复制以下内容回复在 PR 的评论中,或者发给您的 AI 助手: Please update While the separation of concerns is great, our Action Items:
This ensures our Spec remains "Driver Agnostic" while providing built-in compatibility for our default driver ( |
…ort) Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
I've implemented the database field mapping configuration to handle schema differences between ObjectStack (Auth.js conventions) and better-auth. Changes made in commit
The spec is now truly driver-agnostic while providing built-in compatibility for better-auth. |
…nability Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
|
This PR is very large. Consider breaking it into smaller PRs for easier review. |
There was a problem hiding this comment.
Pull request overview
This pull request successfully refactors the authentication architecture by separating authentication configuration from identity data models, resolving architectural confusion and establishing clear boundaries.
Changes:
- Migrated enterprise authentication configurations (OIDC, SAML, LDAP) from
identity.zod.tstoauth.zod.tsunder a newenterprisefield - Redefined
identity.zod.tsas pure data models (User, Account, Session, VerificationToken) representing "who is logged in" - Created
auth-protocol.tswith wire protocol constants and interfaces (AUTH_CONSTANTS, AuthHeaders, AuthResponse, etc.) - Added database field mapping configuration to support driver compatibility (particularly better-auth)
- Comprehensive test coverage for all new schemas
- Extensive documentation updates with practical examples
Reviewed changes
Copilot reviewed 30 out of 30 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
packages/spec/src/system/identity.zod.ts |
Complete refactoring to define User, Account, Session, and VerificationToken data models; removed authentication configuration schemas |
packages/spec/src/system/identity.test.ts |
New comprehensive test suite covering all identity data models with validation and type inference tests |
packages/spec/src/system/auth.zod.ts |
Added enterprise auth configurations (OIDC, SAML, LDAP), database field mapping schema with better-auth defaults, and integrated into AuthConfig |
packages/spec/src/system/auth.test.ts |
Added thorough tests for enterprise auth schemas and database mapping functionality |
packages/spec/src/system/auth-protocol.ts |
New file defining wire protocol constants, interfaces for headers/responses/errors, and standard error codes |
packages/spec/src/index.ts |
Updated exports with clear comments distinguishing configuration, data models, and wire protocol |
packages/spec/json-schema/*.json |
Generated/updated JSON schemas for all new and modified types; removed deprecated AuthProvider and AuthProtocol schemas |
docs/AUTHENTICATION_STANDARD.md |
Added enterprise SSO examples, database field mapping documentation, and updated architecture overview |
content/docs/references/system/*.mdx |
Updated/created MDX documentation for all new schemas with property tables |
… contained failures into the run-level `failed` (objectstack-ai#16314) (objectstack-ai#18261) Fixes objectstack-ai#16314 Clause-②: no Services half of objectstack-ai#15617's ruling (director seat, decision batch objectstack-ai#55, maintainer 「同意」 on option 1, 2026-09-06). The spec half landed the slot on `68d5dfd0f`; this populates it. ## What moved `ExecutionStepMetrics.failures` is declared as *"node executions that failed inside a child run this execution delegated to and went on from"*, folding into `nodes[].failures` and so into the run-level `failed`. Nothing wrote it, so the engine's fold could not see a child's losses: a parent that delegated its rows reported `failed: 0` while `acted` had rolled up since objectstack-ai#4354. Four producers, each read rather than assumed: | site | what it now reports | | :--- | :--- | | `builtin/subflow-node.ts` | a synchronous child's `summary.failed`, on the **non-failed** exit only | | `builtin/map-node.ts` | the accumulated `failed` of the items that COMPLETED during this entry — `map` does **not** share `subflow`'s roll-up path, so it needed its own | | `engine.ts` · `creditChildRun` | a child that PAUSED, whose parent step was written at suspend time; both call sites are completion paths, which is what puts them inside the declared rule | | `run-summary.ts` · `summarizeRun` | folds `metrics.failures` into `nodes[].failures` and so into `failed` | ## The measured target, driven on the real engine The card's shape from objectstack-ai#15617 — parent `loop { subflow(child) }`, one child failing per five rows: ``` before status=completed selected=5 acted=4 skipped=0 failed=0 after status=completed selected=5 acted=4 skipped=0 failed=1 children failed = [0, 0, 1, 0, 0] (unchanged — the child keeps its own row) ``` **The control, unchanged and pinned.** A child that **failed** rather than contained is the delegating step's own failure, counted once: `call: {runs: 5, failures: 1}`, parent `failed = 1`, with nothing of the child's own `failed` riding up. That is the one place the rule parts from `acted`'s, which does carry a failed child's writes — asserted beside it so neither direction can be made symmetric without a red test.⚠️ One correction to the control's fixture, measured rather than assumed: a `loop` body is fail-fast, so an **unguarded** call to a failing child ends the loop at the third row (`call: {runs: 3}`) and the loop node records a failure of its own beside the call's, giving `failed = 2`. The control's declared numbers are the parent that CONTAINS — the same shape the contained-child case uses, differing only in which level contains. Both readings are in the test's own comments. **A delegating node's `status` does not move.** `FlowRunNodeSummary.status` is declared judged on the node's OWN executions, so a `subflow` step that ran fine and rolled a child's losses up reads `success` with `failures > 0`, and on such a node `failures` may exceed `runs`. The fold takes the status verdict **before** it adds the roll-up; that ordering is load-bearing and has its own ablation leg below. PR objectstack-ai#15609's narrowed wording — *"no node execution **of this run** failed"* — was true only while the declaration's two paragraphs disagreed. It is widened back here in `formatRunSummaryLine`'s comment and in `content/docs/automation/flows.mdx`. ##⚠️ Fence — objectstack-ai#18110 is NOT folded in, and the two are separable objectstack-ai#18110 is a separate, still-ungraded card on the same file, in the opposite direction: a **`refused`** child (the run-OUTCOME sense — an `end` node saying no) rolled up as an ordinary success, the refusal reaching nobody. They are **not** mechanically inseparable, and the reason is mechanical rather than a judgement call: `selected` / `acted` / `unmeasuredEffect` already ride the exact exit a refused child takes today, and `failures` was added to that **same** exit. So this diff adds one total to an existing roll-up and decides nothing new about which child outcomes reach it. Nothing here pins the refused case in either direction — deliberately, so objectstack-ai#18110's fix stays free to decide what a parent does with a refused child's totals, all four at once. ## Reverse verification — six ablation legs Each leg mutates one COMMITTED source file, proves the mutation reached disk by occurrence counts on the anchored text **plus** a blob-hash inequality against `HEAD` (never an editor's exit code), runs the suite, restores with `git checkout HEAD -- path` and proves restoration by blob hash equality with `git diff HEAD` empty. A `trap` on EXIT/INT/TERM restores every touched path by absolute path. Baseline and post-restore runs are both 16/16 green. | leg | mutation | result | | :--- | :--- | :--- | | A | the fold stops adding the roll-up | 9 failed / 7 passed | | B | roll-up added **before** the status verdict | 6 failed / 10 passed — incl. "reads `success` with `failures > 0`" and "`failures` may exceed `runs`" | | C | `subflow` stops rolling a completed child up | 2 failed / 14 passed — the measured target and the node status | | D | the asymmetry removed: a FAILED child's `failed` rides up too | 2 failed / 14 passed — exactly the two control tests | | E | `map` stops accumulating its completed items' failures | 1 failed / 15 passed — the `map` test | | F | `creditChildRun` stops crediting a PAUSED child | 2 failed / 14 passed — both resume directions | A first pass declared B/D/E VOID rather than green: its landing proof used `grep -cF` on a multi-line needle, which splits into per-line patterns and counts lines. The legs were re-run with a python-side occurrence count, and the void reading is reported rather than quietly replaced. No permanent ablation artefact is left behind; the worktree is clean and every restored blob matches `HEAD`. ## Verification - `pnpm --filter @objectstack/service-automation test` — **136 files / 1615 tests passed**. - `pnpm --filter @objectstack/service-automation typecheck` — clean (`tsc --noEmit` + `check:test-typecheck`, 0 files / 0 errors in the test-layer ledger). - **Derived gate families: 92 derived / 92 run / 0 NOT MEASURED / 0 UNRUN.** Derived by `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` from the merge-base change set, reconciled back with `--ran` carrying each command's own exit code captured before any pipe: *"92 derived famil(ies) accounted for — 92 run, 0 NOT-MEASURED (a DERIVED zero — all 92 recorded an exit code and none of them is 3)."* Three of them first answered **PREREQUISITE NOT MET** (`check:skill-examples`, `check:dual-build-cjs-loads` exit 3, `check:type-check-debt` exit 3) — ⛔ not passes; the workspace build closure was built and all three then exited 0. - `eslint . --no-inline-config` over the **whole** repo population — **6764 files, 0 errors, 0 warnings**. Not a narrowed run, so no narrowing needs declaring. - Control-character self-scan over the diff's seven files: no match (`check:nul-bytes` is also in the 92). - Not measured here, declared to CI: the required contexts on the merge group's affected set. ## Clause-②: no — derived, not predicted Derived by **reachability from the published entry**, with controls, ⛔ never from the word `export` and ⛔ never from a grep in `dist/index.js`. The package's `exports` declares one entry (`.` → `dist/index.js` / `dist/index.d.ts`) and `files` ships `dist`; the named-export set of that entry was read from the published type surface and cross-checked against the barrel chain in `src/index.ts`. - **Positive controls** — `summarizeRun`, `formatRunSummaryLine`, `AutomationEngine`, `registerLogicNodes`, `installBuiltinNodes`: REACHABLE, so the derivation can answer yes. - **Negative controls** — `registerSubflowNode` and `registerMapNode` are `export function` in their own modules and re-exported by `src/builtin/index.ts`, yet the root barrel's explicit list names six builtins and not those two ⇒ **unreachable by name**; `creditChildRun` (a `private` member) and a nonexistent identifier: also unreachable. So the derivation can answer no, and it is not a grep for `export`. - The delivered diff adds **zero** export statements outside the test file, and **zero** keys to any published payload — `metrics.failures`, `nodes[].failures` and `failed` are all keys `packages/spec` already declares.⚠️ Declared honestly: what moves is the **value** on an already-published payload — a delegating parent's `failed` changes from "this run's own node failures" to "what this run caused". That is conformance to a declaration already on `main`, not a widened surface or a relaxed acceptance set. ## Acceptance notes - ⛔ `packages/spec` untouched — the contract said everything needed, including the failed-child boundary and the `status`-judged-on-own-executions rule, both of which this implements verbatim. - The card's re-check grep names `packages/services/service-automation/src/subflow-node.ts`; the file lives at `src/builtin/subflow-node.ts`. Path drift only — the premise holds. Noted, not filed. - `loop { subflow(failing child) }` unguarded answers `failed = 2` for one lost row: the loop node's own failed execution plus the subflow step's. Literally correct under *"total node executions that failed"*, and unchanged by this diff. Noted, not filed. - The refused-child roll-up is objectstack-ai#18110's, left exactly as found. Noted, not filed here. Authored by Claude Code in session `session_01URLHobLUJB9K1ABV6ofdjj`. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…psMap rows, derived from the renderers' read points (objectui#8348 Q1-C) (objectstack-ai#18403) Fixes objectstack-ai#18305 Executes the objectui#8348 ruling 「8348 以协议为准」 (decision batch objectstack-ai#83, 2026-09-08) and batch objectstack-ai#136 item 3 (Q1-C, maintainer 「同意」): `ComponentPropsMap` gains `object-map`, `object-gantt` and `object-tree`, each row's key set derived the **objectstack-ai#7751 way — from the objectui renderer's own read points**, at the `.objectui-sha` pin `53ded82b`. Not derived from `@object-ui/types`' mirror. That mirror standing in as the authority for map and gantt is the defect this card closes, and for `object-tree` the mirror is measurably wrong about four keys (below). ## The objectstack-ai#7751 derivation table, extended by three rows Every citation is a line in the objectui checkout at `53ded82b`. The shared record-source ladder is `resolveRecordSourceConfig` in `core/src/utils/record-source.ts` — rung 1 `data` (`:151`, returned VERBATIM as a `ViewData`), rung 2 `staticData` (`:155`, wrapped into `{ provider: 'value' }`), rung 3 `objectName` (`:162`). ### `object-map` — `plugin-map/src/ObjectMap.tsx`, registry shell `plugin-map/src/index.tsx` | key | read point | declared as | |---|---|---| | `objectName` | ladder rung 3; `:793` (fetch dep), `:890` (navigation) | `z.string().optional()` | | `data` | `:169` array-shorthand head, then `resolveRecordSourceConfig` at `:174` | `ViewDataSchema` | | `staticData` | ladder rung 2 | `z.array(z.unknown())` | | `filter` | `:742` — `$filter: schema.filter` | `z.array(ViewFilterRuleSchema)` | | `sort` | `:743` — `$orderby: convertSortToQueryParams(schema.sort)` | `z.array(SortItemSchema)` | | `map` | `:370` — `getMapConfig` branch 1, the author face; the registration's declared `{ name: 'map', type: 'object' }` input | `z.unknown()` (see note) | | `mapStyle` | `:365` — `schema.mapStyle \|\| schema.map?.style` | `z.string()` | | `navigation` | `:889` — `useNavigationOverlay` | `z.unknown()` | | `enableClustering` | `:905` — `enableClustering ?? (schema.enableClustering \|\| …)` | `z.boolean()` | Measured and deliberately NOT declared: the flat `map`-config spellings `getMapConfig` branch 2 reads at `:382-396` (`locationField`, `latitudeField`, `longitudeField`, `titleField`, `descriptionField`, `zoom`, `center`) — the ObjectView/ListView flatten product, ruled an internal transport form, not a second authoring surface (maintainer ruling objectui#5018, 2026-08-17); `style`, which `:283` reads ONLY to say it is not consumed as a map style (objectui#5017) and which is the node's own inline CSS record; and the React props of `ObjectMapProps` (`clusterRadius`, the `data` ARRAY prop, the four callbacks, `className`, `dataSource`). ### `object-gantt` — `plugin-gantt/src/ObjectGantt.tsx`, registry shell `plugin-gantt/src/index.tsx` | key | read point | declared as | |---|---|---| | `objectName` | ladder rung 3; `:1359` (layout key), `:1491` (navigation), `:1873` (export name) | `z.string().optional()` | | `data` | `resolveRecordSourceConfig` at `:593` | `ViewDataSchema` | | `staticData` | ladder rung 2 | `z.array(z.unknown())` | | `filter` | `:738` — `$filter: schema.filter` | `z.array(ViewFilterRuleSchema)` | | `sort` | `:739` — `$orderby: convertSortToQueryParams(schema.sort)` | `z.array(SortItemSchema)` | | `gantt` | `:499-501` — `getGanttConfig` branch 1; the registration's `{ name: 'gantt', type: 'object' }` input, and `:501` validates it against this repo's OWN `GanttConfigSchema` | `GanttConfigSchema` | | `navigation` | `:1487` — `schema.navigation ?? { mode: 'drawer' }` | `z.unknown()` | | `label` | `:1871` — `resolveInlineI18nLabel(schema.label, displayLocale)` in the export-file-name chain (`:1849` is the comment ABOVE that chain, not a read) | `I18nLabelSchema` | | `skipWeekends` | `:1205` — `!!sw` into the working calendar | `z.boolean()` | | `holidays` | `:1206` — `new Set(hol)` for the duration math | `z.array(z.string())` | | `persistLayout` | `:1357` — `schema.persistLayout === false` | `z.boolean()` | | `viewName` | `:1359` — the `objectName:viewName` storage key | `z.string()` | | `markers` | `:1826` — `markers={schema.markers}` | `z.array(z.unknown())` | | `criticalPath` | `:1829` — `criticalPathDefault={!!schema.criticalPath}` | `z.boolean()` | | `showBaselines` | `:1832` — `schema.showBaselines !== false` | `z.boolean()` | | `readOnly` | `:1833`, `:1916` — `!!schema.readOnly` | `z.boolean()` | | `mobileReadOnly` | `:1834` — `schema.mobileReadOnly !== false` | `z.boolean()` | Measured and deliberately NOT declared: the 30 flat `GanttConfig` spellings branch 2 reads at `:513-543` (objectui#6469 inherited the objectui#5018 ruling for this block); `title`, which this renderer never reads; and a row cap — the reload's `$top` is the platform ceiling `NON_GRID_ROW_CEILING_TOP`, which the renderer's own comment marks 「⛔ Not authorable」. ### `object-tree` — `plugin-tree/src/ObjectTree.tsx`, registry shell `plugin-tree/src/index.tsx` | key | read point | declared as | |---|---|---| | `objectName` | ladder rung 3; `:534`, `:570`, `:605` | `z.string().optional()` | | `data` | `resolveRecordSourceConfig` at `:359` — rung 1, returned VERBATIM as a `ViewData`. That ONE site is the whole support for this arm | `ViewDataSchema` | | `staticData` | ladder rung 2 | `z.array(z.unknown())` | | `filter` | `:474` — `$filter: schema.filter` | `z.array(ViewFilterRuleSchema)` | | `tree` | `:108` — the nested block `getTreeConfig` reads; the registration's `{ name: 'tree', type: 'object' }` input | `TreeConfigSchema` | | `navigation` | `:591` — `useNavigationOverlay({ navigation: (schema as any).navigation })` | `z.unknown()` | **`data` IS on the row, and that is the measurement, not family symmetry.** The card left this conditional — 「if the renderer's authored surface has no `data`, the row declares none」 — and the condition is FALSE here: the renderer reaches `schema.data` through the shared ladder at `:359`, which returns it verbatim as a `ViewData`. ⛔ `:496` is **not** a second site for that arm, and an earlier revision of this body over-claimed it as one: `(rest as any).data ?? (schema as any).data` is gated by `Array.isArray(passed)` on the very next line, so it honours only the bare-ARRAY shorthand this row **refuses** — the same shorthand `object-map` measures and declines above. One ladder site is sufficient and `:359` is it, so the row stands on a corrected citation. What is absent is the DECLARATION, on both published faces: `ObjectTreeSchema` on `@object-ui/types` at this pin declares no `data`, no `staticData`, no `filter` and no `navigation` at all, and makes `objectName` required — four keys the renderer reads. That is precisely the mirror-is-wrong shape the ruling puts the protocol in front of, so the row follows the read points and the mirror is the face that has to follow. **No `sort`.** `ObjectTree.tsx:473-484` issues `$filter`, `$top` and `$expand` and no `$orderby`, and nothing else in the file reads an order. A `sort` door here would publish a key with no read site — the exact defect objectstack-ai#7751 exists to remove, in the other direction. ## Three derivation decisions, each pinned rather than left to review 1. **The flat config spellings stay unauthorable on all three.** `ObjectView` / `ListView` build these nodes by spreading `options.map` / `options.gantt` / `options.tree`'s CONTENTS at the top level, carrying no block key at all; that is an internal transport form (objectui#5018, inherited by objectui#6469), and the map and gantt renderers name every flat key beside a present block as IGNORED in a dev warning. Writing one now gets a wrong-layer prescription naming the config block — the channel `object-calendar` already uses. Each set is held equal to the config block it points at (`ListMapConfigSchema.shape`; `GanttConfigSchema.shape` plus the legacy singular `dependencyField`; `TreeConfigSchema.shape` plus `titleField`, which `getTreeConfig:117` reads only as `labelField`'s last fallback), so it cannot drift silently. 2. **`filter` and `sort` are the family's one orthography from birth** — `ViewFilterRule[]` (ui#6206-B reaching the family: objectstack-ai#15449, batch objectstack-ai#55 option A) and `SortItem[]` (objectui#8221, batch objectstack-ai#77 option B). A new door on a family-wide ruling has no "measured before the ruling" arm. The whole-map census pins in `component.test.ts` already demanded it: every `filter` door must refuse the record form, and only `record:related_list` may take a string `sort`. 3. **Config-block VALUES follow the read point.** `gantt` takes `GanttConfigSchema` because `ObjectGantt.tsx:501` literally validates the authored block against that schema, imported from `@objectstack/spec/ui`. `tree` takes `TreeConfigSchema`, which objectstack-ai#15469 shut against unknown keys on this very measurement, re-measured unchanged here. `map` stays `z.unknown()` — and that is measured, not conservative-by-default: the spec's own `ListMapConfigSchema` is strict and declares no `style`, the key `getMapConfig:365` reads at `schema.map?.style`, so pointing this door at it would refuse a value the renderer honours today. The pin records the reason so the later value ratchet has one. ## Measured file face 13 files, all additive to a published surface; nothing retired, nothing narrowed. ``` .changeset/18305-component-props-map-map-gantt-tree.md new packages/spec/src/ui/component.zod.ts +3 schemas, +3 rows, +3 guidance sets packages/spec/src/ui/component.test.ts +1 describe, 4 door lists extended packages/spec/src/ui/filter-rule-array-guidance.test.ts +3 doors (seven -> ten) packages/spec/src/ui/component-type-vocabulary.test.ts +1 pin packages/spec/authorable-surface/ui.json +32 rows (generated) packages/spec/json-schema.manifest/ui.json +3 entries (generated) packages/spec/api-surface/ui.json +9 exports (generated) packages/spec/export-origins/ui.json generated packages/spec/declaration-map/ui.json generated content/docs/references/ui/component.mdx generated content/docs/references/index.mdx generated docs/audits/2026-07-unknown-key-strictness-ledger.counts.md generated (444 -> 447 sites) ``` No corpus moves: a whole-tree grep for an authored `object-map` / `object-gantt` / `object-tree` page node finds zero outside `sdui.manifest.json` (a registry dump, not authored metadata), against a positive control that finds `object-grid` authored twice in `examples/app-showcase`. `PageComponentSchema.type` already accepted all three through its open string arm and still does; what changes is that an authored props bag on one of them is judged instead of skipped. ## Verification All readings below are at `e7967ce543` (the branch tip) unless a row says otherwise, exit codes captured by redirect-then-`$?`, never through a pipe. The spec suite, typecheck and the generated-artifact checks were re-run after the patch round below. | run | verdict | |---|---| | `pnpm --filter @objectstack/spec build` | exit 0 | | `pnpm --filter @objectstack/spec test` | exit 0 — 482 files, 13727 tests passed | | `pnpm --filter @objectstack/spec typecheck` | exit 0 (`tsc --noEmit` + `check:scripts-typecheck` + `check:test-typecheck`) | | `pnpm --filter @objectstack/lint test` | exit 0 — 103 files, 3821 passed, 5 skipped (the `ComponentPropsMap` consumer; its closure built first) | | `pnpm --filter @objectstack/lint typecheck` | exit 0 | | `pnpm --filter @objectstack/spec check:generated` | exit 0 after `--fix` regenerated the 5 it proved stale | | `MANIFEST=… check:react-declaration-parity --baseline … --strict` | exit 0 — `object-map` 2 both / 7 spec-only / **0 registry-only**, `object-gantt` 2 both / 14 spec-only / **0 registry-only**; baseline ratchet: no new divergence | | `pnpm exec eslint . --no-inline-config --format json` | exit 0 — **6786 files, 0 errors, 0 warnings**, repo-wide, no narrowing | | 47 further derived gates (`scripts/pm/dispatch-gates.mjs --commands`) | all exit 0 — see the acceptance notes for the one that did not | `packages/spec` has no workspace dependencies, so the dependency-closure build is an empty run by construction. CI on this head is **not enumerated here** and is not claimed green — the verdicts above are local runs only. ## Patch round — the at-tier review's two text findings, and the citations The contract review returned PASS; these are the three things its scope did not settle, all of them text or citation, with **no key, no value shape and no row membership moved**. Verified after the round rather than asserted: the three key sets read back identical, `ComponentPropsMap` still carries 48 rows, `object-tree` still accepts the `ViewData` object arm and still refuses the bare array, the gantt block is still strict, `object-chart` is still absent. 1. **`object-gantt`'s describes said "chart".** Every sibling names its own noun, and `object-chart` is the row this section deliberately leaves absent, so "chart" pointed an author at the one block with no row. Three describes now name gantt (`objectName`, `label`, `readOnly`). **Proof on the shipping artifact** — `content/docs/references/ui/component.mdx`, regenerated with the repo's own `gen:docs`, never by hand: `Object this chart binds to` → **0**; `Object this gantt binds to` → **1**, at `:398`; control `Object this NOUN binds to` → **6**, unchanged, so all six blocks name their own noun exactly once. 2. **`object-tree`'s `objectName` gave a reason that is false for tree.** It said the component-level `dataSource` binding can supply the object. Measured at the pin `53ded82b`: `plugin-tree/src/index.tsx` has **0** hits for the `ElementDataSource*` wiring, against **7** each in `plugin-map`, `plugin-gantt`, `plugin-grid` and `plugin-calendar` — four controls, so the zero discriminates. The key stays optional; the reason is now the true one, the ladder's first two rungs (`data` can name the object itself, `staticData` needs none), plus the explicit negative. In the generated doc the false reason reads **0** hits and the new line **1**, at `:768`. 3. **Two citations corrected**, in the tables above: gantt `label` at `:1871` (`:1849` is the comment), and tree `data` on `:359` alone. `check:docs` was red on `e3a256daa3` — source fixed, shipping artifact not yet regenerated — and is green on `e7967ce543`: `check:docs` exit 0, "223 generated files in sync with packages/spec"; `check:generated` exit 0, "All 15 generated artifacts are up to date"; `check:authorable-surface` exit 0, which is why `gen:schema` had nothing to write and `gen:docs` was the only stale artifact. `pnpm --filter @objectstack/spec test` exit 0, 482 files / 13727 tests, and `typecheck` exit 0, both re-run after the round. `git diff --name-only origin/main...HEAD -- content/docs/releases/` → **0** paths, against a control of **2** under `content/docs/references/`. On this head `Type Check · source gates` is **completed / success** — the job the red had truncated now reaches its end. The rest of CI is still running and is not claimed green. ## Patch round 2 — the at-tier review's FAIL: F1 blocking, F2 and F3 in passing Review of record: comment `5696414075` on this PR, `VERDICT: FAIL` at head `e7967ce543`. That review re-derived the accept set, the public surface, all three key sets, every value posture and the `minor` level and judged them RIGHT — nothing in this round moves a key, a value shape, a row membership or the level. Text, plus the one regeneration it forces. **F1 (blocking) — six describes named a function that does not exist in the code they describe.** The `objectName` / `data` / `staticData` describes on `object-gantt` and `object-tree` said the record source is resolved by, or "read FIRST/SECOND by", `getDataConfig`. Re-measured at the `.objectui-sha` pin `53ded82b`, each file fetched with `git show` in the same command block as its count: | file at `53ded82b` | `getDataConfig` | `resolveRecordSourceConfig` | |:---|---:|---:| | `plugin-gantt/src/ObjectGantt.tsx` | **0** | 2 — import `:64`, call `:593` | | `plugin-tree/src/ObjectTree.tsx` | **0** | 3 — import `:41`, call `:359`, comment `:422` | | `plugin-map/src/ObjectMap.tsx` (control) | **8** | 3 | The control discriminates, and it also settles what the map keeps: `ObjectMap.tsx` really does hold a local `getDataConfig` at `:135` — the array-shorthand head, delegating to the shared ladder at `:174` — so the three map describes are TRUE and keep the name. Gantt and tree reach the shared three-rung ladder directly: `resolveRecordSourceConfig` in `@object-ui/core` `utils/record-source.ts:146`, rungs at `:151` (`data`, returned verbatim), `:155` (`staticData`, wrapped as `{ provider: 'value', items }`) and `:162` (`objectName`, folded to `{ provider: 'object', object }`). So the ladder ORDER those six sentences describe was already true and only the name was wrong; the six now name `resolveRecordSourceConfig`. Neither renderer has any reader above its ladder call, so "read FIRST" is literal for both — unlike the map, whose array head runs first, which is the second reason its describes keep the wrapper's name. **Both sides counted, because this trap already bit this PR once.** An earlier round fixed a false describe in the source alone and `check:docs` caught the generated doc still carrying the old text. After `gen:docs`, at `c431c22e05`: | | `getDataConfig` | `resolveRecordSourceConfig` | |:---|---:|---:| | `packages/spec/src/ui/component.zod.ts` | **3** — map only, `:3226` `:3242` `:3244` | 11 | | `content/docs/references/ui/component.mdx` | **3** — map rows only, `:638-640` | **6** — gantt `:398-400`, tree `:768-770` | Both sides read 9 and 0 before the round. The generated diff is exactly six table rows; no map row moved. **F2** — `component.zod.ts:3545`, the tree `data` field JSDoc, cited two read points where the schema header at `:3487` already rules that `:496` is not a second site for the object arm. It cites `:359` alone now. Source comment; no artifact follows it. **F3** — two citations off by one line, both re-read at the pin: `getGanttConfig`'s `safeParse` is `ObjectGantt.tsx:500` (`:501` is the `if (!result.success)` beneath it), and the tree's "Not authorable" marker is `ObjectTree.tsx:482`, not `:477`. ### Verification for this round - `pnpm --filter @objectstack/spec build`, then `check:generated` — exit 0, all 15 artifacts up to date including `check:docs`, with `content/docs/references/ui/component.mdx` actually regenerated by `gen:docs` rather than hand-edited. Before the regeneration the same gate named that one artifact, and only it, stale. - `pnpm --filter @objectstack/spec test` — exit 0, 482 files / 13727 tests. `pnpm --filter @objectstack/spec typecheck` — exit 0. - `check:objectui-pin-citations` — exit 0, 26 asserting pin citations match `53ded82bf`. It verifies the sha, not the line numbers, and reports 0 anchor content assertions here: F3 was measured by hand at the pin, not caught by this gate. - 34 of the 109 gate families `dispatch-gates` derives for this change set were run locally, every one exit 0; the remaining 75 are the repo-wide farm CI owns. A declared narrowing, not a silent skip. - `pnpm lint` narrowed to the diff, with the three readings that make the narrowing a measurement: the population is eslint's own `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` and eslint itself answers the regenerated `.mdx` with "File ignored because no matching configuration was supplied", so the linted population of this diff is one file; `--format json` reports 1 file, 0 errors, 0 warnings; and no type-aware linting is configured anywhere in `eslint.config.mjs` (no `projectService`, no `parserOptions.project`), so a comment-and-describe edit in one file cannot move any untouched file's verdict. ## Acceptance notes ### Docs Drift Check, re-verified rather than waved through The drift check flagged 6 hand-written pages. Each was re-read; none needs an edit, and the readings are here rather than implied by their absence from the diff. - **`content/docs/protocol/objectui/layout-dsl.mdx` does not ride this PR — measured, not assumed.** It is flagged via the symbol `ComponentPropsMap`, which it names exactly once, at `:721`, and as a MECHANISM: *"For the platform's own types the authoring rules dispatch `ComponentPropsMap` and reject a misspelled prop; a `custom.*` type has no entry there."* That sentence is still exactly true after this change — three more types now have entries, and it enumerates none. The page carries no register of the object-block family at all: no table, no list, and its only numeric completeness claim is about breakpoints (`:579`). **The discriminating control:** `object-calendar` and `object-form` each get **0** hits on that page, and both have carried `ComponentPropsMap` rows since objectstack-ai#7751 landed on 2026-08-12 — so the page already omits 2 of the 6 pre-existing rows, against a positive control of `object-metric` (3), `object-master-detail-form` (3), `object-kanban` (2), `object-grid` (1). A page that has never tracked the row set is not a list this change makes incomplete; its `### Metric Widget` / `### Master-Detail Forms` / `### Kanban Board` sections are chosen worked examples. Writing three new hand-written renderer sections would be new documentation of three renderers, which is a card, not a rider on "add three rows". - **The other five pages are literal-name collisions, not semantic links.** `content/docs/ui/views.mdx`, `concepts/architecture.mdx`, `getting-started/common-patterns.mdx`, `protocol/kernel/i18n-standard.mdx` and `ui/field-grouping-and-order.mdx` each get **0** hits for `ComponentPropsMap` and **0** for `object-map` / `object-gantt` / `object-tree`. They are reached only through string literals that this PR's new `OBJECT_GANTT_FLAT_CONFIG_GUIDANCE` key array happens to contain — `groupByField`, `timeZone`, `colorField`, `startDateField` and friends. `views.mdx` documents the LIST-VIEW `gantt` block (`type: 'gantt'` with `gantt: { startDateField, … }`, `:340-352`), whose schema is `ListViewSchema.gantt` / `GanttConfigSchema` in `view.zod.ts` — a file this diff does not touch at all, so that page's vocabulary is unmoved. - **Release-owned pages: untouched, with a control.** The drift check names 7 pages under `content/docs/releases/`; they are read-only. `git diff --name-only origin/main...HEAD -- content/docs/releases/` returns **0** paths, against a control of **2** paths under `content/docs/references/` in the same diff, so the zero is a reading and not an empty filter. ### `component-reference-rail.test.ts` needs no change — the reading, not the omission The card names it among "the vocabulary tests that pin the row set". Measured on this branch: the file is 164 lines, mentions `ComponentPropsMap` 4 times, and **every one of them is the single subscript `['record:reference_rail']` or prose about that one row** (`:3`, `:21`, `:33`, `:35`). `Object.keys(ComponentPropsMap)` → **0** hits; any `object-` type literal → **0** hits. Control, the same two probes on `component-type-vocabulary.test.ts`, which genuinely does pin the row set: **6** and **8** ⇒ the zeros discriminate. All twelve of its `describe`/`it` subjects are `record:reference_rail` behaviour. It reads no property of the row SET, so nothing in it can move when three `object-*` rows land — and it passed unchanged inside the 482 green spec files above. The row-set pins that DO move with the rows are `component.test.ts` (six ruled blocks to nine, plus the filter / sort / plural-`filters` / optional-`objectName` door lists, plus a new describe) and `component-type-vocabulary.test.ts`. The *reference rail* that regenerates is the docs one, `content/docs/references/ui/component.mdx`. ### Out of scope for this PR, noted rather than fixed Each with the measurement that found it: - **`ListMapConfigSchema` declares no map style at all.** The spec's list-view map block is strict and has neither `style` nor `mapStyle`, while `ObjectMap.tsx:365` reads `schema.map?.style` and objectui's `ObjectMapConfigSchema` declares `style`. `ListMapConfigSchema.safeParse({ style: '…' })` is refused — pinned as a negative in this PR's own tests, because it is the reason `object-map.map` stays `z.unknown()`. An author cannot declare a map style through the spec's list-view face today. Carrier: this PR's `map` value ratchet, whenever it is taken. - **`object-tree` is absent from the tracked `sdui.manifest.json`.** `check:react-declaration-parity` prints `object-tree: NO component in the manifest — not registered or not public`, although `plugin-tree/src/index.tsx` registers it under both `object-tree` and `tree`. The baseline ratchet is unaffected (a block with no baseline row cannot regress), so this PR is green either way, but the new row gets no parity comparison. Carrier: the next `sdui:manifest` regeneration. - **`pnpm check:cross-package-test-inputs` is sensitive to local build state, not to this diff.** It exits 1 in a worktree where `packages/spec/dist/` exists and 0 where it does not — measured here as a one-variable control, with the diff held constant and the directory restored byte-identically (216 files, `dist/ui/index.mjs` hash unchanged). It names `packages/cli/test/init-created-files-summary.e2e.test.ts`, which symlinks `packages/spec/dist` at `:115`; this PR touches neither `packages/cli` nor `turbo.json`. The `Lint & Repo Gates` job runs it on an unbuilt tree, which is why CI is green on it. - **`objectui#9239` has moved since the card was written.** The card describes it as an open, separate calendar-mirror contradiction; it is `closed completed`. Nothing in this PR depends on it, and `object-calendar` is untouched. - **objectui's own gantt face carries the same false attribution F1 removed from ours, and it is pre-existing.** At the same pin `53ded82b`, `packages/types/src/zod/objectql.zod.ts` docblock `:726` states `getDataConfig` is in `plugin-gantt/src/ObjectGantt.tsx`, and three `ObjectGanttSchema` describes repeat the name (`:738`, `:739`, `:838`) — while that file has 0 occurrences of it (control: `ObjectMap.tsx` 8, and the map's own mirror describes at `:641-643` are correspondingly TRUE). Separately, `core/src/utils/record-source.ts` quotes our describes as the ruled contract at `:28`, `:92`, `:95` and `:97` and attributes the text to the map and gantt zod twins, so after this PR that quotation matches the map twin and no longer matches the gantt one. Both sites are objectui's, in objectui's repo; nothing here can fix them and the pin does not move. Handed to the seat to file against objectui rather than fixed in passing. Not touched, deliberately: the `object-chart` deliberate-absence note in `component.zod.ts`, pinned as still absent in the new tests. objectui#8348 carries `pm:blocked` on this card; its remaining slice judges map / gantt / tree against these rows once this lands. Nothing here changes any objectui file. Authored by the `domain:spec` execution seat, session `session_01KB5PFtxuy1x3dcR5gxudx6`. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…ction-*, data-* and element-* migration entries states each lesson in words, not tracker numbers (stage 4) (objectstack-ai#20509) Part of objectstack-ai#20233 Stage 4: the datasource-, filter-, action-, data- and element- families. Clause-②: no **Stage 4 of a staged card.** The card stays open for later stages; this PR carries no closing keyword. Text only: no entry id, `surface`, `from` / `to`, conversion or matching logic moves, and the chain rewrites exactly what it rewrote before. ## What this does `os migrate meta` prints every ADR-0087 semantic entry it crosses as one block: `⚠ [protocol N] SURFACE → REPLACEMENT`, then `why:` (the entry's `reason`) and `verify:` (its `acceptanceCriteria`). AGENTS.md's runtime-string rule applies to all of it: 「Runtime strings — refusal prose, prescriptions, anything an author is shown — carry no tracker number (`pnpm check:doc-authoring`): the lesson goes into the text.」 Form **D** of ruling C+D on objectstack-ai#19123 (`5749154545`) sets the shape: the lesson in words, and no number, dead or alive; a cross-repo number (`cloud#N`, `objectui#N`) is still a tracker number. This stage covers the next five families, `datasource-`, `filter-`, `action-`, `data-` and `element-`: **150 sites → 0** in the three prose fields, across 33 entry files. None of the 37 entries in these families carries a tracker id in `surface` (ruling A of the stage-1 ACCEPT, `5858839916`, is checked and has nothing to do here). Each site now says what the cited ruling, measurement or fix decided. ADR ids stay. `registry.ts`, `spec-changes.json` and `docs/protocol-upgrade-guide.md` are regenerated from the entries (`gen:migration-registry`, `gen:spec-changes`, `gen:upgrade-guide`), never hand-edited. The pin now holds eleven families. ## Census — tracker ids in the author-shown fields **Instrument.** The stage-2 AST instrument, re-written to its published description (the stage-2 and stage-3 copies lived in removed scratchpads): a TypeScript-AST walk over every `packages/spec/src/migrations/entries/**/*.ts`. For each entry object literal it evaluates the string value of `replacement`, `reason`, `acceptanceCriteria` and (counted separately) `surface`, joining string literals with `+`, then counts `#` followed by 4 or 5 digits at a word boundary. **Validated first** by reproducing earlier readings on their own trees (extracted with `git archive`): on `443b2f4fdc` (stage 1) `engine-` 67, `datasource-` 43, `filter-` 30, `action-` 26, `element-` 25, `data-` 24, whole tree 266 entries / 1,016 sites / 9 `surface`; on `0d7ed5a378` (stage 3 landed) whole tree **711** sites / 7 `surface` — every figure equal to the stage-1 census and to stage 3's after-count. Unevaluable fields: 0. **Controls, same run.** - **Lit:** `17.aggregation-node-distinct-retired.ts` reads 7 sites (replacement 1, reason 6), before and after. - **Dark (comment lines):** 823 `//` / docblock lines in entry files carry a tracker id, and none is counted; 823 before and after. Comment lines are the sibling card objectstack-ai#20234's surface, and this PR touches none (proved below). - **Dark (field boundary):** the 7 `surface` sites left in the tree (other families) count 0 in the three-field total. **Base `fc0db22b`:** `datasource-` 9 entries, **43** sites (3 / 38 / 2); `filter-` 11, **32** (1 / 30 / 1); `action-` 6, **26** (0 / 23 / 3); `element-` 5, **25** (2 / 19 / 4); `data-` 6, **24** (0 / 24 / 0). **150** sites (6 / 134 / 10) in 33 of the 37 entries; 88 distinct ids (80 bare, 8 `objectui#`, 2 `cloud#`). Whole tree: 310 entries, **721** sites (711 after stage 3, plus entries `main` added since in other families), 7 `surface`. **After this PR:** all five families **0**; the six earlier families still 0; whole tree **721 → 571**; `surface` 7 (other families). The PM's rough line count (156 sites, 33 files) is a wider instrument; the AST reading is 150 in the same 33 files. | entry | sites (replacement / reason / acceptanceCriteria) | short `#NN` | |---|---|---| | `17.action-descriptor-is-async-retired` | 2 (0 / 2 / 0) | | | `17.action-descriptor-resume-authority-default-flip` | 10 (0 / 7 / 3) | | | `17.action-session-roles-to-positions` | 12 (0 / 12 / 0) | | | `17.data-driver-find-stream-retired` | 1 (0 / 1 / 0) | | | `17.data-driver-query-omit-object` | 9 (0 / 9 / 0) | | | `17.data-engine-batch-retired` | 1 (0 / 1 / 0) | | | `17.data-field-changed-event-retired` | 4 (0 / 4 / 0) | | | `17.datasource-config-inline-credential-refused` | 1 (0 / 1 / 0) | | | `17.datasource-config-placeholder-refused` | 5 (0 / 5 / 0) | | | `17.datasource-config-url-userinfo-refused` | 4 (0 / 4 / 0) | | | `17.filter-regex-options-retired` | 8 (0 / 8 / 0) | | | `18.action-bulk-dispatch-contract-undeclared` | 1 (0 / 1 / 0) | | | `18.action-engine-facade-find-query-envelope` | 1 (0 / 1 / 0) | 1 | | `18.data-file-value-duration-unit-in-key` | 6 (0 / 6 / 0) | 1 | | `18.data-nosql-query-options-timeout-unit-in-key` | 3 (0 / 3 / 0) | 1 | | `18.datasource-config-mongo-options-credential-refused` | 5 (0 / 5 / 0) | | | `18.datasource-config-postgres-url-unparseable-refused` | 4 (0 / 4 / 0) | | | `18.datasource-config-url-query-credential-refused` | 4 (0 / 4 / 0) | | | `18.datasource-credentialsref-mongo-composed-no-username-refused` | 10 (2 / 7 / 1) | | | `18.datasource-credentialsref-mongo-url-no-user-refused` | 10 (1 / 8 / 1) | | | `18.element-data-source-and-object-block-filter-rule-array` | 11 (1 / 9 / 1) | 1 | | `18.element-number-filter-rule-array` | 7 (0 / 6 / 1) | | | `18.element-record-picker-filter-rule-array` | 7 (1 / 4 / 2) | | | `18.filter-between-blank-endpoint-refused` | 3 (0 / 3 / 0) | 1 | | `18.filter-between-field-reference-endpoint-refused` | 5 (1 / 4 / 0) | | | `18.filter-comparand-types-and-widget-nested-slots-refused-at-save` | 2 (0 / 2 / 0) | | | `18.filter-equality-array-comparand-refused` | 1 (0 / 1 / 0) | | | `18.filter-equality-array-comparand-refused-at-save` | 1 (0 / 1 / 0) | | | `18.filter-icontains-comparand-refused-at-parse` | 3 (0 / 3 / 0) | | | `18.filter-ne-array-comparand-refused` | 1 (0 / 1 / 0) | | | `18.filter-preset-ordering-comparand-refused` | 4 (0 / 4 / 0) | | | `18.filter-query-face-comparands-refused-at-save` | 1 (0 / 1 / 0) | | | `18.filter-text-operator-declared-type-refused` | 3 (0 / 2 / 1) | 1 | | **total, 33 entries** | **150 (6 / 134 / 10)** | **6** | ## Text only — proved by a base-vs-head AST comparison For every entry file this PR changes, both versions (`fc0db22b` and the head) are parsed and three things are compared token for token: every import declaration, every property other than the three prose fields (so `id`, `surface`, `from` / `to` and any matcher, by evaluated value), and every comment token in the file. **33 files compared, 0 with a non-prose change.** The instrument is shown able to fail first: on an in-memory copy it reports DETECTED for a mutated `id`, a mutated comment and a mutated `surface`. So none of objectstack-ai#20234's comment lines moved, and no entry's identity or matching moved. ## Every citation read, and what the text now says Each cited id was read with a single-card REST read (body plus the ruling, measurement or landing comments), resolved against the repository its sentence names. Ids are in code spans so this body posts no cross-references. **10 bare ids answer 404** on both the issues and the pulls endpoint (re-probed with a 200 control, `14478`), and **2 `cloud#` ids are unreadable from this session** (403); those sentences are rewritten from what `main` records, listed in Acceptance notes. **`datasource-` (14 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `7990` | Maintainer, 2026-08-12, Option A: close inline credentials per artefact (each schema refuses at publish and diverts to its existing mechanism); the heuristic `sys_metadata` write guard parked. | "The maintainer ruled on 2026-08-12 to close that per artefact: each schema that admitted an inline credential refuses it at publish …"; "the inline-credential closure" | | `8078` (PR) | The spec half of that ruling; measured that `${…}` placeholders reach the client verbatim and that `config.url` still took the secret. | "measured by the credential census while the inline-credential refusal was being built"; "measured when the inline-credential refusal was built" | | `8082` | Maintainer, 2026-08-12, option A: refuse URL userinfo at publish through one shared value-level parse; runtime DSNs unaffected. | "The maintainer ruled on 2026-08-12 (Option A) to refuse the URL userinfo password at publish, through one value-level parse the driver schemas share" | | `8336` | Maintainer, 2026-08-13, direction 2 of two: refuse `${…}` at publish; implementing resolution rejected. | "The maintainer ruled on 2026-08-13 for the second of two directions: refuse the syntax loudly at publish" | | `8337` | The credential-bearing URL query parameter refusal, one syntax over from userinfo. | "the credential-bearing URL query parameters"; "the query string was the third spelling of the identical secret, one syntax over" | | `8876` | **404** — see Acceptance notes. | "the asymmetry the URL grammar keeps between its two userinfo halves — a username is not credential material" | | `8696` | **404** — see Acceptance notes. | "measured when the bound secret was made to reach the mongo client on its URL branch"; "`new URL()` rejects the multi-host form outright, and the mongo arm hands the authored URL to its client untouched"; "the defect class closed when each DSN branch was made to inject the bound secret its composed branch already used" | | `9041` | **404** — see Acceptance notes. | "the sibling URL-branch entry, `datasource-credentialsref-mongo-url-no-user-refused`"; "this URL-branch refusal" | | `9147` | The composed-branch twin: bound secret + no `url` + no `username` refused, inheriting the URL-branch ruling. | "this composed-branch refusal" | | `7314`, `7385`, `8152`, `8875` | The driver-factory arms that dropped something declared: the turso loader's install remedy and half its config, the other optional arms' missing-package remedy, turso's never-read bound secret, and (`8875`, **404**) the mysql DSN branch. | "the family of driver-factory arms, closed one driver at a time, that each dropped something declared without a word: the optional-driver arms that answered a missing package with no remedy, the turso arm that never read its bound secret, and the mysql and mongo DSN branches that discarded one" | | `8873` | **404** — see Acceptance notes. | "the postgres equivalent is judged on its own client's measurement, never inherited" | **`filter-` (24 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `4706` | Maintainer, 2026-08-06, option B: retire `$regex` loudly and add `$icontains`; five-backend true regex (A) excluded (turso remote cannot, no business pull). | "the maintainer's 2026-08-06 ruling (option B: retire `$regex` loudly and add `$icontains`, rather than make five backends agree on one regex dialect)" | | `5701`, `5702` | The contract half and the driver half of that ruling. | "the retirement's own contract-half change"; "the contract half (the `$icontains` declaration, …)"; "the driver half" | | `6148` | **404** — see Acceptance notes. | "the gate that makes a breaking changeset state its ADR-0087 disposition" | | `18012` | **404** — see Acceptance notes. | "Maintainer ruling A of 2026-09-17: a blank `$between` endpoint is refused at the authoring door, and the refusal names the blank side." | | `13495` | driver-memory's reference matcher answered a null-bounded range with every valued row. | "The reference matcher had already been taught to survive the null-bound form of exactly this" | | `objectui#9695` | The console filter builder stops padding a half-typed pair with an empty string. | "is a change to the console's own filter builder and lands on its own schedule" | | `7596` | Maintainer, 2026-08-11, ADR-0049 REMOVE: no `{ $field }` endpoint in either `$between` union. | "Maintainer ruling of 2026-08-11 on column-reference range endpoints, ADR-0049 enforce-or-remove: REMOVE"; "reviewed and accepted with that ruling" | | `7713` (PR) | Shipped that removal with a not-required disposition. | "the 2026-08-11 changeset carried the disposition not-required" | | `5222` | `$field` compiled to a column-to-column comparison on the SQL faces. | "the position the column-to-column comparison compiles on every face" | | `19377` | **404** — see Acceptance notes. | "filed as an entry of its own rather than as an already-registered rider" | | `20116` | The collector for the family "the save door accepts comparand shapes the query faces refuse", closed in stages. | "the second stage of closing the family of comparand shapes the save door accepted and the query faces refused"; "the family of comparand shapes …" | | `7872` | Maintainer, 2026-08-12: the shared face accepts `string` / `number` / `bigint` / `boolean` / `null` / `Date` and refuses everything else. | "the accepted set the maintainer ruled on 2026-08-12: string, number, bigint, boolean, null and Date" | | `19889` | Director seat, 2026-09-24, option A: the schema door refuses what the compile face refuses (the standing array-equality refusal applied to it). | "Ruled on 2026-09-24 (option A), applying the standing refusal of an array in the equality slot to the schema door" | | `19757` | Maintainer, 2026-09-23, option 乙: refuse an equality-slot array at the shared face; 甲 (declare array equality) and 丙 (document the divergence) rejected. | "Maintainer ruling of 2026-09-23 (option 乙 — rather than declaring an array equality the SQL-family backends would have to invent, or documenting a divergence that stays silent on one backend)" | | `19514`, `objectui#9050` | The protocol half of the maintainer's 2026-09-20 ruling C′ on the console's filter converter, rule 1 「the differences are the protocol's to close」. | "The protocol half of the maintainer's 2026-09-20 ruling (option C-prime) on the console's filter converter, whose first rule reads, verbatim and untranslated: 「the differences are the protocol's to close」" | | `18113` | Published the text-comparand refusal predicate and reason beside `FILTER_TEXT_CASES`. | "the pair published in this package beside FILTER_TEXT_CASES for exactly this reason" | | `19886` | Director seat, 2026-09-24, option A on standing text: `$ne` with an array refused at the shared face and the schema door. | "Ruled on 2026-09-24 by the director seat, on the standing contract text (option A)" | | `8690` | Ruled 2026-08-15: option B (engine door refuses an uninterpretable temporal comparand) with option C (the preset refusal at authoring) alongside. | "The authoring half (option C) of the maintainer's 2026-08-15 ruling on uninterpretable temporal comparands, ruled alongside the engine door (option B)"; "measured on the defect report" | | `8808` (PR) | The engine door. | "before the engine door and a 400 after" | | `15661`, `15773` | Maintainer, 2026-09-05, C-deny: refuse a text operator over a never-string declared type, over the existing type sets; landed at the engine seam. | "Maintainer ruling of 2026-09-05 (option C-deny: refuse now, over the type sets the contract already declares, minting no new vocabulary), landed at the engine seam" | | `14079` | Ruling A: a stored non-string value never satisfies a positive text operator and satisfies `$notContains`. | "(a stored value that is not a string never satisfies a positive text operator and satisfies `$notContains`)" | **`action-` (19 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `6748` | Retire `isAsync`: zero readers on a fresh three-repo measurement. | "a fresh three-repo measurement (taken when the key was filed for retirement, and re-run at pickup)" | | `6667` | Enforce `supportsPause` at runtime. | "The sibling took the ENFORCE leg of the same ruling" | | `3801` | The generic resume route needs an authorization gate keyed on the suspended node. | "The generic resume route's authorization gate keys on the SUSPENDED NODE" | | `3823` | The revise-window `wait` pause is service-owned but inherited `'any'`. | "The revise-window incident decided the direction" | | `4484`, `5540`, `6011` | The three retirements whose disposition this one shares. | named by their entry ids, which the sentence already carried | | `5561` | `resumeAuthority` defaulted to `'any'` — fail-open by omission. | the sentence already states it; ADR-0019's resume-seam addendum named by date and title | | `5703` | `supportsPause` / `isAsync` were zero-reader declarations. | "no longer the declaration nothing enforced" | | `5613` | Maintainer, 2026-08-06, contract-first ("C skeleton + A semantics"): declare the shape as it stands, then rename on the typed face. | "The maintainer ruled contract-first on 2026-08-06 (…: declare the shape as it stands first, then rename on the typed face)"; "the runtime half of the same ruling" | | `5697`, `5779` | Phase 1 (the schema) and phase 2's spec half (the canonical key and the alias). | "phase 1 declared …"; "`positions` is now the canonical key …" | | `5050` | The hook-side `roles` removed outright. | "(removed outright)" | | `3280`, `3290` | The hook `ctx.session.tenantId` alias: deprecated, then removed in the next major. | "the hook `ctx.session.tenantId` alias: deprecated first, removed in the next major" | | `4579`, `4657` | The `openApi31` and `activationEvents` retirements. | named by their surfaces, which the sentence already carried | | `17319` | Ruled 2026-09-12, A: an action declares its dispatch contract; B (unify the two wirings) refused. | "the 2026-09-12 ruling that made an action declare its dispatch contract refused exactly that option" | | `14175` | The earlier typing fix that gave `find` the filter alone. | "the parameter shape an earlier typing fix chose (the filter alone), ruled by the director seat on 2026-09-12, with the maintainer's agreement" | **`data-` (15 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `4484`, `4618`, `4673` | The three retirements, each already stated by its own entry. | trailing ids dropped (ADR ids kept) | | `4639` | Multi-record predicate writes got `data.records.*` events. | "since multi-record predicate writes were given events of their own"; "the way bulk writes were given theirs" | | `3196` | Webhook triggers trimmed to producers that exist. | the sentence already states it | | `cloud#1053`, `cloud#1030` | **403 here** — see Acceptance notes. | "20 such sites were measured in the downstream cloud codebase, and a `$like` the type layer would have caught reached runtime there through exactly that hole" | | `6350` | The stock reconciliation of the v17 train's breaking changesets against the ledger; this change was its control sample and was itself an omission. | "Registered by the stock reconciliation that compared the breaking changesets already on the v17 release train against this ledger. This change was that audit's CONTROL sample …"; "(backfilled by that reconciliation)" | | `5181` | The `DriverQuery` narrowing itself. | "this change"; "this narrowing" | | `6321`, `6083` | Two later call-parameter changes, both registered (`6083` **404**; `main` records it as ADR-0122 phase 2). | "Two later, smaller driver call-parameter changes both registered" | | `18669` | Maintainer, 2026-09-17, ruling A: rename the last two duration keys no closed duration type could express, no new closed type, no narrowing. | "Maintainer ruling A of 2026-09-17 on the last two duration keys no closed duration type could express" | | `18122` | Published `DurationMs` / `DurationSeconds` beside `EpochMs`, the unit set derived from six genuine duration rows. | "published beside `EpochMs` as a closed duration type"; "one of the six genuine durations the closed types' unit set was derived from" | | `14478`, `15680` | Ruling B of 2026-09-02 on duration units; the data-directory rename round. | "Maintainer ruling B on duration units (2026-09-02)"; trailing ids dropped | **`element-` (17 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `objectui#6206` | Maintainer, 2026-08-25, option B: one filter orthography platform-wide — the `ViewFilterRule` array, not a record-shaped exception; measurement-first binding. | "the maintainer's 2026-08-25 ruling, option B: …" (three entries); "the console-side half of this convergence" | | `15442`, `15449` | Ruled 2026-09-06, option A: converge the binding and the four `object-*` doors family-wide, one entry; exception and mixed rejected. | "ruled 2026-09-06, option A: converge the binding and the four block doors family-wide, under one entry, rather than record an exception"; "a gap measured on its own" | | `7751` | Maintainer, 2026-08-12: the `object-*` block props enter `ComponentPropsMap`. | "a read-point record derived from the renderers on 2026-08-13, when the `object-*` blocks first got props schemas in the map" | | `objectui#6948` | The console's filter converter had no branch for `$and` / `$or` / `$not`. | "(the console's filter converter had no branch for them)" | | `15828` | `POST /analytics/query` refused the array `where` the adapter sent. | the sentence already states it; "the runtime route's refusal of the array corrects it here" | | `16626` | **404** — see Acceptance notes. | "parked behind a bump of that pin" | | `objectui#7754`, `objectui#7752`, `objectui#6828` | The console adapter lowers an array analytics filter before the wire; `aggregate()` runs `translateFilterArray`. | "the console adapter was changed so `ObjectStackAdapter.aggregate()` runs the same `translateFilterArray`"; "The adapter-side lowering lands in the console's own repository." | | `5158` | Maintainer, 2026-08-04, ruling C: `FilterArray` is input-only sugar with one lowering seam (`parseFilterAST`). | "since the maintainer's 2026-08-04 ruling C declared the array input-only sugar with one lowering seam" | | `5334` | The in-process analytics door, added when an array `where` was silently dropped. | "(added when an array `where` was found silently dropped on the analytics path)" | | `17321` | Ruled 2026-09-12, B: a partial D2 conversion for the losslessly mappable record forms; combinators pass through, named. | "(ruled 2026-09-12, option B: convert what maps losslessly and name what does not, rather than leave every stored row to its next save or flatten combinators)" | | `14406` | Converged `element:record_picker`; its census pin asks whether any `filter` door refuses the array. | "the twin of the census pin that asks whether any `filter` door still refuses the array"; the 2026-08-25 Option-A ordering ruling stated as "measure the consumer's read path before the contract moves" | | `12039` | `element:number` converged. | "after `element:number` converged" | | `objectui#7663` | The console registry's `inputs.filter` flip for the picker. | "a console-side change filed in the objectui repository, blocked on that release" | | `15829` | The dashboard widget `filter` location, a finding of its own. | "a different family, judged on its own" | ## Pin — `packages/cli/test/migrate-meta-engine-guidance.test.ts`, widened `COVERED_PREFIXES` is now `engine-`, `ui-`, `plugin-`, `driver-`, `kernel-`, `system-`, `datasource-`, `filter-`, `action-`, `data-`, `element-` (`data-` does not select `datasource-` or `dataset-`: the match is `startsWith('data-')`). The `REWRITTEN` floor rises from **55 to 88** ids: the 33 entries of this stage that carried a tracker id. The three `it` blocks are textually unchanged: the detector is exercised on both sides, every covered prefix must select an entry, and each covered block is found verbatim in the real CLI's stdout before it is asserted clean. The file keeps its stage-1 name; the header lists the eleven covered families. ## Ablation — the widened pin can fail on a new-family block From committed state, HEAD `d8bcc46a`, in one lock turn, with `scripts/ablation-replace.mjs` in wrap mode (it owns the mutation's restore trap; the leg script adds its own `trap … EXIT INT TERM` that restores `registry.ts` from `HEAD` by absolute path and checks the blob) and `scripts/ablation-dist-preflight.mjs` gating each leg. The bundle is built from the generated `registry.ts`, so that is the file mutated. - **Mutation.** In `registry.ts`, the `acceptanceCriteria` of `datasource-credentialsref-mongo-url-no-user-refused`: anchor `parse reports this URL-branch refusal.` → `parse reports the objectstack-ai#9041 refusal.` The tool read anchor 1 → 0 and replacement 0 → 1, blob `7001c102` → `6f3f81ff`. - **Mutate leg.** Spec build exit 0. Preflight: marker present in 4 built files. Pin: **red**, `1 failed | 2 passed` — `datasource-credentialsref-mongo-url-no-user-refused: the printed guidance cites a tracker id: expected 'objectstack-ai#9041' to be undefined`. - **Restore.** Tool-proven: blob `7001c102` == HEAD, `git diff HEAD` empty. - **Restore leg.** Spec build exit 0. The `--absent` preflight found the marker in none of 224 built files, with the working tree clean against HEAD. Pin: **green**, `3 passed`. Whole tree afterwards: 0 dirty paths. ## Verification Final head **`d8bcc46a`**. Every heavy run went through `scripts/pm/os-verify-lock.sh` (`VERDICT command-exit 0` on each turn; per-step exit codes recorded separately). Where a line was taken at `95fb97ea` it says so: the one commit after it (`d8bcc46a`) changes one entry sentence and the three generated projections, and every reader of that text was re-run at `d8bcc46a`. - **Build:** `pnpm exec turbo run build --concurrency=2 --filter='@objectstack/cli^...'` gives `Tasks: 58 successful, 58 total` (at `95fb97ea`); the spec package was rebuilt at `d8bcc46a` in both ablation legs (exit 0). - **Pin:** at `d8bcc46a`, the ablation's restore leg: `test/migrate-meta-engine-guidance.test.ts` `3 passed`. With its neighbour at `95fb97ea`: `pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 test/migrate-meta-engine-guidance.test.ts test/migrate-meta-default-range.test.ts` gives `Test Files 2 passed`, `Tests 10 passed | 1 skipped` (the skip is the default-range file's own `skipIf`). Re-run with its neighbour at `d8bcc46a`: `Test Files 2 passed`, `Tests 10 passed | 1 skipped`. One earlier run at this head is void and is not counted: it spawned the CLI while a concurrent lock-free gate was rebuilding the package `dist/` trees (`MODULE_NOT_FOUND`, both files); the re-run is the reading. - **Spec, the whole `local` project** (at `95fb97ea`): `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2` gives `Test Files 572 passed (572)`, `Tests 16789 passed | 1 todo`. **The `repo` project** (at `d8bcc46a`): `Test Files 38 passed`, `Tests 690 passed`. **The readers of the changed sentence** (at `d8bcc46a`): `src/migrations`, `src/ui/action-params.test.ts`, `src/ui/filter-rule-array-guidance.test.ts` give `Test Files 5 passed`, `Tests 229 passed`. - **CLI unit** (at `95fb97ea`, no CLI file changed after): `test/vitest-tiers-partition.test.ts` and `src/utils/spec-release-changes.test.ts` give `Test Files 2 passed`, `Tests 28 passed`. - **The call-spelling census that reads `registry.ts`:** `pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2 src/sql-driver-query-signature.test.ts` gives 15 passed. - **Typecheck** (at `95fb97ea`; the last commit edits string literals only): `pnpm --filter @objectstack/spec typecheck` exits 0 (test layer: 53 files / 251 errors held in its ledger); `pnpm --filter @objectstack/cli typecheck` exits 0 (3 files / 28 errors held). - **Gate families:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives **90** families at `d8bcc46a` (the same set it derived at `95fb97ea`). `--ran` over the recorded exit codes reads **90 derived, 90 run, 0 NOT-MEASURED, 0 UNRUN**, all exit 0. They include `check:doc-authoring` ("16735 customer-facing string(s) across 1167 spec sources clean"), `check:issue-citations`, `check:migration-registry` ("registry.ts is current (310 semantic, 230 retired-key, 206 retired-def)"), `check:spec-changes`, `check:upgrade-guide`, `check:generated` ("All 15 generated artifacts are up to date"), `check:org-identifier`, `check:nul-bytes`, `check:dual-build-cjs-loads` (104 require entry points across 66 packages load), `check:type-check-debt`, `check:adr-0087-registration` and `check:changeset-no-major`. - How the reading was assembled: a container restart cut the pass at `d8bcc46a` after 30 families; the other 60 were run afterwards on the same head, and `check:type-check-debt`, whose first resumed run lost a package build to an OOM kill (exit 3, `PREREQUISITE NOT MET`), was re-run alone and exited 0 ("4 ledger entr(ies) re-measured … none above its recorded number"). - The earlier full pass at `95fb97ea` read 88 exit 0, `check:dual-build-cjs-loads` exit 3 (dists not yet built) and **`check:org-identifier` exit 1**: my rewrite of the action-session entry had spelled the removed hook read literally. `d8bcc46a` names that alias in words instead, and the gate reads OK there. - **Lint (a proven narrowing, not the repo-wide run, which is CI's):** `eslint --no-inline-config --format json` over the 36 changed `.ts` files at `d8bcc46a` reports 36 files, 0 errors, 0 warnings. - The population is read from `eslint.config.mjs`: `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`, and all 36 are in it (no file-ignored warning). - Invariance: the config enables no type-aware linting (no `parserOptions.project`, no typed rules), so a text edit cannot move the verdict on a file it does not touch. - **Mergeability:** `main` moved 10 commits past the base, to `4a1df196`; the only ones under `packages/spec/src/migrations/` are the landed `20488` (two entries in the `field-` and `ui-` families, plus `registry.ts`). A driver-free bare-clone `merge-tree --write-tree` of `d8bcc46a` against `4a1df196` exits 0 with no conflicted path, and the census over that merged tree reads 0 sites in all eleven covered families, so `main` was not merged in; CI's merge ref runs the registry gates on the merged tree. ## Acceptance notes - **Ten dead ids, rewritten from the code on `main`.** Each answers 404 on both the issues and the pulls endpoint, with a 200 control (`14478`). - `8696` / `8875` / `8873` (the bound secret reaching the mongo, mysql and postgres clients): `service-datasource`'s `default-datasource-driver-factory.ts`, its CHANGELOG (the mongo URL-branch entry says it "closes the last arm of the family" the other cards closed one driver at a time; the mysql DSN fix is `{ uri, password }`) and `bound-secret-dsn-branches.test.ts`, which pins the bound secret outranking `options.auth`. `common.zod.ts` and `pg-url-grammar.server.ts` record why the shared helpers must not parse (mongo's multi-host form, which `new URL()` rejects). - `8876`: `common.zod.ts` (`urlUserinfoUsername`, the username half of the same grammar; a username is not credential material). - `9041`: `datasource.zod.ts` (`CREDENTIALS_REF_MONGO_URL_NO_USER_REFUSED` and its docblock); the text now names the sibling entry by id. - `6148`: `scripts/check-adr-0087-registration.mjs`'s header (a declared-breaking changeset must state its ADR-0087 disposition in writing). - `18012`: `.changeset/18012-between-blank-endpoint-refused.md` and `filter.zod.ts` (ruling A of 2026-09-17: a blank bound refused at the authoring door, the blank side named). - `19377`: `.changeset/19377-between-field-endpoint-runtime-door.md` and `filter-comparand-shape.ts` (the runtime door brought under the 2026-08-11 removal). - `16626`: `.changeset/filter-orthography-binding-and-object-blocks.md` on `main` records it as the objectui pin bump the element convergence was parked behind; the text now says "a bump of that pin", and the pin sha it names is unchanged. - `6083`: `main` records it as ADR-0122 phase 2 (the spec CHANGELOG); the sentence keeps its claim ("two later, smaller driver call-parameter changes both registered") without the numbers. - **Two cross-repo ids this session cannot read.** `cloud#1053` and `cloud#1030` answer 403 (`objectstack-ai/cloud` is not reachable here, and attaching it was refused). The sentence was rewritten from what this repository records: `packages/spec/CHANGELOG.md` (the published `DriverQuery` entry: 20 measured `as any` sites downstream, and a `$like` that reached runtime through that hole) and the `5181` card body ("cloud 实测 20 处 cast … cloud#1030 的 `$like` 本可在类型层拦住"). Nothing the cloud cards add beyond that is claimed. - **Short numbers and ruling-record ids went too (invisible to the regex).** Six decision-batch numbers (`objectstack-ai#43` ×2, `objectstack-ai#55`, `objectstack-ai#123`, `objectstack-ai#146`, `objectstack-ai#151`), three ruling-record comment ids and one `batch 217 item 3` spelling are numbers an author is shown and cannot follow, so each is dropped. So are the maintainer's batch acknowledgements 「146 同意」 and 「217 同意」 and the bare 「同意」 beside `objectui#6206` (×3), `15442` and `14175`: they record only that a batch was approved, and each sentence now states the ruling's date and content instead. The one verbatim quote that carries a lesson, 「the differences are the protocol's to close」, is kept untranslated. - **"issue NNNN" / "PR NNNN" spellings, checked by hand.** A scan of the five families' evaluated prose for any run of three or more digits and for `issue` / `card` / `PR` / `batch` / `record` / `summon` plus a number now finds only HTTP statuses, ports and example values. The two `PR #…` citations were `#`-spelled, so the instrument saw them. - **One test pinned a removed tracker number.** `packages/spec/src/ui/action-params.test.ts` asserted `reason` matches the `5613` number; it is re-pinned on the sentence that carries the ruling (`/ruled contract-first/`). No other test reads these entries' prose for a tracker id (a grep of test files for the 88 cited numbers finds only comment lines and assertions on other runtime strings). - **No open PR touches these five families.** Read twice. At 2026-09-28T19:04Z: 11 open PRs' file lists (227 rows). Again at 20:28Z, just before opening this one: 8 open PRs (195 rows; the Version Packages PR `17076` skipped both times), none carrying a `migrations/entries/semantic/NN.(datasource|filter|action|data|element)-*` file. PRs `20504`, `20503`, `20460` and `20458` add or edit entries in other families (`turso-`, `view-`, `stack-`, `cube-`) and regenerate `registry.ts`: ordinary concurrency, regenerate on merge. Widening the pin reds no sibling. - **Generated projections** (`spec-changes.json`, `docs/protocol-upgrade-guide.md`) are regenerated, as in stages 1–3; only the protocol-17 entries appear in them. - **What later stages pick up** (whole tree at this head, same instrument): **571** prose-field sites in the other families, **61** short `#NN` sites, **7** `surface` sites, and three `cloud#` sites (`storage-service-list-retired`, `cloud-subpath-retired`, `cluster-driver-dangling-values-removed`; the fourth, `data-driver-query-omit-object`, is done here). - **Noted, not filed — the same rule outside this card's fields.** Runtime strings outside the migration entries still carry tracker numbers. One read in passing: `AutomationEngine`'s boot warning for a pausing node type that never declares `resumeAuthority` (`packages/services/service-automation/src/engine.ts`, the line that reads `so the objectstack-ai#3801 resume gate REFUSES every pause it creates`) is printed to a plugin author, and `resume-authority-declaration.test.ts` asserts the `3801` and `3823` numbers are in it. That is the runtime-string rule this card applies, on a producer outside its surface; it is reported to the seat, not fixed here. ## Line budget Entry files: **367 changed lines** (+214 / −153) across 33 files, against the stage-1 ≈400 budget. The whole diff is **871 lines** (+527 / −344) in 39 files. Of the rest, `registry.ts` is 367, the two projections are 68 (`spec-changes.json` 44, the upgrade guide 24), the widened pin is 42, the re-pinned test 2 and the changeset 25. --- _Generated by [Claude Code](https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Resolves architectural confusion between authentication providers/strategies (configuration) and user identity models (runtime data).
Changes
Migrated enterprise auth to
auth.zod.tsOIDCConfigSchema,SAMLConfigSchema,LDAPConfigSchemafrom identity.zod.tsenterprisefield toAuthConfigSchema:Redefined
identity.zod.tsas data modelsRemoved authentication configuration schemas. Replaced with runtime data models:
UserSchema: Core identity (id, email, emailVerified, name, image, timestamps) - minimal and identity-focusedAccountSchema: Links external OAuth/OIDC/SAML providers to usersSessionSchema: Session state with device fingerprintingVerificationTokenSchema: Email verification and password reset tokensCreated
auth-protocol.tsWire protocol constants and interfaces:
AUTH_CONSTANTS: Standard headers, prefixes, cookies (Authorization,Bearer,os_*)AuthHeaders,AuthResponse,AuthError,TokenPayloadinterfacesAUTH_ERROR_CODES: Standard error codesAdded database field mapping for driver compatibility
DatabaseMappingSchemato map ObjectStack standard field names (Auth.js conventions) to driver-specific field namesmappingfield toAuthConfigSchemawith pre-configured better-auth defaultsBETTER_AUTH_FIELD_MAPPINGSconstant for maintainabilitysessionToken→tokenexpires→expiresAtproviderAccountId→accountIdprovider→providerIdUpdated exports and documentation
AUTHENTICATION_STANDARD.mdArchitecture
Driver Agnostic Design
The spec now supports any authentication driver through field mapping:
BETTER_AUTH_FIELD_MAPPINGS)Breaking Changes
AuthProviderschema removed. Consumers should use:AuthConfigwith optionalenterpriseandmappingfields for configurationUser,Account,Sessionschemas for runtime dataAUTH_CONSTANTSfor protocol constantsOriginal prompt
identity.zod.ts. #48✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.