Skip to content

feat(model): anchors, release tags and manifest facts in model v3 - #15

Open
systemfsoftware-maker wants to merge 2 commits into
mainfrom
feat/model-v3
Open

systemfsoftware-maker wants to merge 2 commits into
mainfrom
feat/model-v3

Conversation

@systemfsoftware-maker

@systemfsoftware-maker systemfsoftware-maker commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

What

Model schema version 3, the source index and manifest target resolution: the model additions the report and the change report read.

  • Every item (function, variable, typeAlias, interface, class, enum, namespace) carries anchor (repository-relative file and 1-based line, or null), releaseTag (public, beta, alpha, internal or untagged) and stability (the TSDoc @stability value, or null).
  • ApiModel v3 carries anchorBase, package.exports and package.imports (ManifestEntry), messages (moved to src/model/message.schema.ts, each with its anchor) and coverage with typed reasons (unsupported, unresolved, unmapped-anchor, pattern-empty, parse, external-star). A model declaring schema version 2 is refused by the decoder.
  • Declarations gains sources; both emitter routes populate it, so the source index never re-reads the filesystem. source-index.ts parses each source with oxc-parser and records declarations, release tags, @stability, re-export statements and ambient block positions; the comment and tag helpers shared with the message resolver live in tsdoc.ts.
  • readPackage resolves every export through the requested condition, then types, import, default, then the first remaining key; a bare-string exports names .; a resolved target that is not TypeScript source is refused with NoSourceEntry, naming the export key and target; imports maps are decoded and recorded as internal surface, so a package with an imports map and no exports analyses to zero entry points with complete coverage.
  • FrontEnd.analyse returns { route, model }; the messages and coverage live in the model. buildModel(declarations) still builds from declarations alone, with every item unanchored and an unmapped-anchor reason.
  • Type parameters keep their in, out and const modifiers, closing the PR feat(front-end): build the API model from a package's source #7 review's deferred finding.
  • The generated schema-law suite is enabled through inlineSchemaTests(), so every exported schema carries round-trip and encode-stability laws.

Evidence

  • vitest run: 335 tests pass across 13 files (the existing suite, the generated schema laws and the new version 3 feature), and the e2e front-end feature passes 4/4.
  • tsc --noEmit and oxlint: clean for the package and both apps.
  • model-v3.integration.test.ts: a bare-string export resolves .; anchors, release tags and @stability land on items; a namespace export anchors to its re-export line while its member anchors; a declarations-only model leaves items anchor: null with an unmapped-anchor reason; an untagged export carries ae-missing-release-tag at its own line; an imports-only package has zero entry points and complete coverage; a namespace member takes its tag from its own doc comment rather than the namespace's or an earlier declaration's.
  • serialise.integration.test.ts: a schemaVersion: 2 document is refused and canonical round-tripping still holds.
  • The e2e front-end feature: both routes serialise equal models and equal messages once the producer is set equal, now including equal message anchors.

Notes

No new runtime dependency. The package keeps its single . export and one mod.ts barrel. No private repository, package or path is named. A changeset records the breaking model version.

Review follow-ups (non-blocking, from the review at 4b700c8; not committed)

  1. Blocking, fixed in cf3b4d9. The first member of a namespace block has no preceding sibling, so precedingEnd returned 0 and the leading-doc-comment lookup reached back past the namespace to an unrelated top-level comment. The block's opening brace is now the comment floor. A model v3 scenario covers a @beta namespace whose first member is untagged and whose second member carries its own @internal tag, plus a later namespace after an internal declaration. The scenario was confirmed to fail without the floor fix.
  2. read-package.ts entryPointsOf: one non-TypeScript export (e.g. ./styles.css) now fails the whole package with NoSourceEntry; the ./legacy fixture case was removed instead of kept; TYPESCRIPT_SOURCE matches .d.ts, so a types: ./dist/x.d.ts fallback becomes an entry point in dist. Confirm intended versus an unresolved coverage reason.
  3. api-model.schema.ts 135-141: the coverage kinds unsupported, unresolved and external-star, and CoverageReason.construct, are declared but never emitted.
  4. recordBlock: dotted namespaces (namespace A.B {}) lose anchors because the nested TSModuleDeclaration body is ignored.
  5. lineAt (source-index.ts:93): the mutable cursor assumes increasing offsets; a line-start table with binary search removes the order dependence.

Every item in the model now carries its source anchor, release tag and
`@stability` value, and the model carries the package's `exports` and
`imports` maps, the analysis messages with their anchors, the anchor base
and the coverage. The schema version moves to 3 and a version 2 model is
refused.

A source index parses every source both emitter routes read with
oxc-parser, so the two routes give the same anchors without declaration
maps; the TSDoc helpers are shared with the message resolver. `readPackage`
resolves targets through the requested condition, then `types`, `import`,
`default` and the first remaining key, accepts a bare-string `exports` and
bare path targets, refuses a target that is not TypeScript source, and
records an `imports` map as internal surface. `analyse` returns
`{ route, model }` with the messages and coverage in the model.

Type parameters keep their `in`, `out` and `const` modifiers, closing the
PR #7 review's deferred finding. The generated schema-law suite is enabled
through `inlineSchemaTests()`.

@kiro-systemf kiro-systemf Bot 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.

Independent review at 4b700c8 (checks green, version bump consistent, no attribution trailers).

Blocking:

  1. src/front-end/source-index.ts:148 with recordBlock/precedingEnd: the first member of a namespace takes its release tag and @stability from the wrong comment. Inside a namespace, siblings = block.body, so for the first member precedingEnd returns 0 and leadingDocComment picks the last doc comment in the whole file before the member: the namespace's own comment or an earlier top-level declaration's. /** @beta */ export namespace Config { export const load = 1 } records Config.load as beta (should be untagged); /** @internal */ export const x = 1 then export namespace NS { export const a = 1 } marks NS.a internal. Floor should be the block's opening brace (block.start). Add a test for a tag on a namespace member.

Non-blocking:
2. read-package.ts entryPointsOf: one non-TS export (e.g. "./styles.css") now fails the whole package with NoSourceEntry; the ./legacy fixture case was removed instead of kept; TYPESCRIPT_SOURCE matches .d.ts, so a types: ./dist/x.d.ts fallback becomes an entry point in dist. Confirm intended vs. an unresolved coverage reason.
3. api-model.schema.ts:135-141: coverage kinds unsupported/unresolved/external-star and CoverageReason.construct are declared but never emitted.
4. recordBlock: dotted namespaces (namespace A.B {}) lose anchors because the nested TSModuleDeclaration body is ignored.
5. lineAt (source-index.ts:93): the mutable cursor assumes increasing offsets; a line-start table with binary search removes the order dependence.

leadreview

The first statement in a namespace block has no preceding sibling, so precedingEnd returned 0 and the leading-doc-comment lookup reached back past the namespace to an unrelated top-level comment. The first member of a @beta namespace inherited beta, and a member of a later namespace inherited an earlier declaration's internal tag.

Carry the block's opening brace as the comment floor in the source index context and use the greater of that floor and the preceding sibling's end. The namespace's own tag still comes from its own leading comment.

Adds a model v3 scenario covering a tagged namespace whose first member is untagged and whose second member carries its own tag, plus a later namespace after an internal declaration.
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.

2 participants