Skip to content

Prepare the upstream typedef-doc issue; retitle the formatter todo - #1530

Merged
sergey-shandar merged 17 commits into
mainfrom
claude/upstream-issue-and-formatter-rename
Aug 14, 2026
Merged

Prepare the upstream typedef-doc issue; retitle the formatter todo#1530
sergey-shandar merged 17 commits into
mainfrom
claude/upstream-issue-and-formatter-rename

Conversation

@sergey-shandar

@sergey-shandar sergey-shandar commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Started as two docs-only close-out items from todo/migrate-typescript-to-mjs.md; review rounds grew it into three parts, one of them code-touching with a **BREAKING CHANGES:** CHANGELOG entry.

1. The upstream typedef-documentation issue is measured and ready to file

todo/blocked/jsdoc-typedef-doc-declaration-emit.md (following the todo/blocked/ convention with a Trigger section) carries minimal reproductions, a measured behavior matrix across five shapes, and a paste-ready upstream issue body. Filing the issue itself is an action on an external repository, so the file records it as the first task.

The measurement — hardened over four review rounds — corrects the earlier record:

  • The loss is shape-dependent. On tsc 7.0.2 (tsgo), a typedef block keeps its documentation only when a blank-line-separated comment block follows it. It loses the documentation when the block directly precedes a declaration (doc mis-attaches to the declaration), sits adjacent to another comment block (they merge into one trivia run, neither attaches), declares two typedefs, or is the last comment block in the file.
  • It is a strada → tsgo regression: tsc 5.9.3 kept (trimmed) prose on every emitted export type in all shapes. The upstream issue therefore targets microsoft/typescript-go; strada's milder tag-stripping/duplication is already tracked (#43534, #61664).

2. The last documented public typedefs move to types.ts (breaking)

A sweep found exactly three documented public @typedefs left in authored .mjs — the population exposed to the bug above. All three moved to new sibling types.ts files, where declaration emit preserves their documentation:

  • ParseContext / djsResultfjs/djs/transpiler/types.ts
  • Step<O>fjs/protocol/mcp/stdio/types.ts

The names leave the emitted module.f.d.mts (now @imported, not re-exported), which is the specifier-level break #1483/#1503 record for this kind of move — the CHANGELOG entry carries **BREAKING CHANGES:** and the "importers must use the types.ts specifier" clause. The stdio proof's Step import follows the move; now-unused type imports are pruned. The sweep measures zero documented public typedefs remaining in .mjs.

3. Formatter todo retitled

fjs/todo/formatter-for-f-js-and-f-ts-files.mdfjs/todo/formatter-for-f-js-files.md, naming .f.mjs plus the stage-2 .f.js as the targets.

Verification (latest head)

  • npx tsc and the prepack round-trip clean from a cleaned tree; npm test matches main exactly (a pure type move should not move the count, and it does not).
  • The emitted types.d.ts files carry the full prose on ParseContext, djsResult, and Step, with their @module headers surviving emit.
  • Zero elided repo-wide; no new unprefixed public export type beyond the three moved names.
  • todo/migrate-typescript-to-mjs.md items check off with this PR's number; the survivor inventory re-measured at 21 with its count history recorded.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF

todo/blocked/jsdoc-typedef-doc-declaration-emit.md carries a measured
minimal reproduction and a paste-ready upstream issue for tsgo dropping
JSDoc @typedef documentation in declaration emit. The measurement
corrects the earlier record: on tsc 7.0.2 the loss is shape-dependent —
standalone typedef blocks keep their documentation verbatim, while a
block touching a following declaration or declaring two typedefs loses
it, a regression relative to 5.9.3, which kept trimmed prose in every
shape. Filing targets microsoft/typescript-go accordingly.

The formatter todo drops the .f.ts extension from its name and title:
no such files can be authored any more.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
functionalscript afb2352 Commit Preview URL

Branch Preview URL
Aug 14 2026, 03:41 AM

Both reference #1530: the typedef-documentation gap is spun out into
blocked/jsdoc-typedef-doc-declaration-emit.md with the corrected
shape-dependent measurement, and the survivor inventory drops the
retitled formatter file (21 -> 20).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF
Keep main's #1526 outcome text for the header item and this branch's
check-off of the upstream-issue item it sits next to.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commenting — one factual error in the new issue's measurement table, plus a minor count note.

Verified at head c839da49 (the head moved from 6af5280e mid-review; the new commit is a
merge of origin/main c51e6dfa, and the branch's own content is unchanged — I re-ran
everything below at c839da49). Merge-base is now c51e6dfa, i.e. current origin/main.

Docs-only confirmed from the merge-base diff, not the title: three .md files, no code, so
no CHANGELOG entry is required (§8.3) and none was added — correct. Link check is
byte-identical to main (140 broken, same set), the rename leaves no dangling reference
to formatter-for-f-js-and-f-ts-files.md except the one in the [x] task that names it as
the old title on purpose, and both files the new issue links (jsdoc-typedef-strip-internal.md,
../migrate-typescript-to-mjs.md) exist.

1. The "standalone" row of the tsgo table is wrong when the block is last in the file

I ran the reproduction with the repo's own compiler (node_modules/.bin/tsc, version
7.0.2) and the flags the issue gives. repro.mjs's emitted repro.d.mts matches the
issue's quoted output exactly, character for character — the "precedes a declaration"
and "two typedefs in one block" rows are right.

The standalone row is not. It reads:

