2026-07-12 02:12:36 +08:00
/**
* Doc-sync gate: require every workspace package README to explain its exact
2026-07-13 15:47:46 +08:00
* model-visible context surface and token behavior. Most packages require the
2026-07-14 00:22:52 +08:00
* canonical context-surface blocks with optional nested verbatim H4 blocks.
* Direct system-prompt surfaces must contain exact `markdown` blocks,
* tool-schema surfaces must link generated catalog sections, local subsection
2026-07-14 11:22:53 +08:00
* links are rejected, and audited allowlists use either one concise sentence
* or the exact bare `None.` form.
2026-07-12 02:12:36 +08:00
*
* Run: `tsx scripts/verify-package-readme-model-experience.ts`.
*/
import { existsSync , globSync , readFileSync } from 'node:fs'
import { relative , resolve } from 'node:path'
2026-07-14 00:39:48 +08:00
import { markdownProseLines , type MarkdownProseLine } from './markdown.ts'
2026-07-12 02:12:36 +08:00
const root = resolve ( import . meta . dirname , '..' )
const HEADING = '## Model Experience'
2026-07-12 02:55:26 +08:00
const LIMITATIONS_HEADING = '## Known Limitations and Deferred Work'
2026-07-13 22:26:33 +08:00
const MODEL_VIEW_LABEL = '**What the model sees**'
const TOKEN_EFFECT_LABEL = '**Token effect**'
2026-07-13 15:00:47 +08:00
const H2_HEADING = /^## .+$/
2026-07-12 02:12:36 +08:00
2026-07-13 15:47:46 +08:00
type SentenceKind = 'none' | 'indirect'
interface SentenceContract {
kind : SentenceKind
reason : string
}
2026-07-14 11:22:53 +08:00
/**
* Packages whose lack of model behavior is already self-evident from their
* package contract, so repeating that contract after `None.` adds no value.
* The reason stays here as reviewable audit evidence.
*/
const BARE_NONE_MODEL_EXPERIENCE : Readonly < Record < string , string > > = {
'packages/util/brand' : 'The package is a type-only primitive erased at compile time.' ,
}
2026-07-13 15:47:46 +08:00
/**
* Packages whose Model Experience is simple enough for one gated sentence.
2026-07-13 22:26:33 +08:00
* Every other package must carry canonical context-surface blocks. A package
* moves on or off this list with the change to its context behavior.
2026-07-13 15:47:46 +08:00
*/
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.' } ,
2026-07-13 22:40:19 +08:00
'packages/bash/bash-local' : { kind : 'indirect' , reason : 'The executor backend delegates model rendering to dsh-tool-bash.' } ,
2026-07-13 15:47:46 +08:00
'packages/code-runtime/code-runtime' : { kind : 'indirect' , reason : 'The service interface delegates model rendering to Code Mode in dsh-tools.' } ,
2026-07-13 22:40:19 +08:00
'packages/code-runtime/code-runtime-worker' : { kind : 'indirect' , reason : 'The worker backend delegates model rendering to Code Mode in dsh-tools.' } ,
2026-07-14 00:22:52 +08:00
'packages/core/agent-core' : { kind : 'indirect' , reason : 'The bundle only mounts model-facing child plugins.' } ,
'packages/core/scope' : { kind : 'none' , reason : 'The routing primitive emits no model-bound content.' } ,
2026-07-13 15:47:46 +08:00
'packages/fs/fs' : { kind : 'indirect' , reason : 'The service interface delegates model rendering to dsh-tool-fs.' } ,
2026-07-13 22:40:19 +08:00
'packages/fs/fs-local' : { kind : 'indirect' , reason : 'The provider backend delegates model rendering to dsh-tool-fs.' } ,
2026-07-13 15:47:46 +08:00
'packages/hooks/hook-protocol' : { kind : 'indirect' , reason : 'Only the hook bridge plugins render decoded hook output to a model.' } ,
2026-07-14 00:22:52 +08:00
'packages/llm/llm' : { kind : 'none' , reason : 'The adapter registry forwards already-assembled requests unchanged.' } ,
2026-07-13 22:40:19 +08:00
'packages/sandbox/sandbox' : { kind : 'indirect' , reason : 'Sandbox consumers render enforcement and availability facts.' } ,
'packages/sandbox/sandbox-local' : { kind : 'indirect' , reason : 'The provider backend delegates model rendering to dsh-bash-sandbox and dsh-tool-bash.' } ,
2026-07-14 11:28:08 +08:00
'packages/session-query/session-query' : { kind : 'none' , reason : 'The trusted query service exposes cloned records only to callers and registers no model surface.' } ,
2026-07-13 15:47:46 +08:00
'packages/skill/skill' : { kind : 'indirect' , reason : 'The provider registry delegates model rendering to dsh-tool-skill.' } ,
2026-07-13 22:40:19 +08:00
'packages/skill/skill-local' : { kind : 'indirect' , reason : 'The provider backend delegates model rendering to dsh-tool-skill.' } ,
'packages/subagent/subagent' : { kind : 'indirect' , reason : 'The provider registry delegates parent-model rendering to dsh-tool-subagent.' } ,
2026-07-13 15:47:46 +08:00
'packages/subagent/subagent-subprocess' : { kind : 'indirect' , reason : 'Only process-based subagent backends compose a child model request.' } ,
'packages/support/acp-snapshot' : { kind : 'none' , reason : 'The test harness observes and normalizes transcripts without changing live requests.' } ,
'packages/support/invariants' : { kind : 'none' , reason : 'The observer validates requests but never rewrites their context.' } ,
2026-07-14 00:22:52 +08:00
'packages/support/llm-replay' : { kind : 'none' , reason : 'The keyless adapter invokes no provider model.' } ,
'packages/support/subagent-mock' : { kind : 'indirect' , reason : 'Only dsh-tool-subagent renders its configured test outcome.' } ,
'packages/ui/acp-agent' : { kind : 'indirect' , reason : 'The app bundle delegates request composition to dsh-agent-core and dsh-acp.' } ,
2026-07-13 15:47:46 +08:00
'packages/ui/app-boot' : { kind : 'indirect' , reason : 'Only the loaded plugin tree contributes model context.' } ,
2026-07-14 00:34:25 +08:00
'packages/ui/jsonrpc-agent' : { kind : 'indirect' , reason : 'Only the externally configured plugin tree contributes model context.' } ,
2026-07-14 11:28:08 +08:00
'packages/ui/permission' : { kind : 'indirect' , reason : 'The service writes mechanism events rendered by dsh-user-approval and dsh-tool-bash.' } ,
2026-07-13 22:40:19 +08:00
'packages/ui/user-interaction' : { kind : 'indirect' , reason : 'Model-facing consumers render provider answers and seam errors.' } ,
2026-07-13 15:47:46 +08:00
'packages/util/timeout' : { kind : 'indirect' , reason : 'Only timeout consumers render timeout outcomes.' } ,
2026-07-13 22:40:19 +08:00
'packages/web/web' : { kind : 'indirect' , reason : 'The provider registry delegates model rendering to dsh-tool-web.' } ,
'packages/web/web-fetch-local' : { kind : 'indirect' , reason : 'The provider backend delegates model rendering to dsh-tool-web.' } ,
'packages/web/web-search-exa' : { kind : 'indirect' , reason : 'The provider backend delegates model rendering to dsh-tool-web.' } ,
2026-07-13 15:47:46 +08:00
'packages/workflow/workflow' : { kind : 'indirect' , reason : 'The service delegates parent and child model rendering to its consumer and engine.' } ,
}
2026-07-12 02:12:36 +08:00
interface Failure {
path : string
message : string
}
2026-07-14 00:39:48 +08:00
type Line = MarkdownProseLine
2026-07-13 15:00:47 +08:00
2026-07-14 00:22:52 +08:00
interface ContextSurface {
heading : Line
modelView : Line
tokenEffect : Line
title : string
verbatimBlocks : number
}
/** Validate H4-plus-markdown literals nested after one context surface's fields. */
function validateNestedVerbatim ( raw : readonly string [ ] ) : { blocks : number ; error? : string } {
2026-07-13 21:33:23 +08:00
let cursor = 0
while ( raw [ cursor ] ? . trim ( ) . length === 0 ) cursor += 1
2026-07-14 00:22:52 +08:00
if ( cursor === raw . length ) return { blocks : 0 }
2026-07-13 21:33:23 +08:00
let blocks = 0
2026-07-14 00:22:52 +08:00
const fragments = new Set < string > ( )
2026-07-13 21:33:23 +08:00
while ( true ) {
while ( raw [ cursor ] ? . trim ( ) . length === 0 ) cursor += 1
if ( cursor === raw . length ) break
if ( ! /^#### \S/ . test ( raw [ cursor ] ? ? '' ) ) {
2026-07-14 00:22:52 +08:00
return { blocks , error : 'content after Token effect must be a titled H4 verbatim block' }
2026-07-13 21:33:23 +08:00
}
const title = ( raw [ cursor ] as string ) . slice ( '#### ' . length )
const fragment = headingFragment ( title )
2026-07-14 00:22:52 +08:00
if ( fragment . length === 0 ) return { blocks , error : 'verbatim H4 title must be non-empty' }
if ( fragments . has ( fragment ) ) {
return { blocks , error : ` verbatim H4 title ${ JSON . stringify ( title ) } is duplicated within its context surface ` }
2026-07-13 21:33:23 +08:00
}
2026-07-14 00:22:52 +08:00
fragments . add ( fragment )
2026-07-13 21:33:23 +08:00
cursor += 1
while ( raw [ cursor ] ? . trim ( ) . length === 0 ) cursor += 1
2026-07-13 22:26:33 +08:00
if ( raw [ cursor ] !== '```markdown' ) {
2026-07-14 00:22:52 +08:00
return { blocks , error : 'each nested verbatim H4 requires an exact ```markdown fence' }
2026-07-13 21:33:23 +08:00
}
cursor += 1
const contentStart = cursor
while ( cursor < raw . length && raw [ cursor ] !== '```' ) cursor += 1
2026-07-14 00:22:52 +08:00
if ( cursor === raw . length ) return { blocks , error : 'unterminated nested ```markdown fence' }
if ( cursor === contentStart ) return { blocks , error : 'nested ```markdown fence must not be empty' }
2026-07-13 21:33:23 +08:00
cursor += 1
blocks += 1
}
2026-07-14 00:22:52 +08:00
return { blocks }
2026-07-13 21:33:23 +08:00
}
/** GitHub-style fragment for the simple ASCII H4 titles allowed by this contract. */
function headingFragment ( title : string ) : string {
return title . toLowerCase ( ) . replaceAll ( '`' , '' ) . replaceAll ( /[^a-z0-9 _-]/g , '' ) . trim ( ) . replaceAll ( /\s+/g , '-' )
}
2026-07-14 00:22:52 +08:00
/** A direct stable system-prompt contribution, as named by the README contract. */
function isDirectSystemPromptSurface ( title : string ) : boolean {
return /\bsystem prompt\b/i . test ( title )
}
/** Anchored generated-catalog links in one model-view field. */
function toolCatalogLinkFragments ( text : string ) : string [ ] {
return [ . . . text . matchAll ( /\]\(\.\.\/\.\.\/\.\.\/docs\/tool-catalog\.md#([a-z0-9_-]+)\)/g ) ]
. map ( match = > match [ 1 ] as string )
}
const toolCatalogFragments = new Set < string > ( )
for ( const line of readFileSync ( resolve ( root , 'docs/tool-catalog.md' ) , 'utf8' ) . split ( '\n' ) ) {
const title = /^## (.+)$/ . exec ( line ) ? . [ 1 ]
if ( title !== undefined ) toolCatalogFragments . add ( headingFragment ( title ) )
}
2026-07-12 02:12:36 +08:00
const failures : Failure [ ] = [ ]
const packageJsons = globSync ( 'packages/*/*/package.json' , { cwd : root } ) . sort ( )
2026-07-13 15:47:46 +08:00
const scannedPackages = new Set ( packageJsons . map ( path = > path . slice ( 0 , - '/package.json' . length ) ) )
2026-07-13 22:26:33 +08:00
let structuredCount = 0
let contextSurfaceCount = 0
2026-07-14 11:22:53 +08:00
let bareNoneCount = 0
let explainedNoneCount = 0
2026-07-13 15:47:46 +08:00
let indirectCount = 0
2026-07-13 21:33:23 +08:00
let verbatimBlockCount = 0
2026-07-14 00:22:52 +08:00
let systemPromptSurfaceCount = 0
let toolSchemaSurfaceCount = 0
2026-07-13 15:47:46 +08:00
2026-07-14 11:22:53 +08:00
for ( const [ pkg , reason ] of Object . entries ( BARE_NONE_MODEL_EXPERIENCE ) ) {
if ( ! scannedPackages . has ( pkg ) ) {
failures . push ( { path : ` ${ pkg } /README.md ` , message : 'bare-none allowlist entry does not name a scanned package' } )
}
if ( reason . trim ( ) . length === 0 ) {
failures . push ( { path : ` ${ pkg } /README.md ` , message : 'bare-none allowlist entry must retain its audit justification' } )
}
if ( SENTENCE_MODEL_EXPERIENCE [ pkg ] !== undefined ) {
failures . push ( { path : ` ${ pkg } /README.md ` , message : 'package cannot appear in both Model Experience sentence allowlists' } )
}
}
2026-07-13 15:47:46 +08:00
for ( const [ pkg , contract ] of Object . entries ( SENTENCE_MODEL_EXPERIENCE ) ) {
if ( ! scannedPackages . has ( pkg ) ) {
failures . push ( { path : ` ${ pkg } /README.md ` , message : 'sentence allowlist entry does not name a scanned package' } )
}
if ( contract . reason . trim ( ) . length === 0 ) {
2026-07-13 22:26:33 +08:00
failures . push ( { path : ` ${ pkg } /README.md ` , message : 'sentence allowlist entry must justify why structured context surfaces are unnecessary' } )
2026-07-13 15:47:46 +08:00
}
}
2026-07-12 02:12:36 +08:00
for ( const packageJson of packageJsons ) {
2026-07-13 15:47:46 +08:00
const pkg = packageJson . slice ( 0 , - '/package.json' . length )
2026-07-12 02:12:36 +08:00
const readme = packageJson . replace ( /package\.json$/ , 'README.md' )
const abs = resolve ( root , readme )
if ( ! existsSync ( abs ) ) {
failures . push ( { path : readme , message : ` missing package README; add one with ${ HEADING } ` } )
continue
}
2026-07-13 21:33:23 +08:00
const text = readFileSync ( abs , 'utf8' )
const rawLines = text . split ( '\n' )
2026-07-14 00:39:48 +08:00
const lines = markdownProseLines ( text )
2026-07-13 15:00:47 +08:00
const h2Headings = lines . filter ( line = > H2_HEADING . test ( line . raw ) )
const modelHeadings = h2Headings . filter ( line = > line . raw === HEADING )
if ( modelHeadings . length !== 1 ) {
2026-07-12 02:12:36 +08:00
failures . push ( {
path : readme ,
2026-07-13 15:00:47 +08:00
message : modelHeadings.length === 0 ? ` missing ${ HEADING } ` : ` contains ${ modelHeadings . length } copies of ${ HEADING } ` ,
2026-07-12 02:12:36 +08:00
} )
continue
}
2026-07-13 15:00:47 +08:00
const modelHeading = modelHeadings [ 0 ] as Line
const modelH2Index = h2Headings . indexOf ( modelHeading )
const limitationsH2Index = h2Headings . findIndex ( heading = > heading . raw === LIMITATIONS_HEADING )
2026-07-12 02:55:26 +08:00
if ( limitationsH2Index >= 0 ) {
if ( modelH2Index !== h2Headings . length - 2 || limitationsH2Index !== h2Headings . length - 1 ) {
failures . push ( {
path : readme ,
message : ` ${ HEADING } and ${ LIMITATIONS_HEADING } must be the final two H2 sections, in that order ` ,
} )
continue
}
} else if ( modelH2Index !== h2Headings . length - 1 ) {
failures . push ( { path : readme , message : ` ${ HEADING } must be the final H2 when ${ LIMITATIONS_HEADING } is absent ` } )
continue
}
2026-07-13 15:00:47 +08:00
const body = lines . slice ( lines . indexOf ( modelHeading ) + 1 )
const nextH2 = body . findIndex ( line = > H2_HEADING . test ( line . raw ) )
const section = nextH2 < 0 ? body : body.slice ( 0 , nextH2 )
2026-07-13 21:33:23 +08:00
const nextH2Line = nextH2 < 0 ? rawLines . length + 1 : ( body [ nextH2 ] as Line ) . index
const rawSection = rawLines . slice ( modelHeading . index , nextH2Line - 1 )
2026-07-13 15:47:46 +08:00
const content = section . filter ( line = > line . raw . trim ( ) . length > 0 )
2026-07-14 11:22:53 +08:00
const bareNoneReason = BARE_NONE_MODEL_EXPERIENCE [ pkg ]
if ( bareNoneReason !== undefined ) {
const rawContent = rawSection . filter ( line = > line . trim ( ) . length > 0 )
if ( content . length !== 1 || rawContent . length !== 1 || content [ 0 ] ? . raw !== 'None.' ) {
failures . push ( { path : readme , message : 'must contain exactly the bare sentence `None.`' } )
continue
}
bareNoneCount += 1
continue
}
2026-07-13 15:47:46 +08:00
const sentenceContract = SENTENCE_MODEL_EXPERIENCE [ pkg ]
if ( sentenceContract !== undefined ) {
const pattern = sentenceContract . kind === 'none' ? /^None, as .+\.$/ : /^Indirectly, through .+\.$/
2026-07-13 21:33:23 +08:00
const rawContent = rawSection . filter ( line = > line . trim ( ) . length > 0 )
if ( content . length !== 1 || rawContent . length !== 1 || ! pattern . test ( content [ 0 ] ? . raw ? ? '' ) ) {
2026-07-13 15:47:46 +08:00
const prefix = sentenceContract . kind === 'none' ? 'None, as ' : 'Indirectly, through '
failures . push ( { path : readme , message : ` must contain exactly one sentence beginning ${ JSON . stringify ( prefix ) } and ending with a period ` } )
continue
}
2026-07-14 11:22:53 +08:00
if ( sentenceContract . kind === 'none' ) explainedNoneCount += 1
2026-07-13 15:47:46 +08:00
else indirectCount += 1
continue
}
2026-07-14 11:22:53 +08:00
const shortSentence = content . find ( line = > line . raw === 'None.' || /^None, as |^Indirectly, through / . test ( line . raw ) )
2026-07-13 15:47:46 +08:00
if ( shortSentence !== undefined ) {
2026-07-14 11:22:53 +08:00
failures . push ( { path : readme , message : ` line ${ shortSentence . index } : short Model Experience form requires an audited entry in BARE_NONE_MODEL_EXPERIENCE or SENTENCE_MODEL_EXPERIENCE ` } )
2026-07-13 15:47:46 +08:00
continue
}
2026-07-14 00:22:52 +08:00
const surfaceStarts = content
. map ( ( line , index ) = > ( { line , index } ) )
. filter ( entry = > /^### \S/ . test ( entry . line . raw ) )
if ( surfaceStarts . length === 0 || surfaceStarts [ 0 ] ? . index !== 0 ) {
2026-07-13 22:26:33 +08:00
failures . push ( { path : readme , message : 'must contain one or more complete context-surface blocks' } )
2026-07-12 02:12:36 +08:00
continue
}
2026-07-14 00:22:52 +08:00
const surfaces : ContextSurface [ ] = [ ]
2026-07-13 22:26:33 +08:00
const surfaceFragments = new Set < string > ( )
let surfaceError = false
2026-07-14 00:22:52 +08:00
for ( let surfaceIndex = 0 ; surfaceIndex < surfaceStarts . length ; surfaceIndex += 1 ) {
const start = surfaceStarts [ surfaceIndex ] as { line : Line ; index : number }
const end = surfaceStarts [ surfaceIndex + 1 ] ? . index ? ? content . length
const entries = content . slice ( start . index , end )
const heading = entries [ 0 ] as Line
const modelView = entries [ 1 ]
const tokenEffect = entries [ 2 ]
const title = heading . raw . slice ( '### ' . length )
const fragment = headingFragment ( title )
2026-07-13 22:26:33 +08:00
if ( fragment . length === 0 ) {
failures . push ( { path : readme , message : ` line ${ heading . index } : each context surface requires a non-empty H3 heading ` } )
surfaceError = true
break
2026-07-12 02:12:36 +08:00
}
2026-07-13 22:26:33 +08:00
if ( surfaceFragments . has ( fragment ) ) {
failures . push ( { path : readme , message : ` line ${ heading . index } : duplicate context-surface link fragment ${ JSON . stringify ( fragment ) } ` } )
surfaceError = true
break
}
2026-07-14 00:22:52 +08:00
if ( modelView === undefined || ! modelView . raw . startsWith ( ` ${ MODEL_VIEW_LABEL } : ` ) || modelView . raw . slice ( ` ${ MODEL_VIEW_LABEL } : ` . length ) . trim ( ) . length === 0 ) {
failures . push ( { path : readme , message : ` line ${ modelView ? . index ? ? heading . index } : context surface requires non-empty ${ MODEL_VIEW_LABEL } : text ` } )
2026-07-13 22:26:33 +08:00
surfaceError = true
break
}
2026-07-14 00:22:52 +08:00
if ( tokenEffect === undefined || ! tokenEffect . raw . startsWith ( ` ${ TOKEN_EFFECT_LABEL } : ` ) || tokenEffect . raw . slice ( ` ${ TOKEN_EFFECT_LABEL } : ` . length ) . trim ( ) . length === 0 ) {
failures . push ( { path : readme , message : ` line ${ tokenEffect ? . index ? ? heading . index } : context surface requires non-empty ${ TOKEN_EFFECT_LABEL } : text ` } )
2026-07-13 22:26:33 +08:00
surfaceError = true
break
}
2026-07-14 00:22:52 +08:00
if ( ( surfaceIndex === 0 && heading . index !== modelHeading . index + 2 )
|| rawLines [ heading . index - 2 ] ? . trim ( ) . length !== 0
|| modelView . index !== heading . index + 2
|| tokenEffect . index !== modelView . index + 2 ) {
2026-07-13 22:26:33 +08:00
failures . push ( { path : readme , message : ` line ${ heading . index } : context-surface heading and fields require one blank line between each element ` } )
surfaceError = true
break
}
2026-07-14 00:22:52 +08:00
const unexpected = entries . slice ( 3 ) . find ( line = > ! /^#### \S/ . test ( line . raw ) )
if ( unexpected !== undefined ) {
failures . push ( { path : readme , message : ` line ${ unexpected . index } : content after ${ TOKEN_EFFECT_LABEL } must be a titled H4 plus \` markdown \` fence inside this context surface ` } )
surfaceError = true
break
}
const nextHeadingLine = surfaceStarts [ surfaceIndex + 1 ] ? . line . index ? ? nextH2Line
const verbatim = validateNestedVerbatim ( rawLines . slice ( tokenEffect . index , nextHeadingLine - 1 ) )
if ( verbatim . error !== undefined ) {
failures . push ( { path : readme , message : ` line ${ tokenEffect . index } : ${ verbatim . error } ` } )
surfaceError = true
break
}
if ( entries . length - 3 !== verbatim . blocks ) {
failures . push ( { path : readme , message : ` line ${ tokenEffect . index } : every nested H4 must own exactly one \` markdown \` fence ` } )
surfaceError = true
break
}
if ( /\]\(#[^)]+\)/ . test ( modelView . raw ) || /\]\(#[^)]+\)/ . test ( tokenEffect . raw ) ) {
failures . push ( { path : readme , message : ` line ${ heading . index } : Model Experience fields must not link between local subsections; nest the H4 in its owning H3 ` } )
surfaceError = true
break
}
2026-07-13 22:26:33 +08:00
surfaceFragments . add ( fragment )
2026-07-14 00:22:52 +08:00
surfaces . push ( { heading , modelView , tokenEffect , title , verbatimBlocks : verbatim.blocks } )
2026-07-12 02:12:36 +08:00
}
2026-07-13 22:26:33 +08:00
if ( surfaceError ) continue
2026-07-14 00:22:52 +08:00
const promptWithoutVerbatim = surfaces . find ( surface = > isDirectSystemPromptSurface ( surface . title )
&& surface . verbatimBlocks === 0 )
if ( promptWithoutVerbatim !== undefined ) {
failures . push ( { path : readme , message : ` line ${ promptWithoutVerbatim . heading . index } : system-prompt surface must contain a titled H4 plus verbatim \` markdown \` block ` } )
2026-07-13 21:33:23 +08:00
continue
}
2026-07-14 00:22:52 +08:00
const hasConcreteLiteral = surfaces . some ( surface = > surface . verbatimBlocks > 0
|| surface . modelView . raw . includes ( '`' )
|| surface . tokenEffect . raw . includes ( '`' )
|| toolCatalogLinkFragments ( surface . modelView . raw ) . length > 0 )
if ( ! hasConcreteLiteral ) {
failures . push ( { path : readme , message : 'structured Model Experience must ground at least one surface with inline code, a nested `markdown` block, or an anchored tool-catalog link' } )
2026-07-13 21:33:23 +08:00
continue
}
2026-07-14 00:22:52 +08:00
let catalogError = false
for ( const surface of surfaces ) {
if ( ! /\bschemas?\b/i . test ( surface . title ) ) continue
const fragments = toolCatalogLinkFragments ( surface . modelView . raw )
if ( fragments . length === 0 ) {
failures . push ( { path : readme , message : ` line ${ surface . heading . index } : tool-schema surface must link an anchored section of ../../../docs/tool-catalog.md ` } )
catalogError = true
break
}
const invalid = fragments . find ( fragment = > ! toolCatalogFragments . has ( fragment ) )
if ( invalid !== undefined ) {
failures . push ( { path : readme , message : ` line ${ surface . modelView . index } : tool-catalog link fragment ${ JSON . stringify ( invalid ) } does not name an H2 section ` } )
catalogError = true
break
}
2026-07-13 15:47:46 +08:00
}
2026-07-14 00:22:52 +08:00
if ( catalogError ) continue
verbatimBlockCount += surfaces . reduce ( ( total , surface ) = > total + surface . verbatimBlocks , 0 )
2026-07-13 22:26:33 +08:00
contextSurfaceCount += surfaces . length
2026-07-14 00:22:52 +08:00
systemPromptSurfaceCount += surfaces . filter ( surface = > isDirectSystemPromptSurface ( surface . title ) ) . length
toolSchemaSurfaceCount += surfaces . filter ( surface = > /\bschemas?\b/i . test ( surface . title ) ) . length
2026-07-13 22:26:33 +08:00
structuredCount += 1
2026-07-12 02:12:36 +08:00
}
if ( failures . length === 0 ) {
2026-07-14 11:22:53 +08:00
console . log ( ` verify-package-readme-model-experience: ${ packageJsons . length } README(s) checked ( ${ structuredCount } structured, ${ contextSurfaceCount } context surfaces, ${ systemPromptSurfaceCount } fenced system-prompt surfaces, ${ toolSchemaSurfaceCount } catalog-linked tool-schema surfaces, ${ bareNoneCount } bare none, ${ explainedNoneCount } explained none, ${ indirectCount } indirect, ${ verbatimBlockCount } verbatim markdown blocks), all conform. ` )
2026-07-12 02:12:36 +08:00
process . exit ( 0 )
}
console . error ( 'verify-package-readme-model-experience failed:' )
for ( const failure of failures ) {
console . error ( ` ${ relative ( root , resolve ( root , failure . path ) ) } : ${ failure . message } ` )
}
process . exit ( 1 )