diff --git a/docs/cookbook/adding-a-package.md b/docs/cookbook/adding-a-package.md index c781c1b4c6..73274ce1eb 100644 --- a/docs/cookbook/adding-a-package.md +++ b/docs/cookbook/adding-a-package.md @@ -66,7 +66,7 @@ Stable system-prompt prose of any length, or another long non-generated literal, Fill Model Experience from the implementation. Direct, multi-surface, conditional, capped, or lifetime effects use one H3 block per context surface; each block has the exact bold-led `What the model sees` and `Token effect` fields shown above. A structured section grounds at least one surface in concrete model-visible text through inline code, a nested `markdown` block, or an anchored tool-catalog link. Put every stable system-prompt paragraph, including a one-liner, in a titled H4 plus `markdown` fence immediately after those fields inside the owning H3 whose title contains `system prompt`; never leave prompt prose in inline code. Quote other short stable model-visible source literals inline, using named placeholders such as `` only for interpolated values, and attach other long non-generated literals to their owning H3 in the same H4-plus-fence form. Describe an attached literal as the text "below" instead of linking between Model Experience subsections; the physical nesting already records ownership. A tool-schema surface uses `schema` in its H3 and links the relevant anchored package section in the generated [tool schema catalog](../tool-catalog.md) instead of copying its default descriptions or JSON Schema; describe only configuration or composition deltas the catalog does not contain. A runtime-only definition outside the catalog's stated scope links that scope and explains the exception before reproducing its stable text. Summarize data-dependent payloads or provider-owned text by identifying their exact shape and renderer. Do not infer prompt visibility from tool-schema visibility because independently registered guidance can remain after a scoped tool restriction. -An audited package with no context effect or one simple consumer-owned path belongs in [`SENTENCE_MODEL_EXPERIENCE`](../../scripts/verify-package-readme-model-experience.ts) and uses one line beginning `None, as ` or `Indirectly, through `. If a tiny package's lack of runtime and model behavior is already self-evident from its package contract, it may instead join the narrow `BARE_NONE_MODEL_EXPERIENCE` allowlist and use exactly `None.`; the allowlist retains the audit reason so the README need not repeat it. Pure routing, transport, and keyless test-support packages otherwise use `None, as ` when they create no model-bound content even if consumers use them during composition. A provider backend whose single context path is formatted and inserted entirely by a named consumer uses `Indirectly, through ` even when it caps or filters data before returning it; a wiring bundle whose model effects all belong to named children uses the same form. Do not give these packages a structured block describing another package's work. Packages that own model input, output shaping, multiple context paths, or an auxiliary request keep context-surface blocks; the verifier gates their H3 headings, field labels, spacing, concrete literal evidence, nested H4-plus-`markdown` blocks, absence of local subsection links, system-prompt literals, and schema-surface-to-catalog links. A package with genuinely no limitations joins the separate allowlist in [`verify-package-readme-limitations.ts`](../../scripts/verify-package-readme-limitations.ts) and ends after Model Experience. The [Model Experience RFC](../rfc/implemented/process/2026-07-12-package-model-experience-contract.md) records the rationale. +An audited package with no context effect or one simple consumer-owned path belongs in [`SENTENCE_MODEL_EXPERIENCE`](../../scripts/verify-package-readme-model-experience.ts) and uses one line beginning `None, as ` or `Indirectly, through `. A generic package whose public contract is model-agnostic may instead join the narrow `NO_MODEL_EXPERIENCE_SECTION` allowlist and omit the heading entirely; the allowlist retains the audit reason so absence cannot mean forgotten documentation. Pure transport and keyless test-support packages otherwise use `None, as ` when they create no model-bound content even if consumers use them during composition. A provider backend whose single context path is formatted and inserted entirely by a named consumer uses `Indirectly, through ` even when it caps or filters data before returning it; so does a wiring bundle whose model effects all belong to named children. Do not give these packages a structured block describing another package's work. Packages that own model input, output shaping, multiple context paths, or an auxiliary request keep context-surface blocks; the verifier gates their H3 headings, field labels, spacing, concrete literal evidence, nested H4-plus-`markdown` blocks, absence of local subsection links, system-prompt literals, and schema-surface-to-catalog links. A package with genuinely no limitations joins the separate allowlist in [`verify-package-readme-limitations.ts`](../../scripts/verify-package-readme-limitations.ts); the two omission allowlists are independent. The [Model Experience RFC](../rfc/implemented/process/2026-07-12-package-model-experience-contract.md) records the rationale. ## 5. Verify diff --git a/docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.md b/docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.md index e05f3c4f23..f6c098ef5d 100644 --- a/docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.md +++ b/docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.md @@ -8,11 +8,11 @@ A package README can explain APIs and runtime mechanics without answering the qu ## Decision -Every workspace package README ends with the canonical [Model Experience section](../../../cookbook/adding-a-package.md#4-write-the-package-readme), immediately before `## Known Limitations and Deferred Work`; a package on the no-limitations allowlist ends with Model Experience itself. Packages with direct, multi-surface, conditional, capped, or lifetime effects use one H3 block per context surface. Each block says what the relevant model literally receives and when under `**What the model sees**:`, then classifies the token effect under `**Token effect**:`; the structured section grounds at least one surface with inline code, a nested `markdown` block, or an anchored catalog link. Every stable system-prompt paragraph, including a one-liner, follows those fields inside the owning H3 as a titled H4 plus `markdown` fence; the H3 title contains `system prompt`. Other short stable source literals remain inline with named placeholders only for interpolated values; other long non-generated literals use the same nested H4 form. Model Experience subsections do not link to each other because physical nesting owns the literal. Tool-schema surfaces use `schema` in their H3 and link the relevant anchored package section of the generated [tool schema catalog](../../../tool-catalog.md) rather than copying default descriptions or JSON Schema, then state only configuration or composition deltas absent from that catalog. A runtime-only definition outside the catalog's stated scope links that scope and explains the exception before reproducing its stable text. Summaries are reserved for data-dependent payloads and provider-owned text. The default subject is the conversation model; a package that invokes an auxiliary model, such as a summarizer or search provider, names that request separately. Agent-scoped visibility is stated where it changes which agent receives a contribution. Prompt text and tool schemas are described separately whenever configuration or scoping can hide one without the other. +Every workspace package README with a model-facing or model-adjacent contract ends with the canonical [Model Experience section](../../../cookbook/adding-a-package.md#4-write-the-package-readme), immediately before `## Known Limitations and Deferred Work`; a package on the no-limitations allowlist ends with Model Experience itself. An audited generic package whose public contract is model-agnostic omits Model Experience through `NO_MODEL_EXPERIENCE_SECTION`, independently of whether it has a limitations section. Packages with direct, multi-surface, conditional, capped, or lifetime effects use one H3 block per context surface. Each block says what the relevant model literally receives and when under `**What the model sees**:`, then classifies the token effect under `**Token effect**:`; the structured section grounds at least one surface with inline code, a nested `markdown` block, or an anchored catalog link. Every stable system-prompt paragraph, including a one-liner, follows those fields inside the owning H3 as a titled H4 plus `markdown` fence; the H3 title contains `system prompt`. Other short stable source literals remain inline with named placeholders only for interpolated values; other long non-generated literals use the same nested H4 form. Model Experience subsections do not link to each other because physical nesting owns the literal. Tool-schema surfaces use `schema` in their H3 and link the relevant anchored package section of the generated [tool schema catalog](../../../tool-catalog.md) rather than copying default descriptions or JSON Schema, then state only configuration or composition deltas absent from that catalog. A runtime-only definition outside the catalog's stated scope links that scope and explains the exception before reproducing its stable text. Summaries are reserved for data-dependent payloads and provider-owned text. The default subject is the conversation model; a package that invokes an auxiliary model, such as a summarizer or search provider, names that request separately. Agent-scoped visibility is stated where it changes which agent receives a contribution. Prompt text and tool schemas are described separately whenever configuration or scoping can hide one without the other. -Every package participates. A package with no model-context effect, or one simple effect rendered entirely by another package, uses one of the verifier's audited short-form allowlists instead of expanding a negative fact into a structured block. It normally carries exactly one sentence beginning `None, as ` or `Indirectly, through `. A tiny package whose lack of runtime and model behavior is already self-evident may instead use exactly `None.` when `BARE_NONE_MODEL_EXPERIENCE` records the package and its audit reason. Pure routing, transport, and keyless test-support packages otherwise use the explained none form when they create no model-bound content. A provider backend whose single context path is formatted and inserted entirely by a named consumer uses the indirect sentence even when it caps or filters data before returning it; wiring bundles do the same when every model effect belongs to named children. The indirect form names that consumer only to locate this package's contribution and does not restate the consumer's implementation. Structured blocks likewise document only package-owned inputs, transformations, and deltas. Packages that own model input, output shaping, multiple context paths, or an auxiliary request keep context-surface blocks even when they add zero direct prompt tokens. +Every non-omitted package participates. A package with no model-context effect, or one simple effect rendered entirely by another package, uses the verifier's audited sentence allowlist instead of expanding one fact into a structured block. It carries exactly one sentence beginning `None, as ` or `Indirectly, through `. Pure transport and keyless test-support packages use the explained none form when they create no model-bound content. A provider backend whose single context path is formatted and inserted entirely by a named consumer uses the indirect form even when it caps or filters data before returning it; wiring bundles do the same when every model effect belongs to named children. The indirect form names that consumer only to locate this package's contribution and does not restate the consumer's implementation. Structured blocks likewise document only package-owned inputs, transformations, and deltas. Packages that own model input, output shaping, multiple context paths, or an auxiliary request keep context-surface blocks even when they add zero direct prompt tokens. -`verify-package-readme-model-experience` discovers packages from `packages/*/*/package.json`, requires one sibling README and the canonical final-section order, and validates one of three package-classified bodies. A bare-none package carries exactly `None.`, an explanatory short-form package carries exactly one sentence with its assigned prefix, and every other package carries at least one H3 context surface with the two exact, non-empty fields and one blank line between each element plus at least one inline literal, nested block, or catalog link across the section. Optional verbatim literals follow those fields inside that surface, each as an H4 title paired to one non-empty `markdown` fence. Local subsection links are rejected, every system-prompt surface requires at least one nested block, and every tool-schema surface links an existing H2 section in the generated tool catalog. The check runs in `doc-sync` and the parallel gate runner. It owns package classification, structural presence, concrete-literal evidence, nested-block shape, catalog-link shape, and order; implementation review owns coverage, link relevance, and the truth of the prose. +`verify-package-readme-model-experience` discovers packages from `packages/*/*/package.json`, requires one sibling README, and validates one of three package classifications. A no-section package carries no Model Experience heading, an explanatory short-form package carries exactly one sentence with its assigned prefix, and every other package carries at least one H3 context surface with the two exact, non-empty fields and one blank line between each element plus at least one inline literal, nested block, or catalog link across the section. For packages with the section, the verifier also enforces the canonical final-section order. Optional verbatim literals follow those fields inside that surface, each as an H4 title paired to one non-empty `markdown` fence. Local subsection links are rejected, every system-prompt surface requires at least one nested block, and every tool-schema surface links an existing H2 section in the generated tool catalog. The check runs in `doc-sync` and the parallel gate runner. It owns package classification, structural presence, concrete-literal evidence, nested-block shape, catalog-link shape, and order; implementation review owns coverage, link relevance, and the truth of the prose. ## Alternatives considered @@ -20,10 +20,10 @@ Every package participates. A package with no model-context effect, or one simpl - **Generate one central context-cost catalog from source** — rejected because an AST can find registrations but cannot infer semantic conditions such as history retention, output truncation, parent-versus-child visibility, or an auxiliary model boundary. The package README is the implementation-local contract; a central copy would add another drift surface. - **Require numeric token counts** — rejected because exact counts depend on the selected model tokenizer, adapter serialization, configuration, and runtime data. The stable contract is the growth shape: fixed per request, conditional per call, retained, replaced, capped, or zero-direct. - **Use a three-column table** — rejected because exact source text and conditional result shapes make cells dense and difficult to scan. Repeated subsections give each context surface readable vertical space while preserving the same fields. -- **Allow zero-impact packages to omit the section** — rejected because absence is ambiguous between an audited zero and forgotten documentation. One explicit sentence is cheap and mechanically distinguishable. +- **Allow every zero-impact package to omit the section** — rejected because unconstrained absence is ambiguous between an audited zero and forgotten documentation. Omission is reserved for model-agnostic generic packages named with a reason in the verifier; model-adjacent zero-impact packages keep one explicit sentence. - **Require the full structured form for audited zero or simple indirect packages** — rejected because it repeats labels around one fact. A gated sentence preserves explicit coverage without the ceremony. - **Convention without a gate** — rejected because a repo-wide contract must also cover every future package; review memory cannot reliably detect an omitted README section. ## Consequences -A reviewer can start at any package and see its contribution to the conversation model, child models, and auxiliary calls without reconstructing the full plugin graph. Token-budget work can distinguish repeated request overhead from data-dependent history, and agent-scoped changes have an explicit documentation checkpoint. Package authors maintain one or more compact context-surface blocks or one classified sentence whenever model-visible behavior changes. The structured fields do not promise provider-exact token counts; measurements remain model- and workload-specific, while the documented growth and visibility contract stays stable. +A reviewer can start at any model-facing or model-adjacent package and see its contribution to the conversation model, child models, and auxiliary calls without reconstructing the full plugin graph. Token-budget work can distinguish repeated request overhead from data-dependent history, and agent-scoped changes have an explicit documentation checkpoint. Package authors maintain one or more compact context-surface blocks or one classified sentence whenever model-visible behavior changes; audited generic packages carry no irrelevant model boilerplate. The structured fields do not promise provider-exact token counts; measurements remain model- and workload-specific, while the documented growth and visibility contract stays stable. diff --git a/packages/core/scope/README.md b/packages/core/scope/README.md index 2076acac4d..a1ee0dcf0b 100644 --- a/packages/core/scope/README.md +++ b/packages/core/scope/README.md @@ -19,10 +19,6 @@ Ownership and visibility derive from ONE fact — which context a registration w Handing out a scoped context hands out the minting plugin's service-resolution surface (resolution walks the minting fiber's dependency chain, not the holder's) — mint it from the plugin whose dependencies the scoped registrations need to resolve. -## Model Experience - -None, as this scoping primitive emits no model-bound text, schema, or message; it only routes registrations and restrictions owned by other plugins. - ## Known Limitations and Deferred Work - **Only scope-aware surfaces isolate state** — registries must file by `scopeOf()` and events must dispatch through `scopeTarget()`; an arbitrary Cordis service remains context-global merely because it is called through a scoped context. diff --git a/packages/util/brand/README.md b/packages/util/brand/README.md index 9d31d89628..8f7943def7 100644 --- a/packages/util/brand/README.md +++ b/packages/util/brand/README.md @@ -24,7 +24,3 @@ Construction goes through the per-id factory in the OWNING package (a plain cast A package brands the ids it OWNS — `CallId` in `dsh-llm` (tool-call correlation), `SessionId` in `dsh-session`, `AgentId` in `dsh-agent`, `BashTaskId`/`OwnerToken` in `dsh-bash`. Branding is for ids that cross package boundaries and could plausibly be confused; **not every string needs a brand.** This package owns ONLY the primitive — no concrete id, no runtime code beyond the (erased) type. Keeping the primitive dependency-free is the point: a capability package can brand its ids without depending on an unrelated package. `dsh-bash`, for example, brands `BashTaskId`/`OwnerToken` by depending on `dsh-brand` alone — it never pulls in `dsh-llm` (or `dsh-session`) just to reach `Branded`. - -## Model Experience - -None. diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index dac1a0ee90..2b74d654ea 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -4,8 +4,8 @@ * 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 - * links are rejected, and audited allowlists use either one concise sentence - * or the exact bare `None.` form. + * links are rejected, and audited package classifications either use one + * concise sentence or omit the section entirely. * * Run: `tsx scripts/verify-package-readme-model-experience.ts`. */ @@ -29,11 +29,12 @@ interface SentenceContract { } /** - * 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. + * 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. */ -const BARE_NONE_MODEL_EXPERIENCE: Readonly> = { +const NO_MODEL_EXPERIENCE_SECTION: Readonly> = { + 'packages/core/scope': 'The package is a model-agnostic registration and lifecycle primitive; model-facing consumers own any context selection.', 'packages/util/brand': 'The package is a type-only primitive erased at compile time.', } @@ -48,7 +49,6 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly> = { 'packages/code-runtime/code-runtime': { kind: 'indirect', reason: 'The service interface delegates model rendering to Code Mode in dsh-tools.' }, 'packages/code-runtime/code-runtime-worker': { kind: 'indirect', reason: 'The worker backend delegates model rendering to Code Mode in dsh-tools.' }, '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.' }, 'packages/fs/fs': { kind: 'indirect', reason: 'The service interface delegates model rendering to dsh-tool-fs.' }, 'packages/fs/fs-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-fs.' }, 'packages/hooks/hook-protocol': { kind: 'indirect', reason: 'Only the hook bridge plugins render decoded hook output to a model.' }, @@ -155,22 +155,22 @@ const packageJsons = globSync('packages/*/*/package.json', { cwd: root }).sort() const scannedPackages = new Set(packageJsons.map(path => path.slice(0, -'/package.json'.length))) let structuredCount = 0 let contextSurfaceCount = 0 -let bareNoneCount = 0 +let omittedSectionCount = 0 let explainedNoneCount = 0 let indirectCount = 0 let verbatimBlockCount = 0 let systemPromptSurfaceCount = 0 let toolSchemaSurfaceCount = 0 -for (const [pkg, reason] of Object.entries(BARE_NONE_MODEL_EXPERIENCE)) { +for (const [pkg, reason] of Object.entries(NO_MODEL_EXPERIENCE_SECTION)) { if (!scannedPackages.has(pkg)) { - failures.push({ path: `${pkg}/README.md`, message: 'bare-none allowlist entry does not name a scanned package' }) + failures.push({ path: `${pkg}/README.md`, message: 'no-section 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' }) + failures.push({ path: `${pkg}/README.md`, message: 'no-section 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' }) + failures.push({ path: `${pkg}/README.md`, message: 'package cannot appear in both Model Experience allowlists' }) } } @@ -188,7 +188,7 @@ for (const packageJson of packageJsons) { 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}` }) + failures.push({ path: readme, message: 'missing package README' }) continue } @@ -197,6 +197,14 @@ for (const packageJson of packageJsons) { const lines = markdownProseLines(text) const h2Headings = lines.filter(line => H2_HEADING.test(line.raw)) const modelHeadings = h2Headings.filter(line => line.raw === HEADING) + if (NO_MODEL_EXPERIENCE_SECTION[pkg] !== undefined) { + if (modelHeadings.length !== 0) { + failures.push({ path: readme, message: `audited model-agnostic package must omit ${HEADING}` }) + } else { + omittedSectionCount += 1 + } + continue + } if (modelHeadings.length !== 1) { failures.push({ path: readme, @@ -227,17 +235,6 @@ for (const packageJson of packageJsons) { const nextH2Line = nextH2 < 0 ? rawLines.length + 1 : (body[nextH2] as Line).index const rawSection = rawLines.slice(modelHeading.index, nextH2Line - 1) const content = section.filter(line => line.raw.trim().length > 0) - 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 - } - const sentenceContract = SENTENCE_MODEL_EXPERIENCE[pkg] if (sentenceContract !== undefined) { const pattern = sentenceContract.kind === 'none' ? /^None, as .+\.$/ : /^Indirectly, through .+\.$/ @@ -254,7 +251,7 @@ for (const packageJson of packageJsons) { const shortSentence = content.find(line => line.raw === 'None.' || /^None, as |^Indirectly, through /.test(line.raw)) if (shortSentence !== undefined) { - 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` }) + failures.push({ path: readme, message: `line ${shortSentence.index}: short Model Experience form requires an audited entry in SENTENCE_MODEL_EXPERIENCE` }) continue } @@ -373,7 +370,7 @@ for (const packageJson of packageJsons) { } if (failures.length === 0) { - 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.`) + console.log(`verify-package-readme-model-experience: ${packageJsons.length} README(s) checked (${omittedSectionCount} audited omissions, ${structuredCount} structured, ${contextSurfaceCount} context surfaces, ${systemPromptSurfaceCount} fenced system-prompt surfaces, ${toolSchemaSurfaceCount} catalog-linked tool-schema surfaces, ${explainedNoneCount} explained none, ${indirectCount} indirect, ${verbatimBlockCount} verbatim markdown blocks), all conform.`) process.exit(0) }