standalone — next line is another comment block or EOF | tsgo 7.0.2: full block kept, verbatim

The or EOF half does not hold. A file whose only content is one standalone typedef block:

/**
 * Only block, then EOF.
 *
 * @typedef {8} Only
 */

emits, on tsgo 7.0.2:

export type Only = 8;
/**
 * Only block, then EOF.
 *
 * @typedef {8} Only
 */

Only is bare and the block dangles after it. Same on the last block of a multi-block
file — with two standalone blocks declaring A then B, A keeps its block but B is
emitted bare and block 2 dangles at the end. The rule that actually fits every case I ran is
the final comment block in the file does not attach; a standalone block attaches only
when something else follows it.

This matters because it is load-bearing in the paste-ready upstream body, whose Expected
section says the emitted type should carry its documentation "as tsgo itself already does
when the typedef block is standalone". A maintainer reaching for the smallest possible
standalone repro — one block, nothing after it — gets the opposite result, and the report
looks wrong on its own terms. Suggest narrowing the row to "next line is another comment
block", and dropping or rewording the "already does when standalone" clause.

For what it is worth, the rest of the analysis holds up. I installed typescript@5.9.3 and
ran the same three files: strada keeps trimmed prose on Width, Rec, Rec2, Only, A
and Bevery shape including the EOF one — and additionally duplicates the original
block onto export const width. So the central claim, that this is a tsgo regression
relative to strada and that strada's own trimming/duplication are the separate older bugs,
is exactly right, and the EOF case is in fact one more shape tsgo loses rather than one it
keeps.

2. All 20 files that still contain the old extension measures 21 (pre-existing, carried forward)

Measured at c839da49, markdown files containing .f.ts anywhere with CHANGELOG.md
excluded (the ruler the surrounding table states): 21, not 20. The file the prose does
not list is fjs/emergent_testing/scenarios.md, whose line 41 reads
# scenario: path to a *.pass.f.ts, *.fail.f.ts, ... — deliberate like the rest, just never
enumerated. Counting the enumeration itself gives 20, so prose and number agree with each
other and both are one short of the tree.

This is not introduced here: main says 21 and measures 22, with the same file missing.
The delta this PR claims is correct — the formatter file really did leave the set, and the
measured set drops by exactly that one file and nothing else. So this is only a note, but
since the PR re-asserts the number and the paragraph right above it says "Re-measure with
the same resolve-against-the-tree method, at the final commit, before claiming a number", it
seems worth fixing while the sentence is already being edited: 21, with scenarios.md added
to the shouldLoad-quoting or superseded-convention group.

Not verified

  • Whether the upstream issue text lands well at microsoft/typescript-go; I only checked
    that the behaviour it describes reproduces.
  • The #43534 / #61664 characterizations, which I took at face value.

…is 21

The "or EOF" half of the standalone row was wrong, verified: a file
whose only content is one documented typedef block emits the type bare
with the block dangling after it. The table gains the EOF row, the
prose states the unifying rule review found — the final comment block
in the file never attaches; a typedef block attaches only when another
comment block follows it — and the upstream body's Expected clause no
longer claims tsgo handles the standalone case. The migration TODO's
summary is corrected the same way.

The survivor inventory measures 21, not 20: scenarios.md quotes the
deleted run.sh verbatim and was in the measured set but never
enumerated, on main as well. It joins the superseded-convention group
and the count paragraph records its own history.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF

Copy link
Copy Markdown
Contributor Author

Both findings addressed in 4f3942e.

1. The "or EOF" claim was wrong — reproduced your counter-example before fixing. A file containing nothing but one documented typedef block emits export type Only = 8; bare with the block dangling after it, exactly as you measured. The table now splits the standalone row in two (followed-by-comment-block: kept verbatim; last-block-in-file: bare), the prose states your unifying rule — the final comment block in the file never attaches, and a typedef block attaches only when another comment block follows it — and the EOF case is added as the smallest reproduction. The paste-ready upstream body's Expected clause no longer claims tsgo handles "standalone"; its Actual section gains the EOF case and the pattern statement, which strengthens the report rather than weakening it — as you said, one more shape tsgo loses. The corresponding summary in todo/migrate-typescript-to-mjs.md is corrected the same way, with the catch attributed to review.

