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-08-13 00:36:22 +08:00
'packages/util/home-paths' : 'The package only resolves harness-owned host paths; model-facing consumers own any rendered use.' ,
'packages/util/launch-environment' : 'The package only resolves host environment values; 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
2026-07-24 19:54:25 +08:00
* a KV - cache field . Every other package must carry canonical model - context
2026-07-19 17:39:50 +08:00
* 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 > > = {
2026-07-23 15:20:47 +08:00
'packages/attachment/attachment' : { kind : 'indirect' , reason : 'The storage seam delegates model request rendering to provider adapters.' } ,
'packages/attachment/attachment-local' : { kind : 'indirect' , reason : 'The local backend delegates model request rendering to provider adapters.' } ,
2026-08-13 00:36:22 +08:00
'packages/shell/shell' : { kind : 'indirect' , reason : 'The service interface delegates all model rendering to dsh-tool-bash.' } ,
'packages/shell/shell-env' : { kind : 'indirect' , reason : 'The env service exposes managed DSH_* facts through the shell tools (dsh-tool-bash/dsh-tool-pwsh); it registers no prompt or schema of its own.' } ,
'packages/shell/bash-local' : { kind : 'indirect' , reason : 'The executor backend delegates model rendering to dsh-tool-bash.' } ,
'packages/shell/pwsh-local' : { kind : 'indirect' , reason : 'The executor backend delegates model rendering to dsh-tool-pwsh.' } ,
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-08-13 00:36:22 +08:00
'packages/core/agent-tool-presentation' : { kind : 'indirect' , reason : 'The row only selects between the two projections dsh-tools owns; it registers no prompt, schema, or result of its own.' } ,
'packages/code-runtime/code-runtime-worker-thread' : { kind : 'indirect' , reason : 'The worker backend delegates model rendering to Code Mode in dsh-tools.' } ,
feat(code-runtime-python): add the fd-3 frame protocol
Introduce @deepseek-ai/dsh-code-runtime-python with the versionless
JSON-lines protocol between the Node host and the CPython subprocess:
the host-side hostile-frame codec (validateChildFrame, encodeJsonPlain,
checkDoneValue, hasUnsafeIntegerToken, hasNonLosslessNumber,
logTruncationMarker) and the Python-side wire-vocabulary mirror
(py/protocol.py).
This is the protocol layer of the code-runtime-python stack, split from
#436 and based on the multi-language seam extension. The PythonCodeRuntime
implementation and its Python JSON codec land in the backend-core PR on
top of this branch.
Ship the minimal buildable package skeleton (package.json, tsconfig,
tsdown, barrel index, invariant companion, bilingual README) because the
workspace-constraint, coverage, and invariant-topology gates require the
package to exist and build the moment its directory does; the backend-core
PR extends those files rather than creating them.
Align py/protocol.py with src/protocol.ts (the round-12 review of #436
found LogMessage.truncated, DoneMessage.error.kind, and Namespace.errorClass
stale) and guard the two runtime-executed surfaces (PROTOCOL_FD and the log
truncation marker) with a real-python3 cross-language mirror e2e test.
2026-07-31 18:51:03 +08:00
'packages/code-runtime/code-runtime-python' : { kind : 'indirect' , reason : 'The CPython subprocess backend delegates model rendering to Code Mode in dsh-tools.' } ,
2026-08-04 00:25:19 +08:00
'packages/client/ui-agent-preset' : { kind : 'indirect' , reason : 'Browser-side settings row; the preset it selects owns every model-facing effect.' } ,
2026-08-21 01:12:49 +08:00
'packages/util/crypto' : { kind : 'indirect' , reason : 'Pure identifier minting; the ids consumers mint with it never enter prompts as semantic content.' } ,
2026-08-09 12:13:58 +08:00
'packages/core/agent-default-model' : { kind : 'indirect' , reason : 'The service supplies a ModelSelection; request assembly and adapters own the model-visible request.' } ,
2026-08-22 20:03:23 +08:00
'packages/llm/deepseek-llm-api-extensions' : { kind : 'indirect' , reason : 'The registry contributes model-hidden provider fields; dsh-llm-deepseek owns their wire placement.' } ,
2026-08-03 20:30:33 +08:00
'packages/preset/agent-presets' : { kind : 'indirect' , reason : 'The mount installs a preset\'s own plugins, which own every model-facing registration it makes visible.' } ,
2026-07-28 23:47:57 +08:00
'packages/typert/registry' : { kind : 'none' , reason : 'Runtime type registry; consumers (cordis_inspect, wire faces, gates) own any model-visible projection of registry contents.' } ,
'packages/typert/loader' : { kind : 'none' , reason : 'Loader integration only registers generated artifacts; consumers own any model-visible projection.' } ,
2026-07-28 10:05:30 +08:00
'packages/e2b/e2b' : { kind : 'none' , reason : 'The shared remote-runtime owner registers no model context; provider adapters and consumers own rendered effects.' } ,
2026-07-24 19:54:25 +08:00
'packages/client/hmr' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers nothing model-facing.' } ,
'packages/client/modules' : { kind : 'none' , reason : 'Browser-side module-loading kernel machinery; registers nothing model-facing.' } ,
2026-08-13 00:36:22 +08:00
'packages/test-support/client-runtime' : { kind : 'none' , reason : 'Browser-side test infrastructure (jsdom bench); registers nothing model-facing.' } ,
2026-08-20 19:19:07 +08:00
'packages/experimental/webworker-runtime' : { kind : 'none' , reason : 'Browser-side host runtime and Node-compatibility layer; the plugins it boots own every model-facing registration.' } ,
'packages/experimental/webworker-packer' : { kind : 'none' , reason : 'Build-time image writer; its output reaches a model only through the tree the worker then boots.' } ,
2026-07-24 19:54:25 +08:00
'packages/client/ui-slots' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers nothing model-facing.' } ,
2026-08-11 17:01:29 +08:00
'packages/client/ui-attachment' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers nothing model-facing.' } ,
2026-07-24 19:54:25 +08:00
'packages/client/ui-primitives' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers nothing model-facing.' } ,
2026-08-17 14:21:34 +08:00
'packages/client/ui-renderer' : { kind : 'none' , reason : 'Browser-side render assembly; registers nothing model-facing.' } ,
2026-07-24 19:54:25 +08:00
'packages/client/connection' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers nothing model-facing.' } ,
2026-08-22 21:13:53 +08:00
'packages/api/remotes' : { kind : 'none' , reason : 'The Remote BFF selects business methods and forwarded events; selected services own any model-visible effect.' } ,
2026-07-24 19:54:25 +08:00
'packages/client/runtime' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers nothing model-facing.' } ,
'packages/client/ui-layout' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers nothing model-facing.' } ,
'packages/client/ui-sidebar' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers nothing model-facing.' } ,
2026-08-19 12:53:57 +08:00
'packages/client/ui-brand-official' : { kind : 'none' , reason : 'Browser-side presentation occupants; registers nothing model-facing.' } ,
2026-07-24 19:54:25 +08:00
'packages/client/ui-conversation' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers nothing model-facing.' } ,
2026-08-13 00:36:22 +08:00
'packages/client/ui-message-feedback' : { kind : 'none' , reason : 'Browser-side controls over the message-feedback sidecar; ratings and notes never enter the Session log, model context, or telemetry.' } ,
2026-08-08 15:33:51 +08:00
'packages/client/ui-tool' : { kind : 'none' , reason : 'Browser-side Tool presentation layer; renders logged calls without changing model context.' } ,
2026-08-13 00:36:22 +08:00
'packages/client/ui-jobs' : { kind : 'none' , reason : 'Browser-side read-only projection of ctx.jobs records; dsh-tool-jobs owns the model-facing behavior.' } ,
2026-08-10 18:37:30 +08:00
'packages/client/ui-workflow-run' : { kind : 'none' , reason : 'Browser-side UI plugin layer; renders durable workflow records without changing model context.' } ,
2026-08-13 00:36:22 +08:00
'packages/client/ui-input-trigger' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers nothing model-facing.' } ,
2026-08-14 16:18:40 +08:00
'packages/client/ui-reference' : { kind : 'indirect' , reason : 'Browser-side reference selection delegates file guidance and session snapshot preparation to Host-owned providers.' } ,
2026-08-13 00:36:22 +08:00
'packages/client/ui-commands' : { kind : 'indirect' , reason : 'The dispatch paths trigger the host command.execute RPC; each command handler\'s host package owns any model-visible effect.' } ,
'packages/client/ui-model-selection' : { kind : 'indirect' , reason : 'Selection routes session.selectModel; the Host snapshots the selection at the next prompt-assembly boundary and owns the model-visible effect.' } ,
2026-07-28 23:34:19 +08:00
'packages/client/ui-goal' : { kind : 'indirect' , reason : 'The strip verbs route goal.* mutations; the host GoalService owns the model-visible goal/change context message.' } ,
2026-08-12 23:51:31 +08:00
'packages/extensions/ui-cordis' : { kind : 'indirect' , reason : 'The definition card drives the host dynamic run/stop verbs that the model\'s cordis_run/cordis_stop tools also reach; the runner owns any model-visible effect.' } ,
2026-08-13 00:36:22 +08:00
'packages/client/ui-permission-presets' : { kind : 'indirect' , reason : 'The picker submits the host /permission command; the knob events it appends own the model-visible effect through the sandbox/approval consumers.' } ,
'packages/client/ui-settings-plugins' : { kind : 'none' , reason : 'Browser-side settings surface; registers no model surface.' } ,
2026-07-28 23:39:50 +08:00
'packages/client/ui-plan' : { kind : 'indirect' , reason : 'The chip dispatches /plan off; dsh-plan-mode owns the model-visible policy, exit tool, and logged state.' } ,
2026-08-13 00:36:22 +08:00
'packages/client/ui-user-questions' : { kind : 'indirect' , reason : 'The package mounts dsh-tool-ask-user; that tool owns the model-visible schema and answer rendering.' } ,
2026-07-24 19:54:25 +08:00
'packages/client/ui-trajectory' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers nothing model-facing.' } ,
'packages/client/ui-workspace' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers nothing model-facing.' } ,
2026-08-13 00:36:22 +08:00
'packages/client/ui-directory-picker-browse' : { kind : 'none' , reason : 'Browser-side directory-browsing surface; registers nothing model-facing.' } ,
2026-08-12 10:43:23 +08:00
'packages/client/ui-directory-picker-native' : { kind : 'none' , reason : 'Browser-side surface driving the host OS chooser; registers nothing model-facing.' } ,
2026-07-24 19:54:25 +08:00
'packages/client/ui-theme' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers nothing model-facing.' } ,
'packages/client/ui-settings' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers nothing model-facing.' } ,
'packages/client/ui-settings-general' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers nothing model-facing.' } ,
2026-08-13 00:36:22 +08:00
'packages/client/ui-settings-models' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers nothing model-facing.' } ,
'packages/client/ui-settings-plugin-inventory' : { kind : 'none' , reason : 'Browser-side inventory projection; registers nothing model-facing.' } ,
2026-07-24 19:54:25 +08:00
'packages/client/locale' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers nothing model-facing.' } ,
'packages/client/web' : { kind : 'none' , reason : 'Browser-side UI plugin layer; registers nothing model-facing.' } ,
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-08-14 16:18:40 +08:00
'packages/context/file-reference' : { kind : 'indirect' , reason : 'The discovery seam and grammar delegate model guidance to the composed provider.' } ,
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-28 14:52:37 +08:00
'packages/e2b/fs-e2b' : { kind : 'indirect' , reason : 'The provider backend 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-24 19:54:25 +08:00
'packages/host/apiproxy' : { kind : 'none' , reason : 'The wire contract and fetch carriers move already-composed messages and register nothing model-facing.' } ,
'packages/host/directory-picker' : { kind : 'none' , reason : 'The GUI-host picking seam registers nothing model-facing.' } ,
'packages/host/directory-picker-auto' : { kind : 'none' , reason : 'The GUI-host picking chooser only mounts a backend row; it registers nothing model-facing.' } ,
'packages/host/directory-picker-browse' : { kind : 'none' , reason : 'The GUI-host picking backend registers nothing model-facing.' } ,
'packages/host/directory-picker-native' : { kind : 'none' , reason : 'The GUI-host picking backend registers nothing model-facing.' } ,
'packages/host/webserver' : { kind : 'none' , reason : 'The HTTP carrier bridges browser and API handler and registers nothing model-facing.' } ,
2026-08-22 23:44:56 +08:00
'packages/webhook/webhook-github' : { kind : 'indirect' , reason : 'The adapter delegates model-visible text to matching rules and dsh-webhook.' } ,
2026-07-24 19:54:25 +08:00
'packages/host/frontend-static' : { kind : 'none' , reason : 'The SPA dist server answers browser asset requests and registers nothing model-facing.' } ,
2026-08-11 06:46:41 -07:00
'packages/host/plugin-inventory' : { kind : 'none' , reason : 'Host-side read-only Loader projection; registers nothing model-facing.' } ,
2026-07-24 19:54:25 +08:00
'packages/bundle/base' : { kind : 'indirect' , reason : 'The bundle is a patch-list carrier; each inserted row\'s package owns its model-facing behavior.' } ,
2026-08-09 12:13:58 +08:00
'packages/bundle/headless' : { kind : 'none' , reason : 'The one-shot runner submits the task as an ordinary user message; prompts and tools belong to the composed base and headless bundles.' } ,
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.' } ,
feat(lsp): LSP capability seam, generic stdio provider, and lsp tool
Implements the LSP capability seam RFC as three packages: dsh-lsp (the
ctx.lsp interface — provider registry by branded id + exclusive extension
mapping, per-query order-independent selection, closed request/result
vocabulary, LspError taxonomy), dsh-lsp-local (a generic stdio language-server
provider — Content-Length JSON-RPC framing, per-(provider, workspace) process
single-flight, transient didOpen/query/didClose, an abortable per-instance
queue, UTF-16 negotiation, host-namespace source reads outside ctx.fs, and
bounded shutdown/kill teardown), and dsh-tool-lsp (the model-facing lsp tool —
four operations, one-based UTF-16 cursor conversion, workspace-grouped location
rendering, hover capping, a required session workspace, and a timeout budget).
Why: an agent had text search and file reads but no way to identify a program
symbol — follow an alias, connect an interface to implementations, or read an
inferred type — before changing code. Splitting model contract, seam, and local
subprocess behavior keeps the four semantic queries stable across future remote
or sandbox-native providers without leaking a JSON-RPC escape hatch.
2026-07-16 12:05:35 +08:00
'packages/lsp/lsp' : { kind : 'indirect' , reason : 'The provider registry delegates model rendering to dsh-tool-lsp.' } ,
2026-08-13 00:36:22 +08:00
'packages/lsp/lsp-stdio' : { kind : 'indirect' , reason : 'The provider backend delegates model rendering to dsh-tool-lsp.' } ,
refactor(subprocess): rename the process seam to subprocess and address review
Review feedback (tianyicui): 'process' is a poor service name. The family is
now packages/subprocess/ — @deepseek-ai/dsh-subprocess (ctx.subprocess,
abstract SubprocessService, Subprocess* vocabulary) and
@deepseek-ai/dsh-subprocess-local (LocalSubprocessService) — renamed
throughout code, compositions, docs (en+zh, pairs re-recorded), catalogs,
and gates. 'subprocess' is the precise term for managed OS children (the
Python-stdlib sense), avoids colliding with Node's global process object,
and reads as one system beside dsh-subagent-subprocess.
ds-review-bot findings addressed:
- kill() on a settled handle is now a no-op (no signal to a possibly-reused
pgid, no referenced grace timer delaying exit); pinned by a spy test.
- The moved DshEnvironmentKey/DshEnvironment/CollectedOutput types get
drift-checked type-equiv blocks on the new subprocess.md page, restoring
their manifest registration.
- subprocess.md is registered in the core.md sub-page index (en+zh).
2026-07-26 12:43:14 +08:00
'packages/subprocess/subprocess' : { kind : 'indirect' , reason : 'The seam delegates all model rendering to consumer seams such as the bash executor family.' } ,
2026-07-28 14:52:37 +08:00
'packages/e2b/subprocess-e2b' : { kind : 'indirect' , reason : 'The remote spawn backend delegates model rendering to consumer seams such as the bash executor family.' } ,
refactor(subprocess): rename the process seam to subprocess and address review
Review feedback (tianyicui): 'process' is a poor service name. The family is
now packages/subprocess/ — @deepseek-ai/dsh-subprocess (ctx.subprocess,
abstract SubprocessService, Subprocess* vocabulary) and
@deepseek-ai/dsh-subprocess-local (LocalSubprocessService) — renamed
throughout code, compositions, docs (en+zh, pairs re-recorded), catalogs,
and gates. 'subprocess' is the precise term for managed OS children (the
Python-stdlib sense), avoids colliding with Node's global process object,
and reads as one system beside dsh-subagent-subprocess.
ds-review-bot findings addressed:
- kill() on a settled handle is now a no-op (no signal to a possibly-reused
pgid, no referenced grace timer delaying exit); pinned by a spy test.
- The moved DshEnvironmentKey/DshEnvironment/CollectedOutput types get
drift-checked type-equiv blocks on the new subprocess.md page, restoring
their manifest registration.
- subprocess.md is registered in the core.md sub-page index (en+zh).
2026-07-26 12:43:14 +08:00
'packages/subprocess/subprocess-local' : { kind : 'indirect' , reason : 'The spawn 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-08-13 00:36:22 +08:00
'packages/sandbox/sandbox-windows-acl' : { kind : 'indirect' , reason : 'The provider backend delegates model rendering to the shell/pwsh sandbox executors and their tools.' } ,
2026-08-11 15:56:06 +08:00
'packages/sdk/client' : { kind : 'none' , reason : 'Client-process library; model-facing behavior lives in the spawned runtime\'s composed plugins.' } ,
'packages/sdk/protocol' : { kind : 'none' , reason : 'Client-facing wire library; the runtime plugins behind the serving entry own model-facing behavior.' } ,
2026-07-24 19:54:25 +08:00
'packages/session/session-projection' : { kind : 'none' , reason : 'The projection registry serves client-facing read models of already-logged session state and registers nothing model-facing.' } ,
'packages/session/session-projection-cache' : { kind : 'none' , reason : 'The persisted cache accelerates host-side cold reads of projection state and registers nothing model-facing.' } ,
2026-08-12 20:26:47 +08:00
'packages/session/session-stats' : { kind : 'none' , reason : 'The sessionStats unit folds already-logged step boundaries into a client-facing read model and registers nothing model-facing.' } ,
2026-07-24 19:54:25 +08:00
'packages/session-query/session-query' : { kind : 'none' , reason : 'The trusted query service exposes cloned records only to callers and registers nothing model-facing.' } ,
'packages/session-query/session-query-sqlite' : { kind : 'none' , reason : 'The search backend returns hits only to callers and registers nothing model-facing.' } ,
'packages/settings/settings' : { kind : 'indirect' , reason : 'The seam stores and resolves user settings; consumer plugins own any model-facing content fed by a value.' } ,
2026-08-13 00:36:22 +08:00
'packages/settings/settings-file' : { kind : 'indirect' , reason : 'The file provider stores and publishes namespace sections; consumers of ctx.settings own any model-facing behavior.' } ,
2026-07-24 19:54:25 +08:00
'packages/credentials/credentials' : { kind : 'indirect' , reason : 'The seam resolves credential references; the consuming adapter owns every model-facing use a value authorizes.' } ,
'packages/credentials/credentials-local' : { kind : 'indirect' , reason : 'The file/environment provider stores credential values; consumers of ctx.credentials own any model-facing behavior.' } ,
2026-08-13 15:33:29 +08:00
'packages/credentials/authorization' : { kind : 'none' , reason : 'A configuration-time conversation with a human; no flow, notice, or prompt reaches a model request.' } ,
2026-07-24 19:54:25 +08:00
'packages/util/atomic-write' : { kind : 'none' , reason : 'Pure filesystem write primitive; registers nothing model-facing.' } ,
'packages/session/session-telemetry' : { kind : 'none' , reason : 'The seam observes the session stream and hands redacted copies outward; it registers nothing model-facing.' } ,
'packages/session/session-telemetry-otel' : { kind : 'none' , reason : 'The backend forwards seam records into the OTel SDK pipeline and registers nothing model-facing.' } ,
2026-08-13 00:36:22 +08:00
'packages/identity/anonymous-user-id' : { kind : 'none' , reason : 'The shared identifier reaches DeepSeek only as model-hidden HTTP metadata; it registers nothing model-facing.' } ,
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-08-05 21:50:44 +08:00
'packages/skill/skill-badge' : { kind : 'indirect' , reason : 'The bundled provider delegates model rendering to dsh-tool-skill.' } ,
2026-08-13 00:36:22 +08:00
'packages/skill/skill-filesystem' : { 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-08-13 00:36:22 +08:00
'packages/test-support/acp-snapshot' : { kind : 'none' , reason : 'The test harness observes and normalizes transcripts without changing live requests.' } ,
'packages/test-support/agent-loop-testkit' : { kind : 'none' , reason : 'The test helper mounts services but neither drives nor modifies model requests.' } ,
'packages/runtime-diagnostics/invariants' : { kind : 'none' , reason : 'The observer validates requests but never rewrites their context.' } ,
'packages/test-support/loader-smoke' : { kind : 'none' , reason : 'The test harness submits an ordinary user task but delegates prompt and tool composition to the loaded tree.' } ,
'packages/test-support/llm-mock-server' : { kind : 'none' , reason : 'The test server substitutes provider wire behavior without invoking a real model.' } ,
'packages/test-support/llm-replay' : { kind : 'none' , reason : 'The keyless adapter invokes no provider model.' } ,
2026-08-07 15:48:29 +08:00
'packages/api/gateway' : { kind : 'none' , reason : 'Remote dispatch infrastructure; invoked business methods own any model-visible effect.' } ,
2026-08-22 21:13:53 +08:00
'packages/api/session-controller' : { kind : 'none' , reason : 'Session API and transport owner; invoked Agent commands own any model-visible effect.' } ,
2026-08-23 06:14:32 +08:00
'packages/api/workspace-controller' : { kind : 'none' , reason : 'Workspace API and state projection owner; it registers no prompt, tool, or session event.' } ,
2026-08-13 00:36:22 +08:00
'packages/typert/protocol' : { kind : 'none' , reason : 'Compiler-independent Remote protocol declarations; registers nothing model-facing.' } ,
2026-07-28 23:47:57 +08:00
'packages/typert/generator' : { kind : 'none' , reason : 'The build-time generator runs outside any agent runtime and touches no model request.' } ,
2026-08-13 00:36:22 +08:00
'packages/jobs/jobs' : { kind : 'indirect' , reason : 'Producer and controller plugins own all model rendering over the job registry.' } ,
'packages/jobs/jobs-local' : { kind : 'indirect' , reason : 'The registry backend delegates model rendering to producer plugins and dsh-tool-jobs.' } ,
refactor(packages): dissolve ui/ and rename sdk/ to scaffold/
git mv per the regrouping RFC: the five human-collaboration seams and
tui join packages/interaction/, app-boot becomes packages/boot/, and
jsonrpc joins the renamed scaffold/ (formerly sdk/) as its server half
beside client/protocol/create-sdk/helper/scripts/telemetry, whose
folders drop the legacy sdk- prefix. Three new group README triplets
replace the ui/ and sdk/ ones; tsconfig references/paths/globs,
knip keys, vitest globs, gate scripts, catalogs, docs, and the
lockfile follow. Adds the four settled FIXME rename markers
(dsh-sdk-server, dsh-sdk-telemetry, dsh-sdk-helper, dsh-sdk-scripts).
The scaffold folders diverge from their npm names until those renames
land, so tsconfig.base.json maps the three affected names explicitly
beside the group wildcard. Also repairs two pre-existing stale-path
classes the strengthened sweep surfaced: docs/web-styling.md's retired
web-ui host package and type-model spec fixture-literal joins.
app-boot's three Loader-composition specs time out at the default 5s
under full-suite parallel load on this filesystem (pre-existing;
pass isolated with --testTimeout=30000); interaction/scaffold/boot
suites otherwise green (687 passed).
2026-07-30 03:13:49 +08:00
'packages/boot/app-boot' : { kind : 'indirect' , reason : 'Only the loaded plugin tree contributes model context.' } ,
2026-08-06 20:52:26 +08:00
'packages/boot/cmdline' : { kind : 'none' , reason : 'Resolves the process command line before any session exists; configured rows own every model-visible consequence.' } ,
chore(repo): wire profile apps and the renamed runtime through builds
Update workspace manifests, the lockfile, Host project references, Knip inputs, package constraints, vendoring rewrites, and Python runtime build/smoke scripts for sdk-app, acp-app, and @deepseek-ai/dsh-sdk-python-runtime. Add the ACP hook packages to the dsh dependency closure so installed profile materialization resolves the same plugins as source workspaces.
Keep Python distribution outputs deliberately unchanged: the wheel modules, executable names, and smoke targets retain their public identities even though their private npm carrier moved. Constraint fixtures pin the new package locations and catch missing application dependencies on every platform.
2026-08-23 01:49:21 +08:00
'packages/sdk/python-runtime' : { kind : 'indirect' , reason : 'Only the externally configured plugin tree contributes model context.' } ,
2026-08-13 00:36:22 +08:00
'packages/interaction/permission-presets' : { kind : 'indirect' , reason : 'The service writes mechanism events rendered by dsh-user-approval and dsh-tool-bash.' } ,
'packages/interaction/user-questions' : { 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-08-13 00:36:22 +08:00
'packages/util/output-retention' : { kind : 'indirect' , reason : 'Only retention consumers render retained content and omission metadata.' } ,
2026-07-24 19:54:25 +08:00
'packages/util/native-command' : { kind : 'none' , reason : 'The host-side subprocess runner registers nothing model-facing.' } ,
2026-07-13 22:40:19 +08:00
'packages/web/web' : { kind : 'indirect' , reason : 'The provider registry delegates model rendering to dsh-tool-web.' } ,
2026-08-13 00:36:22 +08:00
'packages/web/web-fetch-http' : { kind : 'indirect' , reason : 'The provider backend delegates model rendering to dsh-tool-web.' } ,
2026-07-13 22:40:19 +08:00
'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-24 19:54:25 +08:00
interface ModelExperienceEntry {
2026-07-14 00:22:52 +08:00
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-24 19:54:25 +08:00
return { blocks , error : ` verbatim H5 title ${ JSON . stringify ( title ) } is duplicated within its model-context entry ` }
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-08-09 15:27:21 +08:00
/** GitHub-style fragment for the simple ASCII nested titles allowed by these rules. */
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-08-09 15:27:21 +08:00
/** A direct stable system-prompt contribution, as named by the README rules. */
2026-07-24 19:54:25 +08:00
function isDirectSystemPromptEntry ( title : string ) : boolean {
2026-07-14 00:22:52 +08:00
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
2026-07-24 19:54:25 +08:00
let modelContextEntryCount = 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-24 19:54:25 +08:00
let systemPromptEntryCount = 0
let toolSchemaEntryCount = 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-24 19:54:25 +08:00
failures . push ( { path : ` ${ pkg } /README.md ` , message : 'sentence allowlist entry must justify why structured model-context entries 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-24 19:54:25 +08:00
const entryStarts = content
2026-07-14 00:22:52 +08:00
. map ( ( line , index ) = > ( { line , index } ) )
. filter ( entry = > /^### \S/ . test ( entry . line . raw ) )
2026-07-24 19:54:25 +08:00
if ( entryStarts . length === 0 || entryStarts [ 0 ] ? . index !== 0 ) {
failures . push ( { path : readme , message : 'must contain one or more complete model-context entries' } )
2026-07-12 02:12:36 +08:00
continue
}
2026-07-24 19:54:25 +08:00
const modelContextEntries : ModelExperienceEntry [ ] = [ ]
const entryFragments = new Set < string > ( )
let entryError = false
for ( let entryIndex = 0 ; entryIndex < entryStarts . length ; entryIndex += 1 ) {
const start = entryStarts [ entryIndex ] as { line : Line ; index : number }
const end = entryStarts [ entryIndex + 1 ] ? . index ? ? content . length
2026-07-14 00:22:52 +08:00
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 ) {
2026-07-24 19:54:25 +08:00
failures . push ( { path : readme , message : ` line ${ heading . index } : each model-context entry requires a non-empty H3 heading ` } )
entryError = true
2026-07-13 22:26:33 +08:00
break
2026-07-12 02:12:36 +08:00
}
2026-07-24 19:54:25 +08:00
if ( entryFragments . has ( fragment ) ) {
failures . push ( { path : readme , message : ` line ${ heading . index } : duplicate model-context entry link fragment ${ JSON . stringify ( fragment ) } ` } )
entryError = true
2026-07-13 22:26:33 +08:00
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 ) {
2026-07-24 19:54:25 +08:00
failures . push ( { path : readme , message : ` line ${ heading . index } : model-context entry requires exactly three ordered H4 fields: ${ FIELD_HEADINGS . join ( ', ' ) } ` } )
entryError = true
2026-07-13 22:26:33 +08:00
break
}
2026-07-24 19:54:25 +08:00
if ( ( entryIndex === 0 && heading . index !== modelHeading . index + 2 )
2026-07-14 00:22:52 +08:00
|| rawLines [ heading . index - 2 ] ? . trim ( ) . length !== 0
2026-07-19 18:08:42 +08:00
|| fieldStarts [ 0 ] . line . index !== heading . index + 2 ) {
2026-07-24 19:54:25 +08:00
failures . push ( { path : readme , message : ` line ${ heading . index } : model-context entry heading and first field require one blank line between them ` } )
entryError = true
2026-07-14 00:22:52 +08:00
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 ) } ` } )
2026-07-24 19:54:25 +08:00
entryError = true
2026-07-19 18:08:42 +08:00
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 ` } )
2026-07-24 19:54:25 +08:00
entryError = true
2026-07-19 18:08:42 +08:00
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 ` } )
2026-07-24 19:54:25 +08:00
entryError = true
2026-07-19 18:08:42 +08:00
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 ` } )
2026-07-24 19:54:25 +08:00
entryError = true
2026-07-19 18:08:42 +08:00
break
}
const nextHeadingLine = fieldStarts [ fieldIndex + 1 ] ? . line . index
2026-07-24 19:54:25 +08:00
? ? entryStarts [ entryIndex + 1 ] ? . line . index
2026-07-19 18:08:42 +08:00
? ? nextH2Line
if ( rawLines [ nextHeadingLine - 2 ] ? . trim ( ) . length !== 0 ) {
failures . push ( { path : readme , message : ` line ${ nextHeadingLine } : Model Experience headings require a preceding blank line ` } )
2026-07-24 19:54:25 +08:00
entryError = true
2026-07-19 18:08:42 +08:00
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 } ` } )
2026-07-24 19:54:25 +08:00
entryError = true
2026-07-19 18:08:42 +08:00
break
}
if ( fieldEntries . length - 2 !== verbatim . blocks ) {
failures . push ( { path : readme , message : ` line ${ value . index } : every nested H5 must own exactly one \` markdown \` fence ` } )
2026-07-24 19:54:25 +08:00
entryError = true
2026-07-19 18:08:42 +08:00
break
}
parsedFields . push ( { value , verbatimBlocks : verbatim.blocks } )
2026-07-14 00:22:52 +08:00
}
2026-07-24 19:54:25 +08:00
if ( entryError ) break
2026-07-19 18:08:42 +08:00
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-24 19:54:25 +08:00
entryError = true
2026-07-14 00:22:52 +08:00
break
}
2026-07-24 19:54:25 +08:00
entryFragments . add ( fragment )
modelContextEntries . push ( {
2026-07-19 18:08:42 +08:00
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-24 19:54:25 +08:00
if ( entryError ) continue
2026-07-13 22:26:33 +08:00
2026-07-24 19:54:25 +08:00
const promptWithoutVerbatim = modelContextEntries . find ( entry = > isDirectSystemPromptEntry ( entry . title )
&& entry . modelViewVerbatimBlocks === 0 )
2026-07-14 00:22:52 +08:00
if ( promptWithoutVerbatim !== undefined ) {
2026-07-24 19:54:25 +08:00
failures . push ( { path : readme , message : ` line ${ promptWithoutVerbatim . heading . index } : system-prompt entry must contain a titled H5 plus verbatim \` markdown \` block under ${ MODEL_VIEW_HEADING } ` } )
2026-07-13 21:33:23 +08:00
continue
}
2026-07-24 19:54:25 +08:00
const hasConcreteLiteral = modelContextEntries . some ( entry = > entry . verbatimBlocks > 0
|| entry . modelView . raw . includes ( '`' )
|| entry . tokenEffect . raw . includes ( '`' )
|| toolCatalogLinkFragments ( entry . modelView . raw ) . length > 0 )
2026-07-14 00:22:52 +08:00
if ( ! hasConcreteLiteral ) {
2026-07-24 19:54:25 +08:00
failures . push ( { path : readme , message : 'structured Model Experience must ground at least one entry 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
2026-07-24 19:54:25 +08:00
for ( const entry of modelContextEntries ) {
if ( ! /\bschemas?\b/i . test ( entry . title ) ) continue
const fragments = toolCatalogLinkFragments ( entry . modelView . raw )
2026-07-14 00:22:52 +08:00
if ( fragments . length === 0 ) {
2026-07-24 19:54:25 +08:00
failures . push ( { path : readme , message : ` line ${ entry . heading . index } : tool-schema entry must link an anchored section of ../../../docs/tool-catalog.md ` } )
2026-07-14 00:22:52 +08:00
catalogError = true
break
}
const invalid = fragments . find ( fragment = > ! toolCatalogFragments . has ( fragment ) )
if ( invalid !== undefined ) {
2026-07-24 19:54:25 +08:00
failures . push ( { path : readme , message : ` line ${ entry . modelView . index } : tool-catalog link fragment ${ JSON . stringify ( invalid ) } does not name an H2 section ` } )
2026-07-14 00:22:52 +08:00
catalogError = true
break
}
2026-07-13 15:47:46 +08:00
}
2026-07-14 00:22:52 +08:00
if ( catalogError ) continue
2026-07-24 19:54:25 +08:00
verbatimBlockCount += modelContextEntries . reduce ( ( total , entry ) = > total + entry . verbatimBlocks , 0 )
modelContextEntryCount += modelContextEntries . length
systemPromptEntryCount += modelContextEntries . filter ( entry = > isDirectSystemPromptEntry ( entry . title ) ) . length
toolSchemaEntryCount += modelContextEntries . filter ( entry = > /\bschemas?\b/i . test ( entry . title ) ) . length
kvCacheEffectCount += modelContextEntries . 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-24 19:54:25 +08:00
console . log ( ` verify-package-readme-model-experience: ${ packageJsons . length } README(s) checked ( ${ omittedSectionCount } audited omissions, ${ structuredCount } structured, ${ modelContextEntryCount } model-context entries, ${ kvCacheEffectCount } KV-cache fields, ${ systemPromptEntryCount } fenced system-prompt entries, ${ toolSchemaEntryCount } catalog-linked tool-schema entries, ${ 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 )