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

PDR

This product design record states the problem fiscalclock solves, who it is for, what it must do in each release, and how each requirement is checked.

Last reviewed: 1 Nov 2026

Labels on this page follow the Glossary. Decided means settled by the research documents or an Accepted ADR. Proposed means open. Built in 0.1.0 means implemented on main and covered by tests; 0.1.0 is built and passes CI but has not been published to any registry as of 31 Oct 2026 (CHANGELOG). Releases match the Roadmap, which is itself Proposed. Sources for factual claims are on the Evidence page; the landscape is on the Analysis page.

Problem

Fiscal calendars vary on three separate axes: how the year is bounded (a fixed date, a 52/53-week rule, or a list of dates), how it is subdivided (calendar months, 4-4-5 week patterns, thirteen 4-week periods, rule-anchored months) and how it is named ("FY2026", "fiscal 2026", "FY 2025-26"). The Analysis page shows real filers and governments combining these axes freely.

Existing tools each cover part of this inside one ecosystem or product. ThreeTen-Extra (Java), Power BI, SAP, Tempo (Elixir) and Perl's Fiscal5253 already model week-based calendars, so the problem is not that nobody models them. The gap is narrower: no open-source, data-defined engine gives the same answers in Rust, Python and JavaScript, and the crates.io search for "fiscal" found no 52/53-week or 4-4-5 calendar engine as of 31 Oct 2026 (Analysis).

