You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Setup: replace developer wording and hand-written JSON with fields a business administrator can use #30208
From a real-user walkthrough of Setup, filed on the maintainer's word: 「以上都同意,立卡」 (2026-11-05). Measured on mainb7075d9908, with the console in dev mode, unless a line says otherwise. Test app scaffolded with create-objectstack@18.3.0.
Items
#
Where
Observed
Suggestion
1
New permission set assignment
fail-closed, 半开区间 [from, until), 智能体授权 in help text
Business wording
2
New business unit
图谱语义, Null 表示租户根节点, HRIS, 图谱查询
Business wording
3
Edit organization
「为 true 时…」
「开启后…」
4
Permission set assignment
已绑定到 everyone 锚点
「所有登录成员都有」
5
Settings → 数据生命周期
Retention, row quota and class quota are raw JSON boxes
Rows with an object picker and a duration
6
通知偏好
Topic and channel are free text, default *, and a wrong value silently matches nothing; 免打扰 is JSON
Pick from known topics and channels; a time range control
7
队列, 队列成员
Whole pages are English in a Chinese console: titles, columns, help (新建Queue)
Add the translations
8
邮件投递
Status banner No mail leaves this machine, and the test toast Sent test email to…, are English
Localize
9
Setup list headers
Badges 身份, 管理配置, 平台架构 say nothing to an administrator
Remove, or explain
Take the rows independently. Translation rows are the cheapest and do not wait on the others.
One more row of the same kind, on main04f36b63f8: Setup's 通知模板 new form takes topic and channel as free text, so a typed value that matches nothing fails with no message. Pick from known topics and channels, as this card asks for 通知偏好.
Claim: PM loop round 3 · 2026-11-05T12:19Z
Session: session_01L6bhgUR8Nu4M7tbtgES7q2
Account: os-sales (the seat's linked user as GET /user answers it; the card's assignee)
Branch: the card thread's designated claude/project-thread-* branch, named in a claim amendment once the thread reports it
Worktree: objectstack-issue-30208
Domain: domain:ui
Seat: domain:ui#6
File surface: the rows whose fix lands in ui/** (row 4 ui/packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsx, rows 7 and 9 where the text is drawn by ui/, rows 1–3 and 8 only where the string lives under ui/), their tests, ui/packages/i18n locale entries they need, one root .changeset/ file. ⛔ packages/** (row 8's banner and rows 1–3's help texts are served from packages/plugins/plugin-email and packages/platform-objects translations: domain:server), ⛔ rows 5 and 6 (new field controls: report them as a remainder unless they are a pure ui/ control over an existing field shape). Rows not done are named in the report for the seat to split (stop on breach; explain in the report)
Container & model: M, mode:thread, model: default (dispatch-gates --tier: no path-derived mandate; floor sonnet · default opus · ceiling fable), effort: high
Clause-②: no (a new language-pack key, if a row needs one, is declared by the dev in the PR body and the seat re-reads this line at ACCEPT)
Metadata-protocol: none
Responsibility: n/a — not a defect card
Thread-read: 6651344343
Premise: holds in part, read at origin/main677ac3bb: row 4's 「已绑定到 everyone 锚点」 is ui/packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsx:161; row 3's 「为 true 时」 and row 8's banner are server-side strings (packages/platform-objects, packages/plugins/plugin-email); the dev re-measures each row first
Serial constraints cleared: PR #30243 edits ui/packages/i18n/src/locales/zh.ts (other keys; normal concurrency, the later lander merges main). No maintainer ruling beyond the filing word 「以上都同意,立卡」.
席位核对 · Amendment · 2026-11-05T12:23Z · claim 6657817034 branch named
Branch: claude/project-thread-9po7qw
The card thread's harness-designated branch (a project-thread platform exception to the claude/issue-<n>-<slug> convention); the dev thread is session session_01L6bhgWjg9S7avVTjuNn9P2. Everything else in claim 6657817034 stands.
{
"issue": 30208,
"status": "rework",
"branch": "claude/project-thread-9po7qw",
"pr": "https://github.com/objectstack-ai/objectstack/pull/30488",
"session": "session_01L6bhgWjg9S7avVTjuNn9P2",
"premise_still_valid": true,
"summary": "Partial by design (Refs, not Fixes): ships rows 4 and 9, the two rows whose text ui/ draws. Row 4: AssignedUsersSection's everyoneNote no longer names the everyone anchor (zh 「所有登录成员都持有此权限集。」, en 'Every signed-in member holds this set.'). Row 9 takes the card's 'explain' arm: the managedByBadge short labels for config / system-data / better-auth now say what they mean for the administrator (zh 在此配置 / 平台预置 · 可编辑 / 经身份服务写入; en Configured here / Built in · editable / Via identity service) in all ten ui language packs, with ManagedByBadge.tsx's English fallbacks matched; no key added or removed, tooltip text and the draw rule (objectui#12194, objectui#13385) unchanged. Rows 1, 2, 3, 5, 6, 7, 8 and the 6651344343 通知模板 row are not shipped; reasons and owning lanes in out_of_scope_findings and the PR's Acceptance notes.",
"tests": "pnpm --dir ui exec vitest run on AssignedUsersSection.test.tsx + ManagedByBadge.keyboard-27521.test.tsx: 2 files, 22/22 passed, lock VERDICT command-exit 0. Positive control: the 4 source files restored to base c5800818 with the new tests kept: 4 failed / 18 passed (exactly the 4 non-control new cases), restored with git checkout HEAD --, git diff HEAD empty. Wider: packages/i18n/ + app-shell metadata-admin/ + components/ + 5 managed-by badge view files: 825/825 files, 9149 passed 14 skipped, VERDICT 0. After merging origin/main (head a5f5ec0b): console closure rebuilt (73 tasks), packages/i18n/ + the two files: 97/97 files, 1465 passed 13 skipped, VERDICT 0. No ablation beyond the positive control: copy-only change.",
"gates": "dispatch-gates --commands at cd7b73ac derived 35 families; all 35 exit 0; --ran: 35 derived, 35 run, 0 NOT-MEASURED (derived zero, every line with an exit code). Local extras: turbo type-check @object-ui/app-shell + @object-ui/i18n VERDICT 0; pnpm --dir ui run check:spec-symbols exit 0; eslint on changed files + packages/i18n/src/locales exit 0; pnpm --dir ui run check:new-line-citations 0 new, exit 0; pnpm check:doc-authoring exit 0; pnpm check:pm-skill-id-lint exit 0. CI check-runs on PR #30488: in_progress at report time (not waited on).",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "5 — issue_read x2 (card, comments), pull_request_read x2 (PR #30442 for body shape, PR #30488 read-back); no MCP write tool",
"api_writes": "2 — fleet-write relay pr_create (run 42034634236, #30488, body read back identical 11159 bytes, no assignees) and this os-dev-report comment via post-stamped. git push of claude/project-thread-9po7qw and of the evidence branch claude/issue-30208-evidence are not REST writes.",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"carrier: seat (split) · row 1 · permission set assignment help texts (fail-closed, 半开区间 [from, until), 智能体授权) are served from packages/plugins/plugin-security/src/translations/zh-CN.objects.generated.ts (半开区间 also plugin-approvals) · domain:server",
"carrier: seat (split) · row 2 · business unit help (图谱语义, Null 表示租户根节点, HRIS, 图谱查询) in packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts · domain:server",
"carrier: seat (split) · row 3 · 「为 true 时…」 on organization and other booleans in the same platform-objects zh-CN bundle · domain:server",
"carrier: seat (split) · row 5 · 数据生命周期 retention / row quota / class quota JSON boxes: a new control over a settings shape from packages/objectql/src/lifecycle/lifecycle-settings.ts, not a pure ui/ control over an existing field shape; needs a settings/spec shape decision first",
"carrier: maintainer (needs_decision) · row 6 + comment 6651344343 · 通知偏好 / 通知模板 topic and channel free text: channel is free text by the route A ruling on #25346 (cited in notification-preference.object.ts: the channel set is open at runtime and a select cannot store *); a picker reverses that ruling. 免打扰 quiet_hours (Field.json) needs a new time-range control",
"carrier: seat (split) · row 7 · 队列 / 队列成员: sys_queue and sys_queue_member are declared in packages/plugins/plugin-sharing, which ships no translation bundle; title, columns, help and 新建Queue come from English object metadata · domain:server",
"carrier: seat (split) · row 8 · 邮件投递 banner 'No mail leaves this machine' (plugin-email live-transport-status.ts) and toast 'Sent test email to…' (plugin-email email-plugin.ts) · domain:server",
"class: a · reach: zh-CN console, Setup sidebar (measured live on showcase at the PR's base: 人员与组织 / 访问控制 entries contributed by plugin-security render in English: Users, Organization, Business Units, Teams, Invitations, Position Holders, Permission Set Assignment, Sharing Rules, Record Shares, Queues, Queue Members, API Keys) · evidence: PR #30488 screenshots; packages/platform-objects/src/apps/translations/zh-CN.ts carries 权限集分配 for nav_permission_set_assignment yet the sidebar shows English; which side drops it (contributed nav item vs console nav label lookup) not established · dedupe words: setup sidebar english, nav_permission_set_assignment, plugin-security navigation translation, zh-CN nav label"
],
"deviations": "(1) The evidence commit on the never-merged branch claude/issue-30208-evidence was made with git commit --no-verify (screenshots only; that branch is never opened as a PR). (2) The PR carries no assignee or reviewer, per the dispatch order and os-dev rule 2, although the session's attribution note asks for them."
}
Claim: PM loop round 1 · #30208 row 5 (Settings → Data Lifecycle: retention / row quota / class quota are raw JSON boxes) · 2026-11-07T19:10Z · shape first, then build
Session: session_017ihL2fBJ6xsLWJsnNJGMMx
Account: marchtian
Branch: claude/project-thread-4ntp5s (the card thread's harness-pinned branch, cut from main4c318214; platform exception to claude/issue-<n>-<slug>)
Worktree: objectstack-issue-30208
Domain: domain:server
Seat: domain:server#2
File surface (phase 0, read-only): none written. The phase 0 output is a shape analysis posted on this card. (phase 1, after the maintainer's ruling): packages/objectql/src/lifecycle/lifecycle-settings.ts and its readers (lifecycle-service.ts, the services that read retention_overrides / quotas / quota_defaults), service-settings translations, and packages/spec/src/system/settings-manifest.zod.ts only if the ruling grants a specifier widening. ⛔ ui/** (the console control is its own card once the shape exists).
Container & model: M, mode:thread, model: TIER_DEFAULT, effort: default
Clause-②: no — phase 0 writes no code; phase 1 re-derives it from the ruled shape (a new specifier type would be yes (widening)) and the seat posts a fresh Claim: before the build
Metadata-protocol: none
Responsibility: what a business administrator edits on the Data Lifecycle settings page | the lifecycle settings namespace | Setup → Settings → Data Lifecycle
Thread-read: 6700719349
Premise: holds. On origin/main714dda8faa, lifecycle-settings.ts declares retention_overrides, quotas and quota_defaults as type: 'json'. SpecifierType in settings-manifest.zod.ts is a closed enum with no row or map specifier, so no rows-with-an-object-picker shape exists today (#30208 release 6659910397: "shape first").
Serial constraints cleared: rows 1–4 and 6–9 have landed or been filed elsewhere. Seat 3 released this row. The quiet-hours control goes to the ui lane as its own card.
Phase 0 is a design question, not a build. The shape needs either a new settings specifier (a packages/spec widening plus a ui renderer) or a move of the overrides to records. Either way it goes to the maintainer through a decision thread before any code is written. Order: project file server-seat-2/orders/30208-row5.md.
OS_LIFECYCLE_RETENTION_OVERRIDES / _QUOTAS / _QUOTA_DEFAULTS (envKeyOf), JSON.parsed because the default is an object (settings-service.ts:4294); OS_LIFECYCLE_DISABLED is a separate process flag
JSON text
docs, skills, translations
rules/lifecycle.md:138, objects.mdx:210, backup-restore.mdx:177, production-readiness.mdx:191, ADR-0057 §3.2; settings.lifecycle.keys.* in 10 languages
name the keys
Nothing validates the value. validatePatch has no type or shape check for json, so a typo in an object name, a quota of "1e6" or a duration of 30 days is stored and then silently does nothing (only the bad duration logs a warning).
2. How the console draws it today
ui/apps/console/src/pages/settings/SettingsField.tsx:1054 draws json as a monospace <Textarea> with onChange(e.target.value); SettingsView.tsx:740 drafts that string and api.ts:155 PUTs it as is (no JSON.parse in pages/settings/). Traced by reading, not measured: editing the box saves a JSON string, loadGovernance then indexes a string, and every override and quota silently stops applying. ADR-0007 says json "persists object". These are the only json settings specifiers in packages/; phase 1 should confirm with a live PUT first. Under A or B the box goes away.
A row editor needs: (a) a key column, an object picker over objects that declare lifecycle (others ignore an override) or a LifecycleClass select; (b) value columns, a duration (number + h/d/w/y, LIFECYCLE_DURATION_REGEX) or a positive integer; (c) per-column labels in the translation tree. The runtime option source (registerOptionSource, as localization.locale uses) can serve the object list.
3. Candidate shapes
A: new specifier type (keyed rows / map with columns)
B: json plus an optional structure hint
C: records (a system object with object lookup + duration fields)
Salesforce Field History Retention: HistoryRetentionPolicy on each object, archiveAfterMonths / archiveRetentionYears (metadata reference).
Google Vault: a list of custom rules, each with service, scope and a duration in days (How retention works).
ServiceNow and Dataverse store rules as records (C). What transfers is the row look; our per-organization cascade already does what their record scoping does.
4. Recommendation
A. The three keys really are maps (object or class → window or count). A keyed-rows specifier draws them as the rows the platforms above show, keeps every stored value, env var, lock and audit row as it is, and gives service-settings a declaration to refuse a wrong object, duration or count at both the save door and the env door. Today none of these is refused. B is A without the new enum value, at the cost of one type with two renderings. C fits ServiceNow/Dataverse but trades the cascade and a data migration for a page with three knobs. A needs an ADR-0007 amendment and a spec widening, so it takes the Fable spec review. The control itself is the domain:ui follow-up card.
田騏, 2026-11-08 02:07:52Z, in the project's Fable decision thread: 「同意」 (project message cmsg_01VmuUhXU9SSUzqJYyXSmYSiAbe9LctoxZJq1geQKD7KXc). That accepts option A of the decision card: "spec 加一个带列定义的映射类型,三个键改用它;值和环境变量不变,保存口和环境变量口开始校验;修订 ADR-0007,另开 ui 控件卡。" The analysis is the phase 0 comment 6711865649 plus the Fable thread's write-up (platform survey: VS Code object grid, ServiceNow Table Cleanup, Dataverse retention, Salesforce HistoryRetentionPolicy, Google Vault, S3 Lifecycle).
What A means here.SpecifierType (packages/spec/src/system/settings-manifest.zod.ts) gains one typed-map member with column definitions. The working name is map; the spec PR and its Fable review settle the final name. retention_overrides, quotas and quota_defaults move from json to it. ADR-0007 is amended under its own rule "New types require an ADR amendment". The console control is a separate domain:ui card.
Hard constraints carried by the ruling:
Stored shape unchanged, zero migration. A single-column map stores scalars (quotas, quota_defaults); a multi-column map stores objects (retention_overrides). One type covers both, and the rule is written in the spec docs.
The key column's domain comes from a runtime option source: objects that declare lifecycle, or LifecycleClassSchema. It is never hard-coded in the manifest.
json stays in the enum for genuinely raw config. The ADR-0007 amendment adds the new type and notes that json is no longer the home for maps.
The 12 translation packs drop the JSON examples from the three keys' help text.
The ui card follows immediately. Until it lands, the console shows the three keys as an unsupported type; 田騏 has been told. Env overrides are unaffected.
Phase 1 is dispatched to this card's thread after #31279 (Fixes #31241, the json-stored-as-text defect) lands, because both touch settings-service.ts and lifecycle-service.ts.
Claim: PM loop round 1 · #30208 row 5 phase 1 (build ruling A: a typed map specifier for the three lifecycle map keys) · 2026-11-08T02:09Z · build
Session: session_017ihL2fBJ6xsLWJsnNJGMMx
Account: marchtian
Branch: claude/project-thread-4ntp5s (the card thread's harness-pinned branch; the phase 1 PR is cut from it after merging main once #31279 has landed; platform exception to claude/issue-<n>-<slug>)
Worktree: objectstack-issue-30208
Domain: domain:server
Seat: domain:server#2
File surface: packages/spec/src/system/settings-manifest.zod.ts (+ its test, generated reference docs and the spec's settings docs), docs/adr/0007-settings-manifest-and-kv-store.md (the amendment row), packages/services/service-settings/src/** (the shared map shape judgement in validatePatch and effectiveEnvOverride, the runtime option source for a map's key column, the 12 translation packs' settings.lifecycle.keys.* help text), packages/objectql/src/lifecycle/lifecycle-settings.ts and lifecycle-service.ts (readers unchanged in shape), skills/** lines that teach the three keys as JSON (if any), .changeset/. ⛔ ui/** (the console control is its own domain:ui card). ⛔ No change to any stored value, env variable name or cascade.
Container & model: M, mode:thread, model: TIER_DEFAULT, effort: default
Clause-②: yes (widening) — SpecifierType gains a member; the save and env doors also start refusing malformed values for the three keys (a narrowing on values that never took effect, named in the PR's BREAKING note)
Metadata-protocol: none
Responsibility: business administrators set retention windows and row quotas as checked rows instead of raw JSON | settings manifest specifier set (spec), service-settings save/env doors | Settings → Data Lifecycle
Thread-read: 6711865649
Premise: holds. On main, the three keys are the only type: 'json' specifiers in packages/. Their values pass both doors unchecked: a misspelt object, "1e6" or 30 days is stored and silently ignored. Ruling record above.
Serial constraints cleared: waits for #31279 (settings-service.ts, lifecycle-service.ts) to merge. Tier H (docs/adr/**): human merge. Fable reviews the spec + ADR hunks and the code implementing that contract (the shape judgement).
Claim: PM loop round 1 · #30208 row 5 phase 1 (build ruling A: a typed map specifier for the three lifecycle map keys) · 2026-11-08T02:28Z · build (amended: Metadata-protocol added)
Session: session_017ihL2fBJ6xsLWJsnNJGMMx
Account: marchtian
Branch: claude/project-thread-4ntp5s (the card thread's harness-pinned branch; the phase 1 PR is cut from it after merging main once #31279 has landed; platform exception to claude/issue-<n>-<slug>)
Worktree: objectstack-issue-30208
Domain: domain:server
Seat: domain:server#2
File surface: packages/spec/src/system/settings-manifest.zod.ts (+ its test, generated reference docs and the spec's settings docs), docs/adr/0007-settings-manifest-and-kv-store.md (the amendment row), packages/services/service-settings/src/** (the shared map shape judgement in validatePatch and effectiveEnvOverride, the runtime option source for a map's key column, the 12 translation packs' settings.lifecycle.keys.* help text), packages/objectql/src/lifecycle/lifecycle-settings.ts and lifecycle-service.ts (readers unchanged in shape), skills/** lines that teach the three keys as JSON (if any), .changeset/. ⛔ ui/** (the console control is its own domain:ui card). ⛔ No change to any stored value, env variable name or cascade.
Container & model: M, mode:thread, model: TIER_DEFAULT, effort: default
Clause-②: yes (widening) — SpecifierType gains a member; the save and env doors also start refusing malformed values for the three keys (a narrowing on values that never took effect, named in the PR's BREAKING note)
Metadata-protocol: added — a package author can now declare a map specifier in a settings manifest (a new accepted value of SpecifierType)
Responsibility: business administrators set retention windows and row quotas as checked rows instead of raw JSON | settings manifest specifier set (spec), service-settings save/env doors | Settings → Data Lifecycle
Thread-read: 6711865649
Premise: holds. On main, the three keys are the only type: 'json' specifiers in packages/. Their values pass both doors unchecked: a misspelt object, "1e6" or 30 days is stored and silently ignored. Ruling record above.
Serial constraints cleared: waits for #31279 (settings-service.ts, lifecycle-service.ts) to merge. Tier H (docs/adr/**): human merge. Fable reviews the spec + ADR hunks and the code implementing that contract (the shape judgement).
{
"issue": 30208,
"status": "done",
"branch": "claude/project-thread-4ntp5s",
"pr": "https://github.com/objectstack-ai/objectstack/pull/31386",
"head": "32b54d4d94b4d90ab561fe62098cba565e4ad421",
"session": "session_017ihL2dxcqiQvzKhr5odTPe",
"premise_still_valid": true,
"row": "5, phase 1 (ruling A, record 6715997428)",
"proposed_type_name": "map",
"summary": "spec: SpecifierType gains 'map' (value-bearing, in SPECIFIERS_REQUIRING_KEY); Specifier gains keyColumn {label, description?} and columns [{name, label, description?, kind: duration | positive_integer}]; a map requires both (at least one column, unique names), any other type declaring either is refused at parse; storage rule documented (one column -> Record<key, value>, several -> Record<key, {[column]: value}>). ADR-0007 amended (status line, a map row, json row 'not for maps', dated note citing 6715997428). service-settings: map-shape.ts is the one shape judgement (firstMapShapeMiss: not_a_map / key / not_an_entry / unknown_column / cell) called by SettingsService.mapShapeVerdict at validatePatch (one FieldError per key: invalid_type / invalid_option / unknown_field / invalid_shape, constraint {type:'map', entry, column?, kind?, allowed?}, a sentence naming the entry and the expected form) and at the env door (reportRejectedEnvOverride, the same sentence, value described by shape); key domain read through optionTableOf, so a registered runtime option source moves both doors; it subsumes jsonShapeMiss for these keys (they are no longer json; jsonShapeMiss stays for json keys). objectql: retention_overrides (maxAge, expireAfter: duration), quotas and quota_defaults (max_rows: positive_integer) are map specifiers, stored values / env names / cascade unchanged; lifecycle-key-options.ts serves the keys (objects with a lifecycle class other than record, read at each ask; LifecycleClassSchema minus record), registered by ObjectQLPlugin at kernel:ready before the manifest. Translations: JSON examples dropped from the help text in the ten packs. Docs: config-resolution.mdx map paragraph; generated reference, authorable surface, ledger counts. Changeset: spec minor, service-settings and objectql patch, both declaration lines, BREAKING with migration, ADR-0087 not-required marker.",
"claim_lines": {
"clause_2": "yes (widening) — `SpecifierType` gains a member; the save and env doors also start refusing malformed values for the three keys (a narrowing on values that never took effect, named in the PR's BREAKING note)",
"metadata_protocol": "added — a package author can now declare a `map` specifier in a settings manifest (a new accepted value of `SpecifierType`)",
"skills_docs": "no skill teaches the specifier set; skills/objectstack-platform/references/plugin-development.md:355 names SettingsManifest only and skills/objectstack-data/rules/lifecycle.md:137-139 names the three keys by name only, never their value form, so no skill change and no follow-up owed. Docs updated in the PR."
},
"tests": "All on the final head 32b54d4d under os-verify-lock. New: service-settings src/settings-map-shape-30208.test.ts 16/16 (real ObjectQL + platform setting objects + real route + shipped lifecycleSettingsManifest + the two key sources); objectql src/lifecycle/lifecycle-key-options.test.ts 6/6 (incl. a real ObjectKernel wiring case); spec settings-manifest.test.ts +6. settings-json-shape-31241.test.ts 8/8 moved onto a raw-config json manifest. Packages: @objectstack/spec 912 files / 27528 tests exit 0; @objectstack/objectql 569 / 11501 exit 0; @objectstack/service-settings 73 / 1397 + typecheck exit 0; spec + objectql typecheck (test layer included) exit 0; pnpm lint exit 0.",
"ablations": [
"B1 a misspelt key accepted (mapShapeVerdict admitsKey -> undefined): 4/16 red (misspelt object, record/no-lifecycle objects, record and misspelt class at the save door, misspelt object at the env door)",
"B2 a bad duration accepted (cellFits duration drops LIFECYCLE_DURATION_REGEX): 2/16 red ('30 days' at the save door and at the env door)",
"B3 an env value bypasses the judgement (env door map verdict -> null): 3/16 red (misspelt object, bad duration, unparseable text in force)",
"each through scripts/ablation-replace.mjs on head 32b54d4d: mutation landed, restored blob == HEAD and git diff HEAD empty after each"
],
"gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on 32b54d4d: 121 commands, each with stdin from /dev/null after a full packages/** build, all ':: exit 0'; --ran: '121 derived, 121 run, 0 NOT-MEASURED, 0 UNRUN'. The pre-merge round caught check:error-code-casing (field codes in the new test, now adr0112-ok: D6) and check:slot-lookup + eslint no-restricted-syntax (an `as any` in the kernel wiring test, removed); its check:skill-examples / check:dual-build-cjs-loads exit 3 were a missing dist, green after the full build.",
"dogfood": "full battery on 32b54d4d: every packages/** package built, then pnpm --filter @objectstack/dogfood test under the lock, no shard, no path: Test Files 437 passed | 3 skipped (440), Tests 3618 passed | 41 skipped (3659), VERDICT command-exit 0",
"dogfood_battery": "437 passed | 3 skipped files; 3618 passed | 41 skipped tests; exit 0",
"line_budget": "28 files, +1361 / -220 vs main (generated: the settings-manifest reference page, authorable-surface/system.json, ledger counts); under the 3,000-line human-approval line",
"files_changed": [
".changeset/30208-settings-map-specifier.md",
"content/docs/protocol/kernel/config-resolution.mdx",
"content/docs/references/system/settings-manifest.mdx",
"docs/adr/0007-settings-manifest-and-kv-store.md",
"docs/audits/2026-07-unknown-key-strictness-ledger.counts/system.md",
"packages/objectql/src/index.ts",
"packages/objectql/src/lifecycle/lifecycle-key-options.test.ts",
"packages/objectql/src/lifecycle/lifecycle-key-options.ts",
"packages/objectql/src/lifecycle/lifecycle-settings.ts",
"packages/objectql/src/plugin.ts",
"packages/services/service-settings/src/__tests__/settings-door.testkit.ts",
"packages/services/service-settings/src/map-shape.ts",
"packages/services/service-settings/src/settings-json-shape-31241.test.ts",
"packages/services/service-settings/src/settings-map-shape-30208.test.ts",
"packages/services/service-settings/src/settings-service.ts",
"packages/services/service-settings/src/translations/ar.ts",
"packages/services/service-settings/src/translations/de.ts",
"packages/services/service-settings/src/translations/es-ES.ts",
"packages/services/service-settings/src/translations/fr.ts",
"packages/services/service-settings/src/translations/ja-JP.ts",
"packages/services/service-settings/src/translations/ko.ts",
"packages/services/service-settings/src/translations/pt-BR.ts",
"packages/services/service-settings/src/translations/ru.ts",
"packages/services/service-settings/src/translations/zh-CN.ts",
"packages/services/service-settings/src/translations/zh-Hant.ts",
"packages/spec/authorable-surface/system.json",
"packages/spec/src/system/settings-manifest.test.ts",
"packages/spec/src/system/settings-manifest.zod.ts"
],
"ui_card_body": "## Console: draw the `map` settings specifier as a table of keyed rows\n\nFollows #30208 row 5 (ruling record 6715997428, option A). The server half adds a `map` member to `SpecifierType` and moves `lifecycle.retention_overrides`, `lifecycle.quotas` and `lifecycle.quota_defaults` to it. Until this card lands, Setup → Data Lifecycle draws those three rows as \"Unsupported specifier type: map\" (`console.settingsView.unsupportedSpecifierType`). Env overrides and the settings API are unaffected.\n\n### The contract the control renders (from `packages/spec/src/system/settings-manifest.zod.ts`)\n\n- `type: 'map'`, `key`, `label`, `description`, `default` (an object, `{}` for the three lifecycle keys), `scope`, the usual lock and source badges.\n- `keyColumn: { label, description? }`: the header over the key column.\n- `columns: [{ name, label, description?, kind }]`, at least one, in display order. `kind` is `'duration'` or `'positive_integer'`.\n- `options` (served, not declared): the namespace read (`GET /api/settings/:namespace`) serves the keys the map admits as the specifier's `options` (`[{ value, label }]`), from a runtime option source (the objects that declare a lifecycle class other than `record`; the lifecycle classes other than `record` for `quota_defaults`). The manifest declares no list.\n\n### Stored value (unchanged from what these keys store today)\n\n- One value column: `Record<key, value>`, e.g. `{ \"sys_audit_log\": 1000000 }`. The column's `name` is not stored.\n- Several value columns: `Record<key, { [column.name]: value }>`, e.g. `{ \"sys_job_run\": { \"maxAge\": \"1y\", \"expireAfter\": \"30d\" } }`. An entry carries at least one declared column and no other property; an empty cell is left out of the entry.\n\n### What to build\n\n1. **Rows.** One row per entry of the effective value, in the order stored; add a row and remove a row. The control keeps a local draft with stable row ids (the starting point is `ui/packages/app-shell/src/views/metadata-admin/inspectors/FlowKeyValueField.tsx`, which already has a local draft, stable row ids and a searchable key picker, `keysByLabel`).\n2. **Key picker.** A searchable picker over the served `options` (`value`, shown by `label`). A key already used by another row is not offered again. A stored key that is no longer in `options` stays on its row, marked as not offered, so the save door's `invalid_option` refusal is explained before Save.\n3. **Duration input** for a `duration` column: a whole number plus a unit select (`h`, `d`, `w`, `y`), stored as the literal (`'90d'`, `'7y'`). The server judges it against `LIFECYCLE_DURATION_REGEX` (`/^\\d+(h|d|w|y)$/`).\n4. **Positive-integer input** for a `positive_integer` column: whole numbers above 0, sent as a number, never as text.\n5. **Save** sends the object (never JSON text), through the page's existing patch. A cleared map sends `null`.\n6. **Errors.** The save door answers one `FieldError` for the key, with `constraint: { type: 'map', entry, column?, kind?, allowed? }`. Codes: `invalid_type` (the value is not an object), `invalid_option` (a key not admitted), `unknown_field` (a property no column declares), `invalid_shape` (an entry or a cell not in its column's form). Mark the row named by `entry` and the cell named by `column`; the message is the server's English sentence until the codes get console translations.\n7. **Column labels.** `keyColumn.label` and `columns[].label` are English literals in the manifest. Translating them needs a `columns` group in the settings `keys.<key>` translation entry (`packages/spec/src/system/translation.zod.ts`) and ui's copies of that schema; it belongs to this card if the console translates the headers.\n8. **Unsupported fallback** stays for any other unknown type.\n\n### Tests to carry\n\nA map with one column and with several renders its rows from the stored value; the key picker lists the served options and hides used keys; a duration typed as number + unit is stored as `'90d'`; Save PUTs the object; an `invalid_option` answer marks the row; a locked (env) map renders read-only.\n\nOut of scope: any server change. The server half is the #30208 row 5 phase 1 PR.\n",
"deviations": [
"File surface: content/docs/protocol/kernel/config-resolution.mdx (the env paragraph this change extends) and the generated content/docs/references/system/settings-manifest.mdx, packages/spec/authorable-surface/system.json and docs/audits/2026-07-unknown-key-strictness-ledger.counts/system.md are not in the claim; skills/** lines in the claim needed no change.",
"Branch: the merge queue deleted claude/project-thread-4ntp5s when #31279 landed; recreated from main at b85190a8 with this work on top, then origin/main 2ba7a9d4 merged in (no rebase). Nothing of #31279 is re-carried.",
"Spec takes minor (claim amended to Metadata-protocol: added).",
"Column labels (keyColumn.label, columns[].label) are English literals; translating them needs a columns group in the settings keys translation entry and ui's copies, written into the ui card body.",
"retention_overrides columns are not judged against the object's declared policy (an expireAfter on an object with no ttl is admitted); per-column domains are not part of ruling A.",
"New shared test kit packages/services/service-settings/src/__tests__/settings-door.testkit.ts (the #31241 suite's scaffolding, now used by both suites).",
"PR body ends with the Generated-with line only (no session URL), as in #31279; line 2 links this card thread."
],
"mcp_calls": "0",
"api_writes": "2 — fleet-write relay pr_create (request fw-20261108T052312Z-ff0ef0, run 42450576765, read-back 14148 bytes sent = stored, assignees none) -> #31386 (draft); this comment via scripts/pm/post-stamped.mjs. Plus own git push (new branch, no force).",
"open_questions": [
"Type name: `map` proposed (alternatives keyed_rows, table); Fable review decides. Renaming is mechanical."
],
"security_findings": "none",
"out_of_scope_findings": []
}
mapShapeVerdict admits every key when a reachable source answers an empty list. Fail closed when the source answered at all, and keep the allowed.length > 0 gate for the declared-table fallback only. Pin: a registry with no lifecycle objects ⇒ invalid_option.
Missing pins for an array value (not_a_map ⇒ invalid_type) and for a scalar entry in a several-column map (not_an_entry ⇒ invalid_shape).
The lifecycle-service.ts:1161 comment still calls these keys json specifiers.
Server seat 2 is clocking out (田騏, 2026-11-07 23:57Z). The follow-up and the remaining rows of this card go into the handoff brief on #20966.
Release: session_017ihL2fBJ6xsLWJsnNJGMMx · seat clock-out on the maintainer's order 「当前任务处理完,合并后就下班」 (田騏, 2026-11-07T23:57Z) · back to pm:queue for a sibling domain:server seat · 2026-11-08T06:08Z
Resume-from: claude/project-thread-4ntp5s@32b54d4d94b4d90ab561fe62098cba565e4ad421
Seat: domain:server#2
Row 5 phase 1 (ruling A) is PR #31386 on that head. It has a Fable PASS (6717755004), a seat ACCEPT, and green CI. It amends ADR-0007, so it is Tier H and waits on 田騏's merge; nothing more is owed on it before then. The PR says Refs #30208 (a partial landing), so this card does not stay behind with the seat.
Owed by whoever holds this card next, once #31386 merges:
Activity
objectstack-fleet commentedon Nov 5, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: first-touch audit · p3 ·
domain:ui·pm:queue· 2026-11-05T03:04Z⛔ Not a claim.
Generated by Claude Code
objectstack-fleet commentedon Nov 5, 2026
epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff
One more row of the same kind, on
main04f36b63f8: Setup's 通知模板 new form takestopicandchannelas free text, so a typed value that matches nothing fails with no message. Pick from known topics and channels, as this card asks for 通知偏好.objectstack-fleet commentedon Nov 5, 2026
domain:ui#6 · os-sales · session_01L6bhgUR8Nu4M7tbtgES7q2
Claim: PM loop round 3 · 2026-11-05T12:19Z
Session:
session_01L6bhgUR8Nu4M7tbtgES7q2Account:
os-sales(the seat's linked user asGET /useranswers it; the card's assignee)Branch: the card thread's designated
claude/project-thread-*branch, named in a claim amendment once the thread reports itWorktree:
objectstack-issue-30208Domain:
domain:uiSeat:
domain:ui#6File surface: the rows whose fix lands in
ui/**(row 4ui/packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsx, rows 7 and 9 where the text is drawn byui/, rows 1–3 and 8 only where the string lives underui/), their tests,ui/packages/i18nlocale entries they need, one root.changeset/file. ⛔packages/**(row 8's banner and rows 1–3's help texts are served frompackages/plugins/plugin-emailandpackages/platform-objectstranslations:domain:server), ⛔ rows 5 and 6 (new field controls: report them as a remainder unless they are a pureui/control over an existing field shape). Rows not done are named in the report for the seat to split (stop on breach; explain in the report)Container & model:
M,mode:thread,model: default(dispatch-gates --tier: no path-derived mandate; floor sonnet · default opus · ceiling fable),effort: highClause-②: no (a new language-pack key, if a row needs one, is declared by the dev in the PR body and the seat re-reads this line at ACCEPT)
Metadata-protocol: none
Responsibility: n/a — not a defect card
Thread-read: 6651344343
Premise: holds in part, read at
origin/main677ac3bb: row 4's 「已绑定到 everyone 锚点」 isui/packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsx:161; row 3's 「为 true 时」 and row 8's banner are server-side strings (packages/platform-objects,packages/plugins/plugin-email); the dev re-measures each row firstSerial constraints cleared: PR #30243 edits
ui/packages/i18n/src/locales/zh.ts(other keys; normal concurrency, the later lander merges main). No maintainer ruling beyond the filing word 「以上都同意,立卡」.objectstack-fleet commentedon Nov 5, 2026
domain:ui#6 · os-sales · session_01L6bhgUR8Nu4M7tbtgES7q2
席位核对 · Amendment · 2026-11-05T12:23Z · claim 6657817034 branch named
Branch:
claude/project-thread-9po7qwThe card thread's harness-designated branch (a project-thread platform exception to the
claude/issue-<n>-<slug>convention); the dev thread is sessionsession_01L6bhgWjg9S7avVTjuNn9P2. Everything else in claim 6657817034 stands.objectstack-fleet commentedon Nov 5, 2026
domain:ui#6 · os-sales · session_01L6bhgWjg9S7avVTjuNn9P2
os-dev-report
{ "issue": 30208, "status": "rework", "branch": "claude/project-thread-9po7qw", "pr": "https://github.com/objectstack-ai/objectstack/pull/30488", "session": "session_01L6bhgWjg9S7avVTjuNn9P2", "premise_still_valid": true, "summary": "Partial by design (Refs, not Fixes): ships rows 4 and 9, the two rows whose text ui/ draws. Row 4: AssignedUsersSection's everyoneNote no longer names the everyone anchor (zh 「所有登录成员都持有此权限集。」, en 'Every signed-in member holds this set.'). Row 9 takes the card's 'explain' arm: the managedByBadge short labels for config / system-data / better-auth now say what they mean for the administrator (zh 在此配置 / 平台预置 · 可编辑 / 经身份服务写入; en Configured here / Built in · editable / Via identity service) in all ten ui language packs, with ManagedByBadge.tsx's English fallbacks matched; no key added or removed, tooltip text and the draw rule (objectui#12194, objectui#13385) unchanged. Rows 1, 2, 3, 5, 6, 7, 8 and the 6651344343 通知模板 row are not shipped; reasons and owning lanes in out_of_scope_findings and the PR's Acceptance notes.", "tests": "pnpm --dir ui exec vitest run on AssignedUsersSection.test.tsx + ManagedByBadge.keyboard-27521.test.tsx: 2 files, 22/22 passed, lock VERDICT command-exit 0. Positive control: the 4 source files restored to base c5800818 with the new tests kept: 4 failed / 18 passed (exactly the 4 non-control new cases), restored with git checkout HEAD --, git diff HEAD empty. Wider: packages/i18n/ + app-shell metadata-admin/ + components/ + 5 managed-by badge view files: 825/825 files, 9149 passed 14 skipped, VERDICT 0. After merging origin/main (head a5f5ec0b): console closure rebuilt (73 tasks), packages/i18n/ + the two files: 97/97 files, 1465 passed 13 skipped, VERDICT 0. No ablation beyond the positive control: copy-only change.", "gates": "dispatch-gates --commands at cd7b73ac derived 35 families; all 35 exit 0; --ran: 35 derived, 35 run, 0 NOT-MEASURED (derived zero, every line with an exit code). Local extras: turbo type-check @object-ui/app-shell + @object-ui/i18n VERDICT 0; pnpm --dir ui run check:spec-symbols exit 0; eslint on changed files + packages/i18n/src/locales exit 0; pnpm --dir ui run check:new-line-citations 0 new, exit 0; pnpm check:doc-authoring exit 0; pnpm check:pm-skill-id-lint exit 0. CI check-runs on PR #30488: in_progress at report time (not waited on).", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "5 — issue_read x2 (card, comments), pull_request_read x2 (PR #30442 for body shape, PR #30488 read-back); no MCP write tool", "api_writes": "2 — fleet-write relay pr_create (run 42034634236, #30488, body read back identical 11159 bytes, no assignees) and this os-dev-report comment via post-stamped. git push of claude/project-thread-9po7qw and of the evidence branch claude/issue-30208-evidence are not REST writes.", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "carrier: seat (split) · row 1 · permission set assignment help texts (fail-closed, 半开区间 [from, until), 智能体授权) are served from packages/plugins/plugin-security/src/translations/zh-CN.objects.generated.ts (半开区间 also plugin-approvals) · domain:server", "carrier: seat (split) · row 2 · business unit help (图谱语义, Null 表示租户根节点, HRIS, 图谱查询) in packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts · domain:server", "carrier: seat (split) · row 3 · 「为 true 时…」 on organization and other booleans in the same platform-objects zh-CN bundle · domain:server", "carrier: seat (split) · row 5 · 数据生命周期 retention / row quota / class quota JSON boxes: a new control over a settings shape from packages/objectql/src/lifecycle/lifecycle-settings.ts, not a pure ui/ control over an existing field shape; needs a settings/spec shape decision first", "carrier: maintainer (needs_decision) · row 6 + comment 6651344343 · 通知偏好 / 通知模板 topic and channel free text: channel is free text by the route A ruling on #25346 (cited in notification-preference.object.ts: the channel set is open at runtime and a select cannot store *); a picker reverses that ruling. 免打扰 quiet_hours (Field.json) needs a new time-range control", "carrier: seat (split) · row 7 · 队列 / 队列成员: sys_queue and sys_queue_member are declared in packages/plugins/plugin-sharing, which ships no translation bundle; title, columns, help and 新建Queue come from English object metadata · domain:server", "carrier: seat (split) · row 8 · 邮件投递 banner 'No mail leaves this machine' (plugin-email live-transport-status.ts) and toast 'Sent test email to…' (plugin-email email-plugin.ts) · domain:server", "class: a · reach: zh-CN console, Setup sidebar (measured live on showcase at the PR's base: 人员与组织 / 访问控制 entries contributed by plugin-security render in English: Users, Organization, Business Units, Teams, Invitations, Position Holders, Permission Set Assignment, Sharing Rules, Record Shares, Queues, Queue Members, API Keys) · evidence: PR #30488 screenshots; packages/platform-objects/src/apps/translations/zh-CN.ts carries 权限集分配 for nav_permission_set_assignment yet the sidebar shows English; which side drops it (contributed nav item vs console nav label lookup) not established · dedupe words: setup sidebar english, nav_permission_set_assignment, plugin-security navigation translation, zh-CN nav label" ], "deviations": "(1) The evidence commit on the never-merged branch claude/issue-30208-evidence was made with git commit --no-verify (screenshots only; that branch is never opened as a PR). (2) The PR carries no assignee or reviewer, per the dispatch order and os-dev rule 2, although the session's attribution note asks for them." }31 remaining items
objectstack-fleet commentedon Nov 7, 2026
domain:server#2 · marchtian · session_017ihL2fBJ6xsLWJsnNJGMMx
Claim: PM loop round 1 · #30208 row 5 (Settings → Data Lifecycle: retention / row quota / class quota are raw JSON boxes) · 2026-11-07T19:10Z · shape first, then build
Session:
session_017ihL2fBJ6xsLWJsnNJGMMxAccount:
marchtianBranch:
claude/project-thread-4ntp5s(the card thread's harness-pinned branch, cut frommain4c318214; platform exception toclaude/issue-<n>-<slug>)Worktree:
objectstack-issue-30208Domain:
domain:serverSeat:
domain:server#2File surface (phase 0, read-only): none written. The phase 0 output is a shape analysis posted on this card. (phase 1, after the maintainer's ruling):
packages/objectql/src/lifecycle/lifecycle-settings.tsand its readers (lifecycle-service.ts, the services that readretention_overrides/quotas/quota_defaults),service-settingstranslations, andpackages/spec/src/system/settings-manifest.zod.tsonly if the ruling grants a specifier widening. ⛔ui/**(the console control is its own card once the shape exists).Container & model:
M,mode:thread,model: TIER_DEFAULT,effort: defaultClause-②: no — phase 0 writes no code; phase 1 re-derives it from the ruled shape (a new specifier type would be
yes (widening)) and the seat posts a freshClaim:before the buildMetadata-protocol: none
Responsibility: what a business administrator edits on the Data Lifecycle settings page | the
lifecyclesettings namespace | Setup → Settings → Data LifecycleThread-read: 6700719349
Premise: holds. On
origin/main714dda8faa,lifecycle-settings.tsdeclaresretention_overrides,quotasandquota_defaultsastype: 'json'.SpecifierTypeinsettings-manifest.zod.tsis a closed enum with no row or map specifier, so no rows-with-an-object-picker shape exists today (#30208 release 6659910397: "shape first").Serial constraints cleared: rows 1–4 and 6–9 have landed or been filed elsewhere. Seat 3 released this row. The quiet-hours control goes to the ui lane as its own card.
Phase 0 is a design question, not a build. The shape needs either a new settings specifier (a
packages/specwidening plus a ui renderer) or a move of the overrides to records. Either way it goes to the maintainer through a decision thread before any code is written. Order: project fileserver-seat-2/orders/30208-row5.md.objectstack-fleet commentedon Nov 7, 2026
domain:server#2 · marchtian · session_017ihL2dxcqiQvzKhr5odTPe
Row 5 shape analysis (phase 0)
Read-only, on
origin/main4c318214, posted 2026-11-07T19:17Z. No code, no branch push, no PR.1. Readers and writers of the three keys
lifecycle-service.ts:1147-1150loadGovernanceRecord<object, {maxAge?, expireAfter?}>,Record<object, number>,Record<class, number>;?? {}lifecycle-service.ts:1180-1192retention_overridesper organization, keeps onlysource === 'tenant'lifecycle-service.ts:1313quotas[obj] ?? quota_defaults[class], used only iftypeof === 'number' && > 0lifecycle-service.ts:1371, 1693→effectiveWindowMs(1480)parseLifecycleDuration(`^(\d+)(hservice-queuedb-queue-adapter.ts:343,queue-service-plugin.ts:164lifecycle.retention_overrides.sys_job_queue.maxAgeservice-messaging,-job,-automation,metadata-protocolidentity-index migration,sys-setting.object.ts:214OS_LIFECYCLE_RETENTION_OVERRIDES/_QUOTAS/_QUOTA_DEFAULTS(envKeyOf),JSON.parsed because the default is an object (settings-service.ts:4294);OS_LIFECYCLE_DISABLEDis a separate process flagrules/lifecycle.md:138,objects.mdx:210,backup-restore.mdx:177,production-readiness.mdx:191, ADR-0057 §3.2;settings.lifecycle.keys.*in 10 languagesNothing validates the value.
validatePatchhas no type or shape check forjson, so a typo in an object name, a quota of"1e6"or a duration of30 daysis stored and then silently does nothing (only the bad duration logs a warning).2. How the console draws it today
ui/apps/console/src/pages/settings/SettingsField.tsx:1054drawsjsonas a monospace<Textarea>withonChange(e.target.value);SettingsView.tsx:740drafts that string andapi.ts:155PUTs it as is (noJSON.parseinpages/settings/). Traced by reading, not measured: editing the box saves a JSON string,loadGovernancethen indexes a string, and every override and quota silently stops applying. ADR-0007 saysjson"persists object". These are the onlyjsonsettings specifiers inpackages/; phase 1 should confirm with a live PUT first. Under A or B the box goes away.A row editor needs: (a) a key column, an object picker over objects that declare
lifecycle(others ignore an override) or aLifecycleClassselect; (b) value columns, a duration (number + h/d/w/y,LIFECYCLE_DURATION_REGEX) or a positive integer; (c) per-column labels in the translation tree. The runtime option source (registerOptionSource, aslocalization.localeuses) can serve the object list.3. Candidate shapes
columns)jsonplus an optional structure hintcolumns; 3 specifiers; settings-service write check; ui cardjson; same check off the hint; ui rows when hintedlifecycle-servicereads records;action_buttonnavigateto the listjson= code editor" line needs a notesys_settingrows migrate to records (tenant →organization_id); lossless only if bad entries carry overAlso considered: a per-object panel on each object's setup page (the Salesforce place). It scatters the overrides and still needs A or B underneath.
How others present this: all show one row per target (table/service + window), none a free-form map.
sys_auto_flushrecords with table, match field, age in seconds and conditions (Autoflush form, Table cleaner).HistoryRetentionPolicyon each object,archiveAfterMonths/archiveRetentionYears(metadata reference).ServiceNow and Dataverse store rules as records (C). What transfers is the row look; our per-organization cascade already does what their record scoping does.
4. Recommendation
A. The three keys really are maps (object or class → window or count). A keyed-rows specifier draws them as the rows the platforms above show, keeps every stored value, env var, lock and audit row as it is, and gives service-settings a declaration to refuse a wrong object, duration or count at both the save door and the env door. Today none of these is refused. B is A without the new enum value, at the cost of one type with two renderings. C fits ServiceNow/Dataverse but trades the cascade and a data migration for a page with three knobs. A needs an ADR-0007 amendment and a spec widening, so it takes the Fable spec review. The control itself is the
domain:uifollow-up card.Search terms:
json specifier string,retention_overrides console,settings keyed rows,SpecifierType map.objectstack-fleet commentedon Nov 8, 2026
domain:server#2 · marchtian · session_017ihL2fBJ6xsLWJsnNJGMMx
Row 5 ruling record — A (a typed map specifier)
Ruled · 2026-11-08T02:08Z
田騏, 2026-11-08 02:07:52Z, in the project's Fable decision thread: 「同意」 (project message
cmsg_01VmuUhXU9SSUzqJYyXSmYSiAbe9LctoxZJq1geQKD7KXc). That accepts option A of the decision card: "spec 加一个带列定义的映射类型,三个键改用它;值和环境变量不变,保存口和环境变量口开始校验;修订 ADR-0007,另开 ui 控件卡。" The analysis is the phase 0 comment 6711865649 plus the Fable thread's write-up (platform survey: VS Code object grid, ServiceNow Table Cleanup, Dataverse retention, Salesforce HistoryRetentionPolicy, Google Vault, S3 Lifecycle).What A means here.
SpecifierType(packages/spec/src/system/settings-manifest.zod.ts) gains one typed-map member with column definitions. The working name ismap; the spec PR and its Fable review settle the final name.retention_overrides,quotasandquota_defaultsmove fromjsonto it. ADR-0007 is amended under its own rule "New types require an ADR amendment". The console control is a separatedomain:uicard.Hard constraints carried by the ruling:
quotas,quota_defaults); a multi-column map stores objects (retention_overrides). One type covers both, and the rule is written in the spec docs.optionsandpatternalready are (env 来源的 settings 值绕过 manifest 的 options 表校验 —— #5094 在写入 API 上堵住的洞,在 OS_* 覆盖这一侧原样敞开 #5204, settings env 门不执行pattern:写入门拒的形状非法值,OS_*覆盖照收——#5204 不对称的最后一族 #6580): the save door (validatePatch) and the env door (effectiveEnvOverride). The key must name a real lifecycle object or class, a duration must matchLIFECYCLE_DURATION_REGEX, and a quota must be a positive integer.lifecycle, orLifecycleClassSchema. It is never hard-coded in the manifest.jsonstays in the enum for genuinely raw config. The ADR-0007 amendment adds the new type and notes thatjsonis no longer the home for maps.Phase 1 is dispatched to this card's thread after #31279 (Fixes #31241, the json-stored-as-text defect) lands, because both touch
settings-service.tsandlifecycle-service.ts.objectstack-fleet commentedon Nov 8, 2026
domain:server#2 · marchtian · session_017ihL2fBJ6xsLWJsnNJGMMx
Claim: PM loop round 1 · #30208 row 5 phase 1 (build ruling A: a typed map specifier for the three lifecycle map keys) · 2026-11-08T02:09Z · build
Session:
session_017ihL2fBJ6xsLWJsnNJGMMxAccount:
marchtianBranch:
claude/project-thread-4ntp5s(the card thread's harness-pinned branch; the phase 1 PR is cut from it after mergingmainonce #31279 has landed; platform exception toclaude/issue-<n>-<slug>)Worktree:
objectstack-issue-30208Domain:
domain:serverSeat:
domain:server#2File surface:
packages/spec/src/system/settings-manifest.zod.ts(+ its test, generated reference docs and the spec's settings docs),docs/adr/0007-settings-manifest-and-kv-store.md(the amendment row),packages/services/service-settings/src/**(the shared map shape judgement invalidatePatchandeffectiveEnvOverride, the runtime option source for a map's key column, the 12 translation packs'settings.lifecycle.keys.*help text),packages/objectql/src/lifecycle/lifecycle-settings.tsandlifecycle-service.ts(readers unchanged in shape),skills/**lines that teach the three keys as JSON (if any),.changeset/. ⛔ui/**(the console control is its owndomain:uicard). ⛔ No change to any stored value, env variable name or cascade.Container & model:
M,mode:thread,model: TIER_DEFAULT,effort: defaultClause-②: yes (widening) —
SpecifierTypegains a member; the save and env doors also start refusing malformed values for the three keys (a narrowing on values that never took effect, named in the PR's BREAKING note)Metadata-protocol: none
Responsibility: business administrators set retention windows and row quotas as checked rows instead of raw JSON | settings manifest specifier set (spec), service-settings save/env doors | Settings → Data Lifecycle
Thread-read: 6711865649
Premise: holds. On
main, the three keys are the onlytype: 'json'specifiers inpackages/. Their values pass both doors unchecked: a misspelt object,"1e6"or30 daysis stored and silently ignored. Ruling record above.Serial constraints cleared: waits for #31279 (
settings-service.ts,lifecycle-service.ts) to merge. Tier H (docs/adr/**): human merge. Fable reviews the spec + ADR hunks and the code implementing that contract (the shape judgement).Order: project file
server-seat-2/orders/30208-row5-phase1.md.objectstack-fleet commentedon Nov 8, 2026
domain:server#2 · marchtian · session_017ihL2fBJ6xsLWJsnNJGMMx
Claim: PM loop round 1 · #30208 row 5 phase 1 (build ruling A: a typed map specifier for the three lifecycle map keys) · 2026-11-08T02:28Z · build (amended: Metadata-protocol
added)Session:
session_017ihL2fBJ6xsLWJsnNJGMMxAccount:
marchtianBranch:
claude/project-thread-4ntp5s(the card thread's harness-pinned branch; the phase 1 PR is cut from it after mergingmainonce #31279 has landed; platform exception toclaude/issue-<n>-<slug>)Worktree:
objectstack-issue-30208Domain:
domain:serverSeat:
domain:server#2File surface:
packages/spec/src/system/settings-manifest.zod.ts(+ its test, generated reference docs and the spec's settings docs),docs/adr/0007-settings-manifest-and-kv-store.md(the amendment row),packages/services/service-settings/src/**(the shared map shape judgement invalidatePatchandeffectiveEnvOverride, the runtime option source for a map's key column, the 12 translation packs'settings.lifecycle.keys.*help text),packages/objectql/src/lifecycle/lifecycle-settings.tsandlifecycle-service.ts(readers unchanged in shape),skills/**lines that teach the three keys as JSON (if any),.changeset/. ⛔ui/**(the console control is its owndomain:uicard). ⛔ No change to any stored value, env variable name or cascade.Container & model:
M,mode:thread,model: TIER_DEFAULT,effort: defaultClause-②: yes (widening) —
SpecifierTypegains a member; the save and env doors also start refusing malformed values for the three keys (a narrowing on values that never took effect, named in the PR's BREAKING note)Metadata-protocol: added — a package author can now declare a
mapspecifier in a settings manifest (a new accepted value ofSpecifierType)Responsibility: business administrators set retention windows and row quotas as checked rows instead of raw JSON | settings manifest specifier set (spec), service-settings save/env doors | Settings → Data Lifecycle
Thread-read: 6711865649
Premise: holds. On
main, the three keys are the onlytype: 'json'specifiers inpackages/. Their values pass both doors unchecked: a misspelt object,"1e6"or30 daysis stored and silently ignored. Ruling record above.Serial constraints cleared: waits for #31279 (
settings-service.ts,lifecycle-service.ts) to merge. Tier H (docs/adr/**): human merge. Fable reviews the spec + ADR hunks and the code implementing that contract (the shape judgement).Order: project file
server-seat-2/orders/30208-row5-phase1.md.objectstack-fleet commentedon Nov 8, 2026
domain:server#2 · marchtian · session_017ihL2dxcqiQvzKhr5odTPe
os-dev-report
Written 2026-11-08T05:24Z.
{ "issue": 30208, "status": "done", "branch": "claude/project-thread-4ntp5s", "pr": "https://github.com/objectstack-ai/objectstack/pull/31386", "head": "32b54d4d94b4d90ab561fe62098cba565e4ad421", "session": "session_017ihL2dxcqiQvzKhr5odTPe", "premise_still_valid": true, "row": "5, phase 1 (ruling A, record 6715997428)", "proposed_type_name": "map", "summary": "spec: SpecifierType gains 'map' (value-bearing, in SPECIFIERS_REQUIRING_KEY); Specifier gains keyColumn {label, description?} and columns [{name, label, description?, kind: duration | positive_integer}]; a map requires both (at least one column, unique names), any other type declaring either is refused at parse; storage rule documented (one column -> Record<key, value>, several -> Record<key, {[column]: value}>). ADR-0007 amended (status line, a map row, json row 'not for maps', dated note citing 6715997428). service-settings: map-shape.ts is the one shape judgement (firstMapShapeMiss: not_a_map / key / not_an_entry / unknown_column / cell) called by SettingsService.mapShapeVerdict at validatePatch (one FieldError per key: invalid_type / invalid_option / unknown_field / invalid_shape, constraint {type:'map', entry, column?, kind?, allowed?}, a sentence naming the entry and the expected form) and at the env door (reportRejectedEnvOverride, the same sentence, value described by shape); key domain read through optionTableOf, so a registered runtime option source moves both doors; it subsumes jsonShapeMiss for these keys (they are no longer json; jsonShapeMiss stays for json keys). objectql: retention_overrides (maxAge, expireAfter: duration), quotas and quota_defaults (max_rows: positive_integer) are map specifiers, stored values / env names / cascade unchanged; lifecycle-key-options.ts serves the keys (objects with a lifecycle class other than record, read at each ask; LifecycleClassSchema minus record), registered by ObjectQLPlugin at kernel:ready before the manifest. Translations: JSON examples dropped from the help text in the ten packs. Docs: config-resolution.mdx map paragraph; generated reference, authorable surface, ledger counts. Changeset: spec minor, service-settings and objectql patch, both declaration lines, BREAKING with migration, ADR-0087 not-required marker.", "claim_lines": { "clause_2": "yes (widening) — `SpecifierType` gains a member; the save and env doors also start refusing malformed values for the three keys (a narrowing on values that never took effect, named in the PR's BREAKING note)", "metadata_protocol": "added — a package author can now declare a `map` specifier in a settings manifest (a new accepted value of `SpecifierType`)", "skills_docs": "no skill teaches the specifier set; skills/objectstack-platform/references/plugin-development.md:355 names SettingsManifest only and skills/objectstack-data/rules/lifecycle.md:137-139 names the three keys by name only, never their value form, so no skill change and no follow-up owed. Docs updated in the PR." }, "tests": "All on the final head 32b54d4d under os-verify-lock. New: service-settings src/settings-map-shape-30208.test.ts 16/16 (real ObjectQL + platform setting objects + real route + shipped lifecycleSettingsManifest + the two key sources); objectql src/lifecycle/lifecycle-key-options.test.ts 6/6 (incl. a real ObjectKernel wiring case); spec settings-manifest.test.ts +6. settings-json-shape-31241.test.ts 8/8 moved onto a raw-config json manifest. Packages: @objectstack/spec 912 files / 27528 tests exit 0; @objectstack/objectql 569 / 11501 exit 0; @objectstack/service-settings 73 / 1397 + typecheck exit 0; spec + objectql typecheck (test layer included) exit 0; pnpm lint exit 0.", "ablations": [ "B1 a misspelt key accepted (mapShapeVerdict admitsKey -> undefined): 4/16 red (misspelt object, record/no-lifecycle objects, record and misspelt class at the save door, misspelt object at the env door)", "B2 a bad duration accepted (cellFits duration drops LIFECYCLE_DURATION_REGEX): 2/16 red ('30 days' at the save door and at the env door)", "B3 an env value bypasses the judgement (env door map verdict -> null): 3/16 red (misspelt object, bad duration, unparseable text in force)", "each through scripts/ablation-replace.mjs on head 32b54d4d: mutation landed, restored blob == HEAD and git diff HEAD empty after each" ], "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on 32b54d4d: 121 commands, each with stdin from /dev/null after a full packages/** build, all ':: exit 0'; --ran: '121 derived, 121 run, 0 NOT-MEASURED, 0 UNRUN'. The pre-merge round caught check:error-code-casing (field codes in the new test, now adr0112-ok: D6) and check:slot-lookup + eslint no-restricted-syntax (an `as any` in the kernel wiring test, removed); its check:skill-examples / check:dual-build-cjs-loads exit 3 were a missing dist, green after the full build.", "dogfood": "full battery on 32b54d4d: every packages/** package built, then pnpm --filter @objectstack/dogfood test under the lock, no shard, no path: Test Files 437 passed | 3 skipped (440), Tests 3618 passed | 41 skipped (3659), VERDICT command-exit 0", "dogfood_battery": "437 passed | 3 skipped files; 3618 passed | 41 skipped tests; exit 0", "line_budget": "28 files, +1361 / -220 vs main (generated: the settings-manifest reference page, authorable-surface/system.json, ledger counts); under the 3,000-line human-approval line", "files_changed": [ ".changeset/30208-settings-map-specifier.md", "content/docs/protocol/kernel/config-resolution.mdx", "content/docs/references/system/settings-manifest.mdx", "docs/adr/0007-settings-manifest-and-kv-store.md", "docs/audits/2026-07-unknown-key-strictness-ledger.counts/system.md", "packages/objectql/src/index.ts", "packages/objectql/src/lifecycle/lifecycle-key-options.test.ts", "packages/objectql/src/lifecycle/lifecycle-key-options.ts", "packages/objectql/src/lifecycle/lifecycle-settings.ts", "packages/objectql/src/plugin.ts", "packages/services/service-settings/src/__tests__/settings-door.testkit.ts", "packages/services/service-settings/src/map-shape.ts", "packages/services/service-settings/src/settings-json-shape-31241.test.ts", "packages/services/service-settings/src/settings-map-shape-30208.test.ts", "packages/services/service-settings/src/settings-service.ts", "packages/services/service-settings/src/translations/ar.ts", "packages/services/service-settings/src/translations/de.ts", "packages/services/service-settings/src/translations/es-ES.ts", "packages/services/service-settings/src/translations/fr.ts", "packages/services/service-settings/src/translations/ja-JP.ts", "packages/services/service-settings/src/translations/ko.ts", "packages/services/service-settings/src/translations/pt-BR.ts", "packages/services/service-settings/src/translations/ru.ts", "packages/services/service-settings/src/translations/zh-CN.ts", "packages/services/service-settings/src/translations/zh-Hant.ts", "packages/spec/authorable-surface/system.json", "packages/spec/src/system/settings-manifest.test.ts", "packages/spec/src/system/settings-manifest.zod.ts" ], "ui_card_body": "## Console: draw the `map` settings specifier as a table of keyed rows\n\nFollows #30208 row 5 (ruling record 6715997428, option A). The server half adds a `map` member to `SpecifierType` and moves `lifecycle.retention_overrides`, `lifecycle.quotas` and `lifecycle.quota_defaults` to it. Until this card lands, Setup → Data Lifecycle draws those three rows as \"Unsupported specifier type: map\" (`console.settingsView.unsupportedSpecifierType`). Env overrides and the settings API are unaffected.\n\n### The contract the control renders (from `packages/spec/src/system/settings-manifest.zod.ts`)\n\n- `type: 'map'`, `key`, `label`, `description`, `default` (an object, `{}` for the three lifecycle keys), `scope`, the usual lock and source badges.\n- `keyColumn: { label, description? }`: the header over the key column.\n- `columns: [{ name, label, description?, kind }]`, at least one, in display order. `kind` is `'duration'` or `'positive_integer'`.\n- `options` (served, not declared): the namespace read (`GET /api/settings/:namespace`) serves the keys the map admits as the specifier's `options` (`[{ value, label }]`), from a runtime option source (the objects that declare a lifecycle class other than `record`; the lifecycle classes other than `record` for `quota_defaults`). The manifest declares no list.\n\n### Stored value (unchanged from what these keys store today)\n\n- One value column: `Record<key, value>`, e.g. `{ \"sys_audit_log\": 1000000 }`. The column's `name` is not stored.\n- Several value columns: `Record<key, { [column.name]: value }>`, e.g. `{ \"sys_job_run\": { \"maxAge\": \"1y\", \"expireAfter\": \"30d\" } }`. An entry carries at least one declared column and no other property; an empty cell is left out of the entry.\n\n### What to build\n\n1. **Rows.** One row per entry of the effective value, in the order stored; add a row and remove a row. The control keeps a local draft with stable row ids (the starting point is `ui/packages/app-shell/src/views/metadata-admin/inspectors/FlowKeyValueField.tsx`, which already has a local draft, stable row ids and a searchable key picker, `keysByLabel`).\n2. **Key picker.** A searchable picker over the served `options` (`value`, shown by `label`). A key already used by another row is not offered again. A stored key that is no longer in `options` stays on its row, marked as not offered, so the save door's `invalid_option` refusal is explained before Save.\n3. **Duration input** for a `duration` column: a whole number plus a unit select (`h`, `d`, `w`, `y`), stored as the literal (`'90d'`, `'7y'`). The server judges it against `LIFECYCLE_DURATION_REGEX` (`/^\\d+(h|d|w|y)$/`).\n4. **Positive-integer input** for a `positive_integer` column: whole numbers above 0, sent as a number, never as text.\n5. **Save** sends the object (never JSON text), through the page's existing patch. A cleared map sends `null`.\n6. **Errors.** The save door answers one `FieldError` for the key, with `constraint: { type: 'map', entry, column?, kind?, allowed? }`. Codes: `invalid_type` (the value is not an object), `invalid_option` (a key not admitted), `unknown_field` (a property no column declares), `invalid_shape` (an entry or a cell not in its column's form). Mark the row named by `entry` and the cell named by `column`; the message is the server's English sentence until the codes get console translations.\n7. **Column labels.** `keyColumn.label` and `columns[].label` are English literals in the manifest. Translating them needs a `columns` group in the settings `keys.<key>` translation entry (`packages/spec/src/system/translation.zod.ts`) and ui's copies of that schema; it belongs to this card if the console translates the headers.\n8. **Unsupported fallback** stays for any other unknown type.\n\n### Tests to carry\n\nA map with one column and with several renders its rows from the stored value; the key picker lists the served options and hides used keys; a duration typed as number + unit is stored as `'90d'`; Save PUTs the object; an `invalid_option` answer marks the row; a locked (env) map renders read-only.\n\nOut of scope: any server change. The server half is the #30208 row 5 phase 1 PR.\n", "deviations": [ "File surface: content/docs/protocol/kernel/config-resolution.mdx (the env paragraph this change extends) and the generated content/docs/references/system/settings-manifest.mdx, packages/spec/authorable-surface/system.json and docs/audits/2026-07-unknown-key-strictness-ledger.counts/system.md are not in the claim; skills/** lines in the claim needed no change.", "Branch: the merge queue deleted claude/project-thread-4ntp5s when #31279 landed; recreated from main at b85190a8 with this work on top, then origin/main 2ba7a9d4 merged in (no rebase). Nothing of #31279 is re-carried.", "Spec takes minor (claim amended to Metadata-protocol: added).", "Column labels (keyColumn.label, columns[].label) are English literals; translating them needs a columns group in the settings keys translation entry and ui's copies, written into the ui card body.", "retention_overrides columns are not judged against the object's declared policy (an expireAfter on an object with no ttl is admitted); per-column domains are not part of ruling A.", "New shared test kit packages/services/service-settings/src/__tests__/settings-door.testkit.ts (the #31241 suite's scaffolding, now used by both suites).", "PR body ends with the Generated-with line only (no session URL), as in #31279; line 2 links this card thread." ], "mcp_calls": "0", "api_writes": "2 — fleet-write relay pr_create (request fw-20261108T052312Z-ff0ef0, run 42450576765, read-back 14148 bytes sent = stored, assignees none) -> #31386 (draft); this comment via scripts/pm/post-stamped.mjs. Plus own git push (new branch, no force).", "open_questions": [ "Type name: `map` proposed (alternatives keyed_rows, table); Fable review decides. Renaming is mechanical." ], "security_findings": "none", "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Nov 8, 2026
domain:server#2 · marchtian · session_017ihL2fBJ6xsLWJsnNJGMMx
Row 5 phase 1: PR #31386 ACCEPTed and waiting for the maintainer's merge (Tier H, ADR-0007) · 2026-11-08T05:33Z
32b54d4d. Fable PASS 6717755004 confirms the namemap; the seat ACCEPT is the next comment on the PR.mapsettings specifier as a table of keyed rows (Data Lifecycle retention overrides and quotas) #31391 (domain:ui,pm:blockeduntil feat(spec): a map settings specifier; the lifecycle retention overrides and quotas use it #31386 merges). The console draws the three keys as an unsupported type until it lands.mapShapeVerdictadmits every key when a reachable source answers an empty list. Fail closed when the source answered at all, and keep theallowed.length > 0gate for the declared-table fallback only. Pin: a registry with no lifecycle objects ⇒invalid_option.not_a_map⇒invalid_type) and for a scalar entry in a several-column map (not_an_entry⇒invalid_shape).lifecycle-service.ts:1161comment still calls these keysjsonspecifiers.Server seat 2 is clocking out (田騏, 2026-11-07 23:57Z). The follow-up and the remaining rows of this card go into the handoff brief on #20966.
objectstack-fleet commentedon Nov 8, 2026
domain:server#2 · marchtian · session_017ihL2fBJ6xsLWJsnNJGMMx
Release:
session_017ihL2fBJ6xsLWJsnNJGMMx· seat clock-out on the maintainer's order 「当前任务处理完,合并后就下班」 (田騏, 2026-11-07T23:57Z) · back topm:queuefor a siblingdomain:serverseat · 2026-11-08T06:08ZResume-from: claude/project-thread-4ntp5s@32b54d4d94b4d90ab561fe62098cba565e4ad421
Seat:
domain:server#2Row 5 phase 1 (ruling A) is PR #31386 on that head. It has a Fable PASS (6717755004), a seat ACCEPT, and green CI. It amends ADR-0007, so it is Tier H and waits on 田騏's merge; nothing more is owed on it before then. The PR says
Refs #30208(a partial landing), so this card does not stay behind with the seat.Owed by whoever holds this card next, once #31386 merges:
pm:blockedon the ui card Console: draw themapsettings specifier as a table of keyed rows (Data Lifecycle retention overrides and quotas) #31391 (the console's keyed-row table formap);The card's other rows remain unclaimed. The handoff brief on #20966 (6718032182) records it.
objectstack-fleet commentedon Nov 8, 2026
domain:server#5 · os-project-manager · session_01FEkQ5o8PE7ZtVjcor51wBZ
Landed · row 5 phase 1 · 2026-11-08T07:08Z
⛔ Not a claim. Server seat 5 holds this card after the
domain:server#2release (6718063348) and the seat 2 → seat 5 hand-off.mapsettings specifier, used by the lifecycle keysretention_overrides,quotasandquota_defaults(ruling A, 6715997428; ADR-0007 amended).mapsettings specifier as a table of keyed rows (Data Lifecycle retention overrides and quotas) #31391 (the console's keyed-row table formap):pm:blocked→pm:queue.domain:server, p3).domain:ui, p3).Where every row stands
sys_notification_template.topicNo server work is left on this card itself. It closes when #31391 and #31421 land.