feat(core): timeglyph-core — zero-dependency, MSRV-1.75 epoch arithmetic (ADR-0019) - #25
Merged
Conversation
ADR-0019 splits the epoch arithmetic out of timeglyph so a FOUNDATION crate or a
low-MSRV published parser can link it without inheriting jiff, forensicnomicon,
or this package's 1.96 toolchain pin. The seam is integer arithmetic below,
calendar and rendering above.
This commit is the specification only: the workspace member, its manifest, and
the behavioural contract as failing tests. No implementation.
The contract these tests pin down is the parser-facing sentinel policy - None for
a zeroed field, for a value below its own epoch offset, and for nanosecond
overflow - which is deliberately NOT timeglyph's scanner semantics. A scanner is
asked "what could these bytes decode to?" and rightly answers 1601-01-01 for a
zero FILETIME; a parser is asked "what instant does this field hold, if any?" and
must answer "none". Both contracts coexist; timeglyph::secs is untouched.
Provenance of every expected value, since a converter validated only against
numbers its author computed proves nothing:
- FILETIME/WebKit 1601 epoch offsets: [MS-DTYP] section 2.3.3 and Chromium's
base/time/time.h (kTimeTToMicrosecondsOffset).
- Cocoa 2001 offset: NSTimeIntervalSince1970 in Foundation's NSDate.h.
- HFS+ 1904 offset: Apple TN1150, HFS Plus Volume Format.
- FILETIME 132_223_104_000_000_000 = 2020-01-01T00:00:00Z: already asserted by
timeglyph's own crate-level doctest, which reaches that instant through jiff
and the forensicnomicon format table - code this crate shares nothing with.
- Unix 1_614_601_815 = 2021-03-01T12:30:15Z (the WebKit and Cocoa vectors):
already asserted by secs::tests::civil_known_dates, likewise via jiff.
Verbatim failure, as required before any implementation exists:
error[E0432]: unresolved imports `timeglyph_core::cocoa_secs_to_unix_ns`,
`timeglyph_core::filetime_to_unix_ns`, `timeglyph_core::hfs_secs_to_unix_ns`,
`timeglyph_core::webkit_micros_to_unix_ns`,
`timeglyph_core::COCOA_EPOCH_OFFSET_SECS`,
`timeglyph_core::FILETIME_EPOCH_OFFSET`,
`timeglyph_core::HFS_EPOCH_OFFSET_SECS`, `timeglyph_core::WEBKIT_EPOCH_OFFSET`
--> core/tests/conversions.rs:12:5
error: could not compile `timeglyph-core` (test "conversions") due to 1 previous error
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four functions and four constants, no dependencies, MSRV 1.75:
filetime_to_unix_ns(u64) -> Option<i64> 1601 epoch, 100-ns ticks
webkit_micros_to_unix_ns(u64) -> Option<i64> 1601 epoch, microseconds
cocoa_secs_to_unix_ns(f64) -> Option<i64> 2001 epoch, f64 seconds
hfs_secs_to_unix_ns(u32) -> Option<i64> 1904 epoch, whole seconds
The zero-tick sentinel is not a special case: zero is the smallest value below
the epoch offset, so one `checked_sub` against the offset rejects both a zeroed
field and a misaligned pre-epoch value, and the same shape works for all three
integer formats. Cocoa is the documented exception - a 2001 reference date makes
negative values ordinary dates, so only 0.0 and the non-finite values are
rejected there. Cocoa's range check is explicit because `as i64` SATURATES, which
would turn an absurd float into a plausible-looking instant rather than a None.
HFS+ (1904) fell out cleanly and is included. DOS/FAT did not and is skipped: its
packed date word needs a civil-date-to-day-count conversion, which is calendar
math and belongs above the ADR-0019 seam, not below it.
Scaffolding, all of it needed for the member to actually be gated rather than
merely built:
- ci.yml test/clippy/fmt take --workspace/--all. With a root package present,
cargo's default selection is that package ALONE, so the bare commands would
have compiled core/ as a dependency and never run its tests.
- The coverage gate needs the member named on BOTH commands. `cargo llvm-cov
report` does not inherit the previous run's package selection and rejects
--workspace, so a bare `report --lcov` writes an LCOV with no record for
core/ at all - the LINE half of the gate then passes over the crate having
measured nothing, while the JSON/function half still sees it. Verified: with
`-p timeglyph -p timeglyph-core` the LCOV carries LF:23 LH:23 for core.
- .pre-commit-config.yaml clippy gets --workspace, for CI parity.
- supply-chain/config.toml declares timeglyph-core first-party, so a version
bump does not red the vet job (ADR-0018).
- A `core_epochs` fuzz target, wired into fuzz.yml's smoke loop. The inputs are
scalars rather than a structure, so this is not hunting a parse bug - it is
evidence for the panic-free claim at the arithmetic edges, and it reaches the
NaN and infinity encodings via `f64::from_bits` that a cast never produces.
- The root manifest comment reserving the -core split for "a fleet library
wants to link the conversion primitive" now records that ADR-0019 met that
condition, rather than still saying it has not happened.
timeglyph itself is UNCHANGED, deliberately. Its converters are generic over an
(epoch_ns, unit) pair from forensicnomicon's knowledge table, run on an i128
spine, and return a diagnostic ChronoError; timeglyph-core's are four named
functions on i64 with the sentinel policy. Substituting one for the other would
narrow the spine, replace a diagnostic with a bare None, and impose parser
sentinels on the scanner - three behaviour changes, so no call site was migrated
and no dependency was added. In particular secs::filetime still returns
Some(-11_644_473_600) for input 0, which is correct for a scanner.
Verification:
cargo test --workspace --all-features 537 passed, 0 failed
(514 timeglyph, unchanged; 23 new)
cargo clippy --workspace --all-targets --all-features -- -D warnings clean
cargo fmt --all --check clean
cargo deny check advisories/bans/licenses/sources ok
coverage gate timeglyph-core 23/23 lines, 4/4 fns
cargo +nightly fuzz run core_epochs 200,000 execs, no crash
MSRV: `cargo +1.75.0 check -p timeglyph-core` cannot run in-workspace - cargo
1.75 rejects the v4 Cargo.lock, before it ever looks at this crate. Verified the
way a crates.io consumer sees it instead, by copying core/ out of the repo:
`cargo +1.75.0 check --all-targets` and `cargo +1.75.0 test` both pass, and the
lockfile cargo generates there contains exactly one package - timeglyph-core -
which is the zero-dependency claim, checked rather than asserted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… policy `cargo vet --locked` checks the store is byte-identical to its own canonical formatting and fails "a file in the store is not correctly formatted" before it audits anything. cargo-vet's formatter does not preserve comments, so the rationale I had written above [policy.timeglyph-core] made the vet job red on the PR while passing every other gate. `cargo vet fmt` output, verbatim. The rationale it stripped is in the GREEN commit message and in ADR-0018; the store itself is a machine artifact. cargo vet --locked -> Vetting Succeeded (28 fully audited, 120 exempted) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
timeglyph-core declares `rust-version = "1.75"` - a low floor FOUNDATION crates
and published low-MSRV parsers pin against - and until now nothing enforced it.
An MSRV no job checks is an unbacked promise, and the constitution requires a
library repo's low MSRV to be CI-verified.
The job builds core/ COPIED OUT of the repo, which is load-bearing rather than
incidental. Two things in the tree would otherwise decide the outcome instead of
the crate: the workspace Cargo.lock is version 4, which cargo 1.75 refuses to
parse at all (it dies before ever reading core/), and rust-toolchain.toml pins
1.96.0, which would override the toolchain the job installed. Outside the repo
the crate is its own workspace with its own lockfile - which is exactly how a
crates.io consumer builds it, so this is the more faithful check as well as the
only workable one. The committed v4 lockfile is a real artifact and stays.
Three asserts, because a gate that cannot fail is not a gate:
1. core/Cargo.toml's declared rust-version must equal the version the job
installs, so raising the floor reds the job until both move deliberately.
2. `cargo +1.75.0 --version` must actually report 1.75.0. If the toolchain
ever failed to resolve as intended, a newer cargo would compile the crate
happily and the job would pass having verified nothing - the same vacuous
shape as the LCOV that contained no records for the file it claimed to
cover.
3. The lockfile cargo resolves must name exactly one package. That turns the
zero-dependency promise from a manifest comment into an enforced invariant,
which matters more than the MSRV: it is what lets a downstream link this
crate for free.
`cargo +1.75.0`, not a bare `cargo`. The explicit +toolchain sits at the top of
rustup's precedence chain, above RUSTUP_TOOLCHAIN, a directory override and any
rust-toolchain.toml. This is not theoretical - while testing the job locally an
exported RUSTUP_TOOLCHAIN=1.96.0 beat `rustup override set 1.75.0` and built the
crate on 1.96 while every step reported success. Assert 2 is what caught it.
Verified by running each step's script verbatim, and each assert against an
input that should trip it:
rust-version assert OK on "1.75"; fails on a drifted "1.82"
cargo +1.75.0 --version cargo 1.75.0 (1d8b05cdd 2023-11-20)
cargo +1.75.0 check --all-targets Finished dev [unoptimized + debuginfo]
cargo +1.75.0 test 22 passed, 0 failed, plus 1 doctest
zero-dep assert OK at 1 package; fails on the 150-package workspace lock
compiler assert fails on a "cargo 1.96.0" version string
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The `freshness` job is literally `cargo update --locked`: it fails when the
committed lockfile trails what the manifests already permit. It was failing
for exactly that reason, and it was right -- the lock trailed by eleven
packages, including two minor releases of our own foundation crate:
forensicnomicon 1.9.0 -> 1.11.0
forensicnomicon-data 1.3.3 -> 1.4.0
forensicnomicon-core 1.5.0 -> 1.5.1
aho-corasick 1.1.4 -> 1.1.5 clap 4.6.4 -> 4.6.6
clap_builder 4.6.2 -> 4.6.6 hifitime 4.3.0 -> 4.3.1
regex-automata 0.4.16 -> 0.4.18 time 0.3.54 -> 0.3.55
zerocopy(-derive) 0.8.55 -> 0.8.56
So the repo was building against fleet code two minor versions old. That is a
staleness signal doing its job, not a bot artefact.
Supply chain, each finding taking the first mechanism that applies:
- The three forensicnomicon crates were ALREADY covered by valid
[[trusted.*]] entries for h4x0r in audits.toml. They showed as unvetted
only because the publisher cache in imports.lock predated those releases,
so `cargo vet regenerate imports` alone cleared all three -- no new trust
record was needed or added.
- The remaining eight are third-party version churn against names that were
already exempted. Re-stated the pins. Verified the crate NAME set did not
grow (quote dropped out; nothing new entered), so nothing that was not
already shipping is now shipping.
Verification, all on this branch:
cargo update --locked exit 0 (was 101)
cargo test --workspace --all-features pass, 0 failed across all targets
cargo deny check advisories/bans/licenses/sources ok
cargo vet --locked Succeeded (29 audited, 119 exempted)
The MSRV 1.75 (timeglyph-core) job is unaffected: it copies core/ out of the
workspace and builds it standalone, and timeglyph-core is zero-dependency, so
no lock movement can reach it.
CONTROL (proving the gate can still fail): restoring the previous Cargo.lock
made `cargo update --locked` exit 101 again, listing the same 11 stale
packages; re-applying the refreshed lock returned it to exit 0, and the
restored file is byte-identical to the committed one.
h4x0r
marked this pull request as ready for review
August 7, 2026 15:53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Extracts timeglyph's epoch arithmetic into a new workspace member,
timeglyph-core, per ADR-0019. Built under strict TDD: RED then GREEN.Why
timeglyphcannot serve as a FOUNDATION dependency. It declaresforensicnomicon = "1.3"(so the reverse edge is a cycle), pinsrust-version = "1.96.0"(which has already raised a published parser's MSRV floor), and capsjiffbelow 0.2.33. ADR-0013 prescribes the mechanism: the lean<x>-corelibrary plus the full<x>binary, for a dependency that is both a library primitive and a heavy tool.The seam is integer arithmetic below, calendar and rendering above.
(ft - EPOCH_OFFSET) * 100needs nothing; ISO-8601 formatting needsjiff.API
Sentinel policy (the parser-facing contract):
Nonefor a zeroed field, for a value below the format's own epoch offset, and fori64-nanosecond overflow. Zero is not a special case — it is the smallest value below the offset, so onechecked_subcovers both. Cocoa is the documented exception: a 2001 reference date makes negative values ordinary dates, so only0.0and the non-finite values are rejected there.HFS+ fell out cleanly and is included. DOS/FAT is skipped: its packed date word needs a civil-date-to-day-count conversion, which is calendar math and belongs above the seam.
timeglyph is unchanged
No call site was migrated and no dependency was added, because no substitution is behaviour-preserving. timeglyph's converters are generic over an
(epoch_ns, unit)pair from forensicnomicon's knowledge table, run on ani128spine, and return a diagnosticChronoError. Swapping intimeglyph-corewould narrow the spine, replace the diagnostic with a bareNone, and impose parser sentinels on the scanner. In particularsecs::filetimestill returnsSome(-11_644_473_600)for input 0 — correct for a tool whose job is identifying what a value could decode to.Provenance of the test vectors
No expected value was computed by hand. Each is a published epoch constant, or an instant an implementation independent of this crate already decodes the same way:
base/time/time.h(kTimeTToMicrosecondsOffset)NSTimeIntervalSince1970, FoundationNSDate.h132_223_104_000_000_000= 2020-01-01T00:00:00Z1_614_601_815= 2021-03-01T12:30:15Z (WebKit + Cocoa vectors)secs::tests::civil_known_dates, likewise via jiffA gate hole this found
cargo llvm-cov reportdoes not inherit the previous run's package selection and rejects--workspace. A barereport --lcovtherefore writes an LCOV with no record at all for a second member — the line half of the coverage gate passes over it having measured nothing, while the JSON/function half still sees it. That asymmetry is what makes it quiet. Both commands now name every member; verified the LCOV carriesLF:23 LH:23fortimeglyph-core.ci.ymltest/clippy/fmt and the pre-commit clippy hook likewise needed--workspace/--all: with a root package present, cargo's default selection is that package alone.Verification
cargo test --workspace --all-featurescargo clippy --workspace --all-targets --all-features -- -D warningscargo fmt --all --checkcargo deny checktimeglyph-core23/23 lines, 4/4 functionscargo +nightly fuzz run core_epochsMSRV.
cargo +1.75.0 check -p timeglyph-corecannot run in-workspace: cargo 1.75 rejects the v4Cargo.lockbefore it ever looks at this crate. Verified the way a crates.io consumer sees it instead, by copyingcore/out of the repo —cargo +1.75.0 check --all-targetsandcargo +1.75.0 testboth pass, and the lockfile cargo generates there contains exactly one package,timeglyph-core. That is the zero-dependency claim checked rather than asserted.Not done here
timeglyph-coreyet.fuzz.yml's smoke loop still omits the pre-existinginspect_texttarget — unrelated to this change, left alone rather than folded in.🤖 Generated with Claude Code