Skip to content

feat(epic): code-graph: show a package's published API per export subpath, and diff it against a base - #617

Merged
usirin merged 16 commits into
mainfrom
epic/604
Oct 11, 2026
Merged

usirin merged 16 commits into
mainfrom
epic/604

Conversation

@usirin

@usirin usirin commented Oct 10, 2026 •

Copy link
Copy Markdown
Member

About this epic

Epic #604: packages/tea/MAINTAINING.md stamps every export subpath stable, battery or experimental with a semver promise, and no check holds a PR to it: packages/tea/scripts/check-export-stamps.mjs only checks that each subpath has a row, not what changed under it. Agents judging a change (tea-fabrika's builder check) need the same published shape. […]

Fixes #604
Fixes #608
Fixes #612
Fixes #609
Fixes #610
Fixes #611
Fixes #613

tea run

A child opens no pull request, so each tea run is in that child build note:


🤖 Generated with Claude Code

https://claude.ai/code/session_01Lpn7jXU5Fmne1e9uLwewKC

Deviations

None.

usirin and others added 2 commits October 10, 2026 15:05
The contract for the opt-in --api mode: the API map input, the view,
diff and ratchet JSON, which changesets count, the read-only base-commit
rule, flags, the ./api library subpath and exit codes, with a worked
example per output on a three-subpath package.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lpn7jXU5Fmne1e9uLwewKC
usirin and others added 14 commits October 10, 2026 15:29
Records the founder ruling on #612 in MAINTAINING.md's semver section:
an added name owes at least a minor on stable and battery subpaths, any
bump on experimental, and no separate changelog callout on any tier.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lpn7jXU5Fmne1e9uLwewKC
…bpath (#609)

A new opt-in mode, `code-graph <package> --api <map>` and `readPublishedApi`
from the new `@demlik/code-graph/api` subpath. It emits the package's
declarations with the pinned tsgo into a temp folder outside the checkout,
reads each map entry's module symbol through resolve.ts's one
file-to-module-symbol step (now `moduleSymbolOf`), and prints, per subpath,
every published name with its emitted declaration text and the text of the
unpublished declarations it references. Without --api nothing else changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lpn7jXU5Fmne1e9uLwewKC
…a base commit (#610)

`code-graph <package> --api <map> --api-base <rev>` and `diffPublishedApi`
from `@demlik/code-graph/api` list, per export subpath, the names added,
removed and changed since <rev>, with before/after text and the subpath's
tier. The base commit's tree is written from git's objects (`git archive`)
into a temp folder outside the checkout, with the checkout's installed
node_modules linked in, and the same view runs there; the after side is the
working tree as it is. The checkout's files, index, branch and stash are
never written. A rev that names no commit exits 2 before anything runs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lpn7jXU5Fmne1e9uLwewKC
…sets (#611)

`code-graph <pkg> --api <map> --api-base <rev> --api-policy <file>` checks every added, removed
and changed name against the changesets added since the base, by the caller's bump policy: per
tier and change kind, the least bump and whether a callout is owed. It exits 0 on a pass, 1 on a
miss and 2 on a tier the policy gives no row. `readChangesetsSince`, `ratchetApiDiff` and
`BumpPolicySchema` ship from `@demlik/code-graph/api`.

The command line's entry to the mode moves into `api/cli.ts`, which brings `index.ts` back under
the self-gate's 400-line limit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…asks for (#613)

A vitest check in packages/tea/src/api-ratchet diffs every name tea publishes
against a base commit through @demlik/code-graph/api and fails, naming the
name, subpath, tier and before/after text, when a change lacks the changeset
MAINTAINING.md's semver policy asks for.

The API map is read, not written: subpaths from package.json exports, each
source entry from tsup.config.ts, each tier through the reference generator's
tier-table reader. build.yaml runs it on pull requests against the merge's
first parent, the PR's base commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hat moved (#613)

A published name folds in every type it reaches, so dropping one NoInfer from
expectCmdEmitted printed about 250 lines of unchanged types on each side. A
miss now shows the declaration text and the used types whose text differs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@usirin

usirin commented Oct 11, 2026

Copy link
Copy Markdown
Member Author

governance: PASS @ 608e337 content:9a14abcdafb5 — no contradiction, no weakening

Epic tail of issue 604, judged at head 608e337 against base f6339b0. The diff derives governance through one governed root: .github/ (.github/workflows/build.yaml). self false, so the self fence did not apply. Whether this needs a code-owner approval is a separate question CODEOWNERS answers.

Corpus half

No decision record is in the diff, so there was nothing to sweep. This is a hand read. The questions the change decides, each read against the live records:

  • May CI fail a tea pull request whose published-API change lacks the changeset its tier asks for? ADR 0010 (live, amended in part by 0016 and 0022) says every subpath carries one tier and each tier binds its own semver promise, with MAINTAINING.md as the living policy. The new step enforces that policy. It adds no tier and no promise. No contradiction.
  • What does a changed or removed name on a stable or battery subpath owe? ADR 0016 (live, accepted): one minor with a breaking-change note, no deprecation lag. tea's policy.ts asks for a minor with the **Breaking marker on both tiers. No contradiction.
  • What does an experimental change owe? ADR 0016 item 4 says experimental "removes silently", and the same record bans "a removal PR without a changeset" with no tier exception. The policy asks for a changeset of any bump and no callout, which fits both lines: the callout is what "silently" withdraws, the changeset is what the ban keeps. No contradiction.
  • What does an added name owe? No decision record covers it. MAINTAINING.md gains one sentence stating the rule, and policy.ts matches it. Nothing in ADR 0010, 0016 or 0022 says otherwise.
  • May tea's check call code-graph? ADR 0013 (live) and ADR 0008 (live) were read for the pipeline and CI-gate domain. The new step has the same shape as the reference-drift step and changes nothing they decide.

Gate half

governance guards at this head: 83 files scanned, 0 anchored invariants in reach, build.yaml the one guard-bearing file. I read the whole build.yaml diff and the whole file at the head. Two edits:

  • fetch-depth: 2 on the checkout. It fetches one more commit and removes nothing.
  • One new step, "Published API changes carry the changeset their tier asks for", on pull_request only, before "Run tests".

No existing step is removed, reordered, made conditional or given continue-on-error. The new step adds a refusal and softens none.

In reach but outside the governed root: packages/tea/tsconfig.json now excludes src/api-ratchet from the shipped program. tsconfig.test.json carries its own exclude list, which does not name that folder, and CI runs typecheck:test, so no file left the typechecked set.

No gate invariant in this diff's reach is removed or softened.

@usirin

usirin commented Oct 11, 2026

Copy link
Copy Markdown
Member Author

review-code: PASS @ 608e337 content:9a14abcdafb5 — all six epic criteria met; the tea cases reproduced at the head

Epic tail for issue 604, head 608e337 against base f6339b0. 83 files: 76 code, 7 doc. Graded set: 6 body criteria on the epic, 0 rulings, 0 unmarked owner comments. CI at this head is green on the one required context, test-and-build, and the new step ran in that pull-request run against the base commit: "api-ratchet: pass — 0 published names changed against f6339b0".

I read the whole diff, and the build-deviations comment on each of the six children (608, 609, 610, 611, 612, 613). To run the user's job I exported this head's tree into a throwaway repository outside any checkout, installed it, and ran the check there. No checkout of this repo was touched.

Criteria

1. The three realistic tea changes each get the policy's verdict, twice, byte-identical. PASS.
Run as uncommitted edits on top of this head, with TEA_API_BASE=HEAD pnpm --filter @demlik/tea run api:ratchet. Each case ran twice and the two outputs were compared with timing lines dropped.

Change Changeset Exit What it printed
Drop NoInfer from expectCmdEmitted none 1, 1 (identical) names expectCmdEmitted, ./testing, stable, with before cmd: NoInfer<C> and after cmd: C
same minor, no callout 1 "found a minor changeset with no callout"
same patch with **Breaking 1 "found a patch changeset with the callout"
same minor with **Breaking, for another package only 1 "found no changeset"
same minor with **Breaking 0, 0 passes
Widen MemoryStoreOptions.fenced from true to boolean (./mem, stable) none 1, 1 (identical) names the interface with before and after text
same minor with **Breaking 0 passes
Add a new export to ./mem none 1, 1 (identical) "added — needs a minor changeset; found no changeset"
same patch 1 "found a patch changeset"
same minor, no callout 0 passes, as the ruling on issue 612 says

Then what a user would try next. Four edits at once (remove noopRuntime from ./testing, add a name to ./agent, reword a doc comment, change a type nothing published uses): two rows reported, the removal and the experimental addition, each with its own rule, two runs identical. The doc-comment edit and the unreachable type were correctly not reported. A new subpath pointing at a file that already exists cannot slip through on tea: tea's own vitest config refuses an export whose src/<subpath>/index.ts is missing, so a new subpath is always a new file and reads as all added.

The diff side: packages/code-graph/src/api/ratchet/gate.test.ts runs the same shape end to end on a fixture repository for every policy row, and packages/tea/src/api-ratchet/api-ratchet.test.ts pins tea's policy rows and the verdict text.

2. The view matches tea-fabrika's declarations.ts reader. PASS on substance, and not to the letter. The owner should know this.
I had this re-measured at this head against usirin/tea-fabrika at 439b2e9, over all 26 code subpaths and 932 names. Two view runs were byte-identical.

  • Against the reader as published: 920 of 932 names. The 12 it lacks are real exports (RunHandle, Runtime, bindMachine, bootResume and eight more), and 10 more texts are cut short.
  • I read the cause in the reader's source. statementsOf ends an interface or class at the first closing brace at depth 0, and it does not count < as depth. So a generic constraint such as M extends an object type ends the statement early and swallows the next declaration.
  • With that one line fixed: 932 of 932 names in every subpath. The remaining text differences are all one of three kinds, and none is a wrong text in the view: the reader strips export declare from every statement of an overload and the view keeps it on the later ones (11 names); a line break left where a comment was removed (6); and two names in ./agent where tsc 5.7.3 and tsgo print one union in a different member order.

So the row as written is not met against the reader as it stands today, and the gap is the reader's defect. Matching it would mean dropping 12 published names. The builder disclosed this on issue 609 and that child's review accepted it. I accept it too, and I am saying it here in plain words because the criterion text still reads as an exact match. One number in the builder's note is softer than it reads: "932 of 932 texts" holds only with the looser normalization above.

3. A change to a non-exported type shows on the published name that uses it. PASS.
api.test.ts edits the private Step and asserts plain and increment change through references. diff.test.ts does the same for Options and make. On tea at this head, adding a field to the private AlarmSubData reported fromChromeAlarm in ./extension (stable) changed, with the type's before and after text.

4. Without the new mode, output is byte-identical. PASS.
runApiFlags in src/api/cli.ts returns null when none of the three flags is given, before any other mode runs. The only other change to existing code is moduleSymbolOf pulled out of moduleExport in src/resolve.ts, with the same two lines of logic. No existing test or golden is edited. I also ran the base CLI and the head CLI on one copy of packages/tea for 10 views (summary, --graph --pretty, --graph --headers, --plan --json, --smells --json, --tree, --edges --graph, --comments --json, --ci, --cycles --json) and on two of code-graph's own fixture folders for 3 more. stdout, stderr and the exit code matched in all 13. One fixture view differed only in the absolute path of my two scratch folders.

5. A diff run leaves the checkout as it was. PASS.
src/api/base.ts runs only rev-parse, ls-tree and archive, and writes into a temp folder. diff.test.ts compares file hashes, porcelain status, index, branch, HEAD and stash list before and after, on a clean tree and on a dirty one. On tea at this head, with one uncommitted edit, the status, the index, HEAD and the stash list were the same before and after a run, and no code-graph-api-* temp folder was left behind.

6. One module-export step, one tier reader, no dist to src rule in code-graph. PASS.
moduleSymbolOf in src/resolve.ts is the one file-to-module-symbol step, and src/api/read.ts imports it. packages/tea/src/api-ratchet/api-map.ts reads tiers only through parseTierTable from src/docs/reference/tier-table.ts. Nothing under packages/code-graph/src/api mentions dist: the subpath-to-source join lives in tea's api-map.ts and reads tsup.config.ts. A note, not from this diff: scripts/check-export-stamps.mjs and src/docs/export-tiers.test.ts already read the tier table their own way. They predate this work and the ratchet uses neither.

Evidence for marked criteria

Three child rows mark evidence outside the diff, and all three name the same source: the PR body's "tea run" section. I read that section on this pull request. It links one build note per child, and I read each note.

  • Issue 609, the view row. Evidence named: the PR body's "tea run" section, with the command, a diff against tea-fabrika's output and the timing. The linked note (issuecomment-6103055249 on 609) gives the command, 26 subpaths and 932 names, two byte-identical runs of 909,554 bytes, the comparison table against tea-fabrika and 0.93 s per run. My re-measure under row 2 above got the same 26, 932 and 909,554, and the same 920 and 12.
  • Issue 610, the diff row. Evidence named: the PR body's "tea run" section, with the commands and output. The linked note (issuecomment-6103196443 on 610) gives the command, the three changes, exactly three rows across 26 subpaths (DoStoreOptions changed, memoryStoreKind added, expectCmdEmitted changed), two byte-identical runs, and status, index, branch, HEAD and stash unchanged. My runs under rows 1 and 5 above show the same three kinds of row and the same unchanged checkout at this head.
  • Issue 613, the CI row. Evidence named: the PR body's "tea run" section, with each case's command and output. The linked note (issuecomment-6103558583 on 613) gives the command and, per case, the output and exit code with and without the changeset. My table under row 1 above reproduces every case at this head.

Standing checks

  • Test honesty: no existing test or assertion is changed or removed.
  • Release containment: the three flags are opt-in and each changeset says so. The CI step is on for every pull request and its comment in build.yaml says so.
  • tea's policy rows match MAINTAINING.md and ADR 0016. The experimental rows ask for a changeset of any bump, which issue 613 states and MAINTAINING.md calls "noting the change".
  • Silent failure, type design, test gaps: nothing blocking. TeaBumpPolicy is a record over Tier, so a tier with no row does not compile.

Follow-ups, not blocking

Deviations

The PR body says None. for the assembly. The branch is the six child ranges plus three merges of main, with no assembly-only commit, so that holds. Each child's build-deviations comment was read through wire read: 3 entries on 608, 5 on 609, 4 on 610, 6 on 611, 1 on 612, 4 on 613. Each matches what the diff shows. The two that touch a criterion are the tea-fabrika gap (row 2 above) and the removed-subpath gap (issue 622). Nothing undisclosed that this gate could see.

Verdict-written: 2026-10-11T00:35:03Z

@usirin

usirin commented Oct 11, 2026

Copy link
Copy Markdown
Member Author

review-doc: PASS @ 608e337 content:9a14abcdafb5 — docs match the code and sit on the right surfaces

Epic tail for issue 604, head 608e337. Doc class: 7 files. Three changesets, packages/code-graph/SPEC.md, packages/code-graph/docs/reference/cli.md, packages/code-graph/docs/reference/library.md and packages/tea/MAINTAINING.md. CI at this head is green, which covers dead links and leaked paths.

Criteria

The six epic criteria are about behaviour and are graded in the review-code verdict on this pull request. The doc slice carries what the plan asked of it:

  • SPEC.md gains §13 for the mode, with its inputs, JSON, determinism and the promise that existing output does not move. Present, with pointer rows in the usage table, the module layout and the flag table.
  • MAINTAINING.md changes by one sentence, the added-name rule. It matches the ruling on issue 612: at least a minor on stable and battery, any bump on experimental, no separate callout. The tier table and the rest of the policy text are untouched, as the no-go asks.

Hygiene

  • Right surface. The contract is in SPEC.md, the flag and library descriptions are in docs/reference/, the policy sentence is in MAINTAINING.md, and each release note is a changeset. No decision or pattern doc was needed here and none was added.
  • One mode per doc. cli.md and library.md are reference pages, and both new sections stay reference: flags, shapes, exits, one short example each. SPEC §13 is a specification and reads as one.
  • Supersession. Nothing is replaced. cli.md and library.md point at SPEC §13 for the full contract.
  • Status. No frontmatter or status line is in the slice.
  • Claims trace. I checked the falsifiable statements against the code and against runs at this head: the tsgo flag list, the exit codes, which changesets count, the verdict text format, "the checkout is never written", and "the same commits give the same bytes". Each holds. The three changesets are minor for @demlik/code-graph and each says the mode is opt-in.
  • Prose craft. Short sentences, plain words, examples where a shape is hard to hold in the head. Nothing I had to read twice.

Evidence for marked criteria

Three child rows mark evidence outside the diff. I read the PR body's "tea run" section on this pull request and the three build notes it links. No doc-class judgement here rests on them: they are graded in the review-code verdict, with my own runs at this head beside each.

  • Issue 609. Evidence named: the PR body's "tea run" section, with the command, a diff against tea-fabrika's output and the timing. Read in the note it links, issuecomment-6103055249 on 609: the command, 26 subpaths, 932 names, two byte-identical runs, the comparison table and 0.93 s per run.
  • Issue 610. Evidence named: the PR body's "tea run" section, with the commands and output. Read in the note it links, issuecomment-6103196443 on 610: the command and the three rows it printed, two byte-identical runs, the checkout unchanged.
  • Issue 613. Evidence named: the PR body's "tea run" section, with each case's command and output. Read in the note it links, issuecomment-6103558583 on 613: per case, the command, the output and the exit code with and without the changeset.

One finding, not blocking

SPEC §13 says a little less than the code does in five small places: computed member keys in the reference walk, the warn option, two exported option types, a second warning line on a diff run, and the extra changeset frontmatter forms. Each builder disclosed its own, and library.md already states the option and the types. Nothing in §13 is false. It is behind, and the SPEC is the document that is supposed to win, so I filed it: #624

Deviations

None. for the assembly holds: the branch is the child ranges plus merges of main. The doc-side child deviations (608: pointer rows added outside §13, and comments stripped from the text rule; 611: the §4 layout line rewritten to the real file names) each match the diff.

Verdict-written: 2026-10-11T00:35:56Z

@usirin
usirin marked this pull request as ready for review October 11, 2026 00:36
@usirin
usirin requested a review from a team as a code owner October 11, 2026 00:36
@usirin

usirin commented Oct 11, 2026

Copy link
Copy Markdown
Member Author

ship: AWAITING-CP-APPROVAL — PR #617 @ 608e337 → human

This pull request changes owner-protected files, and no owner approval is on it at head 608e337 (0 reviews read). Nothing was queued or merged. Merge intent was not armed (checked at site refuse).

Needed: an owner's approval at this head. The branch must not be rebased or force-pushed after that approval, or it has to be given again.

@usirin
usirin added this pull request to the merge queue Oct 11, 2026
Merged via the queue into main with commit f8f7b9e Oct 11, 2026
2 checks passed
@usirin
usirin deleted the epic/604 branch October 11, 2026 00:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment