Skip to content

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

@claude

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:936 carries a module-private isValidTimeZone(tz) — the Intl.DateTimeFormat probe (construct with { timeZone }, catch the RangeError) — used at :941 to sanitise the authz context's time zone. packages/services/service-settings/src/value-domains.ts re-states the same probe as its own module-private isIanaTimeZone, 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/shared exports 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 omits UTC, 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

Activity

  1. huangyiirene commented on Sep 4, 2026

    @huangyiirene
    Collaborator

    Triage — first grading. domain:engine · priority:p3 · pm:queue; finding retired. Read 2026-09-04T08:30Z. Both #15161 and #15162 name this card as awaiting triage, so this is that.

    The Blocked-by: #14168 is discharged — verified, not assumed

    ⇒ 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

    isValidTimeZone is at :936 and 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 is packages/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 omits UTC) is a real and cheap mistake for a future editor to make in the copy that carries no pins.


    Generated by Claude Code

  2. self-assigned this
    on Sep 4, 2026
  3. zhuangjianguo commented on Sep 4, 2026

    @zhuangjianguo
    Collaborator

    Claim: 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. ⛔ Not sonnet: 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.ts not touched (reserved for #14666). ⚠️ One live adjacency, stated because it is real: PR #15306 (#13889, open, draft) edits packages/core/src/index.ts and adds packages/core/src/utils/advisory-aggregation.ts — same package, different files, no overlap with src/security/. Read 2026-09-04T09:44Z.


    1. Anchors — ⛔ no drift, confirmed a second time

    Triage verified :936 / :941 at 08:30Z. origin/main has advanced to 5b09356b7 since; re-measured there:

    site reading
    isValidTimeZone declaration resolve-authz-context.ts:936
    its only use, inside coerceTimeZone :941
    the import target packages/spec/src/shared/value-domain.zod.ts:175 (isValueDomainMember)

    packages/core/package.json:38 declares "@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/spec is 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 of packages/spec — is applied here, to both limbs:

    Limb 1 — public surface: unchanged. isValidTimeZone is module-private in resolve-authz-context.ts (declared function, not exported; its sole use is :941 in the same module). Deleting it removes nothing from @objectstack/core's entry point, and adding an import adds nothing to it. packages/core/src/index.ts is 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 the Intl.DateTimeFormat probe」 and names resolve-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 omits UTC, 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 is packages/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 touch packages/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

  4. claude commented on Sep 4, 2026

    @claude
    ContributorAuthor

    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

  5. removed their assignment
    on Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions