Repository navigation
feat(spec): a map settings specifier; the lifecycle retention overrides and quotas use it - #31386
Conversation
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
…-4ntp5s Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2dxcqiQvzKhr5odTPe
📓 Docs Drift CheckThis PR changes 3 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 175 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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
|
|
domain:server#2 · marchtian · session_017ihL2fBJ6xsLWJsnNJGMMx Contract review (Fable) — PR #31386 @ 32b54d4Verdict: PASS Scope reviewed: Findingsnit — An empty authoritative key answer admits every key. nit — Two refusal classes are unpinned. (1) An array value: nit — Outside scope, but it contradicts the amendment: Checked and fine
|
|
domain:server#2 · marchtian · session_017ihL2fBJ6xsLWJsnNJGMMx Contract review — ACCEPTHead-sha: ACCEPT · Fable 6717755004 (PASS, nits only ⇒ no delta review) + in-seat review of the rest · 2026-11-08T05:31Z
|
Requested by 田騏 · project thread
Refs #30208
Clause-②: 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)Skills-docs: no skill teaches the specifier set.
skills/**namesSettingsManifestonce (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 anOS_*override of amapkey is judged), and the generatedcontent/docs/references/system/settings-manifest.mdx.维护者速读
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 「同意」)。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 个应该全过。其中会把拼错的对象名、坏时长、文字配额送进保存口和环境变量口,看它们被拒绝。map,由 Fable 复审定。What changes
@objectstack/spec: themapspecifier.SpecifierTypegainsmap, a value-bearing type.SpecifiergainskeyColumn({ label, description? }, the key column's header) andcolumns([{ name, label, description?, kind }],kindisdurationorpositive_integer). Amaprequires 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 storesRecord<key, value>, several storeRecord<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'soptions, and a declaredoptionstable is only the fallback.jsonstays for raw config. Name:mapis proposed (it names theRecordshape the value is; VS Code calls the same thing an object grid); the Fable review settles it.maprow 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).firstMapShapeMissjudges 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.mapShapeVerdictreads the domain through the existingoptionTableOf(a registered source's answer, else the declared table), so a source moves both doors at once. The save door (validatePatch) answers oneFieldErrorper key:invalid_type,invalid_option,unknown_fieldorinvalid_shape, withconstraint: { 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 throughreportRejectedEnvOverridewith 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 untilsealOptionSources, 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'sjsonShapeMissfor these three keys: they are no longerjson, andjsonShapeMissstays for realjsonkeys.@objectstack/objectql: the lifecycle maps.retention_overrides(columnsmaxAge,expireAfter, bothduration),quotasandquota_defaults(onepositive_integercolumn each) aremapspecifiers, defaults and scopes unchanged.lifecycle-key-options.tsserves their keys:lifecycleObjectKeySource(the objects whose lifecycle class is notrecord, exactly the set the sweep walks, read at each ask) andlifecycleClassKeySource(LifecycleClassSchemawithoutrecord).ObjectQLPluginregisters the three sources with the settings service atkernel:ready, before the manifest.loadGovernancereads 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.ar,de,es-ES,fr,ja-JP,ko,pt-BR,ru,zh-CN,zh-Hant) and in the English manifest copy (en.tshas nolifecycleblock; the manifest literal is the English text).quota_defaultsnames the four classes instead.config-resolution.mdxsays how an override of amapkey is judged, with two examples. Generated:content/docs/references/system/settings-manifest.mdx,packages/spec/authorable-surface/system.json, the strictness-ledger counts..changeset/30208-settings-map-specifier.md,minorfor@objectstack/spec,patchfor 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):mapis 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/keyColumnon another type are refused;jsonstill parses.packages/services/service-settings/src/settings-map-shape-30208.test.ts(16), over realObjectQL, the platform setting objects, the real route, the shippedlifecycleSettingsManifestand the two key sources: the manifest lists no keys and the namespace read serves them asoptions; the shapes the sweep reads are stored unchanged; a misspelt object, arecordobject, an object with no lifecycle, therecordclass and a misspelt class areinvalid_option;30 days,"1e6",0and2.5areinvalid_shape;max_ageisunknown_field; an entry naming no column isinvalid_shape; JSON text isinvalid_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 oneerrorline.packages/services/service-settings/src/settings-json-shape-31241.test.ts(8, unchanged in substance): now over a raw-config manifest withjsonkeys, 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 omitsrecord; booted through a realObjectKernel,ObjectQLPluginregisters 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: ajsonsetting 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
mapis proposed; alternatives considered werekeyed_rowsandtable. Renaming is mechanical (the enum member, the superRefine branch,declaredMapShape's type check, the manifest, the ADR row).columnsgroup in the settingskeys[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_overridescolumns are not judged against the object's declared policy. An override ofexpireAfteron an object with nottlblock 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.ui/**out of scope.skills/**lines (none needed changing) and does not namecontent/docs/protocol/kernel/config-resolution.mdx,content/docs/references/**,packages/spec/authorable-surface/system.jsonor the strictness-ledger counts; the first is the env paragraph this change extends, the rest are generated by the spec gates.claude/project-thread-4ntp5swhen fix(service-settings)!: a json setting persists a JSON object or array, and the console sends one #31279 landed, so the branch was recreated frommainatb85190a8with this work on top, thenorigin/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.mjson head32b54d4d(mutation landed, the suite red, restored blob equal to HEAD andgit diff HEADempty after each), againstsettings-map-shape-30208.test.ts(16):mapShapeVerdict'sadmitsKey→undefinedrecord/ no-lifecycle objects, therecordand misspelt class at the save door, and the misspelt object at the env doorcellFits' duration check dropsLIFECYCLE_DURATION_REGEX30 daysat the save door and at the env doornullLocal runs, all on the final head
32b54d4d(theorigin/mainmerge at2ba7a9d4included), underos-verify-lock:@objectstack/spectest 912 files / 27528 tests, exit 0;@objectstack/objectqltest 569 files / 11501 tests, exit 0;@objectstack/service-settingstest 73 files / 1397 tests and typecheck, exit 0;@objectstack/specand@objectstack/objectqltypecheck (test layer included), exit 0;pnpm lint, exit 0.Gates, on
32b54d4d:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 121 commands, each run with stdin from /dev/null after a fullpackages/**build, all:: exit 0.--ran: "121 derived, 121 run, 0 NOT-MEASURED, 0 UNRUN". The round before the merge caughtcheck:error-code-casing(field codes in the new test, now markedadr0112-ok: D6) andcheck:slot-lookupplus an eslintno-restricted-syntaxon anas anyin the kernel wiring test (removed); both green here.Dogfood, full battery on
32b54d4d: everypackages/**package built, thenpnpm --filter @objectstack/dogfood testunder 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