Skip to content

Changelog ​

All notable changes to UW Markdown — the format spec, the protocol spec, the reference library @uwmd/core, the conformance corpus, and starter tools — are documented here. The format is based on Keep a Changelog, and the project follows semantic versioning per surface (the format, the protocol, and each package each carry an independent semver).

Unreleased ​

Accepted normative errata — Protocol 2.17.1 (RFC 0062, unreleased) ​

  • Same-day cash_flow_series.series rows remain distinct and no longer emit PS-02 merely for sharing a valid date. Ordinary date selection still refuses CALC-PERIOD-002 for a repeated requested date; a unique date resolves even when another date repeats. Other registered series and whole-column Excel duplicate guards retain their existing rules.
  • Jared accepted RFC 0062 on 2026-09-24, retaining 2.17.1 as normative errata. Status is accepted, not implemented: the reference implementation and tests are prepared but have not shipped. Format stays 2.0; package versions, dependencies and tags do not change. Published core/CLI 2.13.0 continue to pair with Protocol 2.17.0.

2.13.0 - 2026-09-21 ​

Core/CLI 2.13.0 add the already-specified period-indexed navigation capability and ship implementation, security, and representation fixes against Protocol 2.17.0 and Format 2.0. Signing 0.2.17 and batch 0.8.12 repin core. Standalone packages remain unpublished.

Released ​

The v2.13.0 tag published core/CLI 2.13.0, signing 0.2.17 and batch 0.8.12 to npm with SLSA provenance through trusted publishing (OIDC).

Fixed ​

  • Empty-frontmatter-array fidelity. Model-fidelity exports now serialize an empty frontmatter array as [] instead of a bare YAML key. The bare key reparsed as null and changed the semantic digest on seven private golden-deal/control cases under published 2.12.0; the corrected serializer preserves [] and all model-fidelity round-trips pass. See the de-identified acceptance record.
  • Protocol version-label integrity (RFC 0061). Corrected the protocol document's stale 2.15.0 status line and 2.8.0 §0.3 label to the authoritative 2.17.0, and extended verify-versions to fail if either checked mirror drifts from VERSIONS.md and PROTOCOL_VERSION again.

Added ​

  • Period-indexed path navigation in calc/evaluator.ts, resolving a registered series by its stated period identity (dcf.annual_cash_flows@Y3) per §VIII.2a. Selection is by identity, never by row position.
  • Release readiness check. scripts/check-release-readiness.mjs, wired as npm run release:check and into .github/workflows/release.yml, verifies that npm Trusted Publishers OIDC is configured before a v* tag triggers a publish — the tag is the trigger, so a missing trusted publisher otherwise fails after the point of no return.

Validation ​

  • The frozen private golden corpus accounts for 532 Artifact B assertions: 526 pass and six remain documented source/baseline defects. All 20 refusal assertions and all eight runnable cases pass in this release. GD04 and GD07 remain evidence-blocked. This broad real-deal acceptance does not claim RFC 0045 real-deal property-cash-flow assembly; the required explicit periodic inputs and coverage remain unavailable.
  • Release preparation passed 2,433 workspace tests across 138 files, 589 default conformance checks, all three RFC 0030 capability profiles, 46 JSON Schema compilations, 221 emitted-code checks, test typechecking, lint over 1,047 files, package-content verification, documentation build, version/index/release/lockfile guards, and OIDC release readiness.

Decided ​

  • RFC 0060 — the four tranche-class candidates, and why none opens the enum. The roadmap carried ground_lease, pace, tax_credit_equity and soft_debt as proposed capital_stack classes, to be settled in one bounded RFC. They are settled: TrancheClass is unchanged, and no schema, classification set, sizing function, validator rule, Excel formula or conformance fixture moves.

    • ground_lease — wrong layer. A Tranche has no field for tenure, and no honest amount exists for one. Where the underwriting includes ground rent in OpEx, adding it again as a debt tranche would double-count it.
    • pace — no enum; other_debt is honest where the underwriting treats the obligation as a debt-service-bearing tranche whose terms the existing fields express.
    • soft_debt — no enum; other_debt covers the subset reducible to amount, rate, accrual, position, amortization, IO and term.
    • tax_credit_equity — deferred with its mechanics.sources_uses.sources.tax_credit_equity already records the amount, and the credit investor's return is not a rate, so the class has nothing a sizing verb consumes until a LIHTC profile specifies pay-in and delivery.

    The RFC closes the enum-opening question only. It does not claim these domains are modelled, and says so explicitly. Ground leases in particular are not represented: §4.4 has no ground_rent key, §4.5 noi_model.expenses has no ground-rent line and no generic bucket at all, and no section types the leasehold as an object. Likewise unmodelled: PACE assessment servicing and lien behaviour; residual-receipts, contingent payment and forgiveness for soft debt; tax-credit pay-in, delivery, compliance and recapture. Each survives as demand-gated mechanics, profile or tenure work needing its own RFC and a demonstrated consumer — not as tranche-class debt. decided is terminal; nothing ships from it.

Removed ​

  • The unreleased SQL export surface, withdrawn before release. src/sql.ts exposed exportSql, exportSqlStatements, UWSqlError and ExportSqlOptions from @uwmd/core and its browser entry, added uwmd export --format sql with --schema / --no-ddl / --no-views, and emitted PostgreSQL and Snowflake DDL for six tables plus four reporting views. It is removed. Nothing consumed it outside its own tests, and no released version ever contained it.

    RFC 0049 settles the relational boundary, and this crossed it. The RFC's Non-goals name "warehouse-specific SQL" outright, and it assigns the integration to @uwmd/lake; docs/DATA_LAKE.md opens by saying UWMD "is the backbone of a CRE data lake, not the lake itself." The CLI tests labelled the feature "RFC 0049 database lake export", which the RFC does not authorize.

    It was also a live defect. Both @uwmd/lake and the exporter define uw_documents with the same semantic_digest primary key and the same three index names but different columns — the exporter omits path, currency_code, format_version, protocol_version, document_profile, valid, error_count and warning_count — and both default to schema public, so they resolve to one relation. Core-then-lake makes the lake's INSERT fail on a missing column; lake-then-core writes catalog rows with a null validation verdict.

    Withdrawn rather than moved into @uwmd/lake or a new package: a second, BI-shaped relational projection is an architectural decision, and no adopter requirement exists to justify settling its package and schema now. If one appears it returns through an RFC. JSON export is unchanged — uwmd export and stringifyUWEnvelope behave exactly as before, and the --format flag returns to not existing.

  • The unspecified Tier-3 collection surface, withdrawn before release. Two commits on this branch added sixteen builtins — sum_by, avg_by, min_by, max_by, count_by, count_where, filter, filter_by, find, find_by, map_by, pluck, values, to_array, get, prop — plus numeric bracket indexing in the grammar. All of it is now removed. Nothing consumed it: no pack, no fixture, no tool.

    It was out of spec on four counts. §VIII.3's table is the enumerated builtin set and §II.3 clause 2 requires a host to implement that set; none of the sixteen were in it. §VIII.1 defines member ::= "." identifier | "[" string "]" and §VIII.2a says in terms that "numeric bracket indices are not added to that grammar", yet the parser accepted a[0]. §VIII.1 also requires a host to reject what does not parse against the grammar, so this engine accepted documents every other conforming engine must refuse. And filter/map_by returned arrays into CalcResult.value, which spec/schemas/calc-result.schema.json — the normative schema declared at the head of Part VIII — types as number | string | boolean | null. filter rendered as the display string "[object Object]".

    RFC 0019 had already considered this exact primitive and rejected it (Alternatives §1), choosing static component slots precisely so that "named ranges stay static and the existing emitter is sufficient". Reintroducing it reopened the Excel↔calc parity hole that design closed: the emitter targets Excel 2016, where FILTER and LAMBDA do not exist and an array-valued cell has no representation, so filter and map_by had no workbook counterpart at all.

    The five scalar aggregators do have a plausible Excel story (SUMIF, COUNTIF, AVERAGE, MIN/MAX). If a concrete use case appears, they should return through an RFC that pins the grammar delta, the §VIII.3 table rows, and the emission contract — including the empty-collection, null-versus- blank and truthiness semantics, each of which diverges between the calc engine and Excel and so decides parity rather than following from it.

    A regression test now pins Object.keys(BUILTINS) to the §VIII.3 set and asserts the grammar rejects numeric indices, so the surface cannot drift back by accretion.

Security ​

  • CalcResult.value is type-checked instead of cast. evaluateCalc cast the evaluator's result straight into number | string | boolean | null, so a non-scalar reached receipts and the CLI unchecked. It is now refused with CALC-TYPE-001. The removal above eliminates the only builtins that could produce one, but the guard is the invariant, not the absence of a caller — receipts digest this field.

  • The calc evaluator refuses prototype-polluting path segments. A Tier-3 path could previously name __proto__, constructor or prototype and walk into the prototype chain of the evaluation context. The evaluator now refuses such a segment with the new CALC-FORBIDDEN-PROP code, alongside the existing MAX_NODES traversal bound. This matters more now that RFC 0019 lets a formula traverse collections rather than only fixed paths.

Fixed ​

  • irr bisects again, as protocol §VIII.3 requires. An earlier commit on this branch replaced the bisection loop with a Newton-Raphson pass seeded at 0.1 with a 1e-7 epsilon — the exact procedure §VIII.3 step 5 says an implementation MUST NOT use, and the one RFC 0024 was written to remove. The bisection loop, the 1e-9 / 1e-12 stopping conditions and the endpoint- root check are restored; IRR_INITIAL_SEED and IRR_CONVERGENCE_EPSILON are gone rather than left as dead exports.

    Two consequences, both user-visible:

    • Cross-engine reproducibility is restored. Newton's iterates depend on a derivative evaluation order no document pins, so the regression silently gave up the bit-identical-root guarantee that is RFC 0024's entire purpose.
    • Some bracketed cash flows stopped refusing. irr(-100, -200, 110) brackets a real root at ≈ -0.5509 but Newton stepped outside [-0.999, 10] and raised CALC-IRR-DIVERGE; it now returns the root. Conversely, roots are tighter: irr(-5, 1, 15) returned an NPV residual of 1.9e-8 under the loose epsilon and now meets 1e-9.

    Every conformance fixture passed throughout, in both directions — Newton and bisection agree within the §VIII.5 six-decimal quantum on all of them, exactly as RFC 0024 predicts. The defect surfaced only through an unseeded property test drawing a seed that hit [-5, 1, 15].

  • The calc property suite is seeded by default. calc.property.test.ts drew a fresh seed on every run, so a real defect appeared as an intermittent CI red on whichever push was unlucky and could not be reproduced from the failure alone. It now pins a seed; UWMD_FUZZ=1 restores random exploration, and counterexamples it finds should be pinned as examples in calc.test.ts — [-5, 1, 15] now is.

  • @uwmd/lake 0.1.2 → 0.2.0, lake schema 0.1 → 0.2. The RFC 0049 adapter was exercised against a live PostgreSQL server for the first time — the whole conformance corpus, 382 documents and 24,380 facts, in one transaction — and three defects fell out that an in-memory double could not have caught.

    • uw_facts.value_json was jsonb NOT NULL, but UWMD's canonical fact table represents an object or an array by its flattened children and leaves the container's own value empty. An empty string is not JSON, so 21.6% of real corpus facts were unloadable and the first one aborted the load. The column is now nullable under CHECK (value_json IS NOT NULL OR json_type IN ('object', 'array')); a container is SQL NULL, and a scalar with no value is refused with the new LAKE_FACT_VALUE code rather than silently nulled.
    • uw_receipts.verdict projected a field no receipt carries — a verdict is what verifying a receipt produces (UW_RECEIPT_v1 §5), not something a receipt states — so the column was NULL for every real receipt and uw_receipts_verdict_idx led with a dead column. Both are replaced by validation_errors / validation_warnings from policy.validation, and the index by (pack_id, pack_version). The unit test had missed this by inventing computation.verdict in its fixture.
    • postgresLakeSchema() returns a multi-command script, which cannot be sent through LakeClient.query(sql, params) at all: a values array, even an empty one, selects the extended query protocol. New postgresLakeSchemaStatements() returns the commands individually and is the source of truth; the README and docs/DATA_LAKE.md examples are corrected.

    Also documented, not changed: uw_packages is keyed by the manifest's declared package_id, the one table not keyed by a digest, so two packages claiming one id collapse. See the load record for the evidence, the row counts, and what the run does not prove — no network, no concurrency, no managed service, and a GIN index the planner did not choose at corpus scale.

[2.12.0] - 2026-09-16 ​

Added ​

  • RFC 0058 types expense recoveries and the CAM true-up on the commercial tenant record (§4.3). recovery_terms carries the pro-rata share, the stop, the recoverable pool and a cap whose accumulation must be stated; recovery_true_up records a closed period's reconciliation, with REC-06 checking the share of the pool and REC-08 the settled amount. Registers the REC-NN family, including REC-09, which requires a stated cash_flow_ref to resolve — the settled amount lands in §4.26, the addressable sink where assembly and receipt coverage already verify dated cash. Nothing is projected, grossed up or allocated across tenants.
  • RFC 0059 implements the distribution-waterfall clawback as the terminal true-up protocol §XVI predicted, closing a gap RFC 0035 deferred and RFC 0036 deferred again. An optional distribution_waterfall.clawback states a basis (lp_preferred_shortfall, lp_irr_floor or lp_em_floor), an optional stated net_of_tax_rate, and a cap that must be "promote_received" — a GP cannot owe back more promote than it received. stated_outcomes.clawback_amount is verified against the recomputation at the currency quantum. Registers WF-10–WF-13 and the WF-15 warning; protocol §VIII.10 gains step 5. Nothing is escrowed, crystallized per period, or projected.

Changed ​

  • Protocol 2.15.0 → 2.17.0 for §VIII.10 step 5 and the clawback codes (RFC 0059) and the REC-NN family (RFC 0058). No wire format, formula or precision change; the two receipt fixtures that embed protocol_version are repinned.

Fixed ​

  • verify-codes is a new CI guard: every code an implemented RFC's table or a format-spec rule bullet promises must be one @uwmd/core actually emits, and every emitted validation family must be registered in protocol §XI. It exists because RFC 0058 shipped nine of the ten codes it specified and nothing went red — schemas validated, conformance passed (no fixture exercised the missing rule, because the fixtures came from the same incomplete pass), and the RFC read as delivered. A missing refusal looks exactly like a document with nothing to refuse.

Notes ​

  • Every clawback basis is closed-form. The IRR floor reuses RFC 0036's hurdle balance verbatim rather than iterating on xirr — the calc engine has no iteration, and a design needing a nested solve would have been unreachable. A test pins the boundary property: at a floor equal to the IRR the LP actually achieved, the true-up is zero.
  • RFC 0058 leaves two figures stated, not recomputed, and says why in the spec: the capped amount, because a cumulative or compounding cap depends on a base-year history no single document carries (REC-07 checks only the direction a cap can move), and the allocation of a pool across tenants, because that needs a vacant-space policy and is a modeling decision. REC-05 anchors on the rent roll's own as_of_date and is skipped when absent — never on file metadata, which is an edit timestamp.
  • A waterfall with no provision reports null; a provision that computes to nothing reports 0. Absence and zero are different answers, and a stated amount with no provision behind it is unverifiable, not a disagreement.

[2.11.0] - 2026-09-16 ​

Core/CLI 2.11.0, signing 0.2.15, batch 0.8.10, Protocol 2.15.0, Format 2.0. Five RFCs, all additive: every member of every new structure is optional, and a document stating none of them validates exactly as it did at 2.10.0.

RFCs 0055, 0056 and 0057 type structures nothing yet consumes. No rent escalates, no break is exercised, no balance amortizes, nothing is priced, no strike crossing is projected and no stated saving is applied. Typed-but-inert is the intended state: the format learns to say these things before anything acts on them, and each consumer arrives with its own contract.

Three of the new rules require a disclosure rather than defaulting one, because in each case the unstated reading is the misleading one: HDG-06 (what happens when a rate cap expires), CAPX-07 (whether a stated expense saving is already inside the NOI model) and, from 2.10.0, TAX-08 (whether the terminal tax is inside exit NOI).

Added ​

  • RFC 0049 is implemented as @uwmd/lake 0.1.0 (packages/uwmd-lake, unpublished): the reference PostgreSQL/JSONB lake adapter. It plans idempotent, parameterized INSERT … ON CONFLICT … DO UPDATE statements over six tables from canonical outputs — envelopes, block_values facts, receipts, package manifests and source-evidence references — and executes them against an adopter-supplied { query(sql, params) } client. Raw canonical JSON is stored in jsonb alongside typed shadow columns, so unknown sections, extension keys, explicit nulls and array order survive a load. Identity is always a digest, never a file path.

  • @uwmd/lake readers for the three shapes adopters already hold: lakeInputFromEnvelope, readBlockValuesCSV (UW CSV bundle) and readBatchFactJSONL (@uwmd/batch corpus fact table).

  • RFC 0055 types the commercial lease clauses that have been untyped stubs since Format 1.0 — escalation_schedule, termination_option and co_tenancy_details — and adds the lc_original / lc_outstanding_balance pair beside the tenant-improvement one, registering the LSE-NN validator family. Steps state the resulting rent rather than the increment. Nothing is exercised: no rent escalates, no break is taken, no remedy applies and no balance amortizes. Purely additive.

  • RFC 0056 types debt_structure.rate_hedge and sources_uses.uses.escrows, registering the HDG-NN and ESC-NN validator families. A cap now carries a strike, a notional, a term and a stated post_expiration_assumption rather than the lone rate_cap_pct it had; escrows carry upfront and monthly amounts under a closed vocabulary with a label-bearing other. ESC-04 ties a "replace" assumption to a funded rate_cap_replacement line, which is the budget a three-year cap on a five-year hold has never had anywhere to go. rate_swap and rate_collar are reserved and refused by HDG-02: their mark-to-market can be negative and a cap's cannot. Nothing is priced and no strike crossing is projected. Purely additive.

  • RFC 0057 types sources_uses.uses.renovation — budget, contingency, contingency used, a stated-and-verified remaining, and total drawn as of a date — and adds expense_targeted, capital spend named against the noi_model.expenses line it reduces. Registers the CAPX-NN family. CAPX-07 requires in_noi_model, the disclosure that keeps a stated saving from being applied twice; nothing subtracts a saving from an expense line, from EGI or from NOI. CAPX-08 verifies simple_payback_years and refuses it against zero savings rather than admitting an infinity. Redevelopment downtime needs no new field: §4.25 natural_turnover already carries it. Purely additive.

  • RFC 0054 decides where per-lease economics live and declines the roadmap's premise that lease clauses, CAM true-ups and TI/LC balances share a container. It splits by shape: clauses are attributes of a lease and are typed in place by RFC 0055, while the periodic series waits for a named consumer. The series half is unbuilt by decision, not by oversight — the Tier-3 calc grammar addresses neither collections nor two period dimensions, so a lease × period ledger is unreachable from any pack formula.

Changed ​

  • Protocol 2.13.0 → 2.15.0, registering the LSE-NN, HDG-NN, ESC-NN and CAPX-NN code families. RFC 0055 added LSE-NN to the protocol's code-prefix table without a bump, so released 2.13.0 and main briefly described two different tables; the bump covers all four. No wire format, formula or precision changes.

Notes ​

  • The adapter adds no database driver to any package and does not touch the protocol, the format spec, the schemas, conformance or financial math. Source evidence stores identity and status only: a bytes-bearing payload is refused with LAKE_SOURCE_BYTES. No live PostgreSQL instance is exercised by the tests; a real load remains an adopter integration step.

Fixed ​

  • Format §4.25 and §4.26 claimed lease_up_schedule and cash_flow_series cells stayed "addressable by ordinary path traversal", citing expressions that raise CALC-PARSE-001. Calc paths are flat identifiers; those structures are read by verifiers and host code, not by pack formulas.
  • verify-indexes now fails an RFC that is empty, has no frontmatter block, or has frontmatter with no body, and checks the index table's status column against each RFC's own frontmatter. Both gaps let real drift through.
  • The RFC index listed 0046 (currency identity) and 0047 (property cash-flow input inventory) as accepted after both shipped in 2.10.0.

[2.10.0] - 2026-09-15 ​

Added ​

  • Read-only uwmd verify-cash-flows <file> [--variant <name>] [--json] over the existing cash-flow metric verifier, with strict input guards, explicit no-stated-metrics results and automation exit codes. No protocol, financial formula, precision tolerance or package-version change.
  • RFC 0046 adds optional document-level frontmatter.currency_code and CUR-01. Explicit identity is rendered as a code prefix while locale still controls numeric separators; absent identity preserves legacy symbols. No FX, mixed-currency arithmetic, or currency-specific precision is introduced.
  • RFC 0047 adds read-only inspectPropertyCashFlowInputs and uwmd inspect-property-cash-flows, inventorying source variants, periods, rows and required plan dimensions without assigning economic categories or inferring expense, reserve or payment-timing inputs.
  • RFC 0048 adds a worked standalone document kit: lease abstract and source note profiles, .uwpart.md fragments, inline/externalized composition twins, a packaged .uwpkg.zip example and a named standalone conformance suite.
  • RFC 0050 adds accrual: "split" on preferred-equity tranches, with cash_rate entering cash coverage, accrued_rate excluded from it, full rate driving weighted cost, and CS-02b validation. Debt PIK toggles and accrued compounding remain deferred.
  • RFC 0051 lifts hurdle_mode: "any" | "both" into the normative waterfall contract. On a split tier stating both until_lp_em and until_lp_irr, any caps at the smaller capacity and the default both keeps the larger. WF-01 rejects hurdle_mode without both hurdles.
  • RFC 0052 adds an optional closed sale_deductions vocabulary naming every disposition cost-of-sale row, and an optional net_sale_proceeds figure verified against gross sale less exit costs at the currency quantum. prepayment_penalty, defeasance and loan_payoff are reserved and refused by this unlevered assembler. Purely additive: a plan stating neither member is unchanged. No financial formula or derived exit value is introduced.
  • RFC 0053 types the noi_model reassessment basis and adds an RFC 0041 period-addressed abatement schedule, registering the TAX-NN validator family. TAX-01-TAX-04 check the basis arithmetic, with a round_to_decimals that may be negative so a deliberate rounding is declared rather than unexplained. TAX-05-TAX-07 check the schedule and its tie to the stabilized value. TAX-08 requires a sale-triggered terminal basis to equal exit_value_gross, catching a going-in tax carried into terminal NOI. Stated and verified only: no tax is derived, no jurisdiction rules are inferred, and the exit-value circularity remains the author's to converge.

Changed ​

  • property-cash-flow-assembly.schema.json now references property-cash-flow-plan.schema.json instead of restating it. The hand-copied duplicate had already drifted once; a regression test fails if the reference is replaced by a copy again. No validation outcome changes.
  • The §VIII.5 half-away-from-zero quantizer is exported from cash-flow-series.ts so a surface comparing a stated figure against a recomputation rounds where the verifier rounds, rather than keeping its own copy.

Fixed ​

  • examples/standalone/package/** is marked -text. sources/anchor-lease.txt was CRLF in the working tree and LF in the index, so the manifest pinned a sha256 over bytes no checkout reproduces and standalone/package/integrity failed on every CI runner while passing locally.
  • RFC 0051's title is quoted. It is the first RFC title to contain : , which made VitePress read the plain scalar as a nested mapping and fail the site build. verify-indexes now reports an unquoted frontmatter scalar carrying : across all copied RFCs, so the break surfaces locally.
  • VERSIONS.md no longer claims @uwmd/excel and @uwmd/report are unpublished. Both serve a stale 0.3.0 on the registry, pushed 2026-08-16 during the 1.3.0 manual release and pinned to @uwmd/core 1.3.0. Both are pending deprecation. verify-versions never contacts the registry, which is why the claim went unchallenged.

Released ​

Core/CLI 2.10.0 with Protocol 2.13.0 and Format 2.0. Signing 0.2.14 and batch 0.8.9 repin core. Excel 0.9.2, report 0.8.14 and the 0.1.2 reference module packages remain unpublished.

Verification ​

  • Build, full workspace test suite, test typechecking, 504 default plus 76 declarative conformance checks, three capability profiles, 38 JSON schemas, lint, and the lockfile, package, version, index and release guards.
  • Receipt baselines regenerated for the new engine version, including the 03-result-disagrees receipt whose engine_version must be the new one or RCP-07 reclassifies the scenario from failed to unverifiable.

[2.9.0] - 2026-09-12 ​

Released ​

Core/CLI 2.9.0 package RFC 0045 with Protocol 2.12.0 and Format 2.0. The v2.9.0 trusted-publishing workflow published all four packages; clean registry installation and API/CLI behavior were verified. Signing 0.2.13 and batch 0.8.8 repin core. Excel 0.9.1, report 0.8.13 and the 0.1.1 reference module packages remain unpublished.

Added ​

  • RFC 0045 / Protocol 2.12.0: candidate-only property cash-flow assembly, explicit unlevered/pre-tax coverage and source evidence, with a read-only CLI. The synthetic example is engineering data; real-deal validation remains separate.

Verification ​

  • Clean installation, build, 1,990 workspace tests, 457 default plus 76 declarative conformance checks, 34 JSON schemas, test typechecking, lint, release consistency checks and documentation build passed.
  • Isolated four-package tarball installation verified core/browser and CLI assembly, typed refusals, source preservation and pinned synthetic metrics. Receipt changes are engine-version labels only.

[2.8.0] - 2026-09-12 ​

Released ​

Core and CLI 2.8.0 package the merged RFCs 0042–0044. Protocol remains 2.11.0 and Format remains 2.0. The v2.8.0 tag and trusted-publishing workflow published core, CLI, signing and batch; clean registry installation was verified. Signing 0.2.12 and batch 0.8.7 repin core; unpublished report 0.8.12, Excel 0.9.0 and the two 0.1.0 reference modules remain outside the publication workflow.

Added ​

  • RFC 0042: refinement uses finite stated period inputs while ranking ordinary scalar gaps. Exact variant/override context and per-output diagnostics preserve missing, nonnumeric, malformed, duplicate and ambiguous inputs distinctly.
  • --calc-context supplies validated inputs to uwmd calc, uwmd refine and explicit workbook export. Refinement refuses ordinary scalar overrides; workbook export requires selected custom-calculation IDs.
  • RFC 0043: contextual Excel bindings and complete period-column snapshots, with public types/schemas. The source Excel package exports selected arithmetic calculations across all five standard series. Native Excel verification covers 14 scenarios and 48 cell checks. Reverse import of additional inputs refuses.
  • RFC 0044: browser-safe projectLeaseUpCashFlows and read-only uwmd project-lease-up map a verified exact lease-up variant onto explicit dates. Candidate output preserves exact stated amounts, semantic source digest, and canonical bindings. Typed refusals retain structural/verifier evidence. The output covers rent receipts, concessions and TI/LC only; full DCF economics and cash-flow metric Excel export remain separate contracts.

Verification and documentation ​

  • 1,931 workspace tests, 441 default plus 76 declarative conformance checks, and 31 JSON schemas pass after repinning, along with the full release gates and clean tarball/registry installation smoke checks. Receipt changes are engine labels only; financial digests, formulas and precision boundaries remain unchanged.
  • Independently verified PCG64 against NumPy 1.26.4: 11 seeds, 11,264 raw draws and 176 doubles. Reconciled roadmap/status and added executable consumer guides.

[2.7.0] - 2026-09-12 ​

Released ​

  • Core and CLI 2.7.0 package the merged RFC 0040 and RFC 0041 features. Protocol stays 2.8.0; Format stays 2.0. Package 2.7.0 and Protocol 2.8.0 are independent version streams.
  • Exact-pin updates: signing 0.2.11, batch 0.8.6; unpublished excel/report 0.8.11. The two unpublished modules retain 0.1.0. Receipt engine labels advance to 2.7.0; calculation digests are unchanged.
  • Release tag: v2.7.0. The tag invokes the existing trusted-publishing workflow for core, CLI, signing and batch.

Added ​

  • RFC 0040: signed block _role annotations, deterministic role-aware cross-check selection, component exclusion and per-section coverage evidence. Trusted role assignment survives edits and representation conversion; agent writes cannot assign roles.
  • RFC 0041: explicit year, absolute month/quarter and date selectors on the five standard period series, with contextual variants, overrides and dependency keys. PS-01/02/03 diagnostics and typed evaluation refusals cover malformed, duplicate and unresolvable references. Financial formulas are unchanged.
  • Excel emission and refinement perturbation explicitly decline period selector expressions. Relative Qn/Mn aliases, calendar conversion, module registration and speculative leasing remain separate future work.

2.6.2 - 2026-09-10 ​

Released ​

  • @uwmd/core 2.6.2, @uwmd/cli 2.6.2 (lockstep), @uwmd/signing 0.2.10 (repin-forced patch; core's exact optional-peer pin moves with it), @uwmd/batch 0.8.5 (repin). Unpublished repins: @uwmd/excel 0.8.10, @uwmd/report 0.8.10; @uwmd/module-hospitality and @uwmd/module-data-center repin at 0.1.0 (both unpublished). Protocol stays 2.6.0.
  • Why a patch, not a minor: nothing normative moves. The cut carries the one fix below — remediation copy that renderers show next to an issue — plus a prose correction in §4.25 and the schema examples. No verdict, code, severity or corpus case changes. Receipt baselines issue/01, issue/02 and verify/03 engine_version to 2.6.2.

Fixed ​

  • BUILTIN_REMEDIATIONS CC-01..CC-10 now describe the rules the validator runs. The ten cross-section entries dated from the initial commit and described an earlier quick_metrics-reconciliation draft (CC-01 "NOI mismatch: noi_model vs quick_metrics.noi_underwritten", severity error) that never shipped; §5.3 and validator.ts have always defined CC-01 as rent-roll GPR vs operating-statement GPR within 3%, a warning. Because the validator attaches the registry's title / remediation / spec_ref to every issue it emits, a real CC-05 issue (DSCR NOI vs the NOI model) was surfaced to renderers titled "Cap rate mismatch". Every entry's sections, tolerance and default severity now mirror the table and the rule, pinned by a test that reads the §5.3 table from the spec. The §4.25 prose that justified CC-15's single seam by claiming CC-01 reconciles noi_model to quick_metrics is corrected (no rule does), as are the wiki and the issue-remediation schema examples. Validation verdicts, issue codes and severities are unchanged — the conformance corpus does not move.

2.6.1 - 2026-09-10 ​

Released ​

  • @uwmd/core 2.6.1, @uwmd/cli 2.6.1 (lockstep), @uwmd/signing 0.2.9 (repin-forced patch; core's exact optional-peer pin moves with it), @uwmd/batch 0.8.4 (repin). Unpublished repins: @uwmd/excel 0.8.9, @uwmd/report 0.8.9; @uwmd/module-hospitality and @uwmd/module-data-center repin at 0.1.0 (both unpublished). Protocol stays 2.6.0.
  • Why a patch, not a minor: nothing in the format or protocol spec moves. The cut carries two bug fixes — the core module runtime scoping a declaring module to its declared class, and uwmd validate --json exiting 1 on errors as §II.6a.2 always said — and one corpus case correction (tier-1/09-returns-tax-basis/validate now expects exit 1). The data-center module is new code in a new, unpublished package, not a spec change. Receipt baselines issue/01, issue/02 and verify/03 engine_version to 2.6.1.

Fixed ​

  • uwmd validate --json exits 1 on a document with errors, as §II.6a.2 has always said. The JSON branch returned before the exit-code line, so JSON mode exited 0 on an errors verdict while the human-readable mode exited 1. The tier-1 case generator had hardcoded exit_code: 0 for validate on the assumption that every fixture validates clean; fixture 09 (RFC 0038, an RT-01 error by design) was the first that does not, and it pinned the bug as the baseline. The generator now derives the exit code from the baseline (errors / blocked → 1) and tier-1/09-returns-tax-basis/validate expects 1. Implementations that followed the prose were failing that case; implementations that followed the case were contradicting the prose (raised app-side as underwriter.cc TASK-1121).

Added ​

  • Data-center module (RFC 0039). @uwmd/module-data-center 0.1.0 (unpublished, like hospitality) — the first product module on a module-declared asset class. Declares org.uwmd.data_center (fallback industrial), three kW-denominated sections (dc_capacity required, dc_power, dc_revenue), eleven calculations in threaded order, seven validations (CC-MOD-DC-01..07, null-silent). The RFC's open question closed yes: a module formula reads the standard sections (quick_metrics.purchase_price, noi_model.net_operating_income) with no runtime change, so the per-commissioned-kW price and NOI calcs ship. examples/Mesa-Gateway-Data-Center-Mesa-AZ.uwx.md is the corpus's first custom-class example; conformance/modules/runtime/06–11 add six scenarios and the runner dispatches on expected.module. Protocol and format versions do not move.
  • Fix (@uwmd/core module runtime): a module that declares a custom class (declares_asset_classes) and lists no builtin asset_classes was treated as applying to every document, so it would have raised MOD-SECTION-MISSING on every non-matching file. applicableModules now scopes a declaring module to the class it declares; a module naming neither field still applies to all. Found by RFC 0039, note 2.

2.6.0 - 2026-09-09 ​

Released ​

  • @uwmd/core 2.6.0, @uwmd/cli 2.6.0 (lockstep), @uwmd/signing 0.2.8 (repin-forced patch; core's exact optional-peer pin moves with it), @uwmd/batch 0.8.3 (repin). Unpublished repins: @uwmd/excel 0.8.8, @uwmd/report 0.8.8; @uwmd/module-hospitality repins at 0.1.0. Protocol 2.6.0 — the IRR-hurdle minor: RFC 0036 below.
  • Why a minor, not a patch: §VIII.10 step 3 gains a new normative capacity rule (the closed-form LP hurdle balance behind until_lp_irr), and the validator tightens — WF-01 now refuses a non-increasing hurdle ladder of either kind, so a document 2.5.0 accepted can be refused by 2.6.0. Receipt baselines issue/01, issue/02 and verify/03 engine_version to 2.6.0.

Added ​

  • IRR-hurdled waterfall tiers (RFC 0036). Protocol 2.5.0 → 2.6.0 (a new normative capacity rule in §VIII.10 step 3); the format version does not move. A split tier may now state until_lp_irr (a fraction in (0, 1)) alongside — or instead of — until_lp_em: the tier pays until the LP's dated flows, including the payment being made, reach the hurdle rate under the series' own day count. The boundary is closed-form, not the nested bisection RFC 0035 §C sketched: the LP's hurdle balanceB = −xnpv(F, h) × (1 + h)^t_row over every LP flow so far (earlier rows and earlier tiers of this row), capacity max(0, B) / lp_share. The identity — not a solved "LP IRR ≥ h" — is the normative definition, so the interleaved-capital-call case is deterministic across engines; an implementation MUST NOT determine a tier boundary by iterating on xirr. Both hurdles on one tier means both must be met (the larger capacity governs); laddered promotes (12% → 15% → 18%) follow. WF-01 drops the RFC 0035 reservation refusal and gains: until_lp_irr in (0, 1); lp_share > 0 and the terminal-uncapped rule for a split capped by either field; successive stated hurdles of one kind strictly increasing (compared at the rate / ratio quantum). One tightening: a non-increasing until_lp_em ladder — always a dead tier — is now refused; the corpus scan the RFC required found zero such documents, so it ships as an error rather than the fallback WF-04 warning. Schema section-distribution-waterfall gains the field; WaterfallTierSplit. until_lp_irr is the only type change and there are no new exports. conformance/waterfall/ gains twelve scenarios (boundary, crossing, ladder, already-met, interleaved call, combined hurdles ×2, compound pref then IRR, and four WF-01 rejections) and loses reject-reserved-irr-hurdle — corpus 385 → 396. Every pinned number is engine-generated; the boundary case is hand-checked (LP 120,000 / GP 30,000, LP xirr 0.12 at 6 dp).

2.5.0 - 2026-09-09 ​

Released ​

  • @uwmd/core 2.5.0, @uwmd/cli 2.5.0 (lockstep), @uwmd/signing 0.2.7 (repin-forced patch; core's exact optional-peer pin moves with it), @uwmd/batch 0.8.2 (repin). Unpublished repins: @uwmd/excel 0.8.7, @uwmd/report 0.8.7; @uwmd/module-hospitality repins at 0.1.0. Protocol 2.5.0 — the validator-honesty minor: RFC 0037 and RFC 0038 below.
  • Why a minor, not a patch: ValidationResult gains a required coverage field (consumers constructing one by hand must add it), a new registered code (CC-16) and a new validator family (RT-NN) are emitted, and format §4.9 gains a field. Receipt baseline verify/03 engine_version to 2.5.0. Also carries #162 (docs wording), #163 (RFC 0036 draft) and #165 (continuity plan).

Added ​

  • Continuity plan (MAINTAINERS.md "Continuity"): MIT + public repo is the plan; no successor is named; six months of maintainer unresponsiveness is the dormancy threshold after which the community should fork. Closes the launch-review bus-factor flag.

  • Cross-check resolution over variant maps, and a validation coverage channel (RFC 0037). Ten of the fifteen §5.3 cross-checks resolved their sections with a bare getSection, which returns null on a variant map — so a document carrying, say, a senior facility and a mezz sleeve as two debt_structure variants (conforming under the RFC 0014 envelope, which admits a variant map on any section) silently lost CC-02, CC-03, CC-05 and CC-09 with no diagnostic. Format §5.3 now specifies the resolution order (the check's own preference → default → base → a lone variant), an unresolvable map is reported once per section as CC-16 (info), and ValidationResult gains a coverage record — one entry per registered check, evaluated or skipped with a reason — so "checked and clean" is distinguishable from "never evaluated". uwmd validate prints the summary line; --json carries the record. New exports: CROSS_CHECK_RULE_IDS, CROSS_CHECK_VARIANT_PREFERENCE, CrossCheckCoverage, CrossCheckSkipReason. Tier-1 fixture 08-variant-cross-checks. Raised by underwriter.cc as UPSTREAM-005.

  • dcf.returns.tax_basis (RFC 0038). A closed, optional declaration — pre_tax (default when absent) or after_tax — of the basis every metric in dcf.returns is stated on, so two documents' IRRs can be told apart from a spread. Unregistered values are RT-01 (error) in the new RT-NN family. New exports: RETURN_TAX_BASES, DEFAULT_RETURN_TAX_BASIS, ReturnTaxBasis, getReturnTaxBasis(). Tier-1 fixture 09-returns-tax-basis. Raised by underwriter.cc as UPSTREAM-006.

2.4.0 - 2026-09-04 ​

Released ​

  • @uwmd/core 2.4.0, @uwmd/cli 2.4.0 (lockstep), @uwmd/signing 0.2.6 (repin-forced patch; core's exact optional-peer pin moves with it), @uwmd/batch 0.8.1 (repin). Unpublished repins: @uwmd/excel 0.8.6, @uwmd/report 0.8.6; @uwmd/module-hospitality repins at 0.1.0. Protocol 2.4.0 (three spec-conformance fixes below; no new protocol machinery).
  • Why a minor, not a patch: uwmd parse --json's per-section shape changed to the §II.6a.6 projection — breaking for consumers of the old UWBlock dump. Receipt baseline verify/03 engine_version to 2.4.0.

Fixed ​

  • Protocol §III.6a: the META_* (underscore) provenance family is live, not retired. The table declared it retired on the claim that it was "specified and never emitted" and covered by DQ-NN — both halves false: the reference validator emits META_MISSING, META_FIELD_MISSING_<FIELD> and META_LOW_CONFIDENCE_NO_REVIEW_FLAG, the tier-1 baselines pin them, and the DQ sequence polices incomplete data, not incomplete provenance. Found by underwriter.cc implementing the codes the corpus demands while the spec said they did not exist. Prose-only; no validator or baseline change.

  • uwmd parse now emits the §II.6a.6 conformance projection for sections and superseded — { meta, content } per block (per variant for a multi-variant section), with meta the on-disk _meta verbatim and content the block content proper. Previously it dumped in-memory UWBlocks, so the fence object (with _meta nested inside content), reader artifacts (line numbers, prose, rawJson) and the flattened in-memory meta all leaked into the output — and into the tier-1 baselines, which therefore required shapes §II.6a.6 says MUST NOT be required. A faithful implementation of the spec's own projection failed 6 of 6 tier-1 parse cases (found by underwriter.cc's first driver run; their UPSTREAM-notes item on baseline/spec divergence). All six tier-1-reader/expected/*.parsed.json baselines are regenerated in the projection shape. Breaking for consumers of uwmd parse --json's per-section shape (the fence object is still available in the document itself; superseded_blocks keeps its prior shape for compatibility).

  • tier is no longer required by the implementation-manifest schema, matching §II.5's normative text, which says an implementation whose capabilities do not stack into a clean tier SHOULD "publish capabilities and omit a tier claim rather than round down". The schema's required array contradicted the spec (UPSTREAM-003, reported by underwriter.cc); the spec wins. ImplementationManifest.tier is now optional in @uwmd/core accordingly.

2.3.0 - 2026-09-03 ​

Released ​

  • @uwmd/core 2.3.0, @uwmd/cli 2.3.0 (lockstep), @uwmd/signing 0.2.5 (repin-only patch; core's exact optional-peer pin moves with it), and — first publish — @uwmd/batch 0.8.0 (owner decision 2026-09-03: the corpus fact table's first consumer is the underwriter.cc screener, which should npx @uwmd/batch rather than clone the repo). release.yml now carries a fourth publish step; batch gained the repository manifest field npm provenance requires. Coordinated repins for the still-unpublished surfaces: @uwmd/excel0.8.5, @uwmd/report 0.8.5; @uwmd/module-hospitality repins to core 2.3.0 and stays 0.1.0, unpublished.
  • Carries the #146 data-lake pair: the /guide/data-lake on-ramp (#148) and the corpus fact table + .uwx.md discovery fix (#149). No spec, schema, format, or protocol movement — protocol stays 2.3.0 (the core 2.3.0 / protocol 2.3.0 number collision is coincidence; the surfaces version independently).

Added — corpus fact table for data-lake ingestion (#146 leg B) ​

  • @uwmd/core: flattenEnvelopeBlockValues(envelope) — the normative block_values flattening (UW CSV Bundle spec §3) exposed as data (UWBlockValueRow[]), so hosts can build fact tables without re-implementing it. encodeUWCSVBundle now consumes the same function — one source of truth. Exported from both entries (UWJSONValueType / UWBlockValueRow types alongside). No behavior change to the bundle encoding.
  • @uwmd/batch: buildUWMDFactTable / writeUWMDFactTable and the --facts CLI flag — walks a deal directory and emits uwmd-facts.jsonl (one line per JSON fact per deal: the block_values row prefixed with path, deal_id, asset_class, semantic_digest, and the validation verdict) plus uwmd-facts-manifest.json with counts and a deals_skipped list. Deals that parse but fail validation are included with valid: false; a fact table never silently drops a deal. Durable fact key: (semantic_digest, block_ref, scope, pointer). First consumer: the underwriter.cc batch screener.
  • @uwmd/batch fix: .uwx.md discovery — the directory walker only matched .uw.md, silently scanning zero files in a post-2.0 corpus (where init scaffolds .uwx.md). It now matches both extensions.
  • The data-lake guide (docs/DATA_LAKE.md, /guide/data-lake) gained a "Scaling up" section showing the --facts pipeline; every command and query in it was executed before being written down.

2.2.0 - 2026-09-02 ​

Released ​

  • @uwmd/core 2.2.0, @uwmd/cli 2.2.0 (lockstep), and @uwmd/signing 0.2.4 (repin-only: its core dependency is exact, so the core bump forces a patch; core's exact optional-peer pin moves with it). Coordinated repins for the versioned-but-unpublished surfaces: @uwmd/excel 0.8.4, @uwmd/report 0.8.4, @uwmd/batch 0.7.4; @uwmd/module-hospitality repins to core 2.2.0 and stays 0.1.0, unpublished; tools/web-editor is a file: link and needs no repin.
  • Carries RFC 0035 (distribution waterfall, protocol 2.3.0 §VIII.10) — drafted, accepted, and implemented 2026-09-02, the same day as the 2.1.0 release it follows. Format stays 2.0 (additive section registration does not bump it).

Added — distribution waterfall (RFC 0035; protocol 2.2.0 → 2.3.0) ​

  • Format §4.27 distribution_waterfall — the tiered LP/GP split of a deal's equity cash flows as the fourth state-and-verify structure: cash_flow_ref into a §4.26 dated series, equity_split, and an ordered closed tier ladder — return_of_capital, preferred_return (simple / compound-annual accrual under the series' §VIII.9.1 day count; unpaid pref compounds in the latter), catch_up (closed-form capacity to a target promote share), and split tiers with optional equity-multiple hurdles and a mandatory uncapped terminal split. stated_outcomes (per-party contributions / distributions / MOIC / XIRR via §VIII.9.3, promote_total, profit_total) and an optional stated_schedule checked cell-for-cell (absent cells read 0). New validator family WF-01…WF-03 (ladder grammar incl. the reserved-and-refused until_lp_irr; the cash reference must resolve; a waterfall needs capital).
  • Protocol §VIII.10 — the normative allocation walk (accrue first, pairwise year fractions, contributions by equity_split, ladder fill with pinned per-tier capacities), so two engines produce identical allocations and agree on the promote. verifyWaterfall / computeWaterfall recompute the entire allocation, never trusting stated splits; three-state at the §VIII.9.4 quanta. capital_stack and its CS-WATERFALL-UNSUPPORTED boundary are untouched — the waterfall is the equity side, in its own section.
  • Conformance: new conformance/waterfall/ suite, 14 scenarios — the fully hand-worked classic case (ROC → 8% pref → 100% catch-up to 20% → 80/20, every figure exact on paper; the catch-up lands the GP at exactly 20% of profit), the compound-pref twin, the EM-hurdle boundary crossing, and the six WF refusals (corpus 363 → 377; receipt baselines re-pin protocol_version 2.3.0). 30 new unit tests including a cash-conservation property.
  • Erratum recorded in the RFC, found by building it: the draft defined catch-up "profit" as distributions above returned capital and pref — under which a catch-up following the pref tier has capacity zero forever. Profit means distributions above returned capital; pref receipts count as profit (the industry reading); promote_total still excludes the GP's own pref.
  • Deferred by design: IRR-hurdled tiers (until_lp_irr reserved; the bisection-on-boundary-amount design is named in the RFC), clawback/crystallization, n-party splits, Excel emit (the §4.26 literals posture), and a capital_stack cross-check.

2.1.0 - 2026-09-02 ​

Released ​

  • @uwmd/core 2.1.0, @uwmd/cli 2.1.0 (lockstep), and @uwmd/signing 0.2.3 (repin-only: its core dependency is exact, so the core bump forces a patch; core's exact optional-peer pin moves with it). Coordinated repins for the versioned-but-unpublished surfaces: @uwmd/excel 0.8.3, @uwmd/report 0.8.3, @uwmd/batch 0.7.3; @uwmd/module-hospitality repins to core 2.1.0 and stays 0.1.0, unpublished; tools/web-editor is a file: link and needs no repin.
  • The post-2.0 same-day pair: RFC 0034 (calendar-anchored cash flows, protocol 2.1.0 §VIII.9) and RFC 0015 (portfolio & relationship profiles, protocol 2.2.0 §XV) — both drafted/revised, accepted, and implemented 2026-09-02. Format stays 2.0 (additive section registration does not bump it; see the RFC 0034 erratum).

Added — portfolio & relationship profiles (RFC 0015; protocol 2.1.0 → 2.2.0) ​

  • Protocol §XV (Future work renumbered §XVI): the optional portfolio-relationships capability and the .uwportfolio.json sidecar — typed entities and provenance-backed edges spanning deals, the portable carrier for the entity layer of the RFC 0018 edge registry. Out-of-band by design: no _meta ownership, no canonical-byte changes, no storage/query/aggregate contract (stated fund-level numbers stay with RFC 0021 composites + rollup receipts).
  • Registry-resolved edges: types resolve through BUILTIN_EDGE_TYPES (lookupEdgeType) — a known member-layer type used as an entity edge refuses (PORT-010, the one-table-two-layers rule enforced from the sidecar side); builtin from/to entity-kind constraints refuse (PORT-011); unknown entity/edge types and fields are preserved, reportable via uninterpretedPortfolioTypes, never refused. Provenance source is a document/source identifier, explicitly kept apart from the RFC 0031 _meta.source actor grammar.
  • Core (portfolio.ts, browser-safe, read-only): PortfolioProfile / PortfolioEntity / PortfolioEdge (exactly UWEntityEdge + id), validatePortfolioProfile (PORT-001…PORT-011, new portfolio error category), getPortfolioRelationships, and entityEdgesToPortfolioEdges — the bridge that gives projectPackageLinksToEntityEdges (which produced entity edges with no portable destination) its home. New uw-portfolio-profile.schema.json; portfolio_version starts its own line at 1.0.
  • CLI: uwmd portfolio validate|edges (cli-portfolio.ts) — also what lets the conformance suite exist as commands.
  • Conformance: new conformance/portfolio-relationships/ suite, 7 scenarios — validity, missing provenance, duplicate id (one namespace), dangling endpoint, unknown-preservation (types reported + extension fields retained), wrong-layer refusal, and the package→profile projection round-trip (corpus 356 → 363; receipt baselines re-pin protocol_version 2.2.0).
  • Erratum recorded in the RFC: its conformance sketch asked scenario 05 to prove byte-for-byte survival through a targeted edit, but the reference surface is read-only by the RFC's own design — 05 pins preservation through validation and type reporting instead; byte-level editor conformance waits for an editor to exist.

Added — calendar-anchored cash flows (RFC 0034; protocol 2.0.0 → 2.1.0) ​

  • Format §4.26 cash_flow_series — dated, irregular flows (ISO-8601 calendar dates, signed amounts) as the third state-and-verify structure after capital_stack and lease_up_schedule: multi-variant, asset-class independent, optional at every stage. Stated aggregates (total_net, moic, xnpv at a rate, xirr) are recomputed three-state by verifyCashFlowSeries, never trusted. New validator family CF-01… CF-03 (row grammar incl. real-calendar-day checking; non-decreasing dates with the first row as anchor, ties legal and unmerged; a stated xirr requires a sign change). New section-cash-flow-series.schema.json; chat/summary renderers gain the dated-flow table. The §4.26 worked example's metrics were computed by the verifier and pinned verbatim in conformance/cash-flow/verify-all-metrics, so spec and implementation cannot drift apart.
  • Protocol §VIII.9 — the closed day-count registry (DAY_COUNT_CONVENTIONS: actual/365f default, actual/360, 30/360us pinned to the exact Excel DAYS360 U.S. clamps with no NASD February special-casing — the divergence documented, not discovered); xnpv closed-form over anchor-relative year fractions, accumulated in series order; xirr by the RFC 0024 bisection procedure verbatim (same bracket, tolerances, exact high-endpoint root, no Newton polish; CALC-XIRR-DIVERGE on failure). §VIII.3's iterate sentence now reads: irr and xirr are the only builtins permitted to iterate.
  • Reachable only by declaration (§VIII.9.4): CashFlowMetricDecl + evaluateCashFlowMetric(s) — the §VIII.7/§VIII.8 pattern's third instance. The §VIII.1 grammar and the §VIII.3 expression-callable table are untouched; CalcResult.value is not widened; overrides shadow individual rows (cash_flow_series.series[7].amount) without touching the document. Unit defaults quantize per RFC 0023 (%→6, $→2, x→4). New CALC-CF-SERIES refuses a declaration naming a missing, malformed, or wrong-variant series; an explicit variant does not fall back to default. New capability calc-cash-flow; new CF validator code family.
  • Conformance: new conformance/cash-flow/ suite, 15 scenarios across the three kinds (payload verify, document validate+verify, declaration evaluation incl. the three-convention pinned-root trio and the override shadow), run by default and wired into CI (corpus 341 → 356; the two receipt-issuance baselines re-pin protocol_version 2.1.0).
  • Deferred by design (recorded in the RFC): Excel emit — Excel's XIRR is Newton-seeded, so a live formula cannot hold the bit-exact parity boundary; any future Cash Flow sheet emits literals. Also deferred: a dcf cross-check, defaults entries, and the RFC 0026 Phase 2 waterfall (its stated precondition — a hold-period dated-series primitive — is now removed).
  • Errata against the RFC, found by building it: the draft claimed "format 2.0 → 2.1", but additive section registration has never bumped the format version (§4.23/§4.24/§4.25 all landed at format 1.1); §4.26 follows precedent and the format version does not move. The draft's worked-example metrics were hand-computed and wrong; the spec's are now generated by the verifier.

2.0.0 - 2026-09-02 ​

Released ​

  • @uwmd/core 2.0.0, @uwmd/cli 2.0.0 (lockstep), and @uwmd/signing 0.2.2 (repin-only: its core dependency is exact, so the core bump forces a patch; core's exact optional-peer pin moves with it). Coordinated repins for the versioned-but-unpublished surfaces: @uwmd/excel 0.8.2, @uwmd/report 0.8.2, @uwmd/batch 0.7.2; @uwmd/module-hospitality repins to core 2.0.0 and stays 0.1.0, unpublished; tools/web-editor is a file: link and needs no repin.
  • The major. Everything in the 2.0 cut below is why this is 2.0.0 and not 1.11.0: retired PROTO-EDIT-010, the removed legacy_extension detection field, manual leaving SOURCE_TAGS, and the two per-file sunsets are breaking for consumers of those surfaces. A 2.0 implementation remains a full 1.x reader — the whole 1.x line stays supported input — and 1.x files keep their 1.x validation semantics per format v2 §1.3's per-file boundary.

Changed — the 2.0 cut (BREAKING; RFC 0009 / RFC 0025 / RFC 0031) ​

  • Protocol 1.14.0 → 2.0.0; the implementation now authors format 2.0 (FORMAT_VERSION '2.0') while reading the whole 1.x line (SUPPORTED_FORMAT_VERSIONS). Module requires_protocol / requires_format ranges are satisfied against the supported SETS (SUPPORTED_PROTOCOL_VERSIONS), so ^1 modules keep loading — a 2.0 implementation is a 1.x implementation too.
  • Full v2 editing — PROTO-EDIT-010 is retired. Every writer (editor replace/supersede/pipeline-append, writeAgentBlock, init) goes through one seam (stampMetaIntoBlockContent) that emits the shape the file's uw_version demands: nested _meta + the _overrides lift for 2.0 files, flat for 1.x. Integrity stamping hashes under the file's own canonicalization rule. uwmd init scaffolds uw_version: "2.0" by default (--format 1.1 for the legacy flat scaffold).
  • The per-file 2.0 vocabulary boundary (format v2 §1.3/§4): SRC-01 / SRC-02 escalate to errors in uw_version: "2.0" files (1.x files keep their warnings); new SRC-03 rejects the retired resolution: "manual"; manual left SOURCE_TAGS (actor-only — the SourceTag union keeps it for the actor field); the §2.6 read-time interpretation no longer applies inside a 2.0 file's own blocks.
  • Legacy structured .uw.md sniffing sunset (RFC 0025, format v2 §6.1): structured UWX content under a .uw.md name is now SOURCE_LEGACY_STRUCTURED (error); an explicit UWX override remains the escape hatch; migrateLegacyUWMarkdown still plans the byte-identical rename (probing fences directly). UWSourceDetection.legacy_extension is removed (breaking). The Lite canonicalization 1.0 recognition obligation ends; the generic RCP-10 version-mismatch degradation is retained as quality-of-implementation.
  • Conformance corpus renamed to spec-compliant extensions: every structured fixture moved .uw.md → .uwx.md (86 files), with runner, case-generator, and docs updated; Lite fixtures keep .uw.md legitimately. Receipts baselines re-pin protocol_version 2.0.0; Lite compile-report baselines re-record the bridge's new pinned-1.1 rationale (the Lite bridge stays a 1.x-format producer until its own RFC).

Added ​

  • RFC 0009 — the format 2.0 normative text (the 2.0 cut's spec leg): spec/UW_FORMAT_SPEC_v2.md, a delta specification that incorporates the v1 document by reference and amends it for uw_version: "2.0" files — the nested _meta shape (§2, with the revision rename and integrity.algorithm), the _overrides block annotation (§3), the 2.0 vocabulary (§4: manual actor-only, SRC-01/SRC-02 escalate to errors per-file, new SRC-03 for resolution: "manual"), canonicalization v2 and the migration/signature policy (§5), and the two boundary sunsets (§6: legacy structured .uw.md sniffing becomes an error; the Lite canonicalization 1.0 recognition obligation ends while the generic degradation may be retained). §1.3 records the per-file reading of the 2.0 boundary, reconciling RFC 0031's per-format phrasing with RFC 0009's per-release phrasing in favor of per-file — the only reading consistent with the v1 round-trip guarantee. Plus spec/schemas/uwmd-block-v2.schema.json (nested UWMetaV2 + _overrides, manual-free resolution enum) and the schemas-README row.

1.10.0 - 2026-09-01 ​

Released ​

  • @uwmd/core 1.10.0, @uwmd/cli 1.10.0 (lockstep), and @uwmd/signing 0.2.1 (repin-only: its core dependency is exact, so the core bump forces a patch; core's exact optional-peer pin moves with it). Coordinated repins for the versioned-but-unpublished surfaces: @uwmd/excel 0.8.1, @uwmd/report 0.8.1, @uwmd/batch 0.7.1; @uwmd/module-hospitality repins to core 1.10.0 and stays 0.1.0, unpublished; tools/web-editor is a file: link and needs no repin.
  • This is the RFC 0009 timeline's on-ramp minor: uwmd migrate --to-v2 and dual-shape reading go live so operators can convert files to the v2 nested _meta shape ahead of the 2.0 release. Protocol 1.14.0; format stays 1.1.

Added ​

  • RFC 0009 leg 2 — the STAGE_CONTRACT merge. STAGE_REQUIREMENTS, STAGE_SECTION_OVERLAYS (RFC 0029), and BUILTIN_INCOMPLETE_DATA_POLICIES merge into one registry, STAGE_CONTRACT — rows keyed (stage, section, field_path?, asset_class?) carrying required, on_provisional, and the scope stage's one_of groups; the class overlays become asset_class-qualified rows rather than a side table. The registry is derived mechanically from the compact authoring tables at module init (the tables moved from validator.ts to protocol.ts; validator re-exports them), and validators resolve presence through it. No behavior change — pinned by equivalence tests across the full stage × asset-class × policy cross-product (stage-contract.test.ts), and no protocol version move (no new codes, no observable contract change). New public API: STAGE_CONTRACT, StageContractEntry, lookupStageContract, plus the previously internal stage tables. Refinements against the RFC sketch, recorded in the RFC on flip: required/on_provisional are optional per row, and one_of exists because the sketch's single field_path could not express the scope stage's either-or groups.

  • RFC 0009 (accepted 2026-09-01 at the sprint's Phase 4 gate) — the 1.10.0 on-ramp to format 2.0. Protocol 1.13.0 → 1.14.0; format stays 1.1 (the nested shape is reserved, not admitted, at 1.x):

    • The shim (meta-shape.ts): UWMetaV2 and the structural reshape in both directions. The parser accepts a nested _meta in a uw_version: "2.0" file and exposes the flat in-memory view every consumer already reads (block.meta_shape records the on-disk shape); content._meta stays bytes-derived and untouched. The RFC 0031 legacy-tag rule survives the shape change: resolution set, provenance.source absent, never invented.
    • Versioned canonicalization: the v1 digest rule is frozen forever for uw_version: "1.x" files; canonicalizeV2 is normalize-then-hash, so both accepted shapes digest identically. A defaulted integrity.algorithm ('sha256') is excluded from the digest; a future non-default value is hashed and cannot be stripped undetected. computeBlockHash / verifyChain pick the rule from the file's own frontmatter.
    • META-* validator family (protocol §III.6a): META-V2-IN-V1 and META-V1-IN-V2 enforce one shape per file, in both directions.
    • uwmd migrate --to-v2 (engine migrateToV2, browser-safe): whole-file conversion — frontmatter to "2.0", every block nested, field_overrides lifted to the top-level _overrides annotation, resolution: "manual" rewritten to user_input with a provenance note, hashes re-stamped chain-aware (a parent link that was broken before migration is carried over broken — migration never repairs tamper evidence). Signed blocks refuse by default; --resign (via the optional @uwmd/signing, structurally typed to preserve layering) or --strip-signatures (recorded in provenance.notes) is the key holder's explicit choice. --emit-v2-shape is accepted as a synonym.
    • Editor refusal PROTO-EDIT-010: core 1.x refuses edits against uw_version: "2.0" files rather than corrupting them with flat blocks; full v2 editing ships with the 2.0 cut.
    • Conformance corpus 331 → 341: the seven-scenario meta-v2 suite (tier-1-reader/v2-fixtures/, including the recorded shim-roundtrip baseline and both digest-insensitivity pins) and the three-scenario migrate suite. Receipts baselines re-pin protocol_version 1.14.0.

1.9.0 - 2026-09-01 ​

Released ​

  • @uwmd/core 1.9.0, @uwmd/cli 1.9.0 (lockstep), and @uwmd/signing0.2.0 — signing's first tag-triggered OIDC publish, after its 0.1.0 was manually published earlier the same day from the v1.8.0 tag (its historically paired state) to bootstrap the trusted publisher. Coordinated @uwmd/core repins: @uwmd/excel 0.8.0, @uwmd/report 0.8.0, @uwmd/batch 0.7.0, tools/web-editor 0.8.0; @uwmd/module-hospitality repins to core 1.9.0 and stays 0.1.0, unpublished.
  • The single-day sprint batch: the owner-ordered Phase 2 trio — RFC 0008 (lease-up schedule, §4.25), RFC 0011 (capability tokens, §XIV), and RFC 0001 (display locales, §III.1a) — each accepted and implemented the same day, plus the RFC 0032/0033 adopter-scope clarifications.
  • Format stays 1.1; protocol goes 1.10.0 → 1.13.0 across the span: 1.11.0 added §4.25 + the LU-NN family + CC-15 (RFC 0008), 1.12.0 added §XIV + POL-03 + capability-verify (RFC 0011, future work renumbered §XV), and 1.13.0 added §III.1a + the LOC family (RFC 0001). Every format change is additive at 1.x.
  • Conformance corpus 306 → 331 (the lease-up, capability, and locale named suites). Receipts baselines regenerated — the diff is exactly the engine_version fields — and the 03-result-disagrees frozen receipt re-pinned to the new engine so its disagreement still reads as same-engine failed rather than degrading to RCP-07 unverifiable.

Added — RFC 0001 implemented: display-locale negotiation (corpus 323 → 331) ​

RFC 0001 (accepted and implemented 2026-09-01). Protocol goes to 1.13.0, adding §III.1a — Display locales. A file declares the locale it was authored in (locale frontmatter, default en-US); an implementation declares what it renders (ImplementationManifest.supported_locales, absent = ['en-US']); a display render of an unsupported locale is refused (LOC-01, new LOC family; UnsupportedLocaleError from the renderer) — never silently produced in a different locale.

  • Display-only by construction: locale touches chat/summary/report renders only. Canonical JSON content, CSV renders, UW Lite canonical form, digests/receipts, and calc evaluation are locale-free — CalcEvaluationContext.locale is pinned locale-invariant by conformance.
  • The registry, not ICU: non-en-US formatting comes from the new curated BUILTIN_FORMAT_RULES table (format-rules.ts), stated verbatim in §III.1a — never runtime Intl, whose output varies across runtimes. en-US keeps its historical code path byte-identical; every existing baseline is untouched.
  • First wave: en-US, en-GB, de-DE, fr-FR, ja-JP, zh-CN (NBSP conventions included, self-defended by a charCode test). formatCurrency / formatPercent / formatRatio / formatDate gain a locale option; the renderers thread frontmatter.locale through.
  • New conformance/locale/ suite (8 scenarios: five per-locale rendering pins, the LOC-01/display refusal, calc invariance across all six locales, CSV byte identity). Also in this change: the implementation-manifest schema's capability enum caught up with the type (integrity, refinement, capability-verify were missing) and the reference manifest now claims capability-verify (an RFC 0011 omission).
  • Deferred, per the RFC: currency-code disambiguation (own future RFC), cross-locale conversion, localized message text.

Added — RFC 0011 implemented: capability tokens for write authorization (corpus 315 → 323) ​

RFC 0011 (accepted and implemented 2026-09-01). Protocol goes to 1.12.0, adding §XIV — Capability tokens (optional) (future work renumbers to §XV). An opt-in second gate on writes for orchestrator-bound deployments: a short-lived, scope-limited JWT a coordinator signs — "this actor may write these sections at these stages for this deal, until this time" — verified by the editor before the write.

  • Tokens narrow, never widen. The static §V.3 policy check runs regardless; a token cannot override a POL-01 refusal, and institution/* keeps system_only — the question RFC 0031's catch-all surfaced, now resolved and pinned by a conformance case.
  • sub binds _meta.source under the RFC 0031 actor grammar (instance identity fits the id charset: agent/L2.instance-abc-123); free-text _meta.actor takes no part in authorization.
  • Core stays crypto-free: CapabilityVerifier is injected (EditOptions.capabilityVerifier, the RFC 0016 precedent), honored by applyEditAsync only — the sync path with a verifier configured refuses (PROTO-EDIT-008) rather than silently skipping. Missing or rejected tokens fail POL-03 with a typed reason; an accepted token's jti lands in the new block's notes as capability:<jti>.
  • @uwmd/signing ships the reference verifier (createCapabilityVerifier over the existing KeyStore, JOSE alg names mapped onto the §V.11 shortlist) plus signCapabilityToken for coordinators and fixtures.
  • New capability-verify viewer capability (RFC 0030 mechanism); uwmd edit gains --capability-token/--coord-key (dynamic-import optional peer, like --signing); new generated conformance/capability/ suite (8 scenarios, npm run gen-capability-fixtures).

Added — RFC 0008 implemented: the lease-up schedule section (corpus 306 → 315) ​

RFC 0008 (accepted and implemented 2026-09-01). Protocol goes to 1.11.0. The format jumped straight from rent_roll (a snapshot) to noi_model (a stabilized projection) with no structured representation of the path between them — the entire thesis of a value-add or ground-up deal lived in _notes. Format spec §4.25 now registers lease_up_schedule: a model_type (natural_turnover / absorption_curve), a uniform YYYY-Qn/YYYY-MM period grammar, the driving assumption set (fractions, never percents), the period array, and a stabilized_summary.

  • State-and-verify, the capital_stack precedent. The schedule is data, not formulas — the Tier-3 calc engine is untouched (no iteration, no time axis, no new builtins), and every stated aggregate is recomputed by the new three-state verifyLeaseUpSchedule (per-period net_cash_flow from its components; the stabilized summary against the final period, with the occupancy denominator resolved through the Protocol §XIII size-intensive registry — no denominator is unverifiable, never a guess).
  • New LU-NN validator family (registered under validate per §III.6a): LU-01 grammar/granularity, LU-02 contiguity, LU-03 empty/inverted schedule, LU-04 (warning) turnover with no rent_roll. Plus CC-15 (warning): the base variant's stabilized NOI agrees with noi_model.net_operating_income within the named exported LEASE_UP_STABILIZED_TOLERANCE (2%) — tolerance-checked because the trajectory endpoint and the stabilized-year projection are two independent models, and non-base variants are exempt by design.
  • The section is multi-variant (base / upside / downside under variant=) and optional at every stage; new schema section-lease-up-schedule.schema.json; view-model entry; chat/summary renderers gain the period table; new conformance/lease-up/ suite (9 scenarios). Deliberately deferred, per the RFC: Excel emit, dcf coupling, defaults-table entries, and any shared period-schedule abstraction.

Clarified — RFC 0032: _meta.provisional and signing (no version moves) ​

Answered the adopter question tracked upstream-side as UPSTREAM-002 (underwriter.cc): § V.7's provisional obligation stays a SHOULD, on purpose. _meta.provisional is inside canonical block JSON, so stamping it moves content_hash and any § V.11 signature — and the spec now says plainly that cross-producer agreement of hashes or signatures over provenance metadata is a non-goal (the signing input names actor, timestamp, signed_at, kid, which differ across producers by construction). Cross-implementation agreement guarantees remain where they always were: computed values (§ VIII, RFCs 0023/0024) and verification receipts (RFC 0016). Two paragraphs added, § V.7 and § V.11.2; no schema, code, corpus, or version change — the same treatment as the § V.9 wording fix.

Clarified — RFC 0033: capital_stack is one point in time (no version moves) ​

Answered the adopter question tracked upstream-side as UPSTREAM-003 (underwriter.cc): a ground-up deal's construction loan and its permanent takeout are one senior position in time sequence, and § 4.24 as written forced the producer to choose between double-counting them as concurrent tranches and omitting one. The section now states the scope its own verification semantics imply — the stack is the capitalization contemporaneous with the NOI the sizing verbs read (for a deal underwritten to stabilization, the stabilized stack); temporally disjoint facilities MUST NOT be stated as concurrent tranches, and the retired construction facility stays out. bridge tranches are unaffected. Phased stacks defer with the multi-period spine RFC 0026 § D already parked. One normative bullet in § 4.24; no schema, code, corpus, or version change.

1.8.0 - 2026-08-31 ​

Released ​

  • @uwmd/core 1.8.0, @uwmd/cli 1.8.0 (lockstep), with coordinated @uwmd/core repins in @uwmd/excel 0.7.0, @uwmd/report 0.7.0, @uwmd/batch 0.6.0, and tools/web-editor 0.7.0. @uwmd/signing and @uwmd/module-hospitality repin to core 1.8.0 but stay 0.1.0, unpublished.
  • The largest release since 1.0: everything below, from the 2026-08-26 unfreeze of the v2 RFC train through 2026-08-31 — the signing chain (RFCs 0010 + 0002), the language-agnostic conformance driver (0004), the module runtime and hospitality reference module (0006), module-declared asset classes (0003), sensitivity tables (0007), stochastic calculations (0005), class-aware stage requirements (0029), conformance profiles (0030), the unpoliced-write fix, and the source-vocabulary reconciliation (0031) — plus the pre-unfreeze 2026-08-25/26 batch: the two closed conformance blind spots, RFC 0028 (reportable section readiness), the stage-honest examples pass, and the CLI plumbing extraction.
  • Format stays 1.1; protocol goes 1.6.0 → 1.10.0 across the span: 1.7.0 added §V.11 (block signatures), 1.8.0 added §X.2 (module asset classes), 1.9.0 was RFC 0030 (§II.6a.5/6, the §III.6a rewrite), and 1.10.0 is RFC 0031 (_meta source/resolution split, SRC-NN, the §V.3/§V.7 rewrite). Every format change is additive at 1.x.
  • Conformance corpus 222 → 306 assertions across the span, plus the 44-case RFC 0004 CLI driver and the three RFC 0030 profile checks.

Added — RFC 0031 implemented: the source vocabularies reconciled (corpus 301 → 306) ​

RFC 0031 (accepted and implemented 2026-08-31). Protocol goes to 1.10.0. _meta.source was asked to carry two unrelated facts — who wrote the block, and how its value was resolved — and the specs answered in six vocabularies of which only one was executable; 160 blocks in our own corpus (32%) named a source no policy pattern recognized. The unpoliced-write half shipped earlier as a bug fix; this lands the vocabulary.

  • The field splits. _meta.source is actor-only — manual or <namespace>/<id> over the closed namespace set agent | document | system | institution (ACTOR_NAMESPACES, parseActorSource, both exported; the grammar is normative in format §2.6 and mirrored in uwmd-block.schema.json). New optional _meta.resolution holds one canonical SOURCE_TAGS tag; field_overrides[] carries the same split at leaf granularity, and format §3.4 now states that the leaf wins for its path.
  • Read-time interpretation. A canonical tag found in _meta.source is surfaced as meta.resolution by the parser — on a clone, never by rewriting content._meta, which feeds digests — and the actor is treated as absent. Every pre-split file keeps a correct reading with no edit.
  • Two new validator codes. SRC-01 (unrecognized actor source) and SRC-02 (resolution tag in the actor field), both warnings through format 1.x; SRC-02 becomes an error at 2.0. The SRC family registers under validate in §III.6a.
  • Authority classification reads the parsed namespace, in both the editor and verifyProvenance — never string prefixes. The colon form agent:L0-01 used to be classified as a human write by the prefix test's negative space; a source outside the grammar now satisfies no authority class (it can still write under either). The Tier-4 host was itself an offender: it stamped the bare layer id (L6-01), which resolved only the catch-all — it now writes agent/<id>, and the fence source= mirrors the block's actual _meta.source. uwmd init's fence annotations likewise stop advertising wizard / engine:uwmd over metas that said otherwise.
  • Spec repairs. Format §2.6 is rewritten (the colon-form table is retired; the broken "see Protocol §IX" cross-reference now points at §V.7); §3.1's contradictory precedence ladder becomes non-normative narrative deferring to §V.7 — §V.7 wins where they disagreed (investor profile outranks market data: a declared decision beats a scraped comp), and §3.1's rankings of ai_extracted/agent_computed, which are not cascade steps, are removed; §4.16's private enum is replaced by SOURCE_TAGS and wizard_input — a token no other vocabulary or line of code recognized — becomes user_input; protocol §V.3 gains the grammar, the normative catch-all, and the classification rule; §V.4 states the actual grammar where producers look first; §V.7 stamps resolution rather than source.
  • uwmd migrate --source-tags (and migrateSourceTags / mapLegacySource, browser-safe): a total, mechanical rewrite of the legacy spellings. It recovers actors from the block where possible (agent_id for agent-method tags, manual for user-entered ones, system/uwmd for engine-resolved cascade tags, delimiter swaps for colon forms), refuses to guess at unknown sources, and refuses blocks carrying content_hash rather than silently invalidating an integrity chain. Applied to the corpus: exactly the 160 measured blocks across 42 files, zero unmapped. (The codemod is EOL-aware — a CRLF working copy would otherwise blind the fence regexes and silently skip files.)
  • New conformance/source/ suite (5 scenarios, corpus 301 → 306), including the data-loss regression: an edit against a catch-all-governed block supersedes, a section_replace is refused, and a v2 block with no superseded prior reports POL-02.
  • Errata recorded in the RFC: the draft schema pattern rejected the RFC's own agent/L6-01 example (lowercase-only id charset — corrected to mixed case); fixture 03's live refusal code is PROTO-EDIT-004, with POL-02 as the post-hoc provenance verdict; and the migration's actor-assignment mapping, which the RFC left open, is now specified in the RFC's status note. The protocol document's own H1 had also read "v1.3" since protocol 1.3.0 — seven minors of rot — and now carries the major alone, with §0.3 stating the precise version.
  • Unblocks RFC 0009 (_meta v2 reorg), which can now type the nested provenance field without orphaning the actor vocabulary the edit engine runs on.

Added — RFC 0030 implemented: partial conformance became checkable (corpus 274 → 301) ​

RFC 0030 (drafted, accepted, and implemented 2026-08-30). Protocol goes to 1.9.0. The first external adopter to run the corpus end to end reported four divergences; all four were defects here, with one root cause — the corpus encoded requirements the spec never stated, and the spec offered no way to opt out of the parts that do not apply.

  • Cases declare requires_capabilities and the driver skips what an implementation does not claim. A skip is never counted as a pass: it is excluded from passed, named in the TAP summary, and carried in the JSON report as skipped_by_capability. The tags are derived from the command each case runs — tier-2 reads the operation kind — so there is no table to forget.
  • An absent or empty capabilities list means run everything. Forgetting to declare fails closed against the claimant rather than exempting it from the corpus. --no-skip turns any skip into a failure, and CI runs the reference implementation that way so the mechanism cannot erode its own coverage.
  • §III.6a was wrong. It said every issue code belongs to one of three families (CC-NN, FV_*, META_*) while BUILTIN_REMEDIATIONS shipped eighteen — INT-* and POL-* were not legal families, FV_* had been renamed FV-NN in v1.1, and META_* shipped nowhere. Replaced by a registry naming the capability that owns each family, plus a test asserting every code resolves to one. The table went stale by duplicating a list that lives in code; the assertion is the part that cannot.
  • §II.1.6 is scoped to validate. A Tier-1 Reader that does not claim it owes no validator codes — the requirement previously read as owing INT-NN (needs a hash chain) and POL-NN (needs an edit engine) to any reader.
  • The parse baseline is a specified projection (§II.6a.6), not @uwmd/core's in-memory ParsedUWFile. Dropped from the normative set: annotation, lineStart/lineEnd, per-block prose, and optional _meta fields the document never carried. The old requirement lived in a corpus README and made a TypeScript interface a protocol surface by accident.
  • §II.6 no longer self-certifies by directory membership.tier-3-calc-host/refinement/ is not required of a calc host: §II.3 lists four requirements and refinement is not among them, and the RFC 0004 driver had always generated zero cases for it. New refinement and integrity capabilities make that explicit rather than inferred.
  • New: conformance/profiles/ — three stub implementations that delegate every real subcommand to the reference CLI while claiming less than they can do, and npm run conformance:profiles, which recomputes the skip set in JavaScript and compares it to the Python driver's. Two independent implementations of one rule; a single one checked against its own output would pass even when the rule is wrong.

Fixed — an unrecognized _meta.source no longer permitted a destructive replace ​

resolvePolicy returned null for a source matching no BUILTIN_EDIT_POLICIES pattern, and the editor read that null two ways: checkAuthority treated it as permitted, and dispatchEdit treated it as exempt from supersede_on_edit. A block whose source was outside the executable vocabulary could therefore be replaced in place, destroying the prior version, with POL-01 and POL-02 both unable to fire and verifyChain silent. That is a breach of the append-only provenance invariant.

Measured across all 206 corpus and example files: 613 blocks carry _meta.source, and 263 of them (43%) resolved to no policy — including 33 using canonical SOURCE_TAGS (market_data, user_input, asset_class_default, ai_extracted, system_default, market_data_accepted) and 20 using the agent: colon form that format spec §2.6 tells producers to write.

  • BUILTIN_EDIT_POLICIES now ends in a terminal * catch-all with supersede_on_edit: true and authority: 'either'. matchSource scores by pattern length, so a one-character glob applies only where nothing else matched. An unrecognized source now preserves history; no write that succeeded before starts failing, which is what keeps this a fix rather than a migration.
  • checkAuthority refuses when no policy matches instead of granting. That is reachable only for a caller supplying a policy list with no catch-all — an incomplete policy, which should not read as authorization.
  • A totality assertion pins it: no source resolves to null under the builtin policies. It makes this class of bug unrepresentable rather than merely absent.
  • generateBlankUWFile was itself emitting unpoliced sources — wizard on every section stub and engine:uwmd on the pipeline-log entry, neither matching any pattern. Every freshly created document carried blocks no policy governed, which is why replacing them in place appeared to work; the web editor's suite caught it, since root npm test does not cover tools/. Stubs now stamp manual — deliberately not system/init, which resolves to system/* and is system_only, and these stubs exist to be filled in by a person. The append-only log entry stamps system/init. A test asserts no generated block falls through to the catch-all: the catch-all is a safety net for documents in the wild, not a licence for our own writers to skip choosing a policy.

This is the correctness half of RFC 0031 (still in draft, and deliberately not linked here — it lands in a separate change), split out and shipped ahead of it. The vocabulary reconciliation that RFC proposes — splitting _meta.source into actor and resolution, SRC-01/SRC-02, and the corpus migration — is a design change with real migration cost and is still in draft. The unpoliced-write path is a bug, and waiting on that debate to fix it was the wrong trade.

Added — RFC 0010 implemented: signed blocks, and the receipt signing it unblocked (corpus 245 → 263) ​

RFC 0010 (drafted 2026-04-27, accepted and implemented 2026-08-27) is the first item off the v2 train the owner unfroze on 2026-08-26. Protocol goes to 1.7.0 for the new §V.11.

  • _meta.signature is normative — protocol §V.11 (wire format, the six-field canonical signing input, verification, key distribution, capability declaration), format spec §3.4, and the uwmd-block schema. alg is a closed set at 1.x (ed25519 | es256 | es384); ECDSA is raw r||s per JOSE, never DER.
  • The cryptography lives in a new @uwmd/signing (0.1.0), not in core. @uwmd/core keeps its zero-crypto guarantee and owns only the crypto-free half — the wire shape and canonicalBlockSigningInput. Two injection seams connect them: verifyChain(parsed, { signatureVerifier }) and the signatureVerifier that verifyReceipt has always accepted. Reached from the CLI by dynamic import as an optional peer, the same arrangement @anthropic-ai/sdk gets, so the layering invariant holds.
  • Receipt signing now works. It was the one advertised receipt feature shipping unimplemented; signReceipt + createReceiptSignatureVerifier close it, and a signed receipt verifies instead of reporting RCP-08.
  • INT-05–INT-08 join BUILTIN_REMEDIATIONS. Two distinctions are deliberate and pinned: INT-06 (unknown kid) is never merged into INT-07 (bad signature) — one says load a key, the other says reject the document — and a drifted content_hash escalates from INT-04 warning to INT-07 error on a signed block, because there it means the content in front of you is not the content anybody signed.
  • A verifier with no key store reports signatures as present and unchecked, never as verified: IntegrityResult gains signatures_present alongside signatures_verified, and uwmd verify --signing --keystore=<path> opts in.
  • Conformance: the new conformance/signing/ suite, 6 assertions over 5 generated scenarios (valid, tampered, unknown kid, no hash, no backend) plus a backend-independence invariant asserted without a baseline. Fixtures are generated by scripts/gen-signing-fixtures.mjs from a checked-in test key whose private half is published — a signature over a hash of the file it lives in cannot be hand-authored.
  • Tests: 11 new core tests and 44 in @uwmd/signing, including an ed25519/es256/es384 round trip, signature-swap and wrong-key rejection, key store refusals, and the full issue → sign → verify receipt path.

Added — RFC 0005 implemented: stochastic calculations (corpus 294 → 301) ​

RFC 0005 (drafted 2026-04-26, accepted and implemented 2026-08-27), on the override mechanism RFC 0007 added the same day. Protocol §VIII.8.

  • A declaration, not built-ins. The RFC proposed uniform(), normal(), triangular(), and monte_carlo(expr, n) in the §VIII.1 grammar. Every builtin here is a pure function of its arguments (a sampling builtin carries PRNG state), arguments are evaluated eagerly (so monte_carlo cannot take a lazy expression), and a call whose legality depends on the enclosing declaration's deterministic flag is a context-sensitive grammar checked by a context-free parser. Inputs are declared in JSON and each draw is an ordinary evaluation with overrides. The grammar and the built-ins are untouched.
  • PCG-XSL-RR-128/64 is normative, seeded exactly as the reference srandom does. Uniform doubles take the top 53 bits over 2⁵³, not 64 over 2⁶⁴, which rounds.
  • normal is not bit-exact across platforms, and the spec says so. This is a correctness finding the RFC did not have: specifying the PRNG is necessary but not sufficient, because IEEE 754 does not specify log, exp, sin, or cos. Box-Muller and Marsaglia polar both depend on log, so identical seeds would still produce samples disagreeing in the last place. Every distribution is now sampled by inverse CDF: uniform and triangular are exact, normal uses Acklam's rational approximation whose central 95% is exact and whose tails still need log. Conformance compares the first two exactly and the third at a stated tolerance — which also settles the RFC's open question about fixture comparison, better than the proposed blanket tolerance that would have hidden the difference.
  • Percentiles are nearest-rank, never interpolated, so a percentile is an observed sample and is exactly reproducible whenever the samples are.
  • The distribution never travels through CalcResult. The RFC would have had value carry "the mean when the consumer requests a single number"; CalcResult.value is pinned by receipts, rendered by the CLI, and emitted from by Excel, and a value that silently means a distribution's mean in some rows is how a wrong number reaches a credit memo.
  • Input order is part of the contract, a variable drawn twice is refused, a failed draw is excluded from the summary rather than counted as zero, and stddev is the sample (n−1) form. CALC-STOCH-001–006; capabilities gain calc-stochastic and calc-sensitivity.
  • Conformance: conformance/stochastic/, six scenarios. Reproducibility is asserted in-process without a baseline (re-running must return the identical summary), so it binds any implementation and not only one that matches our frozen numbers; a second scenario asserts a different seed produces a different summary, since a seed that did not change the stream would make reproducibility accidental.
  • Tests: 34 new core tests across the PRNG, the samplers, and the engine.

Known gap at this release — the PCG test vector was self-generated ​

Resolved in the 2026-09-12 maintenance sprint through an independent NumPy comparison; see verification evidence. The original release-time finding below is preserved as history.

prng.ts implements PCG-XSL-RR-128/64 from the published algorithm, but the test vector was generated by that implementation. It proves self-consistency, not agreement with the reference C implementation at pcg-random.org, which nobody has diffed against. Recorded in prng.ts, in RFC 0005, and here rather than left to be assumed away; it should be closed before the RFC is accepted. The steps are in docs/handoff/HUMAN-verify-pcg64-vector.md. Until then, port the TypeScript rather than re-deriving pcg64 from the paper.

Added — RFC 0007 implemented: sensitivity tables, without touching the grammar (corpus 289 → 294) ​

RFC 0007 (drafted 2026-04-27, accepted and implemented 2026-08-27). Protocol §VIII.7. No version bump beyond 1.8.0, which the same day's §X.2 already carried.

  • A declaration, not a builtin. The RFC proposed sensitivity_table(expr, {…}, {…}) inside the §VIII.1 grammar — which has no object literals, no array literals, and a string production that is a value rather than a program. That is three sandbox extensions, one of which makes a string argument executable, to reach axis data already sitting in JSON one level up. What shipped is a JSON SensitivityDecl with an ordinary safe expression as base_formula. The grammar is unchanged.
  • CalcEvaluationContext.overrides is the actual primitive: values keyed by full dotted path, consulted ahead of frontmatter, sections, and prior results. Two properties are normative — overrides shadow and never write (a sweep that mutated the document would silently change the deal), and a null override means "treat this path as absent", distinct from having no override. Scenario sweeps and stress tests need the same mechanism.
  • The grid never travels through CalcResult.value, which stays number | string | boolean | null. RFC 0016 receipts pin that union, the CLI renders it, and Excel emits from it; widening it would break three consumers for a feature none of them asked to carry.
  • A failed cell does not fail the table. A grid where one combination divides by zero is still useful, and refusing the whole thing would hide the cells that worked. failed_cells says how much of the table is real.
  • CALC-SENS-001–005. CALC-SENS-004 (both axes on one variable) is new beyond the RFC and is a trap rather than a redundancy: the second override silently wins for every cell, producing a grid whose rows are identical and whose reader cannot see why. Bounds are 256 cells and 64 per axis, so a 1×256 strip is no cheaper than a square.
  • Conformance: conformance/sensitivity/, five scenarios with hand-checkable round numbers — including one that asserts the document reads unchanged after a sweep.
  • Tests: 22 new core tests covering overrides and the grid.

Fixed — RFC 0007's motivation described renderers that do not exist ​

The RFC states that @uwmd/excel and the web editor "both have ad-hoc grid renderers that re-derive the axis structure from the calc IDs". Neither does, and neither ever did. The rest of the motivation stands on its own; that claim is corrected in the RFC rather than repeated. Excel emit is deferred accordingly — there is no ad-hoc implementation to replace, and SensitivityResult hands an emitter the grid structure whenever one is written.

Added — RFC 0003 implemented: modules can declare asset classes (corpus 274 → 289) ​

RFC 0003 (drafted 2026-04-26, accepted and implemented 2026-08-27), on top of the module runtime RFC 0006 landed the same day. Protocol goes to 1.8.0 for the new §X.2; the format spec gains §2.2a. uw_version stays 1.1 — the grammar addition is additive and no 1.1 document changes meaning.

  • Format spec §2.2a — the identifier grammar. The ten builtins stay a closed set; a custom class is reverse-DNS with at least three lower-snake-case segments. Ownership is fixed at the identifier, so two implementations can never disagree about whose com.example.data_center this is — which is what makes opening this extension point safe where an open enum would not have been.
  • Protocol §X.2 — resolution, in exactly three outcomes. Resolved (builtin, or a loaded module declares it), degraded (MOD-FALLBACK-001, rendered via the declared builtin fallback and reported as degraded, never as a full read), or unresolved (MOD-MISSING-001). Determinism holds in all three: there is no arrangement in which two conforming hosts read the same file differently.
  • AssetClass is deliberately not widened. The RFC proposed folding custom ids into the union, which in TypeScript collapses it to string — silently disabling ASSET_CLASS_MEMBERS' exhaustiveness anchor, every pack and layout lookup's narrowing, and the RFC 0027 / 0029 class tables. A separate UWAssetClassId is used at the boundary where a custom class is legal; anything needing a builtin still asks for one and still narrows.
  • Holding a declaration is not holding the module. resolveAssetClass takes knownDeclarations separately from the registry and will only ever degrade from them. A cached declaration gives a display name and a fallback; what "loaded" means is the module's calculations and validations.
  • Resolution is not validation. validateUWFile checks only what is true for every reader — INVALID-ASSET-CLASS-001/002 and the MOD-DEPENDENCY-UNDECLARED warning. Folding resolution in would make the same file valid or invalid depending on who ran it, and §5 of the format spec now says so explicitly.
  • MOD-ASSET-CLASS-CONFLICT-001 is new beyond the RFC: two modules declaring the same identifier is squatting, and picking one silently would make resolution depend on load order. Display-name collisions stay info.
  • Custom classes get no builtin pack, layout, or size intensive (§X.2.4). Wiring one into the §XIII registry would let a third party change what price_per_unit divides by.
  • Conformance: four manifest rejections plus conformance/modules/asset-classes/, whose first three scenarios are byte-identical documents differing only in what the host has loaded — with a cross-scenario invariant asserting they stay identical, since editing one to fix a failure would void the demonstration.
  • Tests: 36 new core tests (identifier grammar, the three resolution outcomes, declaration conflicts, the validator's two codes).

Added — RFC 0006 implemented: the module system gets a runtime, and a consumer (corpus 269 → 274) ​

RFC 0006 (drafted 2026-04-26, accepted and implemented 2026-08-27), the last item on the priority order the owner set when the v2 train unfroze.

  • The gap was not the loader. The RFC's "loader changes" section was written before modules.ts existed; loading, version-checking, and registration have shipped and been hardened since. What building a real module surfaced is that nothing consumed a registered module: calculations were reachable only by a host that evaluated them itself, validations were shape-checked at load and never executed by anything, and sections were declared and never looked for. The module system was a registry with no runtime.
  • module-runtime.ts is the runtime — evaluateModuleCalculations, validateAgainstModules, checkModuleSections — and introduces no new evaluation machinery. A validation rule is a safe expression in exactly the §VIII.1 grammar, run through evaluateCalc like any other declaration. A module able to evaluate what the calc engine cannot would be a second, unsandboxed language reachable from a third-party manifest.
  • null is not false. A rule asserts what must be true and fires only on false. A document carrying no hotel_brand has not violated a rule about franchise fees; it has said nothing about them, and treating absence as violation would fire every module rule on every partial file.
  • Failures are reported, not skipped: MOD-CALC-ERROR, MOD-RULE-ERROR, MOD-SECTION-MISSING. The first exists because of something the tests found — an unresolved identifier evaluates to null, so a calc depending on a broken one succeeds with no value and every rule reading it falls silent. One typo in a formula quietly disables everything downstream, and this issue is often its only trace.
  • Protocol §X gains the host obligations a registered module implies: declaration-order evaluation, the null rule, no second evaluation path, and report-don't-skip. Section schema fragments stay normative JSON Schema that a host with a validator SHOULD apply — @uwmd/core deliberately is not such a host, so checkModuleSections checks presence and stops rather than shipping a half-implemented subset of JSON Schema.
  • @uwmd/module-hospitality 0.1.0 — the reference module, built against the library's published surface and nothing else, because a reference module that reached inside would demonstrate nothing. Three sections, five calculations, three validations. The manifest's source of truth is TypeScript (a typo in a kind or severity is a compile error) with dist/manifest.json emitted at build for hosts without a TS toolchain.
  • Occupancy is a fraction, and CC-MOD-HOSP-02 is an error rather than a warning because revpar still computes from a percentage — to a number a hundred times too large. Only the rule catches it.
  • Conformance: conformance/modules/runtime/, five scenarios derived from one fixture — the fixture itself (both warning branches in one file), no comp set, occupancy as a percentage, the required section removed, and the same file relabelled office, where nothing must run at all.
  • Tests: 12 new core tests against a toy manifest (core must not depend on a sibling package) and 15 in the module package.

Added — RFC 0004 implemented: a language-agnostic conformance driver ​

RFC 0004 (drafted 2026-04-26, accepted and implemented 2026-08-27). The corpus is the project's main lever for staying coherent as adopters arrive, and it was runnable only from TypeScript — a Python or Rust implementation could "pass the conformance tests" only by writing its own runner, and two runners drift.

  • Protocol §II.6a — the conformance CLI protocol. Six subcommands, exactly one JSON document on stdout (render emits text), stderr free for diagnostics, and a three-value exit contract: 0 success, 1 a failure the protocol describes with stdout still parseable, 2 unrecoverable. Also normative: comparison is a subset test, because the baselines are projections and an implementation reporting extra fields is more informative, not wrong — while arrays compare length-sensitively, since an omitted validation issue is a disagreement rather than brevity.
  • conformance/runner/runner.py — the reference driver. Python 3.10+, standard library only, so an air-gapped implementer can run it. TAP version 14 on stdout plus an optional JSON manifest carrying the implementation's own ImplementationManifest; TAP has no standard way to attach that, and without it two implementations' results cannot be aggregated. 44 cases across tiers 1–3, and npm run conformance:v2.
  • The v1 runner is not replaced. npm run conformance remains the gate at 269 assertions across thirteen suites, most of which are not a single command with a single output. The v2 driver gates the protocol; rewriting receipts, composition, and packaging as CLI calls would have meant deleting coverage to satisfy a migration plan written when the corpus was four tiers.
  • Cases are generated and CI-checked — npm run gen-conformance-cases, with --check in CI. A fixture added without regenerating them would silently drop out of v2 coverage while every suite still reported green.
  • uwmd manifest, and --json on calc and edit. edit --json reports the edit instead of performing it: a driver that rewrote fixtures as a side effect of reading them would corrupt the corpus it is testing.

Fixed — uwmd parse was dropping four fields of the parsed file ​

custom_calculations, custom_scenarios, extensions, and the full superseded blocks were omitted, so a caller who trusted uwmd parse to be "the parsed file" silently lost every custom calculation and every x_* extension in the document. All four are emitted now; superseded_blocks (prior blocks' content only) stays alongside superseded for compatibility, and new callers should read the latter. Surfaced by RFC 0004 making the output observable against a baseline.

Fixed — two capabilities the spec promised but the enum did not carry ​

ViewerCapability gained signing and module-signature-verification. Protocol §V.11.5 and §X.1.4 told implementations to declare them the day before, while the type they would declare them in had no such members.

Added — RFC 0002 implemented: module manifest signatures (corpus 263 → 269) ​

RFC 0002 (drafted 2026-04-26, accepted and implemented 2026-08-27), the second half of the signing chain, on RFC 0010's machinery. Protocol §X.1; no version bump beyond 1.7.0, which the same day's §V.11 already carried.

  • ModuleManifest.signature is normative — protocol §X.1, the new module-signature.schema.json, and an additive $ref from module-manifest.schema.json. A module manifest is executable surface (formulas the calc engine evaluates, validations that decide whether a deal reads as blocking), and a host loading one from npm or a URL had no way to ask whether it was the manifest the author published.
  • The scheme is uwmd-keystore, not Sigstore — a deliberate departure from the RFC's opening design, recorded in it. Sigstore needs a Fulcio trust root and a Rekor inclusion proof, which means either a vendored snapshot that fails closed when stale or network access inside the module loader. Neither fits a protocol whose conformance corpus is offline and deterministic. scheme: "sigstore" is reserved so adding it stays additive.
  • Five verdicts, kept apart: PROTO-MOD-068 missing, -069 unsupported scheme, -070 malformed, -071 unknown key, -072 invalid. missing, unknown_key, and invalid call for three different responses — decide a policy, load a key, reject the module — and a verifier that reports them as one makes all three indistinguishable where the operator has to act.
  • Three host policies: ignore (default, what every host did before), verify-if-present (unsigned loads, broken refuses), and require. Both checking policies refuse on unknown_key: a host that cannot check a signature has established nothing, and treating that as success would make the policy decorative. loadModuleManifestAsync / createModuleRegistryAsync are the async siblings; the sync loaders are untouched and pay nothing.
  • A malformed signature refuses even under ignore. Declining to verify is not a licence to admit a malformed object into a frozen manifest.
  • identity is advisory, and §X.1.5 says so normatively. A signature proves the key holder asserted an identity, never that the assertion is true; the allowedIdentities allow-list is worth only as much as the host's decision to bind that kid to that identity.
  • Conformance: conformance/signing/modules/, six generated scenarios each run under all three policies — 04-unsigned loading under verify-if-present and refusing under require is the entire policy distinction, and asserting one half would let the two collapse unnoticed. The suite reuses RFC 0010's test key: a host that trusts a signer trusts them for both artifact kinds.
  • Tests: 19 new core tests (crypto-free: payload, shape, taxonomy, allow-list) and 18 in @uwmd/signing (round trip, tamper, policy matrix, registry refusal).

Fixed — §V.9's hash exclusions had never fired on a real file ​

Surfaced while building the RFC 0010 fixtures. The canonicalizer decided an object was _meta-shaped by requiring the key section, but every .uw.md on disk spells it section_id (format spec §3, and the block schema's own example). So looksLikeMeta returned false for every parsed block, the two exclusions §V.9 mandates silently never applied, and stamping a content_hash changed the hash it was supposed to be a digest of — INT-04 on an untouched file. It went unnoticed because the only blocks that exercised the exclusion were hand-built test objects, which do set section.

integrity-canonical.ts now accepts either spelling and §V.9's wording is corrected to match. No shipped fixture's stated hash changes.

Added — RFC 0029 implemented: stage requirements are class-aware (corpus 245 → 257) ​

RFC 0029 (drafted, accepted, and implemented 2026-08-26) closes the finding the RFC 0028 example cleanup surfaced: §5.1's per-stage section lists were class-agnostic, so a land deal could never honestly declare full_underwrite (a rent_roll was required and raw land has no tenants) and a mixed-use deal was asked for a property-level rent_roll its components section would wrongly duplicate.

  • §5.1 gains a two-row class-overlay table, mirrored by STAGE_SECTION_OVERLAYS + requiredSectionsFor() in validator.ts (the RFC 0027 declare-once pattern): land is exempt from rent_roll and operating_statement at every stage; mixed_use substitutes components for both — checkable (the substitute is required where the replaced sections were, once), and tightening nothing since MU-01 already requires components. All other classes take the base lists verbatim; hospitality's keys/ADR rent_roll stays the precedent that section ids are class-neutral containers.
  • Both consumers resolve through the one function — stage_readiness and DQ-06 cannot disagree about what a stage requires. CC-14 is untouched: property is never exempt.
  • Pure relaxation: no document that validated stops validating; some land/mixed-use DQ-06 notes disappear and readiness booleans can flip false → true — neither pinned anywhere.
  • Conformance: three new Tier-1 valid fixtures whose frozen validation verdicts pin the behavior — a land deal at full_underwrite with no rent_roll/operating_statement validates clean with zero DQ-06; a mixed-use deal whose components foots to the property NOI likewise; and an office control with the same section inventory as the land fixture still carries its DQ-06 — the overlay is two rows, not a loophole.
  • Tests: 9 new unit tests (resolver across all stages × classes, the substitution dedup, readiness/issues agreement, CC-14 on a property-less land deal).

Changed — Sundance and Roosevelt restaged to full_underwrite (RFC 0029 follow-through) ​

The two examples RFC 0029's overlay unblocked climbed the same day: both gained borrower_sponsor, preliminary_sizing, and market_analysis (numbers derived from each file's own sections) and now declare full_underwrite honestly — stage_readiness true, zero DQ-06, no warnings. Sundance's sizing is LTC-governed, the only test with meaning against a negative land carry; Roosevelt's is the standard three-constraint sizing on the consolidated property, with component-level debt left refused (MU-06) as designed. Agave deliberately stays at screening: it is multifamily (no overlay applies), its subject is the capital stack, and a rent roll + operating statement would be padding.

Added — cli-packages.ts test backfill (core suite → 985) ​

The RFC 0018 CLI command wrappers (lease abstracts, deal packages) gained a direct sibling test — they export plain functions, so unlike cli.ts no extraction was needed; the harness spies console/stdout and replaces process.exit with a thrower. 14 tests pin the command-level contracts, including the encoder's PKGZIP-002 refusal of a sha-mismatched package (which is also why verify's failed branch is unreachable through our own tooling — it is reserved for hostile archives built elsewhere) and the semantic_digest → unverifiable branch. Coverage 0% → 97.5%.

Fixed — CLI flag/positional divergence; cli.ts plumbing extracted and tested ​

uwmd <cmd> --flag=value <file> silently dropped the filename and died with a usage error: the positional-extraction pass skipped the token after any-- token, while parseFlags correctly treats the = form as self-contained. The two passes now share one consumption rule.

The fix came out of the cli.ts test backfill (the last item in wiki 13's coverage note): cli.ts is a top-level script that runs at import time, so it cannot carry a sibling unit test — its pure argument/path plumbing (parseFlags, extractPositionals, replaceUWExtension, hostTierFlag, defaultPartsDir, readManifestFile) now lives in cli-args.ts, imported by cli.ts and covered at 100% by cli-args.test.ts (20 tests, including a pin that the two passes mirror each other exactly and a note on the greedy space form — flags go after positionals). No public API change; the command surface remains covered by the @uwmd/cli smoke tests.

Changed — all twelve worked examples are stage-honest (RFC 0028 follow-up) ​

Every example now satisfies its declared deal_stage's section list — zero DQ-06 notes across the corpus, so a future RFC can revisit DQ-06's severity on honest ground.

  • Seven class examples (retail, industrial, student, senior, office, hotel, self-storage) gained their five missing sections: operating_statement (a T-12 summary reconciled to the NOI model — same revenue, expense, and NOI totals), preliminary_sizing (max loan under 75% LTV / 1.25x DSCR / 9% debt-yield tests, governing constraint named), borrower_sponsor, market_analysis, and validation, all with numbers derived from each file's own sections so the cross-section checks hold.
  • The Parkview twins gained preliminary_sizing — truthfully recording that the proposed $5.04M loan fits the LTV test but exceeds the DSCR and debt-yield sizings, the same tension their FV-04 warning already states.
  • The three feature-focused examples restaged to screening (plus a validation section) rather than acquiring padding: a property-level rent_roll is wrong for land (Sundance) and mixed-use (Roosevelt — its rent data is per-component), and Agave's subject is the capital stack. This surfaced a finding worth keeping: §5.1's stage requirements are class-agnostic, and a class-aware refinement is a candidate future RFC.

No code change; examples and docs only. Full suite, conformance (245), and lint all green with zero baseline updates — the pins that read these examples (size-intensive csv, report cover, Lite round-trip) check specific facts, not whole-file shapes, so additive sections pass through them.

Added — RFC 0028 implemented: a missing required section is now a reportable defect (corpus 244 → 245) ​

RFC 0028 (drafted, accepted, and implemented 2026-08-26) closes the gap its own Appendix A scan measured: format spec §4.1 requires the property section at every stage and §5.1 lists per-stage required sections, yet 28 in-scope corpus documents omitted property entirely and validated clean/warnings, because stage_readiness was computed but never reported as issues.

  • CC-14 (warning): a deal-record UWX document with no property section. Preconditions mirror CC-13's (not a compiled Lite summary, deal profile only; an externalized section is present, not missing); unconditional on deal_stage. Never coincides with CC-13 — one defect, one diagnostic.
  • DQ-06 (info): one issue per section the declared deal_stage requires but the file lacks — the sectional sibling of DQ-04 and the issues-stream mirror of stage_readiness. Suppresses its property entry when CC-14 fired. Info by design: the scan found all twelve worked examples fail their declared stage's list, so info reports without refusing or nagging.
  • operating_statement re-joins STAGE_REQUIREMENTS.full_underwrite and above (RFC decision (a)): §5.1 always listed it, and the validator's variant-aware hasSection case was built for it but no stage list reached it. §5.1 also gains its missing Scope row and a presence-vs-freshness split for Monitoring.
  • Spec: §5.1 is now machine-checked language; §5.3 gains the CC-14 row and severity/applicability prose. Format stays 1.1 (additive; both new rules are sub-error).
  • Conformance: new Tier-1 malformed/10-property-section-missing fixture (CC-14 + DQ-06); the Tier-1 validation-verdict and render baselines absorb the new codes (the baseline system added 2026-08-25 doing its job). The capital-stack/senior-reconciles-debt-structure fixtures were made stage-honest (a property section added, the boilerplate full_underwrite claim dropped) rather than freezing ten noise codes.
  • Tier-4 replay: the cassette's prompt-drift detector fired — agent context includes validation output, which now carries the new codes. New scripts/rerecord-cassettes.mjs turns the replay README's inline recipe into a runnable tool: it re-records every scenario's cassette from its scripted-completion.json through the real recorder, pinned to the replay clock. The cassette diff shows only the request side changed.
  • Tests: 15 new unit tests (validator.section-readiness.test.ts) covering every CC-14 precondition, the CC-13/DQ-06 mutual exclusions, and the operating_statement requirement through its t12 variant.

Follow-up tracked in docs/wiki/13-status.md: all twelve worked examples declare full_underwrite but miss 4–5 of its required sections — bring the examples to stage-honesty (add the sections or restage them), then a future RFC may revisit DQ-06's severity.

Added — two documented conformance blind spots closed (corpus 222 → 244) ​

Both gaps were already written down when their features shipped; this change makes the corpus able to see them.

  • Tier-1 valid fixtures now freeze their validation verdict. Each fixture gains an expected/<id>.validation.json baseline holding overall_status plus every distinct (code, severity) pair from validateUWFile. RFC 0027 Appendix A noted that Tier-1 valid fixtures asserted nothing about validation, so escalating a rule from warning to error would flip uwmd validate to exit 1 on fixtures with the suite still green. A severity flip and a new code are both visible diffs now. The 04-scope-only baseline records the CC-13 warning that fixture truthfully earns, plus its DQ-05 and META_LOW_CONFIDENCE_NO_REVIEW_FLAG info notes.
  • The RFC 0025 decimal-exactness pin the Lite suite could not carry. Every existing Lite fixture's percents (5.50 / 5.75 / 6.25 / 5.00 / −1.50) divide cleanly by 100 in binary64, so all 90 assertions passed unchanged through the RFC 0025 fix — the corpus could not distinguish decimal-point-shift normalization from naive division. 06-decimal-exact-percents states rates only as literals that diverge under division (5.51 / 100 is 0.055099999999999996, one ULP off 0.0551); its twin 07-decimal-exact-fractions states the same values as bare fractions with unit=fraction. A new equivalence.json group requires both to share one digest, which holds only under the RFC 0025 rule, and the frozen canonical.json pins the exact fractions besides.

No library change; runner + fixtures + baselines only. Docs updated: conformance/README.md, conformance/tier-1-reader/README.md, docs/wiki/09-conformance-testing.md.

1.7.0 - 2026-08-25 ​

Released ​

  • @uwmd/core 1.7.0, @uwmd/cli 1.7.0 (lockstep), with coordinated @uwmd/core repins in @uwmd/excel 0.6.0, @uwmd/report 0.6.0, and @uwmd/batch 0.5.0. Carries RFC 0027 (the size-intensive registry), implemented since 1.6.0.
  • Format stays 1.1; protocol bumps to 1.6.0. The format change is additive (nine optional null-default §4.1 fields; CC-13 is a warning), but UW_PROTOCOL_v1.md gained a normative section — §XIII's selection registry, with a MUST on any implementation that displays, exports, or indexes a deal's size — and §0.3's own rule is that the protocol bumps on any normative change to required behavior. ("Future work" is now §XIV.) Also corrected §0.3's stale "currently 1.3.0" prose, which had drifted from PROTOCOL_VERSION unguarded.

Added — the size-intensive conformance group; RFC 0027 is implemented (part 4) ​

The seven scenarios the RFC specified, closing the arc (corpus 215 → 222; RFC 0027 flipped to implemented):

  • registry-covers-every-class — §XIII.1/2/3 pinned against the shipped table: nine primaries, mixed_use and unrecognized classes null, no unpinned class in the registry.
  • pack-agreement — coverage in both directions: each primary appears in its pack's formulas, and every property.* path a pack reads is registry-known.
  • csv-exports-size-for-every-class — all ten worked examples (plus Parkview): size_basis/size_quantity non-empty for nine classes, empty for mixed_use, and total_units keeps its value everywhere — the compatibility pin.
  • report-cover-states-size — the office cover carries RSF 42,500, the hotel's Keys 142, and the multifamily cover gains no size fact.
  • lite-round-trip-non-multifamily — the office example projects to Lite with its RSF anchor and compiles back to the same value.
  • cc-13-warns-and-does-not-refuse — CC-13 fires once as a warning and the office pack's cap_rate still evaluates; a warning must never refuse.
  • cc-13-silent-for-mixed-use — no CC-13 and resolveDealSize null, by design (§XIII.2).

Also: the conformance README corpus tree gains the group, and docs/wiki/13-status.md records the arc (including the §XI → §XIII errata).

Changed — Excel layouts and the web-editor grid read the registry (RFC 0027, part 3) ​

The two remaining hand-maintained copies of "which field is this class's size" now derive from Protocol §XIII. Behavior is unchanged in both tools — this is the duplication-removal half of the RFC.

  • @uwmd/excel — a new sizeNamedInputs(assetClass, labels) helper in layout.ts selects each layout's property-size named inputs through the registry (primary first, then the secondaries the layout labels); the nine per-class layouts drop their hard-coded rows. Labels and sheet positions are untouched, so every workbook byte and parity assertion holds.
  • tools/web-editor — the quick-edit grid's per-class size scoping is generated from SIZE_INTENSIVES: a path is offered to exactly the classes whose registry entry names it, with only the label and input kind remaining editor-local. Two new tests pin the derivation (71 total).

Added — the size-intensive registry lands in @uwmd/core (RFC 0027, part 2) ​

The executable half of the Protocol §XIII registry. New browser-safe exports: SIZE_INTENSIVES, getSizeIntensive(), resolveDealSize(), and the SizeIntensive type.

  • protocol.ts — the §XIII table (nine classes; mixed_use deliberately null, unrecognized classes null, never a guess). resolveDealSize() reads the raw property section — never the cascade, because a deal's size is a fact about the asset — and applies §VIII.2's envelope-content unwrap exactly as the calc evaluator does, so the registry and the packs can never disagree about the same document.
  • validator.ts — CC-13 (warning, never error) with the five normative applicability preconditions, plus its BUILTIN_REMEDIATIONS entry.
  • renderer.ts — csv appends size_basis / size_quantity (appended, never inserted: column order is contract; total_units keeps its position and meaning). summary and chat state a non-total_units class's size; multifamily and senior-housing renderings are byte-identical to before.
  • report.ts — the Lender Package / Credit Memo cover and property table state the class's own size (RSF 42,500, Keys 142); the multifamily cover is unchanged.
  • lite-bridge.ts — UW_LITE_FIELD_MAPPINGS gains one anchor per intensive, mirroring the Lite spec §8 rows from part 1.
  • Conformance — tier-1/04-scope-only expected chat/summary gain the CC-13 warning line: that fixture states "units", a field no consumer reads, so it truthfully never stated a size at the normative path. No other baseline moved; Tier-3 and Excel parity are untouched.
  • Tests — 33 new: the registry/pack agreement suite (both directions), CC-13's severity and every precondition, per-consumer size assertions, the Lite round-trip, and multifamily no-drift pins. Core suite 894 → 927.

Added — the size-intensive registry lands in the specs (RFC 0027, part 1) ​

RFC 0027 was accepted 2026-08-25; this is the spec half of its implementation.

  • Format spec §4.1 — the property payload now declares every asset class's size intensive (rentable_square_feet, gross_leasable_area, net_rentable_square_feet, rentable_units, keys, total_beds, gross_acres, usable_acres, entitled_units), all optional and null by default, with field notes — including the gross_acres vs land_area_acres disambiguation and the two-count note for senior/student housing. New normative language: a property block MUST state its class's primary size field and SHOULD use null/absence, never zero, for inapplicable fields.
  • Format spec §5.3 — new CC-13: the property section must state the primary size field for frontmatter.asset_class. A warning, never an error, with five normative applicability preconditions (UWX only, deal-record profile only, recognized non-mixed-use class, property section present and not externalized).
  • Protocol spec §XIII (new) — the per-class size-intensive selection registry: primary field, label, unit, and secondaries for all ten classes; mixed_use deliberately has no property-level size (§XIII.2); the table is closed for protocol 1.x (§XIII.3). "Future work" renumbered §XIII → §XIV. The RFC text called this section §XI, which the protocol already used for the Error Taxonomy — recorded as RFC 0027 errata.
  • Lite spec §8 — the deal-summary-v1 bridge mapping table gains a row per intensive, so a Lite summary can state any class's size, not only multifamily's.

Spec text only; the executable mirrors (protocol.ts registry, CC-13 in the validator, the renderer/report/lite-bridge consumers) follow in the next PR.

Fixed — the web editor's quick-edit grid is asset-class aware ​

fieldsForSection() in tools/web-editor filtered by section_id alone, and the catalog carried only multifamily's size intensives. Two consequences: a land parcel was offered a "Total units" input, and an office, retail, industrial, self-storage, hospitality, or land deal was offered no size input at all — the denominator of every per-unit metric on the strip was reachable only through the collapsed generic all-fields editor.

  • The catalog now carries the size intensive for all ten classes (rentable_square_feet, gross_leasable_area, net_rentable_square_feet + rentable_units, keys, total_beds, gross_acres / usable_acres / entitled_units, total_units, total_nra_sqft), each scoped to the classes whose packs and worked examples use it.
  • fieldsForSection(section_id, asset_class) narrows the grid opt-out: a field is dropped only when the class is known and the field names other classes, so an unset or unrecognized class sees everything, class-independent fields (year_built, parking_spaces) are never scoped, and mixed_use is unfiltered by design. Nothing became unreachable — GenericFieldEditor still surfaces every scalar leaf.
  • Pinned by a coverage assertion, not an equality: every property.* path a class's calc pack reads must be offered to that class. Web-editor suite 63 → 69 tests.

No published package changes; tools/web-editor is private. Format, protocol, and every schema are untouched.

Known gap — surfaced by the above ​

spec/UW_FORMAT_SPEC_v1.md §4.1's property payload names only total_units, total_nra_sqft, and land_area_*. The nine other classes' size intensives are read by their calc packs and carried by their worked examples but are normatively undeclared, so every tool needing them re-derives the vocabulary from the packs. Written up as RFC 0027 (since accepted), which declares the fields in §4.1 plus a normative per-class selection table in the protocol. Drafting it found three further consumers that never re-derived the mapping and are wrong today — the one-row csv read model, the §7.1 report cover, and the UW Lite bridge — all recorded in the RFC and in docs/wiki/13-status.md.

1.6.0 - 2026-08-22 ​

Released ​

  • @uwmd/core 1.6.0, @uwmd/cli 1.6.0 (lockstep), with coordinated @uwmd/core repins in @uwmd/excel 0.5.0, @uwmd/report 0.5.0, and @uwmd/batch 0.4.0. Carries RFC 0019 (mixed-use composition) and RFC 0026 (typed capital stack), both implemented since 1.5.0.
  • Format stays 1.1 and protocol stays 1.5.0. Both RFCs are additive: the components (§4.23) and capital_stack (§4.24) sections are new and optional, no existing .uw.md/.uwx.md file changes meaning, and UW_PROTOCOL_v1.md is untouched. Minor across the board for the same reason.

Added — a typed capital stack (RFC 0026, implemented 2026-08-22) ​

A deal can state its full financing as an ordered array of typed tranches (senior / mezzanine / preferred equity / common / bridge / seller / other) plus stack-aware sizing figures a deterministic verifier recomputes — state-and-verify (RFC 0021 §6), so an arbitrary tranche count (two mezzanine notes included) lives entirely off the iteration-free Tier-3 calc engine. The ten packs and every single-loan metric are untouched: a document without a capital_stack behaves byte-for-byte as before (pinned by a conformance fixture).

  • capital_stack section (format spec §4.24 + section-capital-stack.schema.json): tranches ordered by seniority and a closed sizing vocabulary — coverage, blended_coverage, debt_yield_through, ltc_through, ltv_through, weighted_cost. Preferred equity carries accrual: current-pay enters cash coverage, PIK does not, and balances count toward attachment-point debt yield either way.
  • verifyCapitalStack (@uwmd/core) — three-state (verified / failed / unverifiable), a sibling of verifyRollup; per-tranche cash debt service (trancheAnnualDebtService) and figure recomputation at a fixed per-fn quantum (CAPITAL_STACK_SIZING_DECIMALS, exported).
  • Validator rules CS-01, CS-02, CS-WATERFALL-UNSUPPORTED (the Phase-2 distribution-waterfall boundary, refused at both section and tranche level), and the generalized CC-03 — the senior tranche must reconcile with debt_structure and the sources_uses senior bucket.
  • Excel: the Capital Stack sheet (@uwmd/excel) — additive for every asset class; one row per tranche with live debt-service formulas, a native-SUM total-capitalization row, and a sizing block whose agree/verdict cells quantize at the verifier's own table. unverifiable figures render as text, never as formulas over blank cells.
  • §4.23 relaxation (MU-06): a mixed-use component MAY carry its own capital_stack, validated by the same CS-* rules; a bare component debt_structure stays refused, and CC-03 stays top-level-only.
  • Conformance: the seven-scenario capital-stack group (215 total), and the worked example examples/Agave-Court-Apts-Scottsdale-AZ.uwx.md.

The multi-period distribution waterfall (promote, hurdles, tiers, catch-up) is documented and deferred to a later phase (RFC 0026 §E).

Added — mixed-use composition (RFC 0019, implemented 2026-08-19) ​

mixed_use — the last AssetClass member without a pack — is now fully supported, closing asset-class coverage at 10 of 10. Composition lives in the document as a bounded components map keyed by each use's own class, not in the pack, so the one-pack-per-class assumption survives (the Tier-3 calc engine has no iteration to evaluate per-component packs and aggregate).

  • components section (format spec §4.23 + section-components.schema.json): per-use subtotals, allocation_pct, and operating-business intermediates.
  • MIXED_USE_PACK (21 metrics) + MIXED_USE_DEFAULTS (mix-independent financing terms). Property cap rate / LTV / DSCR / debt yield / cash-on-cash, a NOI share per component use, allocation-gated price per unit / psf / bed, and per-component GOP / labor pass-throughs. Deliberately no property price/unit, loan/unit, or blended market cap rate.
  • Validator rules CC-11 (asset-class gate), CC-12 (property NOI == Σ component NOI), and MU-01…MU-06, each with a BUILTIN_REMEDIATIONS entry.
  • Excel MIXED_USE_LAYOUT — per-component operating statements plus a consolidation block that foots to the property NOI, with deal-aware metric emission and exact Excel↔evaluator parity. fromWorkbook refuses mixed_use (WORKBOOK-IMPORT-UNSUPPORTED-SHAPE); export is fully supported.
  • Conformance: six Tier-3 calc-host fixtures and seven tier-1-reader/malformed rejection fixtures, plus a component-scoped defaults cascade proof.

Component-level debt (a capital stack) is out of scope and tracked in RFC 0026.

1.5.0 - 2026-08-19 ​

Released ​

  • @uwmd/core 1.5.0, @uwmd/cli 1.5.0 (lockstep), with coordinated @uwmd/core repins in @uwmd/excel 0.4.0, @uwmd/report 0.4.0, and @uwmd/batch 0.3.0. Carries everything prepared for 1.4.0 (RFC 0025's Lite percent decimal exactness, never published — see below) plus RFC 0021 and RFC 0022.
  • Protocol 1.4.0 → 1.5.0, the one change that makes this a minor rather than a patch: the cascade goes from seven steps to eight. Documents are unaffected; a host that hard-codes the step list is not.
  • Minor rather than major across the board. Every addition is additive to the format — an externalized section is a new packaging of a document, not a new document, which is the whole point of I-1 — and no existing .uwx.md or .uw.md file needs editing to keep working.

Added — composable UWX documents (RFC 0021) ⚠️ protocol 1.4.0 → 1.5.0 ​

A record can hold a section in separate .uwpart.md fragments, reference whole child records to form a composite, inherit assumptions along that graph, and state aggregates a verifier recomputes. One invariant carries the design and everything else follows from it:

I-1. An externalized record, resolved, has the same semantic digest as the byte-identical inline record it expands to.

That is what makes composition packaging rather than modelling: externalizing a section is not a semantic change, does not alter identity, and does not invalidate a receipt issued over the resolved form.

  • spec/UW_COMPOSITION_v1.md — normative fragment grammar, the externalization directive, resolution, merge order, and bounds. Three schemas: uwpart, uw-external-section, uw-rollup. Schema corpus 12 → 15.
  • composition.ts — parseUWPart, resolveComposition, resolveComposite, externalizeSection, stringifyUWPart, bounds, and a 9-code COMP-* taxonomy. Browser-safe; performs no I/O.
  • Refusals, not degradations. A missing fragment leaves the section externalized and reports COMP-UNRESOLVED — never a smaller collection. This is the most dangerous failure the design admits: a rent roll missing four tenants still totals, still validates, and still produces a confident DSCR. part_count is redundant with parts.length on purpose, so a truncated array is detectable rather than silently smaller.
  • Merge order is byte-wise on UTF-8, not localeCompare — a locale-dependent order would make the canonical form, and therefore the digest, vary by machine.
  • stale is a third state, distinct from failed. An ancestor whose recorded child digest no longer matches has an unadopted correction, not evidence of tampering. Checked on the edge: a recorded digest is the parent's view of the child, so the same child can be current for one parent and stale for another.
  • The DFS is iterative on purpose. A recursive walk would blow the JS stack before reaching the depth bound on a hostile graph, turning a clean COMP-DEPTH refusal into a crash. Two cycle paths are covered: one reachable from a root, caught during the walk, and one with no root, caught by a reachability check afterwards — without the second, a fully-cyclic graph would report a clean resolve over zero members.
  • The cascade goes from seven steps to eight (inherited_assumption), which is why the protocol minor-bumps. Position is normative in both directions: below user_input, so a value entered on the deal always beats an inherited one; above investor_profile, because an assumption from a named ancestor of this deal is more specific than an institution-wide preference set. The RFC says only "between user_input and market_data" and its diagram omits investor_profile, so it does not actually say — resolved on the merits and recorded in protocol §V.7.1.
  • Rollup receipts sidestep the wall RFC 0019 hit. The Tier-3 sandbox has no iteration, so a composite states its aggregates and the verifier recomputes them over named child digests using a fixed, non-extensible fn vocabulary. No change to the calc engine. A failed child short-circuits before any arithmetic runs, so a total over an unverified child is never reported as agreeing even when the numbers happen to add up.
  • CLI, 20 → 25 commands — uwmd resolve, uwmd compose --externalize (non-destructive by default; --in-place and --dry-run available), and --resolved on verify and export. Without --resolved an externalized record verifies as a directive and exports a rent roll that is a list of filenames, neither of which is the document anyone means.
  • composition conformance suite, 20 assertions. Corpus 175 → 195. I-1 is asserted on the canonical form and the digest, never on source bytes (those differ by construction — that is the point), and again against a shuffled parts array. The two I-1 assertions were verified by breaking them: mutating a fragment's rent, and reordering the inline twin's rows. Each produced exactly one failure in exactly the expected fixture.

Three errata against RFC 0021 as accepted, all found by building it and all accepted into the RFC on 2026-08-18. None changes the design. (1) The directive needs collection_path: collection_key says which field identifies a row and never says which field the rows occupy, and I-1 cannot hold without it — units and rows are different documents. The alternative was a section-to-collection-field table in the library, which is precisely the hand-maintained mirror that has already drifted for section ids. (2) §7's table had a code for every semantic failure and none for a structurally invalid input: COMP-PART-MALFORMED and COMP-DIRECTIVE-MALFORMED are added. (3) The RFC gives each fragment its own _meta but never says what becomes of it when the fragment becomes a row; I-1 settles it — dropped, since the inline twin's rows are plain objects.

RFC 0021 moves to implemented. Two questions it left open stay open and are deliberately not answered here: whether a fragment may itself externalize (forbidden by omission in the RFC, now stated explicitly in spec §2.3), and whether a section may be partially externalized, which complicates I-1 considerably for unclear benefit.

Added — the Lite projection accounts for externalized sections (RFC 0021 §3) ​

  • UWLiteProjectionReport.externalized_sections — sections the record stores in .uwpart.md fragments. Projection is UWX→Lite only and never resolves fragments, so those rows are absent from the envelope and cannot appear in omitted_paths; naming the section is the only complete account available, and §3 requires it. A record whose only loss is an externalized section is now lossy even though it omits no paths. A new required field on the report type — additive for anything reading the report, a compile change for anything constructing one.
  • The directive's own keys no longer stand in for the contents they point at.external.parts[0], part_count, and collection_key had been reported as omitted data. That is worse than silence: an externalized record listed seven omitted paths where its inline twin listed ten, so a consumer comparing the two would conclude the externalized record lost less, while it was in fact missing an entire rent roll. The keys are packaging and are now excluded.
  • Detection is key-presence, deliberately not validity: a projection MUST NOT throw over a malformed directive, since the report is exactly what tells a reader what the document is missing. Validity stays composition's business.
  • uwmd convert warns on the two losses separately — folding them together printed omitted 0 advanced path(s) over a missing section.
  • New conformance scenario composition/lite-projection/externalized/ (2 assertions), including that the projected Lite document is byte-identical to its inline twin: externalization is packaging, so the report is the only place the difference may show. Corpus 193 → 195.

Added — market data as an attributable document (RFC 0022) ​

MarketDataLookup had been interface-only since the v1.1 train, so the top two cascade steps had never had a worked example. The deeper gap was attribution: a market-derived value recorded no trace of which observation set produced it, so two pulls a week apart were indistinguishable after the fact and a receipt over that deal could not be reproduced.

  • market-data-v1 document profile — market-data.ts, registered as an evidence profile (it carries observations, not conclusions; no pack applies). Identity (document_id/as_of/provider/geo) and a per-observation basis are refusals, not warnings: an observation set with no vintage or no named provider is not attributable, and a number with no stated basis is an assertion. A deal_id is refused outright. 22-code MD-* taxonomy.
  • createDocumentMarketData — a deterministic resolver that drops into the existing cascade. Staleness runs from as_of, not a wall-clock guess, so a stale observation falls through to asset_class_default rather than being used past its vintage. selectCurrentMarketData takes the most recent as_of and raises on a tie rather than letting array order decide which of two disagreeing providers is authoritative.
  • inputs_provenance, receipt format 1.0 → 1.1 — see below.
  • Promotion (market_data_accepted) — promoteMarketObservation builds the value and provenance for accepting an observation as the underwritten number. It returns a payload rather than editing a document, because promotion is an explicit Tier-2 edit by a named actor and never a side effect of resolution. The tag is never user_input: a value accepted for lack of better evidence and one established by diligence are different claims, and a file that renders them identically has destroyed something a credit reviewer needs. Promotion does not upgrade confidence.
  • _meta.market_data_ref and DQ-06 — the promoted block records which set, of which vintage, with a digest. Enforced as an error: without it, the tag is an unfalsifiable claim.
  • STANDARD_SECTION_IDS / isStandardSectionId — the canonical section list from FORMAT_SPEC Part IV, pinned by a test that parses the spec.
  • CLI — uwmd market-data validate <file>, and --market-data <file> on scope and refine, so the top cascade steps finally have a runnable example.
  • spec/schemas/uw-market-data.schema.json, a worked example under examples/market-data/, and a market-data conformance suite (14 assertions: every attribution requirement proved to fail rather than store a blank, plus vintage selection, the tie error, staleness fall-through, and promotion).

Added — shared receipt extension section ​

  • inputs_provenance (receipt format 1.1) — artifacts beyond the subject record that a computation depended on. One section with a source discriminator, shared by RFC 0022 (market_data) and RFC 0021 (child_record), because a verifier handles both identically. The 1.0 → 1.1 bump covers both RFCs and is not repeated; 0021's rollup entries are additive here.
  • RCP-11 (unverifiable) when a referenced input is unavailable, and RCP-12 (failed) when it resolves but its digest disagrees. An absent observation set is not evidence of tampering. RCP-12 is evaluated across all held references before any RCP-11, so an unrelated missing reference cannot downgrade a genuine mismatch.
  • A 1.0 receipt remains valid and readable; the existing verify fixtures still carrying 1.0 receipts now double as the backward-compatibility check.

Changed ​

  • MarketDataLookup.resolve may return an optional source_id, surfaced as ResolvedValue.resolved_from. Additive — hosts returning { value, range } are unaffected.
  • market_data_accepted resolves at the user_input cascade step while keeping its own source tag. Without this a promoted value matched no in-file tag and fell through to asset_class_default, silently discarding the analyst's accepted number. It outranks a live market lookup deliberately: accepting one vintage must not be overwritten by a newer pull. CascadeStep is unchanged; the cascade is normatively ordered and a producer must not reorder it (protocol §IX). Extending it is a protocol change, which RFC 0021 §5 subsequently made — inherited_assumption takes it to eight at protocol 1.5.0 — but RFC 0022 needed no such extension.

Changed — RFC status corrections (no behaviour change) ​

Four RFCs were recording a status the repository had already outgrown. Nothing in the code, specs, or corpus changes; the index now matches what shipped.

  • 0017 (Lite/UWX source split), 0020 (.uwx.md terminology alignment), 0024 (iterative-function determinism), and 0025 (Lite percent decimal exactness) move to implemented. Each meets the bar the RFC process sets for that status — shipped in a release, with a CHANGELOG entry already present above or in 1.3.0/1.4.0.
  • 0020 was the misleading one. It sat at draft while its entire content — the format/protocol/XML/CSV spec realignment and the ten worked examples — had already landed, so the roadmap listed it as blocking work that was in fact finished. spec/UW_FORMAT_SPEC_v1.md now carries 20 .uwx.md references.
  • 0014 and 0016 deliberately stay accepted. Both are genuinely partial: 0014 has package publication outstanding, and 0016 ships unsigned-only receipts pending the RFC 0010 signing package. Marking them implemented would overstate them.

Added — verify-versions, a guard over the compatibility matrix ​

VERSIONS.md calls itself the authoritative compatibility matrix, and nothing enforced that. It drifted twice without anything going red: the 1.4.0 release bumped every manifest and left the matrix advertising 1.3.0 across six rows, and tools/vscode-uwmd sat at 0.1.0 while the extension had moved to 0.2.0.

  • npm run verify-versions (scripts/verify-versions.mjs) checks every Current matrix row against its package.json, the UW Protocol and .uw.md format spec rows against PROTOCOL_VERSION / FORMAT_VERSION in protocol.ts, and every "pairs with @uwmd/core 1.x" note against core's actual series. It reads files as data, so it needs no install and no build, and runs as its own CI job alongside the lockfile check.
  • Scoped to Current matrix only. The Planned tables below it name candidate versions that are supposed to differ from what ships.
  • Why the gap existed. protocol.test.ts already asserted the UW Protocol row matched PROTOCOL_VERSION — which is exactly why that row never drifted while the package rows did. The guard generalizes that one check to every row.
  • Cutting a @uwmd/core release now has five guarded steps rather than four; VERSIONS.md was the unguarded one.

[1.4.0] - 2026-08-16 ​

⚠️ Read before upgrading — Lite percent digests move (RFC 0025) ​

UW Lite normalized a percent display by dividing by 100. That is exact for most rates and not for others: Number('5.51') / 100 is 0.055099999999999996, one ULP off the 0.0551 the literal denotes. Because the normalized value enters the RFC 8785 canonical form, a Lite-compiled 5.51% and a hand-authored UWX 0.0551 were different doubles with different digests, and did not compare equal under semantic equivalence or an RFC 0016 receipt — though both spell the same rate.

Scaling is now exact decimal-point movement (UW_LITE_SPEC_v1.md §4.1), and the Lite canonicalization version moves 1.0 → 1.1. The Lite grammar is unchanged: 5.51% parsed before and parses now, and no document needs editing.

Affected documents are narrower than it sounds. Only a Lite document containing a percent that does not divide exactly changes digest. Every fixture in conformance/lite/ uses 5.50%/5.75%/6.25%/5.00%/-1.50%, all of which divide cleanly, so all 90 assertions passed untouched — the one affected document in this repo is examples/Parkview-Apts-Glendale-AZ.uw.md.

Pre-existing receipts degrade to unverifiable, not failed. New RCP-10 fires when digests disagree and the canonicalization version disagrees, so a stale receipt is reported as undecidable rather than as evidence the record was tampered with. Re-issue receipts over affected documents.

Added ​

  • RCP-10 (UW_RECEIPT_v1.md §5.5) — digest mismatch is no longer decisive when the canonicalization version also differs. Same carve-out shape as the §5.3 engine-identity rule, one step earlier in the precedence.
  • UW_LITE_CANONICALIZATION_VERSION and UWX_CANONICALIZATION_VERSION exported from @uwmd/core (and @uwmd/core/browser).
  • Conformance scenario receipts/verify/06-lite-canonicalization-superseded — a 5.51% Lite deal with a receipt stamped 1.0 carrying the genuine pre-fix digest, expecting unverifiable + RCP-10. Corpus: 158 → 159 assertions.

Fixed ​

  • Lite percent normalization loses decimal exactness — lite.ts now shifts the decimal point through the digit string rather than dividing, so the value matches the double a hand-authored UWX fraction produces.
  • Lite issuance conflated two version fields — canonicalization_version was stamped from UW_LITE_REPRESENTATION_VERSION. UWX already kept them separate, so Lite was the odd one out; had this not been split first, bumping the canonicalization version would have falsely claimed the Lite grammar changed.

Decided ​

  • Lite canonicalization 1.0 and legacy .uw.md sniffing now have an expiry: both sunset at Protocol 2.0. RFC 0017 introduced legacy sniffing as a transition with no end date and RFC 0020 declined to set one; RFC 0025 would have opened a second such transition. Both are 1.x compatibility bridges, so they share one boundary rather than accumulating separately. For all of 1.x a verifier MUST keep recognizing canonicalization_version: "1.0" and degrade to RCP-10; at Protocol 2.0 that obligation ends.

Not released — superseded by 1.5.0 ​

1.4.0 was prepared but never published. The manifests, CORE_VERSION, and this entry were all written for it, but the v1.4.0 tag was never pushed, and release.yml triggers on v* only — so the publish job never ran. npm went from 1.3.0 straight to 1.5.0 and no 1.4.0 of any package exists. Nothing was broken for installers: every published package pins @uwmd/core 1.3.0, which does exist. Everything below shipped in 1.5.0 instead.

The version numbers are burned rather than reused, because @uwmd/excel 0.3.0, @uwmd/report 0.3.0, and @uwmd/batch 0.2.0 were published — at 1.3.0 pins. Republishing those numbers carrying a 1.5.0 pin is not something npm allows, so the dependents move to 0.4.0/0.4.0/0.3.0.

No guard covered this. verify-versions compares VERSIONS.md to the manifests and verify-lockfile compares pins to declared versions; all three agreed at 1.4.0, and agreeing on a version nobody published is exactly the state they were built to permit. verify-release (below) is the missing check.

  • Prepared as @uwmd/core 1.4.0, @uwmd/cli 1.4.0 (lockstep), with coordinated @uwmd/core repins in @uwmd/excel 0.3.0, @uwmd/report 0.3.0, and @uwmd/batch 0.2.0. Minor rather than major: the change corrects an IEEE 754 artifact to match what UW_LITE_SPEC_v1.md §4 already specified, the grammar is unchanged so no document needs editing, and RCP-10 keeps legacy receipts reporting unverifiable instead of failing.

1.3.0 - 2026-08-15 ​

⚠️ Read before upgrading — irr can now refuse where it used to answer ​

RFC 0024 makes the irr search procedure normative (protocol §VIII.3), and the reference implementation changes to match. Two inputs that returned a number now raise CALC-IRR-DIVERGE:

  1. A root above 1000% or below -99.9%. irr(-1, 20) returned 18.999999999994728 — a 1900% return, from a search the spec documents as reaching 1000%. The old Newton pass was free to converge outside the bracket it claimed to search. There is no correct code depending on this: reading 19.0 out of it means relying on the spec being wrong.
  2. A cash flow with an even number of roots in the bracket.irr(-100, 230, -132) returned 0.1, though 0.2 zeroes the same NPV equally well — the answer was an artifact of Newton's 0.1 seed. Such a cash flow has no single internal rate of return, and the engine now says so instead of picking one.

Conventional cash flows are unaffected, which is the load-bearing claim here. One sign change means a unique root, and bisection agrees with the old Newton pass to ~5e-13 — orders of magnitude below the six-decimal quantum §VIII.5 reports a rate at. No pack metric uses irr, so no built-in calculation on any asset class moves.

Receipts issued before this release over a document whose IRR changes verify as unverifiable via RCP-07, not failed — the same path RFC 0023 used.

Changed ​

  • irr is pinned to bracket-then-bisect — protocol 1.3.0 → 1.4.0. (RFC 0024) §VIII.3 gains a normative six-step procedure: bracket over [-0.999, 10.0], return an exact endpoint root, bisect to |npv| < 1e-9 or a half-interval under 1e-12 capped at 200 iterations, and no Newton polish. Bisection is the part that is bit-reproducible — every step is (lo + hi) / 2 and a comparison of products, which IEEE 754 requires to be correctly rounded, in an order the spec now fixes. Newton's iterates depend on the association order of a derivative sum no document pins. Under VERSIONS.md rule 2 this is a monotonic strengthening of requirements, so a minor bump.

  • pmt, fv, pv, and nper MUST be closed-form (§VIII.3). nper is solved iteratively in some formulations, and an implementation that does so inherits the reproducibility problem the irr procedure exists to remove. irr is now the only builtin permitted to iterate.

  • RFC 0024 accepted and implemented (iterative-function determinism). Three checks ran before acceptance: the iteration audit the RFC asked for came back clean (irr is the only builtin that converges; nper is closed-form), and no built-in pack declares an irr metric — which narrows the exposure to third-party modules and makes the Excel-parity question the draft called blocking unreachable today.

    Implementation then found two of the RFC's five stated fixtures to be impossible under its own normative procedure, and the RFC now records both as errata. Fixture 02 expected a multi-root cash flow to "return the bisection root, pinned exactly": it cannot: an even number of roots means npv shares a sign at both endpoints, so no bracket exists and step 2 raises. Fixture 04 expected a root exactly at -0.999 to be found: 1.0 + (-0.999) is 0.001000000000000001 in binary64, so that root is not a well-defined quantity at the low endpoint. Both are now specified as they actually behave.

  • @uwmd/core 1.2.0 → 1.3.0. A minor, and the call deserves stating, because irr can now throw where it returned a number — normally a major. The behavior being removed is a value returned from outside the domain the specification claims to search, so there is no correct code depending on it: reading 19.0 out of irr(-1, 20) is relying on the spec being wrong. A deprecation window would have preserved the divergence between engines for the length of the window, which is the thing RFC 0024 exists to close. Callers who do depend on the old value should pin 1.2.x and read §VIII.3.

  • @uwmd/cli 1.2.0 → 1.3.0. calc, receipt, and summary over a module declaring an irr calculation can now report an error where they reported a number. No flag, command, or output shape changes.

  • @uwmd/excel 0.2.0 → 0.3.0, @uwmd/report 0.2.0 → 0.3.0, and @uwmd/batch 0.1.0 → 0.2.0 as coordinated repins only — none of their output changes. Excel maps irr → IRR but no pack declares an irr metric, so no emitted workbook formula is affected; report.ts reads stored section data and never evaluates; batch digests document inputs, not calc outputs. All four dependents pin @uwmd/core exactly, so all four had to move: a republished 0.2.0 carrying a different pin is not a thing npm allows, and leaving them behind would mean the repin never ships. @uwmd/batch was held back at the 1.2.0 cut for the same reasoning that does not apply here — that release did not change its pin.

  • Security reports now go to [email protected] (SECURITY.md), replacing the general [email protected] address. A dedicated alias keeps a vulnerability report off the same triage path as ordinary project mail. The 1.0.0 entry below still names the old address; it is a record of what was true then.

Fixed ​

  • RCP-07 now compares engine identity, not the version string alone.verifyReceipt treated a receipt as same-engine whenever the version matched, so a disagreement between two different engines that happened to share a version number was reported as failed — blaming the record for a cross-engine difference, which is exactly what RCP-07 exists to prevent. Identity is now the pair (engine, engine_version), and ReceiptVerificationOptions gains an optional engine alongside engine_version so a third-party verifier can declare its own name. The issue's expected/actual now carry engine@version rather than bare versions. Receipt spec §5.3 restated to match (UW_RECEIPT_v1.md).

1.2.0 - 2026-08-15 ​

⚠️ Read before upgrading — computed numbers change ​

RFC 0023 quantizes every reported calc value (protocol §VIII.5). Three consequences a caller can observe:

  1. round(num, dec) returns different answers at exact-half boundaries.round(1.005, 2) was 1.00 and is now 1.01. §VIII.3 always specified half-away-from-zero, so this is errata rather than a redefinition — but a module whose formulas were tuned against the old behavior will move.
  2. Every derived metric is quantized. DSCR reports 4 decimals, dollars 2, rates 6. If you diff CLI output, receipt values, or workbook cells against stored expectations, they will differ in the tail.
  3. Receipts issued before this release no longer verify as verified. They return unverifiable via RCP-07, not failed — the disagreement is attributed to the engine change, not to your record. Re-issue to get a clean verified.

Protocol moves 1.2.0 → 1.3.0 accordingly: §VIII.5 adds normative MUST requirements, which under VERSIONS.md rule 2 is a minor bump.

Added ​

  • RFC 0023 implemented — a numeric model and a single quantization boundary (protocol §VIII.5). The spec now states how precise a number is. Evaluation runs in unrounded IEEE 754 binary64; a calculation's reported value is quantized half away from zero at exactly one place, evaluateCalc(), to its effective decimal places. ModuleCalcDecl gains an optional round_to (integer [0, 12]), defaulted from unit by a normative table ($→2, %→6, x→4, otherwise 6) that is deliberately total — an unspecified precision is an unspecified interoperability contract.

    New calc/quantize.ts exports quantizeDecimal, resolveRoundTo, MAX_ROUND_TO, DEFAULT_ROUND_TO, and DEFAULT_ROUND_TO_BY_UNIT from both index.ts and browser.ts. CalcResult echoes the effective round_to. New loader code PROTO-MOD-067 refuses a malformed one. Corpus 147 → 153.

Fixed ​

  • The §VIII.3 irr convergence note misdescribed the implementation it documents. It said the engine brackets on [-0.999, 10.0] and refines with Newton; calc/builtins.ts runs Newton first from a seed of 0.1, capped at 100 iterations, and uses the bracket only as a fallback. The difference is not academic: the bracket therefore constrains no answer, and irr(-1, 20) returns ≈19.0 — a 1900% return out of a search documented as reaching 1000% — where an implementer who bracketed first, as the note described, would raise CALC-IRR-DIVERGE. The note now describes the code and states that consequence outright, along with the seed-dependent root selection (irr(-100, 230, -132) → 0.1, with 0.2 equally a root). Still non-normative; no engine behavior changes in this release. Pinning the algorithm is proposed in RFC 0024 (draft).
  • A receipt could report a clean record as corrupted. receipts.ts ran two checks over the same numbers that could not both be right: RECEIPT_RESULT_TOLERANCE compared stated against recomputed at 1e-6, while results_digest hashed those same raw doubles bit-exactly. A last-ULP difference passed the tolerant check and failed the exact one, surfacing as RCP-04 — corruption. Quantized results leave no tail for the two to disagree about. Receipts issued before this degrade to unverifiable rather than failed, via the existing RCP-07 engine-version rule.
  • round(num, dec) diverged from its own documented contract. It scaled by 10 ** dec, which reintroduces the artifact it exists to remove: 1.005 * 100 is 100.49999999999999, so round(1.005, 2) returned 1.00 where spreadsheet ROUND returns 1.01. §VIII.3 already specified half-away-from-zero, so this is errata, not a behavior change.

Changed ​

  • Excel↔evaluator parity is now asserted as exact equality, replacing agreement to six decimals, across all nine asset classes. Formulas are emitted through the new emitCalcExcelFormula, which wraps them in ROUND(expr, round_to) so the workbook cell quantizes exactly as evaluateCalc does. emitExcelFormula still emits the bare expression; converters should prefer the new function.
  • @uwmd/core 1.1.2 → 1.2.0. The bump is load-bearing: it is what makes RCP-07 classify a pre-quantization receipt as indeterminate rather than failed.
  • @uwmd/cli 1.1.3 → 1.2.0 and @uwmd/excel 0.1.0 → 0.2.0 emit different bytes than before — the CLI's calc, receipt, and summary output carries quantized values, and every workbook formula is now ROUND-wrapped — so a patch bump would have understated the change. Neither removes or renames a flag, an export, or a sheet, so neither is breaking.
  • @uwmd/report 0.1.0 → 0.2.0 as a coordinated repin only. Its output is unchanged: report.ts reads every figure out of stored section data with deepGet and never calls evaluateCalc or resolves a pack, so quantization does not reach it. The bump keeps the workspace on one core version rather than recording a behavior change. Worth knowing for a different reason — a credit memo quotes DSCR and LTV that nothing in the pipeline recomputes, so those figures are only as good as whatever wrote them into the document.
  • @uwmd/batch is deliberately left at 0.1.0: its semantic digests cover document inputs, not calc outputs.
  • The four packages that depend on @uwmd/core pin it exactly, so all were repinned to 1.2.0; nothing depends on the bumped packages themselves.
  • VERSIONS.md's "current matrix" gained rows for @uwmd/report and @uwmd/batch, which it had omitted while describing itself as current.

Added ​

  • RFC 0018 implemented — document profiles and deal packages. The edge registry landed first, per the ordering constraint: BUILTIN_EDGE_TYPES in protocol.ts is the single canonical two-layer vocabulary, with guarantees and supports declared valid on both layers rather than duplicated. Unknown types are preserved; a known type used on the wrong layer is refused, because that is a claim the registry says cannot be true.

    Three document profiles are registered, and an unknown profile is preserved rather than reinterpreted as a deal.

    Lease abstracts (lease-abstract.ts) enforce the two rules the profile exists for: every asserted term carries a source_ref with a locator, and a null term must state why it is null. projectLeaseAbstractToRentRoll refuses to project anything but an executed lease, never computes a figure it was not given, and separates ambiguous conflicts from plain omissions.

    Deal packages (deal-package.ts, -zip.ts, -context.ts) implement manifest validation, a deterministic .uwpkg.zip codec, three-state verification keeping unverifiable distinct from failed, and the JSON context view whose central rule is that source-evidence bytes are never inlined — only described by identity and digest. projectPackageLinksToEntityEdges synthesizes provenance naming the package and member ids; there is deliberately no inverse.

    New CLI: uwmd lease validate|project, uwmd package create|verify|list|to-context|validate-context|edges. New schema: uw-deal-package-manifest.schema.json. New conformance suite: conformance/packages/ (18 assertions).

  • npm run verify-lockfile, gating CI, asserts that every @uwmd/* entry in all three lockfiles is a workspace link rather than a registry tarball, and that each cross-package pin names the version its workspace actually declares. The failure it guards against is silent in the worst way: a lockfile that resolved @uwmd/core from the registry would let the whole suite pass against the last published library, so green CI would mean "the previous release still works" rather than "this commit does." The pin check catches the other half — a dependent left pinned to a version the workspace has moved past, which npm resolves locally, so nothing notices until publish.

Fixed ​

  • CI ran no jobs at all on a stacked pull request. ci.yml's pull_request.branches: [main] filter matches the base of a PR, not its head, so a PR opened against another feature branch triggered nothing. The failure had the same shape as the one above: it did not fail, it reported nothing, and an absent check list sits next to whatever external checks do run and reads as green. The filter is removed — every pull request gets the suite, whatever it targets. The push trigger stays pinned to main, so branch pushes still do not double up with the PR run.

  • Document-authored keys and paths could reach the JS prototype chain. Path navigation walked with a bare property lookup, so a Tier-3 formula segment or a deepGet path naming __proto__, constructor, or prototype resolved onto internals the sandbox exists to keep out of reach — MAX_NODES and the grammar bound what a formula can do, but bounded nothing about what it could name. Navigation is now own-property-only and refuses those three segments, resolving them to null/undefined like any missing path (getPathSegment in parser.ts, shared by deepGet and the Tier-3 evaluator). An array's own length still resolves; inherited members no longer do.

    The matching write paths are closed too: frontmatter and fence-annotation keys matching those three names are parsed and then dropped rather than assigned (dropping the key line alone would have left its indented block to be read as top-level keys); UW Lite reports LITE_FRONTMATTER_KEY_RESERVED / LITE_ATTRIBUTE_KEY_RESERVED; and the Lite bridge rejects a mapping whose target_path contains one, as LITE_MAPPING_INVALID.

    XML decoding is the exception that could not simply reject: UW_XML_MAPPING_v1 §4 forbids key sanitization and requires a uw:member name to be restored verbatim. Top-level members are now restored with Object.defineProperty, so a member named __proto__ round-trips as an own data property instead of invoking the setter and mutating the envelope's prototype. Its duplicate check moved from in to Object.hasOwn, which had reported inherited names as duplicates.

    No spec text changes: every blocked segment resolves to the value protocol §VIII.2 already assigns to a missing path.

  • sha256TextHex could not correctly digest binary content. It encodes its argument with TextEncoder, so hashing bytes round-tripped through a latin1 string re-encoded every byte above 0x7F as multi-byte UTF-8 and digested different bytes than the ones on disk. Harmless while everything hashed was text; a real defect the moment a package carries a PDF. Added sha256BytesHex, and a conformance invariant asserts a binary member survives a package round trip byte for byte.

Changed ​

  • @anthropic-ai/sdk is now an optional peer dependency of @uwmd/core, not a runtime dependency. A parser, a validator, and a calc engine should not oblige anyone to install a vendor SDK. The Tier-4 host was already provider-neutral in design — bancroft.ts reaches the default provider by dynamic import — but index.ts re-exports createAnthropicProvider statically, and that module imported the SDK statically, so import '@uwmd/core' loaded the vendor SDK for every consumer regardless. The import is now dynamic and deferred to the first request; the SDK's types are reached by an erased import type.

    createAnthropicProvider() keeps its signature and stays synchronous — client construction moved behind the first complete()/stream() call, so an invalid API key now surfaces on that call rather than at construction. A consumer who has not installed the SDK gets a typed AGENT_PROVIDER_SDK_MISSING naming the fix, instead of a module-resolution stack trace; AgentProviderError's code union widens by that one member, and transport wrapping no longer relabels it. Hosts that supply their own provider, and everything Tier-1 through Tier-3, need no change.

    Requires a @uwmd/core minor bump at release: the calling shape is unchanged, but what npm install puts on disk is not, so a patch would understate it.

  • Safe-ZIP inspection extracted to zip-safety.ts. RFC 0018 §3 requires deal packages to apply the restrictions the CSV bundle already established; reimplementing them would have produced two subtly different copies of security-relevant code. Callers map a semantic violation to their own code, so the CSV bundle's CSV_* codes are reproduced exactly and its tests pass unchanged.

Governance ​

  • RFC 0021 and RFC 0022 accepted (2026-08-13). With 0018, the arc 0018 → 0021 → 0022 is approved end to end.

    0021 — composable UWX documents. Section externalization into .uwpart.md fragments, recursive composites, shared assumptions inherited along the composition DAG, and rollup receipts. The design rests on one invariant: an externalized record and its inline equivalent have the same semantic digest, so composition is packaging rather than modelling. Rollup receipts deliberately avoid the wall RFC 0019 hit — the Tier-3 sandbox has no iteration, so a composite states aggregates and the receipt verifier recomputes them over named child digests using a fixed, non-extensible fn vocabulary. No calc engine change.

    0022 — market data as an attributable UW document. market-data-v1 with required as_of, provider, and basis; receipts pin the observation set's semantic digest, with an absent set reported unverifiable rather than failed; and promotion to an input of record is explicit and tagged market_data_accepted rather than being rewritten to user_input, so a value accepted for lack of evidence stays distinguishable from one established by diligence.

    Shared receipt surface. Both amend the receipt format RFC 0016 owns — 0021 adds rollup verification, 0022 adds inputs_provenance. The amendments are additive and do not collide, and both RFCs now carry the ownership rule 0018 §5 established for the edge registry: the amendments live in UW_RECEIPT_v1.md and uw-receipt.schema.json, whichever is implemented first establishes the extension section, and receipt_version bumps once to cover both rather than twice for two simultaneously-accepted RFCs.

    No code ships against either yet.

  • RFC 0018 accepted (2026-08-13) — document profiles and deal packages. Approves lease-abstract-v1 and source-note-v1, the UW Deal Package (.uwpkg.zip with a manifest carrying per-member sha256 and semantic_digest), the source-file-free connector JSON context view, and the canonical two-layer edge registry. Both of its previously blocking questions were resolved in the text before acceptance; the remaining open items (legal vocabulary, amendment consolidation, package signing, OCR profile) are explicitly deferred rather than blocking. No code has shipped against it yet.

    Acceptance settled edge-registry ownership under 0018's own §5 rule: whichever of 0018/0015 was accepted first owns the registry, so the canonical entity-plus-member edge vocabulary now belongs to the protocol spec as 0018 defines it, and RFC 0015's edge list is superseded by that section. A later acceptance of 0015 amends that section rather than starting a second table — which was the entire point of writing §5.

    This unblocks RFC 0021 (composition) and RFC 0022 (market data) architecturally; both still require their own acceptance.

Added ​

  • Module loader hardening, and a conformance suite that keeps it honest (T13).modules.ts validated calculations and validations and stopped — sections, view_models, and agent_layers were constrained by spec/schemas/module-manifest.schema.json and validated nowhere. Running identical manifests through ajv and the loader disagreed on seven of eight probes. agent_layers was the one that mattered most: it carries prompt_template, making it Tier-4 prompt surface, and it accepted prompt_template: 42.

    The loader is now schema-complete — all three constructs validated, unknown keys refused at every level (additionalProperties: false), depends_on shape-checked, length bounds enforced — with pointer-level error codes (PROTO-MOD-033 … PROTO-MOD-066).

    New conformance/modules/ suite (22 assertions, runs by default, no network): every fixture is checked against both the loader and ajv, and the suite fails when the two disagree. @uwmd/core cannot depend on a JSON Schema validator under the layering invariant, so the hand-written mirror needed an external referee. Two deliberate divergences — requiring deterministic: true, and parsing the safe-expression grammar, neither expressible in JSON Schema — are declared per fixture with a stated reason; the reverse direction has no opt-out.

    New uwmd modules validate|list for module authors, reporting the exact failing declaration rather than a bare refusal.

  • A UW Lite worked example — the first one the repo has ever shipped (T11).spec/UW_LITE_SPEC_v1.md has specified UW Lite normatively since RFC 0017 while examples/ contained zero instances of it, which RFC 0020 identified as a plausible root cause of the Lite/UWX drift it had to go back and correct. examples/Parkview-Apts-Glendale-AZ.uw.md closes the gap deliberately as a twin of the existing record: same deal, same deal id, same base name, both extensions adjacent in the directory listing, so the distinction is visible before either file is opened. The numbers are the record's own — including the underwritten NOI of $396,635 rather than the $412,096 trailing-twelve actual, a distinction the example calls out because it is exactly what a bare spreadsheet cell leaves ambiguous.

    The example states no DSCR and no LTV. Both are derived, and the prose says so and quotes what the multifamily pack computes (DSCR 1.1091, LTV 0.7000, cap rate 0.0551, debt yield 0.0787). Those four values were evaluated through the pack, not asserted, and three CLI smoke tests now pin them — an example whose prose quotes numbers nothing checks is an example that rots. Projecting the complete record back down to Lite reports 7 projected paths against 1,215 omitted, which is the most concrete statement of the split available.

Fixed ​

  • Duplicate module ids silently shadowed each other. Two manifests sharing an id both loaded and byId returned whichever came last, so a registry lookup resolved to the wrong module. Now refused with PROTO-MOD-066.
  • A typo'd construct name discarded a module's work in silence.calculationz: [...] loaded clean and contributed nothing, with no error.
  • requires_protocol: "^1" was silently unsatisfiable. Only X.Y was padded to full semver, so a bare major failed to parse and every range containing one failed to match — while the schema's own description documents ^1 as valid.
  • A malformed depends_on misreported itself. depends_on: ["other"] was iterated character by character and surfaced as Missing module dependency: undefined.
  • CI never ran the Tier-4 replay suite on pull requests. ci.yml pinned --tier=1,2,3,lite,receipts, so the 4-replay suite added in T10 ran only in release.yml. Both CI steps now call npm run conformance and take the runner's default list, so adding a suite to the default is enough to gate PRs.

Changed ​

  • Errata: module-manifest.schema.json was missing the scope deal stage.DealStage in types.ts has carried scope since the v1.1 train, so a manifest the loader accepted was invalid against the normative schema. Treated as errata rather than an RFC because it aligns a stale mirror to an already-accepted decision rather than making a new one.
  • uwmd init wrote structured UWX content to a .uw.md file.generateBlankUWFile() emits uw_version and fenced uw:section= blocks, but the default output filename was new-deal.uw.md (or <name>.uw.md), so the CLI produced exactly the file the format spec forbids and detectUWSourceRepresentation() flags as legacy on the next load — every deal a newcomer scaffolded was born legacy. Now .uwx.md.
  • uwmd export and uwmd report appended to a .uwx.md name instead of replacing it. replaceUWExtension() did not list .uwx.md, so a UWX input fell through to the append branch: uwmd export deal.uwx.md wrote deal.uwx.md.uw.json rather than deal.uw.json, and report likewise emitted deal.uwx.md.report.html. (.uw.md never matched a .uwx.md filename — the suffixes genuinely differ — so it failed silently rather than mis-matching.) report now routes through the same helper instead of its own inline check, so every UW source extension is handled in one place. Legacy .uw.md inputs resolve exactly as before.
  • These three had no test coverage at all, which is how they survived the .uwx.md migration. Five CLI smoke tests now pin the default output paths, including a regression case for legacy .uw.md input.

Changed ​

  • The format spec now describes .uwx.md, the extension it has specified since RFC 0017. spec/UW_FORMAT_SPEC_v1.md still called itself the .uw.md specification and opened by declaring a .uw.md file "the canonical, lossless" record — the exact claim accepted RFC 0017 moved to .uwx.md when it split UW Lite from UWX. It contained zero occurrences of .uwx.md. Since the docs site renders the spec directly, uwmd.org repeated the error to every visitor. Retitled, realigned throughout, and given a Naming section fixing the vocabulary: UW Markdown is the standard, .uwx.md is the lossless record, .uw.md is UW Lite — a current, deliberately lossy view, not a superseded predecessor, which is where the .doc/.docx analogy stops. Same alignment applied to the protocol, XML, and CSV specs and to README.md. No behaviour change; the library already emitted .uwx.md. See RFC 0020.
  • The ten worked examples were renamed to .uwx.md. Every file in examples/ was a structured record carrying the legacy extension, so the project's own detector flagged each one — "Structured UWX content uses the legacy .uw.md extension; migrate it to .uwx.md." The examples are the most copied artifact in the repo and were modelling deprecated usage. Content is byte-identical; 41 referencing files were updated by path only.

Added ​

  • The Tier-4 agent host is provider-neutral (T9). Protocol §IX has always described it that way, but agents/bancroft.ts constructed an Anthropic client on every run and typed its internals against the SDK — so the claim could not be demonstrated, and agents/ sat at 0% coverage because exercising it needed a network call and an API key. New AgentProvider contract (complete(), optional stream(), neutral request/completion types) with no vendor import; agents/providers/anthropic.ts is now the only file importing @anthropic-ai/sdk, reached through a dynamic import so a host bringing its own provider never loads it. BancroftRunOptions gains provider and apiKey becomes optional — additive; existing callers are unaffected. Streaming falls back to complete() for providers that cannot stream.
  • Recorded-replay Tier-4 conformance (T10). Tier-4 was shape-and-lint only: fixtures were parsed and nothing ran, so nothing checked that a host writes what the protocol says it writes. The l6-risk-rating fixture was in fact un-runnable — it carries only a property section while L6 requires noi_model, valuation, and debt_structure. New createRecordingProvider / createReplayProvider plus a 4-replay conformance suite that replays a cassette through the real runner and compares the document byte for byte. It runs in CI by default because it needs no network and no key. Replay is strict — a request that no longer matches its recording is a typed error naming the changed field — so a cassette doubles as a prompt-drift detector. What it does not prove, and says so in its README, is that any particular model produces the recorded answer.
  • RunOptions.timestamp / RunOptions.logEntryId and BancroftRunOptions.now. Agent output was previously non-reproducible: _meta.timestamp, duration_ms, and a random log-entry suffix differed every run, which is why replay could not assert on a document at all. A constant clock now freezes all three. Real runs leave now unset and keep the random suffix that stops same-millisecond collisions.
  • ASSET_CLASSES — the AssetClass union as a runtime list, exported from @uwmd/core and @uwmd/core/browser. It is derived from a Record<AssetClass, true> exhaustiveness anchor rather than written by hand, so adding a member to the union without updating the list is a compile error (TS2741) instead of silent drift. modules.ts's private duplicate list was deleted in favour of it. Additive; no signature changes.
  • types.test.ts — a drift guard over the asset-class registries. Holds the calc-pack registry and the defaults registry to ASSET_CLASSES, checks each pack declares the class it is registered under and each defaults table stamps its own class, and pins the DEFAULT_THRESHOLDS bands (warning always fires before error). mixed_use is carried as one documented exception that fails loudly the moment its pack lands, so the allowance cannot outlive its reason. A matching guard in the web editor's catalog.test.ts keeps its own asset-class list in step with the format.

Changed ​

  • The @uwmd/core coverage floor was ratcheted to just under measured. Thresholds had sat at 70 lines / 70 statements / 90 functions / 70 branches while actual coverage was ~77 / ~77 / 96.7 / ~75.5 — five to seven points of headroom in which coverage could erode without CI noticing. Raised to 76 / 76 / 95 / 74. The gate itself was already blocking: continue-on-error was removed from the coverage job in 13218c4, contrary to the internal status doc, which still described it as a soft floor. Verified the gate actually fails by breaching it deliberately (exit 1) rather than assuming it. The remaining ~1 point of margin is sized against real jitter — calc.property.test.ts runs fast-check with no fixed seed, so coverage varies by ~0.2 points between identical runs and a floor set flush to a measured figure would fail at random.
  • "No pack registered" negative tests no longer borrow a real asset class. Five tests — in cascade.test.ts, defaults.test.ts, toWorkbook.test.ts, receipts.test.ts, and the conformance/receipts/refuse/02-no-pack-for-asset-class fixture — asserted that an unregistered class resolves to no pack, no defaults, and no Excel layout by pointing at whichever real AssetClass member happened to still be unregistered. Every new pack therefore broke them and had to shuffle the role onward (land → hospitality → senior_housing → student_housing → mixed_use). With mixed_use the last one left, the next pack had nowhere to shuffle to. All five now anchor on the synthetic identifier __unregistered_test_class__, which is deliberately not a member of the AssetClass union and must never become one. No test or fixture references mixed_use any more, so the final pack can be written without touching them. Behaviour is unchanged — the public API already accepted string, so no casts or signature changes were needed.
  • Canonical repository moved to the project organization — every source, download, example, raw-content, and clone URL now points at github.com/UWMD-OSP/UW-Markdown. Package repository metadata, the docs-site nav/footer/edit links, the llms.txt corpus, and the AI guide were all updated in lockstep. Security reports now go to [email protected].

Fixed ​

  • Two accepted RFCs were unreachable from the docs-site nav. RFC 0016 (verification receipts) and RFC 0017 (the Lite/Extended source split) were copied into the site by prebuild.mjs and rendered, but the sidebar's RFC list stopped at 0015 — so two accepted, shipped RFCs could only be found by guessing the URL. Added both, plus 0018.
  • land was the canonical "no pack registered" example and stopped being one. Registering LAND_PACK broke receipts.test.ts and the conformance/receipts/refuse/02-no-pack-for-asset-class fixture, both of which used asset_class: land to exercise RCP_PACK_UNRESOLVED. Both moved to mixed_use — since superseded by the synthetic-identifier change below, which ends the shuffle for good.
  • The hospitality defaults table shipped without its invariant tests. Every asset-class defaults table has a dedicated describe block in defaults.test.ts asserting low <= central <= high, the asset_class_default source stamp, a citation and unit on every entry, and the expected field set — except HOSPITALITY_DEFAULTS, which was added without one and was therefore covered only by the registry-lookup tests. Blocks added for both hospitality and senior housing, plus two cross-field checks the older tables do not make: that the occupancy and vacancy bands are complementary, and that senior housing's labor band sits inside its expense band.
  • npm run lint was red locally but green in CI. Biome linted tools/docs-site/public/editor/, the built web-editor bundle vendored into the docs site, producing 7,267 diagnostics from one generated file. The directory is gitignored, so a fresh CI checkout never saw it and CI passed; anyone running the lint script locally got a wall of noise from a file they had not written. Added to the Biome ignore list.
  • @uwmd/excel and @uwmd/report were building against published core 1.1.0, not the workspace. Both pinned "@uwmd/core": "1.1.0" exactly while the workspace core was at 1.1.2, so npm could not satisfy the pin from the workspace and installed the published 1.1.0 into each package's nested node_modules, shadowing the workspace link. Every build and test in those two packages — including the Excel↔calc-engine parity invariant, whose whole point is that one pack drives both — was silently verified against a different core than the repo's. It went unnoticed because 1.1.0 happened to export everything they referenced. Both pins now track the workspace version.
  • VS Code extension silently passed UW Lite files. It ran the structured parser over every .uw.md; post-RFC-0017 that extension means UW Lite, where the structured parser finds no fenced sections — so it reported zero issues and a clean status for a document nothing had parsed. The parser is now chosen from the content via detectUWSourceRepresentation: Lite documents get LITE_* parse errors plus LITE_COMPILE_* bridge errors, UWX documents get the full structured validator. Diagnostics are also anchored to the line they concern instead of all being pinned to line 1. Financial thresholds remain unavailable for Lite by construction — checkFinancialValidity reads frontmatter.quick_metrics, which the deal-summary bridge does not populate; a toggle for it was prototyped and removed because it did nothing. Extension bumped to 0.2.0.

Added ​

  • Land calc pack — the ninth asset class, and the first that is not an income property. LAND_PACK adds twelve deterministic metrics and deliberately omits cap_rate, dscr, and debt_yield. Land has no stabilized income: its noi_model is a carry model (taxes, CFD assessments, insurance, site security against incidental interim revenue), so NOI is normally negative. Capitalizing it would emit a "−1.6% cap rate" that reads as a yield when it is a carry burden — confidently wrong output, worse than none. Tests pin the omission and assert no land formula reads net_operating_income at all. Land is underwritten instead on basis and density: price_per_buildable_unit, price_per_usable_acre, usable_land_ratio, basis_per_buildable_unit, carry_ratio, and land_to_sellout_ratio. LAND_DEFAULTS follows the same logic and publishes no rent_roll.*, expense-ratio, or exit-cap entry, with tests asserting those absences and that land's LTV band sits at or below every income class. Ships with the Sundance-Ranch-Land-Buckeye-AZ.uwx.md worked example (160 acres, 552 platted lots, $30k/lot) and a LAND_LAYOUT workbook layout whose operating statement is a carry statement that nets negative.
  • Student-housing calc pack — the eighth asset class end-to-end.STUDENT_HOUSING_PACK adds fourteen deterministic metrics. The class looks like multifamily but is not underwritten like it: leases are signed per bed, so every sizing and occupancy metric keys off property.total_beds (price_per_bed, loan_per_bed, noi_per_bed, revenue_per_bed, rent_per_bed_monthly, bed-count occupancy), and a test asserts no metric reads property.total_units. The defining metric is pre_lease_rate — the share of beds committed for the coming academic year — because student housing re-leases its entire rent roll on one date, making pre-lease velocity the leading revenue indicator. preleased_beds and occupied_beds are separate stored counts measured on different dates, never derived from each other; a test pins that they differ. Shipped with STUDENT_HOUSING_DEFAULTS (14 triage-grade ranges including pre-lease rate and per-bed turnover cost, with reserves quoted per bed rather than per unit), the Mill-Ave-Commons-Student-Tempe-AZ.uwx.md worked example (600 beds; 5.75% cap, 1.30x DSCR, and honest negative leverage at 3.0% cash-on-cash), and a STUDENT_HOUSING_LAYOUT workbook layout carrying turnover/make-ready as its own expense row.
  • Senior-housing calc pack — the seventh asset class end-to-end.SENIOR_HOUSING_PACK adds fourteen deterministic metrics. Sizing is per unit as in multifamily; the class-distinctive three are revpor (revenue per occupied unit per month), labor_ratio, and care_revenue_ratio — the numbers that say whether the operator, not the real estate, is carrying the deal. total_labor_expense is a model-level subtotal rather than an entry inside noi_model.expenses, so the three labor lines stay in the expense map and the operating statement still foots without double counting; the test asserts both halves. Shipped with SENIOR_HOUSING_DEFAULTS (15 triage-grade ranges, including a defaulted labor ratio and wage-growth band), the Ocotillo-Senior-Living-Chandler-AZ.uwx.md worked example, and a SENIOR_HOUSING_LAYOUT workbook layout whose income lines carry a signed vacancy-loss row.
  • Hospitality calc pack — the sixth asset class end-to-end.HOSPITALITY_PACK adds fourteen deterministic metrics keyed off keys (price_per_key, loan_per_key, noi_per_key) alongside the shared cap-rate / LTV / LTC / DSCR / debt-yield core, plus the four metrics that make a hotel an operating business rather than a lease: occupancy, adr, revpar, and gop_margin. Hospitality has no lease-based rent roll, so rent_roll carries trailing-twelve room-night statistics (available room nights = keys × 365, occupied room nights from the STR report); noi_model is USALI-shaped, with a gross_operating_profit subtotal struck above the management fee, fixed charges, and the FF&E reserve. RevPAR = ADR × occupancy holds by construction — all three read the same primitives. Shipped with HOSPITALITY_DEFAULTS (14 triage-grade ranges, wider expense and cap-rate bands than the lease-based classes), the Saguaro-Select-Hotel-Tempe-AZ.uwx.md worked example, and a HOSPITALITY_LAYOUT workbook layout. getPackForAssetClass, getAssetClassDefaults, and getLayoutForAssetClass all resolve hospitality; Excel↔evaluator parity is pinned to 6 decimals in both packs/hospitality.test.ts and the converter's toWorkbook.test.ts.
  • .uwx.md registered in the VS Code extension — structured records on the new extension now get highlighting, folding, outline, and validation, where previously they got nothing at all. Structured content still on the legacy .uw.md extension is detected and nudged toward migration.
  • Receipt verification in the VS Code extension — a UW Markdown: Verify Receipt for This Deal command checks the <deal>.receipt.json sidecar against the open document. Verdicts map to notification severity (information / error / warning for verified / failed / unverifiable), with the full breakdown — pack, engine, policy set, digest, every stated result, and the §1 assurance boundary — in a UW Markdown Receipts output channel. Unsaved editor changes are offered as the likely cause of a failure. The verified notification states the boundary inline rather than showing a bare checkmark. The extension verifies but never issues: a receipt issued mid-authoring is stale on the next keystroke. Verification logic lives in a vscode-free module so it unit-tests under plain vitest with no editor harness, and a new VS Code extension (build + test) CI job typechecks, bundles, and runs it — the extension previously had no CI coverage at all.
  • Receipts on the docs-site — adds docs/UW_RECEIPTS.md, published at /guide/receipts: a human-facing explanation of what a receipt does and does not attest, how to issue and verify one, why verification has three outcomes rather than two (including the unverifiable exit code of 3), and why a receipt going stale after an edit is expected rather than alarming. Linked from the Tools nav and both guide sidebars, cross-referenced from docs/TOOLS.md (decision tree, CLI examples, web-editor entry) and a new docs/GLOSSARY.md entry. Also fixes a gap in the docs-site link rewriter: RFC links spelled rfcs/NNNN-slug.md — the correct GitHub-relative form from a file inside docs/ — previously failed the build's dead-link check, since only docs/rfcs/NNNN-slug.md and bare NNNN-slug.md were recognized.
  • Receipts in the web editor — a Receipt tab issues and verifies RFC 0016 receipts entirely client-side via @uwmd/core/browser; nothing is uploaded. Issuance runs the asset class's pack over the open deal and offers the .receipt.json sidecar; verification accepts a receipt file and continuously re-verifies against the in-editor document. The panel renders four states rather than three: core's verified / failed / unverifiable, plus a UI-level stale. A digest mismatch is reclassified as stale only when this session issued the receipt and the deal has since been edited — spec §6's "editors SHOULD treat any existing receipt as stale once a write lands" — while the same mismatch on a receipt that arrived as a file stays failed, so an ordinary edit never reads as tampering and tampering never reads as an edit. Per UW_RECEIPT_v1.md §1 no verdict is a bare checkmark: each states what it attests, and verified carries the "not correct, complete, audited, or approved" caveat inline rather than behind a disclosure. Web-editor suite goes 33 → 56 tests, including the stale-vs-failed distinction, an axe-core pass over the rendered verdict, and a test that forces the Web Crypto branch of sha256TextHex (jsdom leaks process, so the component tests would otherwise only ever exercise the Node path the browser never takes).
  • Verification receipts (RFC 0016) — a detached JSON document binding a canonical digest of an underwriting record to the deterministic outputs a named calc pack produced from it, so a party who did not run the calculation can confirm offline that the numbers follow from the inputs. Adds the normative spec/UW_RECEIPT_v1.md and spec/schemas/uw-receipt.schema.json; receipts.ts in @uwmd/core (exported from both index.ts and the browser entry, since unsigned issuance and verification need no cryptographic dependency); uwmd receipt issue and uwmd receipt verify; and a receipts conformance suite (--tier=receipts, 11 assertions) covering issuance, the four verification outcomes, and refusal. Verification is three-state — verified / failed / unverifiable — and never collapses "cannot decide" into either of the others. Two invariants are asserted without a baseline: re-issuance over an unmodified record reproduces the same digest and results, and every verification lands on exactly one of the three verdicts. Resolves RFC 0016's open question on engine-version mismatch in favour of unverifiable (RCP-07) rather than failed, so a document is never blamed for an engine upgrade. A receipt attests that stated outputs follow from stated inputs; it attests nothing about whether those inputs are true, and consumers MUST NOT render a verified verdict as an unqualified checkmark.
  • Local batch collection indexer — introduces @uwmd/batch, a deterministic directory runner that validates required deal envelopes, captures semantic digests, and emits JSON/CSV read models for database-adjacent underwriting workflows without changing the canonical .uw.md protocol.

[1.1.3] - 2026-08-04 ​

Fixed ​

  • Scoped CLI distribution — renames the npm package from the unpublishable uwmd name to @uwmd/cli; its installed executable remains uwmd.

[1.1.2] - 2026-08-04 ​

Fixed ​

  • Release lockfile completeness — regenerates the npm lockfile so clean CI installs include every package required by the published workspace manifests.

[1.1.1] - 2026-08-04 ​

Fixed ​

  • Coordinated npm release — publishes the uwmd CLI with its exact @uwmd/[email protected] dependency after the initial scoped-package bootstrap release.

[1.1.0] - 2026-08-04 ​

Added ​

  • UW Lite / UWX transition foundation (RFC 0017) adds the pre-launch .uw.md Lite and .uwx.md Extended split. (This entry originally cited "RFCs 0015 and 0016": 0015 belongs to the unrelated portfolio-relationships proposal, and 0016 did not exist. The split is governed by RFC 0017; receipts are defined by RFC 0016, both accepted 2026-08-09. Receipts are not implemented — the original wording overstated what shipped.) Core now exposes representation constants, content-aware source detection, the parseUWXFile compatibility name, and a byte-identical legacy migration planner. The uwmd migrate-source command safely copies a structured legacy .uw.md to a sibling .uwx.md, refuses Lite/mixed content and existing destinations by default, and supports dry-run reporting.
  • Deterministic UW Lite bridge compiles the deal-summary Lite profile into a UW Document Envelope/UWX source, preserves the complete human-readable source in a namespaced extension, and rejects unsupported periods, scenarios, or units instead of guessing. The reverse UWX-to-Lite projection emits a machine-readable omission report whenever advanced data is dropped. The CLI exposes both directions through convert, accepts Lite in export, and lists Lite/UWX in representation discovery. Receipt signing remains implementation work.
  • UW Lite Markdown 1.0 parser foundation adds the normative constrained grammar, lossless source-located AST, explicit anchored fields, normalized currency/rate/ratio values and units, duplicate/ambiguity diagnostics, presentation-insensitive financial canonicalization, canonical rendering, browser-safe public APIs, a conformance fixture, and CLI parse/validate support. Parsing remains separate from deterministic envelope compilation.
  • HTTP and MCP Binding 1.0 (RFC 0014 Phase E) — publishes stable https://uwmd.org/deals/{deal_id} resource identities, an OpenAPI 3.1 contract, negotiated HTTP responses with semantic ETags and preconditions, MCP text/blob resources, compact dual structured/text tool results, resource links, all five reference tool handlers, and a runnable SDK-neutral adapter example. Core binding tests cover JSON/XML/CSV transport, 304/406/412/415/428, resource variants, validation results, and Tier-2 source edits.
  • UW CSV Bundle 1.0 (RFC 0014 Phase D) — adds the normalized model-fidelity directory and deterministic ZIP codecs, manifest inventory and file hashes, semantic-digest verification, bounded extraction defenses, and all six named spreadsheet-safe view profiles. The shared registry and uwmd convert now round-trip Markdown, JSON, XML, and .uw.csv.zip representations.
  • UW XML 1.0 and cross-format conversion (RFC 0014 Phase C) — adds the deterministic uw-xml codec, namespace and normative mapping specification, structural XSD, semantic-digest verification, bounded secure parsing, and shared JSON/XML registry APIs. uwmd convert now converts .uw.md, verified .uw.json, and verified .uw.xml; round-trip, hostile-input, registry, and CLI integration tests cover the implementation.
  • UW Document Envelope 1.0 and UW JSON 1.0 (RFC 0014 Phase A) — adds the format-neutral UWDocumentEnvelope, a normative JSON Schema, one authoritative _meta and prose location per block, semantic canonicalization/digests, equivalence checks, CodecRegistry, the registered uw-json codec, verified parsing, and digested uwmd export output. Core and CLI round-trip, tampering, registry, and schema tests cover the new contract. XML, CSV, discovery, and HTTP/MCP companion profiles now ship in the same 1.1 release train.
  • Protocol 1.2 representation discovery — ImplementationManifest now advertises typed representation descriptors; its normative schema mirrors the addition. negotiateRepresentation implements Accept quality/specificity and fidelity filtering, resolveInputRepresentation resolves Content-Type, and uwmd formats exposes the live registry for API/MCP hosts.
  • Owner-led governance mode — the owner may accept RFCs and merge owner-authored work immediately while the project is solo. External pull requests require owner review; collaborative 14-day normative comment periods activate automatically after the first outside contribution merges.
  • Cross-platform release gates - CI now runs the complete build, workspace tests, and tiers 1-3 conformance suite on Windows/Node 20 in addition to the existing Ubuntu matrix. Core coverage is now a blocking gate at 70% lines, 70% statements, 70% branches, and 90% functions, based on a measured baseline of 72.45% / 72.45% / 74.25% / 91.92% respectively.
  • Repository line-ending policy - .gitattributes pins text files to LF while retaining CRLF for Windows command scripts.
  • CI now builds and tests the web editor (.github/workflows/ci.yml) — a new web-editor job builds @uwmd/core, then runs the web editor's own npm run build (tsc + vite) and npm test (33 vitest cases). The root npm test only covers workspace packages, and tools/* are intentionally not workspace members, so the editor's calc-integrity suite was never gated in CI.
  • Web editor: accessibility & keyboard pass (tools/web-editor/) — a global :focus-visible ring (light on the navy toolbar) so keyboard users can see focus without cluttering mouse use; the New Deal dialog gains aria-modal, aria-labelledby, real <label htmlFor> associations on every field, and initial focus (it already closed on Esc); the editor-views tabs are a labelled <nav> with aria-current on the active tab; the footed-cell override inputs get aria-labels; (the report iframe already had a title). A new a11y.test.tsx runs axe-core against the dialog and a flagged section view and fails on any serious/critical violation (color-contrast excluded — jsdom can't compute rendered colors). Web-editor suite is now 33 tests across 5 files.
  • Web editor: inline validator remediations (tools/web-editor/src/components/SectionView.tsx) — the validator's issues for the active section now render in context at the top of that section (severity, code, field badge, message, and the BUILTIN_REMEDIATIONS copy), and the offending flat numeric field is flagged with a red border, aria-invalid, an associated <label>, and the remediation shown beneath it via aria-describedby — all read off the ValidationMessage (never re-authored, so the footer and the inline copy can't drift). The global ValidationPanel footer is unchanged. SectionView.test.tsx (jsdom) pins that the remediation shows in-context and the field is marked invalid, and that a clean section shows nothing. Web-editor suite is now 31 tests across 4 files.
  • Web-editor component tests for the footed-model surfaces (jsdom) (tools/web-editor/src/components/footed-model.test.tsx) — adds jsdom + @testing-library/react (dev-only; bundle unchanged) and pins the contract every footed surface shares: editing one input re-foots the dependent totals and dispatches exactly one section_replace. Covers ValuationModel (NOI change re-foots indicated value) and the new DcfModel (a year's NOI re-foots its levered cash flow; the gross exit value re-foots the whole disposition→net→proceeds waterfall; "+ Add year" appends a projection year in one dispatch). Component tests are *.test.tsx and opt into jsdom via a // @vitest-environment jsdom docblock, so the pure node suite stays fast. Web-editor suite is now 29 tests across 3 files.
  • DCF footing in @uwmd/core + a DCF footed-model surface in the web editor (dcf.ts, tools/web-editor/src/components/DcfModel.tsx) — the dcf section was the last calc-bearing section still edited as a flat numeric grid. New deriveDCF(content) foots the relationships that follow unambiguously from a DCF block's own stored inputs — per projection year net_cash_flow_levered = NOI − annual_debt_service and cash_on_cash_return = levered / cumulative_equity_invested, and the exit waterfall disposition_costs = exit_value_gross × disposition_costs_pct, exit_value_net = gross − disposition, and net_proceeds_to_equity = net − loan_balance_at_exit. It deliberately leaves exit_value_gross (capitalizes a forward NOI the block doesn't store) and returns.* (IRR / NPV / equity multiple — cash-flow-timing convention) as inputs, in the same narrow, self-contained spirit as deriveValuation. Pure, exported from index.ts + browser.ts, and pinned to the Parkview worked example in dcf.test.ts (8 tests). The web editor's new DcfModel surface edits the assumptions, the per-year cash-flow rows (add/remove years), and the exit gross + loan balance; the derived totals render as locked ƒ derived cells and IRR/NPV/equity multiple show read-only as engine-provided. Removes the five flat dcf entries from the editor's numeric catalog. Web-editor tests grew to 25 (a dcf array round-trip through applyEdit + reparse, plus array-indexed deepGet/deepSet).
  • Web editor test harness (tools/web-editor/) — the calc-aware editor previously shipped with zero tests despite being the Tier-2/3 chokepoint. Adds Vitest (dev-only; not in the production bundle — bundle hash unchanged) with a test script and a Node test environment, plus two suites: src/edits.test.ts (7 tests) pins the runEdit() contract — a frontmatter_set applies and re-parses so in-memory state can't drift from the canonical bytes, a rejected op returns a failed outcome instead of throwing, EditSettings thread confidence + human_review_required onto the written block, supersede mode archives the prior block (two fences) where replace edits in place (one), and carryForwardOverrides() keeps a pinned _meta.field_overrides alive across a later edit that doesn't restate it; and src/catalog.test.ts (16 tests) covers the path helpers and the wrapper-aware getNumeric/setNumeric that preserve the { value, … } provenance wrapper. 23 tests, all green; npm --prefix tools/web-editor run test and run build both pass.
  • Deterministic section-footing in @uwmd/core (derived.ts, rentroll.ts, opstatement.ts) — deriveRentRoll(content) rolls a unit-level (multifamily) or tenant-level (commercial) schedule up into the section's totals (GPR, in-place rent, physical occupancy, loss-to-lease, concessions, net effective rent, leased/vacant SF, rent PSF, WALT, plus a recomputed unit_mix_summary / tenant_concentration), tolerant of both the spec naming (nra_sqft/base_rent_annual) and the worked-example naming (leased_sf/annual_base_rent) and writing back to whichever total keys the block already uses. deriveOperatingStatement(content) foots EGI / total OpEx (excluding below-the-line capex + replacement reserves) / NOI / expense ratio / NOI margin from the income and expense line items, with other_income and utilities sub-rollups. Both are pure (no I/O, never mutate), return DerivedField[] via the shared derived.ts collector (which drops any non-finite result so partial data never writes NaN), and honor invariant #1 (not AI math — plain deterministic arithmetic, the same spirit as the calc packs) and #4 (one source so the editor, CLI, and Excel converter never disagree). Exported from both index.ts and browser.ts; covered by rentroll.test.ts (13) and opstatement.test.ts (8). Extended to three more sections (debt.ts, sourcesuses.ts, valuation.ts): deriveDebt(content) foots monthly + annual debt service from the loan terms — fully-amortizing via a pmt() byte-identical to the calc engine's pmt builtin, loan × rate when interest-only (detected from amortization: "interest_only" or a missing/zero term), rate-key tolerant; deriveSourcesUses(content) foots the per-bucket sources.total/uses.total, the nested closing_costs.total, and the top-level project-cost mirrors from the line items, and reports the sources-vs-uses gap/balanced so an imbalance surfaces without being silently written; deriveValuation(content) foots the income-approach indicated_value = noi_used / cap_rate_applied (and its delta to purchase price) entirely from the block's own inputs. Covered by debt.test.ts (6), sourcesuses.test.ts (6), valuation.test.ts (5).
  • Lender Package / Credit Memo report renderer in @uwmd/core (report.ts) — renderReportHtml(parsed, opts) implements the spec's rendering targets §7.1 (Tier 1 Lender Package: cover page, executive summary with metric grid, property overview, proforma, rent-roll summary, debt structure, sources & uses, borrower summary, exit analysis, assumptions table with source badges + standard disclaimer) and §7.2 (Tier 2 Credit Memo: adds market analysis with comps, financial analysis with annual cash flows + stress matrix + break-even, due diligence, risk assessment, compliance, covenants, pipeline-log appendix). Output is a self-contained, print-aware HTML document (embedded REPORT_CSS with @page/@media print rules) or an <article> fragment for embedding. Zero dependencies, browser-safe, exported from index.ts and browser.ts; every number is read from the file (calc engine/pack output), never recomputed. Handles both multifamily unit_mix_summary and commercial tenants rent rolls, single-variant maps, and missing sections (skipped + reported in sectionsSkipped). New uwmd report CLI subcommand (--tier, --prepared-by, --output, --stdout).
  • PDF report pipeline (packages/uwmd-report/, preview 0.1.0) — new @uwmd/report package providing uwmd-report <file.uw.md> [-o out.pdf] [--tier] [--format pdf|html] and a programmatic generateReport(parsed, opts) API. Prints core's report HTML to PDF via playwright-core with no bundled-browser download: resolves --browser/UWMD_REPORT_BROWSER, then system Chrome, then system Edge, then a Playwright-managed Chromium; typed BrowserNotFoundError with remediation otherwise. Page layout is owned entirely by core's REPORT_CSS (preferCSSPageSize, zero engine margins), so browser print of the HTML and the CLI PDF are identical.

Fixed ​

  • Windows CRLF parsing - parseUWFile() now accepts CRLF documents without silently dropping every fenced UW section. The parser retains raw input byte-for-byte while normalizing line endings only for structural scanning. Regression coverage exercises a complete CRLF section.
  • Unterminated section fences fail closed - a recognized uw:section fence without a closing fence now raises typed UNCLOSED_SECTION_FENCE even outside strict mode, preventing the remainder of a deal from being swallowed as one malformed block.
  • Unsupported document targets fail explicitly - the core pdf and docx render targets now throw typed UnsupportedRenderFormatError instead of returning successful-looking empty output. PDF callers are directed to @uwmd/report; DOCX remains explicitly unimplemented.
  • Publishable core package contents - @uwmd/core now explicitly ships its compiled dist API and package README while excluding source tests. CI and the release workflow run verify-packages so a package whose exports point at missing artifacts cannot be published again.
  • Web editor lint errors that were failing the CI lint job — biome lint flags (recommended-default errors): noAutofocus on the New Deal dialog and the footed-cell override input (replaced the autoFocus attribute with a ref + useEffect().focus()), and noArrayIndexKey on the DCF per-year rows (now keyed by the row's year). These had accumulated across the unpushed web-editor train; the lint job is green again.

Changed ​

  • Dependency hardening - the workspace Vitest and coverage-provider floor is
  • Canonical project identity - the repository is renamed, package repository links follow the new URL, and public package/homepage metadata now points to https://uwmd.org. raised to 3.2.6, clearing the critical development-server advisory. Safe transitive lockfile updates also patch Vite, esbuild, PostCSS, fast-uri, tmp, and brace-expansion versions where upstream ranges permit. Remaining ExcelJS archive/UUID advisories require an upstream release or a separately validated converter migration before the separately versioned Excel add-on can publish.
  • Web editor 0.5.0 — rent-roll & operating-statement become underwriting models, not forms (tools/web-editor/). New RentRollModel and OperatingStatementModel surfaces (shared Footed.tsx) treat the line items as the only inputs and render the section totals as locked "ƒ derived" cells footed live by core's deriveRentRoll / deriveOperatingStatement. Every mutation (cell edit, add/remove row, override, revert) clones the block, mutates one array, re-foots, writes back every non-pinned total, and dispatches a single section_replace through the applyEdit() chokepoint, so the file can never be left internally inconsistent. A footed total can be pinned by hand via an inline override editor; the pin is recorded in the format's own _meta.field_overrides (reason: "overridden") and badged "manual" until reverted to formula. The edit dispatcher (edits.ts) now carries field_overrides forward across unrelated edits (the protocol's buildMeta() doesn't), and the generic scalar editor accepts lockedPaths so each footed total has exactly one editing path. Replaces the old RentRollTable (deleted) and the hand-entered rent-roll/operating-statement numeric fields. The same pattern now extends to three more sections — DebtModel (loan terms in, monthly/annual debt service footed), SourcesUsesModel (every source/use line in, bucket + closing-cost + project-cost totals footed, with a live sources-vs-uses balance check), and ValuationModel (NOI + cap rate in, income-approach indicated value footed) — via a shared model-kit.tsx (the useFooting write-path hook + InputRow / FootedRow row primitives). Those three sections drop out of the flat numeric allow-list (catalog.ts); their footed totals are locked out of the generic scalar editor so each has exactly one editing path.
  • @uwmd/core/browser now exports the intelligence + calc-introspection surfaces — resolveValue/readInFile (cascade), rankGaps (value-of- information), inferGaps/summarizeGaps/readGapsContent, the asset-class default tables (getAssetClassDefaults/getDefaultRange/listDefaultedFields
    • the five *_DEFAULTS), and getExprDependencies/extractDependencyGraph, with their types. All confirmed browser-safe (no node, no SDK); previously only on the node index.ts entry. Enables the web editor's intelligence and calc-transparency panels.
  • Web editor 0.4.0 — extensive build-out (tools/web-editor/): five tabs (Editor, Intelligence, Report, Diff, Source). New Intelligence tab surfaces Scope (every required input resolved through the fallback cascade with its source step + range) and Refine (VOI gap ranking with affected-output ranges and suggested questions). New edit-provenance bar threading actor/source/confidence/notes/human-review into _meta and toggling replace vs. append (supersede) mode (promotes section_replace → section_supersede). New assumptions editor capturing override rationale (is_overridden/original_value/override_rationale), generic field editor for every scalar leaf (narrative strings → textareas), add/remove rent-roll rows, clickable calc cards opening a formula + resolved-inputs
    • result detail (CalcDetail), and a Diff tab (section + frontmatter changes since load/save via core diff()). Edit chokepoint unchanged (edits.ts → applyEdit() → re-parse); modal Escape handling made global.
  • Web editor 0.3.0 — richer editing surfaces (tools/web-editor/): editable rent-roll tables (unit-mix rows for multifamily-style rolls, tenant rows for commercial — cell edits replace the row through applyEdit()); NOI line-item editor (income/expense entries; wrapped {value, …} fields are updated via a wrapper-aware setNumeric that preserves rationale/source provenance, stored totals shown read-only with validator-flagged drift); snapshot-based undo/redo (restores prior canonical source verbatim; Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z + toolbar); New Deal dialog scaffolding a blank file via generateBlankUWFile; per-section superseded-version history view; Source tab (read-only canonical bytes + copy); Ctrl+S download; numeric allow-list expanded from 8 to ~30 fields across eight sections (incl. DCF assumptions and operating statement).
  • Web editor rebuilt on React 18 + Tailwind CSS 4 (tools/web-editor/, preview 0.2.0) — replaces the vanilla-TS stage-3 editor. Adds: a pinned calc strip that evaluates the asset class's full pack via getPackForAssetClass (all five shipped classes, was multifamily-only), block _meta provenance chips (version/source/confidence/review-required), collapsible raw-JSON block view, per-section prose display, pipeline-log table, and a live Report Preview tab rendering renderReportHtml in a sandboxed iframe on every edit with Lender Package / Credit Memo toggle, Download HTML, and Print/PDF. The edit chokepoint is unchanged: every mutation flows through src/edits.ts → applyEdit() → re-parse, so in-memory state can never drift from canonical bytes (React state only holds the result).
  • Initial .uw.json prototype in @uwmd/core established the model-level round-trip and CLI export path later stabilized by RFC 0014 Phase A above.- Renderer unit tests in @uwmd/core covering JSON projection, superseded history opt-in, CSV numeric/percent output, summary/chat rendering, chat truncation, and the explicit PDF/DOCX stub targets.
  • Self-storage calc pack + defaults + worked example + Excel layout — SELF_STORAGE_PACK adds twelve deterministic metrics keyed off NRSF, rentable units, physical occupancy, and economic occupancy. SELF_STORAGE_DEFAULTS registers cascade/refinement ranges, Sonoran-Self-Storage-Peoria-AZ.uwx.md provides a footing worked example, and @uwmd/excel now emits a self-storage workbook layout with Excel↔evaluator parity to 6 decimals.
  • Declarative module loader/registry in @uwmd/core — modules.ts validates and registers in-process ModuleManifest objects, including shape checks, formula/rule parsing, dependency load order, and tier/protocol/format compatibility. Dynamic imports, signing, and custom asset-class identifiers remain v2/RFC work.
  • Repo restructured into OSS-ready monorepo (spec/, packages/, examples/, conformance/, tools/).
  • @uwmd/core package (renamed from uwmd).
  • UW Protocol v1 specification (spec/UW_PROTOCOL_v1.md).
  • TypeScript protocol surface (packages/uwmd-core/src/protocol.ts) — ViewerCapability, SectionViewModel, ModuleManifest, ProtocolError, BUILTIN_VIEW_MODELS, etc.
  • Module manifest JSON Schema (spec/schemas/module-manifest.schema.json).
  • Canonical formatting helpers (packages/uwmd-core/src/format.ts) — formatCurrency, formatPercent, formatRatio, etc.
  • Conformance test corpus (conformance/tier-{1..4}/).
  • Single-file Tier-1 reference viewer (tools/web-viewer/index.html).
  • Top-level OSS scaffolding (LICENSE, CONTRIBUTING, CODE_OF_CONDUCT, CI workflow, issue templates).
  • Tier-2 Editor — applyEdit() dispatcher in @uwmd/core covering frontmatter_set, section_replace, section_supersede, and pipeline_log_append, with BUILTIN_EDIT_POLICIES enforcement and round-trip preservation. New uwmd edit CLI subcommand.
  • Tier-3 Calc Host — safe-expression parser + evaluator + built-ins (sum, avg, min, max, coalesce, if, round, pmt, npv, irr) per protocol §VIII.1 EBNF, with full CalcError taxonomy. New uwmd calc CLI subcommand.
  • Validator wired to BUILTIN_REMEDIATIONS registry (no inline strings; per protocol §III.6).
  • Conformance runner (scripts/run-conformance.mjs) executing tiers 1–4 with CI gate on tiers 1–3. Filled missing tier-2/3/4 fixtures (frontmatter-set-recommendation before/after pair, revpar-basic, dscr-from-section, l6-risk-rating shape assertion).
  • JSON Schemas for all six boundary-crossing protocol types: uwmd-block, edit-operation, protocol-error, implementation-manifest, calc-result, issue-remediation. Programmatic validator (scripts/validate-schemas.mjs) using ajv 2020 + ajv-formats with cross-file $ref pre-registration. CI gate.
  • Governance scaffolding — SECURITY.md, GOVERNANCE.md (owner-led mode with contributor-activated collaborative safeguards and an RFC process), MAINTAINERS.md, .github/CODEOWNERS, ROADMAP.md, docs/rfcs/ directory with template and process README.
  • npm publish workflow (.github/workflows/release.yml) — on v* tag, runs full test + conformance + schema-validation gate, then publishes @uwmd/core to npm with provenance. prepublishOnly script in the package mirrors the same gate locally.
  • VS Code extension (tools/vscode-uwmd/, preview 0.1.0) — syntax highlighting for .uw.md (YAML frontmatter + Markdown + embedded JSON in uwmd json blocks), folding for frontmatter / fenced blocks / heading sections, document outline, and on-save validation surfacing every @uwmd/core issue with its code, title, remediation, and spec_ref. Bundled via esbuild; ships as .vsix.
  • Documentation site (tools/docs-site/, preview 0.1.0) — VitePress build covering the format spec, protocol spec, JSON Schemas, conformance corpus per tier, and the full set of project documents (roadmap, governance, contributing, security, RFC process). Repo-root markdown remains the single source of truth; scripts/prebuild.mjs copies content into the site tree at build time and rewrites relative links to site URLs. Ships as a 2.9 MB static bundle deployable to any static host.
  • Standalone CLI installer (packages/uwmd-cli/, preview 1.0.0) — new uwmd npm package providing npx uwmd <command> for non-developers who don't want to clone the repo. Thin wrapper that re-exposes the @uwmd/core CLI module via a new ./cli subpath export. Covers all eleven subcommands: init, parse, validate, compact, diff, render, edit, calc, run, summary, layers. Same Tier-1/2/3/4 conformance behavior as the in-repo CLI; no separate code path, so no calc-drift risk.
  • @uwmd/core/browser subpath export — new browser-safe entry point that excludes the agent runner and @anthropic-ai/sdk so the library can be bundled directly into web apps. Re-exports parser, validator, compactor, renderer, editor (applyEdit), calc engine (evaluateCalc), formatting helpers, and the full type / protocol surface. Source at packages/uwmd-core/src/browser.ts.
  • v2 RFC drafts (docs/rfcs/) — six initial drafts opened to start the v2 design conversation, one per item in ROADMAP §"v2 spec exploration": RFC 0001 (locale negotiation), RFC 0002 (Sigstore-style module signing), RFC 0003 (custom asset-class declarations from modules), RFC 0004 (language-agnostic conformance runner with TAP14 + JSON manifest output), RFC 0005 (stochastic calc declarations with seeded PCG-XSL-RR-64 PRNG), and RFC 0006 (hospitality reference module). All in status: draft; none required for v1 conformance.
  • Excel converter (packages/uwmd-excel/, preview 0.1.0) — new @uwmd/excel package providing uwmd-excel <file.uw.md> [-o out.xlsx] and a programmatic toWorkbook(parsed) API. Generates a three-sheet multifamily workbook: an Underwriting sheet (header + named-range inputs block + derived-metric formulas block), an Operating Statement sheet (five income lines + EGI=SUM(income) + eleven expense lines + total_opex=SUM(expenses) + NOI=EGI−total_opex), and a Pipeline Log audit sheet. The eight derived-metric formulas (cap rate, LTV, DSCR, debt yield, price/unit, loan/unit, loan/sqft, cash-on-cash) ship as Excel formulas referencing the workbook-scope named ranges (purchase_price, loan_amount, annual_debt_service, total_units, total_nra_sqft, equity_sponsor, noi) — these mirror MULTIFAMILY_STARTER_PACK in @uwmd/core exactly, so opening the workbook in Excel and running uwmd calc against the same .uw.md produce identical numbers by construction. Editing any named-input cell or any line item ripples through to every dependent metric. Multifamily-only and .uw.md → .xlsx only in 0.1.0; the reverse direction is deferred and the calc-aware web editor remains the canonical Tier-2 chokepoint for editing.
  • Calc-aware web editor (tools/web-editor/, preview 0.1.0) — Vite + plain TS bundle on top of @uwmd/core/browser. Drag-drop file load, sidebar with per-section validation badges, frontmatter editor (16 typed inputs spanning text / enum / list fields), and section views with typed numeric inputs on five calc-bearing sections (property, valuation, noi_model, debt_structure, sources_uses). Every edit dispatches through applyEdit() and reparses the file, so in-memory state can never drift from canonical source. Multifamily calc starter pack (cap rate, LTV, DSCR, debt yield, $/unit, $/sqft, price/unit, cash-on-cash) re-evaluates on every render via evaluateCalc. Validation footer surfaces every ValidationMessage with severity, code, section, and BUILTIN_REMEDIATIONS copy. Bundle size: 19 kB app + 11 kB core, gzipped. Replaces the originally-planned narrative-only Tier-2 web editor — that design was rejected because separating safe narrative edits from unsafe numeric edits creates two paths into the same file.
  • Lint toolchain — Biome 1.9.4 configured at the repo root (biome.json) with npm run lint / npm run format scripts. Linter-only (no formatter enforcement) so the existing code style is preserved. New lint job in CI (.github/workflows/ci.yml).
  • CLI smoke tests — uwmd CLI installer now ships a vitest suite (packages/uwmd-cli/test/cli.smoke.test.ts) covering --help, parse, validate, and missing-file error path against the bundled Parkview fixture. CLI was previously untested.
  • .editorconfig at the repo root for cross-editor consistency (LF, UTF-8, 2-space, trim trailing whitespace except in .md).
  • Canonical asset-class calc packs in @uwmd/core (packages/uwmd-core/src/packs/) — MULTIFAMILY_PACK is now the single source of truth for the eight multifamily derived metrics, exported from both @uwmd/core and @uwmd/core/browser as a ModuleManifest. Previously, the same eight calcs were defined twice — once in calc-engine syntax inside tools/web-editor/src/calc-pack.ts and again in Excel formula syntax inside packages/uwmd-excel/src/multifamily.ts. Adding a metric required editing both. Both consumers now import from the canonical pack.
  • Office calc pack + defaults in @uwmd/core — OFFICE_PACK (packages/uwmd-core/src/packs/office.ts) is the second built-in asset-class pack: eleven office derived metrics (cap rate, LTV, LTC, DSCR, debt yield, price/sqft, loan/sqft, NOI/sqft, operating expense ratio, cash-on-cash, occupancy) as a ModuleManifest, exported from both @uwmd/core and @uwmd/core/browser. Office field paths differ from multifamily — size is property.rentable_square_feet, sponsor equity is sources_uses.sources.sponsor_equity, and the NOI model splits into nested income/expenses. OFFICE_DEFAULTS (defaults.ts) adds a 13-field office triage table (wider ranges than multifamily, plus office-specific TI/LC fields) registered for the cascade. New getPackForAssetClass(asset_class) registry helper selects the right pack by asset class; uwmd refine and uwmd scope now resolve office deals automatically off the deal's frontmatter.asset_class. Every office metric is verified through both evaluateCalc() and the emitted Excel formula to 6 decimals against the Riverside office worked example (packages/uwmd-core/src/packs/office.test.ts).
  • Retail calc pack + defaults + worked example in @uwmd/core — RETAIL_PACK (packages/uwmd-core/src/packs/retail.ts) is the third built-in asset-class pack: twelve retail derived metrics, keyed off gross leasable area (property.gross_leasable_area) for the per-SF metrics and GLA for occupancy, plus a retail-distinctive expense_recovery_ratio (income.expense_reimbursements / expenses.total_operating_expenses) for NNN recovery. RETAIL_DEFAULTS (defaults.ts) adds a 13-field retail triage table including the NNN expense_recovery_rate. New third worked example examples/Cactus-Crossing-Retail-Mesa-AZ.uwx.md — a 95k SF grocery-anchored neighborhood center with a tenant-level rent roll, NNN reimbursements, and percentage rent; validates with zero issues. Both registries (getPackForAssetClass, getAssetClassDefaults) now resolve retail, so uwmd refine/uwmd scope work for retail deals automatically. Every metric is verified through evaluateCalc() and the emitted Excel formula to 6 decimals (packages/uwmd-core/src/packs/retail.test.ts).
  • Industrial calc pack + defaults + worked example in @uwmd/core — INDUSTRIAL_PACK (packages/uwmd-core/src/packs/industrial.ts) is the fourth built-in asset-class pack, completing the four core commercial types: twelve metrics keyed off rentable building area (property.rentable_square_feet) and SF occupancy, including the NNN expense_recovery_ratio. INDUSTRIAL_DEFAULTS (defaults.ts) adds a 13-field industrial triage table (lowest expense ratio + highest recovery + tightest caps of the four classes). New fourth worked example examples/Ironwood-Logistics-Industrial-Tolleson-AZ.uwx.md — a 220k SF Class A bulk-distribution warehouse, multi-tenant NNN; validates with zero issues. Both registries now resolve industrial, so uwmd refine/uwmd scope work for industrial deals automatically. Every metric verified through evaluateCalc() and the emitted Excel formula to 6 decimals (packages/uwmd-core/src/packs/industrial.test.ts).
  • Excel converter: office/retail/industrial layouts + multifamily NOI parity fix (packages/uwmd-excel/) — @uwmd/excel is no longer multifamily-only. The converter engine (toWorkbook.ts) is now generic over a WorkbookLayout (layout.ts) selected by frontmatter.asset_class via a registry (layouts.ts, getLayoutForAssetClass); layouts ship for multifamily, office, retail, and industrial. A layout is just { assetClass, pack, incomeLines, expenseLines, namedInputs } — the engine derives the calc-path→named-range map and the derived-metrics block from the layout (buildNamedRangeMap/buildDerivedMetrics) so formulas and named ranges can't drift. Bug fix: the operating statement previously computed EGI = SUM(income lines) while vacancy/credit-loss were stored as positive magnitudes, so the SUM added vacancy instead of subtracting it — overstating EGI and NOI, and (since the noi named range feeds cap-rate/DSCR/etc.) silently breaking Excel↔evaluator parity. Income deduction lines now carry sign: -1, so EGI = SUM and NOI = EGI − total opex foot to the stored effective_gross_income/net_operating_income. The round-trip test (toWorkbook.test.ts) was rewritten to compute parity for all four example deals (assert the statement foots, then evaluate every derived-metric formula against the workbook's named-range values and compare to evaluateCalc to 6 decimals) — the old test only checked formula text and missed the bug. The Riverside office example's operating statement was reconciled (+$2,550 utilities → $382,550 total opex) so EGI − opex foots to its stated $300,000 NOI; cap rate, DSCR, and all other narrative figures are unchanged.
  • AST → Excel formula emitter (packages/uwmd-core/src/packs/excel-emit.ts) — emitExcelFormula(formula, { namedRanges }) translates a Tier-3 safe expression into Excel syntax via a caller-supplied identifier-to-named-range map. Maps arithmetic 1:1, ternary to IF(), modulo to MOD(), logical &&/|| to AND()/OR(), and the 17 supported builtins (sum/min/max/if/round/pmt/npv/irr/abs/floor/ceil/sqrt/pow/log/exp/fv/pv/nper). Throws ExcelEmitError with codes EXCEL-EMIT-PATH (unmapped identifier), EXCEL-EMIT-FN (no Excel equivalent), EXCEL-EMIT-OP (unhandled operator). New parity test in packs.test.ts evaluates each multifamily metric both through evaluateCalc() and through the emitted Excel formula (with named-range substitution) and asserts they agree to 6 decimals.
  • Pre-publication review follow-ups (REVIEW-2026-04-26.md) — first pass at the audit's punch list:
    • Documentation on-ramps: ARCHITECTURE.md (system map: spec → reference library → tools → conformance corpus, with dependency rules and "where to start contributing" guide), VERSIONS.md (compatibility matrix across format / protocol / @uwmd/core / CLI / tools, plus pinning recommendations), docs/GLOSSARY.md (alphabetical entries for both format-specific and CRE domain terms with normative spec links), docs/TOOLS.md (decision tree for picking among the seven shipped tools), and tools/docs-site/tutorials/your-first-uwmd-file.md (5-step walkthrough from blank file to validated minimal .uw.md). All five are wired into the docs-site sidebar via prebuild.mjs so the repo-root markdown remains the single source of truth.
    • Second worked example: examples/Riverside-Office-Phoenix-AZ.uwx.md — suburban office with a bridge-loan capital stack and tenant-level rent roll, demonstrating that the format is not multifamily-only. Validates with zero errors and a single intentional FV_DSCR_BELOW_THRESHOLD warning (bridge-loan DSCR is thin at close by design).
    • Coverage in CI: new coverage job in .github/workflows/ci.yml runs vitest --coverage on @uwmd/core, publishes a coverage table to the GitHub job summary, and uploads the full lcov report as a build artifact. Soft floor (continue-on-error: true) at the current baseline; will be tightened once the number is ratcheted up. Vitest config at packages/uwmd-core/vitest.config.ts.
    • Malformed conformance fixtures: new conformance/tier-1-reader/malformed/ category with three fixtures (META_MISSING, META_LOW_CONFIDENCE_NO_REVIEW_FLAG, CC-04) and matching expected.json declarations. The conformance runner (scripts/run-conformance.mjs) gained a runTier1Malformed pass that dispatches each fixture through validateUWFile and asserts the actual issue codes are a superset of the expected codes.
    • Property-based tests on the calc engine: new packages/uwmd-core/src/calc/calc.property.test.ts using fast-check to assert (1) parser totality on arbitrary ASCII input ≤ 256 chars (always returns AST or throws typed CalcError), (2) evaluator null-safety against the empty context (always returns CalcValue scalar or throws typed CalcError), (3) Excel-emit grammar parity (every AST the constrained generator can produce emits to a non-empty string). Runs 500 + 300 + 300 cases per property in CI.
    • YAML parser hardening: new Appendix D in the format spec defines the supported YAML subset (scalars, mappings, sequences, comments) and the rejected features (anchors, aliases, explicit tags, block scalars, complex keys, directives). The parser at packages/uwmd-core/src/parser.ts now hard-rejects unsupported features at frontmatter parse time with a typed UWMDParseError carrying the new validator code UNSUPPORTED_YAML_FEATURE (registered in BUILTIN_REMEDIATIONS). Eight new unit tests in parser.test.ts cover the rejection paths plus the all-supported subset.
    • Validator code taxonomy: new §III.6a in the protocol spec documents the three validator code prefixes (CC-NN cross-section consistency, FV_* financial validity, META_* meta integrity) and their severity defaults. Pointer to BUILTIN_REMEDIATIONS and the validator implementation as the authoritative registry — the review's claim that codes were unregistered turned out to be wrong; the gap was that the categorization wasn't documented.
    • Pipeline state L3 documented: the L3 layer was missing from the Parkview example's pipeline_state because it is intentionally reserved. The format spec now states this explicitly inside the pipeline_state example block — conforming files MUST NOT define L3_* keys, and any layer key absent from pipeline_state is treated as pending by default.
    • README cleanup: dropped the stale "(in progress)" labels next to the protocol spec and the conformance corpus links. Both shipped per ROADMAP.md.
  • Two new v2 RFC drafts — RFC 0007 (sensitivity tables as a first-class calc primitive — sensitivity_table() builtin returning a structured grid result) and RFC 0008 (a lease_up_schedule standard section for value-add and ground-up deals). Both in status: draft; not implemented.
  • Architectural review v1.1 train (six-phase landing of ARCHITECTURAL-REVIEW-2026-04-27.md) — full surface expansion across format, protocol, library, conformance, and docs in response to the three architectural concerns raised in the review (wrong-actor / wrong-tool writes, incomplete-data first-class support, AI-efficient representation). Implemented as six independently-shippable phases.
    • Phase 1 — Foundation (_meta extensions + validator taxonomy + scope stage). _meta gained five optional fields documented in Format Spec Part III §3: partial, provisional, field_overrides[], content_hash, parent_hash. _meta.source enum widened by four additive values (user_input, asset_class_default, global_default, system_default); existing scenario_default retained. New normative §IX "Fallback cascade" in the protocol spec defines the seven-step resolution order producers MUST walk. New DealStage value scope added below screening for back-of-napkin triage; STAGE_REQUIREMENTS widened to support field-level + either-or readiness checks (required_field_paths, required_one_of). Validator code taxonomy finalized: CC-NN (existing), FV-NN (renamed from legacy FV_* strings, both emitted via legacy_code for one release), DQ-NN (new), INT-NN (new), POL-NN (new), META_* (existing). BUILTIN_REMEDIATIONS extended with one entry per new code. Confidence vs human_review_required disambiguated as orthogonal in spec.
    • Phase 2 — Gaps section + cascade resolver + asset-class defaults + INCOMPLETE_DATA_POLICIES. New optional standard section gaps with spec/schemas/sections/gaps.schema.json — items[] with (section, field_path, reason ∈ {missing, illegible, out_of_scope, deferred, blocked_by_dependency, awaiting_external}, blocks_stage, first_seen, last_checked, owner, note) plus summary rollup. New packages/uwmd-core/src/defaults.ts exports MULTIFAMILY_DEFAULTS covering 11 fields with low/central/high ranges (expense ratio, vacancy, rent growth, management fee, replacement reserve, debt rate, amortization, IO months, LTV, exit cap, closing costs); each entry version-pinned at 1.0.0 with the contract that range revisions bump the version. New packages/uwmd-core/src/cascade.ts exports resolveValue(field_path, parsed, ctx?) walking the protocol §IX cascade; MarketDataLookup and InvestorProfile are interface-only (adopters bring their own). New packages/uwmd-core/src/gaps.ts with inferGaps, applyGapPolicy, summarizeGaps. New INCOMPLETE_DATA_POLICIES registry in protocol.ts with GapAction = halt | degrade | substitute | defer and lookupIncompleteDataPolicy resolution; seeded with conservative (section, stage) defaults. applyEdit gained an opt-in maintain_gaps option that recomputes the gaps section after every successful write under actor system/gaps-maintainer.
    • Phase 3 — Integrity (hashes + chains + uwmd verify). New packages/uwmd-core/src/integrity-canonical.ts implements RFC 8785 (JCS) JSON canonicalization — vendored ~40 LOC, zero dependencies — with the documented exclusion of _meta.content_hash and _meta.signature from the hash input. New packages/uwmd-core/src/integrity.ts exports computeBlockHash, verifyChain, verifyProvenance, returning typed IntegrityIssue[] with codes INT-01 (parent_hash mismatch), INT-03 (partial chain), INT-04 (content_hash recompute mismatch), POL-01 (unauthorized actor per BUILTIN_EDIT_POLICIES), POL-02 (section_replace where section_supersede required). New applyEditAsync recomputes content_hash on every write when options.integrity is enabled and rejects edits whose ctx.parentHash is stale with INT-02. Crypto split: Node crypto.createHash and Web Crypto subtle.digest produce byte-identical hashes verified in integrity-canonical.test.ts. New uwmd verify <file> [--integrity] [--policy] [--validate] [--json] subcommand combines all three checks; uwmd validate --integrity also opts in inline.
    • Phase 4 — Context profiles for AI consumption. New packages/uwmd-core/src/context-profiles.ts exports buildContext(parsed, profile, opts?) returning a ContextResult with {content, tokenEstimate, profile, sectionsIncluded, truncated}. Five normative profiles defined in new protocol §X "Context profiles": summary (frontmatter + quick_metrics + pipeline_state + gaps, ≤600 tokens), live (all non-superseded, prose included), compact (all non-superseded, JSON-only, minified, stable→volatile section order, ≤55% of live tokens), full (every byte), relevant (filtered to opts.sections). Stable→volatile compact ordering targets prompt-cache-friendly prefixes for adopters using LLMs at scale. buildAgentContext rewritten as a thin shim over buildContext(parsed, layer.consumed_profile, {sections: layer.reads}) — external AgentContext shape preserved. BANCROFT_LAYERS entries gained a consumed_profile field. New uwmd render --profile=summary|live|compact|full|relevant [--sections=…] [--max-tokens=N] [--no-meta] flag.
    • Phase 5 — Refinement engine (dependency graph + perturbation VOI + L0a / L0b agents). New packages/uwmd-core/src/calc/dependencies.ts exports getExprDependencies(ast) and extractDependencyGraph(parsed, {packs?}) which walks every MULTIFAMILY_PACK calc plus every custom_calculations[] entry into a DependencyGraph of (outputs: calc → inputs, inputs: input → calcs, formulas). New packages/uwmd-core/src/refinement.ts exports rankGaps(parsed, opts?) returning RankGapsResult with by_voi[] ranked by value-of-information and by_stage_blocking[] for non-numeric completeness gaps. The perturbation method evaluates each provisional input at its low / central / high values using interval arithmetic across the dependency graph, computes today's output range, then computes the collapsed range if the gap were known; VOI = today − collapsed, summed across affected outputs. Question templates ship in defaults.ts co-located with the ranges. Diagnostics surface non-monotonic outputs as warnings. Two new pipeline layers: L0a Scope (prerequisites: none; reads property; writes noi_model, debt_structure, valuation, market_analysis, gaps; consumed_profile: summary) and L0b Scope Refinement (prerequisites: L0a; reads *; writes property, noi_model, debt_structure, gaps; consumed_profile: compact). New uwmd scope <file> deterministically resolves every required input via the cascade and stamps results provisional at deal_stage: scope. New uwmd refine <file> [--targets=...] [--top=N] [--json] runs rankGaps and prints / emits the ranking. L0b prompt template at packages/uwmd-core/src/agents/L0b-prompt.md.
    • Phase 6 — Conformance corpus + RFC drafts. New conformance fixtures: tier-1 04-scope-only.uw.md (back-of-napkin scope file), and six new malformed fixtures (04-broken-chain → INT-01, 06-wrong-actor → POL-01 with sibling policies.json, 07-replace-where-supersede-required → POL-02, 08-provisional-without-gap → DQ-01, 09-partial-without-overrides → DQ-03). New tier-2 fixtures: gaps-section-update (post-write maintainer hook), parent-hash-stamp (integrity-aware edit), stale-parent-rejected (negative-path INT-02). New tier-3 mode: dependency-graph-multifamily exercises extractDependencyGraph. New tier-4 fixtures: l0a-scope-deterministic (shape contract for the scope agent's writes) and consumer-profile-contract (verifies every Bancroft layer declares the right consumed_profile). The conformance runner (scripts/run-conformance.mjs) gained async dispatch, integrity / policy code multiplexing on tier-1 malformed via verifyChain + verifyProvenance, optional <id>.policies.json and <id>.options.json siblings on tier-1/tier-2 fixtures, expected-error.json for negative-path tier-2 fixtures, runTier3Refinement / runTier4Profile modes, and a <volatile> mask on content_hash strings (which canonicalize over a per-run timestamp). Final tally: 29 conformance fixtures pass, 0 fail.
    • Deferred / discussed — five items intentionally captured as RFC drafts rather than implementation in this train: RFC 0009 (_meta v2 sub-object reorganization, the only proposed breaking change; deferred to v2.0 with back-compat parser shim), RFC 0010 (_meta.integrity.signature as a separate @uwmd/signing package; explicit non-goal as everyday open-standard usage), RFC 0011 (JWT-style scoped capability tokens for write authorization in orchestrator-bound deployments), RFC 0013 (per-section embedding sidecar for portfolio-scale "find similar deals", v3 territory), and a new "Range types and napkin mode" subsection added to RFC 0005 framing the v1.1 perturbation engine as the v1.1 approximation of a stochastic v2. Other items explicitly NOT pursued and documented in the architectural review's out-of-scope catalog: per-field provenance everywhere (rejected — field_overrides is the chosen mid-point), calc-engine confidence propagation (deferred indefinitely — math is murky), pure-halt mode for required fields (rejected — partial data is the norm), compiled binary .uwc format (rejected with extended explanation), AST cache files (deferred until measured parsing speed becomes a problem), per-block telemetry (no current adopter need), asset-class default tables for non-multifamily (one pack per RFC; office / retail / industrial follow), and MarketDataLookup reference implementation (interface only — adopters bring CoStar / Yardi / internal).
  • Calc engine v1.1 (Phase B) — protocol version bumped to 1.1.0 with two additive Tier-3 changes. (1) Grammar gains short-circuiting logical operators && and || between conditional and comparison precedence levels, with full null propagation (any null operand → null) and CALC-TYPE-001 on non-boolean operands; the right side is only evaluated when the left does not determine the result. (2) Built-in function set expands by 10: math (abs, floor, ceil, sqrt, pow, log, exp) and financial (fv, pv, nper). All new financials use the same unsigned convention as the existing pmt (positive pv = loan balance, positive pmt = periodic payment) and admit zero-rate edge cases by closed-form fallback. Spec updated at spec/UW_PROTOCOL_v1.md §VIII.1 (EBNF) and §VIII.3 (builtin table). Backwards-compatible: every v1.0.0 expression continues to parse and evaluate identically.

Changed ​

  • @uwmd/core no longer declares a bin entry — the standalone uwmd package owns the binary so the two packages don't conflict when both installed. Library consumers continue to import from @uwmd/core; CLI consumers should install uwmd (or npx uwmd). The CLI module is now exposed as the @uwmd/core/cli subpath export.
  • Release workflow now publishes both @uwmd/core and uwmd together on each v* tag, with a version-match gate that also verifies uwmd's pinned dependency on the corresponding @uwmd/core version.
  • Quickstart in README.md and fixture-regen examples in CONTRIBUTING.md updated to use npx uwmd … (or npm run cli -- … from a clone) instead of the raw node packages/uwmd-core/dist/cli.js path.
  • Format spec — added RFC 2119 preamble; restored §4.18 (Pipeline Log) to canonical numeric position; clarified section count (21 standard + 1 meta).
  • Protocol spec — fixed .uw.institution.json cross-reference (now points to Appendix C.6); added "Normative schema:" links from §I.4, §III.1, §V, §VIII, §XI to the new JSON Schemas; added §XIII "Future work" consolidating v2 deferrals.
  • Root npm test and npm run build now run across all workspaces (--workspaces --if-present), not just @uwmd/core. The existing @uwmd/excel test suite and the new uwmd CLI smoke tests are now covered.
  • vitest bumped from ^1.6.0 to ^3.0.0 in @uwmd/core, @uwmd/excel, and uwmd (clears four moderate npm audit advisories in the vite/esbuild dev-server chain).
  • esbuild bumped from ^0.24.0 to ^0.25.0 in tools/vscode-uwmd (clears the dev-server CSRF advisory).
  • vite bumped from ^5.4.0 to ^7.0.0 in tools/web-editor; the web-editor lockfile now reports 0 vulnerabilities.
  • Calc engine — internal AST node for ternary expressions renamed cond.then → cond.consequent to avoid the false thenable signal the previous shape produced. No external API change (the AST is internal to packages/uwmd-core/src/calc/parser.ts and evaluator.ts).
  • Docs site prebuild (tools/docs-site/scripts/prebuild.mjs) — now copies the six numbered RFC drafts into the site tree and rewrites docs/rfcs/NNNN-… and bare NNNN-… link forms to /about/rfcs/NNNN-…. Fixes 18 dead links that were breaking npm run build for the docs site.
  • Workspace lockfile policy — packages/uwmd-core/package-lock.json removed (workspace member, drifts from the root lockfile). Tools under tools/* are intentionally not workspace members and continue to commit their own lockfiles. Documented in CONTRIBUTING.md.

Removed ​

  • packages/uwmd-core/ANTHROPIC_API_KEY.env — local-dev convenience file containing a real API key. Deleted; the CLI already loads ANTHROPIC_API_KEY from process.env. Future: set it via your shell or a gitignored ~/.config/uwmd/.env instead of inside the repo tree.

[1.0.0-pre] — pre-public ​

Pre-public development of the format spec (UW_FORMAT_SPEC_v1.md) and reference parser/validator/renderer/runner/Claude agent host inside uwmd/.

Released under the MIT License.