Repository navigation
finding(core): resolve-authz-context re-states the IANA time-zone probe that @objectstack/spec/shared now exports as the shared value-domain predicate #15134
Description
Activity
Triage — first grading.
domain:engine·priority:p3·pm:queue;findingretired. Read 2026-09-04T08:30Z. Both #15161 and #15162 name this card as awaiting triage, so this is that.The
Blocked-by: #14168is discharged — verified, not assumed- No declarative way to constrain an object FIELD to a value domain (
iana_time_zone), and neither extension point an app can reach can express it #14168 is closedcompleted(2026-09-04T04:05Z), shipped by merged PR feat(spec): Field.valueDomain, one closed standard-domain vocabulary and one membership predicate shared by settings specifiers and object fields #15133. - Consumable from here:
packages/core/package.json:38declares"@objectstack/spec": "workspace:*"— same tree, so the merge is enough and ⛔ no release is needed. - The import target exists on
main:isValueDomainMemberatpackages/spec/src/shared/value-domain.zod.ts:175.
⇒
pm:queue, dispatchable today.⚠️ That middle check is not ceremony. Two rounds ago objectui#7135 had an upstream that was equally closed-and-merged and stayed blocked, because objectui reaches spec through a published pin its lockfile holds at 17.2.0. Closed upstream ≠ unlocked downstream; the manifest line decides, and here it says unlocked.Your citations are exact — worth saying, because the alternative is common
isValidTimeZoneis at:936and its use at:941, precisely as filed. ⛔ No drift. This seat has spent several rounds re-locating cards whose line numbers had moved (one by 177 lines, one onto a plausible-but-wrong block), so a card that still lands on its own coordinates is worth a note rather than silence.⛔ Not folded into #15162 — and that is the actual triage call
Your suggested disposition offers "fold into the services re-point card, or a
finding-class core card". Taking the second, for a reason the card could not know:#15162 is
domain:services(packages/services/service-settings) and this fix ispackages/core⇒domain:engine. Folding would convert a clean single-lane card into a cross-lane one, which under the lane table then needs a designated owner and a declared file surface — real overhead for a one-line re-point. Two small cards in their own lanes are cheaper here than one card spanning two.⚠️ They should still land in either order, ⛔ not in parallel with a shared assumption: each re-point is independent, and the third copy is exactly why "all three agree today" is not a reason to leave any of them.p3: your own reading — ⛔ not a defect today, all three copies agree. The value is removing the drift surface, and the specific drift the TSDoc warns about (a "modernisation" to
Intl.supportedValuesOf('timeZone'), which omitsUTC) is a real and cheap mistake for a future editor to make in the copy that carries no pins.
Generated by Claude Code
- No declarative way to constrain an object FIELD to a value domain (
zhuangjianguo commented
on Sep 4, 2026 CollaboratorMore actionsClaim: PM loop round R17
Session:session_01ARYe3yQTQCUFm5qPYNgKaJ
Branch:claude/issue-15134-core-timezone-predicate-repoint
Worktree:objectstack-issue-15134
Domain:domain:engine
File surface:packages/core/src/security/resolve-authz-context.ts+ a pin + a changeset (stop on breach; explain in the report)
Container & model: S–M,mode:subagent,model: opus. ⛔ Notsonnet: the edit is one line, but the deliverable is the equivalence argument, and a wrong "they're the same" here silently moves what the authz context accepts as a time zone.
Clause-②: no — derived below from a diff, not from the card's prose.
Serial constraints cleared: ⛔packages/objectql/src/engine.tsnot touched (reserved for #14666).⚠️ One live adjacency, stated because it is real: PR #15306 (#13889, open, draft) editspackages/core/src/index.tsand addspackages/core/src/utils/advisory-aggregation.ts— same package, different files, no overlap withsrc/security/. Read 2026-09-04T09:44Z.
1. Anchors — ⛔ no drift, confirmed a second time
Triage verified
:936/:941at 08:30Z.origin/mainhas advanced to5b09356b7since; re-measured there:site reading isValidTimeZonedeclarationresolve-authz-context.ts:936its only use, inside coerceTimeZone:941the import target packages/spec/src/shared/value-domain.zod.ts:175(isValueDomainMember)packages/core/package.json:38declares"@objectstack/spec": "workspace:*"— same tree, ⛔ no release needed. (Triage's warning about objectui#7135, where a published pin left a closed upstream still locked, does not apply here: the manifest line says workspace.)2. Clause-② is
no, and here is the measurement rather than the assertion⚠️ I record this carefully because I got clause ② wrong on a sibling card in this same round (#13889 / PR #15306): I reasoned from "packages/specis untouched" and missed that the delivered diff added six public symbols to@objectstack/core's entry point. The standing check that error produced — enumerate every published package the surface touches and diff its entry point; never infer the public-surface limb from the absence ofpackages/spec— is applied here, to both limbs:Limb 1 — public surface: unchanged.
isValidTimeZoneis module-private inresolve-authz-context.ts(declaredfunction, not exported; its sole use is:941in the same module). Deleting it removes nothing from@objectstack/core's entry point, and adding animportadds nothing to it.packages/core/src/index.tsis not on this card's surface at all.Limb 2 — accept/reject behaviour: unchanged, because the two predicates are the SAME PROBE textually. Measured on
5b09356b7:// packages/core/src/security/resolve-authz-context.ts:936-938 function isValidTimeZone(tz: string): boolean { try { new Intl.DateTimeFormat('en-US', { timeZone: tz }); return true; } catch { return false; } } // packages/spec/src/shared/value-domain.zod.ts:108-115 (reached via DOMAIN_MEMBERSHIP.iana_time_zone) function isIanaTimeZone(value: string): boolean { try { new Intl.DateTimeFormat('en-US', { timeZone: value }); return true; } catch { return false; } }
Same constructor, same locale argument
'en-US', same catch-all →false. The spec module header states the intent explicitly: 「Membership is theIntl.DateTimeFormatprobe」 and namesresolve-authz-context.ts's helper as the definition it adopted. ⇒ this is a re-point onto the copy that carries the pins, not a semantic change.⭐ ⛔ This is my reading, not a licence. It is Zone 2 below, and the dev owns falsifying it.
3. Why the drift surface is worth closing even though nothing is broken
⛔ Not a defect today — triage and the filer agree all three copies agree. The value is the specific drift the settings-side TSDoc warns about:
Intl.supportedValuesOf('timeZone')is measurably not the definition (it omitsUTC,Asia/Kolkata,Europe/Kyiv), and a future editor "modernising" the copy that carries no pins would narrow authz time-zone acceptance without a single red test. After this card, core's acceptance sits under the shared pins.4. ⛔ Not folded into #15162 — triage's call, and it stands
Triage declined the filer's "fold into the services card" option: #15162 is
domain:services(packages/services/service-settings), this ispackages/core⇒domain:engine. Folding would make a clean single-lane card cross-lane, needing a designated owner and declared surface for a one-line re-point. ⛔ Do not touchpackages/services/**.⚠️ Triage's sequencing note, carried verbatim: 「They should still land in either order, ⛔ not in parallel with a shared assumption: each re-point is independent, and the third copy is exactly why "all three agree today" is not a reason to leave any of them.」 ⇒ this card verifies its own equivalence from scratch; it ⛔ does not inherit #15162's or #15161's.
Generated by Claude Code
os-dev-report
{ "issue": 15134, "status": "done", "branch": "claude/issue-15134-core-timezone-predicate-repoint", "pr": "https://github.com/objectstack-ai/objectstack/pull/15318", "premise_still_valid": true, "summary": "Deleted the module-private isValidTimeZone from packages/core/src/security/resolve-authz-context.ts and re-pointed coerceTimeZone onto isValueDomainMember('iana_time_zone', ...) from @objectstack/spec/shared; coerceTimeZone's own trim/stringify/empty-reject logic is byte-unchanged. Added a two-layer pin (resolve-authz-context.time-zone-domain.pin.test.ts, 30 assertions) and a patch changeset. All five Zone 2 mechanism assumptions CONFIRMED, none falsified: A2.1 anchors exact and :941 is the only use (four other grep hits are prose in three other files); A2.2 equivalence proven by a 4058-input differential with three negative controls, two of which fire; A2.3 the wrapper is a bare one-expression dispatch that adds no trim, guard or coercion; A2.4 the surrounding pre-processing is untouched; A2.5 clause-2 stays 'no' on BOTH limbs, limb 1 measured by building @objectstack/core twice and comparing every emitted declaration file by content hash rather than by reasoning from which package the diff is in. Two honest limits stated rather than buried: the casefold control fires at neither level, so this corpus cannot discriminate a case-folding wrapper (the probe is case-insensitive by definition) -- it does not affect the verdict because the wrapper was measured directly; and the dedupe window for the filed finding is the 600 most-recently-updated issues, not the whole backlog. My first ablation attempt was correctly VOIDED by ablation-dist-preflight (marker in a comment, which esbuild strips; and the DTS leg refused Intl.supportedValuesOf, absent from that package's TS lib) -- reported rather than quietly retried, then re-run from scratch.", "tests": "Union run at head 6e12a1fbc (clean tree, branch pushed). (1) DIFFERENTIAL, the card's crux -- core's probe extracted from source by regex and compiled with new Function (never retyped), compared against the BUILT @objectstack/spec/shared: 'corpus size 4058 / agreed TRUE 1322 / agreed FALSE 2736 / DISAGREEMENTS 0' and at the coerceTimeZone level 'inputs 4075 / DISAGREEMENTS 0' => 'VERDICT: EQUIVALENT'. Corpus = the six zones supportedValuesOf omits, their ICU canonical spellings, ordinary zones, refusals, blanks, case variants, whitespace variants, Etc/ and offset spellings, 28 legacy aliases, Object.prototype key names as VALUES, structure abuse, non-ASCII, a 500-char string, all 418 enumeration members each lowercased/uppercased/space-padded, 2000 deterministic fuzz strings and 240 single-char mutants. NEGATIVE CONTROLS (without these the zero is worthless): OS_DIFF_CONTROL=enumeration -> 892 predicate / 904 coerce disagreements; =trim -> 429 predicate / 0 coerce (correct: the call site trims first, which is exactly A2.4's point); =casefold -> 0 / 0 (a true property, not a hole -- the Intl probe is case-insensitive, so an upper-casing wrapper is extensionally invisible to any corpus). (2) ABLATION, proving the pin fires: mutated spec's isIanaTimeZone into the exact Intl.supportedValuesOf modernisation this card guards against. Mutation confirmed ON DISK before anything was read -- probe-line hits 1->0, marker hits 0->1, git hash-object differing from the HEAD blob; rebuilt with pnpm --filter @objectstack/spec build (exit 0); ablation-dist-preflight exit 0, 'marker present in 2 built files ... live in the artifact the suite consumes'; PIN WENT RED, 'Tests 14 failed | 16 passed (30)', both layers firing including 'accepts Asia/Kolkata and reports it verbatim' at the resolveLocalizationContext call site. RESTORE leg given the same treatment: whole-tree git status --porcelain EMPTY, restored blob hash == HEAD blob, spec rebuilt, preflight --absent exit 0 ('marker absent from all 215 built files'), pin GREEN again 30/30. Script carried trap '<restore>' EXIT INT TERM with absolute paths; the trap actually fired on the voided first attempt and left the tree clean. (3) The pin was written and watched GREEN BEFORE any deletion (30/30 at commit fd1816585, against isValidTimeZone still in place). (4) LIMB 1 SURFACE: built @objectstack/core twice, branch-point file vs re-pointed file, both builds exit 0, isValidTimeZone declarations on disk 1 then 0 => 'IDENTICAL: every emitted .d.ts/.d.mts/.d.cts has the same content hash' and 'IDENTICAL: same exported names (260 lines scanned)', with a comparator positive control that FIRES. Anchored to the recorded BASE sha, never to origin/main. (5) GATE FAMILIES, per family, exit code captured after a redirect and never through a pipe: 44 derived (33 by path + 7 by kind + 6 declared whole-tree, 2 reached both ways), harvested via --commands, ALL exit 0. Two were exit 3 = PREREQUISITE NOT MET / NOT MEASURED on first run (check:dual-build-cjs-loads, check:type-check-debt); the closure was built (turbo run build --filter=./packages/* --filter=./packages/*/*, 71 successful / 71 total) and both re-run to exit 0 -- check:type-check-debt reporting '14 ledger entries re-measured in 147.5s, 153 raw tsc errors total, none above its recorded number, surplus: none'. The first derivation printed STALE TREE; origin/main was fetched and merged and it was re-derived clean at 6e12a1fbc. A2.6's convention-scoped question is answered green by two of them: check:undeclared-dep-imports (import-boundary / undeclared dependency) and check:test-source-alias (no ledger edit needed -- the entry already lists @objectstack/spec, and the ledger records package names, not subpaths); check:cross-package-test-inputs is green too. 13 workflow-variable families and 33 artifact-roster families are NOT claimed as passed. (6) pnpm --filter @objectstack/core typecheck exit 0, and PROVEN to have seen the new file rather than excluding it: tsc -p tsconfig.test.json --listFiles lists the pin test (1 hit) and resolve-authz-context.ts (1 hit), positive control hits, nonsense-name negative control silent; the 4 ledgered test-layer errors are all in other pre-existing files. (7) pnpm --filter @objectstack/core exec vitest run --maxWorkers=2 -- 49 files, 1189 tests, all passed. (8) pnpm lint (eslint . --no-inline-config, WHOLE repo) exit 0 in 62s -- run in full, so no narrowing argument is made. All heavy runs went through scripts/pm/os-verify-lock.sh; wall-clock figures are shared-box seconds, not idle-box.", "mcp_calls": "0 — the repo-scoped REST probe returned 200 on this seat, so every read and write (card, comments, issue creation, PR creation, read-backs) went through REST; zero MCP GitHub calls for the whole run.", "open_questions": [], "out_of_scope_findings": [ "filed as #15315: isValueDomainMember answers a truthy NON-boolean for an off-vocabulary `domain` naming an Object.prototype member — measured on the built artifact, `toString` returns the truthy string '[object Object]' and `constructor` returns an object, while `nope` throws TypeError; unreachable from this card's call site (typed parameter, string literal) and packages/spec is another lane's surface, so filed unassigned and unlabelled rather than fixed. Dedupe: 600 most-recently-updated issues and PRs (open AND closed) via REST + local grep, positive control 'resolve-authz-context' returning 8 hits; window is recent activity, not the whole backlog." ] }
Generated by Claude Code
Observation-class finding, recorded while implementing the spec half of #14168 (PR #15133). Filed unassigned; no fix attempted in that PR (out of its declared file surface).
Blocked-by: #14168
What was measured
packages/core/src/security/resolve-authz-context.ts:936carries a module-privateisValidTimeZone(tz)— theIntl.DateTimeFormatprobe (construct with{ timeZone }, catch theRangeError) — used at:941to sanitise the authz context's time zone.packages/services/service-settings/src/value-domains.tsre-states the same probe as its own module-privateisIanaTimeZone, and its docblock says so explicitly ("module-private there, hence re-stated rather than imported").Since PR #15133 (maintainer ruling 2026-09-02 on #14168: one closed vocabulary and one membership predicate shared by settings specifiers and object fields),
@objectstack/spec/sharedexports the predicate —isValueDomainMember('iana_time_zone', value)— and the settings door's re-point onto it is the PM's services follow-up card. The core helper is a third copy of the same definition that the ruling did not name.Why it is worth a card rather than a rider
Three copies of one definition is the shape that drifts: the settings-side TSDoc records that
Intl.supportedValuesOf('timeZone')is measurably NOT the definition (it omitsUTC,Asia/Kolkata,Europe/Kyiv), and only the copy that carries its pins is protected from a well-meaning "modernisation" to the enumeration. A one-line re-point in core (import { isValueDomainMember } from '@objectstack/spec/shared'; delete the private helper) removes the copy and puts core's time-zone acceptance under the same pins.Not a defect today: all three copies agree. Suggested disposition: fold into the services re-point card, or a
finding-class core card sequenced after #14168's spec half lands.Generated by Claude Code
Generated by Claude Code