The demand signal is specific. The Python library fiscalyear handles calendar-month years only, its latest release is still 0.4.0 of 17 Feb 2022 as of 31 Oct 2026, and its open issues ask for 4-4-5 calendars (#1, open since May 2017), week numbers (#10), several calendars in one application (#6) and a 29 February month end (#20) (Evidence).

People use the results for financial reporting dates. A silently wrong boundary is worse than an error, so every behaviour must be specified, tested against an independent source, and identical in every language.

Goals

  1. One definition format for every calendar shape. A calendar is JSON data with five independent blocks: year, week, subdivision, naming, exceptions (ADR-0004, ADR-0005). Decided.
  2. Identical answers in Rust, Python and JavaScript from one Rust core with thin bindings (ADR-0002, ADR-0019). Decided.
  3. Correctness shown, not claimed. Shared conformance vectors whose expected values come from cited sources or independent generators, never from fiscalclock. Decided.
  4. Answers that explain themselves and errors that can be acted on. Stable error codes, JSON paths and hints; ambiguity is an error with candidates (ADR-0010). Decided.
  5. Usable by software agents. A machine-readable contract, describe, infer, explain and an MCP server (ADR-0013, Agents-and-MCP). Decided, planned for 0.4.

Non-goals

These are out of scope (ADR-0015; AGENTS.md "Out of scope"):

  • holidays;
  • business days;
  • working hours;
  • pay periods;
  • sprints.

fiscalclock exposes periods as plain half-open intervals that other tools can build on. Two further non-goals follow from the design rules:

  • No built-in or named calendars. No function, constant or module is named after an organisation, country, standard or calendar family. Example definitions live in docs/, conformance/ and a separate registry (ADR-0004).
  • No time zones or timestamps in the core. The core works on civil dates only (ADR-0006).

Users and the questions they ask

User Questions Capabilities
Finance and reporting teams Which fiscal quarter is this date in? When does Q3 end? Does this year have 53 weeks? What is the comparable period last year? Locate, period objects, year inspection, comparable periods
Retail and consumer-goods analysts Where does the 53rd week go? What is the same day last year, by position or by weekday? Extra-week placement, equivalent day
Data engineers and BI developers Give me one row per day with every fiscal attribute, for Power BI, dbt or a warehouse Date dimension, bulk paths
Application developers Turn "FY26 Q3" into dates and back; split a contract across periods Labels, range operations, proration
Organisations with several calendars Which of my quarters overlap my supplier's Q2? Cross-calendar mapping
Software agents Which definition matches these reported period ends? Is this definition valid, and what does it mean in plain words? validate, infer, describe, explain, MCP tools

Functional requirements

Each requirement names its release (from the Roadmap) and whether it is built in 0.1.0. "Unscheduled" means the research proposal describes it but the Roadmap does not yet give it a release; each such item is also an open question below.

Definition and validation

ID Requirement Release Built in 0.1.0
FR-001 Accept a calendar definition as one JSON object with spec_version: 1 and the blocks year, week, subdivision, naming, with exceptions reserved (spec/definition-v1.md). 0.1 Yes
FR-002 fixed_date years with start.month, start.day and a required short_month policy (clamp or next) when the start day is 29–31. 0.1 Yes
FR-003 week_based years with anchor edge, month, day and rule (last, first, nearest). The qualifying weekday derives from week.starts_on (ADR-0028). 0.1 Yes
FR-004 table years with explicit boundaries, defined only between the first and last entry, and a table subdivision for period boundaries that follow no rule (ADR-0021, ADR-0022, Accepted). 0.2 No (rejected with UNSUPPORTED_FEATURE)
FR-005 calendar_months subdivision with month_start_day and quarters. 0.1 Yes
FR-006 week_pattern subdivision with 12 or 13 periods, quarters, and a required extra_week.period. 0.1 Yes
FR-007 anchored subdivision: each period ends by an anchor rule applied to each calendar month (broadcast calendar). 0.2 No
FR-008 stub: a fixed-date year with week-based periods, whose stub.anchor ends the first period and whose first and last periods absorb partial weeks (stub.absorb: "first_and_last") (ADR-0024, Accepted). 0.2 No
FR-009 Zero-length special periods attached to the last period (SAP periods 13–16); never returned by locate. 0.2 No
FR-010 extra_week.standalone and extra_week.by_year. 0.2 No
FR-011 A week block with starts_on and a required numbering; 0.1.0 supports contains_start only (ADR-0023). first_full follows in 0.2. 0.1 (first_full 0.2) Yes (contains_start)
FR-012 exceptions: overrides, history keyed by effective date, and transitions with their own periods and label. 0.2 No
FR-013 Validate the whole supported range and report every error with a stable code, a JSON path, a message and, where it helps, a hint (spec/api-v1.md). 0.1 Yes
FR-014 RULE_CONFLICT (two rules imply different dates for one boundary, listing the years affected) and TEMPLATES_AMBIGUOUS. 0.2 No
FR-015 Reject every reserved part of the format with UNSUPPORTED_FEATURE and a hint naming the planned release, so no definition is silently misread. 0.1 Yes
FR-016 Canonical form and calendar_id: the first 16 hex characters of the SHA-256 of the canonical form (ADR-0008). 0.1 Yes
FR-017 A JSON Schema for definitions generated from the Rust types, with every vector's definition validated against it (ADR-0009). Published at a versioned URL in 0.4. 0.2 (published 0.4) No

Locate, periods and navigation

ID Requirement Release Built in 0.1.0
FR-020 locate: every level (year, quarter, period, week) and day-of-level counts for a date. 0.1 Yes
FR-021 Period values with start, end_exclusive, derived last_day, days, weeks and has_extra_week (ADR-0007). 0.1 Yes
FR-022 period, shift, parent and children navigation, crossing years as needed. 0.1 Yes
FR-023 years: year periods for a range of fiscal year numbers, with has_extra_week. 0.1 Yes

Labels

ID Requirement Release Built in 0.1.0
FR-030 Naming schemes with a required year_number (start, end or majority), offset, century_pivot and templates; several schemes per calendar. 0.1 Yes
FR-031 format a period with a named or default scheme. 0.1 Yes
FR-032 parse a label into one period, or return AMBIGUOUS_LABEL with every candidate, or LABEL_NOT_RECOGNISED. 0.1 Yes
FR-033 Reject schemes that give two fiscal years the same number or skip one (YEAR_NUMBER_COLLISION), and majority ties (MAJORITY_TIE) (ADR-0029). 0.1 Yes
FR-034 Era tables and year_number: "table". 0.2 No
FR-035 Aggregate labels parsed to ranges (H1, 9M, YTD, LTM, TTM) and built-in template aliases. Unscheduled No

Date dimension and explain

ID Requirement Release Built in 0.1.0
FR-040 dimension and dimension_csv: one row per day with every fiscal attribute and label, stamped with calendar_id. Arrow output comes with the bulk paths in 0.2; Parquet output is unscheduled. 0.1 Yes (JSON rows and CSV)
FR-041 explain: a step-by-step trace of the rules behind a date's location. The wording of each step is not part of the API. 0.1 Yes

Range operations and proration

ID Requirement Release Built in 0.1.0
FR-050 Periods overlapping a range, period-to-date ranges (YTD, QTD, PTD) and trailing N periods. 0.2 No
FR-051 split with a required basis: actual_days, equal_per_period, weeks or days_30_360; exact rational shares (ADR-0016). 0.2 No
FR-052 allocate an amount in minor units by largest remainder, ties to the earliest period, negative amounts by absolute value; JavaScript takes BigInt or strings. 0.2 No

Comparisons and mapping

ID Requirement Release Built in 0.1.0
FR-060 comparable with a stated target year and a mode (fiscal, truncated, restated), returning its own boundaries and the excluded dates. 0.3 No
FR-061 equivalent_day with a required mode, same_position or same_weekday, and same_date for fixed-date years; NO_COUNTERPART for week 53 under same_position (ADR-0016). See D6. 0.3 No
FR-062 Cross-calendar map: periods of one calendar overlapping a period of another, with overlap days. 0.3 No

Agent tooling

ID Requirement Release Built in 0.1.0
FR-070 A CLI exposing the same operations for scripts. 0.4 No
FR-071 An MCP server with nine read-only tools, run with npx or uvx from the npm and PyPI packages (ADR-0013). 0.4 No
FR-072 describe: a plain-English paragraph for a definition. 0.4 No
FR-073 infer: ranked candidate definitions from known year ends, quarter ends or labels, with the first date where candidates diverge. 0.4 No
FR-074 llms.txt and Markdown copies of the documentation. 0.4 No
FR-075 A definition registry of cited real-world definitions in a separate repository under the maintainer's account, outside every package API, with automated checks and one human approval per entry (ADR-0027, Accepted). 0.4 No

Packages and bindings

ID Requirement Release Built in 0.1.0
FR-080 Rust crate fiscalclock with the typed API and the JSON API fiscalclock::json. 0.1 Yes
FR-081 Python package: abi3 wheels for CPython 3.10 and later plus free-threaded CPython 3.14 wheels (ADR-0011); dates as an exact datetime.date or an ISO string. 0.1 Yes
FR-082 npm package built with wasm-bindgen (ADR-0020); dates as ISO strings or Temporal.PlainDate. 0.1 Yes
FR-083 Every binding rejects timestamps (Python datetime.datetime and pandas Timestamp, JavaScript Date) with TIMESTAMP_REJECTED. 0.1 Yes
FR-084 Every binding's *_json methods return the core's JSON unchanged. 0.1 Yes
FR-085 Python bulk path through the Arrow PyCapsule Interface (ADR-0012); JavaScript typed arrays; optional chrono, jiff and time conversions in Rust. 0.2 No
FR-086 A fiscalyear compatibility module and a pandas accessor. See D7. 0.4 No
FR-087 A helper that converts an instant to a civil date given an IANA zone. Unscheduled No
FR-088 upgrade() from older spec versions. No older spec version exists yet. Unscheduled No
FR-089 Freeze definition format version 1 and API version 1. 1.0 No

Non-functional requirements

ID Requirement How it is checked Release Built in 0.1.0
NFR-001 Correctness evidence. Every behaviour has conformance vectors whose expected results come from a cited source or an independent generator, never from running fiscalclock. Conformance tests in all three packages; CI regenerates the oracle vectors and fails on drift (conformance/README.md). 0.1 Yes
NFR-002 Differential test. The core agrees with a deliberately slow day-by-day reference implementation for every date from 1900-01-01 to 2200-12-31 across at least 200 generated definitions. crates/core/tests/differential.rs 0.1 Yes
NFR-003 Properties. Tiling, nesting, shift and parse(format(p)) round trips; for rule-only week-based definitions, 364/371-day years and 71 long years in 400. Not applied to definitions with exceptions or table years. crates/core/tests/properties.rs 0.1 Yes
NFR-004 Cross-language identity. The same definition and arguments give byte-identical JSON in Rust, Python and JavaScript. CI job identical-output compares every binding's transcript with Rust's byte for byte (ci.yml). 0.1 Yes
NFR-005 Supported range. Fiscal years lying wholly between 0001-01-02 and 9999-12-30; anything else is OUT_OF_RANGE (ADR-0018). Range-edge vectors such as range-edges-nrf.json; check C18 explains the upper bound. 0.1 Yes
NFR-006 Deterministic, no global state. No clock, randomness, environment read or global mutable state in the core or bindings (ADR-0017). Review against AGENTS.md design rules 3 and 6. 0.1 Yes
NFR-007 No panics; every error has a code. Malformed input to any operation returns an error value. any_input_returns_json_and_never_panics in json_api.rs; fuzzing for 24 CPU hours is a Gate C check. 0.1 (fuzzing 0.2) Yes (fuzzing no)
NFR-008 Performance. Each binding reports single-date latency and million-date throughput. No numeric target is set. Benchmarks per binding. 0.2 No
NFR-009 Package size. The proposal calls for a bundle-size budget for the npm package in CI. No figure is set and no check exists. None yet. Unscheduled No
NFR-010 Security. The core uses #![forbid(unsafe_code)] and no_std with alloc; its only dependencies are serde, serde_json and sha2, and adding one needs the maintainer's agreement. Vulnerabilities are reported privately (SECURITY.md). The parsers get a security review before 1.0. crates/core/README.md; review in Phase 5. 0.1 (review 1.0) Yes (review no)
NFR-011 Supply chain. Packages are built and published only by release.yml from a tagged commit on main; every artefact gets a build provenance attestation; PyPI uses trusted publishing; npm publishes with provenance; each publish job waits for the maintainer's approval in a GitHub environment; actions are pinned to commit SHAs. crates.io and npm use tokens for the first publish. The reason given for crates.io, that trusted publishing needs a first manual release, comes from a summarised source that is still unverified (see Evidence). The release workflow's dry run builds, smoke-tests and attests without publishing. 0.1 Yes in the workflow; no package has been published yet
NFR-012 Minimum versions. Rust 1.85, Python 3.10, Node.js 22 (ADR-0025). CI job that builds and runs conformance on Rust 1.85; Python and Node matrices in ci.yml. 0.1 Yes
NFR-013 Platforms. Linux, macOS and Windows. CI matrices and release smoke tests on all three. 0.1 Yes
NFR-014 Licence. Dual MIT / Apache-2.0 (ADR-0014). Licence files; CI checks package copies match. 0.1 Yes
NFR-015 Documented evidence. Every factual claim in prose has a source or a reproducible check. Evidence; research/verification/ rerun by the docs workflow. 0.1 Yes

Acceptance criteria

Acceptance criteria are the gates on the Roadmap; this page does not restate them, so there is one list to maintain.

Release Gate Criteria
Phase 0 (no release) Gate A Roadmap: Phase 0
0.1.0 Gate B Roadmap: Phase 1
0.2.0 Gate C Roadmap: Phase 2
0.3.0 Gate D Roadmap: Phase 3
0.4.0 Gate E Roadmap: Phase 4
1.0.0 none Roadmap: Phase 5

A gate change needs an ADR.

Success metrics

Only metrics that can be measured are listed. Where no baseline exists yet, the table says so instead of setting a target.

Metric How it is measured Value as of 31 Oct 2026
Conformance pass rate in each package CI test jobs for Rust, Python and JavaScript All vector files pass in the CI run on main
Cross-language identity CI job identical-output: number of transcripts identical to Rust's, out of all produced The job requires 14 of 14
Claims reproduced by script research/verification/verify_claims.py output, diffed by the docs workflow 19 passed, 0 failed (expected output)
Cited conformance vectors Count of files in conformance/vectors/cited/ 2 (Apple, NRF); Gate C adds Vishay, Costco, Kroger and Care.com
Open contradictions in the source documents Count of D1 to D12 below with status Open 2 (D10 in part, D11)
fiscalyear issues addressed by a published release For #1, #6, #10 and #20, whether a published fiscalclock release has the matching feature 0 published. In the 0.1.0 build: #1 by week_pattern, #6 by having no global state, #10 by week numbering, #20 by short_month. This mapping is the project's own reading of the issues.
Downloads crates.io, PyPI and npm download APIs No baseline: nothing is published

Open questions

Contradictions in the source documents (D1 to D12)

The research proposal is the source of truth and the gap analysis is supporting evidence. Where they disagree, or the proposal disagrees with itself, the item is listed here with its status rather than resolved silently.

# Contradiction Status
D1 stub is documented as stub: first_and_last in the block table, but the Vishay example uses stub: { "anchor": {...} }. Resolved by ADR-0024 (Accepted, 1 Nov 2026): stub has a required anchor and a required absorb: "first_and_last". Implementation is planned for 0.2; stub is rejected with UNSUPPORTED_FEATURE in 0.1.0.
D2 The Nepal example uses year_number: "table" and boundaries[].number; the block table lists neither. Resolved by ADR-0022 (Accepted, 1 Nov 2026): boundaries is a list of dates, year numbers come from naming, and boundaries[].number and year_number: "table" are dropped. Table years ship in 0.2 (ADR-0021); until then year_number: "table" stays reserved in the spec.
D3 anchor has no weekday field; the examples imply the year-end weekday is the day before week.starts_on, but the proposal never says so. Resolved by ADR-0028, accepted under the maintainer's delegation and written into spec/definition-v1.md.
D4 WEEKS_SUM_MISMATCH defines the required sum only inside a week_based year; the sum inside a fixed_date year (Vishay) is unspecified. Resolved by ADR-0024 (Accepted, 1 Nov 2026): the pattern sums to 52 and the first and last periods absorb the difference, within a seven-day bound. Implementation is planned for 0.2; in 0.1.0 a week_pattern inside a fixed_date year is rejected with UNSUPPORTED_COMBINATION.
D5 The executive summary says "Five oracles"; the oracle table lists seven. Recorded. The wiki uses the seven-row table (Testing-Strategy).
D6 The gap analysis puts equivalent_day in v0.1; the proposal's feature table puts it in v0.3. Followed the proposal (the source of truth): the Roadmap places it in 0.3. Flagged, not settled by an ADR.
D7 The gap analysis puts the fiscalyear compatibility module in v0.1; the proposal gives it no release. Proposed placement: 0.4 on the Roadmap.
D8 The gap analysis says every claim was checked against a primary source, but some sources are Wikipedia and a forum post. Handled on the Evidence page, which classifies each source's method.
D9 The gap analysis uses is_long; the proposal uses has_extra_week. Resolved: has_extra_week (Glossary, spec).
D10 Unsourced naming claims: Thai government years in the Buddhist Era, "many Japanese companies" naming by start year, "FY 2025-26" common in India, the UK and Australia, and Indian FY/AY naming. Partly resolved. India FY/AY now has an official source, which corrects the claim (see Evidence and Analysis). The other three remain unverified.
D11 The Care.com divergence years depend on reading "closest and prior to December 31" as "on or before"; read strictly, 2022 also diverges, and other years outside 2019–2028 diverge too. Care.com also moved to a calendar year in 2019. Open. Checks C16 and C17 record both readings. The Gate C vector must state which reading it encodes.
D12 Whether anchored subdivision and special_periods are in v0.1 is unclear. Resolved for 0.1.0: both are reserved and rejected with UNSUPPORTED_FEATURE; the Roadmap plans them for 0.2.

Other open questions

  1. Which release gets aggregate labels and template aliases (FR-035), the instant-to-date helper (FR-087), upgrade() (FR-088), Parquet output (FR-040) and an npm bundle-size budget (NFR-009)?
  2. What performance targets, if any, should NFR-008 set once benchmarks exist?
  3. ADR-0022, ADR-0024 and ADR-0027 were decided on 1 Nov 2026 under the maintainer's delegation and may be revisited. Their implementation is planned for 0.2 (0022, 0024) and 0.4 (0027).

Traceability

Paths are on main. "Planned" means the test or vector does not exist yet.

Requirement ADR Test or vector
FR-001 ADR-0004, ADR-0005 validation.rs, conformance/vectors/errors/
FR-002 ADR-0005 fixed-jan-31-clamp.json, fixed-feb-30-next.json
FR-003 ADR-0028 42 week-based-* oracle vectors, such as week-based-end-nearest-saturday.json; iso-week-year.json
FR-004, FR-012 ADR-0021, ADR-0022 Planned (0.2); ADR-0022 Accepted
FR-005 ADR-0005 fixed-apr-1-india.json, fixed-oct-1-us-federal.json
FR-006 ADR-0005 nrf-4-5-4.json, nrf-53-week-years.json, apple-10q-2023-12-30.json
FR-007, FR-009, FR-010 ADR-0021 Planned (0.2); broadcast arithmetic is pre-checked by C10 to C14
FR-008 ADR-0024 Planned (0.2): Vishay cited vector
FR-011 ADR-0023 spec_rules.rs, oracle vectors
FR-013, FR-015 ADR-0010 validation.rs; one vector per code in conformance/vectors/errors/
FR-014 ADR-0010 Planned (0.2): Care.com vector expecting RULE_CONFLICT; C16, C17
FR-016 ADR-0008 calendar-id-pair-a.json, calendar-id-pair-b.json, canonical-non-ascii.json
FR-017 ADR-0009 Planned (0.2)
FR-020 to FR-023 ADR-0007 conformance.rs, differential.rs, properties.rs
FR-030 to FR-032 ADR-0010 labels-ambiguous-india.json, parse_inverts_format in properties.rs
FR-033 ADR-0029 spec_rules.rs, conformance/vectors/errors/
FR-034, FR-035 none yet Planned
FR-040 ADR-0008 dimension-nrf.json
FR-041 ADR-0010 json_api.rs
FR-050 to FR-052 ADR-0016 Planned (0.2): proration properties in Gate C
FR-060 to FR-062 ADR-0016 Planned (0.3); arithmetic pre-checked by C06 to C08
FR-070 to FR-075 ADR-0013, ADR-0027 Planned (0.4)
FR-080, FR-084 ADR-0002, ADR-0019 CI job identical-output
FR-081 ADR-0011, ADR-0025 test_conformance.py, test_api.py
FR-082 ADR-0020, ADR-0025 conformance.test.js, api.test.js
FR-083 ADR-0006 test_api.py, api.test.js
FR-085 ADR-0012, ADR-0003 Planned (0.2)
FR-086 to FR-088 none yet Planned
FR-089 ADR-0001 (a freeze needs an ADR) Planned (1.0)
NFR-001 ADR-0019 conformance/generators/check.py, conformance.rs
NFR-002, NFR-003 ADR-0002 differential.rs, properties.rs; C01, C02
NFR-004 ADR-0019 CI job identical-output in ci.yml
NFR-005 ADR-0018 range-edges-jan-1.json, range-edges-apple.json; C18
NFR-006 ADR-0017 Code review against AGENTS.md rules 3 and 6
NFR-007 ADR-0010 json_api.rs; fuzzing planned (0.2)
NFR-008, NFR-009 none yet Planned
NFR-010 ADR-0003 crates/core/Cargo.toml
NFR-011 none yet release.yml
NFR-012 ADR-0025 ci.yml
NFR-013 ADR-0002 ci.yml, release.yml
NFR-014 ADR-0014 LICENSE-MIT, LICENSE-APACHE
NFR-015 none research/verification/, Evidence

Clone this wiki locally