/** * Audience gating for the emitted `CLAUDE.md`. * * ## Why this module exists * * `project-creator` scaffolds for outside customers on their own GitHub * accounts, so framework bytes already leave the building. Measured on a real * customer-shaped scaffold (2026-08-14, session `-20260814b`), the emitted * `CLAUDE.md` was **80.6 KB** and partitioned: * * | bucket | bytes | share | * |---|---|---| * | genuinely proprietary (the ruling set + the Vault index) | 40,251 | 50% | * | the framework's method (3-layer, the session ceremony) | 19,213 | 24% | * | generic scaffolding (semver, tool notes) | 20,542 | 26% | * * **Kerry ruled Option A on 2026-08-14: "ship the artifact, not the doctrine."** * The emitted *code* is the value a customer buys and is untouched by this * module. The prose corpus explaining which internal rulings produced that code * is what a competitor would need, and it is not emitted to `external`. * * Full pros/cons and the measurement: `DECISION-BRIEF-2026-08-14.md`. * * ## Why whole SECTIONS, and never edited sentences * * **Omitting a fingerprinted block is legitimate where EDITING one is not.** * A section that is never emitted makes no identity claim. A section that is * emitted with four sentences quietly removed still carries a fingerprint * asserting it is canonical, which lies to every drift checker downstream — and * an emitter mints that lie into every future project rather than being one bad * file. That is why v0.16.0 refused to redact private repo names in place, and * why this module only ever removes whole `## ` sections. * * The one exception is {@link stripVaultPointers}, which rewrites `[[skill]]` * link syntax into plain text *inside the sections that survive*. That is safe * for the same reason the section drop is: it removes a pointer the reader * cannot follow, and it runs only on blocks this repo itself generates * (`src/standards.ts`), never on a fingerprinted body embedded from * `cursor-project-templates`. */ import type { Audience, ScaffoldInputs } from "./index.js"; /** * Resolve the audience for a set of inputs. Centralised so the default lives in * exactly one place — every call site reads `internal` for an omitted value, and * a future change of default is a one-line edit rather than a grep. */ export declare function resolveAudience(inputs: Pick): Audience; /** * The `## ` sections dropped when `audience === "external"`, each with the * reason it is doctrine rather than deliverable. * * Headings are matched on the text AFTER `## `, case-sensitively, ignoring any * trailing qualifier — so `## Standards block (inserted inline per skill v1.1.0)` * matches the `Standards block` entry. Matching a prefix rather than the whole * line is deliberate: these headings carry version qualifiers that move, and a * gate that silently stops matching on a version bump would re-leak the section * with nothing going red. */ export declare const EXTERNAL_DROPPED_SECTIONS: readonly { readonly heading: string; readonly why: string; /** * True when this section is only CONDITIONALLY emitted upstream, so its * absence is a legitimate state rather than evidence of a renamed heading. * * Only such sections are exempt from the not-found refusal. Everything else * must be found, because for an unconditionally-emitted section "absent" can * only mean the gate stopped matching — and a gate that stopped matching * produces a customer scaffold still carrying the doctrine, with every signal * green. Caught by tier-1 verification on a real scaffold, not by the unit * suite, whose fixture contained all six sections (2026-08-14). */ readonly conditionallyPresent?: boolean; /** * The canonical AGENTS body version that FIRST shipped this heading, for * sections sourced from Templates rather than generated by this CLI. * * 🔴 **This exists because "absent" had two causes and the error named only * one of them.** `project-creator` resolves Templates from a version-PINNED * package while this CLI resolves it from Kerry's disk, so this repo never * sees version skew and its first real consumer sees nothing else. Pinned at * AGENTS v2.15.0, `## Keeping This Framework Current` simply did not exist * yet — the gate threw, said *"a heading was probably renamed upstream"*, and * sent them hunting a rename that never happened while every outside-brand * scaffold refused in their production (filed 2026-08-19). * * Omitted for sections this CLI itself emits (`Standards block`, * `Capture Learnings`, `Agent Coordination`) — those are spliced in from * `src/standards.ts` markers and are present at every Templates version, so * for them "absent" really can only mean the gate stopped matching. * * The throw is deliberately NOT softened. A section missing because the * Templates source predates it is genuinely safe — the doctrine is not in the * document to leak — but "safe" is not the same as "supported", and shipping a * customer scaffold from a Templates version this gate was never written * against is a claim nobody verified. It still refuses; it just now refuses * with the true cause and an actionable fix. */ readonly sinceTemplatesVersion?: string; }[]; /** * The lowest canonical AGENTS body version this gate can serve — the highest * `sinceTemplatesVersion` among the sections it must drop. * * Derived, never hand-written: adding a section sourced from a newer Templates * body raises this automatically, so the declared floor cannot drift from the * list it describes. */ export declare const GATE_MIN_TEMPLATES_VERSION: string; /** Why a section the gate must drop was not found in the document. */ export type NotFoundCause = "version-skew" | "unexplained"; /** A section the gate had to drop and could not find, with the likely cause. */ export interface UnfoundSection { readonly heading: string; /** * `version-skew` — the Templates body in use predates the heading, so the * section is legitimately absent and the fix is upgrading the Templates * source. `unexplained` — the heading should be there, so it was renamed * upstream or the body changed shape, and the fix is `EXTERNAL_DROPPED_SECTIONS`. */ readonly cause: NotFoundCause; /** Present only for `version-skew`: the version that first shipped the heading. */ readonly sinceTemplatesVersion?: string; } /** * Classify each not-found heading against the resolved Templates version. * * `templatesVersion` is the `X.Y.Z` parsed off the canonical `AGENTS_v*.md` * filename — the same value that fills `{{framework_version}}`. Pass `undefined` * when it genuinely is not known; every finding then reads `unexplained`, which * is the pre-existing behaviour and the safe direction to fail in. */ export declare function classifyUnfoundSections(notFound: readonly string[], templatesVersion: string | undefined): UnfoundSection[]; /** What {@link gateSectionsForAudience} did, so callers can report rather than guess. */ export interface AudienceGateResult { /** The gated document. */ readonly content: string; /** Headings actually removed, in document order. */ readonly dropped: readonly string[]; /** * Configured headings that were NOT found in the document. * * **Never silently empty-passes.** A section this gate is supposed to drop * but cannot find means either the heading was renamed upstream or the body * changed shape — in both cases the section is still being emitted and the * gate believes it did its job. Callers surface this; `writeScaffold` refuses. */ readonly notFound: readonly string[]; /** Bytes removed. */ readonly bytesDropped: number; /** * Surviving lines that still refer to a section this gate removed — e.g. the * framework body's *"run the routing decision in the `## Capture Learnings * (session end)` block below"*, when that block is no longer below. * * **Reported, never auto-edited.** Rewriting a sentence inside the canonical * AGENTS body is precisely the modification this module refuses to do: a body * emitted with sentences quietly altered still carries a fingerprint claiming * it is canonical. So the honest handling is to surface the incoherence and * let a human decide, rather than either fixing it invisibly or shipping an * external scaffold that points at sections it does not contain and saying * nothing. */ readonly danglingReferences: readonly { readonly heading: string; /** 1-indexed line number in the GATED document. */ readonly line: number; }[]; /** * Surviving lines that INSTRUCT the reader to use a private Z2W resource. * * 🔴 **A different class from {@link danglingReferences}, and worse.** A * dangling reference points at a section that is not there — the reader * follows it, finds nothing, shrugs. A private reference tells them to *go do * something* in a library they cannot open: `## Self-annealing loop` still * says *"capture it in the Skill Vault"*, which is the same class as the * private-bulletin-repo leak v0.16.0 fixed rather than mere disclosure. * Naming a skill in passing is the accepted posture; instructing someone into * the Vault is not. (Filed by `project-creator` 2026-08-19, who ranked this * the one worth prioritising out of 12 residual files.) * * **Reported, never auto-edited**, for the same reason as the rest of this * module: the sentence lives inside the canonical AGENTS body, and a body * emitted with sentences quietly altered still carries a fingerprint claiming * it is canonical. The durable fix is upstream in `cursor-project-templates`; * this makes the residual visible instead of silent in the meantime. * * Measured on a real customer-shaped `nextjs` scaffold against Templates * v2.18.0 (2026-08-20): **exactly one hit across all 42 emitted files**, so * this does not cry wolf. */ readonly privateReferences: readonly { /** The private resource named, e.g. `"Skill Vault"`. */ readonly resource: string; /** 1-indexed line number in the GATED document. */ readonly line: number; /** The surviving line, trimmed, so a consumer can show it without re-reading the file. */ readonly text: string; }[]; } /** * Remove whole `## ` sections from an assembled `CLAUDE.md` for an external * audience. A no-op for `internal`, byte-for-byte. * * A section runs from its `## ` heading to the next `## ` heading (or EOF). * `###`/`####` subheadings belong to their parent section and travel with it. * * Fenced code blocks are respected: a ```` ``` ```` fence can legitimately * contain a line starting with `## ` (a markdown example), and treating that as * a section boundary would truncate the wrong span. Same fence-blindness bug * `z2w-agent-coordination` hit in their own section tooling on 2026-08-13. */ export declare function gateSectionsForAudience(content: string, audience: Audience): AudienceGateResult; /** * Re-state the standards decision log against what the audience gate actually * removed, so a consumer rendering that log cannot claim a standard is in a * file the gate emptied of it. * * Driven by the gate's own `dropped` array rather than by `audience` — if the * gate did not remove the section (an external scaffold with * `participateInBulletin: false` never has an `## Agent Coordination` section * to drop), the decision is left exactly as the resolver wrote it. * * Pure and total: returns a new array, never mutates, and is a no-op when * `dropped` is empty — which is every `internal` scaffold. */ export declare function applyGateToDecisions(decisions: readonly D[], dropped: readonly string[]): D[]; /** * Rewrite `[[skill-name]]` Vault pointers into plain text. * * An outside reader cannot open the Skill Vault, so a `[[…]]` link is at best * noise and at worst an invitation to go looking for something private. The * skill NAME is left visible as ordinary prose rather than deleted, because the * surrounding sentence is often written around it and removing the token * outright produces broken grammar. * * A no-op for `internal`. */ export declare function stripVaultPointers(content: string, audience: Audience): string;