docs: replace model experience tables with sections

This commit is contained in:
Tianyi Cui
2026-07-13 22:26:33 +08:00
parent 646f3d2d0c
commit d77f797d89
57 changed files with 712 additions and 325 deletions
@@ -1,8 +1,9 @@
/**
* Doc-sync gate: require every workspace package README to explain its exact
* model-visible context surface and token behavior. Most packages require the
* canonical table plus an optional linked long-literal appendix; an audited
* allowlist requires one concise zero-effect or indirect-only sentence instead.
* canonical context-surface blocks plus an optional linked long-literal
* appendix; an audited allowlist requires one concise zero-effect or
* indirect-only sentence instead.
*
* Run: `tsx scripts/verify-package-readme-model-experience.ts`.
*/
@@ -13,9 +14,9 @@ import { relative, resolve } from 'node:path'
const root = resolve(import.meta.dirname, '..')
const HEADING = '## Model Experience'
const LIMITATIONS_HEADING = '## Known Limitations and Deferred Work'
const TABLE_HEADER = '| Context surface | What the model sees | Token effect |'
const TABLE_DIVIDER = '|---|---|---|'
const VERBATIM_HEADING = '### Verbatim model-visible text'
const MODEL_VIEW_LABEL = '**What the model sees**'
const TOKEN_EFFECT_LABEL = '**Token effect**'
const H2_HEADING = /^## .+$/
type SentenceKind = 'none' | 'indirect'
@@ -27,8 +28,8 @@ interface SentenceContract {
/**
* Packages whose Model Experience is simple enough for one gated sentence.
* Every other package must carry the canonical table. A package moves on or
* off this list in the same change that changes its context behavior.
* Every other package must carry canonical context-surface blocks. A package
* moves on or off this list with the change to its context behavior.
*/
const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/bash/bash': { kind: 'indirect', reason: 'The service interface delegates all model rendering to dsh-tool-bash.' },
@@ -75,36 +76,13 @@ function proseLines(text: string): Line[] {
return kept
}
/** Split a canonical table row without treating escaped pipes as delimiters. */
function tableCells(raw: string): string[] | undefined {
const last = raw.length - 1
if (!raw.startsWith('|') || !raw.endsWith('|') || isEscaped(raw, last)) return undefined
const cells: string[] = []
let start = 1
for (let index = 1; index < last; index += 1) {
if (raw[index] !== '|' || isEscaped(raw, index)) continue
cells.push(raw.slice(start, index).trim())
start = index + 1
}
cells.push(raw.slice(start, last).trim())
return cells
}
/** Whether the character at `index` follows an odd-length backslash run. */
function isEscaped(text: string, index: number): boolean {
let backslashes = 0
for (let cursor = index - 1; cursor >= 0 && text[cursor] === '\\'; cursor -= 1) backslashes += 1
return backslashes % 2 === 1
}
/** Validate the optional long-form literal appendix after a Model Experience table. */
/** Validate the optional long-form literal appendix after the context blocks. */
function validateVerbatimTail(raw: readonly string[]): { blocks: number; titles: string[]; error?: string } {
let cursor = 0
while (raw[cursor]?.trim().length === 0) cursor += 1
if (cursor === raw.length) return { blocks: 0, titles: [] }
if (raw[cursor] !== VERBATIM_HEADING) {
return { blocks: 0, titles: [], error: `non-table content must begin with ${VERBATIM_HEADING}` }
return { blocks: 0, titles: [], error: `content after the context surfaces must begin with ${VERBATIM_HEADING}` }
}
cursor += 1
@@ -125,14 +103,14 @@ function validateVerbatimTail(raw: readonly string[]): { blocks: number; titles:
titles.push(title)
cursor += 1
while (raw[cursor]?.trim().length === 0) cursor += 1
if (raw[cursor] !== '```text') {
return { blocks, titles, error: 'each verbatim entry requires an exact ```text fence' }
if (raw[cursor] !== '```markdown') {
return { blocks, titles, error: 'each verbatim entry requires an exact ```markdown fence' }
}
cursor += 1
const contentStart = cursor
while (cursor < raw.length && raw[cursor] !== '```') cursor += 1
if (cursor === raw.length) return { blocks, titles, error: 'unterminated verbatim ```text fence' }
if (cursor === contentStart) return { blocks, titles, error: 'verbatim ```text fence must not be empty' }
if (cursor === raw.length) return { blocks, titles, error: 'unterminated verbatim ```markdown fence' }
if (cursor === contentStart) return { blocks, titles, error: 'verbatim ```markdown fence must not be empty' }
cursor += 1
blocks += 1
}
@@ -147,7 +125,8 @@ function headingFragment(title: string): string {
const failures: Failure[] = []
const packageJsons = globSync('packages/*/*/package.json', { cwd: root }).sort()
const scannedPackages = new Set(packageJsons.map(path => path.slice(0, -'/package.json'.length)))
let tableCount = 0
let structuredCount = 0
let contextSurfaceCount = 0
let noneCount = 0
let indirectCount = 0
let verbatimBlockCount = 0
@@ -157,7 +136,7 @@ for (const [pkg, contract] of Object.entries(SENTENCE_MODEL_EXPERIENCE)) {
failures.push({ path: `${pkg}/README.md`, message: 'sentence allowlist entry does not name a scanned package' })
}
if (contract.reason.trim().length === 0) {
failures.push({ path: `${pkg}/README.md`, message: 'sentence allowlist entry must justify why a table is unnecessary' })
failures.push({ path: `${pkg}/README.md`, message: 'sentence allowlist entry must justify why structured context surfaces are unnecessary' })
}
}
@@ -225,59 +204,81 @@ for (const packageJson of packageJsons) {
continue
}
const headers = section.filter(line => line.raw === TABLE_HEADER)
const header = headers[0]
const headerIndex = header === undefined ? -1 : section.indexOf(header)
if (header === undefined || headers.length !== 1 || headerIndex < 0 || section[headerIndex + 1]?.raw !== TABLE_DIVIDER) {
failures.push({ path: readme, message: `must contain the exact table header ${TABLE_HEADER}` })
const appendixIndex = content.findIndex(line => line.raw === VERBATIM_HEADING)
const surfaceContent = appendixIndex < 0 ? content : content.slice(0, appendixIndex)
if (surfaceContent.length === 0 || surfaceContent.length % 3 !== 0) {
failures.push({ path: readme, message: 'must contain one or more complete context-surface blocks' })
continue
}
const rows: Line[] = []
for (const line of section.slice(headerIndex + 2)) {
if (!line.raw.startsWith('|')) break
rows.push(line)
}
if (rows.length === 0) {
failures.push({ path: readme, message: 'Model Experience table must contain at least one data row' })
continue
}
for (const row of rows) {
const cells = tableCells(row.raw)
if (cells === undefined || cells.length !== 3 || cells.some(cell => cell.length === 0)) {
failures.push({ path: readme, message: `line ${row.index}: invalid three-column Model Experience row: ${row.raw}` })
const surfaces: Array<{ heading: Line; modelView: Line; tokenEffect: Line }> = []
const surfaceFragments = new Set<string>()
let previousTokenEffect: Line | undefined
let surfaceError = false
for (let index = 0; index < surfaceContent.length; index += 3) {
const heading = surfaceContent[index] as Line
const modelView = surfaceContent[index + 1] as Line
const tokenEffect = surfaceContent[index + 2] as Line
const fragment = /^### \S/.test(heading.raw) && heading.raw !== VERBATIM_HEADING
? headingFragment(heading.raw.slice('### '.length))
: ''
if (fragment.length === 0) {
failures.push({ path: readme, message: `line ${heading.index}: each context surface requires a non-empty H3 heading` })
surfaceError = true
break
}
if (surfaceFragments.has(fragment)) {
failures.push({ path: readme, message: `line ${heading.index}: duplicate context-surface link fragment ${JSON.stringify(fragment)}` })
surfaceError = true
break
}
if (!modelView.raw.startsWith(`${MODEL_VIEW_LABEL}: `) || modelView.raw.slice(`${MODEL_VIEW_LABEL}: `.length).trim().length === 0) {
failures.push({ path: readme, message: `line ${modelView.index}: context surface requires non-empty ${MODEL_VIEW_LABEL}: text` })
surfaceError = true
break
}
if (!tokenEffect.raw.startsWith(`${TOKEN_EFFECT_LABEL}: `) || tokenEffect.raw.slice(`${TOKEN_EFFECT_LABEL}: `.length).trim().length === 0) {
failures.push({ path: readme, message: `line ${tokenEffect.index}: context surface requires non-empty ${TOKEN_EFFECT_LABEL}: text` })
surfaceError = true
break
}
const expectedHeadingLine = previousTokenEffect?.index === undefined ? modelHeading.index + 2 : previousTokenEffect.index + 2
if (heading.index !== expectedHeadingLine || modelView.index !== heading.index + 2 || tokenEffect.index !== modelView.index + 2) {
failures.push({ path: readme, message: `line ${heading.index}: context-surface heading and fields require one blank line between each element` })
surfaceError = true
break
}
surfaceFragments.add(fragment)
surfaces.push({ heading, modelView, tokenEffect })
previousTokenEffect = tokenEffect
}
if (content[0] !== header) {
failures.push({ path: readme, message: 'Model Experience table must be the first nonblank content in the section' })
if (surfaceError) continue
const lastSurface = surfaces.at(-1) as { heading: Line; modelView: Line; tokenEffect: Line }
if (appendixIndex >= 0 && (content[appendixIndex] as Line).index !== lastSurface.tokenEffect.index + 2) {
failures.push({ path: readme, message: `${VERBATIM_HEADING} must follow the final context surface after one blank line` })
continue
}
const tableLines = new Set([header, section[headerIndex + 1], ...rows])
const extra = content.filter(line => !tableLines.has(line))
if (extra.length > 0 && (extra[0]?.raw !== VERBATIM_HEADING || extra.slice(1).some(line => !/^#### \S/.test(line.raw)))) {
const first = extra[0] as Line
failures.push({ path: readme, message: `line ${first.index}: invalid content after Model Experience table: ${first.raw}` })
continue
}
const lastRow = rows.at(-1) as Line
const rawTail = rawLines.slice(lastRow.index, nextH2Line - 1)
const rawTail = rawLines.slice(lastSurface.tokenEffect.index, nextH2Line - 1)
const verbatim = validateVerbatimTail(rawTail)
if (verbatim.error !== undefined) {
failures.push({ path: readme, message: verbatim.error })
continue
}
const tableText = rows.map(row => row.raw).join('\n')
const unlinked = verbatim.titles.find(title => !tableText.includes(`](#${headingFragment(title)})`))
const modelViewText = surfaces.map(surface => surface.modelView.raw).join('\n')
const unlinked = verbatim.titles.find(title => !modelViewText.includes(`](#${headingFragment(title)})`))
if (unlinked !== undefined) {
failures.push({ path: readme, message: `verbatim entry ${JSON.stringify(unlinked)} must be linked from a Model Experience table row` })
failures.push({ path: readme, message: `verbatim entry ${JSON.stringify(unlinked)} must be linked from a context surface's ${MODEL_VIEW_LABEL} field` })
continue
}
verbatimBlockCount += verbatim.blocks
tableCount += 1
contextSurfaceCount += surfaces.length
structuredCount += 1
}
if (failures.length === 0) {
console.log(`verify-package-readme-model-experience: ${packageJsons.length} README(s) checked (${tableCount} tables, ${noneCount} none, ${indirectCount} indirect, ${verbatimBlockCount} verbatim blocks), all conform.`)
console.log(`verify-package-readme-model-experience: ${packageJsons.length} README(s) checked (${structuredCount} structured, ${contextSurfaceCount} context surfaces, ${noneCount} none, ${indirectCount} indirect, ${verbatimBlockCount} verbatim markdown blocks), all conform.`)
process.exit(0)
}