Skip to content

Commit 2f5f1f0

Browse files
committed
fix(spec): record the shipped IANA time-zone narrowing in the ADR-0087 ledger
`#16296` gave `sys_job.timezone` and `sys_report_schedule.timezone` the `valueDomain: 'iana_time_zone'` declaration — a write-time narrowing that shipped with no breaking-change marker, so the repo's detector classified it non-breaking and asked for no ADR-0087 disposition. The ledger now carries a `semantic` entry for it, protocol 18, stating what narrowed and the one-line fix per offending row. Nothing is re-released and nothing is ratified in silence. ⚠️ The ruling named TWO narrowings. The second, `#14238`, is NOT one: its diff is 535 insertions and 0 deletions across 11 files — two nullable columns ADDED, carrying the value domain from their first release — and its own changeset says so in writing ("A NON-BREAKING ADDITION ... ADR-0087's registries have no additive entry kind"). A row claiming it narrowed something would be false data in the one ledger this mechanism keeps true, which ADR-0087 refuses by name. The finding is reported to the seat rather than written into the ledger. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
1 parent 1d576b6 commit 2f5f1f0

3 files changed

Lines changed: 144 additions & 0 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): record the shipped `sys_job` / `sys_report_schedule` IANA narrowing in the ADR-0087 ledger (#16421)
6+
7+
Clause-②: no
8+
9+
`#16296` gave `sys_job.timezone` and `sys_report_schedule.timezone` the
10+
`valueDomain: 'iana_time_zone'` declaration. That is a write-time narrowing — a
11+
string these columns used to accept is now refused with the ADR-0114 field code
12+
`value_domain` — and it shipped with no breaking-change marker at all, so the
13+
repo's own detector classified it non-breaking and asked for no ADR-0087
14+
disposition. Measured on the shipped changeset, not inferred.
15+
16+
The ledger now carries a `semantic` entry for it
17+
(`platform-timezone-columns-iana-domain-refused`, protocol 18). Nothing is
18+
re-released and nothing is ratified in silence: the entry states what narrowed,
19+
the one-line fix per offending row (write the canonical zone id, or clear the
20+
column), and the fact that a stored non-member is still readable and still
21+
returned unchanged — it fails only on the row's next write. For
22+
`sys_report_schedule` that refusal is the point: a non-member zone was silently
23+
discarding the cron expression and falling back to `interval_minutes` forever.
24+
25+
No authorable key, export, config field or stored shape moves, and no DDL is
26+
planned — this is a record of a change that already shipped, published so that
27+
`objectstack migrate meta`'s consumers can read it.
28+
29+
Maintainer ruling, director summon #17, decision batch #2 item 1, option B,
30+
quoted verbatim and untranslated: 「同意」.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { SemanticMigration } from '../../types.js';
4+
5+
export const entry: SemanticMigration = {
6+
id: 'platform-timezone-columns-iana-domain-refused',
7+
surface:
8+
'The two platform audit time-zone columns — `sys_job.timezone` and '
9+
+ '`sys_report_schedule.timezone` — carrying a string that is not a member of the '
10+
+ 'IANA time-zone database (`Asia/Shangai`, `Europe/Munich`, `UTC+8`, `PST`).',
11+
replacement:
12+
'The canonical IANA zone id the deployment meant, written in the spelling the tzdb '
13+
+ 'uses: `Asia/Shanghai`, `Europe/Berlin`, `America/Los_Angeles`. `UTC` is a member '
14+
+ 'and is admitted — membership is the shared `Intl.DateTimeFormat` probe, never the '
15+
+ '`Intl.supportedValuesOf(\'timeZone\')` enumeration, which omits `UTC` and would '
16+
+ 'refuse the one fallback this contract names. ⚠️ A non-member is RE-AUTHORED, never '
17+
+ 'repaired on the deployment\'s behalf: the correct zone behind a typo is a fact only '
18+
+ 'the deployment holds, which is what makes this entry semantic rather than a D2 '
19+
+ 'conversion.',
20+
reason:
21+
'#16296 gave both columns `valueDomain: \'iana_time_zone\'`, which had been declared '
22+
+ 'on `sys_business_unit.timezone` / `sys_organization.timezone` since #14238. It is a '
23+
+ 'WRITE-TIME narrowing of the `min`/`max`/`maxLength` transition-gate class: a value '
24+
+ 'already stored outside the domain is never re-read against it, no DDL is planned, '
25+
+ 'and `objectstack migrate meta` has nothing to rewrite — the changeset that shipped '
26+
+ 'it says so in those words, and this entry does not contradict it. What the '
27+
+ 'changeset had no way to carry is that a deployment holding such a value now has '
28+
+ 'WORK TO DO: the next write of that row is refused with the ADR-0114 field code '
29+
+ '`value_domain`, and until then `sys_report_schedule.timezone` keeps doing the thing '
30+
+ 'the narrowing exists to stop — `ReportService.nextRunAt` hands a non-member zone to '
31+
+ 'croner, whose throw was caught and turned into a silent fall back to '
32+
+ '`interval_minutes`, so "every weekday 09:00 Asia/Shanghai" became "every 1440 '
33+
+ 'minutes, forever". Not a throw and not a fall back to UTC: the wrong instant, '
34+
+ 'permanently. ⛔ It went out with NO `**BREAKING**` marker, so the repo\'s own '
35+
+ 'breaking-change detector classified it non-breaking and asked for no ADR-0087 '
36+
+ 'disposition at all — measured on the shipped changeset. #16421 closed that hole '
37+
+ '(the declaration now carries a `(narrowing)` arm the gate reads instead of a prose '
38+
+ 'banner) and this row is the other half of the same ruling: the narrowing that '
39+
+ 'already shipped is RECORDED, ⛔ not re-released and ⛔ not ratified in silence. '
40+
+ 'Maintainer ruling, director summon #17, decision batch #2 item 1, option B, '
41+
+ 'verbatim and untranslated: 「同意」. The direct precedents for registering a change '
42+
+ 'no transform can apply are `schedule-flow-acting-organization-required` (protocol '
43+
+ '18) and `rest-requireauth-default-flip` (protocol 12) — behaviour-only, a '
44+
+ 'deployment judgement, registered anyway because the prescription is real.',
45+
acceptanceCriteria:
46+
'Every `sys_job.timezone` and `sys_report_schedule.timezone` value stored in the '
47+
+ 'deployment is an IANA member. The one-line fix per offending row: write the '
48+
+ 'canonical zone id (`UPDATE … SET timezone = \'Asia/Shanghai\'`), or clear the '
49+
+ 'column — `sys_report_schedule` documents a `UTC` default and `sys_job` has no '
50+
+ 'reader at all. Rows already holding a member parse and behave byte-identically to '
51+
+ 'before; rows holding none are readable, are returned unchanged, and fail only on '
52+
+ 'their next WRITE. A report schedule that was silently running on '
53+
+ '`interval_minutes` resumes its cron cadence once its zone is a member — that '
54+
+ 'resumption, not the absence of an error, is how the fix is verified. ⚠️ The two '
55+
+ 'columns\' `maxLength` (100 vs 64) and defaults (none vs `UTC`) are deliberately '
56+
+ 'still unconverged and are NOT part of this entry; no member is longer than 32 '
57+
+ 'characters on the current Node baseline, so neither bound admits anything the '
58+
+ 'domain does not.',
59+
};

‎packages/spec/src/migrations/registry.ts‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9676,6 +9676,61 @@ const step18: MigrationStep = {
96769676
+ 'who was previously outside an `assignedProfiles` list and could nonetheless open the page '
96779677
+ 'is the pre-existing state, not a regression introduced by the removal.',
96789678
},
9679+
{
9680+
id: 'platform-timezone-columns-iana-domain-refused',
9681+
surface:
9682+
'The two platform audit time-zone columns — `sys_job.timezone` and '
9683+
+ '`sys_report_schedule.timezone` — carrying a string that is not a member of the '
9684+
+ 'IANA time-zone database (`Asia/Shangai`, `Europe/Munich`, `UTC+8`, `PST`).',
9685+
replacement:
9686+
'The canonical IANA zone id the deployment meant, written in the spelling the tzdb '
9687+
+ 'uses: `Asia/Shanghai`, `Europe/Berlin`, `America/Los_Angeles`. `UTC` is a member '
9688+
+ 'and is admitted — membership is the shared `Intl.DateTimeFormat` probe, never the '
9689+
+ '`Intl.supportedValuesOf(\'timeZone\')` enumeration, which omits `UTC` and would '
9690+
+ 'refuse the one fallback this contract names. ⚠️ A non-member is RE-AUTHORED, never '
9691+
+ 'repaired on the deployment\'s behalf: the correct zone behind a typo is a fact only '
9692+
+ 'the deployment holds, which is what makes this entry semantic rather than a D2 '
9693+
+ 'conversion.',
9694+
reason:
9695+
'#16296 gave both columns `valueDomain: \'iana_time_zone\'`, which had been declared '
9696+
+ 'on `sys_business_unit.timezone` / `sys_organization.timezone` since #14238. It is a '
9697+
+ 'WRITE-TIME narrowing of the `min`/`max`/`maxLength` transition-gate class: a value '
9698+
+ 'already stored outside the domain is never re-read against it, no DDL is planned, '
9699+
+ 'and `objectstack migrate meta` has nothing to rewrite — the changeset that shipped '
9700+
+ 'it says so in those words, and this entry does not contradict it. What the '
9701+
+ 'changeset had no way to carry is that a deployment holding such a value now has '
9702+
+ 'WORK TO DO: the next write of that row is refused with the ADR-0114 field code '
9703+
+ '`value_domain`, and until then `sys_report_schedule.timezone` keeps doing the thing '
9704+
+ 'the narrowing exists to stop — `ReportService.nextRunAt` hands a non-member zone to '
9705+
+ 'croner, whose throw was caught and turned into a silent fall back to '
9706+
+ '`interval_minutes`, so "every weekday 09:00 Asia/Shanghai" became "every 1440 '
9707+
+ 'minutes, forever". Not a throw and not a fall back to UTC: the wrong instant, '
9708+
+ 'permanently. ⛔ It went out with NO `**BREAKING**` marker, so the repo\'s own '
9709+
+ 'breaking-change detector classified it non-breaking and asked for no ADR-0087 '
9710+
+ 'disposition at all — measured on the shipped changeset. #16421 closed that hole '
9711+
+ '(the declaration now carries a `(narrowing)` arm the gate reads instead of a prose '
9712+
+ 'banner) and this row is the other half of the same ruling: the narrowing that '
9713+
+ 'already shipped is RECORDED, ⛔ not re-released and ⛔ not ratified in silence. '
9714+
+ 'Maintainer ruling, director summon #17, decision batch #2 item 1, option B, '
9715+
+ 'verbatim and untranslated: 「同意」. The direct precedents for registering a change '
9716+
+ 'no transform can apply are `schedule-flow-acting-organization-required` (protocol '
9717+
+ '18) and `rest-requireauth-default-flip` (protocol 12) — behaviour-only, a '
9718+
+ 'deployment judgement, registered anyway because the prescription is real.',
9719+
acceptanceCriteria:
9720+
'Every `sys_job.timezone` and `sys_report_schedule.timezone` value stored in the '
9721+
+ 'deployment is an IANA member. The one-line fix per offending row: write the '
9722+
+ 'canonical zone id (`UPDATE … SET timezone = \'Asia/Shanghai\'`), or clear the '
9723+
+ 'column — `sys_report_schedule` documents a `UTC` default and `sys_job` has no '
9724+
+ 'reader at all. Rows already holding a member parse and behave byte-identically to '
9725+
+ 'before; rows holding none are readable, are returned unchanged, and fail only on '
9726+
+ 'their next WRITE. A report schedule that was silently running on '
9727+
+ '`interval_minutes` resumes its cron cadence once its zone is a member — that '
9728+
+ 'resumption, not the absence of an error, is how the fix is verified. ⚠️ The two '
9729+
+ 'columns\' `maxLength` (100 vs 64) and defaults (none vs `UTC`) are deliberately '
9730+
+ 'still unconverged and are NOT part of this entry; no member is longer than 32 '
9731+
+ 'characters on the current Node baseline, so neither bound admits anything the '
9732+
+ 'domain does not.',
9733+
},
96799734
{
96809735
id: 'plugin-auto-restart-never-reinitialised',
96819736
surface:

0 commit comments

Comments
 (0)