Skip to main content

Roadmap

What needs to happen for XTL 1.0 (spec) and xl3 1.0 (reference implementation).

The current version is 1.0.0-rc.1 (npm) targeting XTL 0.1 (draft). Breaking changes are still possible during 0.x. The 1.0 cut is gated on technical stability and the time-bound soak below. Ecosystem adoption is tracked separately and does not block the compatibility promise (ADR-0079).

Deep version planning lives in docs/internal/blueprint-to-1.0.md — gap analysis, philosophy boundary (xl3 ≠ JXLS), per-version step plan. This document is the elevator pitch; the blueprint is the rationale.

Single source of truth for 1.0 gates is the table below. When this file and the blueprint conflict, this table wins; the blueprint is updated to match.

What 1.0 means for xl3

The 1.0 target is one stable end-to-end promise:

application-owned JavaScript data → Excel template → generated .xlsx.

An application may pass an already-parsed xl3-source-json/0.1 object to convertJson; no source data file is required. xl3 applies the data to the operator-authored workbook and returns OutputFile[], with each completed workbook in OutputFile.data as a Uint8Array. The same API runs in Node and browsers.

That narrow workflow still carries operator-readable trust: a spec that doesn't shift, a reference impl that doesn't surprise, and a surface small enough that an operator can review a template without reading code. It is not about feature completeness vs JXLS — xl3 intentionally ships a smaller surface (ADR-0043 + ADR-0048). Database/API/ORM access belongs to the host, and dynamic images, custom host-language commands, streaming, macros, and pivot/chart authoring do not block 1.0. The intended audience is Korean operations teams that manage many customer-specific invoice formats (거래명세서, 정산서, 발주서); the engine generalizes beyond this niche, but the niche is the wedge.

1.0 blocking gate table (single source of truth)

Each blocking gate has an owner, the artifact that closes it, the pass-fail criterion, a fallback if the gate is unreachable, and the target milestone. Per-version step plan below references these gates by ID.

