Merge remote-tracking branch 'origin/master' into codex/cordis-catalog-type-links
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Typecheck Markdown `ts` fences against the workspace API. `ignore-check` fences are reported as
|
||||
* opt-outs; generated catalog fragments and `type-equiv` blocks are skipped here because their
|
||||
* opt-outs; generated catalog fragments and source-equivalence blocks are skipped here because their
|
||||
* owning gates verify them. A build-coordinated mode consumes existing declarations without emit.
|
||||
*/
|
||||
|
||||
@@ -33,6 +33,7 @@ const KIND_BY_INFO: Record<string, BlockKind> = {
|
||||
'ts': 'check',
|
||||
'ts ignore-check': 'ignore',
|
||||
'ts type-equiv': 'type-equiv',
|
||||
'ts public-api': 'type-equiv',
|
||||
'ts cordis-catalog': 'cordis-catalog',
|
||||
'ts persistence-catalog': 'persistence-catalog',
|
||||
'ts config-catalog': 'config-catalog',
|
||||
|
||||
@@ -156,10 +156,10 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
{
|
||||
key: 'agents',
|
||||
pkg: 'agent',
|
||||
title: 'Agent registry',
|
||||
title: 'Agent service',
|
||||
mode: 'core',
|
||||
consumers: ['agent-loop', 'acp', 'cli-demo', 'subagent-inprocess', 'stdio-demo', 'invariants'],
|
||||
note: 'Owns live Agent handles and the create/resume factory seam.',
|
||||
note: 'Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation.',
|
||||
},
|
||||
{
|
||||
key: 'agentLoop',
|
||||
|
||||
+47
-11
@@ -20,11 +20,12 @@
|
||||
* cannot land undocumented without CI going red. Pages are English (the
|
||||
* planned zh translation flow arrives separately; see docs/i18n/README.md).
|
||||
*
|
||||
* Signature fences use the ` ```ts website-api ` info string: doc-typecheck
|
||||
* only processes its known info strings, so these bare (non-compilable)
|
||||
* signature fragments are skipped there, while VitePress still highlights the
|
||||
* `ts` token. The sidebar fragment `website/.vitepress/config/api-sidebar.json`
|
||||
* is generated alongside so navigation can never drift from the page set.
|
||||
* Signature fences use the ` ```ts website-api ` info string and retain the
|
||||
* declaration's original source JSDoc. doc-typecheck only processes its known
|
||||
* info strings, so these bare (non-compilable) fragments are skipped there,
|
||||
* while VitePress still highlights the `ts` token. The sidebar fragment
|
||||
* `website/.vitepress/config/api-sidebar.json` is generated alongside so
|
||||
* navigation can never drift from the page set.
|
||||
*
|
||||
* `tsx scripts/gen-website-api.ts` → write pages + sidebar
|
||||
* `tsx scripts/gen-website-api.ts --check` → exit 1 if committed copies are
|
||||
@@ -64,6 +65,8 @@ interface MemberDoc {
|
||||
heading: string
|
||||
/** All overload signature lines (bodies stripped). */
|
||||
signatures: string[]
|
||||
/** Original source JSDoc, dedented only from its containing declaration. */
|
||||
jsDoc: string
|
||||
/** Description prose, one paragraph per line. */
|
||||
doc: string
|
||||
/** Parameter name → `@param` text, in declaration order. */
|
||||
@@ -166,6 +169,20 @@ function load(rel: string): { sf: ts.SourceFile; text: string } {
|
||||
// The module-merge walk (cordisModuleBody / eventMembers / serviceClasses) is
|
||||
// shared with gen-cordis-catalog.ts via cordis-walk.ts.
|
||||
|
||||
/** Original JSDoc with only the source container's indentation removed. */
|
||||
function sourceJSDoc(text: string, sf: ts.SourceFile, node: ts.Node): string {
|
||||
const raw = rawJsDoc(text, node)
|
||||
if (raw === '') return ''
|
||||
const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf))
|
||||
const lineStart = sf.getPositionOfLineAndCharacter(line, 0)
|
||||
const indent = text.slice(lineStart, node.getStart(sf))
|
||||
return raw.split('\n')
|
||||
.map((sourceLine, index) => index > 0 && sourceLine.startsWith(indent)
|
||||
? sourceLine.slice(indent.length)
|
||||
: sourceLine)
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
/** Signature text of a member: full text minus body/initializer, whitespace
|
||||
* collapsed, trailing semicolon stripped. */
|
||||
function signatureOf(member: ts.Node, sf: ts.SourceFile): string {
|
||||
@@ -219,7 +236,7 @@ function memberDoc(
|
||||
const first = group[0]
|
||||
if (!first) throw new Error(`gen-website-api: empty member group for ${name}`)
|
||||
// Doc from the first overload that carries JSDoc prose.
|
||||
const rawDocs = group.map(m => rawJsDoc(text, m))
|
||||
const rawDocs = group.map(m => sourceJSDoc(text, sf, m))
|
||||
const docIndex = rawDocs.findIndex(r => parseJsDoc(r).doc !== '')
|
||||
const raw = docIndex === -1 ? '' : (rawDocs[docIndex] ?? '')
|
||||
const doc = parseJsDoc(raw).doc
|
||||
@@ -255,6 +272,7 @@ function memberDoc(
|
||||
signatures: (ts.isMethodDeclaration(first) && funcLike.length > 1
|
||||
? funcLike.filter(m => ts.isMethodDeclaration(m) && !m.body)
|
||||
: group).map(m => signatureOf(m, sf)),
|
||||
jsDoc: raw,
|
||||
doc,
|
||||
params,
|
||||
returns: returnsText,
|
||||
@@ -424,8 +442,13 @@ function declPaste(rel: string, symbol: string): { doc: string; code: string; so
|
||||
if (matches.length === 0) throw new Error(`gen-website-api: declaration ${symbol} not found in ${rel}`)
|
||||
const first = matches[0]
|
||||
if (!first) throw new Error(`gen-website-api: declaration ${symbol} not found in ${rel}`)
|
||||
const doc = parseJsDoc(rawJsDoc(text, first)).doc
|
||||
const code = matches.map(s => stripBodies(s, sf).replace(/^export\s+(default\s+)?/, '')).join('\n\n')
|
||||
const firstJSDoc = sourceJSDoc(text, sf, first)
|
||||
const doc = parseJsDoc(firstJSDoc).doc
|
||||
const code = matches.map((statement) => {
|
||||
const jsDoc = sourceJSDoc(text, sf, statement)
|
||||
const declaration = stripBodies(statement, sf).replace(/^export\s+(default\s+)?/, '')
|
||||
return jsDoc === '' ? declaration : `${jsDoc}\n${declaration}`
|
||||
}).join('\n\n')
|
||||
return { doc, code, source: pointer(rel, sf, first) }
|
||||
}
|
||||
|
||||
@@ -480,6 +503,8 @@ interface HarnessEvent {
|
||||
scope: string
|
||||
mode: Mode | null
|
||||
signature: string
|
||||
/** Original source event JSDoc, dedented from its module/interface. */
|
||||
jsDoc: string
|
||||
doc: string
|
||||
params: { name: string; text: string }[]
|
||||
source: string
|
||||
@@ -494,7 +519,7 @@ function collectHarnessEvents(violations: string[]): HarnessEvent[] {
|
||||
const body = cordisModuleBody(sf)
|
||||
if (!body) continue
|
||||
for (const { name, member } of eventMembers(body, sf)) {
|
||||
const raw = rawJsDoc(text, member)
|
||||
const raw = sourceJSDoc(text, sf, member)
|
||||
const { doc, mode } = parseJsDoc(raw)
|
||||
if (!mode) violations.push(`event '${name}' (${pointer(rel, sf, member)}) is missing @mode.`)
|
||||
if (!doc) violations.push(`event '${name}' (${pointer(rel, sf, member)}) has no JSDoc prose.`)
|
||||
@@ -509,7 +534,7 @@ function collectHarnessEvents(violations: string[]): HarnessEvent[] {
|
||||
const tag = tags.get(pname)
|
||||
if (tag) params.push({ name: pname, text: tag })
|
||||
}
|
||||
events.push({ name, scope: name.split('/')[0] ?? name, mode, signature: signatureOf(member, sf), doc, params, source: pointer(rel, sf, member) })
|
||||
events.push({ name, scope: name.split('/')[0] ?? name, mode, signature: signatureOf(member, sf), jsDoc: raw, doc, params, source: pointer(rel, sf, member) })
|
||||
}
|
||||
}
|
||||
return events.sort((a, b) => a.name.localeCompare(b.name))
|
||||
@@ -548,6 +573,7 @@ function renderMember(prefix: string, m: MemberDoc): string[] {
|
||||
const call = m.heading === '' ? '' : m.heading
|
||||
lines.push(`### ${prefix}${m.name}${call}`, '')
|
||||
lines.push('```' + FENCE)
|
||||
lines.push(m.jsDoc)
|
||||
for (const sig of m.signatures) lines.push(sig)
|
||||
lines.push('```', '')
|
||||
lines.push(...prose(m.doc), '')
|
||||
@@ -620,7 +646,7 @@ function renderEventsPage(events: HarnessEvent[]): string {
|
||||
for (const e of events.filter(ev => ev.scope === scope)) {
|
||||
lines.push(`### ${e.name}`, '')
|
||||
lines.push(`**Mode:** \`${e.mode ?? 'unknown'}\``, '')
|
||||
lines.push('```' + FENCE, e.signature, '```', '')
|
||||
lines.push('```' + FENCE, e.jsDoc, e.signature, '```', '')
|
||||
lines.push(...prose(e.doc), '')
|
||||
if (e.params.length > 0) {
|
||||
for (const p of e.params) lines.push(`- \`${p.name}\` — ${unlink(p.text)}`)
|
||||
@@ -653,6 +679,16 @@ export function generate(): Map<string, string> {
|
||||
const events = collectHarnessEvents(violations)
|
||||
files.set(`${PAGES_DIR}/harness/events.md`, renderEventsPage(events))
|
||||
|
||||
for (const [rel, content] of files) {
|
||||
if (!rel.endsWith('.md')) continue
|
||||
for (const match of content.matchAll(/^```ts website-api\n([\s\S]*?)\n```$/gm)) {
|
||||
const body = match[1] ?? ''
|
||||
if (!body.startsWith('/**')) {
|
||||
violations.push(`${rel}: a ts website-api fence does not begin with original source JSDoc.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reportViolations('gen-website-api', violations)
|
||||
|
||||
const sidebar = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"comment": "Maps each ` ```ts type-equiv ` block (by doc + declared symbol) to the source declaration and original JSDoc it must match. verify-type-equiv.ts enforces a 1:1 correspondence: every type-equiv block has exactly one entry here, and every entry resolves to exactly one block. Add an entry when you add a type-equiv block; remove it when you remove the block.",
|
||||
"comment": "Maps each ` ```ts type-equiv ` or ` ```ts public-api ` block (by doc + declared symbol + projection) to the source declaration and original JSDoc it must match. Omit projection for the complete declaration; use public-api with a ` ```ts public-api ` block for a body-stripped public class declaration. verify-type-equiv.ts enforces a 1:1 correspondence: every source-equivalence block has exactly one entry here, and every entry resolves to exactly one block. Add an entry when you add a source-equivalence block; remove it when you remove the block.",
|
||||
"entries": [
|
||||
{ "doc": "docs/core-data-structures/core.md", "symbol": "Branded", "source": "packages/util/brand/src/index.ts" },
|
||||
{ "doc": "docs/core-data-structures/core.md", "symbol": "ContentBlockMap", "source": "packages/llm/llm/src/types.ts" },
|
||||
@@ -33,6 +33,8 @@
|
||||
{ "doc": "docs/core-data-structures/llm-streaming.md", "symbol": "TokenUsage", "source": "packages/llm/llm/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/llm-streaming.md", "symbol": "ContentBlockMap", "source": "packages/llm/llm/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/llm-streaming.md", "symbol": "AppIdentity", "source": "packages/llm/llm/src/attribution.ts" },
|
||||
{ "doc": "docs/core-data-structures/llm-streaming.md", "symbol": "BlockAssembler", "source": "packages/llm/llm/src/assembler.ts", "projection": "public-api" },
|
||||
{ "doc": "docs/core-data-structures/llm-streaming.md", "symbol": "LlmAdapter", "source": "packages/llm/llm/src/index.ts", "projection": "public-api" },
|
||||
|
||||
{ "doc": "docs/core-data-structures/token-meter.md", "symbol": "TokenMeasurement", "source": "packages/llm/token-meter/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/token-meter.md", "symbol": "TokenSurfaceNode", "source": "packages/llm/token-meter/src/types.ts" },
|
||||
@@ -50,6 +52,7 @@
|
||||
{ "doc": "docs/core-data-structures/session.md", "symbol": "SessionSurface", "source": "packages/core/session/src/surface.ts" },
|
||||
{ "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceFoldReplacement", "source": "packages/core/session/src/surface.ts" },
|
||||
{ "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceFoldResult", "source": "packages/core/session/src/surface.ts" },
|
||||
{ "doc": "docs/core-data-structures/session.md", "symbol": "Session", "source": "packages/core/session/src/index.ts", "projection": "public-api" },
|
||||
|
||||
{ "doc": "docs/core-data-structures/persistence.md", "symbol": "SessionHeader", "source": "packages/core/session/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/persistence.md", "symbol": "CreateSessionOptions", "source": "packages/core/session/src/types.ts" },
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/**
|
||||
* Verify every `ts type-equiv` block against the source symbol named by the
|
||||
* manifest. Blocks and entries have a one-to-one relationship; comparison
|
||||
* Verify every `ts type-equiv` and `ts public-api` block against the source
|
||||
* symbol named by the manifest. Ordinary entries preserve the complete
|
||||
* declaration; `public-api` entries preserve a class's body-stripped public
|
||||
* declaration. Blocks and entries have a one-to-one relationship; comparison
|
||||
* ignores whitespace and non-JSDoc comments but preserves declaration
|
||||
* structure and every original JSDoc comment.
|
||||
*/
|
||||
@@ -14,23 +16,27 @@ const root = resolve(import.meta.dirname, '..')
|
||||
/** Scan doc-typecheck's full Markdown scope so unmanifested blocks also fail. */
|
||||
const MARKDOWN_GLOBS = ['README.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md', 'website/zh-CN/**/*.md']
|
||||
|
||||
/** One manifest entry: a documented type-equiv block and its source symbol. */
|
||||
/** One manifest entry: a source-equivalence block and its source symbol. */
|
||||
interface ManifestEntry {
|
||||
/** Doc file (repo-relative) containing the ` ```ts type-equiv ` block. */
|
||||
/** Doc file (repo-relative) containing the source-equivalence block. */
|
||||
doc: string
|
||||
/** The declared symbol the block must match (e.g. `SessionEvent`). */
|
||||
symbol: string
|
||||
/** Source file (repo-relative) that exports the symbol. */
|
||||
source: string
|
||||
/** Complete declaration (default), or a body-stripped public class API. */
|
||||
projection?: 'public-api'
|
||||
}
|
||||
|
||||
/** One extracted ` ```ts type-equiv ` block. */
|
||||
/** One extracted ` ```ts type-equiv ` or ` ```ts public-api ` block. */
|
||||
interface EquivBlock {
|
||||
doc: string
|
||||
/** 1-based line of the opening fence (for diagnostics). */
|
||||
line: number
|
||||
/** Symbol name parsed from the block's declaration. */
|
||||
symbol: string
|
||||
/** Complete declaration (default), or a body-stripped public class API. */
|
||||
projection?: 'public-api'
|
||||
/** Block body (the pasted declaration). */
|
||||
code: string
|
||||
}
|
||||
@@ -58,7 +64,7 @@ function stripExport(code: string): string {
|
||||
return code.replace(/^export\s+(default\s+)?/, '')
|
||||
}
|
||||
|
||||
/** Parse the declared symbol name from a type-equiv block body. */
|
||||
/** Parse the declared symbol name from a source-equivalence block body. */
|
||||
function blockSymbol(code: string): string | null {
|
||||
const sf = ts.createSourceFile('type-equiv.ts', code, ts.ScriptTarget.Latest, /* setParentNodes */ false, ts.ScriptKind.TS)
|
||||
for (const stmt of sf.statements) {
|
||||
@@ -70,12 +76,12 @@ function blockSymbol(code: string): string | null {
|
||||
return null
|
||||
}
|
||||
|
||||
/** Extract every ` ```ts type-equiv ` block from one Markdown file. */
|
||||
/** Extract every source-equivalence block from one Markdown file. */
|
||||
function extractEquivBlocks(docRel: string): EquivBlock[] {
|
||||
const text = readFileSync(resolve(root, docRel), 'utf8')
|
||||
const lines = text.split('\n')
|
||||
const blocks: EquivBlock[] = []
|
||||
let open: { line: number; body: string[] } | null = null
|
||||
let open: { line: number; body: string[]; projection?: 'public-api' } | null = null
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const raw = lines[i] ?? ''
|
||||
@@ -90,11 +96,22 @@ function extractEquivBlocks(docRel: string): EquivBlock[] {
|
||||
if (!symbol) {
|
||||
throw new Error(`verify-type-equiv: ${docRel}:${open.line} — type-equiv block has no parseable interface/type/class declaration`)
|
||||
}
|
||||
blocks.push({ doc: docRel, line: open.line, symbol, code })
|
||||
blocks.push({
|
||||
doc: docRel,
|
||||
line: open.line,
|
||||
symbol,
|
||||
code,
|
||||
...(open.projection === undefined ? {} : { projection: open.projection }),
|
||||
})
|
||||
open = null
|
||||
continue
|
||||
}
|
||||
if ((fence[2] ?? '').trim() === 'ts type-equiv') open = { line: i + 1, body: [] }
|
||||
const info = (fence[2] ?? '').trim()
|
||||
if (info === 'ts type-equiv public-api') {
|
||||
throw new Error(`verify-type-equiv: ${docRel}:${i + 1} — use the concise \`ts public-api\` fence`)
|
||||
}
|
||||
if (info === 'ts type-equiv') open = { line: i + 1, body: [] }
|
||||
if (info === 'ts public-api') open = { line: i + 1, body: [], projection: 'public-api' }
|
||||
}
|
||||
if (open) throw new Error(`verify-type-equiv: ${docRel}:${open.line} — unterminated type-equiv block`)
|
||||
return blocks
|
||||
@@ -127,13 +144,77 @@ function sourceDeclaration(sourceRel: string, symbol: string): string | null {
|
||||
return null
|
||||
}
|
||||
|
||||
/** Leading source JSDoc attached to one declaration or member. */
|
||||
function sourceJSDoc(text: string, node: ts.Node): string {
|
||||
return ts.getJSDocCommentsAndTags(node)
|
||||
.filter(ts.isJSDoc)
|
||||
.map(doc => text.slice(doc.pos, doc.end))
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
/** Whether a class member is part of its public declaration. */
|
||||
function isPublicMember(member: ts.ClassElement): boolean {
|
||||
if (ts.isClassStaticBlockDeclaration(member)) return false
|
||||
const name = ts.getNameOfDeclaration(member)
|
||||
if (name && ts.isPrivateIdentifier(name)) return false
|
||||
const modifiers = ts.canHaveModifiers(member) ? ts.getModifiers(member) : undefined
|
||||
return !(modifiers?.some(modifier =>
|
||||
modifier.kind === ts.SyntaxKind.PrivateKeyword
|
||||
|| modifier.kind === ts.SyntaxKind.ProtectedKeyword,
|
||||
) ?? false)
|
||||
}
|
||||
|
||||
/** Remove an implementation body while retaining the source signature. */
|
||||
function bodylessMember(text: string, sf: ts.SourceFile, member: ts.ClassElement): string {
|
||||
const start = member.getStart(sf)
|
||||
let end = member.end
|
||||
if (ts.isConstructorDeclaration(member) || ts.isMethodDeclaration(member)
|
||||
|| ts.isGetAccessorDeclaration(member) || ts.isSetAccessorDeclaration(member)) {
|
||||
if (member.body) end = member.body.getStart(sf)
|
||||
}
|
||||
if (ts.isPropertyDeclaration(member) && member.initializer) end = member.initializer.getStart(sf)
|
||||
const signature = text.slice(start, end).trimEnd().replace(/;$/, '').replace(/=\s*$/, '').trimEnd()
|
||||
return `${signature};`
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a class as an ambient declaration containing only its public fields,
|
||||
* constructor, accessors, and methods. Implementation bodies and private or
|
||||
* protected members are deliberately absent; original class/member JSDoc is
|
||||
* retained so the projection is the source-owned public contract.
|
||||
*/
|
||||
function sourcePublicApi(sourceRel: string, symbol: string): string | null {
|
||||
const abs = resolve(root, sourceRel)
|
||||
const text = readFileSync(abs, 'utf8')
|
||||
const sf = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, /* setParentNodes */ true)
|
||||
for (const stmt of sf.statements) {
|
||||
if (!ts.isClassDeclaration(stmt) || stmt.name?.text !== symbol) continue
|
||||
const classDoc = sourceJSDoc(text, stmt)
|
||||
const abstract = stmt.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AbstractKeyword) ? 'abstract ' : ''
|
||||
const typeParameters = stmt.typeParameters?.map(parameter => parameter.getText(sf)).join(', ')
|
||||
const heritage = stmt.heritageClauses?.map(clause => clause.getText(sf)).join(' ')
|
||||
const header = `declare ${abstract}class ${symbol}${typeParameters ? `<${typeParameters}>` : ''}${heritage ? ` ${heritage}` : ''} {`
|
||||
const members = stmt.members
|
||||
.filter(isPublicMember)
|
||||
.map((member) => {
|
||||
const jsDoc = sourceJSDoc(text, member)
|
||||
const declaration = bodylessMember(text, sf, member)
|
||||
return jsDoc === '' ? declaration : `${jsDoc}\n${declaration}`
|
||||
})
|
||||
const declaration = [header, ...members.map(member => member.split('\n').map(line => ` ${line}`).join('\n')), '}'].join('\n')
|
||||
return classDoc === '' ? declaration : `${classDoc}\n${declaration}`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const manifestRaw = readFileSync(resolve(root, 'scripts/type-equiv.manifest.json'), 'utf8')
|
||||
const manifest = JSON.parse(manifestRaw) as { entries: ManifestEntry[] }
|
||||
const entries = manifest.entries
|
||||
|
||||
// Key a block/entry by doc + symbol (a symbol may be documented in more than one
|
||||
// doc, but at most once per doc).
|
||||
const keyOf = (x: { doc: string; symbol: string }): string => `${x.doc}::${x.symbol}`
|
||||
// Key a block/entry by doc + symbol + projection. A symbol may be documented in
|
||||
// more than one doc, and a doc may carry both complete and projected forms.
|
||||
const keyOf = (x: { doc: string; symbol: string; projection?: 'public-api' }): string =>
|
||||
`${x.doc}::${x.symbol}::${x.projection ?? 'declaration'}`
|
||||
|
||||
// Collect every type-equiv block across ALL docs in scope — not only the docs
|
||||
// the manifest names — so a block in an unmanifested doc is found and reported
|
||||
@@ -152,7 +233,7 @@ for (const d of [...new Set(entries.map(e => e.doc))]) {
|
||||
else if (!docSet.has(d)) errors.push(`manifest references ${d}, which is outside the scanned markdown scope (${MARKDOWN_GLOBS.join(', ')})`)
|
||||
}
|
||||
|
||||
// Duplicate-block guard: the same symbol twice in one doc is ambiguous.
|
||||
// Duplicate-block guard: the same projected symbol twice in one doc is ambiguous.
|
||||
const blockByKey = new Map<string, EquivBlock>()
|
||||
for (const b of blocks) {
|
||||
const k = keyOf(b)
|
||||
@@ -192,7 +273,9 @@ let verified = 0
|
||||
for (const e of entries) {
|
||||
const b = blockByKey.get(keyOf(e))
|
||||
if (!b) continue // already reported as an orphan entry
|
||||
const decl = sourceDeclaration(e.source, e.symbol)
|
||||
const decl = e.projection === 'public-api'
|
||||
? sourcePublicApi(e.source, e.symbol)
|
||||
: sourceDeclaration(e.source, e.symbol)
|
||||
if (decl === null) {
|
||||
errors.push(`symbol ${e.symbol} not found in ${e.source} (manifest entry for ${e.doc})`)
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user