SPEC-TIME-001: Canonical Temporal Resolver
| Reference Number | Version | Effective Date | Supersedes | Authority Level |
|---|---|---|---|---|
| SPEC-TIME-001 | 1.3 | 2026-10-24 | 1.2 | Implementation Spec |
I. Purpose
This document is the normative build specification for canonical_temporal_resolver, the single Canonical Temporal Evaluation tool required by INV-ARC-015.
It defines the runtime object, public primitives, inputs, return contract, fail-closed behavior, and browser-facing timezone contract for all temporal evaluation in Classroom Token Hub.
This specification replaces the historical temporal rebuild plan. The old plan described a broad target architecture; this document defines the concrete resolver that all DOM specifications, FEAT specifications, runtime services, scheduled jobs, route handlers, and tests must use.
canonical_temporal_resolver is the only public temporal evaluation helper. Older helper names are deprecated and must not be used as new domain or FEAT dependencies.
II. Authority
This specification is subordinate to:
docs/INVARIANT/CORE/INV-CORE-000_CORE_INVARIANTS.mddocs/INVARIANT/CORE/INV-CORE-001_CAPABILITY_BASED_ARCHITECTURE_AND_AUTHORITY_MODEL.mddocs/INVARIANT/ARCHITECTURE/INV-ARC-015_TEMPORAL_MODEL_AND_BOUNDARY_ENFORCEMENT.md
If this document conflicts with INV-ARC-015, INV-ARC-015 prevails.
III. Core Principle
canonical_temporal_resolver measures and evaluates time. Domains interpret the resulting temporal truth according to their own business rules.
Examples:
- PROD decides which productivity intervals are compensable; the resolver measures the exact elapsed seconds.
- OBL decides which assessment boundary applies; the resolver resolves whether the boundary has passed.
- The browser displays time in the canonical timezone supplied by the resolver; the browser does not make authoritative temporal decisions.
The resolver must remain business-blind. It must not contain payroll, obligation, hall-pass, store, banking, insurance, or attendance-specific logic.
IV. Evaluation Types
Every resolver call must declare one evaluation type.
| Type | Name | Temporal Authority | Required Context |
|---|---|---|---|
SLE |
System-Level Evaluation | UTC | None |
CLE |
Class-Level Evaluation | Canonical Class Timezone | CanonicalContext with class_id |
SLE
System-Level Evaluations are platform/user-system evaluations whose authority is UTC.
Examples:
- session expiry
- observability
- account inactivity
- system logs
CLE
Class-Level Evaluations are class-scoped evaluations whose authority is the class’s Canonical Class Timezone.
Examples:
- productivity and payroll
- obligation due dates
- hall-pass timing
- store item expiry
- class-local day boundaries
For CLE, the resolver must fail closed if the canonical class timezone cannot be established.
V. Temporal Authority Resolution
The resolver must use one deterministic authority flow:
- Accept evaluation type.
- If
SLE, set temporal authority to UTC. - If
CLE, require canonical execution context. - Resolve Canonical Class Timezone from
ctx.class_id. - Validate that the timezone is a valid IANA timezone.
- Normalize all supplied timestamps into the resolved temporal authority.
- Execute the requested primitive.
- Return a canonical temporal evaluation object.
For architectural consistency, every CLE primitive evaluates in Canonical Class Timezone, even when the calculation would be mathematically equivalent in UTC.
No CLE primitive may operate directly on raw UTC timestamps after authority resolution.
VI. Storage and Input Rules
All persisted timestamps remain UTC.
The resolver may accept UTC-aware timestamps from persistence, but callers must not independently convert them to class-local time before calling the resolver.
The resolver must reject or normalize inputs as follows:
| Input Condition | Required Behavior |
|---|---|
| UTC-aware timestamp | Accept |
| timezone-aware non-UTC timestamp | Normalize through temporal authority |
| naive timestamp | Reject unless the specific test-only pathway explicitly supplies authority |
| missing required timestamp | Fail closed |
| invalid interval with end before start | Fail closed |
| unknown timezone for CLE | Fail closed |
missing ctx.class_id for CLE |
Fail closed |
VII. Canonical Return Object
Every primitive returns a CanonicalTemporalEvaluation object.
Minimum fields:
| Field | Type | Meaning |
|---|---|---|
evaluation_type |
SLE or CLE |
Evaluation category |
temporal_authority |
string | UTC for SLE or IANA timezone for CLE |
canonical_now |
datetime | Current timestamp in the resolved authority |
canonical_now_utc |
datetime | Same instant in UTC for persistence/correlation |
reference_time_utc |
datetime | UTC timestamp used as the evaluation anchor |
class_id |
string or null | Class boundary for CLE |
result |
primitive-specific | Boolean, timestamp boundary, date, or duration result |
The object may expose typed convenience properties for primitive-specific results, but those properties must not introduce business semantics.
Examples:
is_earlieris_lateris_betweenelapsed_secondsremaining_secondsevaluation_dateboundary_startboundary_endperiodshifted_timestampshifted_timestamp_utcdisplay_timezone
VIII. Required Public API
The implementation must expose one entry point:
canonical_temporal_resolver(
evaluation_type: str,
*,
canonical_execution_context: CanonicalContext | None = None,
primitive: str,
reference_time_utc: datetime | None = None,
**primitive_inputs,
) -> CanonicalTemporalEvaluationConstants:
SYSTEM_LEVEL_EVALUATION = "SLE"
CLASS_LEVEL_EVALUATION = "CLE"Permitted primitive names:
current_timeearlier_thanlater_thansame_instantbetween_boundariestime_sincetime_untilcurrent_evaluation_dayevaluation_day_boundariesevaluation_period_boundarieselapsed_durationshift_timestampanchored_recurrence_boundaryminimum_period_durationshift_calendar_dayslocalize_timestamp
The resolver must not expose additional public temporal primitives without amending this spec.
IX. Primitive Contracts
1. current_time
Purpose:
Resolve canonical “now” under the selected temporal authority.
Accepts:
| Input | Required | Notes |
|---|---|---|
reference_time_utc |
No | If omitted, resolver obtains current UTC time internally |
Returns:
| Result Field | Meaning |
|---|---|
canonical_now |
Current timestamp in the resolved temporal authority |
canonical_now_utc |
Same instant in UTC |
display_timezone |
Timezone browser may use for display |
Rules:
- Callers must not call
datetime.now()independently. - Browser display may use
display_timezone, but browser time is not authoritative.
2. earlier_than
Purpose:
Determine whether one timestamp is earlier than another under canonical temporal authority.
Accepts:
| Input | Required | Notes |
|---|---|---|
candidate |
Yes | Timestamp being evaluated |
reference |
Yes | Timestamp being compared against |
Returns:
| Result Field | Meaning |
|---|---|
is_earlier |
candidate < reference |
3. later_than
Purpose:
Determine whether one timestamp is later than another under canonical temporal authority.
Accepts:
| Input | Required | Notes |
|---|---|---|
candidate |
Yes | Timestamp being evaluated |
reference |
Yes | Timestamp being compared against |
Returns:
| Result Field | Meaning |
|---|---|
is_later |
candidate > reference |
4. between_boundaries
Purpose:
Determine whether a timestamp falls inside a canonical temporal window.
Accepts:
| Input | Required | Notes |
|---|---|---|
candidate |
Yes | Timestamp being evaluated |
start_boundary |
Yes | Inclusive start boundary |
end_boundary |
Yes | Exclusive end boundary |
Returns:
| Result Field | Meaning |
|---|---|
is_between |
start_boundary <= candidate < end_boundary |
Rules:
- Boundary semantics are always inclusive start, exclusive end.
- Callers must not implement alternate inclusion rules locally.
5. time_since
Purpose:
Measure exact elapsed time from a supplied timestamp until canonical now.
Accepts:
| Input | Required | Notes |
|---|---|---|
start |
Yes | Starting timestamp |
reference_time_utc |
No | Evaluation anchor; if omitted, resolver obtains current UTC time internally |
Returns:
| Result Field | Meaning |
|---|---|
elapsed_seconds |
Exact integer elapsed seconds from start to canonical now |
Rules:
- Negative elapsed time fails closed.
- No rounding is performed.
6. time_until
Purpose:
Measure exact remaining time from canonical now until a supplied timestamp.
Accepts:
| Input | Required | Notes |
|---|---|---|
target |
Yes | Target timestamp |
reference_time_utc |
No | Evaluation anchor; if omitted, resolver obtains current UTC time internally |
Returns:
| Result Field | Meaning |
|---|---|
remaining_seconds |
Exact integer seconds from canonical now to target |
Rules:
- Negative remaining time is returned as
0only if the caller explicitly requests clamping through a future spec amendment. Version 1.0 fails closed on negative values. - No rounding is performed.
7. current_evaluation_day
Purpose:
Derive the current calendar day under the selected temporal authority.
Accepts:
| Input | Required | Notes |
|---|---|---|
reference_time_utc |
No | Evaluation anchor; if omitted, resolver obtains current UTC time internally |
Returns:
| Result Field | Meaning |
|---|---|
evaluation_date |
Calendar date in the resolved authority |
Rules:
- For CLE, the evaluation date is the class-local date.
- For SLE, the evaluation date is the UTC date.
8. evaluation_day_boundaries
Purpose:
Derive canonical start/end boundaries for an evaluation day.
Accepts:
| Input | Required | Notes |
|---|---|---|
evaluation_date |
No | If omitted, derive current evaluation day |
reference_time_utc |
No | Used only when evaluation_date is omitted |
Returns:
| Result Field | Meaning |
|---|---|
boundary_start |
Start of evaluation day in resolved authority |
boundary_end |
End of evaluation day in resolved authority |
boundary_start_utc |
Start boundary converted to UTC for DB queries |
boundary_end_utc |
End boundary converted to UTC for DB queries |
Rules:
- Day boundary is
[00:00, 24:00)in the resolved authority. - Callers must not derive day boundaries independently.
9. elapsed_duration
Purpose:
Measure exact elapsed duration across one or more timestamp intervals.
Accepts:
| Input | Required | Notes |
|---|---|---|
intervals |
Yes | List of (start, end) timestamp pairs |
Returns:
| Result Field | Meaning |
|---|---|
elapsed_seconds |
Exact sum of elapsed seconds across all intervals |
Rules:
- The caller selects which intervals have business meaning.
- The resolver only validates and measures the intervals.
- Each interval must have
end >= start. - The intervals are summed exactly and the total is truncated to whole seconds once. Truncating each interval would drop up to a second per interval.
- Empty interval lists fail closed unless a future spec amendment explicitly allows zero-duration evaluation.
- Overlapping intervals fail closed in version 1.0 to prevent accidental double counting.
- Adjacent intervals are allowed.
- No rounding is performed.
Example:
Input intervals:
08:00:00 -> 08:20:00
08:30:00 -> 09:00:22
Output:
elapsed_seconds = 3022PROD prices the seconds as they are: payroll defines no time rounding (DOM-PROD-001 §XV.3; operator ruling 2026-09-30). Any business rounding a domain might define belongs to that domain.
canonical_temporal_resolver must not perform business rounding.
10. shift_timestamp
Purpose:
Construct a new timestamp by shifting a supplied timestamp by an exact elapsed duration.
Accepts:
| Input | Required | Notes |
|---|---|---|
timestamp |
Yes | Timestamp being shifted |
elapsed_seconds |
Yes | Signed integer number of seconds to shift |
Returns:
| Result Field | Meaning |
|---|---|
shifted_timestamp |
Shifted timestamp in the resolved temporal authority |
shifted_timestamp_utc |
Same shifted instant converted to UTC for persistence/correlation |
Rules:
- The caller decides what business event requires a shifted timestamp.
- The resolver only normalizes the timestamp, applies the exact second shift, and returns canonical authority-local and UTC forms.
elapsed_secondsmust be an integer.- No business rounding, payroll policy, obligation policy, or display formatting is performed.
Example:
Input:
timestamp = active productivity start
elapsed_seconds = remaining daily-limit seconds
Output:
shifted_timestamp_utc = exact inactive-event timestamp that makes the active duration equal the class limitPROD may use this primitive to construct the exact close-out timestamp for scheduled daily-limit enforcement, but the resolver must not know what a daily limit is.
11. evaluation_period_boundaries
Purpose:
Derive canonical start/end boundaries for a calendar period under the selected temporal authority.
Accepts:
| Input | Required | Notes |
|---|---|---|
period |
Yes | day, week, or month |
reference_time_utc |
No | If omitted, resolver obtains current UTC time internally |
Returns:
| Result Field | Meaning |
|---|---|
period |
Normalized period name |
boundary_start |
Start of the period in resolved authority |
boundary_end |
Exclusive end of the period in resolved authority |
boundary_start_utc |
Start boundary converted to UTC for DB queries |
boundary_end_utc |
End boundary converted to UTC for DB queries |
Rules:
dayboundaries are[00:00, 24:00)in the resolved authority.weekboundaries are Monday-start calendar weeks in the resolved authority.monthboundaries are calendar-month boundaries in the resolved authority.- Callers must not derive week or month boundaries independently.
- The resolver does not decide whether a weekly or monthly period has business meaning.
12. anchored_recurrence_boundary
Purpose:
Derive the n-th boundary of a recurrence anchored to a calendar date, such as a recurring billing period that repeats on the anchor day each month or every seven days.
Accepts:
| Input | Required | Notes |
|---|---|---|
anchor_date |
Yes | Calendar date in the resolved authority that anchors the recurrence |
cadence |
Yes | week or month |
index |
Yes | Integer n ≥ 0; 0 returns the anchor itself |
overflow |
For month |
roll_forward or clamp; how to treat an anchor day that does not exist in the target month |
Returns:
| Result Field | Meaning |
|---|---|
boundary_date |
Calendar date of the n-th boundary in the resolved authority |
boundary_start |
Start of that date (00:00) in the resolved authority |
boundary_start_utc |
Same instant in UTC, for persistence and comparison |
Rules:
- Every boundary is derived independently from
anchor_dateandindex. It is never derived from a previously returned boundary, so a rolled or clamped boundary never changes the anchor or drifts later boundaries. week: the boundary date isanchor_date + 7 × indexcalendar days. It is not168 × indexelapsed hours; DST transitions do not move it off midnight.month: the target month isindexcalendar months after the anchor’s month. If the anchor’s day exists in the target month, that date is the boundary. Otherwise:roll_forward: the boundary is the first day of the following month (the next date that exists);clamp: the boundary is the last day of the target month.
- Boundaries are at
00:00in the resolved authority. Periods between consecutive boundaries are half-open[boundary_n, boundary_{n+1}). - The caller chooses
overflow. The resolver performs calendar arithmetic only and does not know which business rule a product has ratified.
Required example (anchor_date = January 31, month, roll_forward), indexes 0–11 of a non-leap year:
1/31 → 3/1 → 3/31 → 5/1 → 5/31 → 7/1 → 7/31 → 8/31 → 10/1 → 10/31 → 12/1 → 12/31In a leap year index 1 is still March 1 (February 31 does not exist; February 29 is not the anchor day).
13. minimum_period_duration
Purpose:
Report the shortest possible period between consecutive boundaries of a cadence, so that product rules bounded by period length (for example a bill preview interval) are validated against the calendar rather than a constant copied into each product.
Accepts:
| Input | Required | Notes |
|---|---|---|
cadence |
Yes | week or month |
Returns:
| Result Field | Meaning |
|---|---|
minimum_calendar_days |
7 for week; 28 for month |
Rules:
- The value is the minimum over every anchor day and every month under either overflow rule. For
monththe minimum is a February period, 28 days. - A product rule of the form
0 < x < minimum_period_duration(cadence)holds for every period of that cadence.
14. shift_calendar_days
Purpose:
Move a calendar date, or a timestamp’s local date, by whole calendar days in the resolved authority. A calendar day is 23, 24 or 25 elapsed hours, so N days is never N × 24 hours.
Accepts exactly one of date or timestamp:
| Input | Required | Notes |
|---|---|---|
date |
One of | A calendar date in the resolved authority (a date, not a datetime) |
timestamp |
One of | A timezone-aware timestamp |
days |
Yes | Signed integer number of calendar days |
time |
No | With date only: a naive local time of day. The result then also carries shifted_timestamp at that wall-clock time on shifted_date |
Returns:
| Result Field | Meaning |
|---|---|
shifted_date |
The calendar date days days after the source date in the resolved authority |
boundary_start / boundary_start_utc |
For date: 00:00 of shifted_date in the resolved authority, and the same instant in UTC |
shifted_timestamp / shifted_timestamp_utc |
For timestamp: the same local wall-clock time on shifted_date. For date with time: that wall-clock time on shifted_date. Each with the same instant in UTC |
Rules:
- A
timestampkeeps its local wall-clock time, and adatewithtimetakes that wall-clock time. Where the time occurs twice (a fall-back fold), the earlier instant is taken; where it does not occur (a spring-forward gap), the smallest forward shift onto an existing time is taken. - The caller decides what the shift means (a claim window, a waiting period, a grace period, a schedule). The resolver performs calendar arithmetic only.
daysmust be an integer; supplying both or neither ofdateandtimestampfails closed.
15. localize_timestamp
Purpose:
Express a supplied timestamp in the resolved temporal authority, for display or to read its local calendar date.
Accepts:
| Input | Required | Notes |
|---|---|---|
timestamp |
Yes | A timezone-aware timestamp |
Returns:
| Result Field | Meaning |
|---|---|
local_timestamp |
The same instant in the resolved authority |
evaluation_date |
Its calendar date in the resolved authority |
Rules:
- This is the only sanctioned timezone conversion outside the resolver. Callers must not call
astimezone,localize,pytz.timezoneorZoneInfo.
16. same_instant
Purpose:
Determine whether two timestamps are the same instant.
Accepts:
| Input | Required | Notes |
|---|---|---|
candidate |
Yes | Timestamp being evaluated |
reference |
Yes | Timestamp being compared against |
Returns:
| Result Field | Meaning |
|---|---|
is_same |
candidate and reference are the same instant, whatever offsets they carry |
X. Browser Timezone Contract
canonical_temporal_resolver is responsible for resolving and supplying the authoritative display timezone.
| Surface | Display Timezone |
|---|---|
| SLE page or system surface | UTC |
| CLE page or class-scoped surface | Canonical Class Timezone |
The browser may use the supplied timezone to render and continuously update visual clocks.
The browser must not use its displayed clock to decide business truth.
Server-rendered pages follow INV-ARC-022 §V.8 and SPEC-UI-001 §VI–§X, both of which govern this
specification: a page’s builder produces every displayed time as presentation-ready text, localizing
through localize_timestamp (or the boundary primitives) with the page’s context, and the template
prints that text. A template never formats, converts or derives a time (no time filters, strftime,
isoformat or astimezone), and nothing queries the database while a template renders.
tests/test_INV_ARC_022__templates_do_not_format_time.py enforces this over templates/.
Allowed:
Server supplies display_timezone = America/Los_Angeles.
Browser renders navbar clock in America/Los_Angeles.Forbidden:
Browser decides whether payroll is due from local device time.
Browser decides whether an obligation deadline passed from local device time.
Browser computes class-local day boundaries.XI. Domain Boundary Rules
PROD
PROD owns:
- which productivity events are active or inactive;
- which intervals count as compensable;
- how exact elapsed seconds become payable minutes under payroll policy.
The resolver owns:
- measuring the supplied intervals;
- resolving class-local day and boundary truth;
- providing exact elapsed seconds.
OBL
OBL owns:
- which obligation assessment boundary applies;
- which period or cycle has business meaning;
- whether an assessment is due.
The resolver owns:
- resolving the temporal boundary;
- comparing timestamps;
- calculating exact elapsed or remaining time.
LED
Ledger owns monetary time only as recorded occurrence/provenance timestamps. Ledger must use canonical_temporal_resolver for temporal comparisons, reconciliation cutoffs, and display timezone metadata, but Ledger must not encode business-specific temporal semantics.
XII. Prohibited Patterns
The following are prohibited outside canonical_temporal_resolver:
datetime.now()datetime.utcnow()- raw
end - startduration math - direct comparison of two timestamps to make a decision (
a < b,a == b): useearlier_than,later_than,between_boundariesorsame_instant. Ordering a collection by its timestamps (sorted,min,max) is not a decision and may compare directly (owner ruling 2026-10-24) - direct timezone conversion
- raw
.date()derivation for business logic - local day-boundary construction
- hardcoded
America/Los_Angeles - browser/device-local business evaluation
- FEAT-local current-time reads
- test-local current-time reads for domain behavior
Tests must inject reference_time_utc into canonical_temporal_resolver rather than relying on wall-clock time.
XIII. Implementation Location
The canonical implementation must live at:
app/utils/canonical_temporal_resolver.pyThe public names exported by this module must include:
SYSTEM_LEVEL_EVALUATION
CLASS_LEVEL_EVALUATION
CanonicalTemporalEvaluation
canonical_temporal_resolvercanonical_temporal_resolver may call any lower-level time utility it requires, including functions in app/utils/time.py.
No code outside app/utils/canonical_temporal_resolver.py may call app/utils/time.py or any other time tool directly for temporal behavior. All runtime code, tests, DOM logic, FEAT logic, route logic, scheduled jobs, service code, model defaults, and template/page context builders must obtain temporal values through canonical_temporal_resolver.
app/utils/time.py is an internal implementation dependency of the resolver, not a public application API.
The resolver module’s utc_now() is its own wall clock: the source of current_time when no reference_time_utc is supplied, and the single seam tests patch to pin time. Application code must not import or call it; it obtains “now” from the current_time primitive. The module exports no other temporal helper: ensure_utc and advance_local_calendar_days were removed in version 1.3 (the latter became shift_calendar_days).
A SQLAlchemy column default / onupdate needs a callable; one private callable in app/models.py that returns current_time’s canonical_now_utc is the only permitted wrapper. Every other temporal value is obtained by calling canonical_temporal_resolver at the point of use: a private function whose body only forwards to the resolver (_now(), _is_later(), _class_local_date()) is a wrapper and is not permitted. A function that applies a domain rule and uses the resolver to do it (a session idle timeout, “has this period begun”) is domain logic, not a wrapper.
Deprecated names include any prior temporal helper, temporal context, or temporal module name not routed through canonical_temporal_resolver.
Historical documents and migration notes may mention older names, but new implementation, tests, DOM specs, and FEAT specs must use canonical_temporal_resolver.
XIV. Test Requirements
Targeted tests must prove:
- SLE resolves to UTC.
- CLE resolves to Canonical Class Timezone.
- CLE fails closed without class context.
- CLE fails closed when class timezone cannot be established.
current_timereturns UTC and authority-local forms of the same instant.earlier_thanandlater_thannormalize through the selected authority.between_boundariesuses inclusive-start/exclusive-end semantics.time_sincereturns exact elapsed seconds.time_untilreturns exact remaining seconds.current_evaluation_dayderives class-local day for CLE.evaluation_day_boundariesreturns both authority-local and UTC boundaries.elapsed_durationsums one interval.elapsed_durationsums multiple discontinuous intervals.elapsed_durationrejects end-before-start intervals.elapsed_durationrejects overlapping intervals.shift_timestampreturns authority-local and UTC forms of the same shifted instant.evaluation_period_boundariesreturns day/week/month authority-local and UTC boundaries.- Payroll-style rounding is not performed by the resolver.
- Browser display timezone is
UTCfor SLE. - Browser display timezone is Canonical Class Timezone for CLE.
anchored_recurrence_boundaryreproduces the required day-31roll_forwardsequence, in a leap and a non-leap year.anchored_recurrence_boundarydistinguishes itself from a fixed-duration period, from end-of-month clamping (underroll_forward), and from deriving each boundary from the previous result.anchored_recurrence_boundaryweekly boundaries stay at class-local midnight across a DST transition.minimum_period_durationreturns 7 forweekand 28 formonth.shift_calendar_daysmoves a date by calendar days to local midnight across a DST transition, and places a supplied local time on the shifted date.shift_calendar_dayskeeps a timestamp’s local wall-clock time across a DST transition.shift_calendar_daystakes the earlier instant in a fold and shifts forward out of a gap.shift_calendar_daysfails closed on non-integerdays, on both or neither source, and on naive or wrong-typed sources.localize_timestampreturns the instant in the resolved authority and its local date, and rejects a naive timestamp.same_instantcompares instants regardless of offset and rejects a naive timestamp.elapsed_durationsums intervals exactly and truncates the total once.
A structural guard enforces §XII and §XIII over app/: tests/test_SPEC_TIME_001__resolver_is_the_only_time_tool.py, with its detector in tests/guards/temporal_resolver_conformance.py and a mutation proof for each prohibited pattern. It must fail when it scans fewer files than app/ holds; its predecessor scanned a path that no longer existed and passed while checking nothing.
Recommended test file:
tests/dom/temporal/test_SPEC_TIME_001__canonical_temporal_resolver.pyXV. Migration Rules
When migrating existing code:
- Replace every direct call to
app/utils/time.pyoutsideapp/utils/canonical_temporal_resolver.pywithcanonical_temporal_resolver(...). - Keep
app/utils/time.pyonly as an internal lower-level implementation dependency of the resolver. - Do not add compatibility wrappers that preserve old direct temporal APIs for any application caller.
- Update FEAT specs to cite
SPEC-TIME-001where they depend on temporal evaluation. - Add targeted tests before rewiring route/template surfaces that depend on temporal behavior.
XVI. Definition of Done
SPEC-TIME-001 is implemented when:
app/utils/canonical_temporal_resolver.pyexists and exports the required public names;- all required primitives are implemented;
- CLE/SLE authority resolution is deterministic;
- every primitive returns
CanonicalTemporalEvaluation; - no code outside
app/utils/canonical_temporal_resolver.pyimports or callsapp/utils/time.pyor other time tools directly; - domain/FEAT temporal behavior stops doing direct datetime math;
- targeted tests pass;
- PROD payroll can call
elapsed_durationand receive exact elapsed seconds; - OBL can call boundary primitives and receive canonical class-local boundaries;
- browser-facing request/page context can obtain display timezone from the resolver.
XVII. Amendment
Version 1.3 (2026-10-24): adds shift_calendar_days (§IX.14; owner ruling 2026-10-24) localize_timestamp (§IX.15) and same_instant (§IX.16) (owner rulings 2026-10-24). Moving a date by an arbitrary number of calendar days and expressing a timestamp in the authority had no primitive, so callers did it with timedelta, astimezone and .date(). §XIII names the resolver’s utc_now() as its internal clock and test seam, removes ensure_utc and advance_local_calendar_days from the module’s exports, and permits one private column-default callable in app/models.py. §IX.9: elapsed_duration sums exactly and truncates once (owner ruling 2026-10-24). §XII: a decision may not compare two timestamps directly; ordering may (owner ruling 2026-10-24). §XIII: private helpers that only wrap a resolver call are not permitted in application code (owner ruling 2026-10-24). §X: server-rendered times are produced by page builders, never in templates (INV-ARC-022 §V.8, SPEC-UI-001; owner ruling 2026-10-25 by authority). §XIV adds tests 25–31 and the structural guard. Found 2026-10-24: 464 prohibited operations in 72 application files, undetected because the previous guard scanned a non-existent directory.
Revisions to this document must:
- Increment the version number.
- Update the effective date.
- Remain subordinate to
INV-ARC-015. - Preserve the finite primitive model unless
INV-ARC-015is amended first. - Distinguish temporal measurement from business interpretation.
Last updated: 2026-10-25