diff --git a/scripts/structure-ssot.ts b/scripts/structure-ssot.ts index ddc61e09940..fb783434f2a 100644 --- a/scripts/structure-ssot.ts +++ b/scripts/structure-ssot.ts @@ -35,6 +35,14 @@ export type Manifest = { absentPaths: { path: string; reason: string }[]; tiers: { id: number; name: string; purpose: string }[]; docs: { path: string; tier: number; title: string; scope: string; documents: string[] }[]; + contracts?: { + version: 1; + entries: { + id: string; + owner: { document: string; anchor: string }; + dependents: string[]; + }[]; + }; grace: { undocumentedSourceAreas: { path: string; reason: string }[]; unboundInvariants: { id: string; reason: string }[]; @@ -87,6 +95,34 @@ export function loadManifest(raw: string): { manifest: Manifest } | { error: str if (!isArray(doc?.documents)) problems.push("docs[" + i + "].documents must be an array"); }); } + if (m.contracts !== undefined) { + if (typeof m.contracts !== "object" || m.contracts === null || isArray(m.contracts)) { + problems.push("contracts must be an object"); + } else { + const contracts = m.contracts as Partial>; + if (contracts.version !== 1) problems.push("contracts.version must be 1"); + if (!isArray(contracts.entries)) problems.push("contracts.entries must be an array"); + else { + contracts.entries.forEach((entry, i) => { + if (typeof entry?.id !== "string" || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(entry.id)) { + problems.push("contracts.entries[" + i + "].id must be a kebab-case string"); + } + if (typeof entry?.owner !== "object" || entry.owner === null || isArray(entry.owner)) { + problems.push("contracts.entries[" + i + "].owner must be an object"); + } else { + if (typeof entry.owner.document !== "string") problems.push("contracts.entries[" + i + "].owner.document must be a string"); + if (typeof entry.owner.anchor !== "string") problems.push("contracts.entries[" + i + "].owner.anchor must be a string"); + } + if (!isArray(entry?.dependents)) problems.push("contracts.entries[" + i + "].dependents must be an array"); + else { + entry.dependents.forEach((dependent, j) => { + if (typeof dependent !== "string") problems.push("contracts.entries[" + i + "].dependents[" + j + "] must be a string"); + }); + } + }); + } + } + } const grace = m.grace as Partial | undefined; if (!grace) problems.push("grace must be an object"); else { @@ -210,8 +246,7 @@ export function renderIndex(manifest: Manifest): string { lines.push("## Which doc describes which source"); lines.push(""); lines.push("A source area can be described by more than one doc, because these docs are organised by topic and"); - lines.push(BT + "src/" + BT + " is organised by module. Changing an area obliges the same change to update every doc listed"); - lines.push("for it; see [" + BT + "AGENTS.md" + BT + "](AGENTS.md)."); + lines.push(BT + "src/" + BT + " is organised by module. Changing an area requires review of every listed document. Edit only the documents whose local explanation changes; named cross-cutting authorities and dependents are listed below."); lines.push(""); lines.push("| Source path | Described by |"); lines.push("| --- | --- |"); @@ -232,6 +267,23 @@ export function renderIndex(manifest: Manifest): string { lines.push("| " + BT + row.path + BT + " | " + row.reason + " |"); } lines.push(""); + if (manifest.contracts?.entries.length) { + lines.push("## Cross-cutting contracts"); + lines.push(""); + lines.push("Source review remains defined by the source-to-doc map above. This registry names each authoritative statement and the documents that review it. Link validation proves declared topology, not behavioral correctness."); + lines.push(""); + lines.push("| Contract | Authority | Review dependents |"); + lines.push("| --- | --- | --- |"); + for (const contract of [...manifest.contracts.entries].sort((a, b) => a.id.localeCompare(b.id))) { + const owner = contract.owner.document + "#" + contract.owner.anchor; + const dependents = [...contract.dependents] + .sort((a, b) => a.localeCompare(b)) + .map((document) => "[" + BT + document + BT + "](" + document + ")") + .join("
") || "—"; + lines.push("| " + BT + contract.id + BT + " | [" + BT + owner + BT + "](" + owner + ") | " + dependents + " |"); + } + lines.push(""); + } lines.push("## Decision records"); lines.push(""); lines.push("Superseded reasoning lives in " + BT + "decisions/" + BT + " as numbered records. A doc states the contract that holds now and"); @@ -376,7 +428,54 @@ export function runStructureChecks(repoRoot: string): string[] { if (isTracked(absent.path)) fail(absent.path + " is declared absent in manifest.json but is tracked; the docs describing its absence are wrong"); } - // 4. decision records + // 4. cross-cutting contract authority + const contractIds = new Set(); + for (const contract of manifest.contracts?.entries ?? []) { + if (contractIds.has(contract.id)) fail("contract " + contract.id + " is declared twice"); + contractIds.add(contract.id); + + const owner = contract.owner.document; + const ownerAbs = join(structureDir, owner); + if (!declared.includes(owner)) fail("contract " + contract.id + " owner " + owner + " is not a declared structure document"); + if (!present.includes(owner)) fail("contract " + contract.id + " owner structure/" + owner + " is missing"); + else if (!headingAnchors(readFileSync(ownerAbs, "utf8")).has(contract.owner.anchor)) { + fail("contract " + contract.id + " owner structure/" + owner + " has no #" + contract.owner.anchor + " heading anchor"); + } + + const dependents = new Set(); + for (const dependent of contract.dependents) { + if (dependents.has(dependent)) fail("contract " + contract.id + " lists dependent " + dependent + " twice"); + dependents.add(dependent); + if (dependent === owner) fail("contract " + contract.id + " lists its owner " + owner + " as a dependent"); + if (!declared.includes(dependent)) fail("contract " + contract.id + " dependent " + dependent + " is not a declared structure document"); + if (!present.includes(dependent)) { + fail("contract " + contract.id + " dependent structure/" + dependent + " is missing"); + continue; + } + const dependentAbs = join(structureDir, dependent); + const body = withoutFences(readFileSync(dependentAbs, "utf8")); + let linked = false; + let hit: RegExpExecArray | null; + linkRe.lastIndex = 0; + while ((hit = linkRe.exec(body))) { + const separator = hit[1].indexOf("#"); + if (separator === -1) continue; + const file = hit[1].slice(0, separator); + const fragment = hit[1].slice(separator + 1); + const resolved = file === "" ? dependentAbs : resolve(dirname(dependentAbs), file); + const target = toPosix(relative(structureDir, resolved)); + if (target === owner && fragment === contract.owner.anchor) { + linked = true; + break; + } + } + if (!linked) { + fail("contract " + contract.id + " dependent structure/" + dependent + " does not link " + owner + "#" + contract.owner.anchor); + } + } + } + + // 5. decision records const adrFiles = present.filter((p) => p.startsWith("decisions/")); const referenced = new Map>(); // Ownership is the declared link form, read with fences removed. A record path mentioned in prose @@ -417,7 +516,7 @@ export function runStructureChecks(repoRoot: string): string[] { } for (const key of referenced.keys()) if (!adrFiles.includes(key)) fail("a doc links structure/" + key + ", which does not exist"); - // 5. invariant-to-test bindings + // 6. invariant-to-test bindings const overviewPath = join(structureDir, "overview.md"); if (!existsSync(overviewPath)) { fail("structure/overview.md is missing; it is the invariant index, and its absence would silence every binding check"); @@ -472,7 +571,7 @@ export function runStructureChecks(repoRoot: string): string[] { } } - // 6. source-to-doc map + // 7. source-to-doc map const described = new Map(); // What a doc actually names, so a manifest claim cannot invent coverage the prose does not have. const namedByDoc = new Map(); @@ -530,7 +629,7 @@ export function runStructureChecks(repoRoot: string): string[] { fail(area + " is described by no doc; add it to a doc's " + BT + "documents" + BT + " list or record it in grace.undocumentedSourceAreas with a reason"); } - // 7. generated index parity + // 8. generated index parity const indexPath = join(structureDir, "INDEX.md"); const expected = renderIndex(manifest); if (!existsSync(indexPath)) fail("structure/INDEX.md is missing; run bun run structure:index"); @@ -541,11 +640,27 @@ export function runStructureChecks(repoRoot: string): string[] { return failures; } +/** + * Rewrite the generated index only after the manifest validates. The renderer trusts the typed + * shape, so a malformed manifest (for example a string where `contracts.entries` belongs) would + * otherwise crash the renderer with a TypeError and could write a broken index before the checker + * ever ran. Validation failure is an actionable schema diagnostic and leaves INDEX.md untouched. + */ +export function writeGeneratedIndex(repoRoot: string): { wrote: true } | { error: string } { + const loaded = loadManifest(readFileSync(join(repoRoot, "structure", "manifest.json"), "utf8")); + if ("error" in loaded) return { error: loaded.error }; + writeFileSync(join(repoRoot, "structure", "INDEX.md"), renderIndex(loaded.manifest), "utf8"); + return { wrote: true }; +} + if (import.meta.main) { const repoRoot = resolve(import.meta.dir, ".."); if (process.argv.includes("--fix")) { - const manifest = JSON.parse(readFileSync(join(repoRoot, "structure/manifest.json"), "utf8")) as Manifest; - writeFileSync(join(repoRoot, "structure/INDEX.md"), renderIndex(manifest), "utf8"); + const fixed = writeGeneratedIndex(repoRoot); + if ("error" in fixed) { + console.error(fixed.error); + process.exit(1); + } console.log("wrote structure/INDEX.md"); } const failures = runStructureChecks(repoRoot); diff --git a/structure/AGENTS.md b/structure/AGENTS.md index bcab24189b7..8856d5219cc 100644 --- a/structure/AGENTS.md +++ b/structure/AGENTS.md @@ -46,8 +46,10 @@ the inverse. the Responses transport doc and the Images doc. An earlier revision of this folder demanded exactly one owner per area, and that rule was simply false here — a false rule is worse than none, because the gate reports green while the map sends a maintainer to the wrong doc. -- **Changing an area obliges the same change to update every doc listed for it.** Not a follow-up, - not a later cleanup pass. +- **Changing an area obliges the same change to review every doc listed for it.** Review fan-out is + unchanged by contract authority: update the authority when the shared contract changes, and update + a dependent only when its local explanation or consequence changes. A reviewed document whose + content remains accurate does not need copied unchanged prose. - Describing an area means naming a path inside it. If a doc explains a subsystem without ever citing a path, the map cannot see it, and the area lands in `grace.undocumentedSourceAreas` instead — which is a signal to add the path reference, not a place to park work. @@ -62,6 +64,16 @@ What the map still does not do: it cannot tell you that two docs describe the sa contradictory words. Avoiding that is a review judgement. Prefer one statement and a link over two statements that will drift apart. +Cross-cutting authority is declared by the optional versioned `contracts` object in +[`manifest.json`](manifest.json). Each entry has a stable kebab-case `id`, one `owner` with a +manifest-declared document and heading anchor, and a `dependents` array of manifest-declared +documents. Every dependent links the exact owner anchor. The registry supplements the source map; +it never narrows which documents a source change requires review of. + +The gate proves declared topology: identifiers are unique, files and anchors exist, and each +dependent carries the declared link. It does not compare prose or prove that the owner statement is +behaviorally correct. Those remain review judgements. + ## Decision records `decisions/ADR-NNNN-.md` holds the reasoning: intent, prior constraints, alternatives, the @@ -103,8 +115,10 @@ violated is worse than admitting the gap: it converts an open question into fals 1. Write or move the file. 2. Add or update its `manifest.json` entry: `path`, `tier`, `title`, `scope`, `documents`. -3. `bun run structure:index` to regenerate [`INDEX.md`](INDEX.md). -4. `bun run structure:check` until it is green. +3. Add or update any affected contract authority and dependent links in `manifest.json`; review every + document mapped to the changed source area even when its text remains accurate. +4. `bun run structure:index` to regenerate [`INDEX.md`](INDEX.md). +5. `bun run structure:check` until it is green. ## What the gate checks @@ -128,12 +142,17 @@ verifies that: - every bound invariant names an existing test that names the id back, and every unbound one is recorded with a reason; - every `src/` directory and top-level module is described by a doc or recorded as undescribed; +- contract ids are unique, owner and dependent documents exist in the manifest, owner anchors exist, + and every dependent links its declared authority without listing the owner as a dependent; - the manifest itself parses and has the shape the gate expects, reported as a failure rather than a stack trace; - `overview.md` exists, because its absence would otherwise silence every invariant check at once; - `INDEX.md` matches what the manifest generates, compared after newline normalisation so a CRLF checkout is not a failure. +Contract checks establish declared authority and links only. They make no automatic claim that an +owner's prose is semantically complete or that a dependent's explanation is behaviorally correct. + Checks are scanned with fenced code blocks removed, so an example inside a fence does not trip a rule it is only illustrating. diff --git a/structure/INDEX.md b/structure/INDEX.md index 161cc18e132..cd101960def 100644 --- a/structure/INDEX.md +++ b/structure/INDEX.md @@ -85,8 +85,7 @@ Background service, docs, release, and design discipline. ## Which doc describes which source A source area can be described by more than one doc, because these docs are organised by topic and -`src/` is organised by module. Changing an area obliges the same change to update every doc listed -for it; see [`AGENTS.md`](AGENTS.md). +`src/` is organised by module. Changing an area requires review of every listed document. Edit only the documents whose local explanation changes; named cross-cutting authorities and dependents are listed below. | Source path | Described by | | --- | --- | @@ -147,6 +146,16 @@ for it; see [`AGENTS.md`](AGENTS.md). | `src/sidecar/` | no doc names a path here; ops/service-and-sidecars.md describes sidecar behavior in prose only | | `src/types/` | shared declarations plus the tool-name and wire-pin resolvers, which no doc currently describes | +## Cross-cutting contracts + +Source review remains defined by the source-to-doc map above. This registry names each authoritative statement and the documents that review it. Link validation proves declared topology, not behavioral correctness. + +| Contract | Authority | Review dependents | +| --- | --- | --- | +| `paginated-history-writer` | [`codex-home.md#paginated-history-writer-boundary`](codex-home.md#paginated-history-writer-boundary) | [`catalog.md`](catalog.md)
[`config.md`](config.md)
[`gui-and-management-api.md`](gui-and-management-api.md)
[`ops/docs-and-release.md`](ops/docs-and-release.md)
[`providers/openai-tiers.md`](providers/openai-tiers.md)
[`runtime.md`](runtime.md)
[`subagents.md`](subagents.md) | +| `request-copy-accounting` | [`transports/byte-accounting.md#request-copy-accounting`](transports/byte-accounting.md#request-copy-accounting) | [`adapters/registry.md`](adapters/registry.md)
[`catalog.md`](catalog.md)
[`clients/claude-desktop.md`](clients/claude-desktop.md)
[`clients/integrations.md`](clients/integrations.md)
[`data-planes/images.md`](data-planes/images.md)
[`data-planes/inbound-compat.md`](data-planes/inbound-compat.md)
[`ops/docs-and-release.md`](ops/docs-and-release.md)
[`ops/service-and-sidecars.md`](ops/service-and-sidecars.md)
[`overview.md`](overview.md)
[`providers/chat-compat.md`](providers/chat-compat.md)
[`providers/cursor.md`](providers/cursor.md)
[`providers/xai-grok.md`](providers/xai-grok.md)
[`runtime.md`](runtime.md)
[`subagents.md`](subagents.md)
[`transports/inventory.md`](transports/inventory.md)
[`transports/streaming-health.md`](transports/streaming-health.md) | +| `stream-buffer-accounting` | [`transports/byte-accounting.md#stream-buffer-accounting`](transports/byte-accounting.md#stream-buffer-accounting) | [`adapters/registry.md`](adapters/registry.md)
[`catalog.md`](catalog.md)
[`clients/claude-desktop.md`](clients/claude-desktop.md)
[`clients/integrations.md`](clients/integrations.md)
[`data-planes/images.md`](data-planes/images.md)
[`data-planes/inbound-compat.md`](data-planes/inbound-compat.md)
[`ops/docs-and-release.md`](ops/docs-and-release.md)
[`ops/service-and-sidecars.md`](ops/service-and-sidecars.md)
[`overview.md`](overview.md)
[`providers/chat-compat.md`](providers/chat-compat.md)
[`providers/cursor.md`](providers/cursor.md)
[`providers/xai-grok.md`](providers/xai-grok.md)
[`runtime.md`](runtime.md)
[`subagents.md`](subagents.md)
[`transports/inventory.md`](transports/inventory.md)
[`transports/streaming-health.md`](transports/streaming-health.md) | + ## Decision records Superseded reasoning lives in `decisions/` as numbered records. A doc states the contract that holds now and diff --git a/structure/catalog.md b/structure/catalog.md index 0e3fe224dd6..dfa424ea1b5 100644 --- a/structure/catalog.md +++ b/structure/catalog.md @@ -448,9 +448,7 @@ its defaults and exclusions are owned by [Responses transport](transports/respon Provider `showThinkingSummary` is a Responses request default; it does not rewrite catalog summary defaults or client configuration. See [Google summaries](providers/google.md). -## Paginated history writer boundary - -`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates refused restore/removal transitions. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. Apply retains an existing provider definition before candidate admission even when history preflight passes, so migration after artifact commit or during worker startup cannot leave earlier conversations without their provider. See the [history writer contract](codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. +Paginated and migration-capable history follows the [authoritative writer contract](codex-home.md#paginated-history-writer-boundary); this document adds no independent writer guarantee. Codex pool settings and their consumers follow the [reset-first ordering contract](providers/openai-tiers.md#reset-first-account-ordering), including independent-quota fallback, preserved affinity, strategy-specific threshold summaries, and shared short-observation freshness for switch warnings. diff --git a/structure/config.md b/structure/config.md index 04520f2cd07..5046d6e9ed0 100644 --- a/structure/config.md +++ b/structure/config.md @@ -408,9 +408,7 @@ so any not-served-here answer stays legible rather than printing a bare token. ` `src/cli/models-runtime.ts` narrows the opposite case: only a 404 without those keys is the management handler's own unknown-id answer, and it names the id and `ocx models list-custom`. -## Paginated history writer boundary - -`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates detected migration. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. See the [history writer contract](codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. +Paginated and migration-capable history follows the [authoritative writer contract](codex-home.md#paginated-history-writer-boundary); this document adds no independent writer guarantee. Private pool credential metadata follows the [quota-history publication identity contract](providers/openai-tiers.md#quota-history-publication-identity); credential-only and account DTO projections omit it. diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index f5340bcfad0..9935ba95946 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -683,9 +683,7 @@ its defaults and exclusions are owned by [Responses transport](transports/respon The provider editor field policy exposes `showThinkingSummary` as a boolean provider option; it controls Responses summary defaults without a dashboard rendering change. See [Google provider](providers/google.md). -## Paginated history writer boundary - -`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates refused restore/removal transitions. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. Apply retains an existing provider definition before candidate admission even when history preflight passes, so migration after artifact commit or during worker startup cannot leave earlier conversations without their provider. See the [history writer contract](codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. +Paginated and migration-capable history follows the [authoritative writer contract](codex-home.md#paginated-history-writer-boundary); this document adds no independent writer guarantee. Codex pool settings and their consumers follow the [reset-first ordering contract](providers/openai-tiers.md#reset-first-account-ordering), including independent-quota fallback, preserved affinity, strategy-specific threshold summaries, and shared short-observation freshness for switch warnings. Codex account DTOs and cards expose the routing-plan exclusion separately from credential health; the [plan exclusion contract](providers/openai-tiers.md#automatic-pool-plan-exclusions) also governs CLI projection. Private pool credential metadata follows the [quota-history publication identity contract](providers/openai-tiers.md#quota-history-publication-identity); credential-only and account DTO projections omit it. diff --git a/structure/manifest.json b/structure/manifest.json index 1337f774e5c..cbd3c7da0fc 100644 --- a/structure/manifest.json +++ b/structure/manifest.json @@ -401,6 +401,77 @@ ] } ], + "contracts": { + "version": 1, + "entries": [ + { + "id": "paginated-history-writer", + "owner": { + "document": "codex-home.md", + "anchor": "paginated-history-writer-boundary" + }, + "dependents": [ + "catalog.md", + "config.md", + "gui-and-management-api.md", + "ops/docs-and-release.md", + "providers/openai-tiers.md", + "runtime.md", + "subagents.md" + ] + }, + { + "id": "request-copy-accounting", + "owner": { + "document": "transports/byte-accounting.md", + "anchor": "request-copy-accounting" + }, + "dependents": [ + "adapters/registry.md", + "catalog.md", + "clients/claude-desktop.md", + "clients/integrations.md", + "data-planes/images.md", + "data-planes/inbound-compat.md", + "ops/docs-and-release.md", + "ops/service-and-sidecars.md", + "overview.md", + "providers/chat-compat.md", + "providers/cursor.md", + "providers/xai-grok.md", + "runtime.md", + "subagents.md", + "transports/inventory.md", + "transports/streaming-health.md" + ] + }, + { + "id": "stream-buffer-accounting", + "owner": { + "document": "transports/byte-accounting.md", + "anchor": "stream-buffer-accounting" + }, + "dependents": [ + "adapters/registry.md", + "catalog.md", + "clients/claude-desktop.md", + "clients/integrations.md", + "data-planes/images.md", + "data-planes/inbound-compat.md", + "ops/docs-and-release.md", + "ops/service-and-sidecars.md", + "overview.md", + "providers/chat-compat.md", + "providers/cursor.md", + "providers/xai-grok.md", + "runtime.md", + "subagents.md", + "transports/inventory.md", + "transports/streaming-health.md" + ] + } + ] + }, "grace": { "undocumentedSourceAreas": [ { diff --git a/structure/ops/docs-and-release.md b/structure/ops/docs-and-release.md index 9ad63ed256f..d41f3ef01e3 100644 --- a/structure/ops/docs-and-release.md +++ b/structure/ops/docs-and-release.md @@ -165,6 +165,12 @@ invariants belong in `structure/`, not the README. manual. When an investigation graduates into a maintained invariant, summarize it here under `structure/` and link public workflows from `docs-site/`. +Cross-cutting structure contracts are maintained by editing `structure/manifest.json`, the authority +statement, and any dependent whose local explanation changes. Regenerate `structure/INDEX.md` with +the owning command and require the structure check in hosted CI. The +[structure rules](../AGENTS.md#the-source-to-doc-map) retain review of every document mapped to a +changed source area even when no text edit is needed. + ## Branch and devlog policy [`AGENTS.md`](../../AGENTS.md) and [`MAINTAINERS.md`](../../MAINTAINERS.md) are authoritative; this section @@ -412,9 +418,7 @@ its defaults and exclusions are owned by [Responses transport](../transports/res Provider configuration documents distinguish actual summaries from raw reasoning content. The test layout registers the summary-default contract cases and removes the obsolete content-rewrite test with its implementation. -## Paginated history writer boundary - -`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates refused restore/removal transitions. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. Apply retains an existing provider definition before candidate admission even when history preflight passes, so migration after artifact commit or during worker startup cannot leave earlier conversations without their provider. See the [history writer contract](../codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. +Paginated and migration-capable history follows the [authoritative writer contract](../codex-home.md#paginated-history-writer-boundary); this document adds no independent writer guarantee. Private pool credential metadata follows the [quota-history publication identity contract](../providers/openai-tiers.md#quota-history-publication-identity); credential-only and account DTO projections omit it. diff --git a/structure/overview.md b/structure/overview.md index dc28991c5ed..fb5b287d5e7 100644 --- a/structure/overview.md +++ b/structure/overview.md @@ -140,6 +140,11 @@ processes are one logical runner, while each process still installs its own isol guards. The workflow contract and process bounds live in [`ops/docs-and-release.md`](ops/docs-and-release.md#cross-platform-ci). +`structure/manifest.json` declares both source-review coverage and cross-cutting contract authority. +`scripts/structure-ssot.ts` validates that topology, and generated `structure/INDEX.md` publishes it. +The [structure rules](AGENTS.md#the-source-to-doc-map) define when review requires a content edit; +contract authority never reduces the source map's many-to-many review fan-out. + Two invariants are stated here without a binding, and `grace.unboundInvariants` in [`manifest.json`](manifest.json) carries the reason for each. They are true statements about the system; no test in this repository currently pins them, and saying so is more useful than naming a test that diff --git a/structure/providers/openai-tiers.md b/structure/providers/openai-tiers.md index 9ef2da5fa63..c9479b1b051 100644 --- a/structure/providers/openai-tiers.md +++ b/structure/providers/openai-tiers.md @@ -599,8 +599,7 @@ Listener startup diagnostics follow [the runtime lifecycle contract](../runtime. `src/codex/routing/selection.ts` applies optional `codexPool.excludedPlans` to both candidate selection and existing active/affined accounts. An all-excluded pool returns no automatic candidate, including preview and configured-account fallback. Native main remains exempt and unknown plans remain eligible. Explicit account-qualified routes retain pause, credential and entitlement checks while bypassing only this automatic policy. `src/codex/auth-api/account-list.ts` projects `selectionExcludedReason: "plan_excluded"` and `selectionExcludedPlan` from the routing config, even when a newer display-only WHAM plan could not be persisted. The dashboard and account CLI show the policy reason separately from credential health; renewal clears the derived fields. The automatic next-session action and badge are omitted for excluded rows. -## Paginated history writer boundary -`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates refused restore/removal transitions. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. Apply retains an existing provider definition before candidate admission even when history preflight passes, so migration after artifact commit or during worker startup cannot leave earlier conversations without their provider. See the [history writer contract](../codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. +Paginated and migration-capable history follows the [authoritative writer contract](../codex-home.md#paginated-history-writer-boundary); this document adds no independent writer guarantee. The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. diff --git a/structure/runtime.md b/structure/runtime.md index fc99bb89ded..983881c21fe 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -355,9 +355,7 @@ The relay is transparent in both directions, and that includes the close: a down `OCX_LIVE_FRAME_LOG` records both frame metadata and sideband lifecycle stages (`upstream-open`, `upstream-failed`, `relay-attached`, `relay-closed`) in one JSONL, content-free in both shapes. The lifecycle half is what separates a join that never reached this proxy from one whose upstream handshake was refused and from a live relay that carried nothing; frame records alone leave all three as an empty file. `tests/server/server-live-realtime-fixtures.test.ts` drives each sideband stage against de-identified Frameless v3 fixtures in `tests/fixtures/realtime-voice-sideband/` so a failure names the stage. -## Paginated history writer boundary - -`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates refused restore/removal transitions. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. Apply retains an existing provider definition before candidate admission even when history preflight passes, so migration after artifact commit or during worker startup cannot leave earlier conversations without their provider. See the [history writer contract](codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. +Paginated and migration-capable history follows the [authoritative writer contract](codex-home.md#paginated-history-writer-boundary); this document adds no independent writer guarantee. Codex pool settings and their consumers follow the [reset-first ordering contract](providers/openai-tiers.md#reset-first-account-ordering), including independent-quota fallback, preserved affinity, strategy-specific threshold summaries, and shared short-observation freshness for switch warnings. diff --git a/structure/subagents.md b/structure/subagents.md index 527b2b37523..a607de463e3 100644 --- a/structure/subagents.md +++ b/structure/subagents.md @@ -361,9 +361,7 @@ its defaults and exclusions are owned by [Responses transport](transports/respon Final-route summary visibility is recomputed after fallback from the original Responses preference; an earlier provider opt-in does not carry into a later provider. See [reasoning presentation](providers/chat-compat.md). -## Paginated history writer boundary - -`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates refused restore/removal transitions. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. Apply retains an existing provider definition before candidate admission even when history preflight passes, so migration after artifact commit or during worker startup cannot leave earlier conversations without their provider. See the [history writer contract](codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. +Paginated and migration-capable history follows the [authoritative writer contract](codex-home.md#paginated-history-writer-boundary); this document adds no independent writer guarantee. Codex pool settings and their consumers follow the [reset-first ordering contract](providers/openai-tiers.md#reset-first-account-ordering), including independent-quota fallback, preserved affinity, strategy-specific threshold summaries, and shared short-observation freshness for switch warnings. diff --git a/tests/ci-workflows/structure-ssot.test.ts b/tests/ci-workflows/structure-ssot.test.ts index 4783842bca1..6f3d3c7de5a 100644 --- a/tests/ci-workflows/structure-ssot.test.ts +++ b/tests/ci-workflows/structure-ssot.test.ts @@ -1,8 +1,8 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; -import { loadManifest, renderIndex, runStructureChecks, type Manifest } from "../../scripts/structure-ssot"; +import { loadManifest, renderIndex, runStructureChecks, writeGeneratedIndex, type Manifest } from "../../scripts/structure-ssot"; import { repoRoot } from "../helpers/repo-root"; /** @@ -82,6 +82,30 @@ function scaffold(): string { return root; } +/** Add one valid authority and a second doc that preserves many-to-many source review. */ +function contractScaffold(): string { + const root = scaffold(); + const manifest = manifestOf(root); + manifest.docs.push({ path: "second.md", tier: 1, title: "Second", scope: "second scope", documents: ["src/alpha/"] }); + manifest.contracts = { + version: 1, + entries: [ + { + id: "alpha-contract", + owner: { document: "overview.md", anchor: "non-negotiable-invariants" }, + dependents: ["second.md"], + }, + ], + }; + write( + root, + "structure/second.md", + "# Second\n\nAlpha uses " + BT + "src/alpha/keep.ts" + BT + ".\n\nSee the [alpha contract](overview.md#non-negotiable-invariants).\n", + ); + saveManifest(root, manifest); + return root; +} + const fires = (root: string, needle: string): void => { expect(runStructureChecks(root).join("\n")).toContain(needle); }; @@ -111,6 +135,12 @@ describe("structure/ SSOT", () => { expect(runStructureChecks(scaffold())).toEqual([]); }); + test("contracts are optional", () => { + const root = scaffold(); + expect(manifestOf(root).contracts).toBeUndefined(); + expect(runStructureChecks(root)).toEqual([]); + }); + test("a doc on disk that the manifest does not list", () => { const root = scaffold(); write(root, "structure/orphan.md", "# Orphan\n"); @@ -329,6 +359,140 @@ describe("structure/ SSOT", () => { expect(runStructureChecks(root)).toEqual([]); }); + test("contract manifest shapes fail with actionable errors", () => { + const valid = manifestOf(scaffold()); + const cases: [unknown, string][] = [ + [[], "contracts must be an object"], + [{ version: 2, entries: [] }, "contracts.version must be 1"], + [{ version: 1, entries: {} }, "contracts.entries must be an array"], + [{ version: 1, entries: [{ id: "Not Kebab", owner: {}, dependents: [] }] }, "id must be a kebab-case string"], + [{ version: 1, entries: [{ id: "valid-id", owner: null, dependents: [] }] }, "owner must be an object"], + [{ version: 1, entries: [{ id: "valid-id", owner: { document: 1, anchor: 2 }, dependents: [] }] }, "owner.document must be a string"], + [{ version: 1, entries: [{ id: "valid-id", owner: { document: "overview.md", anchor: "overview" }, dependents: {} }] }, "dependents must be an array"], + [{ version: 1, entries: [{ id: "valid-id", owner: { document: "overview.md", anchor: "overview" }, dependents: [1] }] }, "dependents[0] must be a string"], + ]; + for (const [contracts, needle] of cases) { + const loaded = loadManifest(JSON.stringify({ ...valid, contracts })); + expect(loaded).toHaveProperty("error"); + expect((loaded as { error: string }).error).toContain(needle); + } + }); + + test("duplicate contract ids are rejected", () => { + const root = contractScaffold(); + const manifest = manifestOf(root); + manifest.contracts!.entries.push({ + id: "alpha-contract", + owner: { document: "overview.md", anchor: "non-negotiable-invariants" }, + dependents: [], + }); + saveManifest(root, manifest); + fires(root, "contract alpha-contract is declared twice"); + }); + + test("contract owners must be declared files with real anchors", () => { + const undeclared = contractScaffold(); + const undeclaredManifest = manifestOf(undeclared); + undeclaredManifest.contracts!.entries[0]!.owner.document = "ghost.md"; + saveManifest(undeclared, undeclaredManifest); + fires(undeclared, "owner ghost.md is not a declared structure document"); + + const missing = contractScaffold(); + const missingManifest = manifestOf(missing); + missingManifest.docs.push({ path: "ghost.md", tier: 1, title: "Ghost", scope: "ghost", documents: [] }); + missingManifest.contracts!.entries[0]!.owner.document = "ghost.md"; + saveManifest(missing, missingManifest); + fires(missing, "owner structure/ghost.md is missing"); + + const anchor = contractScaffold(); + const anchorManifest = manifestOf(anchor); + anchorManifest.contracts!.entries[0]!.owner.anchor = "missing-heading"; + saveManifest(anchor, anchorManifest); + fires(anchor, "owner structure/overview.md has no #missing-heading heading anchor"); + }); + + test("contract dependents must be unique declared files distinct from the owner", () => { + const duplicate = contractScaffold(); + const duplicateManifest = manifestOf(duplicate); + duplicateManifest.contracts!.entries[0]!.dependents.push("second.md"); + saveManifest(duplicate, duplicateManifest); + fires(duplicate, "lists dependent second.md twice"); + + const owner = contractScaffold(); + const ownerManifest = manifestOf(owner); + ownerManifest.contracts!.entries[0]!.dependents = ["overview.md"]; + saveManifest(owner, ownerManifest); + fires(owner, "lists its owner overview.md as a dependent"); + + const undeclared = contractScaffold(); + const undeclaredManifest = manifestOf(undeclared); + undeclaredManifest.contracts!.entries[0]!.dependents = ["ghost.md"]; + saveManifest(undeclared, undeclaredManifest); + fires(undeclared, "dependent ghost.md is not a declared structure document"); + + const missing = contractScaffold(); + const missingManifest = manifestOf(missing); + missingManifest.docs.push({ path: "ghost.md", tier: 1, title: "Ghost", scope: "ghost", documents: [] }); + missingManifest.contracts!.entries[0]!.dependents = ["ghost.md"]; + saveManifest(missing, missingManifest); + fires(missing, "dependent structure/ghost.md is missing"); + }); + + test("a contract dependent must link the exact owner anchor", () => { + const root = contractScaffold(); + write(root, "structure/second.md", "# Second\n\nAlpha uses " + BT + "src/alpha/keep.ts" + BT + ".\n\nSee [Overview](overview.md).\n"); + fires(root, "dependent structure/second.md does not link overview.md#non-negotiable-invariants"); + }); + + test("a valid contract keeps many-to-many source review", () => { + const root = contractScaffold(); + expect(runStructureChecks(root)).toEqual([]); + expect(renderIndex(manifestOf(root))).toContain( + "| " + BT + "src/alpha/" + BT + " | [" + BT + "overview.md" + BT + "](overview.md)
[" + BT + "second.md" + BT + "](second.md) |", + ); + }); + + test("contract navigation has deterministic ordering and placement", () => { + const root = contractScaffold(); + const manifest = manifestOf(root); + manifest.docs.push({ path: "third.md", tier: 1, title: "Third", scope: "third scope", documents: [] }); + manifest.contracts!.entries = [ + { + id: "zeta-contract", + owner: { document: "overview.md", anchor: "non-negotiable-invariants" }, + dependents: [], + }, + { + id: "alpha-contract", + owner: { document: "overview.md", anchor: "non-negotiable-invariants" }, + dependents: ["third.md", "second.md"], + }, + ]; + const rendered = renderIndex(manifest); + const contracts = rendered.indexOf("## Cross-cutting contracts"); + const decisions = rendered.indexOf("## Decision records"); + const alpha = rendered.indexOf("| " + BT + "alpha-contract" + BT + " |"); + const zeta = rendered.indexOf("| " + BT + "zeta-contract" + BT + " |"); + expect(contracts).toBeGreaterThan(rendered.indexOf("### Not described by any doc")); + expect(decisions).toBeGreaterThan(contracts); + expect(alpha).toBeGreaterThan(contracts); + expect(zeta).toBeGreaterThan(alpha); + expect(rendered.slice(alpha, zeta)).toContain( + "[" + BT + "second.md" + BT + "](second.md)
[" + BT + "third.md" + BT + "](third.md)", + ); + expect(rendered).toContain( + "[" + BT + "overview.md#non-negotiable-invariants" + BT + "](overview.md#non-negotiable-invariants)", + ); + expect(rendered.slice(zeta, decisions)).toContain("| — |"); + }); + + test("contract checks validate topology rather than owner prose", () => { + const root = contractScaffold(); + const owner = readFileSync(join(root, "structure/overview.md"), "utf8").replace("alpha keeps working", "alpha remains available"); + write(root, "structure/overview.md", owner); + expect(runStructureChecks(root)).toEqual([]); + }); + test("a malformed manifest is an actionable failure, not a stack trace", () => { expect(loadManifest("{not json")).toHaveProperty("error"); const shapeless = loadManifest(JSON.stringify({ sizeBudgetLines: 600 })); @@ -401,4 +565,71 @@ describe("structure/ SSOT", () => { write(root, "structure/manifest.json", JSON.stringify(manifest, null, 2) + "\n"); fires(root, "absentPaths[0].path must be a string"); }); + + test("index generation validates the manifest before writing", () => { + const root = scaffold(); + const before = readFileSync(join(root, "structure/INDEX.md"), "utf8"); + const manifest = manifestOf(root) as unknown as { contracts: unknown }; + manifest.contracts = { version: 1, entries: "not-an-array" }; + write(root, "structure/manifest.json", JSON.stringify(manifest, null, 2) + "\n"); + const result = writeGeneratedIndex(root); + expect(result).toHaveProperty("error"); + expect((result as { error: string }).error).toContain("contracts.entries must be an array"); + expect(readFileSync(join(root, "structure/INDEX.md"), "utf8")).toBe(before); + }); + + test("index generation rejects a malformed contract entry without a renderer crash", () => { + const root = scaffold(); + const before = readFileSync(join(root, "structure/INDEX.md"), "utf8"); + const manifest = manifestOf(root) as unknown as { contracts: unknown }; + // A bare string entry used to reach the renderer's id comparator and throw a TypeError. + manifest.contracts = { version: 1, entries: ["oops"] }; + write(root, "structure/manifest.json", JSON.stringify(manifest, null, 2) + "\n"); + const result = writeGeneratedIndex(root); + expect(result).toHaveProperty("error"); + expect((result as { error: string }).error).toContain("id must be a kebab-case string"); + expect(readFileSync(join(root, "structure/INDEX.md"), "utf8")).toBe(before); + }); + + // The seam-level tests above prove the helper; these two drive the real CLI tail, which is + // where the original bug lived (parse + cast + render before validation). The script is + // copied into the scaffold so its import.meta.dir resolves to the synthetic root. + function scaffoldCli(root: string): void { + mkdirSync(join(root, "scripts"), { recursive: true }); + copyFileSync(join(repoRoot(), "scripts/structure-ssot.ts"), join(root, "scripts/structure-ssot.ts")); + } + + test("CLI --fix fails malformed contracts with a named diagnostic and an unchanged index", () => { + const root = scaffold(); + scaffoldCli(root); + const before = readFileSync(join(root, "structure/INDEX.md"), "utf8"); + const manifest = manifestOf(root) as unknown as { contracts: unknown }; + manifest.contracts = { version: 1, entries: "not-an-array" }; + write(root, "structure/manifest.json", JSON.stringify(manifest, null, 2) + "\n"); + const result = Bun.spawnSync([process.execPath, "scripts/structure-ssot.ts", "--fix"], { + cwd: root, + stdout: "pipe", + stderr: "pipe", + }); + expect(result.exitCode).not.toBe(0); + const stderr = result.stderr.toString(); + expect(stderr).toContain("contracts.entries must be an array"); + expect(stderr).not.toContain("TypeError"); + expect(readFileSync(join(root, "structure/INDEX.md"), "utf8")).toBe(before); + }); + + test("CLI --fix regenerates a stale index for a valid manifest", () => { + const root = scaffold(); + scaffoldCli(root); + write(root, "structure/INDEX.md", "# stale\n"); + const result = Bun.spawnSync([process.execPath, "scripts/structure-ssot.ts", "--fix"], { + cwd: root, + stdout: "pipe", + stderr: "pipe", + }); + expect(result.exitCode).toBe(0); + const regenerated = readFileSync(join(root, "structure/INDEX.md"), "utf8"); + expect(regenerated).not.toBe("# stale\n"); + expect(regenerated).toBe(renderIndex(manifestOf(root))); + }); });