Repository navigation
Description
domain:server#2 · marchtian · session_017ihL2fBJ6xsLWJsnNJGMMx
Filing gate: ④ — the per-layer child of #30208 row 5 (ruling A, record 6715997428: "spec 加一个带列定义的映射类型 … 另开 ui 控件卡"); the server half is PR #31386.
Business reason: a business administrator sets retention windows and row quotas per object as checked rows instead of hand-written JSON. Until this lands, Setup → Data Lifecycle shows the three keys as an unsupported specifier type and they can only be set through the API or OS_LIFECYCLE_*.
Reader: the domain:ui seat, the console settings renderer (ui/apps/console/src/pages/settings/SettingsField.tsx), next PR after #31386 merges.
Dedupe (open AND closed): "map specifier console settings" → 30 hits, "retention_overrides console" → 7 hits; none is this control (#30208 is the parent, #31241 the closed json-as-text defect).
Spec contract gate: SpecifierType map with keyColumn / columns, from #31386; no new spec surface is asked here (column-label translations are named in step 7).
Console: draw the map settings specifier as a table of keyed rows
Follows #30208 row 5 (ruling record 6715997428, option A; Fable review 6717755004 settled the name map). 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.
The contract the control renders (from packages/spec/src/system/settings-manifest.zod.ts)
type: 'map',key,label,description,default(an object,{}for the three lifecycle keys),scope, the usual lock and source badges.keyColumn: { label, description? }: the header over the key column.columns: [{ name, label, description?, kind }], at least one, in display order.kindis'duration'or'positive_integer'.options(served, not declared): the namespace read (GET /api/settings/:namespace) serves the keys the map admits as the specifier'soptions([{ value, label }]), from a runtime option source (the objects that declare a lifecycle class other thanrecord; the lifecycle classes other thanrecordforquota_defaults). The manifest declares no list.
Stored value (unchanged from what these keys store today)
- One value column:
Record<key, value>, e.g.{ "sys_audit_log": 1000000 }. The column'snameis not stored. - 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.
What to build
- 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). - Key picker. A searchable picker over the served
options(value, shown bylabel). A key already used by another row is not offered again. A stored key that is no longer inoptionsstays on its row, marked as not offered, so the save door'sinvalid_optionrefusal is explained before Save. - Duration input for a
durationcolumn: a whole number plus a unit select (h,d,w,y), stored as the literal ('90d','7y'). The server judges it againstLIFECYCLE_DURATION_REGEX(/^\d+(h|d|w|y)$/). - Positive-integer input for a
positive_integercolumn: whole numbers above 0, sent as a number, never as text. - Save sends the object (never JSON text), through the page's existing patch. A cleared map sends
null. - Errors. The save door answers one
FieldErrorfor the key, withconstraint: { 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 byentryand the cell named bycolumn; the message is the server's English sentence until the codes get console translations. - Column labels.
keyColumn.labelandcolumns[].labelare English literals in the manifest. Translating them needs acolumnsgroup in the settingskeys.<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. - Unsupported fallback stays for any other unknown type.
Tests to carry
A 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.
Out of scope: any server change. The server half is the #30208 row 5 phase 1 PR.
Activity
objectstack-fleet commentedon Nov 8, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: audit · p2 ·
domain:ui· confirmed · 2026-11-08T05:57Z⛔ Not a claim.
mapsettings specifier: Data Lifecycle retention windows and row quotas edited as keyed rows, not hand-written JSON. This is the ui child of Setup: replace developer wording and hand-written JSON with fields a business administrator can use #30208 row 5, ruling A. The server half is PR feat(spec): a map settings specifier; the lifecycle retention overrides and quotas use it #31386.pm:blockeduntil PR feat(spec): a map settings specifier; the lifecycle retention overrides and quotas use it #31386 merges; the unlock is by hand, because the blocker is a PR named in prose.Generated by Claude Code
objectstack-fleet commentedon Nov 8, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jVEjiYrK7onirxUGyo
Claim: PM loop round 1 · 2026-11-08T07:11Z
Session:
session_01QsR2jUhVXEJm46XTxRs4GM(card thread, dev) · seatsession_01QsR2jVEjiYrK7onirxUGyoAccount:
zhuangjianguoBranch:
claude/project-thread-nr5783Worktree:
objectstack-issue-31391Domain:
domain:uiSeat:
domain:ui#3File surface:
ui/apps/console/src/pages/settings/**(themapcontrol, the SettingsField dispatch, tests), new keys inui/packages/i18n/src/locales/*.ts(all ten packs), root.changeset/; ⛔ nopackages/spec/**, no server changeContainer & model: M,
mode:thread,model: default tier(dispatch-gates --tier: no path-derived mandate),effort: highClause-②: yes
Metadata-protocol: none
Responsibility: n/a — not a defect card (the console has no control for the
mapspecifier the server now serves)Thread-read: 6717976306
Premise: holds: PR #31386 merged on
main;mapis inSpecifierType(packages/spec/src/system/settings-manifest.zod.ts) andSettingsField.tsxhas nomapcase, so it drawsunsupportedSpecifierTypeonorigin/maine6a0b72a9Serial constraints cleared: none — the only other
domain:uicard inpm:dispatched(#31383, seat 1) is the flow designer, no shared fileScope: "What to build" items 1–6 and 8 and "Tests to carry". Item 7 (translated column headers) needs a
packages/specchange and stays out; the dev says whether a spec follow-up is worth filing.Clause-②: yesbecause new@object-ui/i18nlocale keys widen the publishedTranslationKeys; the dev measures it on the build and the seat amends if wrong. PR saysFixes #31391. Brief:/mnt/project-files/issue-31391/brief.md. Summons: the maintainer's 「开一个ui 座位」 (this project, 2026-11-08T06:02Z).objectstack-fleet commentedon Nov 8, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jUhVXEJm46XTxRs4GM
os-dev-report
{ "issue": 31391, "status": "done", "branch": "claude/project-thread-nr5783", "pr": "https://github.com/objectstack-ai/objectstack/pull/31451", "head": "bdc8319d", "session": "session_01QsR2jUhVXEJm46XTxRs4GM", "premise_still_valid": true, "summary": "Setup now draws a `map` specifier (lifecycle retention_overrides, quotas, quota_defaults) as a table of keyed rows: new SettingsMapField.tsx reached by `case 'map'` in SettingsField; rows are added by picking an unused served key, used keys are hidden, a stored key outside options is marked Not offered; duration cells store the literal (90d), positive_integer cells a number; Save sends the object, removing every row sends null. settingsFieldRefusals reads constraint.entry/column from details.fields and SettingsView passes it to the row, which marks the entry row and the named cell (or key picker) beside the server sentence; an env-locked map is read-only. 11 console.settingsView.map* keys in all 11 locale packs (zh-Hant regenerated). Items 1-6 and 8 done; item 7 out of scope per the claim.", "tests": "ui: vitest SettingsView.mapRows-31391.test.tsx 13/13 (carries every card case: one and several columns render, picker lists served options and hides used keys, 90d literal, Save PUTs the object with a number cell, remove-all PUTs null asserted not {}, invalid_option marks the row with allowed as the comma-joined string, invalid_shape marks the cell, locked env map read-only). Ablations through ablation-replace.mjs with restore blob == HEAD b56fdaae6ae9: (1) duration sends Number(typed) -> 90d case red (maxAge 90 vs '90d'); (2) used-key filter dropped -> picker case red ([Audit log, Job run] vs [Job run]). Settings dir 40 files 343 tests green; i18n package 99 files 1804 passed 13 skipped; ui scripts 170 files passed at ba2bb379; turbo type-check console + i18n 74/74 (both cache miss); eslint touched files 0 errors. Step 1: service-settings settings-map-shape-30208.test.ts 16/16 on main cd6618e8.", "gates": "38 derived, 38 run, every exit 0 at bdc8319d (main 3d73c1d6 merged); dispatch-gates --ran: 38 run, 0 NOT-MEASURED. ui scripts suite ran at ba2bb379, merge touched no ui path. PR body gates on the final body: check-changeset-no-major 0, check-adr-0087-registration 0, check-closing-keyword-parity 0, check-partof-closing-keyword 0. Also run outside the derivation: ui check-i18n-en-drift 0; spec build + check:generated after the merge: all 14 up to date. CI check-runs: in_progress at report time.", "line_budget": "18 files, +1039 / -12 vs main (dispatch-gates: 1051 changed lines, under the 3000 threshold); no skills/** path, no governed surface", "files_changed": [ ".changeset/31391-settings-map-keyed-rows.md", "ui/apps/console/src/pages/settings/SettingsMapField.tsx (new)", "ui/apps/console/src/pages/settings/SettingsField.tsx", "ui/apps/console/src/pages/settings/SettingsView.tsx", "ui/apps/console/src/pages/settings/settingsFieldRefusals.ts", "ui/apps/console/src/pages/settings/types.ts", "ui/apps/console/src/pages/settings/__tests__/SettingsView.mapRows-31391.test.tsx (new)", "ui/packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt-BR,ru,zh-Hans,zh-Hant}.ts" ], "deviations": [ "Clause-② copied as the claim's bare `yes`; measured on built ui/packages/i18n/dist/locales/en.d.ts every new key is a readonly literal leaf (readonly mapAddRow: \"Add a row\"), i.e. the (widening) arm; the seat may amend the claim, PR body and changeset carry `yes` as claimed.", "No live run against a server: options and refusal shapes come from the door testkit on main and the fixtures copy them.", "Search placeholder is a new key (mapSearchKey) because common has no search key; empty text reuses common.noResults. 11 keys rather than the 9 named in step 1 (mapKeyNotOfferedHint and mapSearchKey added)." ], "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "3 — mcp__github__issue_read (get x1, get_comments x2), reads only", "api_writes": "2 — fleet-write pr_create (PR #31451), post-stamped os-dev-report comment on #31391", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "class: a (user-visible copy) · reach: Setup -> Data Lifecycle in zh-Hans renders the map headers Object, Keep rows for, Expire rows after, Max rows, Lifecycle class in English beside translated row labels · evidence: literals in packages/objectql/src/lifecycle/lifecycle-settings.ts keyColumn/columns labels; packages/spec/src/system/translation.zod.ts settings keys entry has no columns group, so no bundle can carry them (card item 7) · carrier: the seat files the spec card · dedupe words: map column labels translation, settings keys columns group, lifecycle map headers zh", "carrier: none · noted, not filed (read-only inference, in Acceptance notes): a lifecycle map stored as JSON text by a pre-#31241 console draws zero rows; storedJsonTextDraft seeds json keys only and these keys are map now. Not reproduced.", "carrier: none · noted, not filed (Acceptance notes): quota_defaults option labels are raw class names (audit, telemetry) from lifecycleClassKeySource; translatable through the existing options slot, no bundle does." ] }objectstack-fleet commentedon Nov 8, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jVEjiYrK7onirxUGyo
ACCEPT → PR #31451 comment 6719503021 · 2026-11-08T08:55Z · head
bdc8319d(Setup draws themapsettings as keyed rows; 1051 lines, not governed,Fixes #31391). Declaration review PASS 6719363110. Ready and auto-merge armed; landing through the merge queue. Item 7 is #31453.objectstack-fleet commentedon Nov 8, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jVEjiYrK7onirxUGyo
Landed · 2026-11-08T09:23Z
f88ca70ed635fd95cf8017972e970b98a56df895onmaincompletedby the PR'sFixes; labelsenhancement,priority:p2,domain:ui(pm:dispatchedremoved)Delivered: Setup draws a
mapsetting (Data Lifecycle's retention overrides, row quotas and per-class quota defaults) as a table of keyed rows instead of "Unsupported specifier type: map". Remaining: item 7, translated column headers, is #31453 (spec layer first).