Skip to content

fix(service-settings)!: a settings value that misses its type's declared format is refused at the save door and the env door - #25022

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-24882-settings-format-refusal
Oct 21, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-24882-settings-format-refusal

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24882

Clause-②: no (narrowing)

This PR lands the card's last half: both settings doors now refuse a value that misses the format its type declares. PR #25001 (91e728669) landed the warning half. The console half is objectstack-ai/objectui#13287.

What changes

ADR-0007 declares a format for four specifier types: email, url and phone take "format validation", and color takes a hex string. PR #25001 added one shared verdict, firstFormatMiss(type, value), which already carries the ADR-0114 code for each type. This PR changes its two call sites in packages/services/service-settings/src/settings-service.ts from warning to refusal. The grammars are unchanged.

  • The save door (validatePatch). A miss pushes one FieldError for the key, with the verdict's code (invalid_email, invalid_url, invalid_phone, or invalid_format for color), and continues like every other family. The route answers through the existing SETTINGS_VALIDATION envelope: 400, with the entry in error.details.fields. Nothing in the save is stored. The format is still judged last, so a value that also breaks another family (a length bound, a pattern) is refused for that family.
    • The message names the expected format, for example Logo URL must be a well-formed URL (scheme://…, a root-relative /path, a data: URI or a blob: URL). It never echoes the value, so the entry carries no value member.
    • constraint is { type: 'url' } and so on. This is the existing FieldError.constraint member, keyed by the declaring property as the pattern branch's { pattern } is. It lets a client tell a colour's invalid_format from a declared pattern's.
  • The env door (effectiveEnvOverride). A miss is rejected through reportRejectedEnvOverride, the same reporter every other family uses. The override is not in force, the line is logged once per (variable, value) at error, and the key is no longer locked. The reporter gains one optional fragment, shape. When it is set, the line carries the value's length and never the value, for any key, not only an encrypted one. auditEnvOverrides still walks format-typed keys, so the line arrives at boot.
  • Removed: the warning reporter reportEnvFormatMiss, its dedupe set, the save door's formatMisses list and its post-accept warn loop.

No response member is added, and nothing outside service-settings changes. No organization predicate or rung resolution is touched.

「甲」 reading (amended)

The standing rule 「甲」 (#6015, comment 6292005854), as the maintainer amended it (comment 6303865025, quoting 「暂时不考虑 v19,协议的调整原则上也是尽快,不考虑退役版本。」):

Rule 2 changes: a narrowing that no conversion carries ALSO ships now, in 18.x, as a BREAKING minor (the #24813 changeset shape); no refusal is filed for 19.0, and no pm:on-hold card waits on a 19.0 line. The warning half alone is no longer the 18.x deliverable.

The seat's derivation for this card (comment 6302777692) still holds on its facts:

  • Refusing these values narrows what the settings door accepts.
  • No ADR-0087 conversion can carry it. Stored values are runtime state in sys_setting, and an OS_* override is a deployment's environment.

Under the amended rule 2, that narrowing ships now. The seat re-timed the card on that reading (comment 6304784014). So this PR is a BREAKING minor:

  • the changeset declares Clause-②: no (narrowing);
  • its ADR-0087 disposition is not-required (no-migration-prescription), accepted by check-adr-0087-registration --base origin/main;
  • the narrowing owes a contract review at CONTRACT_REVIEW_TIER before the queue (claim comment 6304887064).

Measured first

Stored rows. A value already stored that misses its format was measured on every shipped format-typed key and on the synthetic phone key. Each was stored as text, then the real manifest was registered over it, the method the option-table and value-domain TOUCH pins use.

  • Reads serve it. get() and GET /api/settings/:namespace return it unchanged. Reads judge nothing. This was expected.
  • A save of another key in the same namespace is not blocked. For example, branding.workspace_name saves over a stored malformed logo_url, and mail.from_name saves over a stored no-reply@localhost. This was expected: the save door judges only the keys a save touches.
  • Only a save that writes the key judges it. The malformed value is refused, a well-formed one replaces it, and an all-null reset clears it.
  • One precision on "touches". validatePatch's TOUCH gate also counts a key whose visible predicate reads a patched key. A synthetic url key gated on mode is judged when mode is saved and it stays visible. That rule is the same for every family. No shipped format-typed key declares visible, so no shipped namespace is affected. It is pinned so the reading is exact.

The env door's fallback. A rejected override contributes nothing, so the key resolves from the next layer: the stored value when one exists, else the manifest default. None of the five shipped format-typed keys declares a default, so the default is null. Reads report that layer as the source, and the key is no longer locked. Both legs are pinned. The changeset states this for operators.

Pending release note corrected

This PR deletes .changeset/24882-settings-format-warning.md, PR #25001's changeset, and replaces it with .changeset/24882-settings-format-refusal.md.

  • What it said. The pending note says a malformed save "is still accepted", that an override "stays in force", that a warn line is logged, and that refusal "waits for the next major version".
  • Why it is now false. This PR changes every one of those statements. No release has consumed the note: @objectstack/service-settings is at 18.0.0, and its CHANGELOG.md has no entry for it.
  • Why it is replaced, not left beside the new one. Keeping it would publish two contradictory entries in one release.

This is the DELIBERATE CORRECTION class that check-empty-changeset.mjs names. Its foreign-changeset refusal is red by design here, and Check Changeset will show red. The remedy that gate prescribes is this paragraph: the note and what changed under it, put on the PR for confirmation. The note is not restored, and no skip-changeset is applied.

Tests

The pin file is renamed: settings-format-warning.test.ts becomes settings-format-refusal.test.ts, with 41 cases.

  • The save door is driven through the real route (registerSettingsRoutes over a real SettingsService). Status starts at 0, so every status is asserted, not defaulted.
  • The env door is driven through get, getNamespace, the GET route and the PUT lock.

What the pins hold:

  • Save door, per type. Every shipped key with a format type (branding.logo_url, company.website, branding.accent_color, company.primary_contact_email, mail.from_email) and a synthetic phone key are covered.
    • A malformed value answers 400 SETTINGS_VALIDATION with exactly one field error: field, ADR-0114 code, constraint.type, no value, and it parses under FieldErrorSchema. Nothing is stored, and no log line is written.
    • A well-formed value saves.
  • Save door, other cases.
    • The other shapes each grammar admits still save, and empty and blank values are not judged.
    • A secret-bearing key's answer carries neither the value nor any fragment of it.
    • Precedence: a value that breaks a length bound is refused for the bound. A batch with one malformed key is refused whole.
  • Env door, per type.
    • A malformed override is not in force: source default, locked: false, no env entry in the cascade chain.
    • It is reported exactly once, at boot, and re-reads do not repeat it. The line never carries the value.
    • A save of the key then succeeds and is served from the stored value.
    • A well-formed override is in force and locks the key with 409.
  • Env door, other cases. A malformed override set over a stored value leaves the stored value answering. A secret-bearing key's line carries no fragment of the value. An override that breaks a length bound is rejected for the bound.
  • Stored rows. The measurements above, per type, plus the reset and the visible-TOUCH pin.

Readings on the final head 58d0be84f, each through scripts/pm/os-verify-lock.sh (VERDICT command-exit 0):

  • pnpm --filter @objectstack/service-settings run typecheck: exit 0. The program includes the new test (tsc --listFiles, 57 test files).
  • pnpm --filter @objectstack/service-settings exec vitest run --maxWorkers=2: 57 files, 970 tests passed.
  • pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 over auth-settings-ordering.pin, auth-settings-seeded-boot.pin and auth-settings-refusal-isolation: 3 files, 13 tests passed. Their 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 and test/settings-organization-isolation: 3 files, 14 tests passed. The rebuilt service-settings dist carries the refusal. No dogfood fixture saves a format-typed key.

Ablations

Each leg ran with the implementation committed, on the file blob ae561682b08a. The mutation went through node scripts/ablation-replace.mjs in WRAP mode, which proves the anchor fell 1 to 0 and the blob changed on disk. The restore after every leg is proven: blob == HEAD (ae561682b08a) and an empty git diff HEAD. The tests import the service from source, so no dist/ is involved.

leg mutation result
A1 the verdict accepts everything 24 of 41 red: every refusal pin at both doors, both secret pins, the batch and precedence pins, the re-save step of every stored-row pin and the visible-TOUCH pin (expected 200 to be 400, expected 'env' to be 'default'). The 17 green are the well-formed, other-shape, empty, reset, fixture and env-precedence pins.
A2 the env line prints the value (the shape branch disabled) 7 red: every non-secret env malformed pin and the stored-value fallback pin (expected '… env override …' not to contain 'not a url'). The secret pin stays green because the encrypted-key redaction still applies, as expected.
A3 the save door's field error carries value 7 red: the six save malformed pins (to not have property "value") and the secret save pin (not to contain 'billing-desk-at-corp-internal').
A4 the TOUCH gate removed 9 red: the six stored-row "not blocked" pins (expected 400 to be 200) and three mail pins that then trip unrelated required keys. This shows the stored-row pins can fail.

A3's first attempt was a no-op that the tool refused: the replacement contained the anchor, so the anchor count did not fall. It was re-run with a non-overlapping replacement. The table records only the leg that landed.

Gates

Gate commands were derived from this worktree with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) on 58d0be84f: 67 commands, the same set as the PM's reading at 93e6c1a5f. Each ran locally with its exit code captured before any pipe.

  • --ran reconciliation: 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN.
  • 65 exited 0.
  • node scripts/check-empty-changeset.mjs --base origin/main exited 1. This is the designed red for the deliberate correction above.
  • pnpm check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET, no dist) on the battery. It was re-run with exit 0 after a full turbo build (112 entries across 69 packages load).
  • check:dts-closure and check:sourcemap-no-sources-content re-ran with exit 0 over the full build (76 and 72 packages).
  • The roster gates whose roster sits under a path of this diff ran with exit 0: check-changeset-fixed.mjs, check:authz-resolver, check:error-code-casing and check:filter-alias-parity.

CI owns the families outside that list.

Acceptance notes

  • content/docs/deployment/environment-variables.mdx says the settings layer's OS_BRANDING_LOGO_URL / OS_BRANDING_ACCENT_COLOR outrank a saved value and lock the field. That stays true for a well-formed value. The page does not say a malformed one is now ignored. Docs are outside this card's file surface. Owner: none.
  • The host-level OS_LOGO_URL / OS_BRAND_COLOR that RuntimeConfigPlugin reads are a different reader and are not judged by the settings door. This is unchanged here. Owner: none.
  • origin/main moved one commit after the merge (aaee0840b, plugin-security only). It was not re-merged, so the measured head stays 58d0be84f.

Generated by Claude Code

…at its type declares, at both doors

The save door answers one FieldError per key with the code the format verdict
carries (invalid_email / invalid_url / invalid_phone / invalid_format) through
the existing SETTINGS_VALIDATION envelope, and stores nothing. The env door
rejects a malformed OS_* override through reportRejectedEnvOverride: not in
force, reported once, never printing the value. The pin file is re-pointed
from warning to refusal.

Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y
Co-authored-by: Claude <noreply@anthropic.com>
…ngeset for service-settings

The warning changeset described behaviour this branch changes before any
release consumed it, so it is replaced by one BREAKING minor changeset that
states the save door and the env door refusal, the env fallback and what an
operator does.

Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-settings, touching 7 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx (via SettingsService (symbol, a top-level class))
  • content/docs/protocol/kernel/config-resolution.mdx (via SettingsService (symbol, a top-level class))
  • content/docs/protocol/kernel/index.mdx (via SettingsService (symbol, a top-level class))
What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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 — 9 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 05b28c199b7408da7ca4bc3988635fa9342cd6de → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6560b87021b940c8463f6424743c5db541508571 — the merge of head 58d0be84ffd522783c97313570a02e0971989ee9 into base 05b28c199b7408da7ca4bc3988635fa9342cd6de, 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 6560b87021b940c8463f6424743c5db541508571 && git checkout 6560b87021b940c8463f6424743c5db541508571
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 05b28c199b7408da7ca4bc3988635fa9342cd6de 58d0be84ffd522783c97313570a02e0971989ee9 && git checkout -B drift-repro 05b28c199b7408da7ca4bc3988635fa9342cd6de && git merge --no-ff 58d0be84ffd522783c97313570a02e0971989ee9

node scripts/docs-audit/affected-docs.mjs --json 05b28c199b7408da7ca4bc3988635fa9342cd6de

⚠️ 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 05b28c199b7408da7ca4bc3988635fa9342cd6de → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 58d0be84ffd522783c97313570a02e0971989ee9
Local-runs: none
Scope: packages/services/service-settings/src/settings-service.ts, packages/services/service-settings/src/settings-format-refusal.test.ts, packages/services/service-settings/src/settings-format-warning.test.ts + packages/services/service-settings/src/settings-routes.ts, packages/services/service-settings/src/settings-service.types.ts, packages/spec/src/api/errors.zod.ts, packages/services/service-settings/src/manifests/branding.manifest.ts, packages/services/service-settings/src/manifests/company.manifest.ts, packages/services/service-settings/src/manifests/mail.manifest.ts, packages/cloud-connection/src/runtime-config-plugin.ts, scripts/check-empty-changeset.mjs, .claude/skills/pm-dispatch/references/landing-operations.md, and the two prose faces .changeset/24882-settings-format-refusal.md and .changeset/24882-settings-format-warning.md (judged sentence by sentence in ①)

Inputs: card #24882 (body and all nine comments), PR #25022 (body, file list, the net diff against main at the head above, five paths), and the check-runs on that head. Read-only: the cited code was read with git show at the head and at origin/main; nothing was built, run or re-run.

① Derived judgments

Every accept-set and public-surface change the diff implies, each named right or wrong.

  • J1 — the save door narrows (right). validatePatch now pushes one FieldError for a non-empty string value that misses the format its specifier type declares, with code from the shared verdict (invalid_email / invalid_url / invalid_phone, and invalid_format for color), message of the form LABEL must be EXPECTED., label, and constraint: { type }, then continues; SettingsValidationError is thrown before any write, and settings-routes.ts maps it to 400 SETTINGS_VALIDATION with the array in error.details.fields. So PUT /api/settings/:namespace and setMany refuse what they answered 200 to on main. Reach, re-derived at the head over every manifest in the tree: branding.accent_color (color), branding.logo_url (url), company.website (url), company.primary_contact_email (email) and mail.from_email (email, required). The other type: 'url' spellings in the tree (settings-service-plugin.ts, datasource-admin-plugin.ts, platform-objects nav) are Setup navigation items, not specifiers; no shipped manifest authors a phone specifier (company.phone is text). The PR and the note list exactly these five keys.
  • J2 — family order kept (right). The format branch is the last branch of the specifier loop in validatePatch and the last check in effectiveEnvOverride, after required, options, pattern, valueDomain and bounds on the one door and options, pattern, valueDomain and bounds on the other. Every earlier branch continues or returns once it has spoken, so a value that also breaks another declared constraint is refused for that one, and the errors a patch that was already refused on main carries are unchanged. The pattern branch's invalid_format carries constraint.pattern; the colour branch's carries constraint.type, so the two are distinguishable without parsing the sentence.
  • J3 — stored rows and the TOUCH gate (right). validatePatch returns early on an all-null patch and judges a specifier only when the patch names its key or a key its visible predicate reads, so a row stored before this check is still served by get, getNamespace and the GET route and does not block a save of the namespace's other keys; it is refused only by a patch that writes it, or by a save of a key its visible reads. The pin file holds all three legs per type, the reset leg and the visible-TOUCH leg on a synthetic key; no shipped format-typed key declares visible.
  • J4 — the env door narrows (right). A malformed OS_* override is now handed to reportRejectedEnvOverride and effectiveEnvOverride returns null, so the override contributes nothing, locks nothing, and the key resolves from the stored value, else the manifest default, with that layer reported as the source. The line is error, deduped per ENVVAR=value in memory for the life of the service, and arrives at boot because auditEnvOverrides walks format-typed keys by type (DECLARED_VALUE_FORMATS.has(declaredTypeOf(...)), unchanged from PR feat(service-settings): warn when a settings value misses the format its type declares (18.x half, 「甲」) #25001), else on the first read. envKeyOf spells the variable OS_ plus namespace and key upper-cased, as the pins assume.
  • J5 — no echo on either refusal path (right). Save door: message interpolates only label (manifest text) and miss.expected (a per-type constant naming no part of the value); no value member and no Received fragment is attached; SettingsValidationError.message concatenates field and message only, and the route forwards err.message and fields and nothing else. Env door: with shape set the reporter's rejected fragment is The rejected value is a string of N characters; it is not printed. for every key, encrypted or not; what, detail and fix are built from formatMiss.type and formatMiss.expected only; the value appears solely in the in-memory dedupeAt key. The other families' lines are untouched and still print a non-secret value, which is main's behaviour and outside this card. Pinned at both doors, including a secret-bearing key and fragment checks.
  • J6 — wire shape stays inside the published schema (right). invalid_email, invalid_url, invalid_phone and invalid_format are members of the closed FieldErrorCode catalog in packages/spec/src/api/errors.zod.ts; FieldError.constraint is an open record, so { type: 'url' } parses (the pins run FieldErrorSchema.safeParse on the entry), and value is optional. constraint.type is a new key on this wire, spelled by the declaring manifest property as { pattern }, { valueDomain }, { visible } and { allowed } are. No response member is added and no spec file changes.
  • J7 — the warning surface is removed (right). The save door's formatMisses list and post-accept warn loop, the env door's reportEnvFormatMiss reporter and its reportedEnvFormatMisses set are deleted; no warn line remains on either door for this family. Log lines are not a contract surface; the new note states the change.
  • J8 — no export changes (right). The diff adds, removes or re-types no exported declaration in settings-service.ts; reportRejectedEnvOverride's optional shape fragment is on a private method. Nothing outside service-settings changes.
  • J9 — grammars unchanged (right), one precision. The four regexes in DECLARED_VALUE_FORMATS are byte-identical to main; only the doc comment changes. The url grammar admits scheme://, a root-relative /path, data: and blob:; a document-relative spelling (no leading slash) and a scheme without // are refused as invalid_url. The shipped readers sit inside the grammar (the manifest's example is https://…/logo.svg; storage hands back a root-relative path). The "absolute or relative URL" wording lives on RuntimeConfigPlugin.logoUrl in runtime-config-plugin.ts, the host-level OS_LOGO_URL reader, which this door does not judge (the PR says so).
  • J10 — the pin file (right). settings-format-warning.test.ts is deleted and settings-format-refusal.test.ts keeps every pin of it that is still true (the other shapes each grammar admits, empty and blank, the secret-bearing key on both doors, precedence on both doors, a well-formed value per type) and adds the refusal pins, the batch pin, the stored-row pins, the reset pin and the visible-TOUCH pin. The save door is driven through the real route with status starting at 0; the env door through get, getNamespace, the GET route and the PUT lock. The deleted "a well-formed override draws no warning" pin is subsumed by "a well-formed override is in force and locks the key with 409".

①-a The deliberate correction: .changeset/24882-settings-format-warning.md, every sentence, read at the merge base

  • D0 (front matter) '@objectstack/service-settings': patch — falsified: the diff ships a BREAKING narrowing, minor under the launch-window convention.
  • D1 "feat(service-settings): a settings value that misses the format its email, url, phone or color type declares is now reported in the server log; the save is still accepted (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)" — falsified: the save is refused with 400, and the save door writes no log line.
  • D2 "Clause-②: no (warning only: every save and every OS_* override accepted before is still accepted, with the same value stored or served, the same status and the same errors)" — falsified: the arm is now (narrowing); a save or an override that misses its format is refused.
  • D3 "ADR-0007 declares a format for four specifier types: email, url and phone take "format validation", and color takes a hex string." — still true; carried verbatim into the new note.
  • D4 "Until now nothing judged that format." — still true as history; carried as "Until now nothing enforced it.", which is the exact reading now that PR feat(service-settings): warn when a settings value misses the format its type declares (18.x half, 「甲」) #25001 judged without enforcing.
  • D5 "PUT /api/settings/branding stored a Logo URL of not a url and answered 200, and an OS_* override was served unchecked." — still true as history; carried ("…answered 200, and an OS_* override was served whatever it held").
  • D6 "Both doors now judge the format through one shared check, and report a miss instead of refusing it:" — first half still true (firstFormatMiss is the one judge), second half falsified; carried corrected as "Both doors now judge the format through one shared check, after every other declared constraint, and refuse a value that misses it."
  • D7 "Save. When a save is accepted and a key it touches holds a value that misses its type's format, the service logs one warn line for that key." — falsified: the save is refused and the warn loop is deleted.
  • D8 "The line names the setting, the type, the expected format and the value's length." — falsified for the save door: no line exists; the expected format now travels in the FieldError message to the caller, who holds the value.
  • D9 "Environment override. An in-force OS_* override whose value misses the format draws one warn line, at boot or on the first read." — falsified: the override is not in force and the line is at error. The timing clause (boot via auditEnvOverrides, else first read) stays true and is restated in the PR body; the new note says "one error line per variable and value" without the timing. A remark, not a lost sentence: the sentence as a unit is falsified.
  • D10 "The line names the variable, the setting, the type and the expected format." — still true of the new error line; carried verbatim.
  • D11 "The override stays in force, is served with source env, and still locks the key." — falsified; the new note states the opposite.
  • D12 "The value is never printed in either line, for any key." — still true in substance: the env line never prints the value, the save door has no line and its FieldError never echoes; carried as "never the value" and "never echoes the value".
  • D13 "The formats are the platform's existing grammars for the same words:" — still true; carried verbatim.
  • D14 "email: an address with a dotted domain, the grammar the mail transport sends with." — still true (regex unchanged); carried verbatim with an added example.
  • D15 "url: scheme://…, a root-relative /path, a data: URI or a blob: URL." — still true; carried verbatim.
  • D16 "An http(s)-only grammar would flag a relative logo path that the branding reader serves." — still true (the root-relative alternative is in the grammar); carried reworded as "A relative logo path that the branding reader serves is accepted."
  • D17 "phone: digits, spaces and + ( ) - ., at least five characters." — still true; carried verbatim.
  • D18 "color: #rgb, #rgba, #rrggbb or #rrggbbaa." — still true; carried verbatim.
  • D19 "Empty values and non-string values are not judged." — still true (firstFormatMiss); carried verbatim.
  • D20 "Surrounding whitespace is ignored, because the readers trim it before use." — still true; carried verbatim.
  • D21 "Unchanged. Status codes, response bodies, stored values, the FieldErrors a refused save carries, and which overrides are in force." — falsified: all five change.
  • D22 "Under the standing rule 「甲」, refusing these values waits for the next major version." — falsified by the maintainer's amendment (comment 6303865025 on [PM seat] triage (objectstack-wide) — 🟢 Routine · session_01AavokzJ5DndAwitDXvKy4U · trig_01NcnCtMS2tH46nUg1a5TaMJ (hourly) · state = body + the round records newer than it #6015) and by this diff.

Result: every still-true sentence (D3, D4, D5, the first half of D6, D10, D12 to D20) is carried into the new note. No still-true sentence is lost. No release consumed the deleted note: @objectstack/service-settings is at 18.0.0 on main, its CHANGELOG.md has no entry for it, and the file was still present in .changeset/ at the merge base.

①-b The new note: .changeset/24882-settings-format-refusal.md, every sentence, against the diff

  • N0 (front matter) '@objectstack/service-settings': minor — right (② below).
  • N1 "fix(service-settings)!: a settings value that misses the format its email, url, phone or color type declares is refused, both when it is saved and when it arrives as an OS_* override (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)" — true (J1, J4).
  • N2 "ADR-0007 declares a format for four specifier types: … color takes a hex string." — true (D3).
  • N3 "Until now nothing enforced it." — true.
  • N4 "PUT /api/settings/branding stored a Logo URL of not a url and answered 200, and an OS_* override was served whatever it held." — true of main.
  • N5 "Both doors now judge the format through one shared check, after every other declared constraint, and refuse a value that misses it." — true (J2).
  • N6 the Clause-②: no (narrowing) line — right (②).
  • N7 the adr-0087 marker, not-required (no-migration-prescription): "No metadata moves: no spec key, authorable spelling, object definition or stored shape is added, removed, renamed or re-shaped, and no exported declaration is removed or narrowed, so there is nothing for objectstack migrate meta to rewrite." — true (J8, no spec path in the diff). "What changes is which runtime values two settings doors accept: a value saved through PUT /api/settings/:namespace or setMany, and a deployment's OS_* environment override." — true. "Settings values are runtime state in sys_setting (ADR-0007) and environment values belong to the deployment, and no metadata conversion reaches either; a row already stored is not rewritten and is still served." — true (J3). "The remedy is to supply a well-formed value, never a rewrite of a metadata source." — true. "The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired)." — consistent with the tree; the gate accepted the exemption (a notice annotation on the Check Changeset run).
  • N8 "BREAKING (the settings doors' accept set narrows), shipped as minor under the launch-window convention for breaking changes." — true.
  • N9 "The formats are the platform's existing grammars for the same words:" — true (J9).
  • N10 "email: an address with a dotted domain, the grammar the mail transport sends with. no-reply@localhost misses it." — true: the regex requires a dotted domain.
  • N11 "url: scheme://…, a root-relative /path, a data: URI or a blob: URL. A relative logo path that the branding reader serves is accepted." — true for a root-relative path, the spelling the sentence before it names; a document-relative spelling without a leading slash is refused (J9's precision).
  • N12 "phone: digits, spaces and + ( ) - ., at least five characters." — true.
  • N13 "color: #rgb, #rgba, #rrggbb or #rrggbbaa." — true.
  • N14 "Empty values and non-string values are not judged. Surrounding whitespace is ignored, because the readers trim it before use." — true (firstFormatMiss returns null for a non-string or blank value and tests the trimmed string).
  • N15 "Saving, FROM: a save naming a value that misses its key's format answered 200 and stored the value as written." — true of main.
  • N16 "Saving, TO: the save answers 400 with error.code: 'SETTINGS_VALIDATION' and one entry per such key in error.details.fields, carrying code invalid_email, invalid_url, invalid_phone or invalid_format (for color), and constraint.type. Nothing in the save is stored. The answer names the expected format and never echoes the value." — true (J1, J5, J6).
  • N17 "Environment override, FROM: a malformed OS_* override was in force: reads served it with source env, and it locked the key against saves." — true of main (the deleted pin file held it).
  • N18 "Environment override, TO: a malformed OS_* override is no longer in force. The server logs one error line per variable and value, naming the variable, the setting, the type and the expected format, never the value. Reads serve the next layer instead: the stored value when one exists, otherwise the manifest default, with that layer as the source. The key is no longer locked, so it can be saved through Setup." — true (J4, J5).
  • N19 "Unchanged: a well-formed value saves, and a well-formed override is in force and locks the key. A value that breaks another declared constraint (a length bound, a pattern) is still refused for that constraint. A value stored before this release is still served by every read, and it does not block a save of the namespace's other keys; only a save that writes that key, or a key its visible condition reads, judges it. An all-null save (reset) still clears it." — true (J2, J3).
  • N20 "What to do: fix the value. For an OS_* override, set the variable to a well-formed value or unset it and configure the setting in Setup. For a stored value, save a well-formed value for the key, or clear it." — true; it matches the reporter's own Fix sentence and the reset pin.

② Semver level

  • The committed changeset bumps @objectstack/service-settings alone, at minor, marked BREAKING, with the Clause-②: no (narrowing) line at the start of a line in both the changeset and the PR body. Right: the diff adds no published declaration and no packages/spec surface (the yes arm), and it narrows two published accept sets, the save door's and the env door's (J1, J4). Under AGENTS.md rule 3 the (narrowing) arm is BREAKING, and the package's own CHANGELOG.md shows the launch-window convention shipping a BREAKING narrowing as minor at 18.0.0, so the level matches what the diff publishes.
  • No skip-changeset label is on the PR (labels: documentation, size/xl, tests, tooling), and skip-changeset would be wrong: a released package changes behaviour.
  • The ADR-0087 disposition not-required (no-migration-prescription) is the right category: nothing an author can write is removed, renamed or re-shaped, so no FROM to TO rewrite exists to prescribe; the note's "What to do" paragraph is a runtime value fix, not a consumer code rewrite. The gate accepted it on this head (notice annotation on Check Changeset).
  • The deletion of the pending warning note is the right correction, not a collision: its sentences are falsified or carried (①-a), the replacement is the one 24882 changeset at the head, and the two sibling changesets main gained since the merge base (24773-oauth-form-types, 24986-cbp-master-edit-object-question) do not share the name.

③ Boundary flags

  • The dev's open question (os-dev report comment 6305731231, options A confirm the replacement / B restore the warning note beside it): answered A. Every sentence of the deleted note is either falsified by this diff or carried into the new note (①-a), the note was unconsumed (18.0.0, no CHANGELOG entry), and restoring it would publish "the save is still accepted … refusing waits for the next major version" beside "refused" in one release. The landing rules (landing-operations.md, the DELIBERATE CORRECTION lines) make a same-head at-tier PASS record that judges every rewritten sentence the confirmation when the diff also changes code; this record is that confirmation, naming both notes by path. The Check Changeset red stays by design and is not a FAIL input here.
  • Deviation: the pending changeset deleted. Answered with the open question above.
  • Deviation: reportRejectedEnvOverride gains an optional shape fragment. Accepted: private method, set only by the format family, and when set the line carries the length for every key (J5). The other families' lines are unchanged and still print a non-secret value; that is main's behaviour, outside this card, and not re-opened here.
  • Deviation: constraint: { type } and no value member on the save door's FieldError. Accepted (J6): inside the published open constraint record, in the convention the other branches use, and the omission of value is what keeps the answer echo-free.
  • Deviation: origin/main moved after the pre-PR merge and was not re-merged. Not a flag: main is four commits past the merge base, none touches service-settings, and the two changesets they add do not collide with the 24882 name; the merge group re-tests the merged tree.
  • Deviations on commit trailers, the merge commit's message, the dual-build rebuild and the worktree removal. Process notes with no contract bearing; nothing to answer.
  • Acceptance note: content/docs/deployment/environment-variables.mdx. Its statement that OS_BRANDING_LOGO_URL / OS_BRANDING_ACCENT_COLOR outrank a saved value and lock the field stays true for a well-formed value; it is silent, not false, on a malformed override now being ignored. Outside the card's file surface, and Flag docs affected by code changes is green on this head. Not escalated; a docs follow-up is the lane seat's to file.
  • Acceptance note: RuntimeConfigPlugin's host-level OS_LOGO_URL / OS_BRAND_COLOR. Answered: a different reader with its own option contract ("absolute or relative URL"), not judged by the settings door and not narrowed by this diff (J9).
  • Out-of-scope findings (carrier none: the grammar spelled in four places, non-string values stored as given, company.phone typed text). Stand as noted; no bearing on this verdict.
  • The earlier open question on PR feat(service-settings): warn when a settings value misses the format its type declares (18.x half, 「甲」) #25001 (log-only loudness). Superseded: the seat chose log-only (comment 6303850131), and the amended rule moved the refusal into 18.x (comment 6304784014). Closed.
  • Check-runs on the head, read last, at 2026-10-21T01:42Z: 40 runs. 27 success, among them Build Core, Test Core (5/6), every Lint lane (CI release and engine gates, ESLint, PM dispatch-gates, docs and code guards, repo and tooling gates, the three scoped lanes), every Type Check lane, Governed Surface Queue Guard, Flag docs affected by code changes, Check Documentation Links, Check PR Size, the three claim guards and filter. 7 skipped (the two opt-in harnesses, Build Docs, Console Pin Gate, the dogfood shards, Dogfood Verify CLI, Temporal Conformance). 1 failure: Check Changeset, whose annotations name .changeset/24882-settings-format-warning.md as present on the merge base and deleted by this PR, with the two-class text; this is the DELIBERATE CORRECTION class the gate's own source declares red by design, and the same run carries the ADR-0087 exemption acceptance as a notice. Pending: Test Core (1/6), Test Core (2/6), Test Core (3/6), Test Core (4/6), Test Core (6/6). A pending run is not a FAIL input; the seat re-reads every run before landing, and the designed-red queue conditions (red by design per the gate's source, not run in the merge group, named on the PR with its reason) are the seat's to confirm from that source; the third is met by this record and the PR body's "Pending release note corrected" section.

Implemented-by: claude/issue-24882-settings-format-refusal
Reviewed-by: session_01UmqzD97e14iws6hmVhgZ5y

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · #24882 (the refusal) · domain:services seat 2 · os-support-ai · session_01UmqzD97e14iws6hmVhgZ5y · 2026-10-21T02:08Z · head 58d0be84ff.

  • This is the review of record for the claim 6304887064.
  • Contract review: owed on the narrowing face, and PASS at CONTRACT_REVIEW_TIER on this same head (6305924680, isolated reviewer, Local-runs: none).
  1. Form:
    • The first line is Fixes #24882. The seat's grep finds no other closing keyword beside an issue number.
    • Clause-②: no (narrowing) is at line start in the body and in the changeset, with its reason. The changeset is BREAKING minor for @objectstack/service-settings, ADR-0087 not-required (no-migration-prescription), which the gate accepted.
    • 5 files, +583/−447.
    • Path face: check-governed-merges --pr 25022 reports NOT governed. Ordinary queue landing.
  2. Premise and fix:
    • Under the maintainer's amendment of 「甲」 (6303865025), both settings doors refuse a value that misses the format its email / url / phone / color type declares (ADR-0007).
    • The save door: a 400 SETTINGS_VALIDATION, one FieldError per key with the verdict PR feat(service-settings): warn when a settings value misses the format its type declares (18.x half, 「甲」) #25001 carries (invalid_email / invalid_url / invalid_phone / invalid_format) and constraint.type, and nothing stored.
    • The env door: a malformed OS_* override is not in force. One error line per variable and value carries the value's length, never the value. Reads fall back to the stored value, else the manifest default, and the key is unlocked.
    • Measured: a stored malformed value is still served by every read and does not block a save of the namespace's other keys. Only a save that writes that key, or a key whose visible reads it, judges it.
    • Pins: 41 cases. Ablations A1–A4 each went red as predicted and were restored to blob == HEAD.
  3. Gates:
    • CI on 58d0be84ff, latest run per check name: 41 checks, 33 success, 7 skipped, 1 failure. Every skip is on the roster in scripts/pm/check-expected-skips.mjs: Temporal Conformance, Build Docs, the Dogfood Regression Gate matrix, Console Pin Gate, Dogfood Verify CLI, Packed-tarball smoke and the 18.x upgrade harness. Lint & Repo Gates, every Type Check and all six Test Core shards are success. The one failure is Check Changeset.
    • Check Changeset is red by design, and all three conditions for landing with it hold:
    • The confirmation the gate asks for: the contract review on this head judged every sentence of the deleted note and of the new one (its F1). No still-true sentence is lost, so the replacement is confirmed. ⛔ The warning note is not restored. This also answers the dev's open question: option A.
    • Local: 67 derived, 67 run, 0 NOT-MEASURED. The service-settings suite: 57 files, 970 passed. plugin-auth and dogfood settings pins pass.
  4. Accepted deviations:
    • The deleted pending changeset (above).
    • reportRejectedEnvOverride's fragments gain one optional shape member, so the format line carries a length and never a value. Other families' lines are unchanged.
    • The save door's FieldError carries constraint: { type } and no value.
    • origin/main moved past the PR's merge base on unrelated paths: no conflict, no service-settings change.
  5. Out of scope:
    • content/docs/deployment/environment-variables.mdx says an OS_BRANDING_* override outranks a saved value and locks the field. Dropped: that stays true for every value the door accepts, and the operator-facing change is stated in the release note.
    • The host-level OS_LOGO_URL / OS_BRAND_COLOR are another reader (RuntimeConfigPlugin), and no wrong answer was measured. Dropped.
    • The reviewer's F3 (the url grammar admits a root-relative /path, not a document-relative one): a remark, matching the note's own grammar line. No action.
  6. Landing: ordinary queue landing with the designed red above. Every other check on this head is green or an expected skip, so the seat flips it on this record.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 21, 2026 02:09
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 21, 2026 02:09
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 21, 2026
Merged via the queue into main with commit 3b3830c Oct 21, 2026
41 of 42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24882-settings-format-refusal branch October 21, 2026 03:00
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 size/xl tests tooling

Projects

None yet

2 participants