Skip to content

Architecture

Karthik P edited this page Nov 1, 2026 · 2 revisions

Architecture

fiscalclock is one Rust core with thin Python and npm bindings that return the core's JSON unchanged, and this page describes that structure as built in 0.1.0.

Last reviewed: 1 Nov 2026

The decision behind this structure is ADR-0002. The CLI and MCP server are planned for 0.4 and are not built (see Agents-and-MCP).

Overview

flowchart TB
    def["JSON definition<br/>(supplied by the user)"]
    subgraph core["crates/core: Rust crate fiscalclock"]
        parse["Parse and validate<br/>(five blocks)"]
        arith["Calendar arithmetic<br/>(own Date type)"]
        api["JSON API<br/>fiscalclock::json::validate / call"]
        parse --> arith --> api
    end
    def --> parse
    api --> rust["Rust crate<br/>crates.io: fiscalclock"]
    api --> py["PyO3 extension _fiscalclock<br/>+ Python layer<br/>PyPI: fiscalclock"]
    api --> wasm["wasm-bindgen module<br/>+ TypeScript layer<br/>npm: fiscalclock"]
    vectors[("conformance/vectors<br/>112 files, 4,070 cases")]
    vectors -.-> rust
    vectors -.-> py
    vectors -.-> wasm
    rust -.-> t1["rust.txt transcript"]
    py -.-> t2["python.txt transcripts"]
    wasm -.-> t3["js.txt transcripts"]
    t1 & t2 & t3 -.-> cmp{{"CI: identical-output<br/>byte-for-byte comparison"}}
Loading

Solid arrows are calls. Dotted arrows are the test path: every package runs the same vectors and writes a transcript of the raw JSON results, and CI compares the transcripts.

Packages

All three packages were released at version 0.1.0 on 31 Oct 2026 (v0.1.0); see the CHANGELOG.

Package Built with Accepts dates as Distribution Source
Rust crate fiscalclock Plain Rust, #![no_std] with alloc, #![forbid(unsafe_code)]; dependencies serde, serde_json, sha2; minimum Rust 1.85 The crate's own Date; ISO strings through the JSON API crates.io crates/core
PyPI fiscalclock PyO3 extension _fiscalclock built with maturin, plus a pure-Python layer with type stubs An exact datetime.date or an ISO string One abi3 wheel for CPython 3.10 and later, and one wheel for free-threaded CPython 3.14 (cp314t), on each of 7 platforms: manylinux x86_64 and aarch64, musllinux x86_64 and aarch64, macOS x86_64 and arm64, Windows x64. Plus an sdist. bindings/python
npm fiscalclock Core compiled to WebAssembly with wasm-bindgen directly (no wasm-pack), plus a TypeScript layer An ISO string or a Temporal.PlainDate ES module with TypeScript types. The node export loads the WebAssembly from disk; the default export, for browsers and other environments, fetches it relative to its own URL with top-level await. Node.js 22 or later. bindings/js

Sources for the table: the wheel matrix in release.yml, the exports in package.json, and the package READMEs. The wheel choices follow ADR-0011: PEP 803 states that "The Stable ABI is currently not available for free-threaded builds", and its abi3t variant targets Python 3.15 (status Final as of 31 Oct 2026). The npm toolchain is ADR-0020. The minimum versions are ADR-0025, and the msrv job in ci.yml builds the core with Rust 1.85.

The core has its own date type and no date-crate dependency (ADR-0003). The proposal's optional chrono, jiff and time conversions are planned for 0.2.

The JSON API boundary

Every operation is defined once in the core as JSON text in and JSON text out: fiscalclock::json::validate and fiscalclock::json::call (spec/api-v1.md). The operations in 0.1.0 are validate, locate, period, shift, parent, children, years, format, parse, dimension, dimension_csv and explain.

The bindings own only type conversion (AGENTS.md, design rule 9).

  • They turn native values into JSON arguments, call the core, and turn the JSON result into native values (datetime.date in Python; plain objects with ISO strings in JavaScript).
  • Their call_json / callJson and validate_json / validateJson functions return the core's JSON text unchanged, with no reordered keys or reformatted values.
  • They reject timestamp types before calling the core, using an error the core builds, so the error is identical in both languages (see Semantics).
  • They hold no calendar arithmetic and no validation logic.

Errors cross the boundary as data: {"errors":[{"code","path","message","hint","details"}]}. Python raises FiscalclockError (a ValueError) and JavaScript throws FiscalclockError, each carrying the same fields (ADR-0010).

Cross-binding identity

Cross-language identity is checked by comparing JSON output, not native objects (ADR-0019).

  1. The Rust, Python and JavaScript conformance runners each run every case in conformance/vectors/ through the raw JSON API.
  2. Each writes a transcript, one line of raw output per case, to target/transcripts/rust.txt, python.txt or js.txt.
  3. The identical-output job in ci.yml downloads 14 transcripts and compares each with the Rust transcript using cmp: one Rust; seven Python (CPython 3.10 and 3.14 on Ubuntu, Windows and macOS, plus free-threaded 3.14 on Ubuntu); six JavaScript (Node.js 22 and 26 on the same three systems). Any differing byte fails the job.

In CI run 41222104138 on 31 Oct 2026 this job passed.

Release pipeline

release.yml runs on a tag vX.Y.Z, or manually as a dry run that stops before attestation and publishing.

flowchart LR
    tag["Tag vX.Y.Z"] --> check["check<br/>versions agree, tag on main,<br/>changelog section exists"]
    check --> build["build<br/>crate, 14 wheels, sdist,<br/>npm tarball"]
    build --> smoke["smoke<br/>install and run on<br/>Linux, macOS, Windows"]
    smoke --> attest["attest<br/>build provenance"]
    attest --> pubc["publish crates.io<br/>(approval)"]
    attest --> pubp["publish PyPI<br/>(approval)"]
    attest --> pubn["publish npm<br/>(approval)"]
    pubc & pubp & pubn --> rel["GitHub release<br/>from changelog"]
Loading
  • check: every manifest states the same version; on a tag, the commit is on main and CHANGELOG.md has a section for the version.
  • build: cargo package for the crate; maturin for the wheels and sdist; npm pack for the npm tarball after its tests pass.
  • smoke: on Ubuntu, macOS and Windows, the built wheel is installed on CPython 3.10 and the packed tarball on Node.js 22, and scripts/smoke.py and scripts/smoke.mjs check a known NRF result and the timestamp rejection.
  • attest: actions/attest-build-provenance signs provenance for every artefact (tag runs only).
  • publish: three jobs, one per registry, each in a GitHub environment (crates-io, pypi, npm) that needs the maintainer's approval. PyPI uses trusted publishing; crates.io and npm use tokens for the first release (see Risks).
  • GitHub release: created from the changelog section once all three publishes succeed.

Every action is pinned to a full commit SHA, and each job gets only the permissions it needs. The dry run 41222111858 on 31 Oct 2026 built every artefact and passed the smoke tests; the publish jobs were skipped, as designed for a dry run.

What never goes where

Part Holds Never holds
crates/core Parsing, validation, all arithmetic, labels, the JSON API I/O, a clock, global state, named calendars
bindings/python, bindings/js Type conversion and the JSON pass-through Calendar arithmetic or validation
cli (planned for 0.4) The command-line interface and MCP server Logic not reachable through the core's JSON API

The full map is in AGENTS.md.

Clone this wiki locally