Skip to content

feat(core): timeglyph-core — zero-dependency, MSRV-1.75 epoch arithmetic (ADR-0019) - #25

Merged
h4x0r merged 5 commits into
mainfrom
feat/timeglyph-core
Aug 7, 2026
Merged

feat(core): timeglyph-core — zero-dependency, MSRV-1.75 epoch arithmetic (ADR-0019)#25
h4x0r merged 5 commits into
mainfrom
feat/timeglyph-core

Conversation

@h4x0r

@h4x0r h4x0r commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Extracts timeglyph's epoch arithmetic into a new workspace member, timeglyph-core, per ADR-0019. Built under strict TDD: RED then GREEN.

Why

timeglyph cannot serve as a FOUNDATION dependency. It declares forensicnomicon = "1.3" (so the reverse edge is a cycle), pins rust-version = "1.96.0" (which has already raised a published parser's MSRV floor), and caps jiff below 0.2.33. ADR-0013 prescribes the mechanism: the lean <x>-core library 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) * 100 needs nothing; ISO-8601 formatting needs jiff.

API

pub const FILETIME_EPOCH_OFFSET: u64 = 116_444_736_000_000_000;
pub const WEBKIT_EPOCH_OFFSET: u64 = 11_644_473_600_000_000;
pub const COCOA_EPOCH_OFFSET_SECS: i64 = 978_307_200;
pub const HFS_EPOCH_OFFSET_SECS: u32 = 2_082_844_800;

pub fn filetime_to_unix_ns(ft: u64) -> Option<i64>;
pub fn webkit_micros_to_unix_ns(us: u64) -> Option<i64>;
pub fn cocoa_secs_to_unix_ns(secs: f64) -> Option<i64>;
pub fn hfs_secs_to_unix_ns(secs: u32) -> Option<i64>;

Sentinel policy (the parser-facing contract): None for a zeroed field, for a value below the format's own epoch offset, and for i64-nanosecond overflow. Zero is not a special case — it is the smallest value below the offset, so one checked_sub covers both. 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.

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 an i128 spine, and return a diagnostic ChronoError. Swapping in timeglyph-core would narrow the spine, replace the diagnostic with a bare None, and impose parser sentinels on the scanner. In particular secs::filetime still returns Some(-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:

Value Source
FILETIME / WebKit 1601 offsets [MS-DTYP] §2.3.3; Chromium base/time/time.h (kTimeTToMicrosecondsOffset)
Cocoa 2001 offset NSTimeIntervalSince1970, Foundation NSDate.h
HFS+ 1904 offset Apple TN1150, HFS Plus Volume Format
FILETIME 132_223_104_000_000_000 = 2020-01-01T00:00:00Z timeglyph's own crate-level doctest, which reaches that instant through jiff and the forensicnomicon table
Unix 1_614_601_815 = 2021-03-01T12:30:15Z (WebKit + Cocoa vectors) secs::tests::civil_known_dates, likewise via jiff

A gate hole this found

cargo llvm-cov report does not inherit the previous run's package selection and rejects --workspace. A bare report --lcov therefore 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 carries LF:23 LH:23 for timeglyph-core.

ci.yml test/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

Gate Result
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 functions
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. That is the zero-dependency claim checked rather than asserted.

Not done here

  • No README badge or docs page for timeglyph-core yet.
  • fuzz.yml's smoke loop still omits the pre-existing inspect_text target — unrelated to this change, left alone rather than folded in.

🤖 Generated with Claude Code

h4x0r and others added 5 commits August 2, 2026 02:28
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
h4x0r marked this pull request as ready for review August 7, 2026 15:53
@h4x0r
h4x0r merged commit ca0b173 into main Aug 7, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant