- validation.spec.ts now accepts the four-digit-year bounds and refuses
each non-canonical spelling (year zero, 2026-02-30, a four-digit
fraction, a numeric offset, surrounding whitespace) with the released
error message, so the accepted set is guarded by a committed gate
rather than by a review-time measurement
- the Agent Note cites that test instead of describing a differential
- session-format-v0-to-v1 declares dsh-util-time only in dependencies,
matching dsh-util-values and the sibling migrations
- util/time lists the migration among its consumers
The client bundle rejects a cross-plugin value import for any package it
does not list as inline-safe, and dsh-util-time is not on that list because
it imports the Temporal polyfill at module scope. The offset therefore
stays computed in packages/client/ui-settings-account, and the Agent Note
records it with the same standing as the other three sites that are not
copies of this vocabulary.
This reverts the localUtcOffsetSeconds entry, its consumers, its tests,
its README rows, and the two lockfile importer entries.
- session-format-v0-to-v1 reads the shared canonical-instant profile
through fourDigitYearEpoch instead of its own regular expression and
round trip, with the accepted set pinned by a differential against the
pre-change predicate (3,508 inputs, 0 divergences)
- localUtcOffsetSeconds holds the seconds-east conversion two clients had
each written, so the offset a client reports has one home
- the Agent Note records the three sites that stay, and why each is not a
copy of this vocabulary
- timeZoneError and browserTimeZone switch over TimeZoneRejection instead of
a ternary: each reason that shares a diagnostic is its own case, and the
default arm assigns the reason to never so adding a reason fails the build
(assertNever where the package already imports it). The unsupported cause
is still forwarded.
- the request-zone suite now exercises the blank refusal, so every case the
switch lists is covered rather than sharing one ternary branch.
- Duration documents that a span is signed: a negative count builds and
decompose() refuses it, so the caller states its own sign policy.
Resolutions:
- packages/util/time/package.json keeps this branch's description and takes
master's 0.2.1-rc.2 version.
- packages/util/README.i18n.yaml is re-recorded after the merge.
The package API carried members and result types ahead of any caller. The
span now publishes fromMilliseconds and decompose, the two entries
dsh-time-context calls; the seconds constructor, the zero identity, the
arithmetic and ordering operations, the millisecond getter, and the four
fractional conversions land with the caller that needs them, recorded in
the Agent Note and the README limitation.
The package entry names its exports instead of re-exporting both modules,
so the three result types and DurationParts, which no consumer names, are
no longer part of the published surface. Every remaining new name has a
named consumer: schedule imports the nine parser and bound names plus
four rejection types, and canonicalizeTimeZone has seven consumer files.
Resolutions:
- packages/util/time/package.json keeps this branch's description and the
polyfill dependency, with master's 0.2.1-alpha.2 version.
- docs/module-graph.md and .zh.md are regenerated: master's graph plus
this branch's context/time-context and schedule edges to util-time.
origin/master removed the runtime invariant plugins and the package rule
that asked a README to justify the absence, so the text that cited that
mechanism no longer refers to anything:
- the Agent Note states the elapsed-rendering change without the reading
invariant it used to cite
- the same note no longer says the durable reading format is one an
invariant validates
- the util/time README no longer explains why no invariant companion is
published, and its module JSDoc no longer claims the library formats
nothing while listing a canonical instant renderer
Resolutions:
- packages/util/time/package.json keeps this branch's description and the
polyfill dependency, with master's 0.2.1-alpha.1 version.
- packages/util/time/README.md and README.zh.md keep this branch's
structure and content, with master's model-experience sentence about
the zone instruction and timestamp. The re-recorded i18n sidecar
follows.
- docs/module-graph.md and .zh.md carry master's graph plus this
branch's context/time-context and schedule edges to util-time.
Delete @deepseek-ai/dsh-invariants, every package's ./invariant companion
and its tests, the invariant gates and Vitest setup, the scoped-event
resolver generator, and the INVARIANT-coded listener rethrow paths.
Update docs, READMEs, Agent Notes, and add an upgrade guide.
- the note says Duration overrides no coercion hook, matching the README
and the class doc rather than claiming it has no toString
- the elapsed change names the two samples that move (NaN and +Infinity,
not -Infinity, which the consumer still clamps to zero)
- the charter sentence names the files that keep the phrase
- the seconds-unit limitation separates a span from an epoch in seconds
- plus and minus construct through fromMilliseconds, so the documented
bound has one enforcement point; the negative control reddens the new
assertion
- the class and README say Duration overrides no coercion hook rather
than having none, and decompose says which parts are floored
- the note no longer calls the differential a review probe, records that
Schedule imports the bound constants, that both strict parsers refuse
year 0, that Schedule's suites do assert a few message substrings, and
that the util charter bans harness dependencies rather than external
packages
- Duration rejects a count outside the exactly representable millisecond
range, which is what makes decompose's whole-part identity exact; the
postcondition is asserted over a sweep and a random sample
- the to* JSDoc no longer claims a round trip restores the count: each is
one division, so it answers the nearest double
- a test rules out a Symbol.toPrimitive coercion hook, the README names
both implementation modules, and the note records that the value type
ships its full operation set before its second consumer exists
- the immutability assertion checked coercion overrides instead of
stringifying the instance, which no-base-to-string rejects
- the config catalog is regenerated for the package description
Duration holds one finite millisecond count behind a frozen class: a
class rather than a branded number because TypeScript erases a brand
through arithmetic, so only an instance can stop seconds and
milliseconds from being added. It adds, subtracts, orders, converts to
larger units fractionally, and splits a non-negative span into whole
days, hours, minutes, and seconds. There is no valueOf and no toString,
so a reader always states its unit.
dsh-time-context reads its elapsed line through it and keeps the
wording: a backward clock reading still clamps to zero there. The
rendered text is byte-identical for every finite sample, verified by a
differential over more than eight thousand inputs. A non-finite sample
now throws from the span constructor instead of rendering 'NaNs', which
the package's own reading invariant already refused.
- the constants name one bound each
- resolveLocalInstant reuses an instance a caller already holds instead of
copying it per candidate date
- the README entry-point paragraph drops its duplicate sentence
- the note replaces an invalid reason for the zone mapper and a
review-vantage sentence with the facts
- isFourDigitYearInstant is the shared predicate; Schedule's five
representability guards delegate to it instead of re-deriving the range
- the reason for a field outside its calendar range is out_of_range in
both strict parsers, so year 0 no longer reads as a wall-clock problem
- LocalCalendarTime.year states which entries reject year 0 and which
accept it at the UTC edges
- the note counts the five removed expressions and records that the zone
mapper is a silent catch-all; the Chinese side mirrors the English
paragraph order
- the README no longer claims resolveLocalInstant answers a string
- the accepted-input table records the widened plain date-time input
- canonicalClientTimeZone is named as the wire-boundary form rather than
left outside a blanket claim about discriminated results
- the non-canonical scheduledAt diagnostic has one source
- resolveLocalInstant accepts an already-built plain date-time, so the
recurrence walkers drop their private copy of the disambiguation and
round-trip decision
- decodeInstant narrows its field explicitly and drops the assertion
- the model-input exhaustiveness fallbacks throw a plain Error, matching
the timing-change fallback, while durable decoding keeps
ScheduleLogError
- the library module and README state which entries answer a
discriminated result
time-context kept a third copy of the IANA profile: its own regular
expression and its own Intl canonicalization in request-zone.ts. It now
reads a request's reported zone through canonicalizeTimeZone and keeps
its stricter policy — an alias whose canonical answer differs from the
reported value is still refused, with the same three diagnostics.
The shared rejection now carries the platform error it caught, so the
unsupported refusal keeps its cause in both time-context and Schedule.
- Instant entries that take already-validated values throw on violated
preconditions; the module header said every entry answers a result.
- formatFourDigitYearInstant also refuses a non-integral epoch.
- Schedule's reason mappers switch over the closed unions with a never
default, so a new library reason fails the build instead of falling
through to a catch-all.
- The Agent Note claimed the schedule suites pin these diagnostics; they
assert codes only, and the note now records the added message assertion.
Move Schedule's strict RFC 3339 instant, local-calendar, and IANA zone
parsing into @deepseek-ai/dsh-util-time, and have Schedule consume it.
The shared entries answer discriminated results and own no failure
vocabulary; Schedule keeps its error codes, diagnostics, durable record
shapes, and all recurrence arithmetic.
* feat(i18n): key translation pairing records by heading section
Replace the whole-file blob hashes in *.i18n.yaml with one entry per
heading section that still holds language-specific content, keyed by the
English heading-slug path. Blocks identical across the pair (generated
table rows, code blocks, shared regions) stay out of the hash, so edits to
different sections and identical regenerations merge with Git's default
text merge, including on GitHub.
Remove the dsh-translation-pairing merge driver, its resolver command and
installer wiring; gen-translation-brief recovers last-confirmed text from
commit history. Migrate all active records, archive the superseded Agent
Note, and record the replayed conflict reduction in the new one.
* test(i18n): refresh translation prompt snapshot for the terminology note
* feat(i18n): exclude only code blocks and generated regions from pairing records
Replace byte-equality masking with an explicit rule: a record hash skips
fenced code blocks and BEGIN/END GENERATED regions, the two kinds of
content the pairing gate already requires to be identical on both sides.
Move generated data into regions written into both pages:
gen-config-catalog renders both languages and the record with per-package
regions labelled by code identifiers; gen-module-graph keeps its table in a
region; gen-doc-graphs splices the event matrix regions into the Chinese
page. Add a generic region splice beside the region grammar.