2026-07-12 02:12:36 +08:00
/**
2026-07-14 14:37:16 +08:00
* Doc-sync gate for package README Model Experience sections. It validates
2026-07-19 17:39:50 +08:00
* audited package classifications, model/token/KV-cache fields, package-owned
* text blocks, generated-catalog links, and final-section order. See the
2026-07-19 22:50:49 +08:00
* [Model Experience Agent Note](../.agents/notes/implemented/process/2026-07-12-package-model-experience-contract.md).
2026-07-12 02:12:36 +08:00
*/
import { existsSync , globSync , readFileSync } from 'node:fs'
2026-07-06 02:28:44 +08:00
import { relative , resolve , sep } from 'node:path'
2026-07-14 14:01:35 +08:00
import { markdownHeadingLines , 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-19 18:08:42 +08:00
const MODEL_VIEW_HEADING = '#### What the model sees'
const TOKEN_EFFECT_HEADING = '#### Token effect'
const KV_CACHE_EFFECT_HEADING = '#### KV Cache effect'
const FIELD_HEADINGS = [ MODEL_VIEW_HEADING , TOKEN_EFFECT_HEADING , KV_CACHE_EFFECT_HEADING ] as const
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
/**
2026-07-14 13:31:31 +08:00
* Generic packages whose public contract is model-agnostic. Their READMEs omit
* Model Experience entirely; the reason stays here as reviewable audit evidence
* so an absent section cannot be mistaken for forgotten documentation.
2026-07-14 11:22:53 +08:00
*/
2026-07-14 13:31:31 +08:00
const NO_MODEL_EXPERIENCE_SECTION : Readonly < Record < string , string > > = {
'packages/core/scope' : 'The package is a model-agnostic registration and lifecycle primitive; model-facing consumers own any context selection.' ,
2026-07-14 11:22:53 +08:00
'packages/util/brand' : 'The package is a type-only primitive erased at compile time.' ,
2026-07-14 19:50:25 +08:00
'packages/util/paths' : 'The package only resolves harness-owned host paths; model-facing consumers own any rendered use.' ,
2026-07-14 11:22:53 +08:00
}
2026-07-13 15:47:46 +08:00
/**
2026-07-19 17:39:50 +08:00
* Packages whose Model Experience is simple enough for one gated sentence plus
* a KV-cache field. Every other package must carry canonical context-surface
* blocks. A package moves on or off this list with 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-23 21:56:39 +08:00
'packages/client/hmr' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers no model surface.' } ,
'packages/client/modules' : { kind : 'none' , reason : 'Browser-side module-loading kernel machinery; registers no model surface.' } ,
2026-07-19 21:17:57 +08:00
'packages/client/ui-slots' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers no model surface.' } ,
'packages/client/ui-primitives' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers no model surface.' } ,
'packages/client/web-react' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers no model surface.' } ,
'packages/client/connection' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers no model surface.' } ,
'packages/client/runtime' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers no model surface.' } ,
'packages/client/ui-layout' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers no model surface.' } ,
'packages/client/ui-sidebar' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers no model surface.' } ,
'packages/client/ui-conversation' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers no model surface.' } ,
2026-07-22 23:39:50 +08:00
'packages/client/ui-question' : { kind : 'indirect' , reason : 'The package mounts dsh-tool-ask-user; that tool owns the model-visible schema and answer rendering.' } ,
2026-07-19 21:17:57 +08:00
'packages/client/ui-trajectory' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers no model surface.' } ,
2026-07-25 16:04:48 +08:00
'packages/client/ui-workspace' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers no model surface.' } ,
2026-07-19 21:17:57 +08:00
'packages/client/ui-theme' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers no model surface.' } ,
'packages/client/i18n' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers no model surface.' } ,
'packages/client/web' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers no model surface.' } ,
2026-07-15 15:57:57 +08:00
'packages/examples/agent-spine-demo' : { kind : 'indirect' , reason : 'The bundle only mounts model-facing child plugins.' } ,
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-14 21:25:36 +08:00
'packages/fs/fs-sandbox' : { 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-19 21:17:57 +08:00
'packages/host/apiproxy' : { kind : 'none' , reason : 'The wire contract and fetch carriers move already-composed messages and register no model surface.' } ,
'packages/host/webserver' : { kind : 'none' , reason : 'The HTTP carrier bridges browser and API handler and registers no model surface.' } ,
2026-07-14 00:22:52 +08:00
'packages/llm/llm' : { kind : 'none' , reason : 'The adapter registry forwards already-assembled requests unchanged.' } ,
2026-07-15 14:47:29 +08:00
'packages/llm/token-meter' : { kind : 'indirect' , reason : 'The measurement service leaves model-visible changes to its consumers.' } ,
2026-07-16 12:05:35 +08:00
'packages/lsp/lsp' : { kind : 'indirect' , reason : 'The provider registry delegates model rendering to dsh-tool-lsp.' } ,
'packages/lsp/lsp-local' : { kind : 'indirect' , reason : 'The provider backend delegates model rendering to dsh-tool-lsp.' } ,
2026-07-26 06:59:01 +08:00
'packages/process/process' : { kind : 'indirect' , reason : 'The seam delegates all model rendering to consumer seams such as the bash executor family.' } ,
'packages/process/process-local' : { kind : 'indirect' , reason : 'The manager backend delegates model rendering to consumer seams such as the bash executor family.' } ,
2026-07-13 22:40:19 +08:00
'packages/sandbox/sandbox-local' : { kind : 'indirect' , reason : 'The provider backend delegates model rendering to dsh-bash-sandbox and dsh-tool-bash.' } ,
2026-07-14 21:25:36 +08:00
'packages/sandbox/sandbox-policy' : { kind : 'indirect' , reason : 'The policy service holds the mode dsh-tool-bash and dsh-tool-fs render in their denial markers.' } ,
2026-07-15 18:17:38 +08:00
'packages/sdk/create-sdk' : { kind : 'indirect' , reason : 'The initializer only writes project files; selected runtime plugins provide the generated project model surface.' } ,
'packages/sdk/helper' : { kind : 'none' , reason : 'The project domain edits files and registers no live agent or model surface.' } ,
'packages/sdk/scripts' : { kind : 'indirect' , reason : 'The launcher delegates model context to the loaded project plugin tree.' } ,
2026-07-17 13:16:51 +08:00
'packages/sdk/telemetry' : { kind : 'none' , reason : 'The launcher-side reporter sends developer-cycle telemetry and registers no live agent or model surface.' } ,
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-15 16:17:59 +08:00
'packages/session-query/session-query-sqlite' : { kind : 'none' , reason : 'The search backend returns hits 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.' } ,
2026-07-17 18:21:54 +08:00
'packages/spill/spill' : { kind : 'indirect' , reason : 'The storage seam delegates model rendering to spill consumers.' } ,
'packages/spill/spill-local' : { kind : 'indirect' , reason : 'The storage backend delegates model rendering to spill consumers.' } ,
2026-07-13 22:40:19 +08:00
'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.' } ,
2026-07-16 17:39:53 +08:00
'packages/support/agent-loop-testkit' : { kind : 'none' , reason : 'The test helper mounts services but neither drives nor modifies model requests.' } ,
2026-07-13 15:47:46 +08:00
'packages/support/invariants' : { kind : 'none' , reason : 'The observer validates requests but never rewrites their context.' } ,
2026-07-14 16:24:21 +08:00
'packages/support/loader-smoke' : { kind : 'none' , reason : 'The test harness observes child-process streams without changing live requests.' } ,
2026-07-25 08:20:40 +08:00
'packages/support/llm-mock-server' : { kind : 'none' , reason : 'The test server substitutes provider wire behavior without invoking a real model.' } ,
2026-07-14 00:22:52 +08:00
'packages/support/llm-replay' : { kind : 'none' , reason : 'The keyless adapter invokes no provider model.' } ,
2026-07-14 18:05:46 +08:00
'packages/tasks/tasks' : { kind : 'indirect' , reason : 'Producer and control-surface plugins own all model rendering over the task registry.' } ,
2026-07-26 05:13:39 +08:00
'packages/tasks/tasks-local' : { kind : 'indirect' , reason : 'The registry backend delegates model rendering to producer plugins and dsh-tool-tasks.' } ,
2026-07-15 15:57:57 +08:00
'packages/examples/acp-demo' : { kind : 'indirect' , reason : 'The app bundle delegates request composition to dsh-agent-spine-demo 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-15 15:57:57 +08:00
'packages/examples/jsonrpc-demo' : { 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-17 18:21:54 +08:00
'packages/util/retention' : { kind : 'indirect' , reason : 'Only retention consumers render retained content and omission metadata.' } ,
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
2026-07-19 17:39:50 +08:00
kvCacheEffect : Line
2026-07-14 00:22:52 +08:00
title : string
2026-07-19 18:08:42 +08:00
modelViewVerbatimBlocks : number
2026-07-14 00:22:52 +08:00
verbatimBlocks : number
}
2026-07-19 18:08:42 +08:00
interface ParsedField {
value : Line
verbatimBlocks : number
}
/** Validate H5-plus-markdown literals nested under one Model Experience field. */
function validateNestedVerbatim ( raw : readonly string [ ] , fragments : Set < 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
while ( true ) {
while ( raw [ cursor ] ? . trim ( ) . length === 0 ) cursor += 1
if ( cursor === raw . length ) break
2026-07-19 18:08:42 +08:00
if ( ! /^##### \S/ . test ( raw [ cursor ] ? ? '' ) ) {
return { blocks , error : 'content after a field paragraph must be a titled H5 verbatim block' }
2026-07-13 21:33:23 +08:00
}
2026-07-19 18:08:42 +08:00
const title = ( raw [ cursor ] as string ) . slice ( '##### ' . length )
2026-07-13 21:33:23 +08:00
const fragment = headingFragment ( title )
2026-07-19 18:08:42 +08:00
if ( fragment . length === 0 ) return { blocks , error : 'verbatim H5 title must be non-empty' }
2026-07-14 00:22:52 +08:00
if ( fragments . has ( fragment ) ) {
2026-07-19 18:08:42 +08:00
return { blocks , error : ` verbatim H5 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-19 18:08:42 +08:00
return { blocks , error : 'each nested verbatim H5 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
}
2026-07-19 18:08:42 +08:00
/** GitHub-style fragment for the simple ASCII nested titles allowed by this contract. */
2026-07-13 21:33:23 +08:00
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 [ ] = [ ]
2026-07-06 02:28:44 +08:00
const packageJsons = globSync ( 'packages/*/*/package.json' , { cwd : root } ) . map ( path = > path . split ( sep ) . join ( '/' ) ) . 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 13:31:31 +08:00
let omittedSectionCount = 0
2026-07-14 11:22:53 +08:00
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-19 17:39:50 +08:00
let kvCacheEffectCount = 0
2026-07-13 15:47:46 +08:00
2026-07-14 13:31:31 +08:00
for ( const [ pkg , reason ] of Object . entries ( NO_MODEL_EXPERIENCE_SECTION ) ) {
2026-07-14 11:22:53 +08:00
if ( ! scannedPackages . has ( pkg ) ) {
2026-07-14 13:31:31 +08:00
failures . push ( { path : ` ${ pkg } /README.md ` , message : 'no-section allowlist entry does not name a scanned package' } )
2026-07-14 11:22:53 +08:00
}
if ( reason . trim ( ) . length === 0 ) {
2026-07-14 13:31:31 +08:00
failures . push ( { path : ` ${ pkg } /README.md ` , message : 'no-section allowlist entry must retain its audit justification' } )
2026-07-14 11:22:53 +08:00
}
if ( SENTENCE_MODEL_EXPERIENCE [ pkg ] !== undefined ) {
2026-07-14 13:31:31 +08:00
failures . push ( { path : ` ${ pkg } /README.md ` , message : 'package cannot appear in both Model Experience allowlists' } )
2026-07-14 11:22:53 +08:00
}
}
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 ) ) {
2026-07-14 13:31:31 +08:00
failures . push ( { path : readme , message : 'missing package README' } )
2026-07-12 02:12:36 +08:00
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-14 14:01:35 +08:00
const headings = markdownHeadingLines ( text )
const h2Headings = headings . filter ( heading = > heading . depth === 2 )
const modelExperienceHeadings = headings . filter ( heading = > heading . text
. trim ( ) . replaceAll ( /\s+/g , ' ' ) . toLowerCase ( ) === 'model experience' )
const modelHeadings = modelExperienceHeadings . filter ( heading = > heading . depth === 2 && heading . raw === HEADING )
2026-07-14 13:31:31 +08:00
if ( NO_MODEL_EXPERIENCE_SECTION [ pkg ] !== undefined ) {
2026-07-14 14:01:35 +08:00
if ( modelExperienceHeadings . length !== 0 ) {
for ( const heading of modelExperienceHeadings ) {
failures . push ( { path : readme , message : ` line ${ heading . index } : audited model-agnostic package must omit every Model Experience heading; found ${ JSON . stringify ( heading . raw ) } ` } )
}
2026-07-14 13:31:31 +08:00
} else {
omittedSectionCount += 1
}
continue
}
2026-07-14 14:01:35 +08:00
const nonCanonicalModelHeading = modelExperienceHeadings . find ( heading = > heading . depth !== 2 || heading . raw !== HEADING )
if ( nonCanonicalModelHeading !== undefined ) {
failures . push ( { path : readme , message : ` line ${ nonCanonicalModelHeading . index } : non-canonical Model Experience heading ${ JSON . stringify ( nonCanonicalModelHeading . raw ) } ; use exactly ${ JSON . stringify ( HEADING ) } ` } )
continue
}
const modelHeading = modelHeadings . at ( 0 )
if ( modelHeading === undefined ) {
2026-07-12 02:12:36 +08:00
failures . push ( {
path : readme ,
2026-07-14 14:01:35 +08:00
message : ` missing ${ HEADING } ` ,
2026-07-12 02:12:36 +08:00
} )
continue
}
2026-07-14 14:01:35 +08:00
if ( modelHeadings . length !== 1 ) {
failures . push ( { path : readme , message : ` contains ${ modelHeadings . length } copies of ${ HEADING } ` } )
continue
}
2026-07-13 15:00:47 +08:00
const modelH2Index = h2Headings . indexOf ( modelHeading )
2026-07-14 14:01:35 +08:00
const limitationsH2Index = h2Headings . findIndex ( heading = > heading . depth === 2 && 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-14 14:01:35 +08:00
const modelHeadingAt = lines . findIndex ( line = > line . index === modelHeading . index )
const body = lines . slice ( modelHeadingAt + 1 )
const h2Lines = new Set ( h2Headings . map ( heading = > heading . index ) )
const nextH2 = body . findIndex ( line = > h2Lines . has ( line . index ) )
2026-07-13 15:00:47 +08:00
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 )
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 )
2026-07-19 17:39:50 +08:00
const sentence = content [ 0 ]
2026-07-19 18:08:42 +08:00
const kvCacheHeading = content [ 1 ]
const kvCacheEffect = content [ 2 ]
if ( content . length !== 3 || rawContent . length !== 3 || ! pattern . test ( sentence ? . raw ? ? '' ) ) {
2026-07-13 15:47:46 +08:00
const prefix = sentenceContract . kind === 'none' ? 'None, as ' : 'Indirectly, through '
2026-07-19 18:08:42 +08:00
failures . push ( { path : readme , message : ` must contain exactly one sentence beginning ${ JSON . stringify ( prefix ) } and ending with a period, followed by ${ KV_CACHE_EFFECT_HEADING } and one non-empty paragraph ` } )
2026-07-19 17:39:50 +08:00
continue
}
2026-07-19 18:08:42 +08:00
if ( kvCacheHeading ? . raw !== KV_CACHE_EFFECT_HEADING
|| kvCacheEffect === undefined
|| /^#{1,6} / . test ( kvCacheEffect . raw )
|| kvCacheEffect . raw . trim ( ) . length === 0 ) {
failures . push ( { path : readme , message : ` line ${ kvCacheHeading ? . index ? ? sentence ? . index ? ? modelHeading . index } : short Model Experience form requires exact ${ KV_CACHE_EFFECT_HEADING } and one non-empty paragraph ` } )
2026-07-19 17:39:50 +08:00
continue
}
2026-07-19 18:08:42 +08:00
if ( sentence === undefined
|| sentence . index !== modelHeading . index + 2
|| kvCacheHeading . index !== sentence . index + 2
|| kvCacheEffect . index !== kvCacheHeading . index + 2 ) {
failures . push ( { path : readme , message : 'short Model Experience sentence, KV-cache H4, and paragraph require one blank line between each element' } )
2026-07-13 15:47:46 +08:00
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
2026-07-19 17:39:50 +08:00
kvCacheEffectCount += 1
2026-07-13 15:47:46 +08:00
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 13:31:31 +08:00
failures . push ( { path : readme , message : ` line ${ shortSentence . index } : short Model Experience form requires an audited entry in 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 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-19 18:08:42 +08:00
const fieldStarts = entries
. map ( ( line , index ) = > ( { line , index } ) )
. filter ( entry = > /^#### \S/ . test ( entry . line . raw ) )
if ( fieldStarts . length !== FIELD_HEADINGS . length || fieldStarts [ 0 ] ? . index !== 1 ) {
failures . push ( { path : readme , message : ` line ${ heading . index } : context surface requires exactly three ordered H4 fields: ${ FIELD_HEADINGS . join ( ', ' ) } ` } )
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
2026-07-19 18:08:42 +08:00
|| fieldStarts [ 0 ] . line . index !== heading . index + 2 ) {
failures . push ( { path : readme , message : ` line ${ heading . index } : context-surface heading and first field require one blank line between them ` } )
2026-07-14 00:22:52 +08:00
surfaceError = true
break
}
2026-07-19 18:08:42 +08:00
const parsedFields : ParsedField [ ] = [ ]
const verbatimFragments = new Set < string > ( )
for ( let fieldIndex = 0 ; fieldIndex < FIELD_HEADINGS . length ; fieldIndex += 1 ) {
const fieldStart = fieldStarts [ fieldIndex ] as { line : Line ; index : number }
const expectedHeading = FIELD_HEADINGS [ fieldIndex ] as string
if ( fieldStart . line . raw !== expectedHeading ) {
failures . push ( { path : readme , message : ` line ${ fieldStart . line . index } : expected exact field heading ${ JSON . stringify ( expectedHeading ) } , found ${ JSON . stringify ( fieldStart . line . raw ) } ` } )
surfaceError = true
break
}
const fieldEnd = fieldStarts [ fieldIndex + 1 ] ? . index ? ? entries . length
const fieldEntries = entries . slice ( fieldStart . index , fieldEnd )
const value = fieldEntries [ 1 ]
if ( value === undefined || /^#{1,6} / . test ( value . raw ) || value . raw . trim ( ) . length === 0 ) {
failures . push ( { path : readme , message : ` line ${ fieldStart . line . index } : ${ expectedHeading } requires one non-empty paragraph ` } )
surfaceError = true
break
}
if ( value . index !== fieldStart . line . index + 2 ) {
failures . push ( { path : readme , message : ` line ${ fieldStart . line . index } : ${ expectedHeading } and its paragraph require one blank line between them ` } )
surfaceError = true
break
}
const unexpected = fieldEntries . slice ( 2 ) . find ( line = > ! /^##### \S/ . test ( line . raw ) )
if ( unexpected !== undefined ) {
failures . push ( { path : readme , message : ` line ${ unexpected . index } : content after ${ expectedHeading } paragraph must be a titled H5 plus \` markdown \` fence owned by that field ` } )
surfaceError = true
break
}
const nextHeadingLine = fieldStarts [ fieldIndex + 1 ] ? . line . index
? ? surfaceStarts [ surfaceIndex + 1 ] ? . line . index
? ? nextH2Line
if ( rawLines [ nextHeadingLine - 2 ] ? . trim ( ) . length !== 0 ) {
failures . push ( { path : readme , message : ` line ${ nextHeadingLine } : Model Experience headings require a preceding blank line ` } )
surfaceError = true
break
}
const verbatim = validateNestedVerbatim ( rawLines . slice ( value . index , nextHeadingLine - 1 ) , verbatimFragments )
if ( verbatim . error !== undefined ) {
failures . push ( { path : readme , message : ` line ${ value . index } : ${ verbatim . error } ` } )
surfaceError = true
break
}
if ( fieldEntries . length - 2 !== verbatim . blocks ) {
failures . push ( { path : readme , message : ` line ${ value . index } : every nested H5 must own exactly one \` markdown \` fence ` } )
surfaceError = true
break
}
parsedFields . push ( { value , verbatimBlocks : verbatim.blocks } )
2026-07-14 00:22:52 +08:00
}
2026-07-19 18:08:42 +08:00
if ( surfaceError ) break
const modelViewField = parsedFields [ 0 ] as ParsedField
const tokenEffectField = parsedFields [ 1 ] as ParsedField
const kvCacheEffectField = parsedFields [ 2 ] as ParsedField
const modelView = modelViewField . value
const tokenEffect = tokenEffectField . value
const kvCacheEffect = kvCacheEffectField . value
2026-07-19 17:39:50 +08:00
if ( /\]\(#[^)]+\)/ . test ( modelView . raw ) || /\]\(#[^)]+\)/ . test ( tokenEffect . raw ) || /\]\(#[^)]+\)/ . test ( kvCacheEffect . raw ) ) {
2026-07-19 18:08:42 +08:00
failures . push ( { path : readme , message : ` line ${ heading . index } : Model Experience fields must not link between local subsections; nest the H5 in its owning H4 field ` } )
2026-07-14 00:22:52 +08:00
surfaceError = true
break
}
2026-07-13 22:26:33 +08:00
surfaceFragments . add ( fragment )
2026-07-19 18:08:42 +08:00
surfaces . push ( {
heading ,
modelView ,
tokenEffect ,
kvCacheEffect ,
title ,
modelViewVerbatimBlocks : modelViewField.verbatimBlocks ,
verbatimBlocks : parsedFields.reduce ( ( total , field ) = > total + field . verbatimBlocks , 0 ) ,
} )
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 )
2026-07-19 18:08:42 +08:00
&& surface . modelViewVerbatimBlocks === 0 )
2026-07-14 00:22:52 +08:00
if ( promptWithoutVerbatim !== undefined ) {
2026-07-19 18:08:42 +08:00
failures . push ( { path : readme , message : ` line ${ promptWithoutVerbatim . heading . index } : system-prompt surface must contain a titled H5 plus verbatim \` markdown \` block under ${ MODEL_VIEW_HEADING } ` } )
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-19 17:39:50 +08:00
kvCacheEffectCount += surfaces . 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-19 17:39:50 +08:00
console . log ( ` verify-package-readme-model-experience: ${ packageJsons . length } README(s) checked ( ${ omittedSectionCount } audited omissions, ${ structuredCount } structured, ${ contextSurfaceCount } context surfaces, ${ kvCacheEffectCount } KV-cache fields, ${ systemPromptSurfaceCount } fenced system-prompt surfaces, ${ toolSchemaSurfaceCount } catalog-linked tool-schema surfaces, ${ 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 )