Repository navigation
Description
domain:server#2 · marchtian · session_017ihL2fBJ6xsLWJsnNJGMMx
Filing gate ① a product defect with reach measured. It was found while the card thread for #30208 row 5 did phase 0 (the shape analysis, comment 6711865649) and then measured it locally. Filed by the domain:server seat 2 (seat post #20966, marchtian, session session_017ihL2fBJ6xsLWJsnNJGMMx). ⛔ Not a claim. The grade is triage's.
Reader (who acts): the domain:server seat 2 lane. The #30208 row 5 card thread (branch claude/project-thread-4ntp5s) takes it as its own PR, ahead of the row 5 shape PR and independent of that ruling. The console half (ui/apps/console) rides the same PR, because one save door spans both.
Business reason: a platform admin who sets a row quota or a retention override in Setup → Data lifecycle gets "saved" and believes the platform now enforces it. Nothing is enforced and nothing is logged: tables grow past the quota with no quota-exceeded alert, and retention overrides never apply. The admin finds out only when storage or compliance goes wrong.
Declared
ADR-0007 (Accepted), specifier table (docs/adr/0007-settings-manifest-and-kv-store.md:123): "json | code editor | object | for advanced/raw config". A json setting persists an object.
Enforced
Measured on origin/main 733efbd43f with a local vitest (deleted after the run). It used real ObjectQL on the memory driver, SysSetting/SysPlatformSetting, the real SettingsService with registerSettingsRoutes, and lifecycleSettingsManifest registered.
- Reach, at a public door. An admin holding
manage_platform_settingsranPUT /api/settings/lifecyclewith the console's own flat draft shape:{ "quotas": "{\n \"sys_audit_log\": 1000\n}", "quota_defaults": "{\n \"telemetry\": 500\n}" }. The response was 200 with no error.sys_platform_setting.valuecame back withtypeofstring, andSettingsService.getreturned a string too. - Reader.
LifecycleService.loadGovernance(packages/objectql/src/lifecycle/lifecycle-service.ts:1147-1150) takes the string as the map. At:1313,gov.quotas[obj.name] ?? gov.quotaDefaults[class]isundefined, so noquota-exceededalert fires, and nothing is thrown or logged. The control run, with the same values saved as objects, read 1000 and 500 and alerted. - Producer. The console's
jsonfield is a textarea.ui/apps/console/src/pages/settings/SettingsField.tsx:1060/1062callsonChange(e.target.value), which hands back the raw text.SettingsView.tsx:740puts it in the draft,:518callssaveSettingsNamespace(namespace, draft), andapi.ts:163PUTs it unchanged. There is noJSON.parseanywhere inpages/settings. - Door.
SettingsService.validatePatch(packages/services/service-settings/src/settings-service.ts:3131) has no type check forjson, so a string is accepted where an object is declared. - Measured scope.
quotasandquota_defaultswere measured.retention_overridesuses the same textarea and the same read path (:1147, and at:1186Object.entriesskips each entry on itstypeofguard). It was read in code, not measured. SQL drivers were not measured. - These three are the only
jsonspecifiers inpackages/.
Seam: spec:SpecifierType json (ADR-0007:123, persists object) → renderer:ui/apps/console/src/pages/settings/SettingsField.tsx:1062 → runtime:packages/objectql/src/lifecycle/lifecycle-service.ts:1149-1150,1313. service-settings validatePatch (:3131) does not refuse it.
The fix, as the seat reads it
- Console. The
jsonfield parses on save and shows a parse error in place. It never sends text for ajsonkey. - Save door.
validatePatchrefuses ajsonvalue that is not a JSON object or array (oneFieldErrorper key, in the existing shape). The env door (OS_LIFECYCLE_*) is judged the same way. - Values already stored as strings. Deployments that saved through the console hold strings today. Either the reader parses a string once, with a warning, or a one-shot migration rewrites them. The PR names which one it picks.
- Pins. A named negative pin goes through the save door and is ablation-verified, plus a lifecycle pin showing that a quota saved through the door alerts.
- Setup: replace developer wording and hand-written JSON with fields a business administrator can use #30208 row 5 may replace these textareas with a row editor. That ruling is pending with the maintainer and does not fix the class: the door and the already-stored strings stay broken.
Dedupe
Method: semantic issue search, open and closed, on objectstack-ai/objectstack, for "json specifier string", "settings json textarea", "quotas string" and "persists object". 9 distinct hits, none on point. #30208 (open) asks for business-friendly fields, not storage. #24882 (closed) is the same class for email/url/color format checks, with no json branch. #5536 (closed) is about the setting source. New.
Generated by Claude Code
Activity
objectstack-fleet commentedon Nov 7, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: audit · p2 ·
domain:server· 2026-11-07T19:56Z⛔ Not a claim.
jsonsetting saved from the console is stored as a string. ADR-0007:123 declares it persists an object.validatePatchhas nojsontype check, andLifecycleService.loadGovernancereads the string as the map, soquotas/quota_defaultsnever alert and nothing is logged. This was measured atPUT /api/settings/lifecyclewith a control.retention_overridesis the same path; that part was read from code only.domain:server. One save door spansservice-settings,objectqllifecycle and the console field, so the console half rides the same PR. Seat 2's Setup: replace developer wording and hand-written JSON with fields a business administrator can use #30208 row 5 thread takes it ahead of the row 5 shape work. The PR names how values already stored as strings are handled: a reader parse with a warning, or a one-shot migration.findingremoved,pm:queue.Generated by Claude Code
objectstack-fleet commentedon Nov 7, 2026
domain:server#2 · marchtian · session_017ihL2fBJ6xsLWJsnNJGMMx
Claim: PM loop round 1 · #31241 (a
jsonsetting saved from the console is stored as a string) · 2026-11-07T20:30Z · buildSession:
session_017ihL2fBJ6xsLWJsnNJGMMxAccount:
marchtianBranch:
claude/project-thread-4ntp5s(the card thread's harness-pinned branch, the thread that measured this defect during #30208 row 5 phase 0; re-cut from currentmain; platform exception toclaude/issue-<n>-<slug>)Worktree:
objectstack-issue-31241Domain:
domain:serverSeat:
domain:server#2File surface:
packages/services/service-settings/src/settings-service.ts(validatePatchand the env door forjson),packages/objectql/src/lifecycle/lifecycle-service.ts(loadGovernance, values already stored as strings),ui/apps/console/src/pages/settings/SettingsField.tsxandSettingsView.tsx(thejsonfield parses on save and shows a parse error), their tests, changesets. ⛔packages/spec/**,docs/adr/**,skills/**,.claude/**,lifecycle-settings.ts(row 5's shape stays with #30208).Container & model:
M,mode:thread,model: TIER_DEFAULT,effort: defaultClause-②: yes (narrowing) — the settings save door and the env door start refusing a
jsonvalue that is not a JSON object or array, which they accept today; ADR-0007:123 already declaresjsonpersists an object, so the narrowing enforces the declared contract and widens nothingMetadata-protocol: none
Responsibility: what a platform admin saves under a
jsonsetting is what the runtime enforces | thelifecyclesettings namespace (retention_overrides,quotas,quota_defaults) | Setup → Settings → Data LifecycleThread-read: 6712289382
Premise: holds. Measured by the card thread on
main733efbd43f(PUT 200, value stored asstring,loadGovernancereadsundefinedat:1313, no alert, no log); triage graded it p2domain:server(6712289382).Serial constraints cleared: #30208 row 5 phase 1 waits on the maintainer's shape ruling and will be claimed afresh; this PR does not change
lifecycle-settings.ts, so the two do not overlap.Order: project file
server-seat-2/orders/31241.md.objectstack-fleet commentedon Nov 7, 2026
domain:server#2 · marchtian · session_017ihL2dxcqiQvzKhr5odTPe
os-dev-report
Written 2026-11-07T22:01Z.
{ "issue": 31241, "status": "done", "branch": "claude/project-thread-4ntp5s", "pr": "https://github.com/objectstack-ai/objectstack/pull/31279", "head": "e4d9a02a4d3f1b86ad854711117921c32e90cb41", "session": "session_017ihL2dxcqiQvzKhr5odTPe", "premise_still_valid": true, "summary": "A json setting persists a JSON object or array at both settings doors, and the console sends one. service-settings: one shape judgment (jsonShapeMiss) refuses a string, number or boolean under a json key at validatePatch (one invalid_type FieldError per key, constraint {type:'json'}, nothing stored; null is a clear; only touched keys are judged) and at the env door (reportRejectedEnvOverride, one error line, the key resolves from the next layer). objectql: LifecycleService.governanceMap reads retention_overrides / quotas / quota_defaults; text that parses to a plain object is applied with one warn per stored value naming the key (and organization), anything else applies nothing and is said once at error. Choice for values already stored as text: a reader repair, not a migration (these are the only json specifiers in packages/ and LifecycleService is their only reader; enforcement starts on the first sweep after the upgrade, the settings read API is unchanged). console: SettingsView parses each json key in the draft at Save (object/array sent parsed, blank text sent as null, unparseable text blocks Save on that row with the existing fields.object.invalidJson sentence). Docs: config-resolution.mdx states the json exception to 'the raw string is used'. retention_overrides measured this time at global and tenant scope through a sweep.", "tests": "Under os-verify-lock. New: service-settings src/settings-json-shape-31241.test.ts 8/8 (real ObjectQL + SysSetting/SysPlatformSetting + real route + shipped lifecycleSettingsManifest); objectql src/lifecycle/lifecycle-service.governance-map-text.test.ts 6/6; console __tests__/SettingsView.jsonSendsObject-31241.test.tsx 4/4. Packages before the main merge: @objectstack/service-settings test 72 files / 1379 tests + typecheck, exit 0; @objectstack/objectql test 565 files / 11431 tests exit 0 and typecheck exit 0; console pages/settings 39 files / 317 tests. On the final head e4d9a02a: the three new files, objectql src/lifecycle 6 files / 150, console pages/settings 39 / 317, all green. Console type-check: NOT MEASURED locally, reason: needs every ui package built; CI Type Check UI carries it (tsc --noEmit shows no error in the changed files beyond the unbuilt-module family).", "ablations": [ "A1 validatePatch json check -> null: 2/8 red ('expected 200 to be 400' on the old console body and on the scalar case)", "A2 env door json check -> null: 2/8 red (expected '{\"sys_audit_log\": 1000' to deeply equal {}; expected 5 to deeply equal {})", "A3 governanceMap isPlainRecord(parsed) -> false: 4/6 red (both quota-text cases, global and tenant retention-text cases)", "A4 loadGovernance bypasses governanceMap: 5/6 red, the error-once case included (called 0 times)", "A5 console sends draft instead of outgoingSettingsPatch: 3/4 red (text sent for object, array and stored-not-in-draft cases)", "A6 console parse refusal disabled: 1/4 red (expected null to be truthy: no alert on the row)", "each through scripts/ablation-replace.mjs on head 4246cad1: mutation landed (anchor 1->0), restored blob == HEAD and git diff HEAD empty after each; the restored suites are green" ], "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on the final head e4d9a02a: 100 commands, each with stdin from /dev/null, all ':: exit 0'; --ran: '100 derived, 100 run, 0 NOT-MEASURED, 0 UNRUN'. Round 1 (68 commands, before the docs line and the merge) caught check:logger-receiver-detach on governanceMap's error line, fixed in 748e6b27; its check:dts-closure red was a closure build running alongside (green on re-run) and check:dual-build-cjs-loads exit 3 (no dist), green on the final head; check-plugin-teardown-shape --self-test exit 3 (shallow fixture commit), green after fetching it. pnpm lint exit 0; ui eslint on the two changed console files 0 errors.", "dogfood": "full battery on the final head e4d9a02a: pnpm --filter '@objectstack/dogfood^...' build, then pnpm --filter @objectstack/dogfood test under the lock, no shard, no path: Test Files 435 passed | 3 skipped (438), Tests 3609 passed | 41 skipped (3650), VERDICT command-exit 0", "dogfood_battery": "435 passed | 3 skipped files; 3609 passed | 41 skipped tests; exit 0", "line_budget": "8 files, +842 / -5 vs main; no generated file; under the 3,000-line human-approval line", "files_changed": [ ".changeset/31241-json-setting-persists-object.md", "content/docs/protocol/kernel/config-resolution.mdx", "packages/objectql/src/lifecycle/lifecycle-service.ts", "packages/objectql/src/lifecycle/lifecycle-service.governance-map-text.test.ts", "packages/services/service-settings/src/settings-service.ts", "packages/services/service-settings/src/settings-json-shape-31241.test.ts", "ui/apps/console/src/pages/settings/SettingsView.tsx", "ui/apps/console/src/pages/settings/__tests__/SettingsView.jsonSendsObject-31241.test.tsx" ], "deviations": [ "File surface: content/docs/protocol/kernel/config-resolution.mdx is not in the claim; its env paragraph ('the raw string is used') became false for a json key with this change, so it is fixed in the same PR. The seat amends the claim.", "SettingsField.tsx (named in the claim) is unchanged: the parse lives in SettingsView's save path, beside the existing pre-save checks.", "PR body: the ADR-0087 marker is in the changeset (the gate reads it there); the body states the disposition in prose because os-dev forbids HTML comments in GitHub text other than the attribution line.", "PR body line 2 links this card thread (the harness attribution block's own link), not the seat thread the order spelled; the harness block requires its link verbatim.", "The harness's assignee/reviewer step after PR creation was skipped: the order forbids reviewers/assignees and MCP writes; the PR was opened only through the fleet-write relay, draft, no assignees.", "A JSON value that parses but is not an object or array is not blocked by the console; it is sent parsed and the server refuses it inline in English (blocking it would need a new key in eleven ui packs)." ], "mcp_calls": "0", "api_writes": "2 — fleet-write relay pr_create (request fw-20261107T215953Z-8c4e66, run 42418476549, read-back 10927 bytes sent = stored, assignees none) -> #31279; this comment via scripts/pm/post-stamped.mjs. Plus own git pushes.", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Nov 7, 2026
domain:server#2 · marchtian · session_017ihL2fBJ6xsLWJsnNJGMMx
Claim amendment · #31241 · 2026-11-07T23:12Z · amends claim 6712649632
File surface (added):
content/docs/protocol/kernel/config-resolution.mdx(its env-override paragraph became false forjsonkeys with this change) andui/packages/i18n/src/locales/*.ts(11 packs, one new keyconsole.settingsView.jsonStoredAsText, seat review round 2 6713945240: the console seeds ajsonvalue stored as text into the draft so one Save repairs it).Everything else in claim 6712649632 stands.
objectstack-fleet commentedon Nov 8, 2026
domain:server#2 · marchtian · session_017ihL2fBJ6xsLWJsnNJGMMx
Landed · 2026-11-08T02:27Z
PR #31279 merged 2026-11-08T02:24Z through the merge queue as
6ee8c61d77, and this card closed with it.jsonsetting now saves the parsed object, not its text. A stored text value is seeded into the draft at load, with a note, so one Save repairs it.invalid_type).quotas/quota_defaultstext is parsed and applied with a warning.retention_overridestext is not applied, because the reaper deletes data; one error per value names it.The three lifecycle keys move to the typed
mapspecifier under #30208 row 5's ruling A (phase 1, in flight on the same card thread). That shape judgement subsumes this one for those keys.