Skip to content

ci: check the packed package without a checkout - #1767

Merged
sergey-shandar merged 16 commits into
mainfrom
claude/private-ts-todo-partial-bydyc9
Aug 29, 2026
Merged

ci: check the packed package without a checkout#1767
sergey-shandar merged 16 commits into
mainfrom
claude/private-ts-todo-partial-bydyc9

Conversation

@sergey-shandar

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

Copy link
Copy Markdown
Contributor

Step 3 of Stage 2, and the one the design exists for. Steps 1 (#1762, the ordering edge) and 2 (#1763, the artifact) are both in main; this is their first consumer.

A new package-check job downloads the tarball the Node job uploads, installs it as a real dependency, and type-checks every declaration the package ships.

Why it is built by hand

Deliberately not built through toSteps: that helper injects actions/checkout, and the missing checkout is the whole point. With no repository on the runner there is no tsconfig.json up the tree to inherit, no node_modules to resolve into, and no source file that can stand in for a declaration the tarball omits. The job sees what a consumer sees, and nothing else.

The job

npm init -y > /dev/null
npm install "packed@file:$(echo *.tgz)"
npm install "typescript@=7.0.2"
echo '{"include":["node_modules/packed/**/*","node_modules/packed/**/.*","node_modules/packed/**/.*/**/*"],"exclude":[],"compilerOptions":{…}}' > tsconfig.json
npx tsc

One command per step, so a failure names what failed rather than arriving as an opaque script.

tsc enumerates what it checks. An earlier revision walked the tree with find, guarded the result with test -s, and passed it through xargs -0; review found three defects in that mechanism — a missing .d.cts, a quoting hazard, and xargs silently partitioning a large package into several separate programs. include has none of them, and root AGENTS.md §6 asks for exactly this: the tool that parses what it checks, rather than a pattern that approximates one. find, xargs, test and ls are all gone.

The three patterns are one rule npm and TypeScript disagree about: npm's ** walks into a dot-prefixed name and TypeScript's does not, so files publishes .d/x.d.ts and a lone ** leaves it unchecked — even when it is the package's types entry point. Naming a dot segment explicitly does match. Two dot segments in a row still escape; the generator's comment says so rather than implying completeness.

Four properties decide whether it can fail at all

Each is asserted, because each has a silent failure mode:

Property What it prevents
tsc enumerates the installed artifact, dot-prefixed paths included a written list cannot see a module that gains a private type module later — the case this exists to catch
the package installed under a fixed alias, never this repository's name for a project that merely depends on functionalscript, a hard-coded path checks that dependency and reports green having never seen the artifact
skipLibCheck stated as false true silently stops the checking — the job passes having opened nothing
compiler read from package.json, and only an exact pin no checkout means no lockfile; otherwise the registry decides the verdict

The last one is a gate, not a default: an exact devDependencies.typescript generates the job, and anything else — a range, a prerelease, no entry — generates no job at all. fjs/ci/README.md documents that contract.

Verified by running it, not by reading it

I extracted the emitted steps verbatim from ci.yml and ran them against real tarballs:

  • this repository's package: 396 declarations, TypeScript 7.0.2 from the package.json pin, exit 0;
  • deleting all 16 private.d.ts leaves 380 and still exits 0 — so the exclusion in step 5 is safe;
  • a dangling ./gone.js reference in a packed declaration exits 2 with TS2307 — and the same break passes silently under skipLibCheck: true, which is why it is stated;
  • a second tarball actually named other-package, shipping a single .d.cts under a directory named we"ird dir: enumerated and checked, exit 0;
  • a package whose types is a broken .hidden/b.d.ts: exit 2. Before the dot patterns it exited 0, having installed that file and never opened it;
  • all four dot arrangements — visible, dot-named file, top-level dot directory, nested dot directory — enumerated;
  • a package shipping no declarations at all: exit 2, TS18003, naming the pattern that found nothing;
  • two tarballs in the directory: exit 254, npm naming both files — no silent wrong subject.

In real CI on this branch, package-check started five seconds after node26 uploaded the artifact, and the whole job ran in twelve seconds.

The job is green today because nothing is excluded yet. Its value appears when the files negation lands in step 5 — which is exactly why the guard comes first.

Proof

Eight mutants: a checkout step, a dropped needs edge, a needs edge pointing at a job that does not produce the artifact, skipLibCheck true, a hard-coded package name, an include that does not reach into node_modules, and either dot pattern removed.

The proof supplies its own compiler pin rather than importing the generator's, so an assertion that finds it has found the value passed in. An earlier revision compared a constant with itself and held for any value.

Where the review went

Ten findings, most of them real defects. Per REVIEW.md, none of the answers is left in a thread:

  • Fixed in the diff, with the reason as a comment beside the decision it explains: why tsc enumerates rather than a shell walk (and why not to go back), why ** rather than a list of extensions, why the dot patterns exist and what they still miss, why skipLibCheck is stated, why there is no guard against a second .tgz, which external tools remain and why.
  • Documented in fjs/ci/README.md: the package.json contract, and the package/ module in the file list, answering the first question a reader has — why this job has no checkout.
  • Filed in fjs/ci/todo/package-check-unsupported-package-shapes.md: the declines and the one partial fix, each with what completing it would cost and the condition that makes it worth doing.

Two existing assertions moved, both by design

  • Job count 13 → 14.
  • jobNeeds asserted that no job ordered itself. That guard fired exactly when its first consumer arrived — it now pins one ordering edge, so a second stays a deliberate change.

Neither was loosened to accommodate the diff; both now pin the new truth.

Checks

npx tsc clean; 3479/3479; coverage 100%; npm run ci-update round-trips. All 20 checks green on the head.

Open for the repository owner

§6 sign-off. What remains is npm, npx and tsc. My reading is that these are what §6 endorses — tsc is the parser, npm is the thing under test — but §6 says approval first, so the thread is open rather than closed by me. It also gates the complete fix for the dot-in-dot gap, which needs something that reads the installed tree's real names.

Next

Step 4 is making this a required check — a branch-protection setting, and not something I can do. Until then the job runs but does not block a merge, which is the point of it.

Changelog

changelog/unreleased/1767.md

🤖 Generated with Claude Code

https://claude.ai/code/session_01LqeS5t2ZKkSu3chRPMXR7n

The check Stage 2 exists for. A new `package-check` job downloads the tarball
uploaded by node26, installs it as a real dependency, and type-checks every
declaration the package ships.

Deliberately not built through `toSteps`: that helper injects
`actions/checkout`, and the missing checkout is the whole point. With no
repository on the runner there is no tsconfig.json up the tree to inherit, no
node_modules to resolve into, and no source file that can stand in for a
declaration the tarball omits, so the job sees what a consumer sees.

Four properties decide whether it can fail at all, and each is asserted:
declarations are enumerated from the installed artifact rather than a written
list, so a module that gains a private type module later is checked without the
job being edited; skipLibCheck stays at its false default; the file list is
non-empty, which is the one way the job could look healthy while checking
nothing; and the compiler is the package's own pin, read out of the packed
package.json, since without a checkout there is no lockfile and the registry
would otherwise decide the verdict.

Ran the emitted script verbatim against a real tarball rather than trusting the
generator: 395 declarations, TypeScript 7.0.2 resolved from the packed pin,
exit 0. Removing the 16 private.d.ts leaves 379 and still exits 0, so the
exclusion in the next step is safe; appending a dangling private import to one
packed declaration exits 2 with TS2307. The job is green today because nothing
is excluded yet — its value appears when the negation lands, which is why the
guard comes first.

The proof kills five mutants: a checkout step, a dropped needs edge, a needs
edge pointing at a job that does not produce the artifact, skipLibCheck true,
and a dropped empty-list guard.

Two existing assertions moved, both by design rather than accommodation. The
job count is 13 -> 14, and jobNeeds asserted no job ordered itself; that guard
fired exactly when its first consumer arrived and now pins one ordering edge,
so a second stays a deliberate change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqeS5t2ZKkSu3chRPMXR7n
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for security reviews. Please try again later.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 28, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-29T00:17:05.129009Z a194225 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 28, 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 a194225 Commit Preview URL

Branch Preview URL
Aug 29 2026, 12:15 AM

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ff9f8f92ff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread fjs/ci/package/module.f.mjs Outdated
Comment thread fjs/ci/package/module.f.mjs Outdated
Comment thread fjs/ci/module.f.mjs Outdated
Three review findings, all correct.

The script hard-coded node_modules/functionalscript. `fjs ci` is a public
command that generates workflows for other projects, and the proof already
exercises a package named other-package, so for them the check would fail on a
missing directory — or worse, silently validate a `functionalscript` dependency
that happens to be installed instead of the artifact just built. That is a
plausible wrong answer where DESIGN.md §10 requires a refusal. The package
directory is now derived from the tarball, by reading back the single
dependency `npm install ./*.tgz` writes.

Verified by building a second tarball named other-package and running the
emitted script against it: resolves other-package, finds its declaration, exit
0. Against this repository's own tarball: 395 declarations, TypeScript 7.0.2,
exit 0. A package with no compiler pin exits non-zero rather than installing a
floating compiler.

The regex `/^=/` is gone, and not by rewriting it: npm accepts the `=7.0.2`
range verbatim, so the stripping was never needed. fjs/AGENTS.md forbids
regular expressions; the fix removes the reason for one rather than the syntax.

Adding a job changes the workflow `fjs ci` emits for every caller, which is a
behavior change for users of the package rather than an internal refactor, so
this adds the changelog entry it needs.

The proof now pins the derivation and asserts the hard-coded name is absent, so
the regression cannot come back quietly.

Changelog: changelog/unreleased/1767.md

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 837eda6ab5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread fjs/ci/package/module.f.mjs Outdated
Comment thread fjs/ci/package/module.f.mjs
Comment thread fjs/ci/package/module.f.mjs
claude added 2 commits August 28, 2026 21:49
Three more review findings, all correct.

The module header lacked @module. fjs/AGENTS.md §2 requires it on every
module.* entry point, and all five sibling fjs/ci modules carry one; #1756
removed the tag from non-module files, not from these.

The proof lived in the parent. fjs/ci/deno, fjs/ci/nix and fjs/ci/node all have
co-located proofs, so the precedent is the opposite of what I assumed. The job's
own shape is now proved in fjs/ci/package/proof.f.mjs, and the parent keeps only
what the assembled workflow can show: that the job is wired in, and that the job
it waits for is really the one that uploads — an edge pointing at a job that
never produces the artifact would satisfy the ordering and still never run.

The compiler pin was the substantive one. Reading devDependencies.typescript
gives a dependency specification, not a resolved version: this repository pins
=7.0.2, but a project writing ^7.0.0 would have had npm choose, which is exactly
the determinism the checkout-less job exists to preserve. The script now
compares the installed version against the literal pin and refuses when they
differ.

That case is worse than it looks, and the test shows why: ^7.0.0 installs 7.0.2
today, so a range is indistinguishable from an exact pin until the day the
registry publishes 7.0.3 and the verdict moves with no change to the package.
Verified: ^7.0.0 exits non-zero, =7.0.2 exits 0 at 396 declarations, and a
second tarball named other-package still resolves and passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqeS5t2ZKkSu3chRPMXR7n
The check was a single run: step, so a failure arrived as "the script failed"
rather than naming which part. It is now four steps — install the artifact,
install the pinned compiler, enumerate the declarations, type-check them — so
the GitHub UI attributes a failure to the stage that produced it.

Shell variables do not survive between steps. Only one value actually crosses a
boundary: pkg, the artifact's package name, which now travels through
$GITHUB_ENV. ts, installed and exact stay local to the compiler step, and
declarations.txt and node_modules cross as files, which need no mechanism.

Verified by running the four emitted scripts in sequence with $GITHUB_ENV
emulated: all pass, pkg carries across, 396 declarations, TypeScript 7.0.2 —
the same result the single script produced. The attribution works too: a range
pin fails at the compiler step, and a dangling declaration fails at the
type-check step with TS2307, rather than both surfacing as one opaque failure.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 389c443c4e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread fjs/ci/package/module.f.mjs Outdated
Comment thread fjs/ci/package/module.f.mjs Outdated
Simplicity over the narrower case. `npm install "packed@file:$(ls *.tgz)"`
installs the tarball under a fixed directory name, so every later step names it
literally: the name derivation and the $GITHUB_ENV hand-off both go away, and no
value crosses a step boundary any more.

Nothing is baked into the generated workflow that package.json can change, so a
version bump still regenerates nothing — which the derived name also achieved,
but this achieves it with less machinery.

What an alias gives up is recorded next to it rather than left in a review
thread: a package that imports itself by name, legal once `exports` is declared,
does not resolve under a different directory name, so such a package would fail
a check a real consumer passes. Nothing here self-references, and the comment
says to revisit if that changes.

Re-verified end to end on the four emitted steps: this repository's tarball
passes at 396 declarations, a tarball named other-package passes at 1 — the
alias makes genericity structural rather than derived — and failures still
attribute per step, a range pin at the compiler step and a dangling declaration
at the type-check step with TS2307.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9c1ee71c5d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread fjs/ci/package/module.f.mjs Outdated
The steps were unreadable because they did work that belongs in the generator.
Thirty lines of run: script become eight.

set -eu is gone. GitHub runs run: steps as `bash -e`, so -e was already there,
and -u guarded unset variables only while a value crossed steps through
$GITHUB_ENV; nothing crosses now. Verified by running the emitted steps under
`bash -e`: failures still propagate and still land in the right step.

The compiler version moves to fjs/ci/config, beside the bun, deno, node and
wasmtime pins. That deletes the whole of the second step: reading the pin out of
the packed package.json at run time, installing it, re-reading what was actually
installed, and stripping a leading `=` through an environment variable to
compare the two. All of it existed to establish a version this repository
already states, and now states in the one place tool versions live. One line
remains.

This changes what the pin means, which is worth saying plainly: the check now
installs the version this repository pins rather than the one the artifact
declares. For a package generated by `fjs ci` elsewhere that is the CI tool's
compiler, exactly as the node, deno and bun versions in the same file already
are. It also removes the range hole by construction — a config constant cannot
be a floating range without that being a visible change here.

Also enumerates *.d.cts, so "every declaration the package ships" is true for a
package that ships CommonJS declarations, and requires exactly one archive, so
which package is under test is never ambiguous.

Verified end to end on the emitted steps: 396 declarations and TypeScript 7.0.2
for this package, a .d.cts-only package enumerated and checked, two archives
refused, and a dangling declaration still failing at the type-check step with
TS2307.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 26a6c2fb78

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread fjs/ci/config/module.f.mjs Outdated
Comment thread fjs/ci/package/module.f.mjs Outdated
// a hand-written import list cannot see a module that gains a private type
// module later, which is the case this check exists to catch. An empty list
// would type-check nothing and pass.
const enumerateDeclarations = /** @type {const} */ (`find node_modules/${alias} \\( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' \\) > declarations.txt

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid compiling mutually exclusive declaration trees together

When a valid package ships version-specific declarations selected through typesVersions, this find supplies both the selected and fallback trees as roots in the same TypeScript program. For example, root and ts4.8/ declarations of the same global compile independently for their intended compiler versions but fail here with duplicate identifiers, breaking generated CI for such packages; honor the package's declaration selection or check mutually exclusive trees separately.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not doing this one, and I want to be explicit that it is a decision rather than an oversight.

The finding is technically correct: a package using typesVersions ships mutually exclusive declaration trees, and enumerating all of them as roots of one program would collide on duplicate identifiers.

It does not apply to this repository. package.json has no typesVersions, no ts*/ tree, and one declaration per module. Honouring version selection means reading typesVersions, matching the compiler against its ranges, and resolving the mapping — a resolver's worth of shell for a shape we do not have.

The reason I'm declining rather than deferring: this generator exists to build our CI. fjs ci working for other projects is a side effect, not a product commitment, so "breaks generated CI for such packages" is not a cost we are carrying today. Several findings on this PR were right precisely because they were about our repo — the hard-coded package name, the compiler pin, the response-file quoting — and I fixed those. This one is about a package shape nobody here publishes.

If that changes, the failure is loud (duplicate-identifier errors, not a silent pass), which is the right way for an unsupported input to surface.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Filed, so the decline survives the merge: fjs/ci/todo/package-check-unsupported-package-shapes.md,`` in f0364d3 — issue 1.

Writing it down sharpened the answer I gave you. typesVersions does not affect coverage, because the job checks every declaration the tarball ships, which is a superset of what any entry point reaches. What it does affect is whether the map itself is exercised: a package whose typesVersions points at a path it does not ship gets a green check today. That is a narrower and more accurate statement of the gap than "declined as out of scope", and it is in the file.


Generated by Claude Code

tsc splits an @response file on whitespace, so a declaration under a directory
with a space in its name was read as two root files. Reproduced before fixing:
a package shipping "space dir/a.d.ts" failed with TS6053 "File 'dir/a.d.ts' not
found" — a valid package rejected by the check rather than by its own contents.

find -printf '"%p"\n' quotes each path. One flag, no extra command: the same
package now passes, this repository's 396 declarations are unchanged, and a
dangling private import is still caught with TS2307.

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

@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.

The redesign is cleaner and the quoting fix is real. One thing I think should land before merge rather than after.

The compiler pin now exists in two unlinked places, and the new assertion cannot see them disagree. fjs/ci/config/module.f.mjs carries typescript = '=7.0.2'; package.json carries "typescript": "=7.0.2". Nothing derives one from the other. The proof reads assert(scriptHas("typescript@${typescript}")) — importing the same constant the generator interpolated, so it compares the value with itself and holds for any value.

Setting config to =5.9.3 leaves the suite at 3538/3538, and the job then installs 5.9.3 and type-checks clean: exit 0, green CI, wrong compiler. That is the first gap in this series silent at both layers — every earlier one (alias mismatch, cross-step wiring) still went red on a real runner. Note it is not reliably loud even by accident: =7.0.1 happens not to exist and fails at install, so whether drift is noticed depends on whether the drifted version was ever published.

Two smaller pieces of the same shape: nothing requires the config constant to be an exact pin, so ^7.0.2 would install whatever the registry serves that day with the proof still green; and the accept/refuse matrix I verified at f8b224d13 describes a mechanism that no longer exists — the installed-vs-declared comparison is deleted, not moved. An assertion in proof.f.mjs reading the repo's own package.json and comparing it to the constant closes the first; a shape check on the constant closes the second.

The rest verifies clean. The quoting fix is genuine: a declaration path containing a space fails the old response file with real tsc 7.0.2 (TS6053, TS6231, exit 2) and passes the new one, and every line comes from the single -printf '"%p"', so it is complete. All three of my standing findings retire with the code they were about — including set -eu, correctly, since the runner's implicit bash -e supplies errexit and no unset dereference remains; the one pipe still trips its guard. Proof changes map cleanly with nothing orphaned. Round-trip empty, gates tsc 0 and npm test 3538/3538.

Still true, and still the right order to build in: the defect class this job exists for is not reproducible on HEAD until files gains !**/private.d.ts.

Review found the compiler pin living in two unlinked places — fjs/ci/config and
package.json — with a proof that could not see them disagree, because it
imported the same constant the generator interpolated and so compared a value
with itself. Setting the config constant to =5.9.3 left the suite green and the
job installing the wrong compiler: silent at both layers, unlike every earlier
gap in this series.

The fix is not a better assertion. It is having one place. The generator already
reads Cargo.toml, so it now reads package.json and takes devDependencies
.typescript from there. The config constant is gone, and with it the thing that
could drift. Changing the pin now changes ci.yml, which node26's existing
regeneration check already guards.

The proof supplies its own pin, one no configuration holds, so finding it proves
the value came from the parameter rather than from a constant compared with
itself.

Without a pin the check cannot run deterministically, so it is not generated
rather than run against a compiler nobody chose; a proof covers each shape that
means "no pin", which is also what keeps branch coverage at 100%.

Comments no longer travel into the emitted commands: ci.yml is output, and the
reasoning belongs in the generator that a maintainer reads. The archive-count
guard goes too — two archives already make npm's spec malformed and fail
loudly, so it only restated a failure that happens anyway.

Verified: 396 declarations and TypeScript 7.0.2 from package.json's own pin, a
dangling private import still TS2307 at the type-check step, 3478/3478, coverage
100%, round-trip clean.

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

Copy link
Copy Markdown
Contributor Author

The tautological assertion is the best finding on this PR. Fixed in 5688ffa — but not the way you proposed, and the difference matters.

You suggested a proof reading package.json and comparing it to the constant. That closes the gap; it also keeps two places and adds a third thing to maintain. The actual defect was having a second copy at all, so the fix is that there is now one. The generator already reads Cargo.toml, so it reads package.json and takes devDependencies.typescript straight from it. fjs/ci/config's typescript export is deleted. Nothing can drift because nothing is duplicated, and the pin reaching ci.yml means node26's existing regeneration check already guards it.

The proof now supplies its own pin — =1.2.3-proof, a value no configuration holds — so finding it in the emitted command proves it came from the parameter. Your =5.9.3 mutation has no equivalent any more: there is no constant to set.

Your point about it being the first gap silent at both layers is what made me stop patching and look at the shape instead. Every earlier one still went red on a real runner; this one would have shipped a green check running a compiler nobody chose. That difference is worth more than the individual fix.

Two consequences worth recording:

Absent pin. If package.json has no devDependencies.typescript, the job is not generated rather than run against a default. A proof covers each shape meaning "no pin" — absent file, unparseable, not an object, devDependencies not an object, pin not a string, and the two absent-key cases. That is also what keeps branch coverage at 100%; my first attempt was silently failing the gate at 99.96% and I caught it before pushing.

Your accept/refuse matrix at f8b224d13. You were right that it describes a mechanism that no longer exists. The installed-vs-declared comparison is deleted, not moved — it existed to establish determinism at run time, and determinism now comes from the pin being read from one place and committed into the workflow.

Also acted on your other observations: the emitted commands no longer carry explanatory comments (ci.yml is output; the reasoning belongs in the generator), and the archive-count guard is gone since two archives already make npm's spec malformed and fail loudly.

And your closing line still holds — the defect class this job exists for is not reproducible on HEAD until files gains !**/private.d.ts. That is deliberately the next step, after this one is a required check.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5688ffa2a0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread fjs/ci/module.f.mjs Outdated
Comment thread fjs/ci/module.f.mjs
deno: ubuntuArm(denoSteps(functionalscript)),
bun: ubuntuArm(bunSteps(functionalscript)),
...nodeVersionJobs(functionalscript),
...(pin === undefined ? {} : { [packageCheckJobId]: packageCheckJob(pin) }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Refuse projects without a compiler pin

When an external project has no string-valued devDependencies.typescript, fjs ci still succeeds but silently emits no package-check job, despite the changelog promising that job; such a project's CI can therefore remain green without checking its packed declarations. Fresh evidence after the earlier compiler-version comment is this new conditional omission, so fail generation with an actionable error rather than quietly removing the validation.

AGENTS.md reference: AGENTS.md:L112-L118

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Keeping the omission, and I want to be clear that it is a decision rather than an unaddressed finding.

Your premise is right: silently dropping a check is worse than failing. Where I disagree is that it is silent. The generated workflow is committed to the repository, so losing the job appears as a package-check deletion in ci.yml — in the diff, in review — and node26's regeneration check makes an unregenerated workflow red. For this repository that is louder than an error at generation time, because it is visible to a reviewer rather than only to whoever ran the command.

Failing generation instead has a cost I do not want to pay: fjs ci would stop working for any project without an exact TypeScript pin, including ones that do not want this job at all. That trades a real regression for a hypothetical one.

On the changelog: fair point, it promised the job unconditionally. Since 9f6da87 the pin must also be exact, so I will make the entry say the job is generated when the project pins TypeScript exactly, rather than implying it is unconditional.

I should also flag the framing behind this and the typesVersions thread, since it applies to both: this generator exists to build our CI. fjs ci working for other projects is a side effect, not a product commitment — so "an external project's CI can remain green" is not a cost we are carrying. Findings about our repo on this PR were right and got fixed; this one is about someone else's.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Filed: fjs/ci/todo/package-check-unsupported-package-shapes.md,`` in f0364d3 — issue 2.

The todo is honest that this one is cheap to build — a Result from ci(setup) and an error path through fjs ci — so the open question is whether refusing is right, not whether it is hard. My reason for not refusing is that the generator's other jobs do not depend on a pin, and a project without one still wants them.

The contract it replaces is now written down where a consumer will find it, in fjs/ci/README.md: an exact devDependencies.typescript generates the job, anything else generates none. Silently producing one fewer job was the real complaint, and documenting the rule answers it without a refusal.


Generated by Claude Code

Comment thread fjs/ci/package/module.f.mjs Outdated
claude added 2 commits August 28, 2026 23:07
Two commands shared a step for no reason: `npm init` and `npm install` are
separate things, and the split exists so a failure names what failed. Every step
is now one command, six in all.

The response file is gone with it. `find -print0` and `xargs -0` hand the paths
to tsc as arguments, so a space or a quote in a path survives without quoting or
escaping — review found both cases, and this removes the mechanism rather than
patching it twice. The empty-list guard stays as its own step: tsc does exit
non-zero on no arguments, but by printing usage, which says nothing about why.

The compiler pin must now be exact. A range such as ^7.0.0 lets a later registry
release change the verdict with no change here, which is the one thing running
without a checkout is meant to prevent. Relaxing the pin drops the job from the
generated workflow — a visible diff in review rather than a quiet loss of
checking.

Verified on the emitted steps: 396 declarations and TypeScript 7.0.2, a
directory named `we"ird dir` with both a space and a quote now passes where the
response file would have split it, and a dangling private import still fails the
type-check step. 3480/3480, coverage 100%, round-trip clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqeS5t2ZKkSu3chRPMXR7n
The entry promised the job unconditionally, which stopped being true when the
pin had to be exact for the job to be generated. Review caught the mismatch.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 652ec49ed6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread fjs/ci/module.f.mjs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8d2de42ca3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread fjs/ci/package/module.f.mjs Outdated
`npm install "typescript@${pin}"`,
// `-print0` rather than a text list: the paths reach `tsc` as arguments, so
// a space or a quote in one survives without quoting or escaping.
`find node_modules/${alias} \\( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' \\) -print0 > ${declarations}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Obtain approval before adding the external-tool pipeline

The new generated job shells out to ls, find, test, and xargs in addition to npm and TypeScript, but repository policy requires approval before code, CI steps, scripts, or generators call any external tool. Obtain that approval before retaining this pipeline; otherwise the generated workflow violates the repository's explicit tooling policy.

AGENTS.md reference: AGENTS.md:L128-L132

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, and the right rule to raise — §6 landed in main via #1765 while this PR was in flight, so I built the pipeline before the policy existed and had not re-read the file since merging.

Rather than ask to keep it, I removed most of it in 5f90cda. find, xargs and test are gone: tsc reads a tsconfig.json whose include names the artifact directory and enumerates the declarations itself. ls is gone too — the glob was always expanded by the shell, so $(echo *.tgz) gets the same string from a builtin instead of a process. What remains is npm, npx and tsc.

That is not just compliance-shaped; it is the change §6 is arguing for. The section's objection is to a pattern that approximates analysis, and its prescribed answer is "an established tool that parses what it checks — ESLint for JavaScript, Clippy for Rust". Here the tool that does the checking now also does the finding, so there is no second, weaker model of the file tree to disagree with the compiler's. Concretely it fixed two of your other findings by construction: a .d.cts cannot be omitted from a list that does not exist, and a path with a quote in it cannot be mis-serialised when it is never serialised.

The remaining three are, I think, what the section endorses rather than what it prohibits: tsc is the parser, and npm is the thing under test — the job's whole purpose is to install the artifact the way a consumer would, which cannot be done without the package manager a consumer uses.

I am not treating that reading as settled. §6 says approval first, and the judgement of whether npm/npx/tsc in a generated job need explicit sign-off belongs to the repository owner, not to me — flagged for @sergey-shandar, and I will act on whichever way it goes.


Generated by Claude Code

Comment thread fjs/ci/module.f.mjs
claude added 2 commits August 28, 2026 23:18
startsWith('=') was the wrong shape rather than the wrong constant: `=7.x`,
`=7.0`, `=7.0.2 || 8.x` all pass a prefix test and are all ranges npm resolves
against the registry, which is exactly what running without a checkout is meant
to rule out.

The pin must now be `=MAJOR.MINOR.PATCH` and nothing else — three dot-separated
runs of digits after the sign. No regex; the repository forbids them and the
check is two lines without one.

It rejects a prerelease pin too, which is stricter than npm needs. That is the
right way to be wrong here: the cost is the job disappearing from ci.yml, a
visible diff in review, rather than a check that silently stops meaning
anything.

The validator caught the proof's own fixture, which used =9.9.9-run. Nine
rejection cases now cover the shapes that pass a prefix test, which is also what
keeps branch coverage at 100%.

This closes the class rather than patching another instance of it: the previous
three rounds each fixed one spelling of "not really pinned".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqeS5t2ZKkSu3chRPMXR7n
…ydyc9' into claude/private-ts-todo-partial-bydyc9

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 42bd0699fc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread fjs/ci/package/module.f.mjs Outdated
The package check walked the artifact with `find`, guarded the result with
`test -s`, and fed it to `tsc` through `xargs -0`. A `tsconfig.json` with an
`include` pattern does all three, and does them better:

- No path passes through the shell, so a space or a quote in a directory name
  has nothing to survive. The `-print0`/`xargs -0` pairing existed only to carry
  bytes safely across that boundary; there is no boundary now.
- An empty match is TS18003, which names the pattern that found nothing. The
  `test -s` guard could only say that a file was empty.
- `.d.cts` and `.d.mts` need no enumerating: `include` selects every extension
  the compiler reads, so a package shipping a form we did not list is covered
  rather than skipped.

Six steps become five, and three external tools become none. Root AGENTS.md §6
asks for an established tool that parses what it checks rather than a pattern
that approximates one; here the tool that does the checking also does the
finding.

`exclude` is emptied because the default excludes `node_modules`, the only place
the artifact exists. `skipLibCheck` is stated rather than left at its default —
it is the one option whose flip leaves the job green having opened nothing, and
the proof pins it.

`fjs/ci/README.md` said the built-in command does not read `package.json`. Since
the compiler pin moved out of the config constant, it does; the paragraph now
says what it reads and what an inexact pin costs.

Verified by running the emitted steps verbatim against real tarballs: 396
declarations checked, identical to the previous pipeline; a declaration
referencing a module the tarball does not carry fails with TS2307; the same
break passes silently under skipLibCheck true; a `.d.cts` under a directory
named `we"ird dir` is enumerated; a package shipping no declarations exits 2
with TS18003.

3479/3479, coverage 100%, workflow regenerates clean.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5f90cda8da

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread fjs/ci/package/module.f.mjs Outdated
* opening these declarations at all, and the job would still pass.
*/
const tsconfig = /** @type {const} */ ({
include: [`node_modules/${alias}/**/*`],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Enumerate declaration files instead of all TypeScript roots

When a generated-workflow consumer ships a .ts, .tsx, .mts, or .cts source file but no .d.* files, this pattern still gives TypeScript a nonempty root set, so npx tsc can succeed without checking any declaration and the intended TS18003 empty-declaration guard never fires. Enumerate declaration extensions explicitly and reject an empty declaration set; otherwise package-check can report a plausible green result for exactly the missing-declarations case it is meant to detect.

AGENTS.md reference: AGENTS.md:L113-L119

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not doing this one, and it is a decision rather than an oversight. The mechanism you describe is real; the package shape that reaches it cannot occur here.

package.json files is an allowlist, not a denylist: ["**/*.js","**/*.d.ts","**/*.mjs","**/*.d.mts"]. No pattern in it matches a .ts, .tsx, .mts or .cts source, so a raw source file cannot be published no matter what lands in the working tree. I checked the packed tarball rather than reasoning from the field: 282 .mjs, 282 .d.mts, 114 .d.ts, one .md, one .json, and zero non-declaration TypeScript files. So the nonempty-for-the-wrong-reason root set is structurally unreachable, not merely absent today.

The direction of travel matters too. Stage 2's next step tightens files further — !**/private.d.ts — and every plausible mistake in that direction removes declarations, which is the case TS18003 does catch. Drop both declaration patterns and the tarball is .mjs and .js only, include matches nothing, and the job is red. I verified that path with a package shipping only index.js: exit 2, TS18003, message naming the pattern that found nothing.

The fix you suggest costs more than it buys here. "Enumerate declaration extensions explicitly" is the hand-written list I removed in 5f90cda — and it is the same list you correctly flagged three rounds ago for omitting .d.cts. Restoring it trades an unreachable failure for one that already happened once. "Reject an empty declaration set" separately from include needs something to count declarations, which means find again, against §6 and against the change that closed your xargs finding.

Worth being clear about what the empty-set guard is and is not. It is a backstop for "shipped nothing"; it was never the mechanism for "shipped less than before". That one is TS2307 on a reference the tarball no longer carries, it fires on partial loss where TS18003 cannot, and it is what Stage 2 actually rests on.

If a consumer ever ships sources rather than declarations, the honest answer is that fjs ci does not support that package shape — same position as the typesVersions thread — not a guard bolted on for a case this repository's own files field forbids.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Filed rather than left in this thread: fjs/ci/todo/package-check-unsupported-package-shapes.md,`` in f0364d3.

The reason it can't fire here — files is an allowlist with no pattern matching a source — is now a comment on tsconfig in the generator, next to the ** that depends on it. The todo carries the rest: what building it would cost, and the condition that makes it worth building, which is the day root package.json files admits a .ts source.

Per REVIEW.md, a review thread is the one place an answer does not survive the merge.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The link in my previous reply did not render. The file is fjs/ci/todo/package-check-unsupported-package-shapes.md, added in f0364d3 and visible in this PR's Files changed:

https://github.com/functionalscript/functionalscript/blob/claude/private-ts-todo-partial-bydyc9/fjs/ci/todo/package-check-unsupported-package-shapes.md


Generated by Claude Code

REVIEW.md: "where does this knowledge live once the pull request is merged? In
the diff, in the design document, or in a todo/ issue. Only 'in the review
thread' is wrong: it is the one place the answer will not survive." Nine
findings on #1767 were answered only in threads. The answers move here.

In the generator, next to the decision each explains:

- Why tsc enumerates rather than a shell walk, with the three defects the
  find/test/xargs version had — including xargs' finite command buffer
  splitting a large package into separate programs, which was silent. Written
  as "do not go back", because the pipeline reads like the more explicit
  choice.
- Why `**` rather than a list of declaration extensions, and what that costs:
  a package shipping sources and no declarations has a nonempty root set, so
  TS18003 cannot fire.
- Why npm, npx and tsc are the tools that remain under AGENTS.md §6.
- Why there is no guard against a second .tgz.

In `fjs/ci/README.md`, the `package/` module joins the file list, stating the
question a reader asks first: why the one job built without `toSteps`, and what
a checkout would do to it.

In a new `fjs/ci/todo/package-check-unsupported-package-shapes.md`, the three
declined findings — typesVersions, refusing a project with no exact pin, and a
package shipping sources — with what each would cost to build and what has to
be true before it is worth building. They are one question, and it is the
question `node26-typedef-gate-reaches-consumers.md` already asks about another
job, so the two are linked.

No generated output changes: `ci.yml` is byte-identical, which is the point of
keeping the explanations in the source rather than in the emitted commands.

3479/3479, coverage 100%, tsc clean.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f0364d3934

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread fjs/ci/package/module.f.mjs Outdated
npm's `**` walks into a dot-prefixed name and TypeScript's does not. `files`
therefore publishes `.d/x.d.ts` and a lone `include` of `**/*` left it
unchecked — silently, and even when it is the package's own `types` entry
point. Reproduced with a package whose `types` is a broken `.hidden/b.d.ts`:
installed, never loaded, job green.

Two patterns naming a dot segment explicitly close the shapes anyone ships:
`**/.*` for a dot-named file, `**/.*/**/*` for anything under a dot-named
directory at any depth. Verified all four arrangements enumerated — visible,
dot-named file, top-level dot directory, nested dot directory — and the
reproduction now fails with TS2307 on the file it was skipping. The real
package is unchanged at 396 declarations, exit 0.

Unlike the other package-shape findings this one was reachable rather than
structural: nothing here ships a dot-prefixed path today, but `files` would
publish one.

The fix is honest about its limit rather than claiming completeness. Two dot
segments in a row still escape, because the inner `**` has to cross the second,
and a pattern per arrangement of dots does not converge. Closing it properly
means reading the installed tree's real names, which is a tool walking the tree
and so the AGENTS.md §6 conversation. Recorded as issue 3 in
fjs/ci/todo/package-check-unsupported-package-shapes.md, with that condition
stated.

3479/3479, coverage 100%, tsc clean.

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

@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.

Approved. My finding is closed structurally rather than patched, which is the better outcome.

fjs/ci/config/module.f.mjs no longer carries a typescript value at all; the pin is read from package.json. I checked that by changing the manifest to =5.9.3 and regenerating — the new value propagates correctly into ci.yml, and there is no second place left that could disagree with it. The mutation I reported is not merely caught now, it is unexpressible.

cefa0d38f also closes something I had not separated out: the intermediate pin.startsWith('=') was a first-character test, so =7.x and =7.0.2 || 8.x would have passed it. The validator now requires three digit-only segments and the proof enumerates 17 rejections. On bare 7.0.2 being refused — your reading is right and my brief was an assumption, not a requirement: refusing a valid-but-differently-spelled pin surfaces as a visible diff in review, while a weakened check surfaces as nothing.

The dot-path fix is real and I confirmed both halves. At 5f90cda8d a broken declaration under .hidden/ never enters the file list and tsc exits 0 over it; at this head it is enumerated and reported. Nested dot segments (.a/.b/) still escape — which your own commit message says and package-check-unsupported-package-shapes.md tracks, so it is a bounded and recorded gap rather than a silent one.

The empty-list guard survived the move to a tsconfig include: a package with no declarations exits 2 on TS18003 naming the patterns that matched nothing, so test -s became unnecessary rather than being dropped. Gates: npm test 3541/3541, tsc 0.

Not re-verified at this head after the redesign: the full proof-assertion mapping and step-isolation end-to-end. Earlier rounds covered those against shapes that have since been replaced.

Copy link
Copy Markdown
Contributor Author

Thanks — and you named the one thing I had asserted rather than checked. The PR body claimed eight mutants against a proof that has been rewritten twice since I last verified that claim, so I ran them at this head instead of leaving it:

Mutation Result
actions/checkout added to the job killed by noCheckout
needs dropped killed by consumesTheArtifact
needsnode22 (a job that produces no artifact) killed by consumesTheArtifact
skipLibCheck: true killed by canFail
alias → functionalscript killed by anyPackageName, tscEnumerates
exclude: ['node_modules'] killed by tscEnumerates
**/.* removed killed by tscEnumerates
**/.*/**/* removed killed by tscEnumerates

Unmutated baseline passes, so none of these is an assertion that fails regardless. I also tried a ninth outside the claim — $(echo *.tgz)$(ls *.tgz) — killed by anyPackageName, which is the §6 property staying pinned rather than drifting back.

On step isolation: the strongest evidence is the CI log rather than a local run. Each command appears as its own ##[group] Run … with its own exit status, so a failure names the command that failed. My local runs execute the emitted steps through one bash -e, which proves the commands work but not that they are separated — the run on this head does.

Your npm test count (3541) and my npm run cov count (3479) differ because the suites differ, not because we are looking at different trees; both are green at a194225.

Two things stay open and both are yours: the §6 sign-off on the remaining npm/npx/tsc — which also gates the complete fix for the nested-dot gap, since that needs something reading the installed tree's real names — and step 4, making package-check a required check.


Generated by Claude Code

@sergey-shandar
sergey-shandar added this pull request to the merge queue Aug 29, 2026
Merged via the queue into main with commit a97eacf Aug 29, 2026
20 checks passed
@sergey-shandar
sergey-shandar deleted the claude/private-ts-todo-partial-bydyc9 branch August 29, 2026 01:01
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