refactor(core): the authz context's time-zone probe is the shared value-domain predicate, not a third copy of it - #15318
Conversation
…d value-domain predicate Written and watched green BEFORE the re-point, against the module-private `isValidTimeZone` still in place: the pin measures the definition, not the edit that is about to happen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
… shared value-domain predicate
Delete the module-private `isValidTimeZone` (the third copy of one
`Intl.DateTimeFormat` definition) and call
`isValueDomainMember('iana_time_zone', ...)` from `@objectstack/spec/shared`.
`coerceTimeZone`'s own pre-processing is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…re-timezone-predicate-repoint
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 24 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c45bd3075ef4662f453df29466dd5dda00f9c8ab && git checkout c45bd3075ef4662f453df29466dd5dda00f9c8ab
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8b54e9de51b43ad808bc2ef6f0a97793c653b262 6e12a1fbc6d5a7c2c3bf34c13ff87c5aeb19b823 && git checkout -B drift-repro 8b54e9de51b43ad808bc2ef6f0a97793c653b262 && git merge --no-ff 6e12a1fbc6d5a7c2c3bf34c13ff87c5aeb19b823
node scripts/docs-audit/affected-docs.mjs --json 8b54e9de51b43ad808bc2ef6f0a97793c653b262
|
ACCEPT — flipping ready and enqueuing. Clause-②
|
Fixes #15134
The edit is one line. The deliverable is the equivalence argument, so that is what this body is.
packages/core/src/security/resolve-authz-context.tscarried a module-privateisValidTimeZone— a third re-statement of oneIntl.DateTimeFormatdefinition. It is deleted;coerceTimeZonenow callsisValueDomainMember('iana_time_zone', ...)from@objectstack/spec/shared. A new pin puts core's time-zone acceptance under the shared definition, and an ablation proves that pin fires.All measurements below were taken in this card from scratch. Per triage's sequencing note — 「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.」 — nothing here is inherited from the sibling re-point cards and neither is cited as evidence.
Verification union run at
6e12a1fbc, the head of this branch, with a clean working tree.Zone 2, item by item
A2.1 — the anchors. CONFIRMED, located by symbol.
Declaration at
:936, exactly as filed.:941really is the ONLY use — the other four hits are prose in docblocks and a comment, in three other files.isValueDomainMemberis atpackages/spec/src/shared/value-domain.zod.ts:175, andpackages/spec/src/shared/index.ts:35re-exports the module, so it is reachable on the./sharedsubpath (declared inpackages/spec/package.jsonexports).packages/core/package.json:38is"@objectstack/spec": "workspace:*"— triage's citation is exact.A2.2 — THE CRUX. CONFIRMED by differential measurement, not by reading.
The core probe was extracted from the source file by regex and compiled with
new Functionrather than retyped, so "I transcribed it correctly" is not one of the things you have to take on trust. The shared side was imported from the built@objectstack/spec/shared, i.e. the artifact core actually resolves.Extracted body, echoed by the harness:
Result:
The corpus, 4,058 inputs. Both verdicts are well represented (1,322 accepts, 2,736 refusals), so this is not a vacuous all-false agreement.
UTC,Asia/Kolkata,Europe/Kyiv,Asia/Ho_Chi_Minh,US/Eastern,GMTAsia/Calcutta,Europe/Kiev,Asia/SaigonAmerica/Argentina/Buenos_Aires)Mars/Olympus,Not/AZone,Europe/Munich,America/Atlantis,Foo/Barutc,Utc,uTc,europe/zurich,EUROPE/ZURICH,asia/kolkata,gmt,us/easternUTC,UTC,UTC,Asia/Kolkata,Europe/ Paris,Europe /Paris, tab- and newline-paddedEtc/and offset spellings:Etc/GMT+5,Etc/GMT-14,Etc/Unknown,+05:30,ZFactory,PST8PDT,CET,Universal,Zulu,Japan,W-SU,NZ-CHAT, and 19 moreObject.prototypekey names as values:__proto__,constructor,toString,valueOf,hasOwnPropertyIntl.supportedValuesOf('timeZone'), each also lowercased, uppercased and space-padded (1,672 inputs)The harness is falsifiable — three negative controls, two of which fire. Without these, "0 disagreements" would be worth nothing.
OS_DIFF_CONTROL)coerceTimeZone-levelenumeration— the exact "modernisation" this card guards againsttrim— a wrapper that normalises whitespacecasefold— a wrapper that upper-casesTwo readings worth stating plainly rather than burying:
trimcontrol fires at the predicate level and not at thecoerceTimeZonelevel. That is correct and it is the point of A2.4 below: the call site's own trim runs first, so it absorbs a normalising wrapper entirely. The corpus can see that class only at the predicate level, which is where it was measured.casefoldcontrol fires at neither level. This is a true property, not a hole I am glossing: theIntl.DateTimeFormatprobe is case-insensitive (the spec TSDoc says so, andeurope/zurichconstructs fine), so an upper-casing wrapper is extensionally invisible. This corpus therefore cannot discriminate a case-folding wrapper — stated so nobody reads the table as stronger than it is. It does not affect the verdict, because the wrapper is measured directly under A2.3 and does no case folding.A2.3 — the wrapper is not the predicate. CONFIRMED.
The dispatch, read off the built artifact rather than the source (
packages/spec/dist/shared/index.mjs:540-548):One expression. No normalisation, no guard, no coercion between the call and
isIanaTimeZone. Probed directly:The core call site passes the string literal
'iana_time_zone', which is a member of the vocabulary, so the dispatch cannot miss. A pin for the no-trim property is in the test file, because the call site's own trim would otherwise hide a wrapper that gained one.One thing I measured here is out of this card's scope and is filed separately as #15315: for a domain argument outside the vocabulary that names an
Object.prototypemember,DOMAIN_MEMBERSHIP[domain]resolves to the inherited function —toStringanswers the truthy string[object Object],constructoranswers an object, whilenopethrows. Unreachable from here (the parameter is typed, and this call site passes a literal), and ⛔ untouched:packages/specis another lane's surface.A2.4 —
coerceTimeZone's pre-processing. UNCHANGED, byte-verified.The whole diff of that function:
The trim / stringify line is untouched, and the
s &&empty-rejection is intact. Only the predicate call moved. The end-to-end pin asserts the surrounding behaviour explicitly (Asia/Kolkatatrims toAsia/Kolkata, a non-string carrier goes throughString(...), blank and nullish fall back toUTC), so a later "cleanup" of it goes red.A2.5 — Clause ② is
no. Both limbs measured.Limb 1 — public surface. CONFIRMED by building the package twice, rather than by reasoning from which package the diff is in.
@objectstack/corewas built with the branch-point file and again with the re-pointed file, and every emitted declaration file was compared by content hash:Byte-identical declaration output is also the consumer answer: no downstream package can see a type change through an unchanged
.d.ts. Anchored to the recorded branch-point sha, never toorigin/main(a shared ref another agent's fetch moves under the worktree). The leg script restores under a trap with absolute paths, and both legs verify the on-disk blob hash before building.Limb 2 — accept/reject behaviour. CONFIRMED by A2.2 and A2.3 above: 4,058 inputs, zero disagreements, with a control that produces 892 when the predicates really do differ.
⇒ Clause ② stays
no. Nothing left the entry point and nothing moved at the door.A2.6 — gate families. Derived, then run PER FAMILY.
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack— no paths passed, so the change set comes from the tool's own merge-base walk (3 paths). The first derivation reported STALE TREE;origin/mainwas fetched and merged and it was re-derived at6e12a1fbc, where the provenance line is clean. Reconciliation: 44 families — 33 by path, 7 by change kind, 6 declared whole-tree, 2 reached both ways. Harvested with--commands(44 lines) rather than by grepping the prose, and run one at a time with the exit code captured after a redirect, never through a pipe.node scripts/check-adr-0087-registration.mjs --self-testnode scripts/check-changeset-no-major.mjs --self-testnode scripts/check-ci-filter-parity.mjsnode scripts/check-closing-keyword-parity.mjsnode scripts/check-closing-keyword-parity.mjs --self-testnode scripts/check-comment-mask-adoption.mjsnode scripts/check-comment-mask-adoption.mjs --self-testnode scripts/check-comment-mask-corpus.mjsnode scripts/check-empty-changeset.mjs --self-testnode scripts/check-keyed-text-bounds.mjsnode scripts/check-keyed-text-bounds.mjs --self-testnode scripts/check-plugin-teardown-shape.mjsnode scripts/check-plugin-teardown-shape.mjs --self-testnode scripts/check-system-context-census.mjsnode scripts/check-system-context-census.mjs --self-testnode scripts/check-undeclared-dep-imports.mjsnode scripts/check-undeclared-dep-imports.mjs --self-testnode scripts/docs-audit/check-affected-docs.mjsnode scripts/docs-audit/check-drift-comment.mjsnode scripts/pm/release-rehearsal-clone.mjs --self-testpnpm check:authz-resolverpnpm check:changeset-gate-self-testspnpm check:cross-package-test-inputspnpm check:dispatcher-error-vocabularypnpm check:doc-authoringpnpm check:dual-build-cjs-loadspnpm check:engine-double-contractpnpm check:kernel-hook-pairspnpm check:logger-receiver-detachpnpm check:nul-bytespnpm check:objectql-double-limitpnpm check:objectui-changesetpnpm check:page-declaration-shapepnpm check:pm-half-statespnpm check:published-filespnpm check:query-options-erasurepnpm check:refd-timer-probepnpm check:slot-lookuppnpm check:test-source-aliaspnpm check:type-check-coveragepnpm check:type-check-debtpnpm check:type-source-resolutionpnpm check:watch-hint-literalpnpm check:where-matcherThe two exit 3s were NOT MEASURED, not passes. Both refused for the same missing prerequisite — no built workspace closure.
pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*'was run (71 successful, 71 total) and both were re-run:check:dual-build-cjs-loadsexit 0,check:type-check-debtexit 0 (14 ledger entries re-measured in 147.5s, 153 raw tsc errors total, none above its recorded number,surplus: none).Two of the runnable families answer A2.6's convention-scoped question directly, and both are green:
check:undeclared-dep-imports(the import-boundary / undeclared-dependency check — the one that would fire on a cross-package import the manifest does not declare) andcheck:test-source-alias(the shrink-only unaliased-artifact ledger). The latter needed no ledger edit:KNOWN_UNALIASED_TEST_IMPORTS['@objectstack/core']already lists@objectstack/spec, and the ledger records package names, not subpaths.check:cross-package-test-inputsis green too — the new test reads no file outside its own package, it only imports.The derivation also lists 13 families whose argv takes a value from the workflow (
$MERGE_BASE,$RUNNER_TEMP,matrix.shard) and are ⛔ not runnable locally, plus 33 artifact-roster families whose silence is a fact about a list rather than about these paths. Neither group is claimed as passed here.A2.7 — the changeset bump:
patch. Derived.Three inputs:
minor, so it was measured rather than assumed.@objectstack/specis already adependenciesentry of@objectstack/core(packages/core/package.json:38,workspace:*), and core already imports values from it at runtime —postureEnforcesWallfrom@objectstack/spec/securitysits two lines above the new import in this very file. What is new is only the./sharedsubpath, which that same already-declared dependency exports in its ownexportsmap. No manifest line changes;check:undeclared-dep-importsis green.On the subpath being new to core, the zero-hit grep and its firing positive control:
⇒ a patch to one package, invisible to every consumer.
check:changeset-no-majorandcheck:empty-changesetself-tests are green, and no ADR-0087 marker is owed (that gate judges declared-breaking changesets only).The test is the actual product
packages/core/src/security/resolve-authz-context.time-zone-domain.pin.test.ts, 30 assertions in two layers: the shared predicate against a freshly constructedIntl.DateTimeFormatprobe over the corpus, andresolveLocalizationContextend-to-end, which is the only thing that pins what core accepts rather than what the spec package exports. Accepted-case assertions use spellings that are accepted but not equal toUTC(utclowercased,Asia/Kolkata,europe/zurich), becauseUTCis also the fallback and could not tell accept from reject. It carries its own anti-vacuity control asserting that the enumeration still omits the six trap zones.Following the suggested route, it was written and watched green before anything was deleted — 30/30 against the module-private
isValidTimeZonestill in place (commitfd1816585), so it measures the definition rather than the edit.Ablation — the pin does fire.
isIanaTimeZoneinpackages/specwas mutated into the exactIntl.supportedValuesOf('timeZone')modernisation this card exists to guard against.1 -> 0, marker hits0 -> 1, andgit hash-objectdiffering from the HEAD blobpnpm --filter @objectstack/spec buildexit 0, thennode scripts/ablation-dist-preflight.mjs @objectstack/spec 'OS_ABLATION_ENUM_MARKER'exit 0 —marker present in 2 built files ... the ablation is live in the artifact the suite consumesTests 14 failed | 16 passed (30). Both layers fired, includingaccepts Asia/Kolkata and reports it verbatimandaccepts utc and reports it verbatimat theresolveLocalizationContextcall sitegit status --porcelainempty, restored blob hash equal to the HEAD blob, spec rebuilt,--absentpreflight exit 0 (marker absent from all 215 built files), pin GREEN again 30/30The first attempt at this ablation was correctly voided by the preflight and is reported rather than quietly retried: the mutation put its marker in a comment, which esbuild strips, so the marker reached only sourcemaps and the run said so. The DTS leg also failed because
Intl.supportedValuesOfis not in that package's TS lib. Both were fixed (a live string comparison as the marker, a cast for the call) and the ablation was re-run from scratch.Everything else that was run
pnpm --filter @objectstack/core typecheck— exit 0. Verified it actually saw the new file rather than excluding it:tsc -p tsconfig.test.json --listFilesliststime-zone-domain.pin.test.ts(1 hit) andresolve-authz-context.ts(1 hit), with a positive control hitting and a nonsense-name negative control silent. The 4 ledgered test-layer errors are all in other, pre-existing files.pnpm --filter @objectstack/core exec vitest run --maxWorkers=2— 49 files, 1189 tests, all passed.resolve-authz-context.*suites specifically — 4 files, 168 tests, passed.pnpm lint(eslint . --no-inline-config, whole repo) — exit 0 in 62s. Run in full, so no narrowing argument is being made.pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*'— 71 successful, 71 total.Wall-clock figures above were measured on a shared container under the verify lock, which excludes other locked runs and nothing else; they are not idle-box numbers.
Out of scope, filed not fixed
isValueDomainMemberanswers a truthy non-boolean for an off-vocabularydomainnaming anObject.prototypemember. Unreachable from this call site, andpackages/specis another lane's surface. Filed unassigned and unlabelled. Dedupe: 600 most-recently-updated issues and PRs (open and closed) pulled via REST and grepped locally forisValueDomainMember,DOMAIN_MEMBERSHIP,valueDomain,value_domain,value-domain; positive controlresolve-authz-contextreturned 8 hits, so the scan was live. The window is recent activity, not the whole backlog — stated so it is not read as exhaustive.⛔ Untouched, as instructed:
packages/services/**,packages/spec/**(the ablation mutated and fully restored one file there; the tree is clean), andpackages/objectql/src/engine.ts.🤖 Generated with Claude Code
https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
Generated by Claude Code