Skip to content

feat(spec): a map settings specifier; the lifecycle retention overrides and quotas use it - #31386

Merged
os-zhuang merged 5 commits into
mainfrom
claude/project-thread-4ntp5s
Nov 8, 2026
Merged

os-zhuang merged 5 commits into
mainfrom
claude/project-thread-4ntp5s

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by 田騏 · project thread

Refs #30208

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)
Skills-docs: no skill teaches the specifier set. skills/** names SettingsManifest once (skills/objectstack-platform/references/plugin-development.md:355) and the three lifecycle keys by name only (skills/objectstack-data/rules/lifecycle.md:137-139), never their value form, so nothing a skill teaches changes. Docs updated here: content/docs/protocol/kernel/config-resolution.mdx (how an OS_* override of a map key is judged), and the generated content/docs/references/system/settings-manifest.mdx.

BREAKING accept-set narrowing at the settings doors (@objectstack/service-settings), for lifecycle.retention_overrides, lifecycle.quotas and lifecycle.quota_defaults only. PUT /api/settings/lifecycle and OS_LIFECYCLE_* used to accept any object under them, so a misspelt object (sys_audit_logs), a quota written as text ("1e6") or zero, a window written as 30 days or a misspelt column (max_age) was stored, answered "saved", and applied nothing. The save door now refuses such a value with 400 SETTINGS_VALIDATION and one field error that names the entry and the expected form; the env door rejects such an override with one error line and the key resolves from the next layer. Every refused value is one the sweep never applied. Migration: send keys the deployment has and cells in their column's form: { "quotas": { "sys_audit_log": 1000000 } }, { "retention_overrides": { "sys_job_run": { "maxAge": "90d" } } }; for an override, OS_LIFECYCLE_QUOTAS='{"sys_audit_log":1000000}'. Stored values, env names and the cascade are unchanged. ADR-0087 disposition: not-required (no-migration-prescription), one marker, in .changeset/30208-settings-map-specifier.md.

维护者速读

  • 改了什么: 设置清单新增一种设置项类型 map("一行一个键,后面跟有类型的列")。「数据生命周期」的保留期覆盖、行数配额、按类默认配额三个设置改用它。存储的值、环境变量名和覆盖层级都不变。保存和环境变量两个入口开始检查这三个值:对象名必须是系统里真有的、声明了生命周期的对象,时长必须写成 7y 这种形式,配额必须是大于 0 的整数。ADR-0007 加了一条修订。
  • 为什么改: 以前这三个值存什么都行,对象名拼错、配额写成文字、时长写成 30 days 都会显示「已保存」,然后什么都不执行。这是 Setup: replace developer wording and hand-written JSON with fields a business administrator can use #30208 第 5 行的裁定 A(田騏 2026-11-08 02:07 「同意」)。
  • 风险与回滚: 以前存进去的错值不受影响,只有保存时碰到这个键才会检查。脚本如果写这些错值会收到 400,环境变量写错会打一条 error,不生效。控制台在 ui 卡落地前会把这三行显示成「不支持的设置项类型」,环境变量和 API 照常可用。回滚方式是 revert。
  • 先看: packages/spec/src/system/settings-manifest.zod.ts 的 map、keyColumn、columns;packages/services/service-settings/src/map-shape.ts,以及 settings-service.ts 里 mapShapeVerdict 的两处调用;packages/objectql/src/lifecycle/lifecycle-key-options.ts;docs/adr/0007-settings-manifest-and-kv-store.md 的修订。
  • 两分钟验证: cd packages/services/service-settings && pnpm exec vitest run src/settings-map-shape-30208.test.ts,16 个应该全过。其中会把拼错的对象名、坏时长、文字配额送进保存口和环境变量口,看它们被拒绝。
  • 需要你做的: 这个 PR 改了 ADR(Tier H),需要你审核并合并。类型名暂定 map,由 Fable 复审定。

What changes

  • @objectstack/spec: the map specifier. SpecifierType gains map, a value-bearing type. Specifier gains keyColumn ({ label, description? }, the key column's header) and columns ([{ name, label, description?, kind }], kind is duration or positive_integer). A map requires both, with at least one column and unique column names; any other type declaring either is refused at parse. The doc states the storage rule: one value column stores Record<key, value>, several store Record<key, { [column.name]: value }>. Column names keep the stored property's spelling (maxAge), so the type covers today's shapes with no migration. The keys a map admits are served at run time by an option source the settings service registers for the key; every manifest read serves them as the map's options, and a declared options table is only the fallback. json stays for raw config. Name: map is proposed (it names the Record shape the value is; VS Code calls the same thing an object grid); the Fable review settles it.
  • ADR-0007. Status line amended (2026-11-08, Setup: replace developer wording and hand-written JSON with fields a business administrator can use #30208); a map row in §Specifier set; json's row now says it is not for maps; a dated note under the set cites ruling record 6715997428.
  • @objectstack/service-settings: one shape judgement for both doors (map-shape.ts). firstMapShapeMiss judges a value against the declared columns and the key column's domain: not an object at all (not_a_map), a key not admitted (key), an entry that is not an object or names no column (not_an_entry), a property no column declares (unknown_column), a cell not of its kind (cell). SettingsService.mapShapeVerdict reads the domain through the existing optionTableOf (a registered source's answer, else the declared table), so a source moves both doors at once. The save door (validatePatch) answers one FieldError per key: invalid_type, invalid_option, unknown_field or invalid_shape, with constraint: { type: 'map', entry, column?, kind?, allowed? } and a sentence naming the entry and the form expected ('sys_job_run'.maxAge must be a duration such as 7y …). The env door (effectiveEnvOverride) rejects such an override through reportRejectedEnvOverride with the same sentence; the value is described by its shape, never printed. The option family skips a map key (its table names keys, not the value). A map key without a source is audited at registration; a sourced one is judged when read, and a key-domain miss is held until sealOptionSources, as a sourced option is. This subsumes fix(service-settings)!: a json setting persists a JSON object or array, and the console sends one #31279's jsonShapeMiss for these three keys: they are no longer json, and jsonShapeMiss stays for real json keys.
  • @objectstack/objectql: the lifecycle maps. retention_overrides (columns maxAge, expireAfter, both duration), quotas and quota_defaults (one positive_integer column each) are map specifiers, defaults and scopes unchanged. lifecycle-key-options.ts serves their keys: lifecycleObjectKeySource (the objects whose lifecycle class is not record, exactly the set the sweep walks, read at each ask) and lifecycleClassKeySource (LifecycleClassSchema without record). ObjectQLPlugin registers the three sources with the settings service at kernel:ready, before the manifest. loadGovernance reads the same shapes; fix(service-settings)!: a json setting persists a JSON object or array, and the console sends one #31279's legacy-text handling stays.
  • Translations. The help text of the three keys drops its JSON examples in the ten translation packs that carry them (ar, de, es-ES, fr, ja-JP, ko, pt-BR, ru, zh-CN, zh-Hant) and in the English manifest copy (en.ts has no lifecycle block; the manifest literal is the English text). quota_defaults names the four classes instead.
  • Docs. config-resolution.mdx says how an override of a map key is judged, with two examples. Generated: content/docs/references/system/settings-manifest.mdx, packages/spec/authorable-surface/system.json, the strictness-ledger counts.
  • Changeset: .changeset/30208-settings-map-specifier.md, minor for @objectstack/spec, patch for the two services, both declaration lines, the BREAKING section with its migration, and the ADR-0087 marker.

Tests

  • packages/spec/src/system/settings-manifest.test.ts (+6): map is in the closed set and an unknown type still throws; a one-column and a several-column map parse, camelCase column names kept; a map without a key, without columns, with an empty column list or without a key column is refused; a column kind outside the set and a duplicate column are refused; columns / keyColumn on another type are refused; json still parses.
  • packages/services/service-settings/src/settings-map-shape-30208.test.ts (16), over real ObjectQL, the platform setting objects, the real route, the shipped lifecycleSettingsManifest and the two key sources: the manifest lists no keys and the namespace read serves them as options; the shapes the sweep reads are stored unchanged; a misspelt object, a record object, an object with no lifecycle, the record class and a misspelt class are invalid_option; 30 days, "1e6", 0 and 2.5 are invalid_shape; max_age is unknown_field; an entry naming no column is invalid_shape; JSON text is invalid_type; a clear falls back; a stored malformed value does not block an unrelated save; env JSON text of an admitted map is in force, while a misspelt object, a bad duration and unparseable text are not, each with one error line.
  • packages/services/service-settings/src/settings-json-shape-31241.test.ts (8, unchanged in substance): now over a raw-config manifest with json keys, since no shipped manifest declares one.
  • packages/objectql/src/lifecycle/lifecycle-key-options.test.ts (6): the object source admits exactly the swept objects, reads storage objects when present, admits an object registered later, answers nothing without a registry; the class source omits record; booted through a real ObjectKernel, ObjectQLPlugin registers the three sources before the manifest, and they admit an object the stack registered.
  • packages/services/service-settings/src/__tests__/settings-door.testkit.ts: the door scaffolding the settings: a json setting saved from the console is stored as a string, so retention overrides and row quotas silently do nothing #31241 suite carried, shared by both suites.

Acceptance notes

  • Name. map is proposed; alternatives considered were keyed_rows and table. Renaming is mechanical (the enum member, the superRefine branch, declaredMapShape's type check, the manifest, the ADR row).
  • Column labels are English literals. Translating them needs a columns group in the settings keys[key] translation entry and ui's copies of that schema; that belongs with the console control (ui card body in the report), not here.
  • retention_overrides columns are not judged against the object's declared policy. An override of expireAfter on an object with no ttl block is admitted, as the sweep simply has nothing to apply it to. Narrowing per column would need the source to answer per column; not part of ruling A.
  • The console draws the three keys as "Unsupported specifier type: map" until the ui card lands. The order puts ui/** out of scope.
  • The claim's file surface names skills/** lines (none needed changing) and does not name content/docs/protocol/kernel/config-resolution.mdx, content/docs/references/**, packages/spec/authorable-surface/system.json or the strictness-ledger counts; the first is the env paragraph this change extends, the rest are generated by the spec gates.
  • Branch. The merge queue deleted claude/project-thread-4ntp5s when fix(service-settings)!: a json setting persists a JSON object or array, and the console sends one #31279 landed, so the branch was recreated from main at b85190a8 with this work on top, then origin/main (2ba7a9d4) merged in; nothing of fix(service-settings)!: a json setting persists a JSON object or array, and the console sends one #31279 is re-carried.

Verification

Ablations, each through scripts/ablation-replace.mjs on head 32b54d4d (mutation landed, the suite red, restored blob equal to HEAD and git diff HEAD empty after each), against settings-map-shape-30208.test.ts (16):

# Mutation Red
B1 a misspelt key accepted: mapShapeVerdict's admitsKey → undefined 4/16: the misspelt object, the record / no-lifecycle objects, the record and misspelt class at the save door, and the misspelt object at the env door
B2 a bad duration accepted: cellFits' duration check drops LIFECYCLE_DURATION_REGEX 2/16: 30 days at the save door and at the env door
B3 an env value bypasses the judgement: the env door's map verdict → null 3/16: the misspelt object, the bad duration and the unparseable text are in force

Local runs, all on the final head 32b54d4d (the origin/main merge at 2ba7a9d4 included), under os-verify-lock: @objectstack/spec test 912 files / 27528 tests, exit 0; @objectstack/objectql test 569 files / 11501 tests, exit 0; @objectstack/service-settings test 73 files / 1397 tests and typecheck, exit 0; @objectstack/spec and @objectstack/objectql typecheck (test layer included), exit 0; pnpm lint, exit 0.

Gates, on 32b54d4d: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 121 commands, each run with stdin from /dev/null after a full packages/** build, all :: exit 0. --ran: "121 derived, 121 run, 0 NOT-MEASURED, 0 UNRUN". The round before the merge caught check:error-code-casing (field codes in the new test, now marked adr0112-ok: D6) and check:slot-lookup plus an eslint no-restricted-syntax on an as any in the kernel wiring test (removed); both green here.

Dogfood, full battery on 32b54d4d: every packages/** package built, then pnpm --filter @objectstack/dogfood test under the lock, no shard and no path: Test Files 437 passed | 3 skipped (440), Tests 3618 passed | 41 skipped (3659), VERDICT command-exit 0.

🤖 Generated with Claude Code

SpecifierType gains map: a keyColumn header and typed value columns
(duration or positive_integer). One column stores a scalar per key,
several store an object per key. The keys a map admits come from a
runtime option source, not the manifest. ADR-0007 amended; json stays
for raw config.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2fBJ6xsLWJsnNJGMMx
One shared judgement for both doors: the value must be an object of
keyed rows, each key admitted by the key column's option source and
each cell of its column's kind. Refusals name the entry and the
expected form.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2fBJ6xsLWJsnNJGMMx
retention_overrides, quotas and quota_defaults become map specifiers
with their stored shapes unchanged. The plugin registers key sources
before the manifest: lifecycle-declared objects, and lifecycle
classes other than record.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2fBJ6xsLWJsnNJGMMx
Ten translation packs and the English manifest copy no longer teach the
three keys as hand-written JSON. config-resolution.mdx states how an
env override for a map key is judged. Changeset added.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2fBJ6xsLWJsnNJGMMx
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation protocol:system tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 8, 2026
@github-actions

github-actions Bot commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/objectql, @objectstack/service-settings, @objectstack/spec, touching 59 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json eff90af6a2a68202ef167b8ed91aadecfaed90b7.

⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json) — pages documenting those are invisible to this run
  • 9 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 64 of 227 client-bound route-ledger rows — the other 163 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 163: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 104 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 175 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json eff90af6a2a68202ef167b8ed91aadecfaed90b7 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from b915065c3d196cc31098588669d05006b99d7125 — the merge of head 32b54d4d94b4d90ab561fe62098cba565e4ad421 into base eff90af6a2a68202ef167b8ed91aadecfaed90b7, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b915065c3d196cc31098588669d05006b99d7125 && git checkout b915065c3d196cc31098588669d05006b99d7125
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin eff90af6a2a68202ef167b8ed91aadecfaed90b7 32b54d4d94b4d90ab561fe62098cba565e4ad421 && git checkout -B drift-repro eff90af6a2a68202ef167b8ed91aadecfaed90b7 && git merge --no-ff 32b54d4d94b4d90ab561fe62098cba565e4ad421

node scripts/docs-audit/affected-docs.mjs --json eff90af6a2a68202ef167b8ed91aadecfaed90b7

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs eff90af6a2a68202ef167b8ed91aadecfaed90b7 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · marchtian · session_017ihL2fBJ6xsLWJsnNJGMMx

Contract review (Fable) — PR #31386 @ 32b54d4

Verdict: PASS

Scope reviewed: packages/spec/src/system/settings-manifest.zod.ts (enum :58, column kind/column/key-column sub-schemas :84-116, keyColumn/columns slots and stored-shape doc :592-611, superRefine map block :680-720) and settings-manifest.test.ts:198-261; packages/spec/authorable-surface/system.json:1143,1153; content/docs/references/system/settings-manifest.mdx; docs/adr/0007-settings-manifest-and-kv-store.md:3,109,125-126; packages/services/service-settings/src/map-shape.ts (declaredMapShape :44-54, firstMapShapeMiss :83-112, cellFits :114-117, mapMissSentence :133-158); settings-service.ts (MAP_MISS_CODES :823-829, maps registry :928-929/:1380-1388, env audit :1453-1455, option-family skip :1525-1527, env map branch :1620-1635, mapShapeVerdict :1975-1988, servedManifest :1998-2017, save-door map branch :3606-3627, coerceEnvValue :4507); packages/objectql/src/lifecycle/lifecycle-key-options.ts:57-82 and its test :26-133; lifecycle-settings.ts:44-83; plugin.ts:1220-1233; settings-map-shape-30208.test.ts:55-281. Judged by reading only; the suite was not run here.

Findings

nit — An empty authoritative key answer admits every key. mapShapeVerdict (settings-service.ts:1984) builds admitsKey only when allowed.length > 0, so a reachable registry that declares no lifecycle object (or a source answering { options: [] }) admits any key. The stated fallback covers the source being unavailable (pinned, lifecycle-key-options.test.ts:62-64), not "answered none". This follows the option-family precedent (:1532), so it is not a contract breach, but the ruling's wording argues for fail-closed. Fix: pass admitsKey whenever optionSourceAnswer returned an answer (empty included), and keep the allowed.length > 0 gate only for the declared-table fallback. Pin: a registry of [] lifecycle objects, save { quotas: { sys_audit_log: 1 } } ⇒ invalid_option.

nit — Two refusal classes are unpinned. (1) An array value: isPlainRecord (map-shape.ts:175-179) refuses it as not_a_map, but the only not_a_map pin is JSON text. Pin save({ quotas: [] }) ⇒ invalid_type. (2) A scalar entry in a several-column map ({ sys_job_run: '1y' } ⇒ not_an_entry at :98); only the empty-object form is pinned. Low value and also unpinned: the column-name identifier regex (zod.ts:99), and the held-verdict path for a map key (:1627). Because firstMapShapeMiss stops at the first miss, a held key miss on one entry hides a cell miss on another until the seal. The reader tolerates bad cells, so that is cosmetic.

nit — Outside scope, but it contradicts the amendment: lifecycle-service.ts:1161 still says "These keys are json specifiers". One-line comment fix.

Checked and fine

  • Schema: map is a closed-enum, value-bearing member. It refuses no keyColumn, no or empty columns, duplicate column names, a kind outside duration|positive_integer, and keyColumn/columns on any other type; each is pinned. map is the right name: it matches the stored Record<key, …> and the ADR row, whereas keyed_rows describes the rendering and table collides with data tables.
  • Stored-shape rule: stated in the spec doc, the mdx, the ADR row and lifecycle-settings.ts, and enforced by firstMapShapeMiss. lifecycle-service.ts has no diff; loadGovernance reads the same shapes. Zero migration holds.
  • Pure widening: both new slots are optional, and no specifier at the merge base declares columns/keyColumn, so no existing manifest is invalidated.
  • ADR amendment: status line, a dated paragraph under "New types require an ADR amendment", the map row, and the json row annotated. It is minimal and consistent with the closed-set rule, and the escape-hatch text is untouched.
  • One judgement, both doors: save (:3606-3627) and env (:1620-1635) both call mapShapeVerdict → firstMapShapeMiss, with the domain taken from optionTableOf. There is no bypass: env text goes through coerceEnvValue, the whole-value option check is skipped for maps, sourced map keys are judged on read, and the seal re-runs effectiveEnvOverride. Codes are stable, sentences name key, entry, column and expected form, and strings are described by length only.
  • Cascade / locks / per-org: untouched, and retention_overrides stays scope: 'tenant'.
  • Option source: the domain is derived at call time (lifecycle class ≠ record, the sweep's own set). Gaining or losing lifecycle is pinned, and the source is registered before the manifest, pinned over a real kernel. The manifest declares no key list.
  • Ablations: the dev's three ablations are consistent with the pins. The positive-integer case, JSON text, an unknown column and an empty entry are pinned on the save door.
  • Known gaps: English column labels are I18nLabelSchema, so the ui card can supply translations without a spec change. Checking retention columns against each object's declared policy is outside ruling A.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · marchtian · session_017ihL2fBJ6xsLWJsnNJGMMx

Contract review — ACCEPT

Head-sha: 32b54d4d94b4d90ab561fe62098cba565e4ad421

ACCEPT · Fable 6717755004 (PASS, nits only ⇒ no delta review) + in-seat review of the rest · 2026-11-08T05:31Z

  • The contract follows ruling A (record 6715997428, 田騏 「同意」 2026-11-08 02:07Z), and it keeps all six hard constraints. map is added to SpecifierType with keyColumn/columns. Stored shapes are unchanged: a single column stores scalars, several store objects. One judgement, firstMapShapeMiss via mapShapeVerdict, serves both the save door and the env door. The key domain comes from runtime option sources (lifecycle class ≠ record), and the manifest holds no list. json stays for raw config. The JSON examples are removed from the ten packs that carried them and from the English manifest text. The ui control is a separate card (the seat files it). Fable confirms the name map.
  • Fable's nits are not blocking and are carried as a follow-up, recorded on Setup: replace developer wording and hand-written JSON with fields a business administrator can use #30208 and in the seat's handoff brief: an empty source answer should fail closed, an array value and a scalar entry need pins, and the lifecycle-service.ts:1161 comment still says json.
  • The seat's own read (outside Fable's scope) finds nothing to fix. That covers the changeset (spec minor, services patch, both declaration lines, BREAKING with migration, one ADR-0087 marker), the BREAKING note in the body, the ten translation packs, config-resolution.mdx, the strictness-ledger count (397 → 399, regenerated) and the Skills-docs: line (no skill teaches the specifier set or the keys' value form, which checks out).
  • Gates on 32b54d4d (report 6717706756): 121 derived, all exit 0, --ran 0 NOT-MEASURED. Full dogfood: 437 files passed, 3 skipped; 3618 tests passed, 41 skipped; exit 0. The spec, objectql and service-settings suites, typecheck and lint all pass. Ablations B1–B3 went red, then were restored.
  • Landing: this PR touches a governed surface (docs/adr/**), so it is Tier H and waits for the maintainer's merge; it is under 3000 lines. The seat readies it and requests review.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 8, 2026 05:32
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 8, 2026
Merged via the queue into main with commit 243eba1 Nov 8, 2026
66 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-4ntp5s branch November 8, 2026 06:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/xl tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants