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
18 changes: 18 additions & 0 deletions .changeset/16045-spec-declaration-text-snapshots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@objectstack/spec': minor
---

`api-surface-declarations/<entry>.txt` — every export of every published entry point now ships a readable pin of the `.d.ts` declaration text the packed build actually emits for it, and the 27-entry `api-surface-signatures.json` hash it subsumes is retired (#16045).

`Clause-②: yes (widening)`

Until now this package pinned its public surface on one axis. `api-surface/<entry>.json` records each export as `name (kind)` — 5336 rows across 17 entry points, re-derived on the landing tree — and a signature change, a renamed interface field and a dropped union member move **none** of them. The only shape pin was `api-surface-signatures.json`: 27 rows, 0.5% of the surface, and reference-level even there, because it hashed `checker.typeToString()`, which prints `z.input<typeof ActionSchema>` without expanding it. A breaking shape change to a ratified public type could pass every witness green.

- **Text, ⛔ not a hash, deliberately.** A digest answers "did the bytes move" with one opaque bit whose known failure at scale is that a red one gets *accepted* rather than investigated. Each shard holds one block per declaration — `// ── Name (kind) ──` followed by the declaration verbatim — so a diff names the export and shows the change, and the existing review discipline is what guards it.
- **The input is the packed `.d.ts` reached through the `exports` map**, i.e. the declarations a consumer installs, never `src/`. Two of the manifest's 19 `exports` entries are asset subpaths with no declaration (`./openapi.json`, `./package.json`), which is why this artifact and `api-surface/` both hold 17 shards.
- **What it costs, measured on the landing tree**: 12,661,943 bytes (12.08 MiB) of text across 17 shards, 237,706 lines, 1.02 MiB gzipped against this package's ~17.6 MiB compressed `dist`. The skew is extreme — the median declaration is 81 bytes and the 20 largest hold ~65% of the bytes, because a Zod schema's packed declaration is its fully expanded structural type. That expansion is exactly what makes an inner field rename visible; it also means four declarations exceed 20,000 lines each.
- **Leading TSDoc is excluded**, so a re-worded `.describe()` does not churn this artifact — documentation drift stays `check:docs`'s axis.
- **The retirement is a strict superset, proven before it landed**: all 27 factory names resolve to a declaration block in `api-surface-declarations/root.txt`, 0 missing. For those 27 declarations text and hash discriminate the same amount (both print a type reference); what is *gained* is the 5309 other declarations, including the schemas those factories point at, whose expanded blocks are where an inner-key narrowing shows up. Nothing published read the retired file: it was not in this package's `files[]`.
- **Sharded per entry point from day one**, for the reason `api-surface/` is: the merge queue rebuilds server-side where no custom merge driver runs, so two PRs sharing one generated file evict the second.

Regenerate with `pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec gen:api-surface-declarations`; `check:api-surface-declarations` names that command when it fails. It reads the built dist, so a missing or stale one is a hard refusal in both modes rather than a green run over nothing.
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,10 @@ packages/spec/authorable-surface.base.json merge=os-regen
packages/spec/authorable-defaults/** merge=os-regen
packages/spec/json-schema.manifest/** merge=os-regen
packages/spec/api-surface/** merge=os-regen
packages/spec/api-surface-declarations/** merge=os-regen
packages/spec/src/meta-spelling/meta-url-data.generated.ts merge=os-regen
packages/spec/export-origins/** merge=os-regen
packages/spec/declaration-map/** merge=os-regen
packages/spec/api-surface-signatures.json merge=os-regen
docs/protocol-upgrade-guide.md merge=os-regen
docs/audits/2026-07-unknown-key-strictness-ledger.counts.md merge=os-regen
content/docs/references/** merge=os-regen
Expand Down
38 changes: 33 additions & 5 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5187,11 +5187,16 @@ jobs:

# The authorable KEY surface — what a metadata author may write, which for
# this platform is the third-party API. `api-surface/` records exported
# names and `api-surface-signatures.json` hashes factory types as TypeScript
# PRINTS them (a reference, never structurally expanded), so neither sees a
# key added to or removed from a schema. #3883 removed three authorable keys
# with every witness green; #3733 did it by accident. ADR-0059 §5 deferred
# this gate until a narrowing actually slipped both — it has.
# names only, so it does not see a key added to or removed from a schema;
# the shape sibling that used to sit beside it (`api-surface-signatures.json`,
# retired at #16045) hashed factory types as TypeScript PRINTS them — a
# reference, never structurally expanded — so it did not see one either.
# #3883 removed three authorable keys with every witness green; #3733 did it
# by accident. ADR-0059 §5 deferred this gate until a narrowing actually
# slipped both — it has. `check:api-surface-declarations` (consumer-gates
# lane) now records the declaration TEXT of every export, which DOES move on
# such a key; this gate stays the authority on the AUTHORABLE key set, which
# is a different question from the declared TypeScript shape.
#
# ⚠ ORDER: this step must stay ABOVE the `check:docs` step below. Its
# `--check` run of scripts/build-schemas.ts writes the gitignored
Expand Down Expand Up @@ -6113,6 +6118,29 @@ jobs:
- name: Check @objectstack/spec public API surface
run: pnpm --filter @objectstack/spec run check:api-surface

# [#16045] The SHAPE half of the same surface, and the step the card above
# exists for: `api-surface/` pins 5336 `name (kind)` rows and a signature
# change, a renamed interface field and a dropped union member move NONE of
# them, so 99.5% of the pinned surface could not go red on a breaking shape
# change to a ratified public type. The declaration-text snapshot records
# what the packed `.d.ts` actually declares for every export, per entry
# point. Ruled at #16045 (director batch #60, maintainer 「同意」): text and
# ⛔ NOT a hash, because a red hash gets accepted rather than investigated
# and a readable diff is what makes contract review a guard.
#
# WHY THIS LANE. It resolves each entry point through the `exports` map to
# the BUILT `.d.ts` — the declarations a consumer installs — so it is
# build-dependent and sits after the two build steps above with its family
# (`check:api-surface`, `check:published-readme-exports`). A missing or
# stale dist is a HARD REFUSAL in both of the script's modes, never a skip:
# a build-dependent gate that silently reads nothing reports "not measured"
# as if it were "measured and clean" (#4690).
#
# It adds no required context: a step in an existing lane, so no open PR
# waits on a check whose name no head has ever reported (#9325).
- name: Check @objectstack/spec declaration text (the shape half)
run: pnpm --filter @objectstack/spec run check:api-surface-declarations

# [#11350] Consumer-shaped declaration-emit pin against the BUILT root
# entry (an un-annotated `export default defineStack(...)` must compile
# with `declaration: true` — the TS2883 class). The pin is environment-
Expand Down
7 changes: 6 additions & 1 deletion docs/spec-generated-artifact-sharding.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ arithmetic composes on a merge and the judgement does not).

- **`spec-changes.json`** — keyed by version, so two PRs append under different majors.
Never a conflict surface worth splitting.
- **`api-surface-signatures.json`** — 1.3KB, one line per `defineX` factory.
- **`api-surface-signatures.json`** — RETIRED at #16045, and the one row here whose reason
did not survive its own artifact. It was 1.3KB, one line per `defineX` factory, so it was
never worth splitting. Its replacement is the opposite shape: `api-surface-declarations/`
holds the declaration TEXT of every export (12 MiB across 17 shards on the tree that
landed it), so it is sharded per entry point from the day it arrived, for the same
merge-queue reason `api-surface/` is.
- **`authorable-surface.base.json`** — the #5235 deletion-gate anchor. Nothing but an
explicit `gen:authorable-surface-base` writes it (#5358), so it was never on the churn
path that made the other three the queue's serialization point. It also carries **one**
Expand Down
Loading
Loading