2. Survivor count is 21. The missing file was fjs/emergent_testing/scenarios.md — which quotes the deleted run.sh verbatim, and which I wrote, making this a one-short count of my own making on both sides of the ledger. It now joins the superseded-convention group in the enumeration, and the count paragraph carries its history (205.md out via #1520, formatter out via #1530, scenarios.md never enumerated until review caught it) so the next re-measurement has the full trail.


Generated by Claude Code

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at head d7a12908 (previous review was at c839da49), against baseline origin/main = 373c3325 ("LL(1): match with an explicit stack and a shared-input cursor (#1531)").

Both items from the last round are fixed, and I re-derived them rather than taking the fix on trust.

1. The EOF row — fixed, and the new wording is correct. Splitting standalone — next line is another comment block or EOF into two rows is exactly right. I re-ran every shape the table now claims with the repo's own tsc 7.0.2 (tsc --allowJs --checkJs --declaration --emitDeclarationOnly --strict), not just the EOF one:

shape tsgo 7.0.2 observed table says ok?
standalone, blank line then another comment block block kept verbatim on export type Width, @example included full block kept, verbatim
standalone, last block in file (Only) export type Only = 8; bare, block dangles after bare, block dangles after
block directly followed by a declaration (Width) bare; block attaches to export declare const width bare; doc attaches to the following declaration
one block, two @typedefs (Rec/Rec2) both bare, block dangles after them both bare, block dangles after them

The quoted repro.d.mts output is byte-identical to what tsgo 7.0.2 actually emits — I compared the fenced block against the emitted file programmatically, quoted === actual is true. And strada 5.9.3 on the same four inputs keeps trimmed prose on every export type, including the EOF shape, so the "regression relative to strada" framing holds. The > as tsgo itself already does when the typedef block is followed by another comment block rewrite in the paste-ready body is now accurate for the shape I measured.

2. The .f.ts file count — fixed and now measures exactly. All 21 files that still contain the old extension — I measured 21 markdown files containing .f.ts at this head (33 files total, minus CHANGELOG.md as released history and the 11 .mjs/.ts sources that legitimately still match .f.ts in code such as shouldLoad). The enumerated list below the sentence also contains exactly 21 entries and matches the measured set element-for-element, fjs/emergent_testing/scenarios.md included. The parenthetical history of the count is accurate.


One new item, small, but it is in the paste-ready upstream body.

The generalization added in this round over-reaches. Table row 1 says the trigger is "next line is another comment block", and the upstream body states it as "The pattern across all cases: … a typedef block attaches only when another comment block follows it." What actually decides it is whether a blank line separates the two comment blocks — without one they form a single trivia run and neither attaches. Controlled pair, identical except for one blank line:

// b1.mjs — no blank line between the blocks
/**
 * Doc A.
 *
 * @typedef {8} A
 */
/**
 * Doc B.
 *
 * @typedef {9} B
 */

tsgo 7.0.2 emits:

export type A = 8;
export type B = 9;
/**
 * Doc A.
 *
 * @typedef {8} A
 */
/**
 * Doc B.
 *
 * @typedef {9} B
 */

Add one blank line after */ on line 5 (b2.mjs, the only difference) and Doc A moves back onto export type A while Doc B still dangles. So in b1 block A is followed by another comment block — literally on the next line, which is what row 1 says — and it is still emitted bare. The same thing happens with a trailing export {}: with the blocks adjacent, both are hoisted above export type A and B gets nothing.

This matters only because the sentence lives in the body you are about to file: a maintainer who tests the stated pattern with adjacent blocks refutes it in one paste, which is an easy way for a good report to get bounced. Two options, both cheap: scope the claim the way the surrounding prose already does honestly (the rule that fits every measured case) rather than across all cases, or tighten row 1's condition to "separated by a blank line from a following comment block". Nothing about the actual defect or the repro changes either way.

Pre-existing, not introduced by this PR, mentioned only so it is on the record: row 1's strada cell says original block also emitted, duplicated and misplaced. In the row-1 shape I measured, strada 5.9.3 emits only the single trimmed copy — no duplication. The duplication I can reproduce belongs to the "block directly followed by a declaration" shape, where strada emits the full original block on export const width: 8; and a trimmed copy on export type Width — and that row's strada cell says only "prose kept on export type, trimmed". The parenthetical looks like it is one row off. This text is unchanged from c839da49, so it is not a regression; worth a line the next time the table is touched.

Other gates. Docs-only change (todo/blocked/jsdoc-typedef-doc-declaration-emit.md added, todo/migrate-typescript-to-mjs.md edited, fjs/todo/formatter-for-f-js-and-f-ts-files.mdformatter-for-f-js-files.md), so per AGENTS.md §8.3 no CHANGELOG entry is required and none was added — correct. Broken-link check: 137 broken relative links in both trees and the sets are identical (diff clean over the full output, 159 lines each), so the rename stranded nothing. The only surviving mention of the old filename is the completed-task line that records the retitle, which is intentional and links the new path. No code touched, so no Rust or generator gates apply.

Everything except the one over-general sentence is verified correct. Happy to approve once that is scoped.

…o types.ts

Review round 2 refined the rule again, verified with the controlled
pair: two adjacent comment blocks merge into one trivia run and neither
attaches — the trigger is a blank-line-separated following comment
block, not merely a following one. The table gains the adjacent-blocks
row, the rule and the upstream body are scoped to measured cases, and
the strada duplication note moves to the row it belongs to
(followed-by-declaration), where review found it one row off.

Per maintainer direction, the exposure itself shrinks: the last three
documented public typedefs authored in .mjs move to sibling types.ts
files — ParseContext/djsResult to fjs/djs/transpiler/types.ts, Step to
fjs/protocol/mcp/stdio/types.ts — where declaration emit preserves
their documentation; the stdio proof imports Step from the new path,
and the now-unused List/OrderedMap/Unknown type imports are dropped.
A sweep measures zero documented public typedefs left in .mjs.
Verified: prepack-equivalent clean, docs present in emitted types.d.ts,
zero elided repo-wide, npm test 2540/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF

Copy link
Copy Markdown
Contributor Author

Round 2 addressed in 1822997, plus a scope extension per @sergey-shandar's direction.

The over-general sentence — fixed, your blank-line rule verified first. I reproduced your controlled pair before editing: adjacent blocks (b1) emit both types bare with both blocks dangling; adding the one blank line (b2) restores Doc A onto export type A. The table's first row now reads "separated by a blank line from a following comment block", a new row records the adjacent-blocks case with the trivia-run explanation, and both the prose rule and the upstream body's pattern statement are scoped to "every case measured so far" with the blank-line condition explicit — no maintainer can refute the stated pattern with a one-paste adjacent-blocks test now.

The one-row-off strada cell — also fixed while the table was open: the duplication note moved from the standalone row to the followed-by-declaration row, matching what you measured.

And the exposure itself shrinks. Following the maintainer's direction to move typedefs to types.ts where good: a sweep for documented public @typedefs authored in .mjs found exactly three left in the tree — ParseContext/djsResult in fjs/djs/transpiler and Step<O> in fjs/protocol/mcp/stdio. Both directories gained a types.ts; the modules @import from it, the stdio proof's Step import follows, and the now-unused List/OrderedMap/Unknown type imports are dropped. The sweep measures zero documented public typedefs remaining in .mjs, so the upstream bug's blast radius here is future code only — recorded in the issue file's Problem section. This makes the PR code-touching, so a CHANGELOG entry is added.

Verified at this head: npx tsc and the prepack round-trip clean, the moved docs present in the emitted types.d.ts, zero elided repo-wide, npm test 2540/0.


Generated by Claude Code

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewing at 1eae96fc. The head moved while I was working — I started at 18229970 and re-ran every gate at 1eae96fc; the only delta between them is a merge of main, so all numbers below are from the new head.

Baseline: origin/main at 16142109.

The previously open finding is fixed, and this time the fix is right

I did not assume it. I rebuilt the reproduction as seven separate single-shape fixtures and compiled each with both compilers — strada 5.9.3 (installed standalone) and tsgo 7.0.2 (the repo's pinned typescript) — under --allowJs --checkJs --declaration --emitDeclarationOnly --strict.

The controlled pair is the one that matters. Same two documented typedef blocks, the only difference being one blank line:

tsgo 7.0.2 emits
P1 block / P2 block, adjacent both blocks emitted as one run, then export type P1 and export type P2 both bare
P1 block / blank line / P2 block full block attaches to export type P1; P2 bare, its block dangling on the trailing export {}

So the corrected rule — the final comment block in the file never attaches, and a typedef block attaches only when a blank-line-separated comment block follows it — is what the compiler actually does. I checked it against all five table rows, and it predicts every one:

  • row 1 (blank-line-separated follower) — tsgo keeps the full block verbatim, @example and @typedef tags included. ✔
  • row 2 (last block in file) — bare, block dangles after. ✔
  • row 3 (two adjacent blocks) — both bare, neither attaches. ✔
  • row 4 (block then a declaration) — bare, doc attaches to the following export declare const. ✔
  • row 5 (one block, two @typedefs) — both bare. ✔

Row 1's strada cell is also correct now: I measured strada keeping trimmed prose on export type A with no duplication, while row 4's fixture is where strada emits the original block in full on the declaration and a trimmed copy on the type. That is exactly the split the previous round put in the wrong row, and it has been moved correctly.

One small imprecision, flagged only for accuracy and not worth a change on its own: row 5's "the block dangles after them" and row 2's "after it" are true of the reproduction as written (and of any such block that is not the first thing in the file), but when the block is the first thing in the file tsgo emits the orphaned run before the bare types instead — I measured both. Row 3's phrasing ("form one trivia run and neither attaches") is position-independent and correct as written; rows 2 and 5 describe the layout the repro happens to produce. The substantive claim — "emitted bare" — holds in every position.

The "sweep measures zero remaining" claim also checks out, and non-vacuously: the same sweep run against origin/main finds exactly three (ParseContext, djsResult in fjs/djs/transpiler, Step in fjs/protocol/mcp/stdio) and zero at this head.

One finding: the CHANGELOG entry is missing **BREAKING CHANGES:** (§8.4)

This is a public specifier change, and the repo has consistently treated it as breaking.

A name-keyed surface diff sees nothing — bin/extract.mjs output is byte-identical to main. The file-aware diff is where it shows:

- fjs/djs/transpiler/module.f.d.mts:22:export type ParseContext = {
- fjs/djs/transpiler/module.f.d.mts:27:export type djsResult = {
+ fjs/djs/transpiler/types.d.ts:11:export type djsResult = {
+ fjs/djs/transpiler/types.d.ts:21:export type ParseContext = {
- fjs/protocol/mcp/stdio/module.f.d.mts:41:export type Step<O extends Operation> = …
+ fjs/protocol/mcp/stdio/types.d.ts:14:export type Step<O extends Operation> = …

The three names are gone from the emitted module.f.d.mts — not re-exported, only @imported. import type { Step } from '…/stdio/module.f.mjs' compiles today and does not after this PR. The repository's own fjs/protocol/mcp/stdio/proof.f.mjs was importing Step from ./module.f.mjs and had to be updated in this very diff, which is the same break an external importer takes.

The precedent is unambiguous, and includes a pure type move with no .f.ts.f.mjs migration attached:

BREAKING CHANGES: every public type exported by an authored .f.mjs module moves to a sibling types.ts (31 modules, including asserts, types/list, types/bit_vec, types/result, text, crypto/sha2) — importers of these types must use the types.ts specifier [#1483]

and, for these exact two modules:

BREAKING CHANGES: fjs/djs/parser/module and proof … moving ParseError into a sibling types.ts — importers must use … the types.ts specifier for the type [#1503]

The #1503 entry for fjs/djs/transpiler even records that ParseContext and djsResult stay as JSDoc typedefs in the module — this PR is the change that moves them, so the same sentence is owed here.

This matters beyond bookkeeping: per §8.4's table, an unprefixed entry in ## Unreleased lets the next release ship as 0.44.1, and ^0.44.0 resolves it as a safe upgrade — for a consumer importing Step from module.f.mjs, it is not. Suggested shape, following the house wording:

BREAKING CHANGES: the last documented public typedefs authored in .mjs move to sibling types.ts files — ParseContext/djsResult to fjs/djs/transpiler/types.ts and Step to fjs/protocol/mcp/stdio/types.ts — so their documentation survives declaration emit; importers must use the types.ts specifier for these types [#1530]

Everything else is clean

  • npx tsc --noEmit — exit 0. npm run prepack from a clean tree — exit 0. npm test2540 / 0, same as main.
  • node bin/linkcheck.mjs — broken-link sets identical to main (159 lines both sides).
  • The documentation actually survives now, which is the point: the emitted fjs/djs/transpiler/types.d.ts and fjs/protocol/mcp/stdio/types.d.ts carry the full prose on ParseContext, djsResult, and Step.
  • §4 — both new types.ts files have the @module header with its blank line, and grep -c '@module' on each emitted declaration returns 1, so the header did not get folded into the first import type's trivia and elided. That is the exact trap these files are most exposed to, since every import in them is import type.
  • §6.2 — the three names are genuinely public and were public before; unprefixed is right, and no new unprefixed type appears anywhere else (file-aware export type listing: 589 lines on both sides, no other diff).
  • The @import lists in both .f.mjs files were correctly pruned of the imports that went with the moved typedefs.

Happy to approve once the CHANGELOG entry carries the prefix.

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addendum — the head moved again between my last re-read and posting.

I verified at 1eae96fc; the head is now 016adf64. I checked what moved rather than assuming: 1eae96fc..016adf64 is one more merge of main, and the entire content delta is #1534 (Remove Eff wrapper; convert all modules to raw step combinators), which landed on main in the meantime and which I reviewed and approved separately at 86710344.

None of this PR's own files changed — git diff 1eae96fc 016adf64 over fjs/djs/transpiler/{types.ts,module.f.mjs}, fjs/protocol/mcp/stdio/{types.ts,module.f.mjs}, todo/blocked/jsdoc-typedef-doc-declaration-emit.md, fjs/todo/formatter-for-f-js-files.md and todo/migrate-typescript-to-mjs.md is empty — and the CHANGELOG entry for #1530 is byte-identical at the new head, still without the **BREAKING CHANGES:** prefix.

So everything in my review above stands as written, and the one finding is unchanged. I did not re-run the gates at 016adf64; the numbers I quoted are from 1eae96fc, and the only difference is already-approved, already-merged code.

Removing ParseContext, djsResult, and Step from the emitted
module.f.d.mts is the specifier-level break #1483 and #1503 record for
exactly this kind of move — this repository's own stdio proof took the
same break in this diff. The entry gains the prefix and the "importers
must use the types.ts specifier" clause in the house wording. Also make
rows 2 and 5 of the measurement table position-independent per the
review's non-blocking note: a detached block lands after the bare types
in the reproductions, before them when it leads the file; emitted bare
is the invariant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF

Copy link
Copy Markdown
Contributor Author

Round 3 addressed in 3d353f5.

**BREAKING CHANGES:** prefix added, in your suggested house wording with the "importers must use the types.ts specifier for these types" clause. You're right on every count: the file-aware diff is the real ruler (the name-keyed surface being byte-identical is exactly why this class of break hides), the stdio proof's own import update in this diff is the same break an external importer takes, and #1503's entry even recorded ParseContext/djsResult as staying in the module — so this PR moving them owes the sentence #1503 promised.

The position-dependence note too, though you flagged it as not worth a change on its own: since the table was open anyway, rows 2 and 5 now say "the block dangles detached" with a shared parenthetical — after the bare types in the reproductions as written, before them when the block leads the file, "emitted bare" being the invariant. Your row-3 phrasing was already position-independent and is untouched.

Thanks for the three-round tightening of the measurement — the upstream body is considerably harder to refute than the first draft was.


Generated by Claude Code

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 2902a1a1 (previously commented at 016adf64; baseline origin/main = 67fb2dac, which this head merges).

The open finding is resolved. The typedef-move entry now carries **BREAKING CHANGES:** per §8.4, and it links only the PR. I re-derived the break rather than taking the fix on trust — after npm run prepack in both trees, a file-aware surface dump (1508 exported declarations on each side, so the probe is not vacuously empty) gives:

- fjs/djs/transpiler/module.f.d.mts      type ParseContext
- fjs/djs/transpiler/module.f.d.mts      type djsResult
- fjs/protocol/mcp/stdio/module.f.d.mts  type Step
+ fjs/djs/transpiler/types.d.ts          type ParseContext
+ fjs/djs/transpiler/types.d.ts          type djsResult
+ fjs/protocol/mcp/stdio/types.d.ts      type Step

and the name-keyed diff of the same data is empty — which is why the break is invisible unless the diff carries the file. Nothing else moves; no new unprefixed public export type anywhere.

One thing the round-3 correction got wrong

The new parenthetical in todo/blocked/jsdoc-typedef-doc-declaration-emit.md:41-43 is contradicted by this document's own smallest reproduction, two paragraphs below it:

(Where a detached block lands is position-dependent — after the bare types in the reproductions below, before them when the block is the first thing in the file. "Emitted bare" is the invariant.)

tsgo 7.0.2 emits export type Only = 8; bare, with the block dangling after it

In the only-block reproduction the block is the first thing in the file, so the two statements disagree about that one file. I rebuilt the fixtures rather than reasoning about it. Compiling it verbatim with tsgo 7.0.2 (--allowJs --checkJs --declaration --emitDeclarationOnly --strict):

export type Only = 8;
/**
 * Only block, then EOF.
 *
 * @typedef {8} Only
 */

The block is first in the file and lands after. Line 120's existing wording is right; the new parenthetical is wrong on the case it was added to cover, and the upstream issue body (:141-142, "with the block dangling after it") is right too — so the error is confined to the parenthetical.

"First thing in the file" is not the discriminator. Across seven single-shape fixtures the detached block precedes the bare types only when it is at the start of the file and a later statement exists:

fixture detached block lands
block at file start, export const f follows (two @typedefs) before the bare types
two adjacent blocks at file start, export {} follows before
block preceded by a statement, last in file (one @typedef) after
block preceded by a statement, last in file (two @typedefs) after
block is the entire file after
repro.mjs's multi-typedef block (statements both sides) after the bare types, before export {}

Since the document already says "emitted bare is the invariant", the simplest fix is to drop the positional clause, or reduce it to "where the detached block lands varies with the surrounding statements" — the two per-reproduction statements are already accurate on their own.

Severity: this is a todo/blocked/ document, not code, so I do not consider it a merge blocker. I am raising it rather than waving it through because this is the third correction to this passage and the second one to introduce a new inaccuracy while fixing the previous one.

What I re-verified this round (settled items not redone)

I did not re-derive the blank-line rule or the row-1/row-4 "duplicated" placement — those were settled at 016adf64 and neither the rule text nor rows 1, 3, 4 changed. I did re-run all seven fixtures under both strada 5.9.3 and tsgo 7.0.2 against the current table wording, since rows 2 and 5 were reworded:

  • Row 2 (standalone, last comment block in file) — tsgo: bare type, block detached; strada: trimmed prose kept. Matches.
  • Row 5 (one block, two @typedefs) — tsgo: both bare, block detached; strada: trimmed prose on both. Matches.

Both rewordings are correct in substance; only the parenthetical that accompanies them is not.

Also checked at this head:

  • npm test: pass: 2548, fail: 0 — identical to origin/main. A pure type move should not move the count, and it does not.
  • npm run prepack from a cleaned tree: exit 0 in both trees.
  • §4 module headers on the two new types.ts files survive declaration emit — @module count 1 in each emitted .d.ts, and the prose on djsResult, ParseContext and Step all reaches the declaration. The PR's stated purpose is achieved, not just asserted.
  • Broken-link sets: 159 entries in both trees, diff empty. The formatter-for-f-js-and-f-ts-files.mdformatter-for-f-js-files.md rename strands nothing; the one surviving mention of the old name is inside the checked-off task text in todo/migrate-typescript-to-mjs.md:1096, which is history, not a link.
  • The document's "a sweep measures zero remaining" claim (:26) — independently swept every authored .mjs for a documented unprefixed @typedef and got zero. Confirmed.
  • CHANGELOG §8.3: one entry, PR link only, released sections untouched.

Minor and not worth a commit on its own: the PR description still says "both docs-only (no CHANGELOG entry per §8.3)", which was true three rounds ago and is no longer.

The round-3 parenthetical said a detached block lands before the bare
types when it is the first thing in the file — contradicted by this
document's own only-block reproduction, where the block is first and
lands after. Review measured that position is not the discriminator at
all, so per its suggestion the parenthetical now claims nothing
positional: placement varies with the surrounding statements, the
per-reproduction statements are each measured, and emitted bare is the
invariant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF

Copy link
Copy Markdown
Contributor Author

Round 4 addressed in e021bef, plus the PR description refresh.

The wrong positional claim is gone. You caught me doing exactly the thing you warned about — a second correction introducing a new inaccuracy — and the contradiction with the document's own only-block reproduction two paragraphs below is conclusive on its face. Per your suggested simplest fix, the parenthetical now claims nothing positional: placement varies with the surrounding statements, the per-reproduction statements (which were both already accurate) stand as the measured record, and "emitted bare" is the invariant. Your fixture table showing "file start and a later statement exists" as the actual discriminator is noted, but I deliberately did not encode it into the document — after two failed generalizations of this passage, the safe wording is the one that asserts only what each reproduction shows.

The PR description is updated — the "both docs-only" framing was three rounds stale; it now describes the three parts including the breaking typedef move.

Thanks for the file-aware surface probe in this round — 1508 declarations a side with an empty name-keyed diff and exactly six file-keyed moves is the cleanest possible demonstration of both the break and its invisibility.


Generated by Claude Code

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 5. Head 4728f8a9; baseline origin/main = e7e1881a (which now contains
#1536 and #1537 — the branch merges them in, and against that baseline this PR's
own delta is still the same nine files). npx tsc --noEmit → 0; npm test
2551 / 0 fail, exactly matching main's 2551, as a pure type move should.

The round-4 parenthetical is fixed — correctly

The claim I falsified last round is gone, and the replacement is accurate. I
rebuilt every fixture and recompiled under both compilers rather than reading
the change:

  • Both reproductions in the document reproduce exactly as printed. The
    smallest one now reads "tsgo emits export type Only = 8; bare, with the
    block dangling after it" — which is what tsgo 7.0.2 actually does, and is the
    correction I asked for.
  • All five table rows reproduce as written, under both strada 5.9.3 and
    tsgo 7.0.2, including the strada column's trimming and the row-4 duplication.
  • "Where a detached block lands varies with the surrounding statements" is true:
    it lands after the type when the block ends the file, before the types when a
    statement follows the merged trivia run.

Finding: the generalized rule is falsified in both directions

the final comment block in the file never attaches, and a typedef block
attaches to its emitted type only when a blank-line-separated comment block
follows it

That sentence appears twice — at jsdoc-typedef-doc-declaration-emit.md ~line 45
and, more consequentially, inside the ready-to-file upstream issue body (~line
143). It is not what tsgo does.

I compiled a 2×5 matrix — {block is the first thing in the file, block is
preceded by a statement} × {EOF, adjacent statement, blank line + statement,
adjacent comment block, blank line + comment block} — under tsgo 7.0.2. Whether
the block attaches to its export type:

block preceded by EOF adjacent stmt blank + stmt adjacent comment blank + comment
nothing (file start) detached detached attached detached attached
a statement detached detached detached detached detached

The stated rule predicts attachment in the two "blank + comment" cells and
detachment in the two "blank + stmt" cells. Measured, the split runs the other
way — along the row, not the column. Two minimal pairs:

1. Necessary direction fails. A blank-line-separated statement — not a
comment block — is enough:

/**
 * doc
 *
 * @typedef {8} T
 */

export const post = 1

tsgo 7.0.2 emits the block in full, verbatim, on export type T = 8;. Remove
only the blank line and the type goes bare and the block moves to
export declare const post = 1 (that is table row 4). So the trigger is the
blank line, not the kind of thing that follows.

2. Sufficient direction fails. The exact shape the rule says attaches, with a
single statement prepended:

export const pre = 0

/**
 * doc
 *
 * @typedef {8} T
 */

/** unrelated */

export {}

A blank-line-separated comment block follows, and the block is not the final one
in the file — yet tsgo emits export type T = 8; bare, block dangling after
it. Delete the export const pre = 0 line and nothing else, and it attaches.

The sentence right below the rule inherits the error: "the documentation is lost
whenever the typedef block touches a following statement or comment block,
declares more than one type, or ends the file" predicts fixture 2 keeps its
documentation. It does not. Table row 1 is likewise under-qualified — it holds
only because its fixture puts the block at file start.

The rule that fits all ten measured cells: the block attaches to its emitted
export type only when it is the first thing in the file and a blank line
separates it from whatever follows
(statement or comment block); otherwise the
type is emitted bare. Both conditions are necessary.

I want to be precise about what this does and does not overturn. Round 4 removed
"the block is the first thing in the file" from the parenthetical, and removing
it there was right — as a claim about where a detached block lands it was
false, and the only-block reproduction disproves it. But file-start turns out to
be the real discriminator for attachment, and it is exactly what the rule is
missing. The property was dropped from the one place it did not belong and never
added to the one place it does.

This matters mainly because the upstream issue body would tell the TypeScript
team a rule their own compiler contradicts with a six-line file, which weakens an
otherwise well-measured report.

Confirmed unchanged

  • The strada claim now holds on a wider set than before: all ten matrix
    shapes keep (trimmed) prose on the emitted type under 5.9.3, so "kept the prose
    in every shape" is right and the strada → tsgo regression framing is right.
  • **BREAKING CHANGES:** prefix present on the typedef-move entry, with the
    types.ts specifier clause; entry links only /pull/1530.
  • ParseContext/djsResult/Step are present in the emitted types.d.ts and
    absent from module.f.d.mts; both types.d.ts files keep their @module
    header and contain no elided. Full prose survives on all three.
  • Public surface: 527 type aliases / 889 consts, name-keyed diff empty against
    main (the move is invisible to a name-keyed diff — the break is file-level,
    which is what the CHANGELOG entry says). No new unprefixed public export type.
  • linkcheck: 159 on both trees, identical sets — the formatter-todo rename
    stranded nothing.
  • The PR description's stale "docs-only (no CHANGELOG entry)" line from earlier
    rounds is gone; it now describes the breaking entry correctly.

One finding, in the document rather than the code.

Review falsified the previous rule in both directions with a 2x5
matrix: a blank-line-separated statement is enough when the block leads
the file, and a blank-line-separated comment follower is not enough
when a statement precedes the block. Both minimal pairs reproduced here
before editing, plus one out-of-sample cell the matrix did not cover —
a block preceded by a module header comment, the shape of every real
module: the header rides above the bare export type while the typedef's
own block mis-attaches to the following declaration, confirming the
rule's prediction and giving the upstream report its practically
important reproduction. The rule now reads: attaches only when first in
the file and blank-line-separated from what follows, both conditions
necessary. Table row 1 gains the file-start qualifier, the upstream
body and the migration TODO summary carry the corrected rule, and the
header-preceded reproduction is recorded verbatim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF

Copy link
Copy Markdown
Contributor Author

Round 5 addressed in c8268b6 — and your matrix pointed at something bigger than a wording fix.

Your rule adopted, both minimal pairs reproduced first. The blank-line-separated statement attaching (at file start) and the blank-line-separated comment follower failing to attach (statement-preceded) both reproduce exactly as you measured. The rule everywhere — table row 1 (now qualified with file-start), the prose, the upstream body, and the migration TODO's summary — reads: a typedef block attaches to its emitted export type only when it is the first thing in the file and a blank line separates it from whatever follows; both conditions necessary.

One out-of-sample cell your matrix didn't cover, measured before adopting the rule: a block preceded by a comment (a module header) rather than a statement. It confirms the rule's prediction — and it's the practically important case, since every real module starts with a header: T's own documentation never attaches, the header rides above the bare export type, and the doc block mis-attaches to the following declaration. Delete the header and the same input attaches in full. That reproduction is now recorded verbatim in the document and stated in the upstream body's Actual section — it turns the report from "documentation is lost in some shapes" into "in any realistically-shaped module, typedef documentation is always lost", which also explains why this repository's sha2 case looked universal when first recorded.

You were right about the passage's track record; the discipline this round was reproducing every claim — yours and the new cell — before writing any of them down.


Generated by Claude Code

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at afb2352f (previous review was at 4728f8a9). Baseline: origin/main = 87c58701.

The generalized rule is now correct. Given that this passage has been through five successive corrections, I did not take the new wording on its word: I rebuilt the fixtures from scratch and re-measured every shape the wording asserts, under both compilers.

The rule, re-measured

New wording: "a typedef block attaches to its emitted export type only when it is the first thing in the file and a blank line separates it from whatever follows — statement or comment block alike; both conditions are necessary."

Eleven fixtures, each compiled with tsc --allowJs --checkJs --declaration --emitDeclarationOnly --strict, under strada 5.9.3 and tsgo 7.0.2 (the repo's pinned =7.0.2). "Attached" = the doc block appears immediately above the emitted type Width.

fixture strada 5.9.3 tsgo 7.0.2 rule predicts
first-in-file, EOF attached bare bare
first-in-file, adjacent statement attached bare bare
first-in-file, blank + statement attached attached attached
first-in-file, adjacent comment block attached bare bare
first-in-file, blank + comment block attached attached attached
statement-preceded × all five followers attached (5/5) bare (5/5) bare
module-header-preceded, blank + statement attached bare bare

11/11 match. Both conditions really are necessary and neither alone is sufficient: the only two attaching shapes are first-in-file and blank-separated, and the follower's kind does not matter — which is exactly what the previous wording got wrong in both directions.

The supporting details check out too:

  • Row 1's "full block kept, verbatim" on tsgo — confirmed, the emitted block still carries the @typedef tag.
  • Row 4's strada column ("trimmed, and the original block also emitted in full on the declaration, duplicated") — confirmed: strada emits the untrimmed block on export const after: 1 and a second, tag-stripped copy on export type Width.
  • The header-preceded narrative — confirmed on the doc's own repro, byte for byte: tsgo emits the module header above a bare export type T = 8; and lands the typedef's block on export declare const post = 1.
  • "Delete the header block … and the same input attaches the doc to export type T in full" — confirmed by running exactly that deletion; the doc block moves onto export type T = 8;. (This one had a real chance of being wrong, since it only holds because the repro keeps a blank line before export const post = 1. It does.)

The corrected rule is carried identically into the ready-to-file upstream issue body.

One nit in the upstream issue body (non-blocking)

Under Expected, "as tsgo itself already does when the typedef block is followed by another comment block" is looser than the rule stated three lines later. Measured, a following comment block attaches only when the typedef block is also first in the file and blank-separated: first-in-file + adjacent comment is bare, and statement-preceded + blank + comment is bare. Since this text is going to upstream maintainers, "when the typedef block is first in the file and blank-separated from what follows" would avoid handing them a claim they can falsify in one paste.

Battery

  • npx tsc --noEmit — exit 0.
  • npm run prepack from a freshly cleaned tree — exit 0.
  • npm test — 2553 pass / 0 fail, exactly matching origin/main.
  • Link check — broken-link sets are identical to origin/main (137 on both, comm empty in both directions). The formatter-for-f-ts-files.mdformatter-for-f-js-files.md rename stranded nothing.
  • §4 module headers survive in the two new declarations: @module count is 1 in each of fjs/djs/transpiler/types.d.ts and fjs/protocol/mcp/stdio/types.d.ts.
  • CHANGELOG carries the **BREAKING CHANGES:** prefix and links only /pull/1530.

Settled, not re-litigated

The break itself was independently re-derived at 4728f8a9, and the round-4 parenthetical fix was confirmed then; git diff 4728f8a9 afb2352f touches only the two todo documents and the merge of main, so nothing under fjs/ moved since.

@sergey-shandar
sergey-shandar added this pull request to the merge queue Aug 14, 2026
Merged via the queue into main with commit 5f5c687 Aug 14, 2026
19 checks passed
@sergey-shandar
sergey-shandar deleted the claude/upstream-issue-and-formatter-rename branch August 14, 2026 06:14
@sergey-shandar sergey-shandar mentioned this pull request Aug 14, 2026
sergey-shandar pushed a commit that referenced this pull request Aug 14, 2026
Release under the new directory-per-version changelog workflow:
changelog/unreleased/ is renamed to changelog/0.45.0/ with its entry
files kept as they are (.gitkeep dropped — the next entry PR recreates
unreleased/). Minor bump: the release contains BREAKING CHANGES entries
(#1516, #1520, #1530, #1531, #1547).

Update AGENTS.md §8.3–8.4 and changelog/README.md for the new workflow:
releasing renames the directory instead of concatenating entries, and
future entries carry no PR number or link inside the file — the file
name already has it. Released entries are kept as-is. Extend
todo/changelog-website.md so the future generator reads both release
forms: <version>.md files (through 0.44.0) and <version>/ directories.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M296KXwQHHuUGhryRReKpJ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants