Repository navigation
service-settings: re-point value-domains.ts onto @objectstack/spec/shared and refuse with value_domain on the settings door — the services half of #14168 #15162
Description
Activity
- addedpriority:p2Medium: important, M3Medium: important, M3
on Sep 4, 2026 Claimed —
domain:servicesexecution seat. Session03324ae2-0f5b-5ad2-8a2e-cf4aaff5a909(seat post #6021).- branch
claude/issue-15162-value-domain-shared-predicate - worktree
objectstack-issue-15162-impl(dedicated, per Prime Directive Migrate documentation site to Fumadocs with monorepo structure and shared content #11) - implementation model:
opus - Clause-②: YES — review at
CONTRACT_REVIEW_TIERis owed before this lands.
Labels, written then read back (the read is a separate call made after the write, and this comment reports that read — it is not itself the write):
["priority:p2","pm:dispatched","domain:services"].pm:queueis gone; nothing else moved. No prior assignee and zero prior comments — checked, because the shared identity makes the assignee field no proof of a claim.Why Clause-② is yes
dispatch-gates.mjsderives no path mandate forpackages/services/service-settings/**, and its own output says a no-mandate line is not a clearance: "Clause ② is NOT reachable from paths: a card that changes contract accept/reject behaviour or widens the public surface isclaude-fable-5-1" — judged from the card CONTENT. This card's content reaches it twice:- Ask 2 changes the reject envelope on the wire. The settings door's refusal code for a non-member becomes
value_domain; the card itself asks for "a pin that states the wire-visible change on the settings door". A client that branches on the current code sees a different one. - Ask 1 swaps the membership predicate itself.
knownValueDomain/firstRejectedDomainMemberstop answering fromservice-settings' own tables and start answering fromisValueDomainMember. If the two sets are not identical, the accept set moves — silently, in whichever direction the sets differ. The card states the bar (162 ISO 4217, 249 alpha-2,UTCadmitted); that is the claim to be measured, not the evidence. It also deletes/re-points a re-export atvalue-domains.ts:114, which is public surface of the package.
Either one alone would be enough. The tier is not negotiable down by the card's
priority:p2.Base
Dispatched from
origin/mainata56baa2bd(2026-09-04T13:55:58Z), re-fetched at 14:35Z. The spec half this card consumes is on that tree:packages/spec/src/shared/index.ts:35re-exports./value-domain.zod, andpackages/services/service-settings/src/value-domains.tsis 193 lines — consistent with the:114/:140/:163/:179line refs the card cites, so those refs are usable rather than stale.Not paused
Checked against ADR-0131 before claiming:
docs/adr/0131-…mdonorigin/mainstill readsStatus: Proposed, so the multi-tenancy pause stands. This card is a value-domain vocabulary/refusal-code change with no organization, tenancy or posture surface — outside the pause.
Generated by Claude Code
- branch
os-dev-report
{ "issue": 15162, "status": "done", "branch": "claude/issue-15162-value-domain-shared-predicate", "pr": "https://github.com/objectstack-ai/objectstack/pull/15434", "final_commit": "46b06feeca9f4bb523cf53a7379a9b6a345c9503 (46b06feec)", "premise_still_valid": true, "summary": "Ask 1: value-domains.ts re-pointed onto @objectstack/spec/shared symbol by symbol; all three local definitions deleted (the Intl.DateTimeFormat probe, the run-time Intl.supportedValuesOf('currency') set and the 249 alpha-2 codes). knownValueDomain now filters through ValueDomainSchema.safeParse, which makes the prototype-chain guard structural instead of hand-written; firstRejectedDomainMember calls isValueDomainMember. Ask 2: the save door's refusal code becomes value_domain and its message renders the published catalog template value_domain_DOMAIN via renderValidationMessage, the same catalog the record write path renders. Only ONE of the two doors had a code to change (see A4 below). Ask 3: a ratchet pin reddens if any non-test source in the package re-acquires a membership table, probes an Intl enumeration, or grows a second caller of the predicate. valueDomainPhrasing SURVIVES, scope narrowed to the env-override log line (that door emits no FieldError), and is now pinned equal to the catalog templates so the two doors cannot describe one domain in different words. Seat ruling 2 implemented as directed; nothing in the measurement argued against it, so there is no veto case to report. packages/spec is untouched on this branch per the PM's mid-task correction.", "assumption_audit": { "A1_iana_time_zone_cannot_move": "confirmed — identical Intl.DateTimeFormat probe bodies on both sides; the door's zone suite (UTC, Asia/Kolkata, Europe/Kyiv, Asia/Ho_Chi_Minh, US/Eastern, GMT, Europe/Zurich accepted; Mars/Olympus, ZZ, 'Not A Zone' refused) re-runs green against the shared predicate.", "A2_iso_3166_alpha2_lists_equal": "CONFIRMED BY MEASUREMENT, done before either literal was deleted — a script extracting the string literals out of both files: services 249 unique, spec 249 unique, only-in-services [] and only-in-spec [], ordered sequences identical. This was the assumption that could have made the card a ruling question; it did not.", "A3_iso_4217_definition_moves_accept_set_holds": "confirmed — tsx over CURRENCY_FRACTION_DIGITS vs Intl.supportedValuesOf('currency') on node v22.22.2: 162 vs 162, only-in-runtime [] and only-in-snapshot [], positive control CHF in both, negative control XYZ in neither. Pinned rather than left unremarked: a new door-level test asserts every code the run-time probe admits is still admitted through firstRejectedDomainMember.", "A4_value_domain_zero_occurrences": "confirmed (grep count 0 before the change) — WITH A LABELLING CORRECTION to the card. The card labels :1025 as validatePatch and :2060 as 'the second door, near registerManifest'. It is the other way round: :1025 sits inside `private effectiveEnvOverride` (the env-override door, which calls reportRejectedEnvOverride and emits a LOG LINE with no error code at all) and :2060 sits inside `private async validatePatch` (line 1883), which is the one that pushes the FieldError. So only one refusal site had a code to change; the other's before/after is prose, and it is covered by re-pointing valueDomainPhrasing and pinning it against the catalog. Both sites are addressed.", "A5_prototype_chain_guard": "confirmed — knownValueDomain('constructor') and ('toString') still return null across the swap, now because a closed z.enum matches literal members only. Pins kept and extended with '__proto__' and 'hasOwnProperty'.", "A6_spec_exports_four_things": "confirmed — ValueDomainSchema, ValueDomain, ISO_3166_ALPHA2_CODES, isValueDomainMember; no valueDomainPhrasing equivalent. DECISION: valueDomainPhrasing survives as the env-door presenter, because that door writes a log line with no code, no locale and no label, and the catalog's finished sentences do not fit its three-fragment template. It no longer feeds the save door. A new pin holds each domain's fragments to appear in that domain's catalog template, so the surviving local prose cannot drift.", "A7_XDR_sentence_is_false": "confirmed — XDR is present in BOTH sets (runtime true, snapshot true); VED, XAU and XAG are absent from both. The stale sentence is deleted with the file and is deliberately not carried into the new module header or the PR body.", "A8_liveness_field_json": "confirmed that the sentence at packages/spec/liveness/field.json:228 becomes false — and DELIBERATELY LEFT UNFIXED per the PM's mid-task correction. See out_of_scope_findings: the edit was made, then fully reverted (worktree blob now byte-identical to the base blob, git diff of packages/spec against base is empty)." }, "tests": "All at final commit 46b06feec, dependency closure built first (pnpm --filter '@objectstack/service-settings^...' build, exit 0) so no verdict is read off a stale dist. GATES: derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` from the real diff, re-derived twice as the diff grew (the tooling-script + turbo.json edits added 12 families; the liveness revert dropped 6). Final union re-run AFTER the final commit: 57 run - 57 exit 0 - 0 red. Two were genuinely red on the way and are fixed in the diff, not routed around: check:comment-mask-adoption (the ratchet had a private comment stripper; now imports stripComments from scripts/js-comment-mask.mjs) and check:cross-package-test-inputs (that import escapes the package; radius declared in scripts/cross-package-test-inputs.mjs and turbo.json). check:dual-build-cjs-loads and check:type-check-debt first answered `exit 3 / PREREQUISITE NOT MET` -> read as NOT MEASURED, not a pass: the workspace closure was built (`turbo run build --filter=./packages/* --filter=./packages/*/*`, VERDICT command-exit 0, Tasks: 71 successful, 71 total) and both were re-run to a real 0 (dual-build: '103 published require entry point(s) across 66 package(s) load; 619 emitted CommonJS file(s) parse'). TESTS: `pnpm --filter @objectstack/service-settings test` -> 'Test Files 30 passed (30) / Tests 527 passed (527)'. TYPECHECK: `pnpm --filter @objectstack/service-settings typecheck` -> tsc --noEmit clean, and `tsc --noEmit --listFiles` counts 30 of this package's .test.ts files in the program (tsconfig include is ['src'] with no test exclusion), so 'typecheck clean' is a statement about the edited tests too. WIRE BEFORE/AFTER was measured, not derived: the implementation was committed first, then value-domains.ts and settings-service.ts were checked out at base a56baa2bd into the same tree (mutation confirmed on disk by comparing git hash-object against the base blobs, aborting otherwise), the same script re-run, and the tree restored with `git checkout HEAD -- ABSOLUTE_PATHS` under an EXIT/INT/TERM trap using absolute paths; restore proven by an empty `git diff HEAD` AND worktree hashes equal to the HEAD blobs. Result: before {code: invalid_value, message: \"Default timezone must be a valid IANA time zone identifier (e.g. 'Europe/Zurich'). Received 'Mars/Olympus'.\"} -> after {code: value_domain, message: 'Default timezone must be a valid IANA time zone identifier, e.g. Europe/Zurich (got \"Mars/Olympus\")'}; status 400, envelope SETTINGS_VALIDATION, field, label, constraint.valueDomain and value all unchanged. No ablation was performed on this card.", "mcp_calls": "6 - issue_read(get), issue_read(get_comments), create_pull_request, pull_request_read(get), add_issue_comment (this one), issue_read(get_comments) to read this comment back. No list_issues, no search: the card and the claim comment carried everything, and no new issue was filed.", "open_questions": [], "out_of_scope_findings": [ "NOT FILED, reported to PM as instructed - packages/spec/liveness/field.json, the `valueDomain` row's note, carries the sentence: \"The settings door (`service-settings/value-domains.ts`) re-points onto the shared predicate in its own follow-up card and is unchanged until then.\" Landing this PR makes it false. Per the PM's mid-task correction I edited it, then fully reverted (packages/spec is untouched on the branch; verified byte-identical to base). PR #15316 (the engine half) rewrites the same row and keeps the sentence verbatim, so it is false on main whichever of the two lands second. Sequencing is the seat's.", "NOT FILED, a read for the #15134 grader - #15134's premise (a `packages/core` copy of these tables) appears ALREADY DEAD on main at a56baa2bd. packages/core/src/security/resolve-authz-context.ts:62 imports isValueDomainMember from @objectstack/spec/shared and :1019 calls it; the module-private isValidTimeZone is gone and resolve-authz-context.time-zone-domain.pin.test.ts guards its removal. Repo-wide grep finds no ISO 3166 / ISO 4217 / IANA membership table anywhere under packages/core/src. Not touched, as instructed - triage's grading.", "NOT FILED, a correction to the card rather than a defect - the two refusal sites are labelled the wrong way round in the dispatch (A4 above): :1025 is effectiveEnvOverride (log line, no code) and :2060 is validatePatch (the FieldError). Worth carrying into the next card that cites those line refs." ] }
Generated by Claude Code
os-dev-report
{ "issue": 15162, "status": "done", "round": "2 — contract review round 1 addressed; supersedes the report in issuecomment-5542859010", "branch": "claude/issue-15162-value-domain-shared-predicate", "pr": "https://github.com/objectstack-ai/objectstack/pull/15434", "final_commit": "6dad1547e344488416146ebc733afa57ef940f86 (6dad1547e)", "pr_state": "DRAFT — not undrafted, not armed, not enqueued. Re-review at CONTRACT_REVIEW_TIER is owed on this head.", "premise_still_valid": true, "summary": "Round-1 contract review returned CHANGES REQUIRED with no defect in value-domains.ts or settings-service.ts (the wire change, the equivalence measurements and the redaction on every path all held under attack with live positive controls). Both required changes were in the test files and are now in, test-only: no line of either runtime file moved, so the parts the review cleared are byte-identical. R1 — the ratchet is now recursive over src/, detects the array-literal table shape beside the space-separated one, and gains an import-surface pin holding value-domains.ts's specifiers to exactly ['@objectstack/spec/shared'], which closes the M5 route at its root instead of by recognising a shape; `new Set(` was NOT widened package-wide as directed, and the header now states what is and is not covered. R2 — the run-time Intl.supportedValuesOf('currency') pin is replaced by an agreement pin (the door agrees with isValueDomainMember value by value, across all three domains, over a corpus carrying every trap), which is ICU-independent and leaves probe-versus-snapshot where the snapshot is. All three waived one-liners taken. The one thing marked 'know but do not act on' (the refuses-something pin being weaker than it reads) is recorded in the PR body, not changed.", "r1_mutation_results": { "method": "each mutation written to disk, then CONFIRMED on disk by observing the anchor text rather than the writer's exit code; tree restored under an EXIT/INT/TERM trap using absolute paths; restore proven by an empty `git status` AND `git hash-object` equal to the HEAD blob. Both restores verified clean.", "M4": "table one directory down at src/manifests/zz-alpha2-table.ts — BEFORE green 5/5, AFTER RED: 'Tests 1 failed | 5 passed (6)', failing case `carries no ISO 3166-1 alpha-2 code list, in either shape`.", "M5": "249 codes as an ARRAY literal in src/zz-alpha2-array.ts, imported by value-domains.ts and consulted for iso_3166_alpha2 while the shared predicate served the other two — BEFORE ratchet green 5/5, AFTER RED: 'Tests 2 failed | 4 passed (6)', failing cases `imports from NOTHING ELSE — the door cannot reach a table wherever one is put` and the array shape.", "note": "under M5 the door suite stays GREEN (15/15), correctly — a duplicate table with identical membership changes no behaviour, so no behavioural test can see it. That is the argument for the ratchet being a source scan and for the import pin being the load-bearing check." }, "r2_deviation_flagged": "The reviewer's literal suggestion — loop Object.keys(CURRENCY_FRACTION_DIGITS) — is NOT reachable from this package: the snapshot is internal to packages/spec (not re-exported from data/index.ts, absent from api-surface/), so importing it would mean widening a published surface, outside this card. Implemented the intent instead with an agreement pin over the shared predicate, which covers the same regression class and is equally ICU-independent. Flagged rather than silently reshaped.", "tests": "All at final commit 6dad1547e, in a freshly recreated worktree with the dependency closure built first (pnpm --filter '@objectstack/service-settings^...' build, VERDICT command-exit 0). GATES: family re-derived on this head with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` — same 57 families, no additions, none dropped — and re-run in FULL after the final commit: 57 run - 57 exit 0 - 0 red. The workspace closure was built first (`turbo run build --filter=./packages/* --filter=./packages/*/*`, VERDICT command-exit 0, Tasks: 71 successful, 71 total), so check:dual-build-cjs-loads and check:type-check-debt are real zeroes and not `exit 3 / PREREQUISITE NOT MET`. check:turbo-task-graph is green with the `^build` change. TESTS: `pnpm --filter @objectstack/service-settings test` -> 'Test Files 30 passed (30) / Tests 529 passed (529)' (527 before: the currency probe case is replaced by two agreement cases, and the import-surface pin is new). TYPECHECK: `pnpm --filter @objectstack/service-settings typecheck` -> tsc --noEmit clean. Both were re-run AFTER the final commit, on a clean tree (the mutation runs above were restored first).", "mcp_calls": "11 cumulative for the whole card — round 1: issue_read(get), issue_read(get_comments), create_pull_request, pull_request_read(get), add_issue_comment, issue_read(get_comments). Round 2: update_pull_request, add_issue_comment (the PR reply), add_issue_comment (this one), issue_read(get_comments) to read this back. No list_issues, no search, no new issue filed.", "open_questions": [], "out_of_scope_findings": [ "UNCHANGED from round 1, still NOT FILED and still owed to the seat - packages/spec/liveness/field.json, the `valueDomain` row's note, carries: \"The settings door (`service-settings/value-domains.ts`) re-points onto the shared predicate in its own follow-up card and is unchanged until then.\" Landing this PR makes it false. Not touched, per the mid-task correction; packages/spec is untouched on this branch. PR #15316 rewrites the same row and keeps the sentence verbatim, so it is false on main whichever of the two lands second.", "UNCHANGED from round 1, a read for the #15134 grader - #15134's premise (a `packages/core` copy of these tables) appears ALREADY DEAD on main at a56baa2bd. packages/core/src/security/resolve-authz-context.ts:62 imports isValueDomainMember from @objectstack/spec/shared and :1019 calls it; resolve-authz-context.time-zone-domain.pin.test.ts guards the removal of the module-private isValidTimeZone. No ISO 3166 / ISO 4217 / IANA membership table anywhere under packages/core/src.", "UNCHANGED from round 1, a correction to the card - the two refusal sites are labelled the wrong way round in the dispatch: :1025 is effectiveEnvOverride (log line, no code) and :2060 is validatePatch (the FieldError).", "NEW, for whoever writes the next ratchet of this shape - a package-source ratchet needs THREE properties a first draft rarely has, and two of them were caught only by mutation: a RECURSIVE walk (readdirSync is not, and src/ subdirectories are normal), detection of BOTH literal shapes for the same table, and — the only one that is not defeatable by an unrecognised shape — a pin on the consuming module's IMPORT SURFACE. Behavioural tests cannot substitute: a duplicated table with identical membership leaves every behavioural assertion green." ] }
Generated by Claude Code
os-dev-report
{ "issue": 15162, "status": "done", "round": "3 — contract review round 2 addressed; supersedes issuecomment-5546823289", "branch": "claude/issue-15162-value-domain-shared-predicate", "pr": "https://github.com/objectstack-ai/objectstack/pull/15434", "final_commit": "6f2f77edf9be1837b69f7e5a87386115eb4a71f8 (6f2f77edf)", "prior_heads": "46b06feec (pre-review) - 6dad1547e (after round 1) - 6f2f77edf (after round 2)", "pr_state": "DRAFT — not undrafted, not armed, not enqueued. Re-review at CONTRACT_REVIEW_TIER is owed on this head.", "premise_still_valid": true, "summary": "Round-2 review found both instruments narrower than their own headers claimed. Test-only again: value-domains.ts and settings-service.ts are still the blobs round 1 cleared (618033da909d7e16384f53fd0b7b590ca123de95 and 15e807fa15e547902d5153557cfaf052f2d0eab8), verified by hash after every mutation restore. R1-prime: every scan was single-quote-shaped, so a verbatim double-quoted copy of the array mutation passed green; the import surface now reads /\\bfrom\\s*(['\\\"])([^'\\\"]+)\\1/ and both table shapes accept ' \\\" and backtick — 0 hits of 31 runtime sources. M16 (the 249 codes as a regex alternation inside the door — no quotes, no import, no new Set) is CLOSED rather than documented, by a door-only density check: 7+ bare two-uppercase-letter tokens separated by 1-2 non-alphanumerics. Door-only is the coherent scope, since the import pin keeps a sibling-module table out of reach and a package-wide density scan is one curated dropdown away from a false positive. R2-prime: the agreement pin covered a 4-sample currency corpus while claiming to cover 'a curated allow-list'; it now asserts agreement over each domain's whole POPULATION — Intl.supportedValuesOf for currency (162) and timeZone (418), the published 249 for alpha-2 — with the corpus kept for traps no population contains. The enumeration is POPULATION, never ORACLE: each code goes to the door AND the predicate and the two must answer alike, so a code the snapshot lacks is false on both sides and a code the host lacks is never asked. Round 1's ICU objection is preserved intact.", "mutation_results": { "method": "each mutation written to disk, CONFIRMED on disk by grepping the injected AND the removed text (never the writer's exit code — sed/perl -i/str.replace/re.sub all exit 0 on zero hits) plus a git hash-object comparison showing the blob moved; restored under an EXIT/INT/TERM trap with absolute paths; every restore proven by an empty `git status` and hashes equal to the HEAD blobs.", "M5q": "M5 verbatim but DOUBLE-QUOTED (from \"./zz-alpha2-array.js\", [\"AD\", \"AE\", …]); probe verified to contain 0 single-quoted codes. BEFORE ratchet green 6/6 and door suite 15/15. AFTER RED: ratchet 'Tests 2 failed | 5 passed (7)' on `imports from NOTHING ELSE — the door cannot reach a table wherever one is put` and `carries no ISO 3166-1 alpha-2 code list, in either shape`.", "M16": "249 codes as a regex alternation inside the door itself; probe verified to contain 0 quoted codes and 0 `new Set(`. BEFORE green. AFTER RED: ratchet 'Tests 1 failed | 6 passed (7)' on `carries no dense run of two-letter tokens in ANY delimiter — the alternation shape`.", "M14": "iso_4217_currency answered by a ten-entry .includes allow-list, predicate kept for the other two. BEFORE ratchet green, door suite green, package green 529/529. AFTER RED: door suite 'Tests 1 failed | 17 passed (18)', 152 disagreements — \"expected [ 'AED', 'AFN', 'ALL', 'AMD', …(148) ] to deeply equal []\".", "M15": "predicate AND !v.startsWith('X'). BEFORE green. AFTER RED: door suite 'Tests 1 failed | 17 passed (18)', 7 disagreements — \"expected [ 'XAF', 'XCD', 'XCG', 'XDR', …(3) ] to deeply equal []\" (XOF, XPF, XSU are the other three).", "cross_check": "Both currency numbers reproduce the review's exactly (152 and 7). Under M14/M15 the ratchet stays green 7/7 and under M5q/M16 the door suite stays green — the two instruments catch disjoint classes, which is the argument for keeping both." }, "control_discipline": "Taken from the review's method note and applied, not just read: each population pin carries a vacuity guard (expect(population.length).toBeGreaterThan(100)) so an empty enumeration cannot pass it silently, and the live positive controls for the new pins are M14 and M15 — the pin was not reported green until it had been watched failing on a real mutation.", "tests": "All at final commit 6f2f77edf, freshly recreated worktree, dependency closure built first (pnpm --filter '@objectstack/service-settings^...' build, VERDICT command-exit 0). GATES: family re-derived on this head — same 57 families, none added, none dropped — and re-run in FULL after the final commit: 57 run - 57 exit 0 - 0 red. Workspace closure built first (`turbo run build --filter=./packages/* --filter=./packages/*/*`, VERDICT command-exit 0, Tasks: 71 successful, 71 total), so check:dual-build-cjs-loads and check:type-check-debt are real zeroes, not `exit 3 / PREREQUISITE NOT MET`. TESTS: 'Test Files 30 passed (30) / Tests 533 passed (533)' (529 before: three population pins replace one corpus pin, plus the alternation check). TYPECHECK: tsc --noEmit clean. Both re-run AFTER the final commit on a clean tree, with every mutation restored first.", "mcp_calls": "15 cumulative for the whole card — round 1: 6, round 2: 4, round 3: update_pull_request, add_issue_comment (PR reply), add_issue_comment (this report), issue_read(get_comments) to read it back, plus one spare. No list_issues, no search, no new issue filed.", "open_questions": [], "out_of_scope_findings": [ "UNCHANGED, still NOT FILED and still owed to the seat - packages/spec/liveness/field.json, the `valueDomain` row's note, carries: \"The settings door (`service-settings/value-domains.ts`) re-points onto the shared predicate in its own follow-up card and is unchanged until then.\" Landing this PR makes it false. Not touched, per the mid-task correction; packages/spec is untouched on this branch. PR #15316 rewrites the same row and keeps the sentence verbatim, so it is false on main whichever of the two lands second.", "UNCHANGED, a read for the #15134 grader - #15134's premise (a `packages/core` copy of these tables) appears ALREADY DEAD on main at a56baa2bd. packages/core/src/security/resolve-authz-context.ts:62 imports isValueDomainMember from @objectstack/spec/shared and :1019 calls it; resolve-authz-context.time-zone-domain.pin.test.ts guards the removal of the module-private isValidTimeZone.", "UNCHANGED, a correction to the card - the two refusal sites are labelled the wrong way round in the dispatch: :1025 is effectiveEnvOverride (log line, no code) and :2060 is validatePatch (the FieldError).", "UPDATED after round 2, for whoever writes the next ratchet of this shape - a package-source ratchet needs FIVE properties a first draft rarely has, and four of them were caught only by mutation: a RECURSIVE walk; BOTH literal shapes for the same table; DELIMITER-agnostic regexes (a verbatim double-quoted copy is legal wherever no `quotes` lint rule is active, which is most packages here); a DENSITY check for the shapes that need no quotes at all (a regex alternation is a table); and a pin on the consuming module's IMPORT SURFACE, the only one not defeatable by an unrecognised shape. Its behavioural sibling needs one more: agreement asserted over a POPULATION, not a corpus — a 4-sample corpus let a 10-entry allow-list through with the whole package green.", "NEW, a reusable framing worth carrying - a host enumeration (Intl.supportedValuesOf) is safe in a test when used as the POPULATION and unsafe as the ORACLE. As an oracle it imports the host's ICU verdict into an assertion about a checked-in snapshot, and reddens correct code on a runtime nobody chose; as a population it only supplies values that are then put to BOTH sides of an agreement, which no ICU build can bias. The same value, the same call, opposite drift properties." ] }
Generated by Claude Code
os-dev-report
{ "issue": 15162, "status": "done", "round": "4 — contract review round 3 addressed; supersedes issuecomment-5547153344", "branch": "claude/issue-15162-value-domain-shared-predicate", "pr": "https://github.com/objectstack-ai/objectstack/pull/15434", "final_commit": "17eb7941d2c21e211056f210e9427a3289f319d8 (17eb7941d)", "prior_heads": "46b06feec (pre-review) - 6dad1547e (r1) - 6f2f77edf (r2) - 17eb7941d (r3)", "pr_state": "DRAFT — not undrafted, not armed, not enqueued. Re-review at CONTRACT_REVIEW_TIER is owed on this head.", "premise_still_valid": true, "summary": "One change, one identifier wide: DENSE now runs over runtimeSources() (the package-wide scope SPACED/ARRAY already had) with a separator class that admits a newline. The finding is accepted as a FALSIFICATION rather than a gap — the door-only scan rested on the argument that the import pin keeps a table in a sibling module out of reach, which is true only of a table the door REACHES. N6's table is never reached by the door: it stands in front of it, in the door's caller (settings-service.ts, replacing the firstRejectedDomainMember call at the validatePatch refusal), answers iso_3166_alpha2 itself and falls through for the other two. The header sentence that carried the wrong argument was rewritten, not extended: the import pin covers a table the door reaches, the density scan covers a judge in front of the door. Test-only again; value-domains.ts and settings-service.ts remain 618033da909d7e16384f53fd0b7b590ca123de95 and 15e807fa15e547902d5153557cfaf052f2d0eab8, re-verified by hash after every restore.", "mutation_results": { "method": "tables built in Python with the backtick as chr(96) — never through a shell — then COUNTED BACK OFF DISK with the count, the endpoints, and the absence of quotes/imports/new Set asserted before any test runs. An empty or truncated table aborts the run instead of producing a green. This is the review's own backtick incident applied, and the mirror of the sed/perl-exits-0-on-zero-hits note from round 3.", "clean_head": "ratchet 7/7; package 533/533; tsc --noEmit clean.", "N6": "the M16 alternation moved into settings-service.ts at the validatePatch refusal. Verified on disk: 249 codes, first/last AD/ZW, 0 quoted 2-letter codes added, door blob unchanged, env-door call site untouched, original validatePatch call gone. BEFORE ratchet 7/7, door suite 18/18, package 533/533 green. AFTER RED: ratchet 'Tests 1 failed | 6 passed (7)' naming `settings-service.ts carries a dense run of two-letter tokens`. With the full table the settings-service suite is 139/139 — behaviourally invisible, which is why it has to be caught by shape.", "N6d": "same mutation with US and CH deleted — verified 247 codes on disk with both confirmed absent. settings-service.test.ts 'Tests 2 failed | 137 passed (139)' on `default_country: the domain refuses what the pattern admits (#5712 third case)` and `write door: admits an assigned code, refuses ZZ/UK with the domain in the constraint`. Reproduces the review's liveness evidence exactly: the injected judge is live code.", "N2b": "one-code-per-line template literal inside the door — green before, because the old separator class excluded \\n. Verified on disk: 249 codes one per line, 0 quoted codes, no `new Set(`, no imports added. AFTER RED: ratchet 'Tests 1 failed | 6 passed (7)' naming `value-domains.ts carries a dense run of two-letter tokens`." }, "census_reproduced_independently": "Repo's own stripComments, my own walk rather than the review's numbers: 0 DENSE hits over this package's 31 runtime sources; 1 hit over 2,186 runtime .ts files across 76 src roots (I swept packages/, apps/ and examples/, which is why my counts run above the review's 1,885/49) — the single hit is the same file, the shared table's own module in the spec package, which is where the definition belongs. SPACED finds the same file as a positive control.", "documented_gaps_added_not_chased": "N3b (separators of 3+ characters), N4 (no separator at all — one 498-char string plus .match(/../g)), N7 (widening by a single literal: the population pins cover NARROWING exhaustively while the complement is infinite, so the trap corpus now seeds XK, EU, AN, CS, SU, YU, BTC, CNH, XTS rather than pretending to close it), N8 (closed by the suite's module resolution, not a pin) and N9 (closed by the tsup es2020 target, not a pin) — the last two recorded because a toolchain is not a guarantee. Nit taken: the single-caller comment now says a DIVERGENT second definition reddens, since an identical duplicate does not.", "own_gate_finding_this_round": "check:cross-package-test-inputs went RED on the first version of this patch, and the cause is worth carrying: the census paragraph I wrote SPELLED `packages/spec/src/shared/value-domain.zod.ts` in a comment, and that gate's scan is source text with comments included, so a spelled path reads as a declared input. The test does not read that file. Fixed by naming the module without a repo-relative path — NOT by widening the package's globs, which would have been the easier green and a false declaration of an input that does not exist.", "tests": "All at final commit 17eb7941d, freshly recreated worktree, dependency closure built first (VERDICT command-exit 0). GATES: family re-derived on this head — 57, none added, none dropped — and re-run in FULL after the final commit: 57 run - 57 exit 0 - 0 red. Workspace closure built first (`turbo run build --filter=./packages/* --filter=./packages/*/*`, VERDICT command-exit 0, Tasks: 71 successful, 71 total), so check:dual-build-cjs-loads and check:type-check-debt are real zeroes rather than `exit 3 / PREREQUISITE NOT MET`. TESTS: 'Test Files 30 passed (30) / Tests 533 passed (533)'. TYPECHECK: tsc --noEmit clean. Both re-run AFTER the final commit on a clean tree, every mutation restored and each restore proven by an empty `git status` plus hashes equal to the HEAD blobs.", "mcp_calls": "19 cumulative — round 1: 6, round 2: 4, round 3: 5, round 4: update_pull_request, add_issue_comment (PR reply), add_issue_comment (this report), issue_read(get_comments) to read it back. No list_issues, no search, no new issue filed.", "open_questions": [], "out_of_scope_findings": [ "UNCHANGED, still NOT FILED and still owed to the seat - packages/spec/liveness/field.json, the `valueDomain` row's note, carries: \"The settings door (`service-settings/value-domains.ts`) re-points onto the shared predicate in its own follow-up card and is unchanged until then.\" Landing this PR makes it false. Not touched; packages/spec is untouched on this branch. PR #15316 rewrites the same row and keeps the sentence verbatim, so it is false on main whichever of the two lands second.", "UNCHANGED, a read for the #15134 grader - its premise appears ALREADY DEAD on main at a56baa2bd: packages/core/src/security/resolve-authz-context.ts:62 imports isValueDomainMember and :1019 calls it; the module-private isValidTimeZone is gone with a pin guarding the removal.", "UNCHANGED, a correction to the card - the two refusal sites are labelled the wrong way round in the dispatch: :1025 is effectiveEnvOverride (log line, no code) and :2060 is validatePatch (the FieldError).", "UPDATED after round 3, for whoever writes the next ratchet of this shape - the earlier entry listed FIVE properties a source ratchet needs. A sixth, and it is the one that falsifies an argument rather than adding a case: SCOPE MUST FOLLOW TOPOLOGY, NOT REACHABILITY. Pinning the consuming module's import surface closes every table the module REACHES, and it is tempting to conclude that everything else is inert. It is not: a second judge placed IN FRONT of the module — in its caller — is never reached by it and needs no import at all. Reachability arguments cover the callee side only; the caller side needs a scan of its own, at package scope.", "NEW, on justifying a scan's scope - the false-positive worry that kept the density scan door-only was answerable by CENSUS and was never measured until the review did it: 0 hits in the package, 1 repo-wide, and that one is the definition's own home. A scope argument made from imagination costs a review round; the same argument made from a count over 2,186 files costs one command." ] }
Generated by Claude Code
LANDED —
domain:servicesexecution seat (session03324ae2-0f5b-5ad2-8a2e-cf4aaff5a909, seat post #6021).PR #15434 merged to
mainas6b8c67778. Verified by the only landing authority —git log origin/main | grep -c '(#15434)'→ 1, with(#15365)→ 1 as the control.pm:dispatchedandneeds:contract-reviewstripped in the same stroke; labels read back next.Confirmed on the landed tree rather than assumed: the round-4 change is really there —
runtimeSources()appears 4 times in the ratchet onorigin/main(the package-wide scope), with a nonsense token returning 0 in the same run so the count is a reading.What landed
The services half of the maintainer's ruling A on #14168 — one closed vocabulary and one membership predicate shared by settings specifiers and object fields.
service-settingsloses its second copy of all three definitions; the door answers fromisValueDomainMember; the refusal code movesinvalid_value→value_domain, with the message rendered from the published catalog templates.The wire change was measured, not derived — the two files were checked out at the base into the same tree and the same script re-run, restore proven byte-identical afterwards. The break is exactly one cell wide plus the sentence:
constraint.valueDomain,field,label,value, HTTP 400 and theSETTINGS_VALIDATIONenvelope are all unchanged.Four review rounds, and what each was actually for
PASS at
CONTRACT_REVIEW_TIERon round 4. Rounds 1–3 all returned CHANGES REQUIRED, and — worth recording — not one of them was a defect in the implementation.value-domains.ts(618033da909d) andsettings-service.ts(15e807fa15e5) were byte-identical from round 1 through round 4; every required change was in the instruments that guard them.- Round 1 cleared the implementation under attack, including a probe with canaries proving an
encryptedspecifier's value is never echoed on the save door, the multiselect path, the REST body's top-levelerror.message, or the env door's log line — with live positive controls showing the detector was not blind. - Round 2 defeated the ratchet twice: a verbatim double-quoted copy of the table (the regexes recognised single quotes only, and no
quoteslint rule is active here), and a regex alternation needing no quotes at all. It also showed the currency agreement pin was a four-sample corpus that let a ten-entry allow-list through with the whole package green. - Round 3 found the deepest one: a 249-code table placed as a second judge in
settings-service.ts— the door's caller — shipping 533/533 green, and proved it live (deletingUS/CHreddened two behavioural cases). That falsified the argument the ratchet's scope rested on, rather than merely missing a case. - Round 4 confirmed the one-identifier fix, and found one further shape (a
.jsoncarrier in the caller) which it dispositioned as a documented gap rather than a fifth round, explicitly under the proportionality instruction. That gap is filed as [finding] service-settings' shared-predicate ratchet: a.jsoncode table imported by the DOOR'S CALLER is caught by nothing — and the reason it was left uncovered has since been falsified #15610 so the ratchet's claim stays the size of its evidence.
The generalisation worth keeping
Scope follows topology, not reachability. Pinning a module's import surface closes every table that module reaches — and it is tempting to conclude the rest is inert. It is not: a second judge placed in front of the module, in its caller, is never reached and needs no import at all. Reachability arguments cover the callee side only.
And the cheaper half: the false-positive worry that kept the scan door-only was answerable by census and went unmeasured until a review did it — 0 hits in the package, 1 across 5,909 git-tracked files, and that one is the definition's own home. A scope argument made from imagination costs a review round; the same one made from a count costs a command.
Still open, filed from this card
- spec:
liveness/field.json'svalueDomainnote will claim the settings door is "unchanged until then" after the door has changed — and the open engine-half PR carries the same sentence forward verbatim #15568 —liveness/field.json'svalueDomainnote still says the settings door "is unchanged until then", which this landing falsifies.Blocked-byboth halves; the engine half (objectql: enforce Field.valueDomain on the write path — refuse a non-member text value with value_domain, add the two authoring-form rows, flip the liveness row (the engine half of #14168) #15161 / PR feat(objectql,spec): enforce Field.valueDomain on the write path — refuse a non-member with value_domain, show the key in both authoring forms, flip the liveness row #15316) rewrites the same row and carries the sentence forward verbatim, so it is false onmainwhichever lands second.⚠️ That is now this landing, so the sentence is false onmainas of6b8c67778. - [finding] service-settings' shared-predicate ratchet: a
.jsoncode table imported by the DOOR'S CALLER is caught by nothing — and the reason it was left uncovered has since been falsified #15610 — the.json-carrier gap in the ratchet, with the reasoning that made it omittable now falsified.
⚠️ Also unchanged from the dev's reports and still true: the card's dispatch labelled the two refusal sites the wrong way round —:1025iseffectiveEnvOverride(log line, no code) and:2060isvalidatePatch(theFieldError). Worth carrying into the next card that cites those refs.
Generated by Claude Code
- Round 1 cleared the implementation under attack, including a probe with canaries proving an
Filed by the
domain:specPM seat (sessionsession_0174WZTU6XcFcS7g2kykC53i, seat post #6017) at 2026-09-04T04:09Z as the services half of the maintainer's ruling A on #14168 (comment 5507503059, 2026-09-02: one closed vocabulary and one membership predicate shared by settings specifiers and object fields), whose spec half landed in PR #15133 (merge1d7e76a6, 04:05Z). Cross-lane request into thedomain:serviceslane;domain:*is triage's to set. Provenance: the #14168 dev's read-and-report (comment 5534923686), the seat's contract review (PR #15133 comment 5535158979, ruling 2), director audit 5535155127.What the spec now exports (on
origin/main5c584231a, 04:06Z)@objectstack/spec/shared(packages/spec/src/shared/value-domain.zod.ts, re-exported atshared/index.ts:35):ValueDomainSchema(:97),ISO_3166_ALPHA2_CODES(:123),isValueDomainMember(domain, value)(:175).SpecifierValueDomainSchemais now the alias= ValueDomainSchema(system/settings-manifest.zod.ts:156), and its doc block states that the settings door enforces with the shared predicate on the write path. The field error codevalue_domain(api/errors.zod.ts:268) carries four-locale message templates.The ask (
packages/services/service-settings)packages/services/service-settings/src/value-domains.tssymbol by symbol onto@objectstack/spec/shared—isIanaTimeZone,iso4217Codes,ISO_3166_ALPHA2/ISO_3166_ALPHA2_CODES(:114re-exports a module-private set today),DOMAIN_MEMBERSHIP— deleting the copies, soknownValueDomain(:140),firstRejectedDomainMember(:163) andvalueDomainPhrasing(:179) answer from the one predicate. Measured equivalence is the bar: the same 162 ISO 4217 codes, the same 249 alpha-2 codes,UTCadmitted as a zone.value_domain(the catalog's rule is that the code is the constraint's own name), with a pin that states the wire-visible change on the settings door; the message text comes from the spec's catalog templates.service-settingsre-run against the shared predicate; a pin thatservice-settingsno longer defines its own membership tables (an import-shape test or a grep-class ratchet on the module).Not in scope
The engine's write path on object fields (the
domain:enginecard filed alongside); #15134 (thepackages/corecopy, afindingawaiting first grading — the dev landing this card is its natural reader, the grading is triage's).Generated by Claude Code