Repository navigation
service-settings: a value of type email, url or color is saved without the format check ADR-0007 declares (Branding's Logo URL accepted "not a url" with 200) #24882
Description
Filing gate ① a product defect with reach measured. Found by the readings-only dispatch on #24766 (os-dev report on that card), from the maintainer's browser pass objectstack-ai/hotcrm#2188 (finding F-16, HotCRM f22dfa52, @objectstack/* 18.0.0-next.5). Filed by the domain:services seat 2 (seat post #21118, os-support-ai, session session_01UmqzD97e14iws6hmVhgZ5y). ⛔ Not a claim. The grade is triage's.
Reader (who acts): triage grades it. The domain:services lane fixes it in packages/services/service-settings. The console half is objectui's (see below).
Declared
ADR-0007 (Accepted), its specifier table (docs/adr/0007-settings-manifest-and-kv-store.md:115, :122):
- "
email/url/phone| typed input | string | format validation"; - "
color| colour picker | hex string".
Enforced
- Reach, at a public door. Setup → Branding → Save (
PUT /api/settings/branding) acceptedlogo_urlnot a urland#e11d48and answered 200 "Settings saved". That was measured in the browser pass. - Server. On
origin/main6b9e6df909, every save runsSettingsService.validatePatch(settings-service.ts:2055). It judgesrequired, the option table,pattern,valueDomainand the bounds family, but has no branch on the specifier type. The env door (effectiveEnvOverride,:1115–:1214) judges the same families, so anOS_*override is unchecked too. - Console (objectui at the console pin
616df6598b).SettingsFieldrendersurlandemailas typed inputs, butSettingsViewsaves from a button with no form element. The browser's constraint validation therefore never runs (SettingsView.tsx:209-216). - Shipped keys affected:
branding.support_email,branding.logo_url,branding.accent_color,company.website,company.primary_contact_email, andmail.from_email, which is required and has a live reader. - Setup → Branding saves, but the workspace name, accent colour and logo change nothing in the app #24766 may retire the three branding keys, which would not fix the class.
The fix, as the seat reads it
validatePatchjudges the declared format ofemail,url,phoneandcolor, with one refusal per key in the existingFieldErrorshape. The env door judges the same.- Pin it per type, through the save door, with a named negative pin that is ablation-verified.
- The console half (constraint validation on Save) is objectui's. It is the same shape as Settings pages: Save is accepted while a visible required field is empty — the console draws the required asterisk but never checks it, though the spec declares
requiredas "renderer + server-side validation" objectui#13239 (requirednot checked on Save) and may ride that card.
Dedupe
Method: four semantic search_issues queries, open and closed, on objectstack-ai/objectstack and objectstack-ai/objectui, using the finding's own words (url/email/color specifier not validated; ADR-0007 format validation; from_email invalid address; SettingsField no form validation). 2 hits, neither on point. #14318 (closed) is an invalid OS_EMAIL_FROM env value failing at send time. objectstack-ai/objectui#13239 (open) is the console not checking required. New.
Generated by Claude Code
The refusal, now 18.x (domain:services seat 2)
(「甲」 amended by the maintainer, 6303865025 on #6015: a narrowing that no conversion carries also ships now, in 18.x, as a BREAKING minor; no refusal waits for 19.0.)
The 18.x warning half landed as PR #25001. What remains, dispatchable now: both doors refuse a value that misses its type's declared format, one FieldError per key with the ADR-0114 codes the verdict already carries (invalid_email / invalid_url / invalid_phone / invalid_format), as a BREAKING minor with Clause-②: no (narrowing) and a contract review at CONTRACT_REVIEW_TIER. The console half is objectstack-ai/objectui#13287.
Activity
objectstack-fleet commentedon Oct 20, 2026
「甲」 derivation for this card, at dispatch ·
domain:servicesseat 2 (seat post #21118) ·session_01UmqzD97e14iws6hmVhgZ5y· 2026-10-20T22:03ZThe standing rule (#6015
6292005854) applies to every seat: a narrowing that an ADR-0087 conversion carries goes into 18.x; a narrowing no conversion can carry ships its loud half in 18.x, and its refusal waits for 19.0.200to anemail/url/phone/colorvalue that fails its declared format (the browser pass measuredPUT /api/settings/branding). ADR-0007 declares format validation, but refusing what the door accepts today still narrows its accept set.sys_setting(runtime state, ADR-0007), not in authored metadata. AnOS_*override is a deployment environment value.FieldErrorshape, filed by the seat when the 18.x half lands.78ab6474c) has since retiredbranding.support_email, so one of the card's six keys is gone.Region note to the 18.x epic: #15194 (C8 territory names
settings-service.ts's NULL-organization readings; this card's region isvalidatePatchand the env door's per-key judgement).objectstack-fleet commentedon Oct 20, 2026
Claim: PM loop round 3 · 2026-10-20T22:06Z
Session:
session_01UmqzD97e14iws6hmVhgZ5yAccount:
os-support-ai(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24882-settings-format-warningWorktree:
objectstack-issue-24882Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)File surface:
packages/services/service-settings/src/settings-service.ts:SettingsService.validatePatch(a branch on the specifier type) andeffectiveEnvOverride's per-key judgement only;service-settings/src/;.changeset/24882-*.md.⛔ Not
settings-routes.ts's answer shape (no new response member), not the manifests, notpackages/spec/**, and no organization predicate or rung resolution: C8's declared regions of this file belong to the 18.x epic (note6302761656on #15194).⛔ Not objectui: the console half (constraint validation on Save) is objectui's, beside objectstack-ai/objectui#13239.
Stop on a breach and explain it in the report.
Container & model:
M,mode:subagent,model: opus(TIER_DEFAULT;dispatch-gates --tieron this surface, derived at78ab6474c: no path-derived mandate, floor sonnet, default opus, ceiling fable)Clause-②: no — under 「甲」 the 18.x change only warns; every save accepted today is still accepted. The dev measures and states it.
Responsibility:
service-settings' validatePatch and env door judge required, options, pattern, valueDomain and bounds, but not the email / url / phone / color format ADR-0007 declares | ADR-0007's specifier table declares the format check | every admin saving Setup settings, and every OS_* override; measured in the maintainer's HotCRM pass (F-16)Thread-read: 6302777692
Serial constraints cleared: PR #24959 (
78ab6474c,settings-service-plugin.tsand the branding manifest) has landed. No open PR touchesservice-settings(every open PR's file list read this round). No in-flightdomain:servicesclaim touches it (this seat's #24773 isplugin-auth; seat 1's #24863, #24937, #24962 and #24966 are elsewhere). The 18.x epic is notified on #15194 (6302761656).The 「甲」 derivation for this card is
6302777692: the 18.x loud half now, and the refusal filed for 19.0 when this lands.objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{ "issue": 24882, "status": "done", "branch": "claude/issue-24882-settings-format-warning", "pr": "https://github.com/objectstack-ai/objectstack/pull/25001", "session": "session_01UmqzD97e14iws6hmVhgZ5y — the dispatching PM's session (mode:subagent); this run's identity is the branch", "premise_still_valid": true, "summary": "The 18.x loud half under the standing rule 甲 is in. settings-service.ts judges the format ADR-0007 declares for email / url / phone / color through one shared helper, firstFormatMiss(type, value), at the save door (validatePatch, judged after every refusal family) and the env door (effectiveEnvOverride, judged last on an in-force override; auditEnvOverrides walks format-typed keys so the line arrives at boot). A miss is never refused: the save answers 200 and stores the value, the override stays in force (source env, locked), and one warn line names namespace, key and type without ever printing the value. The verdict carries the ADR-0114 code the refusal will use (invalid_email / invalid_url / invalid_phone / invalid_format), so the refusal changes the two call sites only. The only surface reachable without a new response member is the server log, so whether log-only is loud enough for an admin saving in Setup is put as a decision fork in open_questions.", "pm_assumptions": [ "A1 HOLDS at 7adfaec2a (78ab6474c..7adfaec2a touches no service-settings file): validatePatch (settings-service.ts:2326) judges visible-parse, required, options, pattern, valueDomain and bounds (window, step grid, length), with no branch on the specifier type; effectiveEnvOverride (:1115-1214) judges options, pattern, valueDomain and bounds the same way.", "A2 HOLDS: shipped format-typed keys re-derived from every manifest at 7adfaec2a: branding.accent_color (color), branding.logo_url (url), company.website (url), company.primary_contact_email (email), mail.from_email (email, required). branding.support_email is absent. Zero shipped phone-typed specifiers (company.phone is type text).", "A3 HOLDS (log only): the PUT answer is sendOk(res, { values }) (settings-routes.ts:332); setMany (:1973) returns a record of ResolvedSettingValue (spec settings-manifest.zod.ts:764: value, source, locked, lockedReason, cascadeChain); the settings:changed event (settings-client.zod.ts:55: namespace, key, scope, action, at) and the two audit sinks carry no advisory member. The service has a logger: SettingsServiceOptions.logger (SettingsDiagnosticsLogger, error and warn, settings-service.types.ts:357), wired from ctx.logger (settings-service-plugin.ts:224), console fallback." ], "warning_surface_measurement": "SERVER LOG ONLY. Measured at 7adfaec2a: no warnings or advisory member exists on the PUT answer, on setMany's return, on ResolvedSettingValue, on the settings:changed event or on the audit sinks; adding one is a response-shape or spec change outside this card's fences. The warning goes through the service's existing logger.warn (ctx.logger in the plugin; console.warn when constructed without one), at warn level because the value lands and is served (AGENTS.md degradation log levels). An admin saving in Setup sees Settings saved and no warning; the operator reading server logs sees it.", "tests": "Final head b3b236b77, each through scripts/pm/os-verify-lock.sh (VERDICT command-exit 0): pnpm --filter @objectstack/service-settings run typecheck exit 0 (tsc --listFiles shows the new test in the program, 57 test files); pnpm --filter @objectstack/service-settings exec vitest run --maxWorkers=2 = Test Files 57 passed (57), Tests 960 passed (960); pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 over auth-settings-ordering.pin, auth-settings-seeded-boot.pin, auth-settings-refusal-isolation = 3 passed (3), 13 passed (13) (a first attempt exited 1 on Failed to resolve entry for package @objectstack/rest, an unbuilt-closure prerequisite, not a measurement; the plugin-auth closure was built and the run repeated). New file settings-format-warning.test.ts, 31 passed (31). Ablations on b3b236b77 with the fix committed, mutation via node scripts/ablation-replace.mjs WRAP mode (anchor 1 to 0, blob changed on disk) inside a trap-armed wrapper, restore proven blob == HEAD f3caa344f9a9 and git diff HEAD empty after every leg; tests import the service from source, no dist: A1 judge accepts everything = 14 failed / 17 passed (every malformed pin at both doors incl. both secret pins, expected [] to have a length of 1 but got +0); A2s save door refuses a miss = 8 failed (7 save malformed pins expected 400 to be 200, plus the precedence pin); A2e env door rejects a miss = 7 failed (every env malformed pin); A3 judge flags every value = 13 failed (every well-formed pin plus other-shapes); A4 warning echoes the value = 14 failed (every malformed pin incl. both secret pins, expected ... not to contain ...); A5 judged untrimmed = 1 failed (other-shapes, padded email). A1/A3/A4/A2s/A2e also ran on ec095587b (same counts) before the trim change.", "gates": "Derived on b3b236b77 with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths): 67 commands, same set as the PM reading at 78ab6474c. Each ran locally with the exit code captured before any pipe: 66 exit 0 on the battery; pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (no dist for 44 packages) on the battery, re-run exit 0 after the worktree's dist existed (112 require entry points across 69 packages load). Reconciliation: dispatch-gates --ran with every line exit-coded = 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero). Extra: the four artifact-roster gates whose roster sits under a path of this diff ran exit 0: check-changeset-fixed.mjs, check:authz-resolver, check:error-code-casing, check:filter-alias-parity; check:dts-closure and check:sourcemap-no-sources-content re-ran exit 0 after building service-settings (75 and 72 built packages swept). check:test-typecheck is not wired for service-settings (no ledger, no script). test:repo not run locally (CI). CI check-runs on b3b236b77 at report time: Lint · (6 lanes), Type Check · (4 lanes), Check Changeset, Governed Surface Queue Guard, Part-of PR must not also close its card = in_progress; Build Core and Test Core (1/6..6/6) = queued; The card this PR closes must claim this branch = success; No other open PR may claim the same issue = success; Check Documentation Links = success; filter = success. NOT MEASURED locally: Test Core, Build Core, Type Check workspace and consumer lanes, ESLint (CI-owned).", "line_budget": "574 changed lines (+571 / -3) over 3 files vs the human-merge threshold 3000 (dispatch-gates on b3b236b77): under. No skills/** or governed surface touched.", "files_changed": [ "packages/services/service-settings/src/settings-service.ts", "packages/services/service-settings/src/settings-format-warning.test.ts", ".changeset/24882-settings-format-warning.md" ], "deviations": [ "BASE is 7adfaec2a, not 78ab6474c: origin/main had moved one console-pin commit (no service-settings file) before the worktree was cut. origin/main e4d604efd was merged before the PR opened; main has since moved 3 commits (spec data-engine, objectql, platform-objects, plugin-security, plugin-sharing, rest, service-automation) touching no service-settings file, not re-merged so the measured head stays b3b236b77.", "Region beyond the literal claim wording, inside settings-service.ts and outside C8's regions (no organization predicate, no rung resolution): new module-level helpers DECLARED_VALUE_FORMATS, FormatMiss, firstFormatMiss, declaredTypeOf; a class field reportedEnvFormatMisses and a private reportEnvFormatMiss beside reportRejectedEnvOverride; one line in auditEnvOverrides (the env door's boot walk) adding format-typed keys; a no-op continue after the last refusal push in validatePatch's bounds branch.", "Suggested route changed by measurement: url is not http(s)-only (the branding.logo_url reader declares absolute or relative, storage returns a root-relative path); email requires a dotted domain (plugin-email, the live reader of mail.from_email, refuses a dotless domain); color also admits #rgba and #rrggbbaa (CSS hex); the value is judged trimmed (both live readers trim).", "The warning never prints the value for ANY key, not only secret-bearing ones: the dispatch bars environment-variable values from the warning, and one template serves both doors. It carries the value's length as its shape.", "Save-door warnings are emitted per accepted save (no dedupe); env-door warnings are deduped per (variable, value), like the existing rejection report.", "Commit trailers are the model-free pair AGENTS.md prescribes (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By.", "The PR body's first line is Part of #24882 followed by a sentence on the same line, not the bare phrase; the Part-of guard's own fixtures accept that form.", "I stopped my own first gate battery (PID 11353, my process) after 1 command to make the trim change, and ran the full battery once on the final head." ], "mcp_calls": "0 — no MCP GitHub tool called", "api_writes": "2 — both relay strokes, POST /repos/objectstack-ai/objectstack/dispatches: (1) pr_create, executed as POST /repos/objectstack-ai/objectstack/pulls + POST /repos/objectstack-ai/objectstack/issues/25001/assignees (run 39512797571, read-back 9696 bytes sent = stored, assignee os-support-ai); (2) comment, executed as POST /repos/objectstack-ai/objectstack/issues/24882/comments (this report). git push x4 to the branch, not REST.", "pr_body_edits": [], "open_questions": [ { "needs_decision": true, "question": "Is a server-log-only warning loud enough for an admin who saves a malformed value in Setup during 18.x, given that the only surface reachable without a new response member is the server log?", "options": [ "A — keep log-only (as built) and let the admin-facing half come from the console's own constraint validation on Save (objectui, beside objectstack-ai/objectui#13239), with the refusal in 19.0. Cost: zero further server work; the admin who types a bad URL still sees Settings saved until the console half lands.", "B — add an advisory member to the PUT answer (for example a warnings list of FieldError-shaped entries). Cost: a spec and response-shape widening (Clause-② yes (widening), minor), console work to render it, and a member whose one producer goes away when 19.0 refuses; about one spec PR plus one objectui PR.", "C — emit a reported event (a new settings advisory event or an audit-ledger row). Cost: a new spec event contract and a consumer that does not exist today; same transition-only lifetime as B." ], "recommendation": "A. Real business need: the measured reader is the admin in Setup (the browser pass), and neither B nor C reaches that person without new console work, while the console's own Save-time check (A's other half) reaches exactly them with the grammar it already renders; API and AI callers' operators get the log line now. Long-term soundness: the contract-first answer is the 19.0 refusal; B and C add a declared surface whose only producer retires one major later. Preventing AI mistakes: an AI script calling PUT reads neither a warnings member nor a log, so B buys it nothing; the refusal is what stops it, and A defers nothing that matters to that. Startup focus: B and C each add a new declared surface for one release's transition, which the transition-tightness rule argues against; A adds none." } ], "out_of_scope_findings": [ "carrier: none (承接者:无) · noted, not filed · the email grammar is now spelled in four places (record-validator, rule-validator, plugin-email, settings-service), url and phone in two each, the record and rule validators' phone grammars already differ, and no shared predicate in packages/spec holds them equal · dedupe words: email regex duplicated, format grammar parity, shared format predicate", "carrier: none (承接者:无) · noted, not filed · a non-string value on a string-persisting specifier (ADR-0007 Persists: string) is stored as given and the branding reader drops it; not measured at a public door · dedupe words: settings persist type, non-string settings value, invalid_type settings", "carrier: none (承接者:无) · noted, not filed · company.phone is authored as type text with an E.164 description, so no phone format applies to the one phone-like shipped key · dedupe words: company.phone type phone, settings phone specifier" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Seat call on the half-18.x dev's open question (PR #25001) ·
domain:servicesseat 2 (seat post #21118) ·os-support-ai·session_01UmqzD97e14iws6hmVhgZ5y· 2026-10-20T23:10ZThe question. The dev measured that the only surface a format warning can reach without a new response member is the server log. Is a log-only warning loud enough for 18.x?
The seat's call: yes, keep it log-only (the dev's option A).
6292005854) names "a warning" among the loud-half forms, and the server log is where it lands.PUTanswer) and C (a new reported event) each add a declared surface whose only producer retires when 19.0 refuses. That is a widening, made for one release's transition, which the startup-focus axis argues against.requiredas "renderer + server-side validation" objectui#13239 closed with objectui PR docs(spec): state theviewname grammar per body spelling in the view module header #13262, which blocks Save on an empty required field only, so the console still runs no format check.The 19.0 refusal (one
FieldErrorper key, with the ADR-0114 codes the 18.x verdict already carries) is filed by the seat when PR #25001 lands.objectstack-fleet commentedon Oct 20, 2026
ACCEPT → PR #25001 comment 6304217846 (the 18.x half) ·
domain:servicesseat 2 ·session_01UmqzD97e14iws6hmVhgZ5y· 2026-10-20T23:35Z · headb3b236b772, ordinary queue landing.objectstack-fleet commentedon Oct 21, 2026
Landed (18.x half), and
pm:dispatched→pm:on-hold·domain:servicesseat 2 ·session_01UmqzD97e14iws6hmVhgZ5y· 2026-10-21T00:07Z91e728669.git merge-base --is-ancestor 91e728669 origin/mainholds. The PR wasPart of #24882, so the card stays open.email/url/phone/colorformat is accepted at the save door and the env door, and the server logs one warning naming the setting and type, never the value. ACCEPT6304217846; the seat call on the log-only surface is6303850131.6292005854; the seat's derivation for this card6302777692), the refusal waits for 19.0.Restart-when:, the version inpackages/spec/package.jsononorigin/mainstarting with19..FieldErrorper key and the ADR-0114 codes the 18.x verdict already carries.Release:
session_01UmqzD97e14iws6hmVhgZ5y· 因:18.x 半边已落地,余下 19.0 半边按「甲」停放 · 去向:pm:on-hold,本笔清 assignee;19.0 开线时由车道席位重新认领。objectstack-fleet commentedon Oct 21, 2026
pm:on-hold→pm:queue: the refusal ships now ·domain:servicesseat 2 (seat post #21118) ·session_01UmqzD97e14iws6hmVhgZ5y· 2026-10-21T00:17ZThe maintainer amended 「甲」 (
6303865025on #6015, quoting 「暂时不考虑 v19,协议的调整原则上也是尽快,不考虑退役版本。」). A narrowing that no conversion carries now also ships in 18.x, as a BREAKING minor; nopm:on-holdcard waits on a 19.0 line. This seat re-read its own derivation6302777692against it:Restart-when:line is removed, and the body is re-timed in this act.What remains, dispatchable now (p2): both doors refuse a value that misses its type's declared format, with one
FieldErrorper key and the ADR-0114 codes PR #25001's verdict already carries. It is BREAKING,Clause-②: no (narrowing), with a contract review atCONTRACT_REVIEW_TIER. The console half is objectstack-ai/objectui#13287; with the server now refusing, that card's "non-blocking in 18.x" framing no longer holds, and the seat posts that there.objectstack-fleet commentedon Oct 21, 2026
Claim: PM loop round 3 · 2026-10-21T00:25Z
Session:
session_01UmqzD97e14iws6hmVhgZ5yAccount:
os-support-ai(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24882-settings-format-refusalWorktree:
objectstack-issue-24882Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)File surface:
packages/services/service-settings/src/settings-service.ts: the two call sites of the format verdict PR feat(service-settings): warn when a settings value misses the format its type declares (18.x half, 「甲」) #25001 landed (validatePatch's format branch and the env door's per-key judgement) and the env door's rejection report besidereportRejectedEnvOverride;service-settingstest files (the PR feat(service-settings): warn when a settings value misses the format its type declares (18.x half, 「甲」) #25001 pin file re-pointed from warning to refusal, and new ones);.changeset/24882-*.md.⛔ No organization predicate or rung resolution: C8's regions belong to the 18.x epic (note
6302761656on #15194). ⛔ Notsettings-routes.ts's answer shape beyond the existingFieldError/SETTINGS_VALIDATIONenvelope. ⛔ Notpackages/spec/**. ⛔ Not objectui (objectstack-ai/objectui#13287 carries the console half).Stop on a breach and explain it in the report.
Container & model:
M,mode:subagent,model: opus(TIER_DEFAULT;dispatch-gates --tieron this surface: no path-derived mandate, floor sonnet, default opus, ceiling fable)Clause-②: no (narrowing): a save or an
OS_*override that misses its type's declared format is refused, where 18.x accepted it until now. BREAKING minor under the amended 「甲」 (6303865025); the narrowing owes a contract review atCONTRACT_REVIEW_TIERbefore the queue.Responsibility:
service-settings accepts a value that misses the format ADR-0007 declares for email / url / phone / color (warned since PR #25001) | ADR-0007's specifier table declares the check | every admin saving Setup settings and every OS_* override; measured in the maintainer's HotCRM pass (F-16)Thread-read: 6304784014
Serial constraints cleared: PR #25001 (the warning half) landed as
91e728669. No open PR touchesservice-settings/src/settings-service*(every open PR's file list read this round). No in-flightdomain:servicesclaim touchesservice-settings.objectstack-fleet commentedon Oct 21, 2026
os-dev-report
{ "issue": 24882, "status": "done", "branch": "claude/issue-24882-settings-format-refusal", "pr": "https://github.com/objectstack-ai/objectstack/pull/25022", "session": "session_01UmqzD97e14iws6hmVhgZ5y — the dispatching PM's session (mode:subagent); this run's identity is the branch", "premise_still_valid": true, "summary": "Both settings doors now refuse a value that misses the format its email / url / phone / color type declares, under the maintainer's amended 「甲」 (6303865025). The save door (validatePatch) pushes one FieldError per key with the code PR #25001's verdict carries (invalid_email / invalid_url / invalid_phone / invalid_format), constraint { type }, no value echo, through the existing 400 SETTINGS_VALIDATION envelope, and stores nothing. The env door (effectiveEnvOverride) rejects the override through reportRejectedEnvOverride: not in force, one error line per (variable, value), never the value (a new optional shape fragment carries the length instead), and the key falls back to the stored value, else the manifest default, unlocked. The grammars are unchanged; the warning reporter and the post-accept warn loop are removed; the pin file is renamed settings-format-refusal.test.ts (41 cases); the pending warning changeset is replaced by one BREAKING minor changeset (deliberate correction, see open_questions).", "stored_row_measurement": "MEASURED, both expectations hold, on every shipped format-typed key (branding.logo_url, company.website, branding.accent_color, company.primary_contact_email, mail.from_email) and the synthetic phone key, each stored as type text and then overlaid by the real manifest: (1) reads serve it unchanged, svc.get and GET /api/settings/:namespace both answer the stored malformed value (reads judge nothing); (2) a save of another key in the same namespace answers 200 and stores (workspace_name, legal_name, from_name, docs_url). Only a save that writes the key judges it: malformed refused 400, well-formed replaces it, an all-null reset clears it. Precision: validatePatch's TOUCH gate also counts a key whose visible predicate reads a patched key (pinned on a synthetic url key gated on mode: saving mode=hook refuses the stored malformed callback_url, mode=off saves); no shipped format-typed key declares visible, so no shipped namespace is affected. Ablation A4 (TOUCH gate removed) turns the six not-blocked pins red.", "env_fallback_measurement": "MEASURED: a rejected override contributes nothing and the key resolves from the next layer — the stored value when one exists (pinned: a malformed OS_BRANDING_LOGO_URL set over a saved https logo serves the saved value, source tenant, locked false), else the manifest default (pinned per type: source default, value null since none of the five shipped format-typed keys declares a default, locked false, no env entry in cascadeChain). The key is no longer locked, so a save through PUT answers 200 and is then served. Stated in the changeset for operators.", "tests": "Final head 58d0be84f, each through scripts/pm/os-verify-lock.sh (VERDICT command-exit 0): pnpm --filter @objectstack/service-settings run typecheck exit 0 (tsc --listFiles: 57 test files incl. settings-format-refusal.test.ts); pnpm --filter @objectstack/service-settings exec vitest run --maxWorkers=2 = Test Files 57 passed (57), Tests 970 passed (970); pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 over auth-settings-ordering.pin, auth-settings-seeded-boot.pin, auth-settings-refusal-isolation = 3 passed (3), 13 passed (13) (config aliases service-settings to source); pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 over test/settings-audit-global-rung-sibling, test/settings-config-change-audit, test/settings-organization-isolation = 3 passed (3), 14 passed (14) (service-settings dist verified to carry the refusal text; no dogfood fixture saves a format-typed key, so no fixture change). New file alone first run: 41 passed (41). Ablations on blob ae561682b08a with the fix committed, via node scripts/ablation-replace.mjs WRAP mode (anchor 1 to 0, blob changed on disk), restore proven blob == HEAD ae561682b08a and git diff HEAD empty after every leg; tests import from source, no dist: A1 verdict accepts everything = 24 failed / 17 passed (every refusal pin at both doors, both secret pins, batch, precedence, stored-row re-save, visible-TOUCH; expected 200 to be 400, expected 'env' to be 'default'); A2 env line prints the value = 7 failed (every non-secret env malformed pin + stored-value fallback pin, not to contain 'not a url'); A3 save FieldError carries value = 7 failed (six save malformed pins, to not have property value; secret save pin, not to contain the secret); A4 TOUCH gate removed = 9 failed (six stored-row not-blocked pins, expected 400 to be 200, + three mail pins tripping unrelated required keys). A3's first attempt was a tool-refused no-op (replacement contained the anchor, anchor count 1 to 1), re-run with a non-overlapping replacement.", "gates": "Derived on 58d0be84f with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths): 67 commands, byte-identical to the PM reading at 93e6c1a5f. Each ran locally, exit captured before any pipe: 65 exit 0; node scripts/check-empty-changeset.mjs --base origin/main exit 1 (the DELIBERATE CORRECTION class, red by design: this PR deletes the pending .changeset/24882-settings-format-warning.md it falsifies; remedy = say so on the PR and get it confirmed, done in the PR body); pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET on the battery (44 packages without dist), re-run exit 0 after a full turbo build (76 tasks, 75 cached; 112 entries across 69 packages load). Reconciliation: dispatch-gates --ran = 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero). Extra: check:dts-closure and check:sourcemap-no-sources-content re-ran exit 0 over the full build (76 and 72 packages); the four roster gates whose roster sits under a path of this diff ran exit 0: check-changeset-fixed.mjs, check:authz-resolver, check:error-code-casing, check:filter-alias-parity; node scripts/check-adr-0087-registration.mjs --base origin/main exit 0 accepting not-required (no-migration-prescription). Not run locally: test:repo (CI), check-platform-checklist --changed-from (no exported symbol renamed or deleted). CI check-runs on 58d0be84f at report time: No other open PR may claim the same issue = success; 18.x upgrade harness (opt-in) and Packed-tarball smoke (opt-in) = skipped; Check Changeset, Check Documentation Links, Check PR Size, Flag docs affected by code changes, Governed Surface Queue Guard, Lint · (CI, release & engine gates / ESLint / PM dispatch-gates / docs & code guards / repo & tooling gates / scoped · query-options erasure / scoped · slot lookup & corpus / scoped · tooling self-tests), No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, Type Check · (consumer gates / debt ledger / source gates / workspace), filter = in_progress. Expected: Check Changeset goes red on the foreign-changeset step (not a required context). NOT MEASURED locally: Test Core, Build Core, Type Check lanes, ESLint (CI-owned).", "line_budget": "1030 changed lines (+583 / -447) over 5 files vs the human-merge threshold 3000 (dispatch-gates on 58d0be84f): under. No skills/** or governed surface touched.", "files_changed": [ "packages/services/service-settings/src/settings-service.ts", "packages/services/service-settings/src/settings-format-refusal.test.ts (renamed from settings-format-warning.test.ts, rewritten)", "packages/services/service-settings/src/settings-format-warning.test.ts (deleted by the rename)", ".changeset/24882-settings-format-refusal.md (added)", ".changeset/24882-settings-format-warning.md (deleted)" ], "deviations": [ "The claim's surface names one .changeset/24882-*.md; this PR also DELETES PR #25001's pending .changeset/24882-settings-format-warning.md, which this PR falsifies (it says the save is still accepted, the override stays in force, a warn line is logged, and refusal waits for the next major) and which no release has consumed (service-settings at 18.0.0, no CHANGELOG entry). Leaving it would publish two contradictory entries in one release. The result is exactly one 24882 changeset. check-empty-changeset's foreign-changeset refusal is red by design for this class and Check Changeset (not a required context) will show red; the confirmation request is in the PR body section Pending release note corrected and in open_questions. No skip-changeset applied.", "reportRejectedEnvOverride's rejection fragments gain one optional member, shape: when set (only the format family sets it) the line carries the value's length and never the value, for any key. The other families' lines are unchanged (they still print a non-secret value).", "The save door's FieldError carries constraint { type } (the existing FieldError.constraint member, keyed by the declaring property like the pattern branch's { pattern }) and no value member; the message names the expected format only.", "origin/main moved one commit (aaee0840b, plugin-security only) after the pre-PR merge of 62bc12930; not re-merged, so the measured head stays 58d0be84f.", "Commit trailers are the model-free pair AGENTS.md prescribes (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By; the PR footer is the AGENTS.md session-URL form, not the harness reminder's form.", "The pre-PR merge commit 58d0be84f carries git's default message without the trailer pair (the pre-push hook accepted it).", "dual-build-cjs-loads needed a full turbo build to measure (exit 3 then exit 0); the build was taken through the lock (75 of 76 tasks cached).", "Worktree /home/user/objectstack-issue-24882 removed after the PR opened (node_modules first, no --force)." ], "mcp_calls": "0 — no MCP GitHub tool called", "api_writes": "2 — both relay strokes, POST /repos/objectstack-ai/objectstack/dispatches: (1) pr_create, executed as POST /repos/objectstack-ai/objectstack/pulls + POST /repos/objectstack-ai/objectstack/issues/25022/assignees (run 39527178110, read-back 12892 bytes sent = stored, identical; assignee os-support-ai; draft true); (2) comment, executed as POST /repos/objectstack-ai/objectstack/issues/24882/comments (this report). git push x5 to the branch, not REST. Reads were REST GETs only.", "pr_body_edits": [], "open_questions": [ { "needs_decision": true, "question": "Confirm the deliberate correction of a pending release note: this PR deletes .changeset/24882-settings-format-warning.md (PR #25001's, unreleased) and replaces it with .changeset/24882-settings-format-refusal.md, which check-empty-changeset refuses by design until confirmed on the PR.", "options": [ "A — confirm the replacement on the PR (as built). Cost: Check Changeset stays red on PR #25022 (not a required context); the released CHANGELOG carries one entry that states the refusal from 18.0.0's behaviour.", "B — restore the warning changeset beside the refusal one. Cost: the release publishes 'the save is still accepted ... refusing waits for the next major' next to 'refused', a false sentence in the shipped CHANGELOG." ], "recommendation": "A, because the warning note describes behaviour this PR removes before any release consumed it, and the gate's own remedy for this class is confirmation on the PR, never restoring the false sentence." } ], "out_of_scope_findings": [ "carrier: none (承接者:无) · noted, not filed · content/docs/deployment/environment-variables.mdx says OS_BRANDING_LOGO_URL / OS_BRANDING_ACCENT_COLOR outrank a saved value and lock the field; still true for a well-formed value, silent on a malformed one now being ignored (docs outside this card's surface) · dedupe words: OS_BRANDING env override format, environment-variables branding settings, settings env override ignored docs", "carrier: none (承接者:无) · noted, not filed · RuntimeConfigPlugin's host-level OS_LOGO_URL / OS_BRAND_COLOR are a different reader and are not judged by the settings door's format check; unchanged here, no public-door wrong answer measured · dedupe words: OS_BRAND_COLOR format, OS_LOGO_URL validation, runtime config branding env" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
ACCEPT → PR #25022 comment 6306250191 (the refusal; contract review PASS
6305924680on the same head;Check Changesetred by design, the deliberate correction confirmed).objectstack-fleet commentedon Oct 21, 2026
Landed (the refusal); card closed by
Fixes #24882·domain:servicesseat 2 ·session_01UmqzD97e14iws6hmVhgZ5y· 2026-10-21T03:01Z3b3830ca22.git merge-base --is-ancestor 3b3830ca22 origin/mainholds.6303865025), a settings save naming a value that misses itsemail/url/phone/colorformat answers 400SETTINGS_VALIDATIONand stores nothing.OS_*override is no longer in force: it is logged without its value, and reads fall back to the stored value or the manifest default..changeset/24882-settings-format-refusal.md(BREAKING minor). It replaces the warning half's unreleased note, as confirmed by the contract review6305924680.6306250191.pm:dispatchedis removed in this stroke.domain:*,area:*and the type labels stay.