IDGateOwnerArtifactPass criterionFallbackStatus (audited 2026-08-30)Planned
G1Conformance corpus ≥ 140maintainerconformance/fixtures/ls conformance/fixtures/ | wc -l ≥ 140DONE — corpus is well past the ≥ 140 threshold; the live count is in conformance/DASHBOARD.md, which is generatedDONE (160 fixtures; ADR-0066 added 141-145 mid-0.7.x; ADRs 0067-0069 added 146-155 in 0.8.0; 156 (#49 native value preservation) + 157 (#51 grouped side cells) added post-0.8.1; 158 (#52 arithmetic associativity) in 0.9.0; 159-161 (ADR-0073/0074 subtotal errors) in 0.10.0; 162-169 (data-loss group, spec-derived) + 170 (numFmt across @repeat expansion, Stage 2) added 2026-07-28 for G24; number 098 is unused, so at that point the count was 169 against a highest index of 170; ADRs 0051-0065 reserved further numbers for 0.7.1)
G2Stage 2 OOXML canonicalization spec'dmaintainerADR-0006 + canonicalizer in src/covered by fixtures 024-027, 093 + ADR-0006 amendmentDONEDONE
G3Error code catalog frozenmaintainerimpl/js/src/__tests__/error-codes.test.ts snapshotno breaking catalog change for 30 days (additions allowed, logged — see "Frozen vs unchanged")DONE — held through 0.11.0 under the amended criterion. cabe0e1 added xl3/source-json/invalid (62→65 codes since v0.9.0); additive, so the gate stands. Last breaking catalog change: none since the gate ticked 2026-06-23✅ ticked 2026-06-23 (last catalog change 2026-05-24 a8f7ad3 +3 codes xl3/block/overlap·xl3/block/empty-table·xl3/directive/orphan; 89bee51 added xl3/expression/bracket-outside-block 2026-05-23; 30-day freeze elapsed — post-05-24 commits touched the file with comments/JSDoc only, EXPECTED_CODES unchanged)
G4JXLS boundary publishedmaintainerADR-0048file exists, references PORTERS_GUIDEDONEDONE
G5Deferred-impl ADRs landedmaintainerADR-0038 impl ✅ (2026-05-18) + ADR-0040 PE implADR-0038 portion shipped (fixtures 132-135); ADR-0040 CF/DV + outline level both landedDONE 2026-08-16 — the pure rule module (impl/js/src/range-extension.ts, 30 unit cases) is now wired in as the post-expansion sweep ADR-0040 calls for (extendRangesForExpansion), on both the plain and grouped @repeat paths, and pinned by Stage 2 fixture 171-cf-dv-range-extension. Writing the fixture for the other half — recorded as shipped in 0.6.0 but never covered, because the ADR cited a fixture number that went to an unrelated fixture — exposed a real defect: expanded rows never received the template row's outlineLevel, so they kept splice residue. Fixed in the expansion write loops and pinned by 172-outline-level-preservation. Both fixtures fail if the code is reverted0.6 (partial) / closed 2026-08-16
G6Public API surface frozenmaintainerimpl/js/src/__tests__/api-surface.test.ts snapshotno breaking surface change for 30 days (additions allowed, logged — see "Frozen vs unchanged")DONE — held through 0.13.0 under the amended criterion. cabe0e1 added convertJson/previewJson (0.11.0) and 30742f3 added VERSION/getEngineInfo (0.13.0, #103), taking the snapshot 13 → 15 → 17; both additive, so the gate stands. Last breaking surface change: none since the gate ticked 2026-06-17✅ ticked 2026-06-17 (snapshot unchanged since 2026-05-18 16f0608)
G7JSDoc examples on @stable exportsmaintainerTypeDoc outputevery @stable symbol has @example blockDONE — re-verified 15/15 @stable declarations carry @example✅ DONE 2026-06-21 — 13/13 @stable callables carried @example (PR #59); re-verified at 15/15 after 0.11.0 added convertJson / previewJson (previewJson shipped without one and was fixed 2026-07-27)
G8Performance characterizedmaintainerscripts/BENCH.md1k/10k/100k row × 5/10/20 col matrix + memory-ceiling + parse/eval/write split publishedDONE 2026-07-28 — npm run bench:matrix sweeps 1k/10k/100k × 5/10/20 (9 cells, all completing), with a parse/eval/write split and per-cell peak RSS. Published in scripts/BENCH.md. Headline: write is 61–82% of wall clock, parse is data-independent (~3 ms), and memory is the binding constraint at ~2.2 KB/cell (2M cells → 4.2 GB)0.7.1
G9Perf regression guardsmaintainerimpl/js/src/__tests__/perf-regression.test.ts≥ 2 large scenarios with a ratio-based assertion, running in CIDONE 2026-07-28 — row scaling and @join scaling, each asserting 10× the rows costs < 20× the time. Observed 6.2× and 6.9×; a quadratic regression lands near 100×0.7.1
G10Cross-browser smokemaintainerci.ymlSafari + Firefox bundle-load + 1 convert() per runDONE 2026-07-28 — cross-browser job runs npm run browser:smoke, which loads the IIFE bundle in Playwright webkit (the engine Safari is built on) and firefox, checks all 19 runtime exports from the shared manifest, and runs one convert() per engine against fixture 001. Not Safari-the-application: engine coverage only0.7.1
G11Stage 2 in CImaintainerci.ymlnpm run conformance:stage2 runs on every PRDONE — runs at ci.yml:350.7.1
G12Undecided behavior pinned (pivot/sparkline/ListObject/page break)maintainerconformance fixtures + ADR per itemeach: fixture pinning current behavior OR ADR explicitly deferring to 1.xdefer to 1.1 with ADRDONE 2026-07-28 — ADR-0076 takes the deferral arm for all four, amending ADR-0036 with rows 10-13 and recording measured reference-impl behavior as non-normative. No fixture: pinning a behavior the ADR declines to assert would have to be deleted in 1.1. Also resolves ADR-0046 § 5, which pointed at an ADR-0036 row that was never written0.7.1 / 0.8
G16Maintainer set wideningmaintainerGOVERNANCE.md≥ 2 people with accept/reject rights for ADRs and impl PRsexplicit accept of single-maintainer 1.0 governance shape via amendment to GOVERNANCEDONE (via fallback) 2026-07-28 — ADR-0077 explicitly accepts a single-maintainer 1.0 and GOVERNANCE.md states it. The primary criterion (≥ 2 accepters) is not met; the fallback is. ADR-0077 records what that does and does not mitigate, and names the triggers to revisit0.8
G17Korean cookbook i18n completemaintainerwebsite/i18n/ko/.../guides/all cookbook recipes have Korean translationDONE 2026-07-28 — guide 19 translated for ko / ja / zh-CN. All 19 guides + index now exist in all three localesDONE — recipes 01-18 in 0.6; guide 19 and all locale indexes completed 2026-07-28
G19Migration guide 0.x → 1.0maintainerdocs/migration-0.x-to-1.0.mddocuments every behavior change or confirms additive-onlydowngrade to CHANGELOG note if confirmed additive-onlyDONE 2026-07-28 — docs/migration-0.x-to-1.0.md written. The CHANGELOG fallback did not apply: 0.x was not additive-only. 17 behavior changes across 0.2/0.3/0.6/0.7/0.10 are documented, split by whether they raise or silently alter output0.8
G20SECURITY.md + threat modelmaintainerSECURITY.md + spec amendmentdocs zip-bomb / oversized workbook / formula-execution stance + limits APIDONE — SECURITY.md covers zip bomb, large workbook, formula stance, and points at the limits table0.7.1
G21Hard limits documented (no streaming until 1.1)maintainerspec/evaluation.mdrow / memory hard limit values + AbortSignal API documentedDONE 2026-07-28 — AbortSignal shipped (ConvertOptions.signal on all four entry points, xl3/abort/cancelled catalogued, no partial output). Limits now measured rather than drafted: spec/evaluation.md publishes ~2.2 KB/cell memory and a verified ~2M-cell ceiling from the G8 matrix. The unmeasured 1M-row soft cap was withdrawn — it needs ~10 GB and was never reachable0.7.1
G22API surface — internal model types separatedmaintainerimpl/js/src/index.ts exports + STABILITY.mdonly convert/preview/analyze + stable interfaces marked @stable; model/parser types marked @experimental or moved to xl3/internalDONEDONE (0.6)
G23RC soakmaintainergit tagsRC published; ≥ 21-day soak (extended from 7 day per review feedback); 0 critical issuesDONE — ticked 2026-06-16✅ ticked 2026-06-16 (21-day soak from rc.1 2026-05-26; 0 critical — soak-period fixes #49–52 folded into 0.9.0, none reset the clock per the G23 breaking-change definition)
G24Technical stability windowmaintainerrelease calendar90 days after the later of the final blocking gate closing or the last breaking spec/API/error-code change; all release gates green and no known critical data-loss/security issue at cutbreaking change → restart clock; critical issue → block cutIN PROGRESS — all blocking prerequisites are complete. G5 closed on 2026-08-16; the pre-RC audit then corrected the public OutputFile.data declaration from ArrayBuffer to the Uint8Array value the runtime had always returned. The conservative breaking-signature rule therefore starts the final window on 2026-08-30. The data-loss group is complete (9 fixtures, 162-170, tagged data-loss) and covers all 4 required paths — silent-stringify, date round-trip, formula-result kind, and numFmt dropwindow 2026-08-30 → 2026-11-28 if no further breaking spec/API/error-code change; adoption-track progress does not affect it

1.x adoption track (non-blocking for 1.0)

These metrics remain public project-health commitments, but they do not block the 1.0 compatibility promise. Their IDs are retained for stable references. See ADR-0079 for the separation of technical readiness from ecosystem growth.

IDAdoption outcomeOwnerArtifactSuccess criterionStatus
G13Second-language impl validationexternal (xl3-py)conformance/reports/*.jsonxl3-py passes ≥ 80% Stage 1 OR ≥ 80% Stage 2, or another Rust/Go/Java implementation reaches a documented 50%🟡 NOT JUDGEABLE — the last xl3-py report passed 133/133 fixtures it ran, but it is not current enough to establish a percentage of today's corpus; needs a fresh report
G14External-contributor ADRexternalspec/decisions/NNNN-*.md≥ 1 ADR with non-maintainer as Author (≥ 60% of Context/Decision sections by line count)OPEN — no qualifying ADR yet
G15Production reference caseexternal (with maintainer help)IMPLEMENTATIONS.md "Production users" row≥ 1 named user with permission to list, including a public case study from the maintainer's employerOPEN — no public named production user yet
G18Production use case in READMEmaintainerREADME.mdpublish a concrete production reference after G15OPEN — follows G15; it does not control the 1.0 status label

Status column audited 2026-08-30. The Planned column is the historical milestone plan and is kept as-is; where the two disagree, Status is the current fact.

0 blocking prerequisites are open; G24 is in progress. G5 and the G24 data-loss fixture requirement are complete; fixtures 162-170 cover the required paths. G13/G14/G15/G18 remain open in the non-blocking 1.x adoption track.

The technical stability window runs from 2026-08-30 through 2026-11-28. G5 closed on 2026-08-16, but the 1.0 RC audit corrected the declared OutputFile.data type to the Uint8Array runtime contract on 2026-08-30. The public-signature rule makes that the later date. Additive exports and error codes do not reset the window; breaking changes do.

The stability window is the binding constraint. External validation, an external contribution, and a public production reference remain important adoption goals, but they no longer determine whether the technical 1.0 contract may be cut.

Definitions (testable)

  • Adoption track (G13, G14, G15, G18): public, testable project-health metrics that continue through 1.x but do not start, stop, or reset G24.

  • External contributor (G14): not in GOVERNANCE.md maintainer set AND not in Co-authored-by history of merged ADR commits at PR open time. Drive-by typo edits do not count; named Author in ADR front-matter; authored ≥ 60% of Context/Decision sections by line count.

  • Breaking change (G24, G23): (a) removal, rename, or signature change of an existing export in the public API surface snapshot, (b) error code catalog rename / removal / repurpose, (c) ADR acceptedrejected or contradicting status flip. Patch releases and additive ADRs do NOT reset the quarter clock.

    Amended 2026-07-28. (a) previously read "any change to public API surface snapshot", which made it asymmetric with (b) — an added error code was additive but an added export was breaking. Read literally that made 0.11.0's convertJson / previewJson a clock reset, which contradicts the 0.11.0 note below. Both clauses are now scoped to changes that can break a caller. Additions remain append-only per ADR-0015 and MUST be recorded in spec/STABILITY.md.

  • Frozen vs unchanged (G3, G6): these gates guarantee the surface does not churn before 1.0, not that it stops growing. Their pass criterion is 30 days with no breaking change as defined above. Adding an export or an error code does not un-tick them; removing, renaming, or re-signing one does, and restarts both the 30-day window and — via G24 — the quarter clock.

    Rationale for scoping it this way rather than literal "unchanged": under a literal reading, any additive fix delays 1.0 by up to 30 + 90 days, so fixing a real defect is penalised. G21 is exactly that case — closing it requires adding xl3/abort/cancelled. A rule that punishes that pushes work toward not fixing it. Scoping to breaking changes keeps the criterion doing real work (a removal still resets everything) without creating that incentive.

  • Critical bug fix (G23 RC exception): (a) silent data loss in convert(), (b) error code catalog inconsistency between docs and runtime, OR (c) an accepted ADR's MUST that cannot be implemented as written. Maintainer cites which of (a)/(b)/(c) in the PR.

  • Perf guards (G9), why not in the corpus: the gate originally named conformance/fixtures/ and "≥ 2 large fixtures". Both halves conflict with the corpus itself. AUTHORING.md makes "fixture file sizes should be tiny" a hard rule, and the corpus is the cross-implementation contract every port must pass to claim conformance — a Python impl being slower than the JS one is not an XTL conformance failure. Amended 2026-07-28 to live as a CI test beside the reference impl, with data generated at run time so nothing large is committed. The ratio-based requirement is unchanged and is the part that mattered: an absolute budget encodes the machine it was written on, a ratio survives a hardware change.

  • Data-loss test (G24 testable form): corpus has a dedicated data-loss fixture group (≥ 8 fixtures) exercising silent- stringify, numFmt drop, formula rewrite, and date round-trip paths; all pass on the reference impl.

    Satisfied 2026-07-28 by fixtures 162-170. "Group" is the data-loss tag, not a subdirectory: conformance/fixtures/ is flat and the runner treats every directory in it as a fixture, so a nested data-loss/ folder would be read as a fixture with no meta.yaml. The dashboard already groups by tag. The numFmt-drop path cannot be pinned at Stage 1, which compares values and not formats, so 170 declares comparison_stage: 2.

  • Technical stability window (G24 vs G23): the 90-day window starts on the later of the final blocking technical/process gate closing or the last breaking change. Adoption-track metrics are excluded. If a breaking change happens during RC soak, both the soak (G23) and G24 reset. Under ADR-0079 the final implementation gate closed on 2026-08-16; the subsequent OutputFile.data signature correction moves the current start to 2026-08-30.

Per-version step plan

Gate-based, not date-based. Calendar estimates have been removed — each milestone closes when its listed gates close.

0.6.0 — Deferred-impl, narrow scope

Theme: close the highest-impact deferred-impl gate cleanly.

Gates closed: G5 (@group/@subtotal impl only — the rest of ADR-0040 PE moves to 0.6.1), G17 (Korean cookbook 16/17 missing translations), G22 (API surface cleanup before @group exposes new internal types).

The previous "single 0.6.0 with everything" plan was scoped too ambitiously per the engineering-feasibility review. ADR-0038 impl alone is a full pipeline insertion (new directive, group-boundary state machine, transform-pass partition, renderer rewrite, group-scoped aggregate eval). Splitting 0.6.0 keeps the milestone shippable.

0.6.1 — Rest of deferred-impl (planned, not yet shipped)

Gates closed: G5 completion (ADR-0040 PE: CF/DV sqref extension), pivot/page-break behavior fixtures toward G12.

Status as of 0.7.0 shipping: this milestone was bypassed by the spec-audit batch (0.7.0). G5/G12 work folded into 0.7.1.

0.7.0 — Spec-audit batch (shipped 2026-05-22)

Theme: close 17 syntactic-conflict gaps surfaced by a deep audit of the lexer, cell classification, directive composition, aggregate args, and reserved-sheet semantics. Unplanned in the original gate table; the perf/CI/limits work originally tagged 0.7.0 moves to 0.7.1.

Shipped artifacts:

  • 15 new ADRs (0051–0065) + amendments to ADR-0021 (group-order catalog entry) and ADR-0041 (header-cell multi-line normalization).
  • 4 new error codes — xl3/parser/unbalanced-literal, xl3/lists/invalid-use, xl3/eval/bad-aggregate-arg, xl3/expression/unknown-name.
  • Grammar additions: positive_integer, group_directive, subtotal_directive, aggregate_call, lexical-disambiguation note.
  • src/directive-parser.ts strictness for leading-zero integers.
  • Two-pass parallel review (claude-general + codex); all CRITICAL/HIGH findings closed before tag.

Gate impact:

  • G1 — 139 fixtures today. The 0.7.0 ADRs reserved fixture numbers 141–187; impl is pending. G1 closes when those fixtures land in 0.7.1.
  • G3 — 30-day error-code-catalog clock reset on 2026-05-22 by the 4 new codes.
  • G6 — no public API surface change; G6 clock unaffected.

0.7.1 — Performance + external validation begins (relabeled from old 0.7.0)

Gates closed: G5 completion (ADR-0040 CF/DV sqref range), G8 (perf benchmarks), G9 (perf regression fixtures), G10 (cross-browser), G11 (Stage 2 in CI), G20 (SECURITY.md draft + threat model), G21 (hard limits + AbortSignal docs).

Also closes the G1 ≥ 140 fixtures floor by landing fixtures 141–187 reserved by the 0.7.0 ADRs.

Progress toward: G12 (undecided behavior pinning), G13 (xl3-py).

Relabel: alphabeta after G8 publishes and xl3-py reaches ≥ 50% Stage 1.

0.8.0 — Data-block design overhaul (shipped 2026-05-23)

Theme: unplanned in the original gate table. A late-0.7.x audit of data-block expansion surfaced two structural bugs (#46 duplicate shared-formula owners, #47 stale references in displaced side cells) that needed a column-scoped rewrite before further feature work. The original "0.8.0 = sociological gates" plan moved to 0.8.x patches.

Shipped artifacts:

  • ADR-0066 — data block is now column-scoped: bounding box of {{...}} markers extended through contiguous non-empty cells. Cells outside that range keep their row positions when the block expands. Closes #46 / #47 by construction.
  • ADR-0067 — explicit @block directive in three forms (bare, A:D column range, A2:D7 rectangle).
  • ADR-0068 — strict multi-block detection on opt-in sheets: every [Column] marker must sit inside some block; block rectangles cannot overlap.
  • ADR-0069 — per-block directive scoping by proximity: @filter/@sort/@top/@source/@join/@group/@repeat attach to the closest data block whose column range overlaps the directive's column.
  • 4 new error codes: xl3/expression/bracket-outside-block, xl3/block/overlap, xl3/block/empty-table, xl3/directive/orphan.
  • Conformance fixtures 146-155 (multi-block, side-by-side blocks with different sources, vertically stacked blocks, per-block filter, per-block ROW() scoping, the three new error paths).

Gate impact:

  • G1 — corpus 139 → 154 fixtures. Floor cleared again.
  • G3 — 30-day error-code-catalog clock reset 2026-05-23 by the 4 new codes. Earliest tick: 2026-06-22, contingent on no further code additions/renames during 0.8.x patches.

0.8.x — Sociological gates (in flight)

Gates closed: G14 (external ADR), G15 (production case; in progress via maintainer's-employer deployment), G16 (maintainer widening or explicit single-maintainer acceptance), G19 (migration guide), G20 completion.

The plan is to ship 0.8.x patches during the recruitment period rather than wait silently. G3 quarter discipline: while sociological gates run, error code additions and renames are deferred — any addition resets the G3 clock and pushes the 0.9-rc target. Only critical-bug-fix codes (per the definition in this file) may be added during the 0.8.x window.

0.9.0-rc.x — Pre-1.0 freeze

Gates closed: G3, G6, G7, G23 (≥ 21-day RC soak).

This milestone originally treated G23/G3 as the start of G24. ADR-0079 supersedes that calculation: G24 starts only after the final blocking technical/process gate has closed.

0.9.0 — Final freeze cut (2026-06-23)

All four pre-1.0 freeze gates ticked: G3 (2026-06-23 — last catalog change 2026-05-24 + 30 days), G6 (2026-06-17), G7 (2026-06-21, PR #59), G23 (2026-06-16 — 21-day soak, 0 critical). RC-soak fixes #49–52 folded in; #54/#56/#57 deferred to the post-1.0 milestone (POST-1.0 additive — the milestone was titled "0.10.0" at this cut; renamed 2026-07-27 once 0.10.0 and 0.11.0 both shipped without these items, and #57 was closed as superseded by the host-driven __inputs__ source metadata in #82). At this cut, G24 was calculated from 2026-06-23 via G3 (confirmed by the 2026-07-28 audit, #86). ADR-0079 later superseded that calculation and rebased the window to 2026-08-16, when G5 became the final blocking implementation gate to close.

0.10.0 — Org move + @subtotal correctness (shipped 2026-07-19)

Repository transferred to the xl3-lang GitHub organization and the npm package renamed @jinyoung4478/xl3@xl3-lang/xl3 (install name only; the old package is deprecated on npm with a pointer to the new one). Folds in the @subtotal correctness fixes #66 (ADR-0073) and #69 (ADR-0074): a @subtotal row carrying a current-row [Column] reference, and group + subtotal under explicit @block mode, both used to fail silently with plausible-but-wrong output — each now raises a dedicated error.

Gate impact:

  • G1 — corpus 157 → 160 fixtures (159 mixed-row error, 160 formula-cache-is-not-a-marker, 161 explicit-block rejection).
  • G3 / G24 — 2 error codes added (xl3/subtotal/mixed-row, xl3/subtotal/explicit-block-unsupported); none renamed, removed, or repurposed. Additive per the breaking-change definition above, so the quarter clock does not reset.
  • G6 — public API surface snapshot unchanged. The rename affects the install name only: convert / preview / analyze, the subpath exports, and the xl3-conformance bin are identical.

0.11.0 — JSON source input (shipped 2026-07-19)

convertJson / previewJson accept the language-neutral xl3-source-json/0.1 wire format in place of a data.xlsx, so non-Excel hosts (Python, DB/ETL services) skip the workbook round-trip (ADR-0075, #71, implemented in #80). The JS reference impl also moved to impl/js/ under a root npm workspace (#79) — layout only, no behavior change.

Gate impact:

  • G3 / G24 — 1 error code added (xl3/source-json/invalid); additive, clock not reset.
  • G6 — the frozen surface gains two exports (convertJson, previewJson, recorded in spec/STABILITY.md). Additive: no existing export removed or re-signed, and the .xlsx path is untouched.
  • G24 window (historical calculation) — both cuts landed 2026-07-19 without a breaking change. ADR-0079 later rebased G24 to 2026-08-16, when the final blocking implementation gate actually closed; the old 2026-06-23 / 2026-09-21 calculation is superseded.
  • Resolved by the 2026-07-28 audit (#86) — the conclusion above is correct, but it rested on a definition that did not say so. Breaking change clause (a) read "any change to public API surface snapshot", which made convertJson breaking while an added error code was not. Clause (a) is now scoped to removal / rename / re-signature, matching (b), and G3/G6 carry an explicit "additions allowed" criterion. G3 and G6 therefore held through this cut and the quarter was never reset.

0.12.0 — CLI + byte-reproducible output (shipped 2026-08-02)

An xl3 bin so any language can render a template: it takes xl3-source-json/0.1 on stdin and writes .xlsx files, which closes the gap ADR-0075 opened but could not reach — a JVM or Python service no longer needs a JS runtime, or a throwaway data.xlsx, to render. Render output is also byte-reproducible now: zip entry mod-times were stamped from the clock, so identical inputs produced different bytes whenever two renders crossed a DOS-timestamp tick.

Gate impact:

  • G3 — no error code added, renamed, or repurposed. The CLI reports existing codes; it does not raise its own.

  • G6 — public API surface snapshot unchanged. The bin is a package entry point, not an export, and ZIP_ENTRY_DATE stays internal to excel-document.ts.

  • G8 / G9 — the determinism fix adds a zip header rewrite: 2 ms at 10k cells, 7 ms at 500k. bench moved 220 → 229 ms on wide-flat and not at all on the other two scenarios, so scripts/BENCH.md stands and the ratio-based perf guards are unaffected.

  • G24 window — output bytes changed for identical inputs while workbook content did not. Not breaking under the definition above, which scopes clause (a) to removal / rename / re-signature of the API surface and clause (b) to spec and error-code semantics. The spec's output contract is content-level — Stage 1 compares cell values, Stage 2 compares canonicalized parts, and neither reads zip mtimes — so the corpus is unaffected (169/169 at Stage 2, all three timezones) and the quarter is undisturbed. Worth stating explicitly because a changed output byte looks like the kind of thing that resets the clock: what the spec promises is the workbook, not the packaging.

    The one consumer-visible consequence: a golden-file test baselined on 0.11.0 bytes needs re-baselining. Documented in CHANGELOG under Fixed.

0.13.0 — Runtime engine identity (shipped 2026-08-12)

VERSION and getEngineInfo() (#103). A host fielding "this conversion looks wrong" is always asked which xl3 produced it, and there was no way to answer from inside the process: the main entry exported no version, version.ts holds semver helpers only, and exports did not expose ./package.json — so the remaining route was reading node_modules/@xl3-lang/xl3/package.json off disk and injecting it at build time. getEngineInfo() also answers whether the optional xl3-wasm backend is the one in use, which had no runtime route either.

Gate impact:

  • G3 — no error code added, renamed, or repurposed.
  • G6 — the frozen surface gains two exports (VERSION, getEngineInfo) plus the EngineInfo type, recorded in spec/STABILITY.md: 15 → 17. Additive, so the gate stands. Nothing existing was removed or re-signed.
  • G8 / G9 — render path untouched. VERSION is a compile-time constant and getEngineInfo() is never called during a conversion; bench sat at 240 / 71 / 71 ms, inside the <10% run-to-run band scripts/BENCH.md documents.
  • G24 window — additive under the breaking-change definition above. ADR-0079 later starts the technical window on 2026-08-16, when G5 closed; this release does not move that date.

One correction landed with this cut: the spec/STABILITY.md additive log carried ConvertOptions.signal and xl3/abort/cancelled as unreleased, but both shipped in 0.12.0 — impl/js/src/abort.ts and the catalog entry are present at tag v0.12.0. The rows now say so. The 0.12.0 CHANGELOG section still has no entry for the G21 abort work itself; that section is published history and is left as-is rather than backfilled.

1.0.0 — Final cut

Gate closed: G24 (the 90-day technical stability window completes), all repository release gates are green, and no known critical data-loss or security issue remains. G13/G14/G15/G18 continue on the 1.x adoption track. The frozen product claim is the in-memory JavaScript data → Excel template → .xlsx export workflow above, not JXLS feature parity.

Recruitment and outreach

Adoption-track outcomes (G13/G14/G15/G18) require people, not code. The project has two distinct recruitment surfaces:

Korean operations audience (G15, future cookbook contributors)

Channels: Korean developer communities (Naver Café, Kakao 오픈톡, LinkedIn KR), internal company / vendor template-author surveys. Each minor release publishes a Korean-language post tied to the release moment (0.6 = @group/@subtotal demo for invoice subtotal patterns; 0.7 = perf numbers; 0.8 = case study).

English OSS audience (G13, G14)

Channels: HN, lobste.rs, r/excel, conference CFPs (JSConf, EuroPython for xl3-py). Each major moment ships with one specific external artifact:

  • 0.7.0 release: "Show HN: xl3 0.7 — 100k-row Excel template engine"
  • 0.8.0 release: case study + xl3-py conformance dashboard
  • 1.0.0 release: stable technical contract + current port status, without claiming multi-implementation maturity unless G13 has actually closed

Non-goals for 1.0

These are intentionally deferred. Each has an ADR explaining why:

  • Date arithmetic beyond Y/M/D/EOMONTH/EDATE/DATEDIF — the rest of the family deferred per ADR-0019 amendment.
  • Locale-aware string collationADR-0020.
  • Multi-join, left-join, multi-row matchesADR-0014 out-of-scope section.
  • XLOOKUP wildcard / approximate / reverse searchADR-0013 out-of-scope section.
  • Dynamic image insertionADR-0037.
  • Runtime cell mutationADR-0042.
  • Functions rejected per ADR-0043 gate — math expansion, type tests (except ISBLANK per ADR-0047), NOW / WEEKDAY etc., conditional aggregates, TEXT() format-token expansion. See ADR-0045.
  • Streaming output / SXSSF analog. Deferred to 1.1+. At 1.0, hard memory/row limits are documented (G21) instead.
  • Template compile caching API. Deferred to 1.1+.
  • PDF / HTML output. Out of scope; xl3 is xlsx-in, xlsx-out.
  • Cross-writer Stage 2 fixtures beyond 093ADR-0006 amendment.
  • Output-side template schema (produces) and design-time chaining — the input-validation proposal in xl3#109 (validateSource(): "does this source satisfy this template?", reusing the existing xl3/source/* codes) is scoped to the input side first. Its output counterpart — TemplateModel.produces carrying output column labels, which is what would let a host verify a templateA → templateB edge without running a conversion — is deferred. It needs a convention the project has not fixed: which literal row of a sheet template is the output header. Unlike requires, that is not derivable from what the parser resolved, so it has to be decided and written down rather than inferred independently by each engine. No ADR yet; the design discussion is in xl3#109.

These remain candidates for XTL 1.1, 1.2, 1.x based on demand.

How to help advance the adoption track

ItemHow to help
G13 second-impl ≥ 80%Contribute to xl3-py, or start a new port (Rust, Java, Go). See PORTERS_GUIDE.md.
G14 external ADRPick a deferred item (pivot table preservation, page-break, ADR-0045 carved-out function), draft an ADR in spec/decisions/. See GOVERNANCE.md "How changes enter the project." A few "starter ADR stubs" are available as good-first-ADR issues on GitHub.
G15 production caseUse xl3 internally, share what worked / didn't. Drop a row in IMPLEMENTATIONS.md if appropriate. The maintainer's own employer (Snack24h) qualifies if it ships a public case study.
Function re-proposalIf you need a function rejected per ADR-0045, file the Function re-proposal issue template.

How this roadmap evolves

This document is the public elevator pitch; the blocking table and adoption track are the single source of truth. The deeper docs/internal/blueprint-to-1.0.md carries the gap analysis, philosophy boundary, and per-version rationale. As gates tick, both documents update. As new gaps surface, both add them.

Cuts, additions, or movements between the blocking table and adoption track are discussed via the same ADR/issue process as everything else.