Repository navigation
feat(front-end): build the API model from a package's source - #7
Open
systemfsoftware-maker wants to merge 102 commits into
Open
systemfsoftware-maker wants to merge 102 commits into
systemfsoftware-maker wants to merge 102 commits into
Conversation
Dated unified plan for the isolated-declarations fast path, the TypeScript 7 compiler fallback behind one emitter port, the syntactic model builder and the analysis-message resolver. Cites the U7 contract (origin R15/R17/R18, KTD9/KTD11/KTD13) and the cell-architecture, boundary-testing, schema-laws and package-topology packs; folds in the ce-doc-review findings.
…(U1) tsconfig.build.json sets isolatedDeclarations, so the build fails when an export loses its annotation (plan R15, KTD9). Exported schemas carry their Schema types; the Verdict and CompareSurfaces schema classes become typed constants beside their instance types (TS9021/TS9038), which keeps effect-schema-discovery generating their codec laws. Verdict output bytes are unchanged (host digest e4478f03...6da2).
apps/api-extractor-profile times discovery, oxc-transform and transpileDeclaration isolated emit, TypeScript 7 and 5.9.3 tsc emit, and oxc-parser and createSourceFile parsing for this repository's package and the 39 public systemfsoftware packages with a TypeScript source entry at 732f66a0c8, and records each candidate's isolated-clean verdict beside tsc --isolatedDeclarations (plan R12, R13, KTD10). Committed before the fast-path emitter is chosen.
…er (U3) The committed profile picks oxc-transform 0.150.0 with oxc-parser by the plan's pre-registered rule (KTD2): both candidates agree with tsc --isolatedDeclarations on all 40 packages, and oxc emit plus parse takes 77.79 ms against transpileDeclaration's 947.03 ms. The Decision section of docs/profiles/2026-10-12-front-end-phases.md records the numbers. Adds the DeclarationEmitter port with IsolatedEmitter and CompilerEmitter tags (no driver imports), the Declarations meeting point, the package reader (NoSourceEntry for a package with no TypeScript source entry), the declaration graph both emitters close over, and the isolated adapter that refuses with NotIsolated and the emitter's diagnostics.
… 7 fallback and router (U4-U8)
- build-model, type-expr-of, canonical-type-text: one syntactic builder turns
declaration text into the ApiModel v2 on both paths (KTD6, R6-R8); import()
types and imported names give one reference; undecomposed types print through
one canonical printer.
- resolve-messages: ae-forgotten-export, ae-missing-release-tag,
ae-internal-missing-underscore, ae-incompatible-release-tags and
ae-internal-mixed-release-tag from declaration text (KTD7, R9-R11), checked
against upstream api-extractor 7.59.1 run in-process.
- typescript-7-compiler: the CompilerEmitter adapter runs this package's tsc 7
as a process, decodes its output through lawful line codecs, and closes over
the same declaration graph as the isolated adapter (KTD3, R2, R5).
- analyse and the FrontEnd namespace route FastPath or Fallback (KTD4, KTD8).
- The declaration graph treats a declaration module without export {} as
exporting every top-level declaration, as TypeScript does.
- The isolated adapter declines with NotIsolated when oxc's output for a file
that had diagnostics does not parse (a destructured export), instead of
refusing the package.
- e2e journeys: fast path and fallback give this package the same bytes and
messages (AE1), an inferred export routes to the fallback naming TS9010
(AE2), a type error is refused with TS2322 (AE5).
analyse builds the declaration graph once and hands it to the builder and the resolver; the closure walk pushes onto a queue instead of copying it per file; compiler-output decoders are built once; one bindingName and one optional-type printer; the analysis message ids are one const list; resolver de-duplication uses Arr.dedupeWith. No behaviour change: 198 package tests, 4 e2e tests and the host digest are unchanged.
…README front-end section (U9) The harness now runs FrontEnd.readPackage, the routed emitters, buildModel and resolveMessages on every corpus package and records the route and build outcome. On systemfsoftware 732f66a0c8: 12 fast path, 28 fallback, 37 of 40 models built; declare global, a module augmentation and an external re-export are refused with UnsupportedDeclaration. The first profile and its emitter decision stay unchanged.
- Changeset: a minor intent for @systemfsoftware/api-extractor, because the FrontEnd namespace is new public surface (package-topology/surface-changes-are-versioned). - Compiler-output codecs: Position is capped below 1e9 and digit text has one spelling (no zero padding). Each refinement has a refusal law in its own file (schema-laws/refusals-beside-generated-laws), and each law fails when its refinement is widened. The four committed Refuted seeds are kept as regression coverage, because they still express valid cases. - The isolated emitter declines with NotIsolated, naming the declaration file, when an entry point's source cannot be read, instead of modelling an empty entry point. - An entry point's declarations load from its recorded source path first, so a .tsx entry is no longer served from a sibling .ts. - The TypeScript 7 fallback passes --stripInternal false, so both routes keep @internal declarations. The e2e equality journey now covers a stripInternal package. - An enum member whose declared value is computed is refused with UnsupportedDeclaration naming the member, instead of losing its value. - README documents the builder's refusals and the unrefused export * from another package. - The plan is superseded by a new dated plan that adds U10 and KTD12, the Turbo evidence. KTD12 records that Turbo already owns build, cache and affected-task decisions here. #4 (implicit exports) stays rejected: tsc 7.0.2 accepts importing a non-exported interface from an import-bearing .d.ts (exit 0), and adding `export {}` makes the same import TS2459.
…s emit options (ce-compound) The fallback runs tsc -p <package tsconfig>, so a package's stripInternal (or any other option that changes declaration text) diverges from the fast path's fixed options unless the adapter pins it on the command line.
…s the adapter establishes it The delta review noted that tsc -p still loads every other tsconfig option, so the doc no longer claims the fallback text depends on the package only through source.
…#1, #2) - The compiler fallback passes `--declarationDir` pointing at its scoped temp directory. A tsconfig `declarationDir` overrides `--outDir` for declarations, so tsc wrote into the analysed package and the adapter then refused it. The front-ends equality journey now covers a package that sets `declarationDir` and asserts nothing is written into it. - The fast path reads an entry source only when it resolves inside the package root, sharing `relativeWithin` with the fallback's emitted-file filter. An entry outside the root was modelled on the fast path while tsc refused it (TS6059), so the two routes disagreed. It is now declined with NotIsolated and reaches the same refusal on the fallback. - The compiler-fallback solution doc adds the output-location case.
oxc-transform prints `1_000` as `1e3` where tsc prints `1000`, so an enum member or `const` initialised with `1_000` gave the fast path and the compiler fallback different models (R3, R7). Numeric literals, bigint literals and numeric property keys now print their value instead of their source spelling. The builder feature gains a scenario pairing oxc output with tsc's spelling of the same numbers.
- A declaration's release tag comes only from a doc comment after the previous top-level statement. The resolver took the last doc comment anywhere before the statement, so `/** @internal */` on one export, or an interface member's doc comment, tagged the exports after it. - `ae-internal-missing-underscore` names only export names without a leading underscore, as api-extractor 7.59.1 does; `_hidden` was flagged too. - The compiler fallback passes `--removeComments false`. A tsconfig `removeComments` stripped the `/** @public */` comment on that route only, so the routes disagreed on `ae-missing-release-tag`. The upstream-oracle feature gains a scenario with a tagged export, an interface member's doc comment and untagged exports after them; the equality journey gains a `removeComments` package, and the solution doc records the second text-changing option.
ryanleecode
pushed a commit
that referenced
this pull request
Oct 13, 2026
* test(api-extractor): retire the generated schema laws The effect-schema-vite plugin wrote codec and recursion laws for every exported schema into src/schema-laws.test.ts at test time. One of them, ∀x_TypeExprNesting_≤MaxDepth1, failed in CI run 38372869767 (PR #7's merge ref): fast-check drew a TypeExpr nested deeper than the recursionBudget it generates under. That law checks the test arbitrary, not the codec: recursionBudget only shapes generation, and decoding has no depth limit. Through the public surface, the shrunk value and one nested three levels deeper decode, serialise byte-stably and keep every node, and Verdict.judge still reports a change at the deepest member. So the laws go, not the codec: vitest.config.ts drops inlineSchemaTests() and the generated test file is deleted. No replacement test is added. The property suite needs none of the plugin's budget materialization; all 103 remaining tests pass. main carries no in-source decode or verdict refusal properties. The plugin's devDependencies stay in packages/api-extractor/package.json: removing them changes the 0.0.0 tarball the v0.0.0 tag records, so they go in the first-release change. Hence the `none` intent. * test(api-extractor): keep the recursion budgets governing generation The schema-law plugin also ran effect-schema-recursion-budget's transform, which turns each `recursionBudget` annotation into the generator the property tests draw from. Without it, the budgets on TypeExpr, Member and ApiItem shaped nothing, and only the retired recursion laws would have reported that. The package's README says a package that generates no schema laws registers recursionBudgetTransform in its own Vitest configuration, so vitest.config.ts now does. The transform's runtime import is resolved by plain lookup from the schema modules, so the package is a devDependency of the private workspace root, not of packages/api-extractor: editing that manifest would change the 0.0.0 tarball. The lockfile gains the root importer entry only; the tarball is the one already in .sfs-deps.
Under schema-laws/refusal-properties-cover-unobservable-boundaries (systemfsoftware #761), each in-source refusal property from ruling #2 was judged against the public entry: - The digit-spelling refusal is observable. TypeScript 7.0.2 prints an unresolved import specifier raw, so a package's own text can put `error TS007: ...` on tsc's stdout, and FrontEnd.analyse reports whether that line became a diagnostic. The property is removed; the e2e refusal journey now asserts that only tsc's own TS2882 diagnostics come back. That journey fails at fd4ad93, which returned a forged code 7, and passes here. - The Position cap refusal is not observable (Digits admits at most nine digits before the cap, and nothing outside the schema encodes), and its predicate restated the refinement rather than an independent oracle. The property is removed; the cap stays. - The four Refuted seeds belonged to the generated laws #9 deleted; the seed store is removed. The plan moves to a superseding dated file with U11.
Supersedes the 1629 plan. U12 gives every finding of the fd4ad93, a2fe1eb, 2b2e595 and 2c13063 reviews, the delta passes, the anchor-50 notes and the pack rows a ruling: a named fix with a test, a rejection with its reason, or a follow-up with an owner and a trigger. KTD3 is amended: tsc stdout is package-influenced text, so the fallback reads diagnostics from the TypeScript 7 API on one derived config and lists emitted files from its scoped directory.
A diagnostic no compiler reported (a missing entry source, a missing entry declaration, a tsc exit with nothing on record) printed as TS0, a code TypeScript never defines. Diagnostic.code is now optional and, when present, a positive integer, and refusal messages print TS<code> only for a real code. Routing scenario: the missing-entry refusal message names no code; it fails at 2c13063.
…er text exact (U12) - C1 (P1): a local key now has a start and an end delimiter, so one key can never occur inside another and a text type keeps Options, Ids, Key instead of Opt_2ions, Id_2s, K_2ey. - B5: a named optional tuple member prints its question mark. - 2b2e595 setter note: the printer passed 'method' for every signature, so a set accessor in a printed type literal gained ': any'. - C5/A16: one property-key resolver; each caller states its private-name decision. C9: one ordered keyword list. Both behaviour-preserving. - A8, C8/A9: scenarios pin the interface and class item shapes and the declare global, declare module and external re-export refusals. Each fix's scenario fails at 2c13063 and passes here.
…olver (U12) - S1: a generic wrap<A> no longer hides the ae-forgotten-export of a real non-exported A in the same file; type parameters bind only within the declaration that introduces them. - B9: an oracle scenario pins the full message array, order and de-duplication included. - A18: the upstream comparison also compares the entry point. ExtractorMessage carries no item field, so item is not comparable. The fixture's temp dirs move to the system temp directory.
…(U12)
- A11: .cts source entries are read like .ts, .tsx and .mts.
- A12: a pattern export ("./*") expands to one entry per matching source
under Node's exports rules: the * matches any substring, an explicit
subpath wins, and a longer key prefix wins between patterns. Only the
target's directory is walked, inside the package root.
Scenarios fail at 2c13063 and pass here.
- A10: an import whose specifier names no declaration file is reported, not dropped: the fast path declines naming the import, and the compiler route refuses it with UnsupportedDeclaration. - D1: an entry declaration loads only from its recorded source, so a deleted mod.tsx is not modelled from a sibling mod.ts. - D2: two entry points sharing a declaration path are declined. - S3: the fast path compares real paths, so a symlink out of the package is not read. - C7/A17: the entry mapping and the missing-entry list live once in the declaration graph. Scenarios fail at 2c13063 and pass here.
…not tsc stdout (U12 C3) tsc's stdout is package-influenced text: a module specifier holding a newline prints a well-formed 'error TS7:' or 'file(1,1): error TS8:' line, which parseCompilerOutput decoded as a real diagnostic. No line grammar can tell them apart, so the adapter reads no stdout: - One emit policy (compilerEmitPolicy), written with the package tsconfig as 'extends' into a derived config in the scoped directory; tsc runs as 'tsc -p <derived>'. The derived config carries the package's own typeRoots (from the API's parseConfigFile), else the default node_modules/@types chain, which TypeScript would otherwise resolve from the temp directory. - Emitted declarations are the .d.ts files under the scoped directory. - On a non-zero exit, diagnostics come from typescript/unstable/async on the same config, errors only, in tsc's reporting order, de-duplicated. A failed run the API has no error for (TS5033 on a long file name) is refused with one code-less diagnostic carrying tsc's text verbatim. - Before tsc runs: a missing or escaping entry source (D1) and two entries sharing a declaration path (D2, which tsc 7.0.2 accepts and silently collapses) are refused. - compiler-output.schema.ts is deleted, so the MessageText lookahead and the UNPARSED_FILED_LINE sentinel pack rows go with it. - FrontEnd exports TYPESCRIPT_PRODUCER (C10), compilerEmitPolicy and derivedTsconfig. The profile harness writes the same derived config (B2/A6) and fails on a failed git (A15); the profile is not re-run. The e2e refusal journey gains canonical sourceless and filed forged lines, a package with no tsconfig, an entry excluded from include, a 251-character file name, an escaping entry and a shared declaration path through CompilerEmitter (C6, A13, B10, D2). The equality journey gains a package with declared typeRoots. At 3e81954 the refusal journey returned codes 1234 and 4321 for the forged lines.
…e derived config (U12 C3) A derived config in a temp directory resolves two defaults from its own location: include (**/*) and typeRoots (@types from node_modules upward). A package tsconfig that names no include therefore compiled nothing (TS18003), and the default typeRoots chain would replace a package's declared typeRoots. The adapter reads the package config once with the TypeScript 7 API's parseConfigFile and carries its resolved fileNames as files and its declared typeRoots, else the default chain from the package root. The equality journey gains a package with no include and one with typeRoots: ["./types"]; each fails at 7520eae and passes here. The solution doc and KTD3 record both settings, with the gate for each.
…er stamp (U12 C11) README and the changeset now name the producer stamp as the one route difference, and the README describes entry expansion (.cts, subpath patterns), the entry checks that send a package to the fallback, and that compiler diagnostics come from the TypeScript 7 API.
…ulings (U12) Supersedes the 1737 plan. D1 named a reader refusal (MissingSource) that would have reversed ruling #5's fast-path decline; the adapters decline (fast path) and refuse before tsc (compiler). D2 said tsc refuses a shared declaration path with TS5056; tsc 7.0.2 exits 0 and writes one file, so the compiler adapter refuses it first. The DeepSeek delta review's two findings are ruled: the D1 text finding is accepted by this correction, the dropped-exit-cause finding is rejected with its reason.
…inned type-unchecked (U20) Review at 681efe3: - maintainability P2: the model feature was 1120 lines again; its eight namespace scenarios move verbatim into 'Building the namespaces of a package API model' (896 and 233 lines). - testing P2: no scenario showed the fast path skips the type checker. An annotated but type-incorrect package (count: number = 'one') takes FastPath and models count as a number. Pins existing behaviour.
…024 directories (U20) A root pattern (./* -> ./*.ts) walked into node_modules, whose pnpm symlinks fan out like any alias tree, so 24e9b67's bound refused the package although no export name can pass through a node_modules segment. The walk no longer enters a directory whose name is an invalid export segment wherever that segment lies wholly inside the matched name; with that, the bound drops to 1024. Reader scenario: a node_modules fan-out of depth 13 is refused at 3016e54 and read now. Pins (existing behaviour): a symlink to an ancestor two levels up is walked once (DeepSeek delta gap); a manifest without name or version is ManifestRejected (api-contract gap).
Supersedes the 0008 plan. Records the fixing commits for the walk bound and node_modules pruning, cross-file scope, private constructors, index signature names, namespace-body type and value spaces in the builder and the resolver, the single classifier and the test split; the rejections with their reasons; and the PR4 and PR5 follow-ups with their triggers.
…ed (U21) Correctness residual at 5d32c3f: itemsForExport returned no items when an export list named nothing, so a hand-written entry .d.ts holding 'export { present, Missing }' gave a model silently missing Missing, on both routes. It is refused with UnsupportedDeclaration naming the file and the export. Refusals scenario; fails at 5d32c3f with a model holding only present. The README lists the cause.
…tays on the prefix (U21) Review at 5d32c3f: - DeepSeek peer P2: an export path found under a wide key (./* -> ./src/*.ts) kept its entry when a narrower key (./a/* -> ./vendor/*.ts) wins it in Node and names no file, so ./a/x was modelled although no consumer can import it. Each pattern entry is kept only when its key is the one Node's pattern-key order picks for the export path, which also leaves ./internal/z out under a null ./internal/*. - DeepSeek peer P3: a key with two stars produced export paths holding a literal star; only single-star keys are patterns, and others name no entry. - DeepSeek delta P3: a star inside a file name (./src*.ts) walked the package root, node_modules included, and tripped the walk bound. The walk enters only directories the target prefix can lead to. Reader scenarios; each fails at 5d32c3f. Pin (existing behaviour): a node_modules fan-out under src/ is pruned. The README states the rules.
…ck (U21) Adversarial P2 and DeepSeek delta P2 at 5d32c3f: the resolver scoped a namespace body with that block's names only, so with N declared twice and an imported A, a reference to A in the second block was reported as a forgotten export although tsc and the builder bind it to N.A. One pass over each file maps a namespace path (N, N.M) to all its blocks, and entering a block scopes it with their union. Analysis scenarios for a top-level and a nested merge agree with upstream; both fail at 5d32c3f.
…ces; the changeset names new runtime dependencies (U21) Testing and adversarial gaps at 5d32c3f: no scenario named a class or an enum from both a type position and a typeof query, nor across merged blocks. One pins both (existing behaviour). api-contract P3: the release note now says the package installs typescript 7, oxc-parser, oxc-transform and @microsoft/tsdoc at runtime.
Supersedes the 0042 plan. Records the fixing commits for merged namespace scopes in the resolver, the unresolved-export refusal, Node's winning pattern key, single-star keys and the prefix-bound walk; the correction of U20 K1's fail-before depth; the pins; the rejections with their reasons; and the follow-ups with owners and triggers.
Maintainability P3 at 9854ba8: the pre-pass and the reference scan each kept a dotted namespace path. The pre-pass now also records each named namespace's path by its start offset, and the scan looks it up instead of keeping its own stack. No behaviour change: analysis scenarios unchanged.
…lists it (U22) DeepSeek delta P3 at 9854ba8: an entry holding only `export * from './a.js'` over `export { present, Missing }` was refused naming the entry, a file that holds no such statement. The refusal now names the file whose export map lists the name, found through star exports in resolution order. Refusals scenario; fails at 9854ba8. Maintainability P3: the builder's body-statement flatten was a copy of the graph's statementsIn, which it now imports.
…ing pattern target (U22) Testing gaps at 9854ba8: static fields, auto-accessors and accessor pairs model exactly while a private field and a static block hold nothing; template literal, predicate, asserts, indexed access, abstract construct and mixed-signature object types keep canonical text; a pattern export whose target directory does not exist adds no entry. Pins existing behaviour.
…ckage (U22) Adversarial P2 at 9854ba8: without the platform package typescript installs as an optional dependency, every package needing the fallback is refused with CompilerRejected carrying tsc's message, which names the missing package (probed: the tsc shim exits 1 with 'Unable to resolve @typescript/typescript-linux-x64').
…(U22) Found pinning the testing gap at 9854ba8: `protected p` modelled as a public `p`, an abstract method as a concrete one and a protected constructor as `constructor`, so public->protected and concrete->abstract changes were invisible to the diff. Following U20 K3 (`private constructor`) and the existing `static` prefix, a member is named with its modifiers in TypeScript's written order: protected, static, abstract. Builder scenario; fails at eec0927.
Supersedes the 0108 plan. Records the fixing commits for member modifiers, the star-export refusal origin, the native compiler requirement and the two de-duplications; the pins; the rejections with their reasons; and the follow-ups with owners and triggers.
…vate static member (U23) Testing P2 and correctness gap at ea58038: two of the three abstract element arms had no scenario. The class modifier scenario now holds an abstract property, an abstract auto-accessor and a private static member; it fails when either abstract arm is removed.
api-contract P3 at ea58038: the isolatedDeclarations refactor (U1) moved the keyword list into a constant and dropped the comment from the public KeywordName schema.
…used naming its file (U23) DeepSeek delta P3 at ea58038: M2 named the listing file for an export list that names no declaration, but an `export { x } from 'lodash'` reached through the entry's `export *` was still refused naming the entry, which holds no such statement. Both refusals now name the file whose export map lists the name. Refusals scenario; fails at ea58038.
…o members (U23) DeepSeek delta P3 at ea58038 read the split as a defect. TypeScript admits only this split (TS2808 refuses a getter less accessible than its setter, TS2676 mismatched abstractness), and two members say it exactly: `w` reads publicly and only a subclass writes `protected w`. Pins existing behaviour.
Supersedes the 0130 plan. Records the fixing commits for the external re-export refusal origin, the abstract-arm pins and KeywordName's doc comment; the accessor-pair pin; the rejections with their probes; and the follow-ups with owners and triggers.
… resolved it (U24) DeepSeek delta P3 at fa6bca2: N1 named the first star target listing the name, so with `a.d.ts` listing `x` unresolved before `b.d.ts` re-exporting it from `lodash`, the refusal paired `a.d.ts` with a re-export it does not hold. An external symbol now carries the file whose binding resolved it, and the refusal names that file; the unresolved-name refusal keeps naming the first listing file. Refusals scenario; fails at fa6bca2.
Testing gap at fa6bca2: U1 moved VerdictTypeId from an instance field to Verdict.prototype for isolatedDeclarations, and only an orchestrator probe read it. A verdict decoded from the cell's document holds the brand; the scenario fails when the prototype brand is removed.
Correctness P2 (confidence 50) at fa6bca2 read a conditions object under the family's source condition as a silently dropped entry. KTD5 reads the condition where the family writes it, and its value is the source's path; the README now says a condition holding anything else names no entry, beside the nested and array forms, and the reader scenario pins it. Pins existing behaviour.
Supersedes the 0143 plan. Records the fixing commits for the external re-export's resolving file, the verdict brand pin and the source condition's value rule; the rejections with their probes; and the follow-ups with owners and triggers.
…the printer's helper (U25) Maintainability P2 at c25bf9a: typeParameterNamesOf repeated the exported typeParameterNames. No behaviour change; the analysis scenarios pass unchanged.
Testing P3 at c25bf9a: the compiler's 'does not exist' refusal had no scenario beside its pinned 'resolves outside the package' sibling. The refusal journey analyses a package whose entry names no file: the fast path declines and the compiler refuses with a Note naming src/mod.ts. Pins existing behaviour (ruling #5).
Reliability P2 at c25bf9a: oxc-parser and oxc-transform load their per-platform bindings at import, so without them (probed: both throw 'Cannot find native binding') importing the package fails. The README states it beside the TypeScript 7 platform-package paragraph.
…del's KeywordName (U25) Maintainability residual at c25bf9a: the printer and the model listed the twelve keyword names independently, so a keyword added to the model would refuse packages at runtime. The table is a record keyed by name and the entries are derived from KeywordName.literals, so a missing key is a type error (probed: dropping void fails tsc with TS2339).
Correctness residual at c25bf9a: the README named only the two-star key. The reader scenario now pins both. Pins existing behaviour.
Supersedes the 0157 plan. Records the fixing commits for the resolver's duplicate helper, the missing-entry refusal row, the oxc binding requirement, the keyword table keyed by KeywordName, the directory candidate pin and the two-star target; the rejections with their reasons; and the follow-ups with owners and triggers.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
FrontEnd.analyseturns a package's TypeScript source into the versionedApiModel(schema version 2) and api-extractor's analysis messages. A package whose exports are all annotated takes the isolated-declarations fast path, with no type-check. Every other package goes through the TypeScript 7 compiler, and both routes give the same model.This is PR3 of the six-PR plan in
docs/plans/2026-10-10-1146-feat-effect-api-extractor-plan.md. It builds on the model, diff and verdict (PR1) and the toolchain (PR2). The.api.mdreport and differential lane (PR4), the CI benchmark (PR5), and the CLI and changeset-bump check (PR6) come later. The unit plan isdocs/plans/2026-10-12-2048-feat-front-ends-plan.md.Routing
DeclarationEmitterhas twoContext.Servicetags. A TypeScript 7.1 API adapter can provideCompilerEmitterwithout changing the model schema. TypeScript 7.0.2'sunstable/syncAPI has no declaration-emit request, so 7.0 uses the process adapter.docs/profiles/2026-10-12-front-end-phases.mdcovers the 40 public systemfsoftware packages plus this one, which is isolated-clean. Both candidates agreed withtsc --isolatedDeclarationson every package. Emit plus parse took 77.79 ms withoxc-transformandoxc-parser0.150.0, against 947.03 ms with TS 5.9.3transpileDeclarationandcreateSourceFile. The fallback took 9257.41 ms. TS 7 and 5.9.3 printed 124 of 569 declaration files differently, so the builder reads structure and never depends on how one compiler prints. The commit adding the profile comes before the commit adding itsDecisionsection.-front-end-phases-built.mdrepeats the run with the build and resolve phases.UnsupportedDeclarationnames the file and the construct: a named re-export from another package,declare global, module augmentation,export =, or an enum member with a computed value. One gap is documented in the README:export *from another package adds no items.Review receipt (full path, exact head
a98496e37468e7edebed5673423ad9bc2bf20fff)ce-code-review mode:agent depth:full base:a053ee67129db1f9ba47a234ce8f06267c0b80b0 plan:docs/plans/2026-10-13-2019-feat-front-ends-plan.md. Run20261013-202026-a98496e3, statuscomplete, verdict Ready with fixes.metadata.jsonrecordshead_shaa98496e37468e7edebed5673423ad9bc2bf20fff. Report-only: nothing below was applied. The2c13063receipt is superseded.openrouter/deepseek/deepseek-v4.1-flashat thinkingmax, read-only tools, no extensions. OpenRouter reports the served model asdeepseek/deepseek-v4.1-flash.independence_verified: true(host Claude, peer DeepSeek). It contributed 3 findings. No Grok or xai-oauth route was used.systemfsoftware@8d071180, with no resolver warnings or errors. The 50 enrolled rules come out as 27 comply, 5 violate and 18 not matched. The root-directed #761 rule complies and is listed on its own row.Fixes since the
2c13063receipt (U12, U13)Each fix has a test that fails before it and passes after it. Rulings and scenarios are in the plan's U12 and U13 sections.
a98496edocs(front-end): rule the U13 delta review; record the node_modules mirror exception18c81e0fix(front-end): mirror a package installed under node_modules without colliding with its own symlink (U13 delta)5831e97docs(solutions): point the emit-option learning at the 1956 plana4ed6fddocs(front-end): rule the e5ec09d review (U13); third phase profile; mirrored node_modules learningd77e2ddfix(front-end): a mapped-type key or an infer variable shadows a declaration of the same name (U13)432b608fix(front-end): resolve types through the package's own node_modules ancestry; profile harness maps .cts and counts missing emits (U13 ci(mutation): run mutation testing on main through systemfsoftware's reusable workflow #5, ci(release): decide changeset coverage from turbo's build hashes #6)c4202d9fix(front-end): the fallback ignores the package's isolatedDeclarations; a diagnostic names a file only inside the package (U13 chore(release): version packages #4, feat(api-extractor): versioned API model, Effect-aware diff and semver verdict (PR1) #1)c093a03fix(front-end): skip null and array export targets; pattern exports read only TypeScript sources (U13 build: move api-extractor onto the systemfsoftware family toolchain #2, chore(packs): enroll ci-wall-clock and repin compound packs to 8d07118079 #8)e5ec09ddocs(solutions): point the emit-option learning at the 1834 plan35c33f4docs(plan): correct D1 and D2 to what the adapters do; record delta rulings (U12)8efc442docs(front-end): say both routes give one model apart from the producer stamp (U12 C11)1ba9588fix(front-end): carry the package's root files and type roots into the derived config (U12 C3)7520eaefix(front-end): read compiler diagnostics from the TypeScript 7 API, not tsc stdout (U12 C3)3e81954fix(front-end): decline what the fast path cannot prove (U12)fa1f695fix(front-end): read .cts entries and expand subpath pattern exports (U12)55eb06efix(front-end): scope type parameters to their declaration in the resolver (U12)5a862acfix(front-end): keep local alias names, optional tuple marks and setter text exact (U12)25d0ef9fix(front-end): synthetic diagnostics carry no TypeScript code (U12 C4)fb70da8docs(plan): rule every open front-end finding (U12)Findings at this head
declaration-graph.ts:369default, plus a builder scenario. A patch exists, unapplied.type-expr-of.ts:299type-expr-of.ts:513profile.ts:133declarationPathOfand the tsconfig choice to the harness, or rule the copies deliberate (KTD10).read-package.ts:182ManifestRejectedbesideNoSourceEntry.typescript-7-compiler.ts:275TYPESCRIPT_PRODUCERon a real fallback model.analysis-messages.integration.test.ts:33resolve-messages.ts:28Evidence beyond the reviewers (public surface, TypeScript 7.0.2, upstream api-extractor 7.59.1):
FrontEnd.buildModelonexport default function (x: number): string;besideexport declare const keep: number;returns items[keep]. The same holds forexport default class { m(): void; }. The named spelling gives adefaultitem. oxc-transform andtscboth print the anonymous form, so both routes lose the export.tsc's in four shapes. A consumer importing a named member gets TS2614 fromexport default class Widget(Widget) and fromexport default function format(format). It gets TS2460 fromdeclare const x; export { x as y }(x), and TS2614 fromdeclare const x, z; export default x(z). The builder exports[Widget, default],[default, format],[x, y]and[default, x, z]. Ruling chore(release): version packages #4 probed onlyexport {}.tscalso stops implicit exports on a non-empty export list and onexport default <identifier>, but not on exported declarations. This is new evidence for the root, not a re-raise of chore(release): version packages #4.@publicbehaves the same in both resolvers. For an@internalinside an@examplefence, ours reportsae-internal-missing-underscoreand upstream reportsae-missing-release-tag. For`@beta`in inline code, ours reports nothing and upstream reportsae-missing-release-tag.55eb06eonly in part. Upstream'sExtractorMessagecarries no item field, soitemis asserted in hand-written scenarios (analysis-messages.integration.test.ts:76,183,220-221,255-257) and not in the oracle rows (:29-33). The plan's A18 line overstates what landed.Suppressed at the confidence-50 anchor:
oxc-isolated-emitter.ts:144)profile.ts:439)analyse.ts:38)oxc-isolated-emitter.ts:51)read-package.ts:39)typescript-7-compiler.ts:269)Enrolled pack rules (all five packs, every rule)
callable-vs-resource-syntax.mddeclaration-emitter.service.ts:6-8), whose taxonomy is ruled by service-and-layer-boundaries.decode-never-cast.mdas T/as unknown asanywhere insrc(onlyas const);package.jsongoes throughSchema.decodeEffect(Schema.fromJsonString(PackageManifest))(read-package.ts:15,187);tscdiagnostics are read from the TS API as typed values, not cast (`typescfour-channel-contracts.mdSchema.TaggedErrors carrying data only (front-end-error.schema.ts:13-49); none wraps a caught exception, so none owescause: Schema.optional(Schema.Unknown). Residual:catchCauseswallows (see residual risks).handle-state-privacy.mdEffect.contextin a closure (oxc-isolated-emitter.ts:163-165,typescript-7-compiler.ts:376-379). No module-levelWeakMap/letregistry.pipeable-dual-parity.mdpipeline-composition.mdcell.runappears inside anEffect.gen.pure-decision-workflows.md*.workflow.tsdecision is added;judge-surfaces.workflow.ts:13is a type annotation only.resource-vs-handle-duality.mddeclaration-emitter.service.ts:6-23).sandwich-phase-order.mdEffect.gen:analyse.ts:38-50(export const analyse = (source: PackageSource): Effect.Effect<Analysis, …> => Effect.gen(…)) andread-package.ts:182-193, against the rule's "Every outside interaction is an I/scoped-lifecycle-boundaries.mdmakeTempDirectoryScoped,typescript-7-compiler.ts:114,147), the compile insideEffect.scoped(:361), and the TS API handle viaEffect.acquireUseReleasewithapi.close()as release (:94-98,:226-230).service-and-layer-boundaries.md*.service.tswith zero driver imports (declaration-emitter.service.ts:1-4); driver-backedlayerexports live in adapter modules beside it (oxc-isolated-emitter.ts:160,typescript-7-compiler.ts:369); no*Live, no.port.ts/.layer.ts.single-namespace-barrel.mdmod.ts:1-3exports three cohesive namespaces;FrontEnd.ts:1-25is the one barrel for the capability — no error/schema fragment barrels.staged-lawful-builders.mdstore-serializable-unit-of-work.mdstore-unit-of-work-handle.mdfake-and-real-store-laws.mdno-mocks-on-internal-glue.mdvi.fn/vi.mock/vi.spyOnanywhere in the package or the e2e app; every feature imports the public entry only (front-end-model.integration.test.ts:2,front-end-routing.integration.test.ts:2,package-reader.integration.test.ts:2, `analysis-messages.pin-dependency-semantics.mdtsc) is pinned executably by the route-equality journeys over real packages (front-ends.integration.test.ts,throughBothRoutes) and by the committed per-candidate verdicts in `docs/profiles/20port-fakes-pass-the-port-contract.mdIsolatedEmitter/CompilerEmitter; both routes run their real adapters (e2eFrontEnd.isolatedEmitterLayer/compilerEmitterLayer; KTD11 "no emitter is faked").real-system-oracles.mdtscof this package's dependency on real temp packages (front-ends.integration.test.ts:1-6,live), the in-process features use real temp dirs/files, and refusal is proven (TS9010 routing, TS5033, missing config, exclustaged-protocol-evidence.mdarbitrary-filter-floors.mdsrccarries its generation hint (api-model.schema.ts:98-102, used at:140and:153);Diagnostic.codeuses the built-inSchema.isGreaterThanonSchema.Int(declarations.schema.ts:44); noS.isPatterninsrc; the lookacross-field-invariants-as-struct-checks.mddata-only-schema-classes.mdSchema.TaggedErrors (the one allowed case), hold data only, and define argument-lessget message()getters reading their own fields (front-end-error.schema.ts:16-49,verdict.schema.ts:63-93); no instance methods, no non-metadata stainvariants-as-refinements.mdDiagnostic.codeis refined (Schema.Int.check(Schema.isGreaterThan(0)),declarations.schema.ts:44) instead of a bareFinite(U12 C4); no consumer re-checks a decoded value. Gap: no brand, no refusal property (root-directed row).law-failure-is-a-codec-defect.mda053ee6is "test(api-extractor): retire the generated schema laws (#9)"; there is nosrc/schema-laws.test.tsand no schema-laws plugin invitest.config.ts:1-11.recursive-schema-suspend.mdSchema.suspend+ explicitSchema.Codec<T>andrecursionBudget(api-model.schema.ts:113-116,145;type-expr.schema.ts:133-134,156-171,174-190); this diff changes onlyKeywordName's declaration form (type-expr.schema.ts:14-19).refusals-beside-generated-laws.mdimport.meta.vitestblock and no refined codec from ruling #2 survives U12 C3.rich-type-over-foreign-encoded.mdread-package.ts:39-40(typeof exports === 'object' ? Object.entries(exports)…),:25-28(Schema.is(ConditionMap),isString) interpret the foreign manifest's export map in application code whilepackage-source.schema.ts:6-24keeps only the raw Encodetagged-unions-over-state-by-presence.mddeclarations.schema.ts:42-46makesfile/codeoptional-by-provenance and consumers probe them (front-end-error.schema.ts:8,11); the sentinel design the rule targets (UNPARSED_FILED_LINE) is gone. A finding re-raising this shape is a non-finding per thtests-own-no-schemas.mdfront-end-model.integration.test.ts:2,6) and the only test-side shapes live in the harness fixturetests/__fixtures__/upstream-messages.ts:1-5. The `comcondition-branch-agreement.mdsrc/mod.ts:1-3;exports['.']inpackages/api-extractor/package.jsonis unchanged by this diff.declared-entry-points.mdpackage.json), and./package.jsonremains.enumerated-entry-modules.mdmod.ts:1-3names its three namespaces;FrontEnd.ts:1-25names every export it publishes (noexport *), and it is pure surface — re-exports only, no definitions.import-time-inertness.mdContext.Services, regexes, version stamps (oxc-isolated-emitter.ts:10,typescript-7-compiler.ts:11),import.meta.resolve(typescript-7-compiler.ts:13). No I/O, env read, clock, randomness, fibone-access-path.mdmod.ts:1-3); no aliases, no flat+namespace duplicate;TYPESCRIPT_PRODUCERandOXC_TRANSFORM_PRODUCERare different symbols (FrontEnd.ts:13,24).public-signature-types.mdFrontEnd.ts:1-25—Analysis,Route/FastPath/Fallback,Declarations,DeclarationFile,Diagnostic,PackageSource,SourceEntryPoint,DeclarationEmitter, both ids, the four errorsurface-changes-are-versioned.mdFrontEndnamespace →minorin.changeset/old-cobras-punch.md:1-2; wire versions are unchanged (API_MODEL_SCHEMA_VERSION = 2,VERDICT_SCHEMA_VERSION = 1), and U1's class→typed-constant moves keep both theflake-outputs-build-once-on-main.mdnix build .#sfs-depsat :29-30; consumer-store at :32; no cache step); .github/actions/checks-lane/action.yml:23,27; flake.nix:82-105 (sfsDepsOf linkFarm over the pinnedgates-share-one-job.mdheavy-lanes-state-their-daily-minutes.mdmainonly')heavy-suites-judge-every-main-commit.mdlight-checks-run-the-pinned-tool.mdno-dev-shell-on-verdict-lanes.mdno-lane-grades-the-repos-own-docs.mdone-stuck-job-backstop.mdpr-gate-runs-only-what-the-diff-affects.mdred-main-reverts-its-culprit-same-day.mdrelease-promotes-the-staging-proven-build.mdsetup-under-a-minute.mdschema-laws/refusal-properties-cover-unobservable-boundaries.md (systemfsoftware 0923912e, #761)The violations:
cell-architecture/sandwich-phase-order(unsettled):analyseandreadPackageare bareEffect.geninteractions, notSandwichcells. The plan fixes a port and a namespace, not a cell shape.schema-laws/rich-type-over-foreign-encoded(unsettled):read-package.tsinterprets the foreign export map in code, andpackage-source.schema.tskeeps only the raw union.schema-laws/tagged-unions-over-state-by-presence: settled by U13 feat(api-extractor): versioned API model, Effect-aware diff and semver verdict (PR1) #1 and U12 C4, which makeDiagnostic.fileandcodeoptional.ci-wall-clock/flake-outputs-build-once-on-mainandone-stuck-job-backstop: pre-existing..github,flake.nixandflake.lockare identical to base.ce-compound
No new learning this round: no code changed. The existing
docs/solutions/logic-errors/compiler-fallback-inherits-declaration-emit-options.mdpasses all three validators at this head, and the learnings reviewer cites it as honored.Verification
a98496e37468e7edebed5673423ad9bc2bf20fff:pnpm turbo run lint typecheck build test --forcegives 9/9 tasks. Package tests 148/148, e2e 4/4.tsc -p tsconfig.build.json --noEmitis clean, so the package stays isolatedDeclarations-clean.attwreportsok.e4478f03…6da2), so verdict output bytes are unchanged.git grepfinds no locale APIs insrc.a98496e37468e7edebed5673423ad9bc2bf20fff: CI run 38462248388 (11 jobs) success; Changeset Check run 38462248604 success.2b2e595) refuted the generated law∀x_TypeExprNesting_≤MaxDepth1with replayproperty=3804450008;seed=2068186473;runs=3;attempt=2;size=10;path=2,1,1,3,3,5,5,4. The same replay failed onorigin/maincd9f133d, andmainretired the generated laws in test(api-extractor): retire the generated schema laws #9 (a053ee6).