mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
docs: fix release smoke test failures
This commit is contained in:
@@ -12,6 +12,7 @@ import { dirname, resolve, sep } from 'node:path'
|
||||
import ts from 'typescript'
|
||||
import { LINK_MAP } from './gen-cordis-catalog.ts'
|
||||
import { parseJsDoc, pointer, rawJsDoc } from './jsdoc.ts'
|
||||
import { githubSlug } from './verify-md-links.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const OUT = 'docs/config-catalog.md'
|
||||
@@ -766,11 +767,6 @@ export function collectConfigCatalog(scanRoot: string = root): CatalogEntry[] {
|
||||
return entries.sort((a, b) => a.pkg.localeCompare(b.pkg))
|
||||
}
|
||||
|
||||
/** GitHub-style anchor slug for a `## \`pkg\`` heading. */
|
||||
function slug(heading: string): string {
|
||||
return heading.toLowerCase().replace(/[^a-z0-9 -]/g, '').replace(/ /g, '-')
|
||||
}
|
||||
|
||||
/** Render the `Requires:` service-key line, or '' when the plugin injects nothing. */
|
||||
function requiresLine(inject: string[]): string {
|
||||
return inject.length ? `Requires: ${inject.map(k => `\`${k}\``).join(' · ')}` : ''
|
||||
@@ -782,7 +778,7 @@ function requiresLine(inject: string[]): string {
|
||||
function refLink(ref: TypeRef, byName: Map<string, CatalogEntry>): string {
|
||||
const target = byName.get(ref.specifier)
|
||||
if (target?.kind === 'config' && ref.imported === target.configTypeName) {
|
||||
return `[\`${ref.alias}\`](#${slug(target.pkg)})`
|
||||
return `[\`${ref.alias}\`](#${githubSlug(target.pkg)})`
|
||||
}
|
||||
const page = LINK_MAP[ref.imported]
|
||||
if (page) return `[\`${ref.alias}\`](subsystems/${page})`
|
||||
@@ -792,7 +788,7 @@ function refLink(ref: TypeRef, byName: Map<string, CatalogEntry>): string {
|
||||
|
||||
/** Render one configurable plugin's section. */
|
||||
function renderConfigEntry(entry: CatalogEntry, byName: Map<string, CatalogEntry>): string[] {
|
||||
const out = [`## \`${entry.pkg}\``, '']
|
||||
const out = [`<a id="${githubSlug(entry.pkg)}"></a>`, '', `## \`${entry.pkg}\``, '']
|
||||
const requires = requiresLine(entry.inject)
|
||||
if (requires) out.push(requires, '')
|
||||
out.push('```' + FENCE, ...(entry.pastes ?? []).map(p => p.text).join('\n\n').split('\n'), '```', '')
|
||||
|
||||
@@ -10,6 +10,7 @@ import { globSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { resolve, sep } from 'node:path'
|
||||
import ts from 'typescript'
|
||||
import { parseJsDoc, pointer, rawJsDoc, reportViolations } from './jsdoc.ts'
|
||||
import { githubSlug } from './verify-md-links.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const OUT = 'docs/persistence-catalog.md'
|
||||
@@ -341,7 +342,8 @@ function typeLinks(payload: string): string {
|
||||
|
||||
/** Render one log event entry. */
|
||||
function renderEvent(e: AnnotatedLogEventEntry): string[] {
|
||||
const out = [`#### \`${e.name}\` — ${e.surface ? 'surface' : 'log-only'}`, '']
|
||||
const heading = `${e.name} — ${e.surface ? 'surface' : 'log-only'}`
|
||||
const out = [`<a id="${githubSlug(heading)}"></a>`, '', `#### \`${e.name}\` — ${e.surface ? 'surface' : 'log-only'}`, '']
|
||||
out.push('```' + FENCE, e.declaration, '```', '')
|
||||
const links = typeLinks(e.payload)
|
||||
if (links) out.push(links, '')
|
||||
|
||||
@@ -63,6 +63,7 @@ import * as ToolWeb from '@deepseek-ai/dsh-tool-web'
|
||||
import VmWorkflowEngine from '@deepseek-ai/dsh-workflow-worker-thread'
|
||||
import * as ToolRalph from '@deepseek-ai/dsh-tool-ralph'
|
||||
import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow'
|
||||
import { githubSlug } from './verify-md-links.ts'
|
||||
|
||||
/** Attachment seam marker that makes the attachments-conditional `read_image` schema harvestable. */
|
||||
class CatalogAttachmentStore extends AttachmentStore {
|
||||
@@ -704,7 +705,7 @@ export function render(catalog: ToolCatalog): string {
|
||||
'',
|
||||
]
|
||||
for (const entry of catalog) {
|
||||
lines.push(`## \`${entry.pkg}\``, '')
|
||||
lines.push(`<a id="${githubSlug(entry.pkg)}"></a>`, '', `## \`${entry.pkg}\``, '')
|
||||
for (const schema of entry.schemas) {
|
||||
// Collection validated that every harvested schema has a source.
|
||||
const source = entry.sources[schema.name] as string
|
||||
|
||||
@@ -606,7 +606,7 @@ function docSyncLeafGates(options: {
|
||||
pnpmScript('translation-prompt', 'verify-translation-prompt', { label: 'translation prompt' }),
|
||||
pnpmScript('translation-pairing', 'verify-translation-pairing', { label: 'translation pairing' }),
|
||||
pnpmScript('doc-budgets', 'verify-doc-budgets', { label: 'doc budgets' }),
|
||||
pnpmExec('docs-site-projection', ['vitest', 'run', 'scripts/project-doc-site.spec.ts'], {
|
||||
pnpmExec('docs-site-projection', ['vitest', 'run', 'scripts/project-doc-site.spec.ts', 'scripts/verify-doc-site-fragments.spec.ts'], {
|
||||
label: 'documentation projection',
|
||||
}),
|
||||
// Keep the VitePress build itself in one gate because projection rewrites website/.generated.
|
||||
|
||||
67
scripts/verify-doc-site-fragments.spec.ts
Normal file
67
scripts/verify-doc-site-fragments.spec.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/** Tests for built-site fragment validation. */
|
||||
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { inspectSiteFragments } from './verify-doc-site-fragments.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
function fixture(): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-doc-fragments-'))
|
||||
roots.push(root)
|
||||
mkdirSync(join(root, 'guide'), { recursive: true })
|
||||
writeFileSync(join(root, 'index.html'), '<a id="home"></a><a href="/guide/start#ready">start</a>')
|
||||
writeFileSync(join(root, 'guide/start.html'), [
|
||||
'<h1 id="ready">Ready</h1>',
|
||||
'<a name="legacy"></a>',
|
||||
'<a href="#ready">same page</a>',
|
||||
'<a href="./start.html#legacy">html alias</a>',
|
||||
'<a href="../#home">root</a>',
|
||||
'<a href="https://example.com/page#missing">external</a>',
|
||||
].join(''))
|
||||
return root
|
||||
}
|
||||
|
||||
describe('inspectSiteFragments', () => {
|
||||
it('rejects a directory with no built pages', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-doc-fragments-empty-'))
|
||||
roots.push(root)
|
||||
|
||||
expect(() => inspectSiteFragments(root)).toThrow('no HTML files found')
|
||||
})
|
||||
|
||||
it('resolves clean, encoded, and same-page routes', () => {
|
||||
const root = fixture()
|
||||
writeFileSync(join(root, 'guide/encoded.html'), '<h1 id="a b">Encoded</h1><a href="./encoded#a%20b">self</a>')
|
||||
|
||||
expect(inspectSiteFragments(root)).toEqual({ checked: 5, broken: [] })
|
||||
})
|
||||
|
||||
it('reports missing ids and missing built routes', () => {
|
||||
const root = fixture()
|
||||
writeFileSync(join(root, 'guide/broken.html'), [
|
||||
'<a href="./start#missing">id</a>',
|
||||
'<a href="./absent#missing">route</a>',
|
||||
].join(''))
|
||||
|
||||
expect(inspectSiteFragments(root).broken).toEqual([
|
||||
{
|
||||
source: 'guide/broken.html',
|
||||
href: './start#missing',
|
||||
target: 'guide/start.html',
|
||||
fragment: 'missing',
|
||||
},
|
||||
{
|
||||
source: 'guide/broken.html',
|
||||
href: './absent#missing',
|
||||
fragment: 'missing',
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
143
scripts/verify-doc-site-fragments.ts
Normal file
143
scripts/verify-doc-site-fragments.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Verify fragment links against the HTML emitted by VitePress. Markdown and
|
||||
* VitePress use different heading-slug algorithms, so source-link validation
|
||||
* alone cannot prove that a published fragment exists.
|
||||
*
|
||||
* Run with `tsx scripts/verify-doc-site-fragments.ts` after `docs:build`.
|
||||
*/
|
||||
|
||||
import { globSync, readFileSync } from 'node:fs'
|
||||
import { resolve, sep } from 'node:path'
|
||||
import { JSDOM } from 'jsdom'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
/** One fragment reference that does not resolve in the built site. */
|
||||
export interface BrokenSiteFragment {
|
||||
/** HTML file containing the link. */
|
||||
source: string
|
||||
/** Link value as emitted by VitePress. */
|
||||
href: string
|
||||
/** Built HTML target, or `undefined` when the route was not emitted. */
|
||||
target?: string
|
||||
/** Decoded fragment id requested by the link. */
|
||||
fragment: string
|
||||
}
|
||||
|
||||
/** Result of checking every fragment-bearing anchor in a built site. */
|
||||
export interface SiteFragmentReport {
|
||||
/** Number of internal fragment references inspected. */
|
||||
checked: number
|
||||
/** References whose route or fragment id is absent. */
|
||||
broken: BrokenSiteFragment[]
|
||||
}
|
||||
|
||||
interface BuiltPage {
|
||||
file: string
|
||||
route: string
|
||||
ids: Set<string>
|
||||
document: Document
|
||||
}
|
||||
|
||||
function posixPath(path: string): string {
|
||||
return path.split(sep).join('/')
|
||||
}
|
||||
|
||||
function routeFor(file: string): string {
|
||||
if (file === 'index.html') return '/'
|
||||
if (file.endsWith('/index.html')) return `/${file.slice(0, -'index.html'.length)}`
|
||||
return `/${file.slice(0, -'.html'.length)}`
|
||||
}
|
||||
|
||||
function aliasesFor(page: BuiltPage): string[] {
|
||||
if (page.route === '/') return ['/', '/index', '/index.html']
|
||||
if (page.route.endsWith('/')) {
|
||||
const stem = page.route.slice(0, -1)
|
||||
return [page.route, stem, `${stem}/index`, `${stem}/index.html`]
|
||||
}
|
||||
return [page.route, `${page.route}.html`]
|
||||
}
|
||||
|
||||
function decodedFragment(hash: string): string {
|
||||
try {
|
||||
return decodeURIComponent(hash.slice(1))
|
||||
} catch {
|
||||
return hash.slice(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check fragment-bearing links in a VitePress output directory.
|
||||
*
|
||||
* @param distRoot - Directory containing generated HTML files.
|
||||
* @returns Counted internal links and every unresolved target.
|
||||
*/
|
||||
export function inspectSiteFragments(distRoot: string): SiteFragmentReport {
|
||||
const files = globSync('**/*.html', { cwd: distRoot }).map(posixPath).sort()
|
||||
if (files.length === 0) {
|
||||
throw new Error(`verify-doc-site-fragments: no HTML files found under ${distRoot}; run docs:build first.`)
|
||||
}
|
||||
const pages: BuiltPage[] = files.map((file) => {
|
||||
const document = new JSDOM(readFileSync(resolve(distRoot, file), 'utf8')).window.document
|
||||
const ids = new Set<string>()
|
||||
for (const element of document.querySelectorAll<HTMLElement>('[id]')) ids.add(element.id)
|
||||
for (const element of document.querySelectorAll<HTMLAnchorElement>('a[name]')) {
|
||||
const name = element.getAttribute('name')
|
||||
if (name !== null) ids.add(name)
|
||||
}
|
||||
return { file, route: routeFor(file), ids, document }
|
||||
})
|
||||
|
||||
const byRoute = new Map<string, BuiltPage>()
|
||||
for (const page of pages) {
|
||||
for (const alias of aliasesFor(page)) byRoute.set(alias, page)
|
||||
}
|
||||
|
||||
const origin = 'https://dsh-docs.invalid'
|
||||
const broken: BrokenSiteFragment[] = []
|
||||
let checked = 0
|
||||
for (const page of pages) {
|
||||
for (const anchor of page.document.querySelectorAll<HTMLAnchorElement>('a[href]')) {
|
||||
const href = anchor.getAttribute('href')
|
||||
if (href === null || !href.includes('#')) continue
|
||||
let targetUrl: URL
|
||||
try {
|
||||
targetUrl = new URL(href, `${origin}${page.route}`)
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
if (targetUrl.origin !== origin || targetUrl.hash === '') continue
|
||||
const fragment = decodedFragment(targetUrl.hash)
|
||||
if (fragment === '') continue
|
||||
checked++
|
||||
const target = byRoute.get(targetUrl.pathname)
|
||||
if (target === undefined || !target.ids.has(fragment)) {
|
||||
broken.push({
|
||||
source: page.file,
|
||||
href,
|
||||
...(target === undefined ? {} : { target: target.file }),
|
||||
fragment,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return { checked, broken }
|
||||
}
|
||||
|
||||
function main(): number {
|
||||
const distRoot = resolve(root, 'website/.dist')
|
||||
const report = inspectSiteFragments(distRoot)
|
||||
if (report.broken.length === 0) {
|
||||
console.log(`verify-doc-site-fragments: ${report.checked} internal fragment reference(s) resolve.`)
|
||||
return 0
|
||||
}
|
||||
|
||||
console.error(`verify-doc-site-fragments: ${report.broken.length} broken fragment reference(s):`)
|
||||
for (const item of report.broken) {
|
||||
const target = item.target === undefined ? 'target route was not built' : `${item.target} has no id ${JSON.stringify(item.fragment)}`
|
||||
console.error(` ${item.source}: ${JSON.stringify(item.href)} (${target})`)
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
if (import.meta.main) process.exitCode = main()
|
||||
Reference in New Issue
Block a user