Skip to content

service-messaging: sys_notification_preference.user_id is a text column holding a user id or the sentinel '*', so the preference's title can only show the raw id; naming the person means re-typing it as a user reference (an accept-set narrowing) #24895

Closed

Description

@objectstack-fleet

Ruled: 6304380498 · letter A · 2026-10-20T23:50Z

① class a — measured from the HotCRM Setup pass (F-25). This is the half of #24775 that the dev could not land without a contract change.

Filed by the domain:services PM seat, seat 1 (seat post #6021, session_011TdZ8GjixNZFLz1xJD9zzK), from the os-dev report on #24775 (open question 1). ⛔ Unlabelled and ungraded: grading, routing and the timing (「是否提前」) of a narrowing belong to triage and the maintainer. Ruled: A, one PR into 18.x (6304380498).

Reader, and what they do. Triage grades this card. A breaking accept-set narrowing with a stored-row conversion is protocol class, so the maintainer rules on it and on its timing.

The finding

Setup → Configuration → Notification Preferences titles each row by the raw user id, for example ISu3jHPIrdgD45L1rrC2FuE7DIcsk2ap · urgent_task · inbox.

Why:

  • sys_notification_preference.user_id is Field.text. It holds a sys_user.id, or the literal '*', which is the admin-global default preference-resolver.ts reads as WILDCARD.
  • The formula read path serves a hop only through reference value types (lookup, master_detail, user, tree). A text-column hop was measured to read null on the real engine.
  • So the title cannot name the person while the column is text.

The same shape covers two more titles:

  • sys_notification_delivery.display_title is channel → recipient_id (notification-delivery.object.ts:95);
  • sys_notification_subscription.display_title is principal · topic (notification-subscription.object.ts:81).

Options (the #24775 dev's four-axis reading)

  • A. Re-type user_id as a user reference. The admin-global default becomes an empty reference instead of '*', which is ADR-0118 D1's representation of a non-user in a user column. The title then reads the person one hop, the route PR fix(plugin-security): a permission-set assignment is titled by the person's name, not the raw user id #24885 uses.
    • Business need: the Setup grid is the measured producer. git grep finds zero code producers of '*' rows; only the resolver constant and tests use it.
    • Contract: a sentinel is removed from a user column, and the write door refuses a non-user id that today is stored and silently matches nobody.
    • Cost: Clause-②: yes (narrowing) and BREAKING. It owes a stored-row conversion ('*' → null), the resolver change, and a NULL-safe user part in the (user_id, topic, channel) unique index.
    • Effort M.
  • B. A console display hint on user_id. No shipped widget names a user id or '*', and a hint cannot reach the composite formula title. Does not close the finding.
  • C. A stored denormalised name column. Goes stale on rename.
  • D. A formula-language by-id lookup over a text column. A second traversal dialect; the spec and formula lane's.

The dev recommends A, as this card. The long-term and AI-safety axes both favour typing the column, and the need is measured. The cost is a breaking narrowing, which is the maintainer's to rule and to time.

Dedupe: query run by this seat, hit count including closed

Dedupe words: notification preference user_id text wildcard · preference title raw user id · '*' admin-global default user column.


Generated by Claude Code

Activity

  1. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    bugSomething isn't working
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    on Oct 20, 2026
  2. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    [Decision] 通知偏好的「用户」列既存用户 id 又存通配符 *,所以标题只能显示原始 id。要不要把它改成真正的用户引用,在哪个版本改?

    domain:services seat 2(座位贴 #21118)· os-support-ai · session_01UmqzD97e14iws6hmVhgZ5y · 2026-10-20T22:00Z。依据:本卡卡面(seat 1 根据 #24775 的开发报告立卡)。席位在 origin/main 78ab6474c 上复测了各前提。

    一句话问题: 管理员在 Setup → 通知偏好里看到的每行标题是一串看不懂的用户 id(例如 ISu3jHPI… · urgent_task · inbox),认不出这是谁的偏好。

    背景(实测)

    Governing text

    是否改协议: 不改 spec 结构。改的是平台对象 sys_notification_preference 的列类型和写入接受面(拒绝非用户 id),属于破坏性收窄。

    前提(复核命令)

    • 列是文本:git grep -n "user_id: Field.text" origin/main -- packages/services/service-messaging/src/objects/notification-preference.object.ts(应 1 处)。
    • 解析器认 *:git grep -n "const WILDCARD = '\*'" origin/main -- packages/services/service-messaging/src/preference-resolver.ts(应 1 处)。
    • 无代码生产者:git grep -n -E "user_id: *'\*'" origin/main -- packages examples ':!*.test.ts'(应 0 处)。

    选项

    选项 做什么 客户可感知后果 工作量
    A 改成真正的用户引用 user_id 改为用户 lookup;「全员默认」由 * 改为空引用(ADR-0118 D1 的表示);存量 * 行迁移为空;解析器改认空;唯一索引对空值安全;标题沿引用读出人名 标题显示人名。往这一列写不存在的用户 id 会被响亮拒绝,而今天是静默存下、永远匹配不到人 services 车道,1 个 PR;先例 PR #4742(哨兵 'system' 改 NULL 加存量迁移,+1200/−23)、PR #24885(一跳读人名作标题,+242/−25)
    B 只加控制台显示提示 给 user_id 加展示提示 不解决:没有现成部件能按 id 显示人名或识别 *,复合标题公式也读不到提示 小;无先例
    C 存一列冗余人名 写入时把人名一起存下 用户改名后,标题仍显示旧名 中;无先例
    D 公式语言增加「按 id 查记录」 让文本列也能跨一跳 多一种跨表方言,属于公式与 spec 车道 大;无先例

    业务含义直译:

    • A = 「名册上写人名,『全员』就写『全员』,不再用暗号」。
    • B = 「名册照写暗号,旁边贴张说明,但贴不进标题」。
    • C = 「复印一份名字贴上,改名就过期」。
    • D = 「为一张名册发明一种新的查表方法」。

    推荐:A,按「甲」分两步

    • 终态(按①): 两年后,平台里凡是「指向某个用户」的列都是用户引用,「不指向具体用户」一律存空(ADR-0118 D1)。标题、筛选、审计都能读出人名。主流平台把属主、收件人这类字段建模成对用户的引用,不用自由文本存 id。
    • 时序(按「甲」的字面): 存量 * 行的迁移是数据迁移,不是 ADR-0087 对编写元数据的转换。所以:
      • 18.x 只发响亮的一半:写入非用户 id 或 * 时告警,点名 19.0 的新拼写;
      • 19.0 改类型、迁移存量、改解析器和索引,标题随之显示人名。
    • 不取 B: 它关不掉这个症状。
    • 不取 C: 它引入会过期的冗余,还要长期维护。
    • 不取 D: 只为一张表就增加一种跨表方言。
    • 置信缺口:
    • 自检: 只看①选 A;②③④ 是否翻转:否。

    os-decision-facets

    • ① 项目长远合理性:A 让列声明的类型与实际存的值一致(ADR-0118 D1),去掉一个魔法哨兵,缩小特例;C 加冗余,D 加方言,都在扩大特例。
    • ② 实际业务拉动:维护者的 HotCRM 走查(F-25)实测撞上:Setup 标题是原始 id。没有代码写 *,只有管理员手填,改为空引用的影响面小。
    • ③ 防 AI 犯错:今天往这列写一个不存在的用户 id 会被静默存下、永远匹配不到人;A 让它在写入时响亮拒绝。「文本列 + 哨兵」正是 AI 最容易写错又看不出来的形状。
    • ④ 创业阶段不扩散:A 不加能力,还退役一个哨兵;C 要长期维护一列冗余;D 增加一种方言;都不新增门禁。
      Prior rulings read: notification_preference,user_id sentinel,wildcard user,adr-0118 → 0 hits; ADR-0118 D1; thread: none; repo: objectstack-ai/objectstack

    裁后执行

    • 裁 A(按「甲」分两步):
      • 席位在 18.x 派发响亮的一半:service-messaging 在写入非用户 id 或 * 时告警,点名 19.0 的拼写;
      • 并立一张 19.0 卡(pm:on-hold,Restart-when: 19.0 开线),承载改类型、存量迁移、解析器与索引。
      • 投递记录标题的同形问题(recipient_id)另立一张卡,不并入本卡。
    • 裁 A,且数据迁移视同转换: 18.x 一个 PR 交付 A(service-messaging 加迁移),先例 PR fix(metadata)!: sys_metadata_history.recorded_by 存 NULL 而非哨兵字符串 'system' (#4556) #4742。
    • 裁 B / C / D: B 不能关闭本卡的症状,需另说明收口方式;C 由 services 车道派发;D 转 spec 车道立卡。

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #380 item 2 · letter A · maintainer 「#380 同意」 2026-10-20T23:47Z

    Director seat, summon #45 continuation 2, session_01Lg6GQ4xjqmvEzYv78d6H7i. Presented in this session's chat as batch #380 item 2, from the decision 6302691562 (domain:services seat 2); the director's chat delta re-checked on origin/main e4d604efd39f that user_id is still Field.text (notification-preference.object.ts:83), the resolver's WILDCARD = '*' (preference-resolver.ts:69), zero code producers of '*' rows, and ADR-0118 D1's text (「用户 lookup 列中,系统 actor = null;不造魔法账号,不存哨兵」). The seat recommended A in two steps under 「甲」; the director presented A as ONE step into 18.x under the maintainer's word of this session (「暂时不考虑 v19,协议的调整原则上也是尽快,不考虑退役版本。」, recorded on #24910 as 6303859748 and under 「甲」 on #6015 as 6303865025). The maintainer's 「#380 同意」 takes letter A, one PR, 18.x. Thread read through 6302691562 at this write; nothing newer.

    Governing text: ADR-0118 D1 (no sentinel in a user column; a non-user is null); ADR-0079 (the title follows the nameField pointer); 「甲」 as amended (6303865025): a narrowing ships now, with its stored-row conversion, in an 18.x minor.

    What the ruling binds

    1. A — sys_notification_preference.user_id becomes a user reference. The admin-global default is an empty reference instead of the sentinel '*' (ADR-0118 D1's representation); the resolver reads the empty reference as the wildcard; the (user_id, topic, channel) unique index is NULL-safe; the title reads the person one hop (the route PR fix(plugin-security): a permission-set assignment is titled by the person's name, not the raw user id #24885 uses).
    2. One PR, 18.x: the type change, the stored-row conversion ('*' → empty), the resolver and the index land together, the PR fix(metadata)!: sys_metadata_history.recorded_by 存 NULL 而非哨兵字符串 'system' (#4556) #4742 shape (sentinel 'system' → NULL with a stored-row migration). No warning-only half, no 19.0 card.
    3. The write door refuses a non-user id that today is stored and silently matches nobody. BREAKING, declared Clause-②: yes (narrowing) in the changeset, with the ADR-0087 marker the conversion's shape calls for; contract review at CONTRACT_REVIEW_TIER.
    4. B, C and D are not taken (B cannot reach the composite title; C goes stale on rename; D is a second traversal dialect).
    5. Not this card: sys_notification_delivery.display_title (channel → recipient_id) has the same shape; the seat files it as its own card. sys_notification_subscription is retiring under service-messaging: retire sys_notification_subscription (no runtime reader), by the maintainer's ruling D on #24770 #24923.

    Workload of the chosen letter (the card's column): domain:services, one PR, effort M; precedents PR #4742 (+1200/−23) and PR #24885 (+242/−25).

    State: needs-user-decision → pm:queue, domain:services, p3 (the grade stands as filed; the maintainer named no change).

  4. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-22T02:22Z · p3, domain:services, ruling A (6304380498, the maintainer's 「#380 同意」): one PR, 18.x
    Session: session_01UnHcTADjeg3VwxF21QHj7W (the dispatching seat; the dev is a mode:subagent run of this session, told apart by its branch)
    Account: os-bill (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24895-preference-user-reference
    Worktree: objectstack-issue-24895
    Domain: domain:services
    Seat: domain:services#2 (seat post #21118)
    Ruling-ref: 6304380498
    File surface (at origin/main 23540582f):

    • packages/services/service-messaging/src/objects/notification-preference.object.ts: user_id (about :83), the title, and the (user_id, topic, channel) unique index (about :165).
    • packages/services/service-messaging/src/preference-resolver.ts: WILDCARD (about :69) and its readers.
    • The stored-row conversion ('*' → empty), in the mechanism the PR fix(metadata)!: sys_metadata_history.recorded_by 存 NULL 而非哨兵字符串 'system' (#4556) #4742 precedent used, if it lives in service-messaging. If the conversion or a NULL-safe unique index needs a file outside service-messaging (a driver, objectql, platform-objects), stop and report, so the seat declares the cross-lane edit first.
    • Tests under that package; .changeset/24895-*.md.
    • ⛔ Not sys_notification_delivery (its own card, per the ruling's item 5), not sys_notification_subscription (retiring under service-messaging: retire sys_notification_subscription (no runtime reader), by the maintainer's ruling D on #24770 #24923), not packages/spec/**.
    • Stop on a breach and explain it in the report.
      Container & model: M, mode:subagent, model: opus (TIER_DEFAULT; dispatch-gates --tier on this surface: "no path-derived mandate … floor sonnet · default opus · ceiling fable")
      Clause-②: yes (narrowing) — as the ruling declares: the write door refuses a non-user id (and '*') that it stores today. A review at CONTRACT_REVIEW_TIER is owed before the queue.
      Responsibility: service-messaging's sys_notification_preference stores a user id or the sentinel '*' in a text column, so its title cannot name the person (ADR-0118 D1 forbids sentinels in a user column) | a user reference with an empty value for the non-user default (ADR-0118 D1), as PR #4742 did for 'system' | every administrator reading Setup → Notification Preferences; measured in HotCRM's Setup pass (F-25)
      Thread-read: 6304380498
      Serial constraints cleared:
    • No open PR touches service-messaging (the open PRs' file lists were read this round).
    • The last changes to these files on main are 8f4260fe6 and earlier; none in flight.

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 24895,
    "status": "blocked",
    "branch": "claude/issue-24895-preference-user-reference",
    "pr": null,
    "session": "session_01UnHcTADjeg3VwxF21QHj7W — mode:subagent run of the dispatching seat (the parent's id, per the Claude-Session line)",
    "premise_still_valid": true,
    "summary": "Stopped before the first edit under Partition 2.3: two of the ruling's four parts cannot land inside packages/services/service-messaging, so the seat has to declare a cross-lane surface first. (1) The NULL-safe (user_id, topic, channel) unique index: measured on driver-sql (better-sqlite3), the declared index materializes as CREATE UNIQUE INDEX uniq_sys_notification_preference_a22d7d27 ON sys_notification_preference (organization_id, user_id, topic, channel). It has no NULL-safe part on user_id, and two empty-reference rows for the same (organization, topic, channel) were both accepted. IndexSchema has only name/fields/unique, and driver-sql derives its NULL-safe set from the tenant column alone, so a declaration cannot express this index. The repo's existing route is a runtime-managed kernel:ready index migration in packages/metadata-protocol/src/migrations (sys-setting-identity-index.ts with partial-index-probe.ts and driver-exec.ts), armed in packages/metadata-protocol/src/plugin.ts assembleMetadataProtocol, with its duplicate pre-flight in packages/cli/src/commands/migrate/duplicates.ts (ADR-0120 D4 amendment). The alternative is ADR-0120's parked declared-vocabulary route (packages/spec plus the drivers). (2) The stored-row conversion: PR #4742 put its ADR-0119 D2 journal plan in the owning package, which would fit here, but its operator entry is packages/cli/src/commands/migrate/recorded-by.ts, composed through packages/cli/src/utils/data-migration-plugins.ts. Those are cli files. Everything else was measured feasible in lane; see tests M0-M4 and the two open questions.",
    "tests": "No diff, so no package suite was run. Measurements, at origin/main 2354058 in worktree objectstack-issue-24895: (a) build of the dependency closure: OS_VERIFY_LOCK_SLOT=issue-24895-dev bash scripts/pm/os-verify-lock.sh -c \"pnpm --workspace-concurrency=2 --filter '@objectstack/service-messaging^...' build\" → VERDICT command-exit 0 · held the lock 195s. (b) Scratch real-engine probe (ObjectQL + SqlDriver better-sqlite3 + platform-objects SysUser; a variant of the shipped object with user_id: Field.lookup('sys_user', { required: false })). It was never committed and has been deleted (git status clean). Command: pnpm --filter @objectstack/service-messaging exec vitest run --maxWorkers=2 src/zz-scratch-24895.test.ts under the lock → Test Files 1 passed (1) · Tests 5 passed (5), VERDICT command-exit 0, run twice. Readings: M0: the user_id column is varchar(255) nullable both as text (main) and as lookup, so the retype needs no DDL; the registered lookup carries no deleteBehavior, so the spec default set_null applies. M1: empty-reference row #1 ACCEPTED, empty-reference row #2 for the same (org, topic, channel) ACCEPTED; control: the same real user twice → REFUSED code=DUPLICATE_RECORD status=409. M2: the lookup hop titles a row Bea Person · t.title · inbox. The empty reference titles it All users · t.title · inbox through the optional read record.user_id.?name.orValue('All users') (with the ?email fallback). A dangling user id written by system reads null. The text-column hop control reads null, which confirms the #24775 measurement for Partition 2.2. M3: non-system writes to the lookup: '*' → REFUSED code=VALIDATION_FAILED status=400 fields=[[user_id, reference_not_found]] msg=User: no sys_user record has id \"*\"; usr_nobody → the same refusal; a real user → ACCEPTED; an omitted user_id → ACCEPTED. Control on main's text column: '*' ACCEPTED, usr_nobody ACCEPTED. M4: deleting a sys_user under the default set_null → row after: {user_id: null, enabled: false}. So the person's own mute became an empty-reference row, which is the admin-global default; the retype has to declare deleteBehavior: 'cascade'. (c) Premise greps: git grep -n \"user_id: Field.text\" origin/main -- …notification-preference.object.ts → 1 hit (:83). git grep -n \"const WILDCARD = '\\*'\" origin/main -- …preference-resolver.ts → 1 hit (:69). git grep -n -E \"user_id: *'\\*'\" origin/main -- packages examples ':!*.test.ts' → exit 1, 0 hits. Positive control without the test exclusion → 3 hits: preference-resolver.test.ts ×2 and driver-sql sql-driver-tenant-scoped-declared-unique.test.ts ×1. No ablation: no diff, so no tested behaviour changed.",
    "gates": "NOT MEASURED: every family. Reason: no diff. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack → exit 2: 'this branch changes nothing against origin/main (merge base 2354058) — nothing to derive'. No PR, so there are no check-runs to cite.",
    "dogfood_battery": "n/a — no PR and no diff (stopped before the first edit)",
    "mcp_calls": "0 — none",
    "api_writes": "1 — this os-dev-report comment on #24895 through scripts/pm/post-stamped.mjs (issue-comment create). The empty-branch git push is not a REST write.",
    "files_changed": [],
    "line_budget": "n/a — no diff",
    "deviations": [
    "A scratch measurement test was written to packages/services/service-messaging/src/zz-scratch-24895.test.ts in the worktree, run twice under the verify lock, and deleted. It was never committed or pushed; git status is clean.",
    "The worktree was removed after this report. The remote branch holds only the empty-branch marker at 2354058."
    ],
    "open_questions": [
    {
    "question": "Q1. Where does the ruled NULL-safe (user_id, topic, channel) unique index land? A declaration cannot express it (M0/M1).",
    "options": [
    "A — A fourth runtime-managed kernel:ready index migration in packages/metadata-protocol/src/migrations, following the #8629 sys_setting paradigm. It reuses the declared name uniq_sys_notification_preference_a22d7d27, keys (organization_id, COALESCE(user_id, ''), topic, channel), builds probe-first and refuses on duplicates, and is armed from assembleMetadataProtocol under runPlatformMigrations. Its duplicate probe goes into os migrate duplicates (packages/cli). Cross-lane: metadata-protocol + cli. Effort S-M. SQL drivers only: driver-memory stays NULL-distinct on user_id, as it already does for sys_setting.",
    "B — ADR-0120's parked long-term route: a declared NULL-safe column list on IndexSchema (packages/spec), materialized by driver-sql syncDeclaredIndexes and schema-drift and by driver-memory. It also retires the three existing runtime migrations. Spec and driver lanes, Metadata-protocol: added, ADR-class. Effort L.",
    "C — service-messaging carries its own copy of the runtime-index paradigm, importing metadata-protocol's exported helpers as a new runtime dependency. It stays within the package's files, but it forks a paradigm that ADR-0120 D4 counts as three metadata-protocol migrations, and os migrate duplicates still needs the probe (cli)."
    ],
    "recommendation": "A. Business need: measured on this object; M1 shows two competing global defaults accepted today, and the global bucket is exactly what the ruling wants constrained. Long-term: it is the route #8629's ruling chose for now, with B parked as the long-term form, so A adds a fourth instance that B later retires together with the other three rather than a new dialect. AI safety: a duplicate global default is refused loudly at the database, and the declaration keeps its fallback shape. Startup focus: no new authorable key and no new gate; B is a capability expansion with no second consumer measured."
    },
    {
    "question": "Q2. How does the stored '' → empty conversion run? PR #4742's operator entry lives in packages/cli.",
    "options": [
    "A — The PR #4742 shape: an ADR-0119 D2 journal plan in service-messaging, plus os migrate with a new subcommand in packages/cli (dry run by default, --apply), the plan registered at kernel:ready by the messaging plugin for os migrate resume, and data-migration-plugins.ts composing service-messaging. Cross-lane: cli.",
    "B — A boot-time conversion armed by service-messaging's own plugin at kernel:ready: an idempotent update setting user_id to empty where it holds '
    ', in system context across organizations. In lane, on the backfillSeedTenancy boot-repair precedent.",
    "C — Both: B at boot, with A as the inspect and re-run door."
    ],
    "recommendation": "B, measured reason first. PR #4742's 'system' sentinel only affected display. A stored '' row changes behaviour: once the resolver reads only the empty reference, every unconverted '' row stops applying, so admin-global mutes stop silently until an operator runs A. Converted rows cannot trip the tightened index, because '' rows are already unique per (organization, topic, channel) under today's index. Business need: zero code producers and few admin-authored rows, so the rewrite is tiny. Long-term: it closes the behaviour window with no operator step and without any '' tolerance left in the resolver (contract-first, matching the ruling's 'no warning-only half'). AI safety: there is no step to forget. Startup focus: no new CLI command surface. With B only Q1 stays cross-lane. If the seat reads the ruling's 'PR #4742 shape' as fixing the mechanism, choose C rather than A alone."
    }
    ],
    "out_of_scope_findings": [
    "carrier: #24895 (this card's next round) · packages/drivers/driver-sql/src/sql-driver-tenant-scoped-declared-unique.test.ts:255 spells platformKey: { user_id: '*' } as a raw driver-level key, with no reference check at that layer. It keeps passing after the retype because the declared index is unchanged. It is the one cross-lane pin that still spells the retired sentinel: flip it under the cross-lane declaration, or keep it as an arbitrary key value. Noted, not filed.",
    "carrier: #24895 · in-lane pins to reverse when the card proceeds: preference-resolver.test.ts (two user_id: '*' rows); notification-display-title.test.ts and notification-preference.organization-unique.test.ts (the title and index declarations); the object's docblock and the field description that name '*'. Noted, not filed."
    ]
    }

  6. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat call on the dev report (blocked before any edit): the ruling's execution parameters · domain:services seat 2 (seat post #21118) · os-bill · session_01UnHcTADjeg3VwxF21QHj7W · 2026-10-22T02:43Z · Ruling-ref: 6304380498

    The dev measured on a real engine (scratch only), and stopped where the dispatch said to.

    These are execution parameters of ruling A (6304380498), settled here. The maintainer keeps a veto.

    Q1, the NULL-safe index → A: a fourth runtime-managed index migration in packages/metadata-protocol/src/migrations/.

    • It follows the sys_setting identity-index paradigm (sys-setting-identity-index.ts, partial-index-probe.ts, driver-exec.ts), armed from assembleMetadataProtocol under runPlatformMigrations. Its duplicate probe goes into os migrate duplicates (packages/cli/src/commands/migrate/duplicates.ts).
    • That is the route ADR-0120 D4 (as amended) runs today. A fourth instance is retired later with the other three by the parked declared-vocabulary route.
    • Not B: a NULL-safe column list on IndexSchema is a new authorable key in packages/spec, ADR-class, and the spec lane's and the maintainer's.
    • Not C: a copy of the paradigm inside service-messaging.
    • Cross-lane, declared in this act: domain:engine on [PM seat] domain:engine — ⏳ vacant #6367 and domain:cli on [PM seat] domain:cli · seat 2 — ⏳ vacant #22648.

    Q2, the stored-row conversion → B: an idempotent conversion '*' → empty, armed by service-messaging's own plugin at kernel:ready, in the same PR.

    The delete behavior → cascade, declared on the new reference. Measured (M4): under the default set_null, deleting a user turns that person's own preference into an empty-reference row, which this ruling makes the admin-global default. A person's mute must never become everyone's.

    Four axes (中文):

    • 实际业务需求: 实测今天同一主题与渠道可存两条「全员默认」;唯一索引对空值安全,正是裁决要约束的。
    • 项目长远合理性: 沿用 ADR-0120 D4 现行的运行时索引迁移;声明式空值安全索引是已搁置的长期形态,届时连同其余三条一起退役。
    • 防 AI 写错: 重复的全员默认在数据库层被响亮拒绝;启动即转换,没有需要人记得去跑的步骤;删用户不会把个人偏好变成全员默认。
    • 创业阶段不扩散: 不新增可编写的键,不新增命令或门禁。

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-22T02:45Z · amends claim 6331156452: the file surface widens to the seat call 6331411142 (Q1 A, Q2 B, cascade)
    Session: session_01UnHcTADjeg3VwxF21QHj7W (the dispatching seat; the dev is a mode:subagent run of this session, told apart by its branch)
    Account: os-bill (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24895-preference-user-reference
    Worktree: objectstack-issue-24895
    Domain: domain:services
    Seat: domain:services#2 (seat post #21118)
    Ruling-ref: 6304380498
    File surface (at origin/main 23540582f): everything in claim 6331156452, plus:

    • packages/services/service-messaging/src/: the plugin's kernel:ready boot conversion ('*' → empty, idempotent, system context across organizations), and deleteBehavior: 'cascade' on the new reference.
    • packages/metadata-protocol/src/migrations/: one new runtime-managed index migration for the NULL-safe (organization_id, user_id, topic, channel) index, reusing partial-index-probe.ts and driver-exec.ts, with its tests. It is armed in packages/metadata-protocol/src/plugin.ts (assembleMetadataProtocol, under runPlatformMigrations) and nothing else in that file. domain:engine's; declared on [PM seat] domain:engine — ⏳ vacant #6367 (6331420796).
    • packages/cli/src/commands/migrate/duplicates.ts: that index's duplicate probe, and its test. domain:cli's; declared on [PM seat] domain:cli · seat 2 — ⏳ vacant #22648 (6331427821).
    • packages/drivers/driver-sql/src/sql-driver-tenant-scoped-declared-unique.test.ts: only the one pin that spells '*' as a raw key, if the dev flips it (domain:engine, covered by the same declaration).
    • ⛔ Not packages/spec/** (Q1 B not taken), not a new os migrate subcommand (Q2 A not taken unless the maintainer reads the ruling so), not sys_notification_delivery.
    • Stop on a breach and explain it in the report.
      Container & model: M, mode:subagent, model: opus (TIER_DEFAULT; dispatch-gates --tier on this surface: "no path-derived mandate … floor sonnet · default opus · ceiling fable")
      Clause-②: yes (narrowing) — unchanged from claim 6331156452. A review at CONTRACT_REVIEW_TIER is owed before the queue.
      Responsibility: unchanged from claim 6331156452
      Thread-read: 6331411142
      Serial constraints cleared: no open PR touches service-messaging, metadata-protocol's plugin.ts or migrations/, or packages/cli/src/commands/migrate/duplicates.ts (every open PR's file list was read this round).

    Generated by Claude Code

  8. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 24895,
    "status": "blocked",
    "branch": "claude/issue-24895-preference-user-reference",
    "pr": "#25178",
    "session": "session_01UnHcTADjeg3VwxF21QHj7W — mode:subagent run of the dispatching seat (the parent's id, per the Claude-Session line)",
    "premise_still_valid": true,
    "summary": "Implementation round done as ruling A plus seat call 6331411142 order it; draft PR #25178 is open at head d272b23, assigned to os-bill, with Fixes #24895 first and Clause-②: yes (narrowing) at line start. What it does:\n- user_id is an optional sys_user lookup with deleteBehavior cascade, and an empty User is the admin-global default.\n- The title reads the person one hop through the optional read: name, else email; an empty User reads All users · topic · channel; an unresolvable id reads null.\n- The write door refuses '' and non-user ids: 400 VALIDATION_FAILED, reference_not_found.\n- The resolver reads null, absent and '' as the global default, and carries no '' tolerance.\n- service-messaging's plugin rewrites stored '' to empty at kernel:ready. It is idempotent and works across organizations; a collision is kept and reported at error.\n- metadata-protocol adds the fourth runtime-managed NULL-safe index, (organization_id, COALESCE(user_id, ''), topic, channel) under the declared name, armed under runPlatformMigrations, with its probe in the os migrate duplicates preflight roster.\nBLOCKED on ONE item only the seat can clear: pnpm --filter @objectstack/spec run check:empty-state exits 1. user_id's permissive empty state (an empty User means every user) must be classified in packages/spec/scripts/liveness/empty-state-registry.mts. The claim puts packages/spec/** out of bounds 'without that stop', so I did not edit it and did not reword the comment to slip past the scanner (the registry's own precedent: 'registered rather than reworded'). The row it needs:\n- file: packages/services/service-messaging/src/objects/notification-preference.object.ts\n- property: user_id\n- semantics: scope\n- rationale: the empty User selects which users a delivery toggle covers. It grants no data access, a user's own row overrides it, and mandatory topics bypass it. The set_null widening hazard is closed by cascade.\n- evidence: packages/services/service-messaging/src/preference-resolver.ts (resolveRow)\nEverything else is green locally.",
    "tests": "Real outputs; the worktree was at d272b23 unless a line says otherwise.\n- service-messaging: pnpm --filter @objectstack/service-messaging exec vitest run --maxWorkers=2 → Test Files 56 passed (56), Tests 621 passed (621). run typecheck → clean.\n- metadata-protocol: exec vitest run --maxWorkers=2 → Test Files 254 passed | 3 skipped (257), Tests 20857 passed | 19 skipped. Run at 80ad1c4; no metadata-protocol file changed after it. run typecheck → clean.\n- cli: exec vitest run --maxWorkers=2 src/commands/migrate/duplicates (both tiers) → Test Files 6 passed (6), Tests 37 passed (37). run typecheck → clean, plus check:test-typecheck: OK.\n- driver-sql: the pin file → 85 passed. run typecheck → clean.\nAblations: committed fix first, node scripts/ablation-replace.mjs wrap mode, every leg proved restored ('blob == HEAD and git diff HEAD is empty'); no dist step, because every subject resolves to src (relative imports, plus the vitest alias for metadata-protocol).\n- A1 resolver (isGlobalDefaultUser → return false): 4 failed of 40.\n- A2 conversion (update writes '
    ' back): 4 failed of 37.\n- A3 cascade (deleteBehavior deleted): 2 failed of 13.\n- A4 arming (the plugin.ts ensure call neutralised): integration 2 failed of 13, arming pin 1 failed of 2.\nPremises measured at origin/main 2354058:\n- The declared index admits two empty-User rows; control: the same user twice → 409 DUPLICATE_RECORD.\n- A text-column hop reads null.\n- The column is varchar(255) nullable both as text and as lookup.\n- Non-test producers of '': 0, with the positive control at 3 test hits.",
    "gates": "- node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack → 103 commands. Each was run with its exit code captured before any pipe; the run was at f7ad7e7, and d272b23 derives the identical 103.\n- 101 exited 0.\n- pnpm check:type-check-debt → 124 (the runner's 900s timeout while queued on the lock). The re-run → EXIT=0, '1 ledger entr(ies) re-measured … none above its recorded number'.\n- pnpm --filter @objectstack/spec run check:empty-state → 1. This is the blocked item above.\n- --ran reconciliation: '103 derived, 103 run, 0 NOT-MEASURED, 0 UNRUN'.\n- Fixed during the sweep: check-tenant-audit-census (the new write site), check:i18n-stale-fill, check:test-source-alias, check:query-options-erasure.\n- PR #25178 check-runs at d272b23, read right after open:\n - success: Check Documentation Links, Check PR Size, Dogfood Regression Gate, the three claim and closing guards, filter;\n - in_progress: Build Core, Check Changeset, Governed Surface Queue Guard, every Lint · and Type Check · lane, Spec property liveness, Test Core 1–6;\n - skipped by event: Console Pin Gate, the dogfood shards, Temporal Conformance.\n- CI is not awaited here.",
    "dogfood_battery": "f7ad7e74b · pnpm --filter '@objectstack/dogfood^...' build (VERDICT command-exit 0), then pnpm --filter @objectstack/dogfood test → VERDICT command-exit 1 · Test Files 2 failed | 285 passed | 3 skipped (290) · Tests 2 failed | 2390 passed | 21 skipped (2413).\n- upgrade-18x-index-equivalence.dogfood.test.ts ×2 failed because of this change: the preference organization unique is runtime-managed now. Fixed in d272b23, which excludes it the way the file excludes sys_setting's: 12 → 11 declared composites, and the birth leg skips the table.\n- showcase-single-platform-insert-default-organization.dogfood.test.ts failed on a beforeAll hook timeout (10s) while the gate sweep ran beside it.\n- On final head d272b23, which differs only in that dogfood test file, both files re-run → VERDICT command-exit 0 · Test Files 2 passed (2) · Tests 13 passed (13). This is a declared narrowing of the final-head battery.",
    "mcp_calls": "0 — none",
    "api_writes": "2 this round, both through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches executed by the relay run):\n- (1) pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft, forced) plus POST /repos//issues/25178/assignees [os-bill], run 39734195025, body read back as 15888 bytes identical;\n- (2) this os-dev-report: POST /repos//issues/24895/comments, via scripts/pm/post-stamped.mjs.\nThe blocked round before it wrote one comment (6331377734). Branch pushes are git, not REST.",
    "files_changed": [
    ".changeset/24895-preference-user-reference.md",
    "content/docs/permissions/tenant-audit-census.mdx",
    "docs/audits/2026-08-tenant-audit-write-call-sites.counts.md",
    "packages/cli/src/commands/migrate/duplicates.contract.test.ts",
    "packages/cli/src/commands/migrate/duplicates.integration.test.ts",
    "packages/drivers/driver-sql/src/sql-driver-tenant-scoped-declared-unique.test.ts",
    "packages/metadata-protocol/src/migrations/notification-preference-identity-index.test.ts",
    "packages/metadata-protocol/src/migrations/notification-preference-identity-index.ts",
    "packages/metadata-protocol/src/migrations/runtime-index-preflight.test.ts",
    "packages/metadata-protocol/src/migrations/runtime-index-preflight.ts",
    "packages/metadata-protocol/src/plugin.notification-preference-index-armed.test.ts",
    "packages/metadata-protocol/src/plugin.ts",
    "packages/qa/dogfood/test/upgrade-18x-index-equivalence.dogfood.test.ts",
    "packages/services/service-messaging/src/messaging-service-plugin.test.ts",
    "packages/services/service-messaging/src/messaging-service-plugin.ts",
    "packages/services/service-messaging/src/objects/notification-display-title.test.ts",
    "packages/services/service-messaging/src/objects/notification-keyed-text-bounds.test.ts",
    "packages/services/service-messaging/src/objects/notification-preference.object.ts",
    "packages/services/service-messaging/src/preference-resolver.test.ts",
    "packages/services/service-messaging/src/preference-resolver.ts",
    "packages/services/service-messaging/src/preference-user-conversion.ts",
    "packages/services/service-messaging/src/preference-user-reference.integration.test.ts",
    "packages/services/service-messaging/src/translations/en.objects.generated.ts",
    "packages/services/service-messaging/src/translations/es-ES.objects.generated.ts",
    "packages/services/service-messaging/src/translations/es-ES.source-hashes.generated.ts",
    "packages/services/service-messaging/src/translations/ja-JP.objects.generated.ts",
    "packages/services/service-messaging/src/translations/ja-JP.source-hashes.generated.ts",
    "packages/services/service-messaging/src/translations/zh-CN.objects.generated.ts",
    "packages/services/service-messaging/src/translations/zh-CN.source-hashes.generated.ts"
    ],
    "line_budget": "n/a — no governed or skills surface; 29 files, +1565/−103 vs merge base e3aa111 (under the 3000-line human-merge threshold)",
    "deviations": [
    "Claim-surface conflict, named rather than chosen silently: packages/qa/dogfood/test/upgrade-18x-index-equivalence.dogfood.test.ts is outside the claim. os-dev.md's Definition of done orders a dogfood red caused by this change fixed in the same PR, and os-dev.md wins over dispatch text, so I edited it. The seat may strip it.",
    "Outside the claim, forced by check-tenant-audit-census for the conversion's new write site: content/docs/permissions/tenant-audit-census.mdx (generated region plus seven hand-written counts, 236→237 and three companions) and docs/audits/2026-08-tenant-audit-write-call-sites.counts.md (generator). The site was written census-readable, so no undecidable count moved.",
    "packages/spec/scripts/liveness/empty-state-registry.mts was NOT edited (claim ⛔ packages/spec/**), so check:empty-state stays red; this is the blocked item.",
    "packages/cli/src/commands/migrate/duplicates.ts itself is unchanged: the probe lives in metadata-protocol's runtime-index-preflight.ts (inside the claimed migrations/ directory), which duplicates.ts already consumes. Only the cli roster pins changed.",
    "The final-head dogfood battery is narrowed to the two files the fix touched (above).",
    "Container restart mid-run: the process was stopped during a query-options-erasure run. I resumed from the transcript; the one uncommitted hunk (module-top imports in messaging-service-plugin.test.ts) was finished work and was committed as f08bc62. The gate sweep was re-run in full afterwards.",
    "The first sweep ran beside the full dogfood battery on a 4-core box: type-check-debt hit my runner's 900s timeout (re-run green), and the showcase hook timeout is attributed to that contention (its re-run is green).",
    "Merged origin/main twice (no overlap with this diff); every reading above is after the merges.",
    "Worktree removed after the PR opened; remote branch at d272b23."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: none · docs/qa/platform-checklist/areas/integration-system.json:1668 still describes "'
    ' wildcards + admin-global default" for sys_notification_preference; the user half reads stale. Noted in the PR's Acceptance notes, not filed.",
    "carrier: none · docs/adr/0030-notification-platform-convergence.md P2 row ('admin-global * defaults') is history on a governed surface; left as is, noted, not filed.",
    "carrier: the seat (ruling item 5) · sys_notification_delivery.display_title has the same raw-id shape; it is the seat's own card, not this PR."
    ]
    }

  9. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 24895,
    "status": "done",
    "branch": "claude/issue-24895-preference-user-reference",
    "pr": "#25178",
    "session": "session_01UnHcTADjeg3VwxF21QHj7W — mode:subagent run of the dispatching seat (the parent's id, per the Claude-Session line)",
    "premise_still_valid": true,
    "summary": "Unblock round on PR #25178. Final head is 55663d7, a draft assigned to os-bill. The PR body is unchanged since it was opened.\n\n(1) The declared registry row (#6017 6334445279) is in packages/spec/scripts/liveness/empty-state-registry.mts, commit cd4c2cb.\n- file: the preference object; property: user_id; semantics: scope.\n- Rationale, in the registry's own register: an empty User selects which users a delivery toggle covers (the admin-wide default, ruling record 6304380498); it grants no data access; a user's own row overrides it; mandatory topics bypass it; the set_null widening is closed by cascade.\n- Evidence: preference-resolver.ts resolveRow.\n- Nothing else in packages/spec moved. No spec-seat reclassification was posted on #6017 or the PR before the push.\n\n(2) CI red on cd4c2cb, root-caused and fixed in 55663d7.\n- Failing test: packages/types/src/driver-error-classification.callers.test.ts › '[#21418] operatorFacingErrorText — every caller is enumerated and pinned' › 'the non-test callers are exactly the enumerated ones, call sites included' (the types test:repo project, Test Core 1/6).\n- Root cause: the boot conversion called operatorFacingErrorText twice, which made it an unlisted caller of a helper whose callers must each be rostered and sentinel-pinned against raw-SQL statements.\n- That helper was the wrong tool. It recovers a dialect's text from the raw-SQL seam's composed fault, but the conversion only goes through the data engine, whose errors carry no such fault and whose SQL driver already redacts statements and bound values.\n- Fix: a local messageOf(err) (the package's plain message reading), with the reason in a docblock; no roster or packages/types edit.\n- Proof pair: putting one operatorFacingErrorText(err) call back reds exactly that test (Received adds \"packages/services/service-messaging/src/preference-user-conversion.ts\": 1, 1 failed of 11). Restored to blob == HEAD.\n\n(3) CI is all green on 55663d7.\n\n(4) The full dogfood battery on 55663d7, run alone, has two timeout failures that I cannot attribute to this change. They are reported with their output, not re-run.",
    "tests": "All at 55663d7 unless marked.\n- pnpm --filter @objectstack/types run test:repo → Test Files 1 passed (1), Tests 11 passed (11).\n- pnpm --filter @objectstack/service-messaging exec vitest run --maxWorkers=2 src/preference-user-reference.integration.test.ts src/messaging-service-plugin.test.ts → Test Files 2 passed (2), Tests 37 passed (37).\n- pnpm --filter @objectstack/service-messaging run typecheck → exit 0.\n- pnpm --filter @objectstack/spec exec vitest run scripts/liveness/empty-state.test.ts (spec's empty-state tests) → Test Files 1 passed (1), Tests 32 passed (32), at cd4c2cb.\n\nRegistry evidence pair, at cd4c2cb:\n- pnpm --filter @objectstack/spec run check:empty-state with the row → exit 0, '✓ all classified (2 closed, 2 open, 4 output, 12 scope)'.\n- Without it (node scripts/ablation-replace.mjs deleting the 7-line row, wrap mode) → exit 1: 'UNCLASSIFIED … notification-preference.object.ts:110 — user_id', '1 finding(s)'.\n- The file was restored: 'blob == HEAD (07cc5b5b9b7d) and git diff HEAD is empty'.\n\nRoot-cause pair: ablation-replace of const detail = messageOf(err); → operatorFacingErrorText(err) → test:repo exit 1, '× the non-test callers are exactly the enumerated ones', 1 failed of 11; restored to blob == HEAD.\n\nEarlier package runs, unchanged since: metadata-protocol 254 files passed; cli duplicates 6/37; driver-sql pin 85.",
    "gates": "Local:\n- Re-derived at cd4c2cb: 106 commands; delta vs d272b23 +3, all exit 0:\n - node scripts/check-dev-prereqs.mjs --self-test\n - pnpm check:pm-governed-merges\n - pnpm check:pnpm-flag-spellings\n- check:empty-state → 0.\n- Re-derived at 55663d7: the same 106, no delta. Families reading the edited conversion file re-run → 0: check-tenant-audit-census, check:durability-log-level, check:doc-authoring.\n- --ran at 55663d7: '106 derived, 106 run, 0 NOT-MEASURED, 0 UNRUN'.\n\nCI, PR #25178 check-runs at 55663d7:\n- success: Lint & Repo Gates, TypeScript Type Check, Test Core and Test Core 1–6, Build Core, Governed Surface Queue Guard, Spec property liveness, Check Changeset, every Lint · and Type Check · lane, Dogfood Regression Gate, Build Docs, and the claim and closing guards;\n- skipped by event: Console Pin Gate, the dogfood shards, Dogfood Verify CLI, Temporal Conformance, and the opt-in harnesses.",
    "dogfood_battery": "55663d72e, alone under the verify lock, with no gate sweep of mine beside it (other agents' unlocked work on the shared 4-core box is not excluded by the lock; it held 2903s against 2101s on the earlier run). Command: pnpm --workspace-concurrency=2 --filter '@objectstack/dogfood^...' build && pnpm --filter @objectstack/dogfood test, no shard and no path.\nResult: VERDICT command-exit 1 · Test Files 2 failed | 285 passed | 3 skipped (290) · Tests 1 failed | 2391 passed | 21 skipped (2413).\n- (a) showcase-single-platform-insert-default-organization.dogfood.test.ts › beforeAll → 'Error: Hook timed out in 10000ms' at stack = await bootShowcase() (:42).\n - Not attributable: the file's beforeAll declares no budget, so a full showcase boot plus sign-in runs on vitest's default 10 s. Sibling showcase files give the same boot 60–120 s.\n - It timed out the same way in the f7ad7e7 battery and passed in the isolated two-file run at d272b23.\n - This change adds to a boot one presence probe plus a probe-first rebuild of one empty-table index (the same shape sys_setting's migration already runs every boot), and one indexed find.\n- (b) object-designer-field-reorder.dogfood.test.ts › 'creates and publishes the object in its declared order' → 'Error: Test timed out in 5000ms' (:98, saveDraft + publish).\n - Not attributable: a metadata object publish path this diff does not touch.\nThis cancels the earlier narrowed claim: d272b23's two-file re-run is superseded by this full run.",
    "mcp_calls": "0 — none",
    "api_writes": "1 this round, through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches → POST /repos//issues/24895/comments): this os-dev-report, via scripts/pm/post-stamped.mjs. Earlier in the claim: the blocked-round comment 6331377734, pr_create #25178 (POST /pulls plus POST /issues/25178/assignees), and report comment 6334379488. Branch pushes are git.",
    "files_changed": [
    ".changeset/24895-preference-user-reference.md",
    "content/docs/permissions/tenant-audit-census.mdx",
    "docs/audits/2026-08-tenant-audit-write-call-sites.counts.md",
    "packages/cli/src/commands/migrate/duplicates.contract.test.ts",
    "packages/cli/src/commands/migrate/duplicates.integration.test.ts",
    "packages/drivers/driver-sql/src/sql-driver-tenant-scoped-declared-unique.test.ts",
    "packages/metadata-protocol/src/migrations/notification-preference-identity-index.test.ts",
    "packages/metadata-protocol/src/migrations/notification-preference-identity-index.ts",
    "packages/metadata-protocol/src/migrations/runtime-index-preflight.test.ts",
    "packages/metadata-protocol/src/migrations/runtime-index-preflight.ts",
    "packages/metadata-protocol/src/plugin.notification-preference-index-armed.test.ts",
    "packages/metadata-protocol/src/plugin.ts",
    "packages/qa/dogfood/test/upgrade-18x-index-equivalence.dogfood.test.ts",
    "packages/services/service-messaging/src/messaging-service-plugin.test.ts",
    "packages/services/service-messaging/src/messaging-service-plugin.ts",
    "packages/services/service-messaging/src/objects/notification-display-title.test.ts",
    "packages/services/service-messaging/src/objects/notification-keyed-text-bounds.test.ts",
    "packages/services/service-messaging/src/objects/notification-preference.object.ts",
    "packages/services/service-messaging/src/preference-resolver.test.ts",
    "packages/services/service-messaging/src/preference-resolver.ts",
    "packages/services/service-messaging/src/preference-user-conversion.ts",
    "packages/services/service-messaging/src/preference-user-reference.integration.test.ts",
    "packages/services/service-messaging/src/translations/en.objects.generated.ts",
    "packages/services/service-messaging/src/translations/es-ES.objects.generated.ts",
    "packages/services/service-messaging/src/translations/es-ES.source-hashes.generated.ts",
    "packages/services/service-messaging/src/translations/ja-JP.objects.generated.ts",
    "packages/services/service-messaging/src/translations/ja-JP.source-hashes.generated.ts",
    "packages/services/service-messaging/src/translations/zh-CN.objects.generated.ts",
    "packages/services/service-messaging/src/translations/zh-CN.source-hashes.generated.ts",
    "packages/spec/scripts/liveness/empty-state-registry.mts"
    ],
    "line_budget": "n/a — no governed or skills surface; 30 files, +1589/−103 vs merge base e3aa111 (under 3000)",
    "deviations": [
    "PR body is NOT patched; per the seat's order it is stale in four places, listed under open_questions[0] as the sentences the seat should add.",
    "I stopped my own first final-head battery run at cd4c2cb, killing only PID 28037 and its descendants, when the seat reported CI red on that head. The fix changed the head, so the battery was run again on 55663d7.",
    "origin/main was not merged this round: the PR reads mergeable true and CI is green on 55663d7.",
    "The worktree was removed after the push; the remote branch is at 55663d7."
    ],
    "open_questions": [
    {
    "question": "PR #25178 body sentences for the seat to add (I may not PATCH it).",
    "options": [
    "Under 'Outside the claim's letter': 'Declared since: the empty-state registry row (#6017 6334445279), the dogfood fixture (#22648 6334455088), the tenant-audit census docs (#6023 6334465051), runtime-index-preflight.ts and the driver-sql fixture (#6367 6334470965).'",
    "Replace 'One gate is still red': 'packages/spec/scripts/liveness/empty-state-registry.mts carries one row (file: the preference object, property user_id, semantics scope; rationale: an empty User selects which users a delivery toggle covers and grants no access, a user row overrides it, mandatory topics bypass it, cascade closes the set_null widening; evidence: preference-resolver.ts resolveRow). check:empty-state → 0 with it, 1 without it.'",
    "Add: 'The boot conversion reports the engine error message (messageOf), not operatorFacingErrorText. The types test:repo roster enumerates that helper's raw-SQL callers, and this path issues no raw SQL (55663d7).'",
    "Replace the dogfood line: 'Full battery at 55663d7, run alone under the lock: VERDICT command-exit 1 · Test Files 2 failed | 285 passed | 3 skipped (290) · Tests 1 failed | 2391 passed | 21 skipped (2413). Both failures are timeouts not attributable to this change: the showcase-single-platform-insert-default-organization beforeAll hook (default 10 s budget for a full showcase boot) and the object-designer-field-reorder publish test (5 s).'"
    ],
    "recommendation": "Add all four as written, so the body matches head 55663d7."
    }
    ],
    "out_of_scope_findings": [
    "carrier: none · docs/qa/platform-checklist/areas/integration-system.json:1668 still describes "'*' wildcards + admin-global default" for sys_notification_preference; the user half reads stale. Noted in the PR's Acceptance notes, not filed.",
    "carrier: none · docs/adr/0030-notification-platform-convergence.md P2 row ('admin-global * defaults') is history on a governed surface; left as is, noted, not filed.",
    "carrier: the seat (ruling item 5) · sys_notification_delivery.display_title has the same raw-id shape; it is the seat's own card, not this PR.",
    "carrier: none · packages/qa/dogfood/test/showcase-single-platform-insert-default-organization.dogfood.test.ts:42: its beforeAll boots the full showcase and signs in on vitest's default 10 s hook budget, while sibling showcase files budget 60–120 s for the same boot. It timed out in both full batteries (f7ad7e7, 55663d7) on a loaded box and passed in isolation. Test-budget only, not a product defect; noted, not filed."
    ]
    }

  10. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #25178 comment 6336745394 · domain:services seat 2 · session_01UnHcTADjeg3VwxF21QHj7W · 2026-10-22T08:52Z · contract review PASS at CONTRACT_REVIEW_TIER 6336714107 · ready and auto-merge set through the relay; the landing note follows the merge.


    Generated by Claude Code

  11. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 2 · session_01UnHcTADjeg3VwxF21QHj7W · 2026-10-22T09:22Z

    Reading Value
    PR #25178 merged 2026-10-22T09:22Z through the merge queue, merge commit e282f32134
    This card closed completed by Fixes

    State: pm:dispatched is removed in this act.

    What changed:

    • sys_notification_preference.user_id is a sys_user reference, and an empty User is the admin-wide default.
    • Stored '*' rows are rewritten at boot.
    • A NULL-safe index keeps one default per organization, topic and channel.

    Follow-ups filed at landing:

    The stale QA-checklist annotation is in the seat's sign-off brief.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions