Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,39 @@ Until `1.0.0`, minor releases may include breaking changes
`matcherAppliesToMember` applies a qualified matcher when it equals either
name. No surface passes a member yet; that is ADR-0059's federation work.

### Added

- **`@adrkit/core` exports `todayUtcDate()`, the `--as-of` default.** When
`--as-of` is absent, `adr queue`, the queue Action, and `adr new` now take
today's UTC date from this one function instead of three inline copies. So
does the unpublished `@adrkit/sdk`, and the decision-portal Action now takes
its default from the SDK rather than computing a fifth copy.
- **The decision-portal Action's `as-of` accepts an ISO datetime with an
explicit timezone**, as `adr queue --as-of` does, and resolves it to the UTC
calendar date. A timezone-less datetime is still refused. Underneath, the
unpublished `@adrkit/sdk`'s `queue({ asOf })` takes the same grammar and
echoes the resolved date. A library consumer can now default the
queue exactly as CI does, instead of risking a local-date default that
disagrees near midnight
([ADR-0031](docs/adr/0031-publish-a-narrow-consumer-sdk-as-the-contract-and-document-the-cli-json-as-its-s.md)
action item 8). No output changes.
- **`@adrkit/core` owns the `adr explain`, `adr lint`, and `adr new` JSON
shapes.** `buildExplainReport`/`formatExplainReportJson`,
`buildLintReport`/`formatLintReportJson`, and
`buildNewAdrReport`/`formatNewAdrReportJson` join `formatQueueReportJson` and
`renderJsonGraph`, and the CLI prints their output (ADR-0031 action item 4).
The bytes are unchanged, which goldens captured before the change pin.
- **A conformance fixture for the CLI JSON contract.** A fixed corpus with
golden `adr queue --format json` and `adr check --json` output, at
`packages/core/test/conformance/golden/`. The CLI and `@adrkit/sdk` are
tested against it, and a downstream repository can run its `manifest.json`
against the `adr` it pins
([ADR-0030](docs/adr/0030-keep-extension-surfaces-that-carry-a-dependency-tree-outside-this-repository.md)
clause 5).
- **`docs/RELEASING.md` documents the two consumer contracts**: the
`@adrkit/sdk` surface (still unpublished) and the CLI JSON, each with the
additive-only obligation stated (ADR-0031 action item 5).

## [0.19.0] - 2026-10-10

This release ships the decision portal: a third, build-only nested Action,
Expand Down
55 changes: 55 additions & 0 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,61 @@ renames, or reinterprets that field breaks badges in other people's
repositories; call it out in the release notes and keep the v1 field emitted for
at least one minor release.

## Consumer contracts: `@adrkit/sdk` and the CLI JSON

[ADR-0031](adr/0031-publish-a-narrow-consumer-sdk-as-the-contract-and-document-the-cli-json-as-its-s.md)
names two contracts for code outside this repository, and attaches the
additive-only obligation of ADR-0029 clause 11 to them instead of to
`@adrkit/core`. Core's exports are the engine: the surface test pins them so
each change is deliberate, but a downstream surface should bind to one of these
two.

**`@adrkit/sdk`** is the typed library contract: `openDecisions()` and a
`DecisionSet` handle (`records`, `issues`, `get`, `queue`, `graph`,
`governing`), with its own declared types and no re-export of core's. The
surface, its counts, and the reasoning behind each entry are in
[`docs/sdk-surface.md`](sdk-surface.md). It is implemented and **not
published**. It stays at `0.0.0` with no `exports` or `files` and is absent
from `RELEASE_PACKAGES`, until a later record authorizes publication
(ADR-0031 clause 8, with the gates in
[ADR-0053](adr/0053-implement-adrkit-sdk-over-core-s-kernels-and-accept-an-in-repo-consumer-as-its-r.md)
decision 9). Once published it is versioned independently, as
`@adrkit/spec-kit` is, and is additive-only: fields, options, and methods may
be added, but none is removed, renamed, or reinterpreted. A core change reaches
SDK consumers only through a deliberate edit to its mapping layer.

**The CLI JSON** is the language-agnostic contract. Every `--json` shape below
is a type core owns and serializes with `JSON.stringify(value, null, 2)` plus a
trailing newline:

| Command | Shape | Formatter |
|---|---|---|
| `adr queue --format json` | `QueueReport` v1 ([`queue-report.md`](../specs/007-arb-queue/contracts/queue-report.md)) | `formatQueueReportJson` |
| `adr check --json` | `CheckOutcome` | serialized directly |
| `adr explain --json` | `ExplainReport` | `formatExplainReportJson` |
| `adr lint --json` | `LintReport` (`{ checked, findings }`) | `formatLintReportJson` |
| `adr new --json` | `NewAdrReport` (`{ id, path }`) | `formatNewAdrReportJson` |
| `adr graph --format json` | `{ nodes, edges }` | `renderJsonGraph` |

It is additive-only too. A key may be added. Removing, renaming, or
reinterpreting a key, or reclassifying an exit code, is a break. One
exception is admitted, by
[ADR-0059](adr/0059-aggregate-decision-corpora-across-an-organization-s-repositories-as-a-pinned-rea.md)
clause 5: `adr lint --json` reports a log-qualified `relatesTo` or
`conflictsWith` whose log is absent as `federated-ref-unresolved`. When
`--as-of` is absent, `adr queue` uses core's `todayUtcDate()`, the one
implementation of the default in
[`cli-contract.md` §As-Of Resolution](../specs/007-arb-queue/contracts/cli-contract.md).

Goldens pin these shapes. The conformance fixture,
[`packages/core/test/conformance/golden/`](../packages/core/test/conformance/golden/README.md),
is a fixed corpus with golden `adr queue` and `adr check` output (ADR-0030
clause 5). The CLI and the SDK are both tested against it, and a downstream
repository can run its `manifest.json` against the `adr` version it pins.
`packages/cli/test/fixtures/json-report/` pins `explain`, `lint`, and `new`
the same way. A change that has to regenerate a golden changes the contract.
Say so in the CHANGELOG and the release notes.

## Local release simulation

From a clean checkout:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,12 +264,23 @@ recorded as its own wrongness signal.
object shapes structurally identical to a core type. Measuring it is what revealed that this
record's own wrongness criterion tested the wrong property — see *Consequences*, where the
criterion is replaced and the correction recorded.**
4. [ ] Converge `explain`, `lint`, and `new` `--json` onto core formatters, as `queue` is,
so the two modes describe the same shapes.
5. [ ] Document the CLI JSON contract and the SDK surface in `docs/RELEASING.md`, with the
additive-only obligation stated at both.
6. [ ] Build the conformance fixture ADR-0030 action item 5 names, as the SDK's own test
fixture, so one artifact discharges both records.
4. [x] Converge `explain`, `lint`, and `new` `--json` onto core formatters, as `queue` is,
so the two modes describe the same shapes. **Done 2026-10-10:** `buildExplainReport` /
`formatExplainReportJson`, `buildLintReport` / `formatLintReportJson`, and
`buildNewAdrReport` / `formatNewAdrReportJson` in `packages/core/src/report/`, exported
from the entry point; the CLI supplies inputs only. Output is byte-for-byte unchanged:
goldens captured from the inline assembly before it was removed
(`packages/cli/test/json-report.test.ts`), and a before/after comparison of the old and
new CLI over this repository's corpus and the SDK fixtures.
5. [x] Document the CLI JSON contract and the SDK surface in `docs/RELEASING.md`, with the
additive-only obligation stated at both. **Done 2026-10-10:** "Consumer contracts:
`@adrkit/sdk` and the CLI JSON", linking `docs/sdk-surface.md`.
6. [x] Build the conformance fixture ADR-0030 action item 5 names, as the SDK's own test
fixture, so one artifact discharges both records. **Done 2026-10-10:**
`packages/core/test/conformance/golden/` (a fixed corpus, `manifest.json`, `queue.json`,
`check.json`). `packages/cli/test/conformance.test.ts` requires the CLI to reproduce both
goldens byte for byte, and `packages/sdk/test/conformance.test.ts` runs the SDK against
them. Publishing it to downstream consumers rides on the package publication records.
7. [~] **Close the `--as-of` resolution gap: `queue`'s date resolver is a contract that lives
outside the contract.** `resolveAsOf` in `packages/cli/src/queue.ts` is ~25 lines
implementing `cli-contract.md §As-Of Resolution` — bare `YYYY-MM-DD` or an ISO datetime with
Expand Down Expand Up @@ -301,7 +312,7 @@ recorded as its own wrongness signal.
boundary — the same divergence class this item was opened to close. Carried as a
successor item rather than marked closed.

8. [ ] Export the absent-input clause of `cli-contract.md` §As-Of Resolution — successor to
8. [x] Export the absent-input clause of `cli-contract.md` §As-Of Resolution — successor to
item 7. `new Date().toISOString().slice(0,10)` is inlined at
`packages/cli/src/queue.ts`, `packages/ci/src/queue-action-entrypoint.ts`, and
`packages/core/src/scaffold/new.ts`. Until it is exported, "the queue as of now" — the
Expand All @@ -324,3 +335,13 @@ recorded as its own wrongness signal.
answer, it was an unreachable rule, so no behavioural assertion could have detected it.
A suite of correctness cases alone would have reported this contract gap as fully
covered.

**Done 2026-10-10.** `todayUtcDate()` lives in `packages/core/src/queue/as-of.ts` beside
`resolveAsOf`, is exported from the entry point, and is consumed by `adr queue`, the queue
Action (bundle regenerated), `adr new`'s scaffold, and the SDK's `queue()`, which was the
fourth copy. A test in `packages/core/test/queue/as-of.test.ts` fails if any of the four
spells the expression again; observed failing by restoring it in the SDK. The static portal,
which may import only `@adrkit/sdk`, takes the default the SDK echoes in `queue().asOf`
rather than computing its own (a second test bans the expression under
`packages/portal/src`). A caller-supplied `asOf` goes through the same method, which now takes
`adr queue --as-of`'s whole grammar and echoes the resolved UTC date.
Original file line number Diff line number Diff line change
Expand Up @@ -325,20 +325,20 @@ No external validation is claimed.
2. [x] Implement `openDecisions` and the six `DecisionSet` members over
`@adrkit/core`, keeping the declared types, adding the fields of decision
11, and adding no re-export.
3. [ ] Before publication, export the absent-input `--as-of` default from
3. [x] Before publication, export the absent-input `--as-of` default from
core (ADR-0031 action item 8) and replace all four inline copies with it:
the CLI, the CI Action, the scaffold, and the SDK's `queue()`.
4. [x] Add behaviour tests per entry point and contract tests against
`adr queue`, `adr graph`, and `adr explain` JSON, each observed failing first.
5. [ ] Build the conformance fixture (ADR-0031 action item 6, ADR-0030 action
5. [x] Build the conformance fixture (ADR-0031 action item 6, ADR-0030 action
item 5) and run the SDK's tests against it.
6. [x] Extend `packages/cli/test/node-compatibility.test.ts`, or add an
equivalent check, to cover `packages/sdk/src`.
7. [x] When the implementation lands, update `src/index.ts`'s header,
`package.json`'s `//status`, and `docs/sdk-surface.md`'s "Nothing is
implemented" and rung statement to match the tree, and replace the
body exclusion there with decision 11's reason.
8. [ ] Converge `explain`, `lint`, and `new` `--json` onto core formatters
8. [x] Converge `explain`, `lint`, and `new` `--json` onto core formatters
(ADR-0031 action item 4).
9. [ ] Before any publication record: a consumer built against the implemented
SDK in this repository and exercised in CI, with the build-time gap of
Expand Down
30 changes: 23 additions & 7 deletions docs/sdk-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,11 @@ follows the CLI command that answers the same question, call for call:
| `.records` | `lintCorpus().records`, `decisionBucketFor`; `body` is core's `Adr.body` | `adr graph --format json` nodes (id, title, status, order); `body` against each file's text after its closing fence |
| `.issues` | `lintCorpus().findings`, `sortFindings` | `adr lint --json` findings (`rule` → `code`) |
| `.get(id)` | id lookup, last duplicate wins as in `toGoverningDecisions`; `31` pads to `0031` | — |
| `.queue({ asOf })` | `buildQueueReport`; a supplied `asOf` checked with `resolveAsOf` and required to be a bare `YYYY-MM-DD` | `adr queue --format json --as-of` items (including the review counts) and `corpusFindings` |
| `.queue({ asOf })` | `buildQueueReport`; a supplied `asOf` resolved with `resolveAsOf`, the `adr queue --as-of` grammar (bare date, or a datetime with an explicit zone), and the resolved date echoed | `adr queue --format json --as-of` items (including the review counts) and `corpusFindings` |
| `.graph({ kinds })` | `buildAdrGraph`, edges filtered to `kinds` (default `['supersedes']`) | nodes: `adr graph --format json`; edges: `--kind supersedes` by default, each `--kind` for one kind, unfiltered for all three |
| `.governing(path)` | `readSourceMarkers` → `resolveAffects` → `resolveSourceMarkers` → `mergeSourceDeclarations` → `toGoverningDecisions` → `bucketDecisions` → `sortFindings` | `adr explain <path> --json` groups and findings |

Five points where the declared surface left a choice, and how each was settled:
Six points where the declared surface left a choice, and how each was settled:

- **Issue codes follow the CLI JSON for the same question.** `DecisionSet.issues` and
`PathGovernance.issues` carry lint rule names verbatim (`corpus-file-skipped`), and
Expand All @@ -210,9 +210,17 @@ Five points where the declared surface left a choice, and how each was settled:
`buildAdrGraph`'s stable id order, never looked up by id, so a corpus that repeats an id
shows both records, as the CLI does. `get(id)` alone resolves a repeated id to the last
record, the one `governing` and `adr explain` resolve it to.
- **The `asOf` default is inlined** as `new Date().toISOString().slice(0, 10)`, a fourth copy of
the absent-input clause. ADR-0031 action item 8 — a publication gate for this package —
replaces all four with one core export; it is deliberately not done here.
- **The `asOf` default is core's `todayUtcDate()`**, the one export of `cli-contract.md`
§As-Of Resolution's absent-input clause, which `adr queue`, the queue Action, and `adr new`
also call. The SDK once inlined a fourth copy; ADR-0031 action item 8 replaced all four.
- **A supplied `asOf` takes the CLI's whole grammar.** `queue({ asOf })` accepts exactly what
`adr queue --as-of` accepts — a bare `YYYY-MM-DD`, or an ISO datetime with an explicit zone —
through core's `resolveAsOf`, and `QueueView.asOf` echoes the resolved UTC calendar date; a
timezone-less datetime throws `RangeError`. The sketch accepted bare dates only and told a
caller with a datetime to import `resolveAsOf` from core, which is the reach-into-core
ADR-0031 names as the facade failing; widening the input closed that without a new symbol.
The static portal relies on both: it passes a caller's `asOf` (or none) to `queue()` and builds
from the echoed date, so it computes no date and restates no grammar of its own.

All filesystem access is `openDecisions` plus `governing`'s marker read through core's confined
reader (skipped with `readMarkers: false`). Nothing in the package writes. The returned records,
Expand Down Expand Up @@ -338,8 +346,16 @@ Decisions this left open, and how they were settled:

- **ADR-0014 rung 1 only.** The implementation has unit, behaviour, and CLI-equivalence tests
(`packages/sdk/test/`), the equivalence run against two fixture corpora and this repository's
own `docs/adr`. No consumer has exercised the surface, because the one it was designed for does
not exist yet. ADR-0031's own trade-off — *"the risk is that the guess hardens before the
own `docs/adr`, and it is tested against the committed conformance fixture
(`packages/core/test/conformance/golden/`, ADR-0031 action item 6) that the CLI's goldens also
pin. One consumer outside `packages/sdk/` exercises it: the static portal (`packages/portal`,
ADR-0054) calls `openDecisions` and five of the six handle members (`records`, `issues`,
`queue`, `graph`, `governing`; not `get`), and takes its default `asOf` from `queue()`'s echo.
`bun test` runs it in CI over fixture corpora, and the portal Action's bundle is tested the
same way. That consumer is build-time only: it opens a corpus once per build, so the handle's
long-lived use is not exercised (ADR-0053 decision 8), and whether it satisfies ADR-0053
action item 9 is for the publication record to decide. The consumer the surface was designed
for, a Backstage backend, does not exist yet. ADR-0031's own trade-off — *"the risk is that the guess hardens before the
Backstage plugin tests it"* — is not mitigated by this work, only made concrete enough to
criticize.
- **Implemented, not exercised.** The eight-call `explain` assembly the surface claims to absorb
Expand Down
Loading
Loading