Repository navigation
fix(plugin-auth): a refused auth setting no longer drops the settings saved with it - #20429
Conversation
… siblings The auth settings pass is applied in pieces split along the lines where AuthManager.applyConfigPatch can refuse: each key that lands in the emailAndPassword or plugins block is applied alone, every other key rides one application the manager does not validate. A refusal is reported once at error, naming the key, and the rest of the pass still applies. A pass that fails as a whole (the namespace read) is reported at error too. Claude-Session: https://claude.ai/code/session_017B6YKCGu8CTY2KBWgwaHAs Co-authored-by: Claude <noreply@anthropic.com>
…he auth settings pass applyConfigPatch joins the durability-critical callee vocabulary: a refused patch leaves the settings console showing a value the runtime never applied, so its catch may not regress to warn. Claude-Session: https://claude.ai/code/session_017B6YKCGu8CTY2KBWgwaHAs Co-authored-by: Claude <noreply@anthropic.com>
…ledger Claude-Session: https://claude.ai/code/session_017B6YKCGu8CTY2KBWgwaHAs Co-authored-by: Claude <noreply@anthropic.com>
…-vocabulary copy Claude-Session: https://claude.ai/code/session_017B6YKCGu8CTY2KBWgwaHAs Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 15 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 90165465b613a4a980ac920cc7d735b80d11b108 && git checkout 90165465b613a4a980ac920cc7d735b80d11b108
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 24b7085939ddee31fbeb59c049b0576cf51acf37 950c40c51df7bcc81c9988a3e9ccd957140b6c65 && git checkout -B drift-repro 24b7085939ddee31fbeb59c049b0576cf51acf37 && git merge --no-ff 950c40c51df7bcc81c9988a3e9ccd957140b6c65
node scripts/docs-audit/affected-docs.mjs --json 24b7085939ddee31fbeb59c049b0576cf51acf37
|
Contract reviewServed-tier: Inputs read: card #20412 (body + all 3 comments: claim ① Derived judgmentsThe security question first — does any split change WHICH values the manager accepts or refuses, and can any piece now land a combination the whole-patch validation refused as incoherent. Read off
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Rendered by an isolated contract-review subagent and adopted by the Generated by Claude Code |
…tudio's metadata list and quick-find show them (objectstack-ai#20541) Part of objectstack-ai#20299 Clause-②: no Stage 1 of objectstack-ai#20299. Three docs-shaped liveness rows move `dead` → `live`, because Studio already shows them to a human: `flow.description`, `hook.label` and `hook.description`. The other four rows of the family need objectui code and are objectstack-ai/objectui#11027's: `app.areas.description`, `permission.rowLevelSecurity.label` / `.description` and the `view` container `label`. They are untouched here, and objectstack-ai#20299 remains open for them. Ledger data, two README Notes cells and one gate-test fixture only. ⛔ No schema, parse, `.describe()` or accept-set change. None of the three rows sets `authorWarn`, so the set of lint warnings does not change. The re-grade reverses no ADR-0033 decision: all three stay docs-shaped, deliberately KEPT and exempt from enforce-or-remove. Each row keeps the note it carried while `dead`, labelled as history. ## Files - `packages/spec/liveness/flow.json`: the `description` row. - `packages/spec/liveness/hook.json`: the `label` and `description` rows, plus one dated sentence appended to the file `_note`. Its 2026-08-10 lookup ("both verdicts stand unchanged") is now history. - `packages/spec/liveness/state-counts/flow.md` and `packages/spec/liveness/state-counts/hook.md`: the two per-type count shards, regenerated by `gen:liveness-counts`, not hand-edited. `flow` has 35 live and 5 dead (was 34 / 6). `hook` has 21 live and 1 dead (was 19 / 3). Across all shards, the read-time sum that `check:liveness` prints is 952 live and 136 dead (was 949 / 139). No file commits that total. - `packages/spec/liveness/README.md`: the `flow` and `hook` Notes cells. Both listed these keys as dead. - `packages/spec/scripts/liveness/check-liveness.test.ts`: the "stays GREEN when a `dead` entry carries the SAME rotted pointer" case borrowed `flow.description` as its sample `dead` row. It now uses the `flow.active` tombstone, which the gate itself holds at `dead`, and it asserts that precondition. - `.changeset/20299-display-annotations-ledger.md`: a `patch` changeset for `@objectstack/spec`. The ledgers ship in its `files[]` (`liveness`), and `@objectstack/lint` reads them. **File-surface declaration.** The claim's surface named `flow.json`, `hook.json`, the regenerated counts and the changeset. The README cells and the test fixture are outside it. The dispatch's pin sweep required both to move with the flip ("grep for any test, doc or ledger note that asserts these three rows are `dead` … and move it with the flip"). Without the fixture move, the gate test goes red: see the reverse verification below. The README ships in the same `liveness` directory. ## The lane's call, and the ruling it applies A Studio list column and the metadata quick-find count as consumers of a DISPLAY-shaped key. That is the objectstack-ai#7131 ruling's table in `packages/spec/liveness/README.md` ("Designer previews count as consumers"): for a display key, being shown to a human is the whole of the claimed effect. The README's `producer` discipline binds too, so each row names who hands the reader a record. ## Premise, reader half: measured at the `.objectui-sha` pin `dd3f7e1be` Instrument: read-only `git -C ../objectui show SHA:PATH` and `git grep … SHA`. Nothing in objectui was edited, checked out or stashed. - The Studio route `metadata/:type` mounts `MetadataResourceListPage` (`packages/app-shell/src/console/AppContent.tsx`, both the with-app and the zero-app branch). - `MetadataResourceListPage` renders a registered custom `ListPage` if one exists, and otherwise `DefaultMetadataList`. `DefaultMetadataList` takes `config.listColumns ?? defaultColumns(config.primaryKey ?? 'name')`. `defaultColumns` returns the primary key, `label` and `description`, and each cell goes through `defaultCell`. - **No registration gives `flow` or `hook` a `ListPage` or `listColumns`.** `git grep registerMetadataResource` over `packages` and `apps` gives 44 hits. The non-test registrations are `builtinComponents.tsx` (object, field, permission, view, dashboard, page, book), `anchors.ts#registerBuiltinAnchors`, `datasource/register.ts` and `default-schemas.ts`, which sets `defaultSchema` / `fieldOrder` only. `flow` and `hook` register only in `anchors.ts`, with anchors, create fields and defaults. `git grep -E 'ListPage\s*:'` hits only `datasource/register.ts`, the positive control. The shorthand spelling `ListPage[,}]` / `listColumns[,}]` has no registration hit. - `MetadataQuickFind` (`QuickFind.tsx`) indexes every type's items off the same `client.list(type)` read. It keeps `label` and `description` and draws `label` beside the name and `description` under it. It is mounted on `DirectoryPage` and `StudioHomePage`. - `MetadataClient.list(type)` (`packages/data-objectstack/src/metadata-client.ts`) is `GET {base}/{type}`, with base `/api/v1/meta`. It accepts a top-level array or `items`. - **Per-string control at objectui main `5d689c3`**, counted with `git show REF:PATH | grep -cF` at both refs. Every cited string counts the same at the pin and at main (1/1 each): the `listColumns ?? defaultColumns` line, the `description` and `label` default-column entries, `if (customConfig?.ListPage) {`, the `:type` route element, QuickFind's `label: item?.label,`, `description: item?.description,` and `{r.description}`. The `ListPage:` and `listColumns` registration counts also match. ## Premise, producer half: measured booted, not read Instrument: a throwaway `@objectstack/verify` test in `packages/qa/dogfood` (deleted after the run, never committed). It booted the real `examples/app-showcase` composition in-process with `bootStack(showcaseStack)`, signed in as the dev admin and read the exact doors the list page reads. It was run twice, before and after a container restart, with the same result. | door | status | body | rows | |:--|:--|:--|:--| | `GET /api/v1/meta/flow` | 200 | top-level keys `type`, `items` | 30 flows. All 30 carry their authored `label` and `description`, with `_packageId: com.example.showcase` | | `GET /api/v1/meta/hook` | 200 | top-level keys `type`, `items` | 4 hooks. All 4 carry `label` and `description`, with `_packageId: com.example.showcase` | | `GET /api/v1/meta/package` | 200 | top-level keys `type`, `items` | `com.example.showcase` with `scope: project`, which is what `buildPackageScopeOptions` admits, so the list page's package scope shows those rows | The server-side list answer is `packages/rest/src/meta-item-read-gate.ts#createMetaListAnswer`, which both transports serve. Its type-specific steps are for `api`, `app`, `view`, `doc` and `object` only. For `flow` and `hook` it runs the per-caller gate and the translation step, and neither drops `label` or `description`. Each row's `producer` cites that answer, the route, the list-page fall-through, the registration and the client read. ## Verification, at `cb0206219c` All heavy runs went through `scripts/pm/os-verify-lock.sh` with `--maxWorkers=2`. The box is shared, so wall-clock figures are shared-box readings. - `pnpm --filter @objectstack/spec run check:liveness`: exit 0. "✓ every governed-type property … is classified …" and "✓ packages/spec/liveness/state-counts.md is current". - `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2` (the whole `local` project): **573 files, 16835 passed, 1 todo**. - `@objectstack/spec` `repo` project, narrowed. The whole `repo` project hit its 330 s timeout on the shared box, so that run is NOT MEASURED. It was narrowed to the 10 `repo` files that read the ledgers: `scripts/liveness/evidence.test.ts`, `scripts/liveness/proof-registry.test.ts` and eight `*-retirement` / `*.pin` tests. Result: **10 files, 211 passed**. CI runs the whole project. - `pnpm --filter @objectstack/spec typecheck`: exit 0. It covers `tsc --noEmit`, `check:scripts-typecheck` and `check:test-typecheck`. `tsc -p tsconfig.scripts.json --listFiles` names `scripts/liveness/check-liveness.test.ts`, so the edited test is compiled. - **Gates.** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 68 commands at `cb0206219c`. All 68 ran, and each exit code was captured before any pipe. `--ran` over the exit-coded record reported: "✓ 68 derived famil(ies) accounted for — 68 run, 0 NOT-MEASURED (a DERIVED zero …)". - 66 exited 0. - `check:dual-build-cjs-loads` first exited 3 (PREREQUISITE NOT MET: eight unbuilt packages). It was re-measured after building them (all turbo cache hits) and exited 0: "104 published require entry point(s) across 66 package(s) load". - `check:platform-checklist` exits 1 on a finding this diff does not reach. See Acceptance notes. - **Reverse verification** of the moved fixture, after the commit. The fixture's carrier was swapped back to the flipped row through `scripts/ablation-replace.mjs` (anchor `setEvidence(root, 'flow', 'active', ` hit ×1 → ×0, replacement ×0 → ×1, blob `9ff17ad4` → `1091717a`). The case went **red**: `expected 1 to be +0`, and the gate named `flow/description → packages/plugins/driver-sql/src/sql-driver.ts`. That is the old fixture failing because the row is now scanned as `live`. The tool restored the file: blob after restore `9ff17ad4` equals `HEAD`, and `git diff HEAD` is empty. The direction observed was a turn to red, as expected. - Upstream check: `origin/main` has moved to `6427e2cf56` since the base `fb386074f5`. None of the six paths changed upstream (`git diff --name-only BASE origin/main -- PATHS` is empty), so the regenerated counts need no merge. ## Acceptance notes - **`check:platform-checklist` is red on the base and does not involve this diff.** `docs/qa/platform-checklist/areas/identity-auth.json` anchors `packages/plugins/plugin-auth/src/auth-plugin.ts#twoFactor`. Since `7d63088958` (PR objectstack-ai#20429), that symbol exists in the file only as a member inside a `patch` object, which `symbol-anchors.mjs` does not accept as a declaration. The files that finding names (the checklist area, `auth-plugin.ts`, `scripts/check-platform-checklist.mjs`, `scripts/symbol-anchors.mjs`, `scripts/checklist-select.mjs`) are byte-identical between this branch's base `fb386074f5` and `origin/main` `1378ec7c0c`, and none of the six paths here is among them. Carrier: the plugin-auth / checklist owner. Not filed here. - **Mentions left as they are, because the flip does not make them false.** `docs/audits/2026-06-flowschema-property-liveness.md` records the dated 2026-06 audit and is not a current-state claim. `.claude/skills/spec-property-retirement/SKILL.md` §0 uses `hook.label` / `flow.description` as the example of "build the renderer, do not retire", and that is what happened. `liveness/app.json` (`areas.description`) and `liveness/job.json` cite "the hook.label precedent" for "docs-shaped, kept, not warned", and that is still true. - **Observation, objectui side (not filed).** `QuickFind.tsx`'s docblock calls it a "Cmd+K palette", but it binds Cmd+Shift+M, to leave Cmd+K to `CommandPalette`. The rows say "metadata quick-find" and name no key. - Also noted: `hook` still has no registered metadata-admin preview. A `HookPreview` is not needed for these rows, and objectui#11027 already excludes it. --- _Generated by [Claude Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…the record key is the member's name (objectstack-ai#20300) (objectstack-ai#20458) Fixes objectstack-ai#20300 Clause-②: no (narrowing) Retires the inner `name` on analytics cube measures and dimensions (`MetricSchema.name`, `DimensionSchema.name`). `measures` and `dimensions` are records, and the record key was always the member's identity: `GET /api/v1/analytics/meta` publishes every member as `CUBE.KEY`, and every consumer resolves a member by indexing the bag with its key. The inner copy was REQUIRED, read by nothing, and silently ignored when it disagreed with its key. ADR-0049 enforce-or-remove, by triage's verdict `5859547666` (RETIRE) under the maintainer's criterion, verbatim: 「每族该问的是:主流平台有没有这个能力 —— 有 ⇒ 补消费端(一次做对);没有 ⇒ 退役,而不是看仓里有没有人读」. Cube.dev and LookML key a member by its declared name, with no second inner name that can disagree. **Tier H.** The diff touches `skills/objectstack-ui/rules/dashboards.md` (a deletion only; see Deviations 1). It lands on the maintainer's word, then the seat lands it. 103 files, +1446 / -423 (1869 changed lines, under the 5,000 line class). ## Patch round after objectstack-ai#20390 (head `f639af5f3`) The sections below describe `c4771e604`. This head adds two commits and nothing else: - **`5ce26b0b2`** merges `origin/main` `75b2169243` through `os-regen-merge.sh`. The one hand conflict, `packages/spec/vitest.repo-tests.json`, was resolved by stacking both entries. Main’s step-18 siblings and this PR’s entries are all present in the four registries. - **`f639af5f3`** stamps `retiredAfter: 17.4.0` on `cube-member-inner-name-removed`. objectstack-ai#20390 (`e956924e1`) made the stamp required on every `retiredFromLoadPath: true` conversion. This entry is unpublished, so it takes the package label, as `view-list-tabs-removed` and `action-aria-removed` do on `main`. The net diff is 103 files, +1447 / −423: the stamp is the one added line. CI is green on this head. At-tier record `5875291969`: PASS. ## What this head carries (`c4771e604`) | surface | change | | --- | --- | | schema | `retiredKey()` tombstones on `MetricSchema.name` and `DimensionSchema.name` (both `strictObject`s, the `action.aria` posture). `tsc` types the key `never`, and the parse raises the prescription at `measures.KEY.name` / `dimensions.KEY.name`. The `measures` / `dimensions` describes now state that the record key IS the member's name. | | D2 | `cube-member-inner-name-removed` (protocol 18, `retiredFromLoadPath`), chained into `step18.conversionIds` with a rationale paragraph. It strips the inner `name` from every member of every `analyticsCubes[]` entry, and its notice names the cube. | | D3 | semantic entry `cube-member-inner-name-retired`: the judgement a DISAGREEING value still owes its author (which spelling was meant). | | registration | `RETIRED_KEYS_BY_MAJOR[18]` gains `data/Metric:name` and `data/Dimension:name` (per-file entries, generated region). | | ledger | both `analytics_cube.json` rows STAY `dead` (the tombstone keeps the key in the walked shape) with a `REMOVED 2026-09-28` note and a re-measured `verifiedAt`. The README row is updated; the counts do not move. | | producers | `dataset-compiler.ts` stops writing it (the triage line), and so do the two untyped internal mints tsc cannot see (`CubeRegistry.inferFromObject`, and `inferCubeFromQuery` / `inferMeasure` in `analytics-service.ts`). | | authors | the showcase cube (8 members), the `service-analytics` README example (3), and the published `objectstack-ui` skill example (6) | | fixtures | about 300 member literals and map-built members across 84 test and fixture files in eight packages; the three existing step-18 cube conversion fixtures are trimmed so the whole-table replay stays disjoint | | pins | `packages/spec/src/data/cube-member-inner-name-retirement.test.ts` covers every door (schema, `/meta` binding, `defineCube`, `defineStack` with its `STACK_SCHEMA_INVALID`/422 envelope, and a `@ts-expect-error` tsc leg), the D2 legs (stored row, boot door with a lit control, a disagreeing value, idempotence, load-path retirement), the registration, and a tree-scoped structural absence pin over the declared five-root radius. The flipped `analytics.test.ts` blocks (the snake_case pins on a value nothing read) are now tombstone pins. | | changeset | `@objectstack/spec` minor (BREAKING banner, FROM → TO, the one-line fix, what an author sees, and the ADR-0087 `registered` marker); `@objectstack/service-analytics` patch | What an author who still writes it sees: `tsc` fails at the authoring site. The parse refuses it with: "`measures.METRIC.name` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it never had an effect: the record key is the metric's name. … Delete the key. To rename a metric, rename its key in `measures` — and every query, dashboard and report that names `CUBE.KEY`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand." A stored or built cube heals at rehydration and at the artifact door. ## Zone 2, measured - **A1 holds.** Zero reads of a member's inner `name` in non-test source (`analytics-service.ts#getMeta` and `memory-analytics.ts#getMeta` publish `CUBE.KEY`; `native-sql-strategy.ts#lookupMember` and `memory-analytics.ts#resolveMeasure` / `#resolveDimension` index the bag by key). Lit control: four reads of `measure.label` / `dimension.label` in the same two projections. The one `measure.name` hit (`dataset-compiler.ts:383`) is a `DatasetMeasure`, not a cube member. objectui at pin `f8a9d0fb05`: no cube-member authoring. `CubeSchema` is used only in `clientValidation.ts` (control: that hit resolves at the same sha). - **A2 holds, and is wider than the card.** Non-test producers: `dataset-compiler.ts` (2 sites), `CubeRegistry.inferFromObject` (3) and the ad-hoc mint in `analytics-service.ts` (4, plus `inferMeasure`'s 3 returns), the showcase, the README and the skill. Every one wrote the name EQUAL to its key, so no producer writes a disagreeing value. Test fixtures: 21 disagreed, all in `driver-memory` (e.g. `totalAmount: { name: 'total_amount' }`), and every one was queried by its key (`orders.totalAmount`). That is the trap, live in-repo. No `platform-objects` or template authors any cube. Stored rows: `analytics_cube` wraps as `analyticsCubes` at `applyConversionsToStoredItem`, and the pin's stored-row leg replays it. - **A3.** Worked on the merged cube contract (`public` enforced, objectstack-ai#20348 landed). Line numbers are from the merged tree. - **A4.** Merged `origin/main` `6e3e5462c` (which carries objectstack-ai#20357's step-18 appends) with `bash scripts/pm/os-regen-merge.sh`. - The driverless merge-tree (a bare shared clone with no `merge.os-regen.driver` registered) answered exit 0 with no conflicted paths. - The script's step 2 took main's side of `content/docs/references/data/analytics.mdx`, which was regenerated from the merged tree in its own commit (`e19628132`). - After the merge: both sides' ids are present in both step-18 lists (`view-list-tabs-removed` and `cube-member-inner-name-removed`) and in the rationale. - After the merge: `check:generated` reported all 15 artifacts current, measured right after a spec build of the merged tree. ## Deviations 1. **The `skills/**` split was ordered, then withdrawn.** The seat ordered the skill hunk split into a companion PR, and withdrew that on this measurement: - The example is an `os:check` block that `check:skill-examples` type-checks inside `typecheck-consumers`, a member of the required `TypeScript Type Check` aggregator. - Putting one inner `name` back into the example with `ablation-replace` (restored to the HEAD blob, `git diff HEAD` empty) gave exit 1: `dashboards.md:450:15 error TS2322: Type 'string' is not assignable to type 'undefined'`. So this PR without the hunk is red. - A companion PR alone on `main` would be red too: at base `dbddf02c1`, `MetricSchema.name` is a REQUIRED `z.string()`. That half is derived from the schema, not built. - No landing order is green, so the hunk stays here, as a pure deletion. 2. **File surface wider than the claim, same package and same defect class.** `CubeRegistry.inferFromObject` and the `analytics-service.ts` mints are untyped (a `Record` of `any`) producers that tsc cannot see; A2 put every producer in scope. Fixture edits span `service-analytics`, `driver-memory`, `spec`, `client`, `objectql`, `runtime`, `qa/dogfood` and `qa/downstream-contract`. 3. **Route.** The `spec-property-retirement` skill's route table maps `.strict()` to deletion plus a guidance map. I took the triage's `retiredKey()` route instead, which `shared/retired-key.ts` documents for closed shapes (strictly stronger than a guidance entry) and which `action.aria` used this week. As a result the ledger rows stay, per the card's acceptance. The `CubeJoinSchema` docblock line that said cube shapes never take a tombstone is corrected. 4. **D2 strips a disagreeing value too.** Triage: "lossless when it equals the key; a disagreeing value gets a D3 entry". The D3 entry exists. The strip still removes a disagreeing value because the key already won everywhere, so no answer changes, and leaving it would stop the cube loading at the boot door. The notice prints both spellings (`from: name "total_amount"`, `to: (removed; the record key "totalAmount" is the name)`). 5. **`service-analytics` is graded `patch`.** Its members are filed under the same keys, and every `/analytics/*` answer is unchanged. `@objectstack/spec` carries `minor`: a published narrowing ships `minor` in the launch window, and the changeset declares it as `Clause-②: no (narrowing)` under its BREAKING banner. ## Tests (head `c4771e604` unless stated) - **`@objectstack/spec`:** - `test` 564 files / 16641 tests green (merged tree `e19628132`; spec `src/` is unchanged since). - `test:repo` 37 / 675 green (pre-merge `c1cae40df`). Post-merge, its three tree-reading legs this diff owns were re-run green: the retirement pin, `retired-key-migrate-sentence`, and `build-schemas-check-mode` (107 tests together with the pin). - `typecheck` green (src, scripts, and the test layer under its shrink-only ledger). `tsc -p tsconfig.test.json --listFilesOnly` lists the new pin, so its `@ts-expect-error` legs are live. - **`@objectstack/service-analytics`:** typecheck green, 132 files / 3093 tests green (`14cac89f4`). - **`@objectstack/driver-memory`:** typecheck (which reaches the test layer) green, 57 / 1374 green (`14cac89f4`). tsc found the two map-built members there; the same shape was then swept in service-analytics and runtime. - **Touched test files in other packages:** `client` 7/7, `objectql` `protocol-meta` 95/95, `runtime` `cross-field-refusal-operand-withhold` 11/11, `downstream-contract` `contract.test.ts` 13/13 plus typecheck exit 0. - **Reverse verification.** Putting `name: m.name` back in `dataset-compiler.ts` via `ablation-replace` gave `src/dataset-compiler.ts(673,7): error TS2322: Type 'string' is not assignable to type 'undefined'` against the rebuilt spec `.d.ts`. Restored: blob equals HEAD, `git diff HEAD` empty. The direction was the predicted one (red). - **Gates.** `dispatch-gates.mjs --commands` at `c4771e604` derives 126 families. All 126 were run and recorded, and `--ran` reports 0 UNRUN. - 123 exit 0. - 2 exit 3, PREREQUISITE NOT MET: `check:dual-build-cjs-loads` and `check:type-check-debt` (both need the whole-repo build). - 1 exit 1: `check:platform-checklist`, inherited from `main` (see Acceptance notes). Its inputs are byte-identical to `main`, and it is not a per-PR CI gate. - **Lint (a proven narrowing).** - Scope: `eslint --no-inline-config --format json` over exactly the 95 changed code files returned 0 errors and 0 warnings. - Population: read from `eslint.config.mjs` (`files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']`); the file count comes from the JSON output. - Invariance: the config itself records that it never enables type-aware linting (no `parserOptions.project`), so no untouched file's verdict can move. **NOT MEASURED** (CI runs these): - `qa/dogfood`: the two touched dogfood tests and `expression-conformance`. The package does not resolve `@objectstack/verify` unbuilt, so no test ran. Static reading: 0 `analytics.zod` references in that ledger, against 11 `.zod.ts` references as the control. - The showcase `typecheck`: 7 TS2307 for unbuilt connectors and plugins, and 0 diagnostics in `showcase.cube.ts`. - `downstream-contract`'s `consumer-specifier-ledger` needs `@objectstack/cli` built. - The two exit-3 gates above. ## Skills readings `skills/objectstack-ui/rules/dashboards.md` goes 468 → 468 lines. The whole package (every `SKILL.md`) goes 4404 → 4404. Against base the hunk is 6 lines modified and nothing added: each change deletes a `name: 'KEY',` fragment. ## Acceptance notes (noted, not filed) - `check:platform-checklist` is red on `main` at `3cf644938`: - The failure: `areas/identity-auth.json` anchors `plugin-auth/src/auth-plugin.ts#twoFactor`, and `7d6308895` (objectstack-ai#20429) turned that line-start key into an inline nested object key (`plugins: { twoFactor: true }`), which the shared resolver reads as absent by design. - It is independent of this diff: both files are byte-identical to `main`. - The gate is run by hand, not per PR. Carrier: the next PR to touch `identity-auth.json` or `auth-plugin.ts`, or the checklist owner. - The inner `name` carried a snake_case regex. The record key never had one, and it legitimately takes camelCase and dotted spellings in-repo (`driver-memory` fixtures; `'owner.amount_sum'` in `dotted-measure-refusal.test.ts`). Nothing is enforced on the key today; this is an observation, not a change here. - The absence pin states its blind spot: members built under computed keys (`Object.fromEntries(… { name: n, … })`). tsc found the typed ones in `driver-memory`, and the rest of that shape was swept by an AST scan (object literals holding `name`, `sql` and `type`). What remains is only this pin's own refusal specimens and the schema shape. ## 维护者速读(草稿) **改了什么**:分析立方体(cube)的度量与维度不再接受内部 `name` 字段;成员的名字就是它在 `measures` / `dimensions` 里的键。写了 `name` 会在编译期和解析期被明确拒绝,并给出迁移提示。已存储的立方体在加载时自动去掉该字段,照常可用。 **为什么改**:这个字段从来没有任何代码读取,系统一律按键识别成员;当 `name` 与键不一致时,作者写的值被静默忽略。Cube.dev、LookML 等主流方案也只有一个名字。按您「主流平台有没有这个能力」的判据,判定退役。 **风险与代价(含回滚)**:对外行为不变 —— `/analytics/meta` 与查询接口的成员名仍是 `立方体.键`。仍写 `name` 的作者源码需要删除该字段(`os migrate meta --from 17` 列出改动)。回滚:还原本 PR 即可,无数据迁移需要撤销。本 PR 同时改了一个对外发布的 skill 示例(仅删除 `name`),因此需要您的批准。 **席位意见**: **你要做的**:审阅后批准(Approve)本 PR。 Line 3 and Deviation 5 were amended by the `domain:spec` seat 1 (`session_01B3TqpoQbTAfG7G74GMDWNW`) before the at-tier review: this diff widens no accept set and adds no export, so `Clause-②` is `no (narrowing)`, as most retirements of this family on `main` declare. `20323-action-aria-removed.md` declared `yes`; the definition in `clause2-line.mjs` decides, not the precedent. The changeset line moved with it in `2252e5728`, and the claim on objectstack-ai#20300 was amended in place. The patch-round section above was added by the same seat after the at-tier record `5875291969`. --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20412
Clause-②: no
What changed
AuthPlugin'sauthsettings pass (bindAuthSettings→applySettings,packages/plugins/plugin-auth/src/auth-plugin.ts) used to send the whole namespace toAuthManager.applyConfigPatch()as ONE patch inside one outertry. When the manager refused one key, the whole patch was dropped (password policy, MFA, rate limits, session lifetime, social providers) and the only trace wasctx.logger.warn('Auth: failed to apply auth settings: …'), while the settings console showed every value as saved.Seat ruling applied: a refused key does not take its accepted siblings with it.
applyConfigPatchvalidates a patch on entry wherever it carriesemailAndPassword(assertAudienceConfig, against the standing audience posture) orplugins(assertScimAdminCoherence). Each settings key that lands in one of those blocks is applied ALONE. Every other key goes out in ONE application that the manager does not validate.mfa_required: truestays one piece with thetwoFactorplugin it turns on. A refusedpluginsblock therefore leaves MFA at its standing value too, and MFA is never enforced without its enrollment endpoints.try. A refusal is logged ONCE aterror, naming the key, saying what keeps ruling and what the console shows, followed by the manager's own message, which carries the remedy. This follows the audience block beside it:[auth] auth settings REFUSED (auth.require_email_verification) — the standing runtime value keeps ruling while the settings console shows the stored value as saved; the other auth settings in this pass still apply. [audience] invalid audience configuration: posture 'open' FORCES email verification on unless the DEPLOYMENT turns it off — …catchis left with only a pass that fails as a whole, the namespace read first among them. It moves fromwarntoerror([auth] auth settings NOT APPLIED — …) for the same reason: stored values the console shows as saved were not applied. This is the same line the card names as the defect.Premise check (the ruling's precondition), measured before writing the fix
The refusal can be attributed without re-implementing validation.
applyConfigPatch(auth-manager.ts, around line 4162 at40b315b0) runs exactly two entry validators, each gated by which top-level block the patch carries. Splitting the pass along that gating gives each refusable key its own verdict from the manager. It needs no knowledge of what the validators check. Per-key isolation yields the same accept/refuse verdict as the whole-block application for every key on this tree:assertAudienceConfigreads onlyrequireEmailVerificationfromemailAndPassword, andassertScimAdminCoherencereads onlyadmin/scim, which this pass never sets. So nothing that used to be accepted is refused, and the refused key is refused for the same reason. Landing point: the plugin (the consumer that composed the one patch), notauth-manager.ts.Live triggers, measured on
main40b315b0before the fix (new pins run against unmodifiedauth-plugin.ts)src/auth-settings-refusal-isolation.test.tsdrives the realAuthPlugin+AuthManagerthrough a stub settings namespace read:40b315b0require_email_verification: falsesaved with siblingssession.expiresInstaysundefined(every sibling dropped)open+ console-storedfalseat bootsession.expiresInundefinedemail_domain+ env (OS_AUTH_REQUIRE_EMAIL_VERIFICATION)falsesession.expiresInundefinedpluginsSCIM/admin coherence refusal (OS_SCIM_ENABLEDappearing after construction withplugins.admin: false)session.expiresInundefinedwarnonlyResult:
Tests 5 failed | 1 passed (6), with every negative pin failing onexpected undefined to be 259200/604800or on the missingerrorline.The settings service has no cross-field validation between
require_email_verificationandaudience_posture(packages/services/service-settings/src/manifests/auth.manifest.ts). The first trigger is therefore reachable by two console saves, with no stack config or env involved.Surface beyond the claim's file list (declared)
scripts/check-durability-degradation-log-level.mjs:applyConfigPatchjoinsDURABILITY_CRITICAL_CALLEES(AGENTS.md, Degradation log levels: "found a new one, add it toDURABILITY_CRITICAL_CALLEESin the same PR that fixes it"). Seams 36 → 38, all loud. Withauth-plugin.tsat40b315b0, the gate reds on the oldwarncatch (ablation below).scripts/measure-durability-swallow-family.mjs: that census's by-valuegate-vocabularycopy gains the same name.check:swallow-census-controlsreds on the drift otherwise, measured: "declared by the gate, missing from the copy: applyConfigPatch".scripts/engine-double-contract.pinned.json:node scripts/check-engine-double-contract.mjs --writefor the new test file'sfindOnedouble. The double is copied fromaudience-posture-setting.test.ts, which is already pinned. The gate itself asked for this: "New pinned coverage is GOOD … Run--writeand commit". The result was one added row and 0 seam rows lost.No
packages/spec, noservice-settings, noauth-manager.tschange.Tests
packages/plugins/plugin-auth/src/auth-settings-refusal-isolation.test.ts(6 tests):password_min_length(sameemailAndPasswordblock),password_require_complexity,mfa_required+twoFactor,rate_limit_max,session_expiry_days, Google social provider;errorline naming the key and carrying the manager's remedy text, and the oldwarnnever fires;password_reject_breached,mfa_required) and keeps MFA with its plugin;errorline (auth settings NOT APPLIED).pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2:Test Files 115 passed (115),Tests 2460 passed (2460)(at846b2858; later commits touch onlyscripts/ledgers and the census copy).pnpm --filter @objectstack/plugin-auth run typecheck(afterpnpm --filter @objectstack/plugin-auth build): exit 0. The new test file is in thetsconfig.test.jsonprogram (--listFiles: 1 hit), andcheck:test-typecheck: OK.pnpm exec eslint --no-inline-config --format jsonon the four changed lintable files: 4 files, 0 errors, 0 warnings. The population is read fromeslint.config.mjs(files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']minusNEVER_LINTED; none of the four reported as ignored). Invariance: type-aware linting is not enabled anywhere ineslint.config.mjs(noparserOptions.project, noprojectService), so this diff cannot move any untouched file's verdict. The repository-widepnpm lintis CI's.Ablations (the fix committed first; each leg's restore proven by blob hash plus an empty
git diff HEAD)auth-plugin.tswas restored to40b315b0(tree only). Proven on disk: the blob equals the base blobdd862c8d, the oldwarncount is 1 and theapplyPiececount is 0. The new pins went red,Tests 5 failed | 1 passed (6)(four onexpected undefined to be 259200/604800, one on the missingerrorline; the control stayed green).check:durability-log-levelwent red:✗ 1 durability report(s) degrade quietly … packages/plugins/plugin-auth/src/auth-plugin.ts:1799. Restored to the HEAD blob45e547d5.warn(node scripts/ablation-replace.mjs, anchor hit 1 → 0, blob45e547d5→0a104f02). The pins went red,Tests 4 failed | 2 passed (6): each fails atexpected [] to have a length of 1(2for the plugins test). The siblings still applied, so this leg isolates the level pin.check:durability-log-levelwent red atauth-plugin.ts:1767. Restored to45e547d5withgit diff HEADempty.Gates (derived by
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsat the final head, 86 commands, all run at950c40c5)check:durability-log-level,check:swallow-census-controls,check:engine-double-contract,check:logger-receiver-detach,check:doc-authoring,check:nul-bytes,check:issue-citations,check:adr-0087-registration --base origin/main,check:changeset-no-majorandcheck:pm-dispatch-gates(its battery took 1026.8s on this shared box).check:dual-build-cjs-loads. Reason: PREREQUISITE NOT MET (exit 3), because the gate reads the built output of every workspace package and 39 have nodist/in this worktree. A targeted probe:plugin-auth's owndist/index.js(CJSrequire) anddist/index.mjs(ESMimport) both load, andAuthPluginis a function.check:type-check-debt. Reason: PREREQUISITE NOT MET (exit 3), because the ledgered packagesruntime,service-clusterandservice-jobhave no built type entry. This diff touches none of them.plugin-auth's own typecheck andcheck:test-typecheckare green above.check:optional-error-sink,check:startup-registry-verdict,node scripts/measure-return-propagating-durability-seams.mjs,node scripts/measure-durability-swallow-family.mjs --self-test(all families), andpackages/metadata-protocolsys-metadata-repository.draft-drain.test.ts, which reads the gate file (11/11). All exit 0.Acceptance notes
open→invite_only) and storesrequire_email_verification: false. That save logs oneREFUSEDline for the key. The effective requirement already matches (underinvite_onlyan undeclared value resolves to off), and the next pass accepts the storedfalse. Before this change the same save dropped the whole pass atwarn. No one else is carrying this: noted, not filed.pluginsrefusal. It is reachable from this pass only whenOS_SCIM_ENABLEDappears afterAuthManagerwas constructed withplugins.admin: false. The constructor refuses an incoherent config at boot, andbuildPluginList()separately refuses the lazy better-auth build in that state. The pin holds the MFA/twoFactorcoupling and the per-key naming, and it is not a claim of common reach.REFUSEDline.Auth: failed to apply auth settings:is gone. It is replaced by[auth] auth settings REFUSEDand[auth] auth settings NOT APPLIED.Generated by Claude Code