diff --git a/.dv/records/olive-crabs-wade.md b/.dv/records/olive-crabs-wade.md new file mode 100644 index 0000000..703851d --- /dev/null +++ b/.dv/records/olive-crabs-wade.md @@ -0,0 +1,14 @@ +--- +type: feat! +packages: + - '@dv-cli/clipc' +notes: >- + rawCliErrorPayloadSchema and rawCliErrorEnvelopeSchema are no longer exported from the package + surface — Zod is now an implementation detail of @dv-cli/clipc. Consumers validate error envelopes + with the new parseCliErrorEnvelope / safeParseCliErrorEnvelope functions, which return plain + (Zod-free) results (CliErrorEnvelopeParseResult). The RawCliErrorEnvelope / RawCliErrorPayload + contract interfaces are unchanged. The raw schema remains available to the in-repo JSON Schema + generator via the ./internal/error-schema subpath. +--- + +Stop exporting the raw Zod error schemas; validate via Zod-free parser functions instead diff --git a/.dv/records/patient-lamps-soar.md b/.dv/records/patient-lamps-soar.md new file mode 100644 index 0000000..afa7b20 --- /dev/null +++ b/.dv/records/patient-lamps-soar.md @@ -0,0 +1,15 @@ +--- +type: feat +packages: + - '@dv-cli/dv' +notes: >- + Adds JSDoc to every exported symbol on @dv-cli/dv's library entry (runners, option/result + interfaces, Plan contract). Re-exports the domain types reachable through the public option/result + shapes (ChangeType, Package, PluginReference, PluginAssignment, ResolvedPlugin, SlugRandomSource, + ConfigMigrationStepResult, MigrationChange, PluginConstraintUpdate, PLUGIN_OP_NAMES) so they can + be imported by name. The Plan contract types are now hand-written interfaces instead of z.infer + aliases, keeping Zod's internal types off the public surface. deno doc --lint is now a hard CI + gate. +--- + +Document the entire public library surface with JSDoc and re-export the domain and contract types it references diff --git a/.github/workflows/dv-validate.yml b/.github/workflows/dv-validate.yml index 05ebdb2..67b999b 100644 --- a/.github/workflows/dv-validate.yml +++ b/.github/workflows/dv-validate.yml @@ -98,13 +98,11 @@ jobs: - name: deno publish --dry-run (slow-types gate, both packages) run: deno task publish:check - # Doc-lint surfaces missing JSDoc on the public surface. Soft for - # now (continue-on-error): the library surface has a large JSDoc - # backlog (issue #29) and we don't want to block merges on it — - # but the count should stay visible and trend down. Flip to a hard - # gate once #29 lands. - - name: deno doc --lint (non-blocking) - continue-on-error: true + # Doc-lint enforces JSDoc on the public surface of @dv-cli/dv and + # @dv-cli/clipc. Hard gate as of #29: the backlog is cleared, so any + # new undocumented public symbol (or a private-type leak into the + # surface) fails the build rather than merely trending a count. + - name: deno doc --lint run: deno task doc:lint # === dv-specific gates === diff --git a/apps/cli/src/cli/add.ts b/apps/cli/src/cli/add.ts index 371ae64..a102673 100644 --- a/apps/cli/src/cli/add.ts +++ b/apps/cli/src/cli/add.ts @@ -27,34 +27,57 @@ import { openEditorForRecordBody } from "./editor.ts"; // an interactive TTY flow with prompts + $EDITOR. The two paths share // validation: known packages, allowed Change Type, non-empty body. +/** + * Inputs to {@link runAdd}. Non-interactive fields mirror the `dv add` CLI + * flags; any left unset in a TTY context are filled by interactive prompts, + * while a non-TTY context requires `changeType`, `packageNames`, and + * `message`. + */ export interface RunAddOptions { - // Non-interactive inputs (CLI flags). Any of these missing in a TTY - // context will be filled by interactive prompts; in a non-TTY context, - // type / packages / message are required. + /** Record Change Type (`feat`, `fix`, `feat!`, `fix!`). */ changeType?: ChangeType; + /** Target Package names the Record applies to. */ packageNames?: string[]; + /** One-line Record summary; suppresses the `$EDITOR` body flow. */ message?: string; + /** Reference links (issues, PRs) recorded in the Record. */ links?: string[]; + /** Free-form notes appended to the Record body. */ notes?: string; - // Override `records.auto-stage` for this invocation. + /** Override `records.auto-stage` for this invocation. */ stageOverride?: boolean; - // Override $EDITOR / $VISUAL for this invocation. Honored only when - // the editor template is actually launched (i.e. no --message). Same - // POSIX-shell parsing as the env var. + /** + * Override `$EDITOR` / `$VISUAL` for this invocation. Honored only when the + * editor template is actually launched (i.e. no `message`). Same POSIX-shell + * parsing as the env var. + */ editorOverride?: string; - // Test seam: deterministic slug generation. + /** Test seam: deterministic slug generation. */ slugRandomSource?: SlugRandomSource; } +/** Outcome of a successful {@link runAdd}: the Record file it wrote. */ export interface RunAddResult { + /** Absolute path to the created Record file under `.dv/records/`. */ recordPath: string; + /** Absolute path to the repository root. */ repoRootPath: string; + /** Whether the new Record was git-staged (per `records.auto-stage`). */ staged: boolean; } +/** + * Authors a single Record file under `.dv/records/` from {@link RunAddOptions}, + * via flag inputs (CI / scripts / agents) or an interactive TTY flow with + * prompts and `$EDITOR`. Both paths share validation: known Packages, an + * allowed Change Type, and a non-empty body. + * + * @param options Record inputs and per-invocation overrides. + * @returns The written Record's path and staging state. + */ export async function runAdd(options: RunAddOptions): Promise { const repoRootPath = await requireRepoRoot(); const configFilePath = configPath(repoRootPath); diff --git a/apps/cli/src/cli/init.ts b/apps/cli/src/cli/init.ts index 550240e..39359db 100644 --- a/apps/cli/src/cli/init.ts +++ b/apps/cli/src/cli/init.ts @@ -31,13 +31,23 @@ const STARTER_CHANGELOG_GITIGNORE = `# In-progress record edit files. Normally c .dv-record-edit-* `; +/** Result of a {@link runInit} run, reporting which scaffold files were created. */ export interface InitResult { + /** Absolute path to the git repo root that was scaffolded. */ repoRoot: string; + /** Whether `.dv/config.yaml` was newly written (false if it already existed). */ configCreated: boolean; + /** Whether the Records directory was newly created. */ recordsDirCreated: boolean; + /** Whether `.dv/.gitignore` was newly written. */ gitignoreCreated: boolean; } +/** + * `dv init`: scaffolds the `.dv/` directory — a starter `config.yaml`, the + * Records directory, and a `.gitignore` — at the git repo root. Existing + * files are left untouched. + */ export async function runInit(): Promise { const repoRoot = await requireRepoRoot(); const cfgPath = configPath(repoRoot); diff --git a/apps/cli/src/cli/migrate.ts b/apps/cli/src/cli/migrate.ts index 36177c5..b5a1a41 100644 --- a/apps/cli/src/cli/migrate.ts +++ b/apps/cli/src/cli/migrate.ts @@ -7,30 +7,40 @@ import { import { requireRepoRoot } from "../subtools/git/mod.ts"; import { makeStyler } from "./styler.ts"; -// `dv migrate config` per specs/config-format.md § Migrating from -// the pre-1.0 string form. Reads `.dv/config.yaml`, runs every -// registered migration step from the config-migrations subtool, -// and writes the result back (unless --dry-run). The subtool owns -// the per-step logic; this file is a thin orchestration over it, -// matching the architectural pattern of every other cli/*.ts. -// -// Each future breaking config change ships its own migration step -// in `subtools/config-migrations/step-*.ts`. This command stays -// the single user-facing entrypoint for all of them. - +/** + * Inputs to {@link runMigrateConfig}, the `dv migrate config` orchestration + * per `specs/config-format.md` § Migrating from the pre-1.0 string form. + */ export interface RunMigrateConfigOptions { + /** Compute the migration without writing `.dv/config.yaml` back. */ dryRun: boolean; + /** Emit a machine-readable JSON summary instead of human output. */ emitJson: boolean; + /** Whether to apply ANSI color to human-readable output. */ colorEnabled: boolean; } +/** + * Outcome of {@link runMigrateConfig}: the rewritten `.dv/config.yaml` plus + * per-step results from the config-migrations registry. + */ export interface RunMigrateConfigResult { + /** Absolute path to the `.dv/config.yaml` that was read. */ configFilePath: string; + /** `true` when no registered step changed anything (config already current). */ alreadyMigrated: boolean; + /** Per-step results in registry order; each records the changes it made. */ stepResults: ConfigMigrationStepResult[]; + /** `true` when the rewritten text was persisted (never set under `dryRun`). */ fileWritten: boolean; } +/** + * Runs `dv migrate config`: reads `.dv/config.yaml`, applies every registered + * config-migration step (text-in/text-out so user comments survive), and + * writes the result back unless `dryRun`. A thin orchestration over the + * config-migrations subtool, which owns the per-step logic. + */ export async function runMigrateConfig( options: RunMigrateConfigOptions, ): Promise { diff --git a/apps/cli/src/cli/plugin-invoke.ts b/apps/cli/src/cli/plugin-invoke.ts index e06a649..1580e94 100644 --- a/apps/cli/src/cli/plugin-invoke.ts +++ b/apps/cli/src/cli/plugin-invoke.ts @@ -31,6 +31,11 @@ import { makeStyler } from "./styler.ts"; const DEFAULT_INVOKE_TIMEOUT_MS = 60_000; +/** + * The closed set of plugin Op names `dv plugin invoke` can exercise, in + * pipeline order. The source of truth for {@link PluginOpName}; consumers + * iterate it to enumerate the contract surface (see `specs/plugin-contract.md`). + */ export const PLUGIN_OP_NAMES = [ "info", "discover", @@ -42,46 +47,75 @@ export const PLUGIN_OP_NAMES = [ "finalize", ] as const; +/** Name of a plugin Op `dv plugin invoke` can exercise (`info`, `discover`, etc.). */ export type PluginOpName = (typeof PLUGIN_OP_NAMES)[number]; +/** Narrow an arbitrary string to a {@link PluginOpName} type guard. */ export function isPluginOpName(value: string): value is PluginOpName { return (PLUGIN_OP_NAMES as readonly string[]).includes(value); } +/** Inputs to {@link runPluginInvoke}: which plugin and Op to exercise plus the env/stdin context dv would supply. */ export interface RunPluginInvokeOptions { + /** Plugin positional from the CLI (path or configured plugin reference). */ pluginPositional: string; + /** Op to invoke on the resolved plugin. */ opName: PluginOpName; + /** Package name to pass as Op context (sets `DV_PACKAGE_NAME`). */ packageName?: string; + /** Package directory to pass as Op context (sets `DV_PACKAGE_PATH`). */ packagePath?: string; + /** Repo root to pass as Op context (sets `DV_REPO_ROOT`). */ repoRoot?: string; + /** Discovery glob to pass to a `discover` Op. */ discoverGlob?: string; + /** New Version string for a `write-version` Op (sets `DV_NEW_VERSION`). */ newVersion?: string; + /** Git Tag to pass as Op context (sets `DV_GIT_TAG`). */ gitTag?: string; - // finalize-only inputs: --trigger flips DV_FINALIZE_TRIGGER - // ("version" or "v1"); --bumped-packages is the literal JSON - // payload dv would have built from the run's plan. For an - // ad-hoc debug invocation, the user may pass any well-formed - // value (e.g. `[]` to simulate "nothing bumped"). + /** + * finalize-only inputs: `--trigger` flips `DV_FINALIZE_TRIGGER` + * (`"version"` or `"v1"`); `--bumped-packages` is the literal JSON + * payload dv would have built from the run's Plan. For an + * ad-hoc debug invocation, the user may pass any well-formed + * value (e.g. `[]` to simulate "nothing bumped"). + */ finalizeTrigger?: "version" | "v1"; + /** Literal JSON payload for `finalize`'s bumped-Packages input. */ bumpedPackagesJson?: string; + /** Raw JSON to feed the plugin on stdin, overriding the default payload. */ stdinJson?: string; + /** Per-invocation timeout in milliseconds. */ timeoutMs?: number; + /** Emit the machine-readable `--json` result instead of human output. */ emitJson: boolean; + /** Whether ANSI color is enabled for human output. */ colorEnabled: boolean; + /** Emit `--debug` stdio tracing to stderr. */ debug?: boolean; } +/** Outcome of {@link runPluginInvoke}: the resolved plugin, the full stdio exchange, and the conformance verdict. */ export interface RunPluginInvokeResult { + /** Absolute path the plugin reference resolved to. */ resolvedPluginPath: string; + /** Op that was invoked. */ opName: PluginOpName; + /** Environment variables passed to the plugin process. */ environmentVariables: Record; + /** Payload written to the plugin's stdin, or `undefined` if none. */ stdinPayload: string | undefined; + /** Raw stdout captured from the plugin. */ rawStdout: string; + /** Raw stderr captured from the plugin. */ rawStderr: string; + /** Plugin response parsed from stdout against the per-Op Zod schema. */ parsedResponse: unknown; + /** Whether the response conformed to the per-Op response schema. */ conformant: boolean; } +/** Run a single plugin Op via JSON-over-stdio and return the full exchange and conformance verdict. */ export async function runPluginInvoke( options: RunPluginInvokeOptions, ): Promise { diff --git a/apps/cli/src/cli/plugin-list.ts b/apps/cli/src/cli/plugin-list.ts index 4163919..553b5bb 100644 --- a/apps/cli/src/cli/plugin-list.ts +++ b/apps/cli/src/cli/plugin-list.ts @@ -29,33 +29,58 @@ import { makeStyler } from "./styler.ts"; // `dv plugin verify ` is the deeper, per-plugin contract // check; `dv plugin list` is the shallower, whole-config audit. +/** Inputs to {@link runPluginList}, mirroring the `dv plugin list` flags. */ export interface RunPluginListOptions { + /** Emit machine-readable `--json` instead of the human-readable table. */ emitJson: boolean; + /** Whether ANSI color is enabled for human-readable output. */ colorEnabled: boolean; + /** Emit per-plugin stdio tracing to stderr (`--debug`). */ debug?: boolean; } +/** Outcome of auditing one plugin assignment: resolved, or failed to resolve/discover. */ export type PluginListEntryStatus = "ok" | "resolve-failed" | "discover-failed"; +/** One plugin assignment's audit result — what it resolved to and which Packages it claims. */ export interface PluginListEntry { + /** Position of this assignment in the config `plugins` list. */ assignmentIndex: number; + /** The plugin {@link PluginReference} this assignment points at. */ pluginReference: PluginReference; + /** Stable identity key for {@link PluginListEntry.pluginReference}. */ pluginReferenceKey: string; + /** Globs the assignment matches Packages against. */ matchGlobs: string[]; + /** Whether resolution and discovery succeeded for this assignment. */ status: PluginListEntryStatus; + /** Filesystem path the plugin resolved to, when resolution succeeded. */ resolvedPluginPath?: string; + /** Kind of the resolved plugin ({@link ResolvedPlugin}), when resolution succeeded. */ resolvedPluginKind?: ResolvedPlugin["kind"]; + /** Packages this plugin claimed during discovery. */ packages: Package[]; + /** Stable {@link DvError} code, when this entry failed. */ errorCode?: string; + /** Human-readable failure detail, when this entry failed. */ errorMessage?: string; } +/** Aggregate result of `dv plugin list`: per-assignment {@link PluginListEntry} rows plus a failure flag. */ export interface RunPluginListResult { + /** Absolute path of the repository root the audit ran against. */ repoRootPath: string; + /** One {@link PluginListEntry} per config plugin assignment. */ entries: PluginListEntry[]; + /** True if any entry has a non-`ok` {@link PluginListEntryStatus}. */ hasFailures: boolean; } +/** + * Run the `dv plugin list` audit: load config, resolve each plugin assignment, + * and discover the Packages each claims. Read-only — no write-side plugin ops + * fire, and per-assignment failures are surfaced as entries rather than thrown. + */ export async function runPluginList( options: RunPluginListOptions, ): Promise { diff --git a/apps/cli/src/cli/plugin-verify.ts b/apps/cli/src/cli/plugin-verify.ts index 6f948f8..3e8efaf 100644 --- a/apps/cli/src/cli/plugin-verify.ts +++ b/apps/cli/src/cli/plugin-verify.ts @@ -35,32 +35,57 @@ const DEFAULT_VERIFY_TIMEOUT_MS = 60_000; const DEFAULT_VERIFY_GLOB = "*"; const BOGUS_OP_NAME = "__dv_plugin_verify_bogus__"; +/** Inputs to {@link runPluginVerify}, mirroring `dv plugin verify`'s flags. */ export interface RunPluginVerifyOptions { + /** Plugin positional: a name to resolve, or a path to the executable. */ pluginPositional: string; + /** Repo root the plugin resolves against; defaults to `Deno.cwd()`. */ repoRoot?: string; + /** Glob passed to the `discover` Op; defaults to `*`. */ discoverGlob?: string; + /** Per-Op timeout in milliseconds; defaults to 60s. */ timeoutMs?: number; + /** Emit machine-readable JSON instead of the human summary. */ emitJson: boolean; + /** Whether ANSI color is enabled for human output and trace lines. */ colorEnabled: boolean; + /** Trace each plugin invocation to stderr. */ debug?: boolean; } +/** Result of conformance-checking one Op against its schema. */ export type CheckOutcome = "pass" | "fail" | "skipped"; +/** One Op's conformance check in the verify run. */ export interface CheckReport { + /** Op name (or synthetic check, e.g. `info`, the bogus-op probe). */ name: string; + /** Whether the Op conformed, failed, or was skipped as side-effectful. */ outcome: CheckOutcome; + /** Human-readable explanation of the {@link outcome}. */ detail: string; } +/** Aggregate outcome of a `dv plugin verify` run. */ export interface RunPluginVerifyResult { + /** Absolute path of the resolved plugin executable. */ resolvedPluginPath: string; + /** Per-Op {@link CheckReport} entries, in execution order. */ checks: CheckReport[]; + /** Count of checks with a `pass` {@link CheckOutcome}. */ passedCount: number; + /** Count of checks with a `fail` {@link CheckOutcome}. */ failedCount: number; + /** Count of checks with a `skipped` {@link CheckOutcome}. */ skippedCount: number; } +/** + * Conformance-check a plugin against the versioned per-Op schemas in + * `specs/schemas/plugin-responses.json`. Backs `dv plugin verify`: invokes + * `info`, exercises safe Ops end-to-end, skips side-effectful Ops, and + * asserts a bogus Op name fails loudly. See {@link RunPluginVerifyOptions}. + */ export async function runPluginVerify( options: RunPluginVerifyOptions, ): Promise { diff --git a/apps/cli/src/cli/release.ts b/apps/cli/src/cli/release.ts index d8a15d8..a11817c 100644 --- a/apps/cli/src/cli/release.ts +++ b/apps/cli/src/cli/release.ts @@ -49,42 +49,74 @@ import { makeStyler } from "./styler.ts"; const DEFAULT_FAST_OP_TIMEOUT_MS = 60_000; +/** Options controlling a {@link runRelease} invocation. */ export interface RunReleaseOptions { - // Tri-state: undefined → resolve from `safety.dry-run-by-default` - // config, then false. + /** + * Tri-state: `undefined` → resolve from `safety.dry-run-by-default` + * config, then `false`. + */ dryRun?: boolean; - // Re-run the release Op for already-tagged Packages (failed-publish - // recovery per specs/cli.md § dv release --force). + /** + * Re-run the release Op for already-tagged Packages (failed-publish + * recovery per `specs/cli.md` § `dv release --force`). + */ force: boolean; - // Tri-state: undefined → resolve from `git.auto-push` config. + /** Tri-state: `undefined` → resolve from `git.auto-push` config. */ push?: boolean; + /** Skip the TTY confirmation prompt; required in non-TTY contexts. */ yes: boolean; + /** Emit the {@link RunReleaseResult} as JSON instead of human-readable output. */ emitJson: boolean; + /** Whether ANSI color is enabled for human-readable output. */ colorEnabled: boolean; - // Tri-state: undefined → honor `git.require-clean-tree`. true → - // skip the check. false → force it on. Flag pair: `--allow-dirty` - // / `--no-allow-dirty`. + /** + * Tri-state: `undefined` → honor `git.require-clean-tree`. `true` → + * skip the check. `false` → force it on. Flag pair: `--allow-dirty` + * / `--no-allow-dirty`. + */ allowDirty?: boolean; + /** Emit plugin stdio tracing to stderr. */ debug?: boolean; } +/** Per-Package result of invoking the release plugin Op. */ export interface ReleaseOpOutcome { + /** Name of the Package the release Op ran for. */ package: string; + /** The `pkg-name@x.y.z` Tag minted or reused for this Package. */ tag: string; + /** Whether the release Op succeeded. */ ok: boolean; + /** Whether the plugin reported publishing the Package. */ published?: boolean; + /** Whether the release Op was skipped (e.g. already tagged, no `--force`). */ skipped?: boolean; + /** Optional human-readable detail (failure reason or skip note). */ message?: string; } +/** Release envelope returned by {@link runRelease}. */ export interface RunReleaseResult { + /** The release {@link Plan} computed and executed for this run. */ plan: Plan; + /** Tag names newly minted this run. */ mintedTagNames: string[]; + /** Tag names already present and reused (no new Tag minted). */ reusedTagNames: string[]; + /** Per-Package release Op outcomes. */ releaseOpOutcomes: ReleaseOpOutcome[]; + /** Tag names pushed to the remote (empty when push was disabled). */ pushedTagNames: string[]; } +/** + * Run the stateless second phase of the two-phase release: mint a + * per-Package git Tag for every Package whose current Version isn't + * already tagged, invoke each Package's release plugin Op, and + * optionally push the Tags. A Package needs releasing iff its current + * Version has no matching git Tag. Returns the {@link RunReleaseResult} + * envelope. + */ export async function runRelease( options: RunReleaseOptions, ): Promise { diff --git a/apps/cli/src/cli/rename.ts b/apps/cli/src/cli/rename.ts index 29643cd..0977354 100644 --- a/apps/cli/src/cli/rename.ts +++ b/apps/cli/src/cli/rename.ts @@ -33,25 +33,46 @@ import { makeStyler } from "./styler.ts"; const DEFAULT_FAST_OP_TIMEOUT_MS = 60_000; +/** Inputs to {@link runRename} — one declared lineage edge plus run modifiers. */ export interface RunRenameOptions { + /** Old Package name being renamed *from* (the source of the lineage edge). */ fromPackageName: string; + /** New Package name being renamed *to* (the target of the lineage edge). */ toPackageName: string; + /** Explicit `at` Version, overriding the discovery-inferred first Version under `to`. */ atVersionOverride?: string; + /** Preview the ledger append with zero side effects; no file is written. */ dryRun: boolean; + /** Emit the machine-readable `--json` result instead of human output. */ emitJson: boolean; + /** Whether ANSI color is enabled for human-readable output. */ colorEnabled: boolean; } +/** Outcome of {@link runRename} — the appended lineage edge and what changed on disk. */ export interface RunRenameResult { + /** Path to the rename ledger (`.dv/renames.yaml`) that was (or would be) written. */ ledgerPath: string; + /** Old Package name recorded as the edge source. */ fromPackageName: string; + /** New Package name recorded as the edge target. */ toPackageName: string; + /** Version stamped on the entry's `at` field (the new name's first Version). */ atVersion: string; + /** Whether `atVersion` was discovery-`inferred` or supplied as an `override`. */ atVersionSource: "inferred" | "override"; + /** Whether the ledger file was created fresh (vs. appended to an existing one). */ fileCreated: boolean; + /** Whether the ledger was actually written (false under `dryRun`). */ fileWritten: boolean; } +/** + * Append a `from → to` lineage edge to the rename ledger (`.dv/renames.yaml`), + * declaring Package lineage so existing Records and release history referencing + * the old name resolve to the new one. Renames are declared, never guessed; this + * is bookkeeping only and never touches the actual package. + */ export async function runRename( options: RunRenameOptions, ): Promise { diff --git a/apps/cli/src/cli/status.ts b/apps/cli/src/cli/status.ts index a4fb04b..fbfa8bd 100644 --- a/apps/cli/src/cli/status.ts +++ b/apps/cli/src/cli/status.ts @@ -50,17 +50,29 @@ import { makeStyler, type Styler } from "./styler.ts"; const DEFAULT_FAST_OP_TIMEOUT_MS = 60_000; +/** Options controlling a {@link runStatus} run. */ export interface RunStatusOptions { + /** Emit the {@link Plan} as `--json` instead of human-readable output. */ emitJson: boolean; + /** Whether ANSI color is enabled for human-readable output. */ colorEnabled: boolean; + /** Emit plugin tracing to stderr. */ debug?: boolean; } +/** Result of a {@link runStatus} run. */ export interface RunStatusResult { + /** The previewed {@link Plan}, or `null` when no config was found. */ plan: Plan | null; + /** Whether `.dv/config.yaml` was absent. */ configMissing: boolean; } +/** + * `dv status`: a read-only preview of `dv version`. Builds and returns the + * same {@link Plan} as `dv version --dry-run` (sharing its Plan schema) with + * zero side effects. + */ export async function runStatus( options: RunStatusOptions, ): Promise { diff --git a/apps/cli/src/cli/v1.ts b/apps/cli/src/cli/v1.ts index e692009..fba2e55 100644 --- a/apps/cli/src/cli/v1.ts +++ b/apps/cli/src/cli/v1.ts @@ -74,43 +74,73 @@ import { makeStyler } from "./styler.ts"; const DEFAULT_FAST_OP_TIMEOUT_MS = 60_000; +/** Inputs to {@link runV1}, the gated `dv v1 ` 0.x → 1.0.0 promotion. */ export interface RunV1Options { + /** Name of the Unstable Package to promote to 1.0.0. */ packageName: string; + /** Preview only, with zero side effects; flag overrides `safety.dry-run-by-default`. */ dryRun?: boolean; + /** Stage and finalize files but skip the git commit. */ noCommit: boolean; + /** Drop Unresolved References instead of halting on them. */ prune: boolean; + /** Emit the machine-readable `--json` result instead of human output. */ emitJson: boolean; + /** Whether ANSI color is enabled for human output. */ colorEnabled: boolean; + /** Skip the confirmation prompt (required in non-TTY contexts). */ yes: boolean; + /** Proceed despite a dirty working tree. */ allowDirty?: boolean; + /** Emit plugin stderr tracing for debugging. */ debug?: boolean; } +/** Outcome of a {@link runV1} promotion. */ export interface RunV1Result { + /** The {@link Plan} that was computed and executed. */ plan: Plan; + /** SHA of the promotion commit, or `null` when `noCommit`/dry-run. */ commitSha: string | null; + /** Name of the Package promoted to 1.0.0. */ promotedPackage: string; + /** Number of Records consumed by the promotion. */ consumedRecordCount: number; + /** Constraint-only cascades applied to consumers of the promoted Package. */ cascadedUpdates: CascadedUpdate[]; + /** Manifest files a write-version plugin staged into the commit. */ finalizedFiles: FinalizedFile[]; } +/** A single constraint-only cascade: a consumer's manifest updated to the new Version. */ export interface CascadedUpdate { + /** The Package whose Version bumped, triggering the cascade. */ bumpedPackage: string; + /** The consuming Package whose manifest constraint was updated. */ dependent: string; + /** Path to the consumer's manifest file that was rewritten. */ dependentPath: string; } -// One additional file the finalize pass staged into the v1 commit. -// v1 only ever bumps one package, so there's at most one plugin -// involved; we still carry the same shape as version's RunVersionResult -// for consistency (callers reading the result don't have to branch -// on the command). +/** + * One additional file the finalize pass staged into the v1 commit. + * `dv v1` only ever bumps one Package, so there's at most one plugin + * involved; we still carry the same shape as `version`'s + * `RunVersionResult` for consistency (callers reading the result don't + * have to branch on the command). + */ export interface FinalizedFile { + /** Key of the plugin that produced the file. */ pluginKey: string; + /** Path to the staged file. */ path: string; } +/** + * Execute the gated `dv v1 ` promotion: bumps the named Unstable + * Package to its 1.0.0 Stability commitment, cascades constraints to + * consumers, and (unless dry-run or `noCommit`) commits the result. + */ export async function runV1(options: RunV1Options): Promise { const repoRootPath = await requireRepoRoot(); const configFilePath = configPath(repoRootPath); @@ -810,32 +840,50 @@ interface RenderHumanSummaryArgs { // of these is ready to promote?" — *not* a bulk-promote, which is // why catalog mode is dry-run-only. +/** Inputs to {@link runV1Catalog}, the dry-run-only `dv v1` discovery listing. */ export interface RunV1CatalogOptions { - // Honors the same dry-run resolution as runV1 (flag > config > - // false). Catalog mode requires effective dry-run to be true — - // it has no real-run path because there's no single target to - // promote. The leaf passes through whatever the user supplied; - // we resolve + enforce here so the policy lives in one place. + /** + * Honors the same dry-run resolution as {@link runV1} (flag > config > + * false). Catalog mode requires effective dry-run to be true — it has + * no real-run path because there's no single target to promote. The + * leaf passes through whatever the user supplied; we resolve and + * enforce here so the policy lives in one place. + */ dryRun?: boolean; + /** Drop Unresolved References instead of halting on them. */ prune: boolean; + /** Emit the machine-readable `--json` result instead of human output. */ emitJson: boolean; + /** Whether ANSI color is enabled for human output. */ colorEnabled: boolean; + /** Emit plugin stderr tracing for debugging. */ debug?: boolean; } +/** Result of {@link runV1Catalog}: the projected promotion Plan for every Unstable Package. */ export interface RunV1CatalogResult { - // A standard Plan with N pending entries — one per Unstable - // Package — each projected to 1.0.0. Tracked still lists every - // discovered Package so callers comparing against `dv status`' - // output see the same shape. unresolvedReferences are computed - // against the *whole* Record set, same as runV1, so the catalog - // surfaces ledger issues that would block any actual promotion. + /** + * A standard {@link Plan} with N pending entries — one per Unstable + * Package — each projected to 1.0.0. Tracked still lists every + * discovered Package so callers comparing against `dv status`'s + * output see the same shape. Unresolved References are computed + * against the *whole* Record set, same as {@link runV1}, so the + * catalog surfaces ledger issues that would block any actual promotion. + */ plan: Plan; - // Convenience counters for human callers; redundant with plan - // but matches the shape RunV1Result returns. + /** + * Convenience count of Unstable Packages eligible for promotion; + * redundant with `plan` but matches the shape {@link RunV1Result} returns. + */ eligibleCount: number; } +/** + * List every discovered Package currently in the Unstable regime (major + * == 0) alongside the per-Package {@link Plan} that `dv v1 --dry-run` + * would emit. A discovery aid for "which of these is ready to promote?" — + * *not* a bulk-promote, which is why catalog mode is dry-run-only. + */ export async function runV1Catalog( options: RunV1CatalogOptions, ): Promise { diff --git a/apps/cli/src/cli/validate.ts b/apps/cli/src/cli/validate.ts index 8825fdc..ba1bf54 100644 --- a/apps/cli/src/cli/validate.ts +++ b/apps/cli/src/cli/validate.ts @@ -12,38 +12,60 @@ import { } from "../subtools/renames/mod.ts"; import { makeStderrTracingHooks } from "./debug-trace.ts"; -// `dv validate` per specs/cli.md § dv validate. -// -// Safe and side-effect-free — runnable in CI as a pre-merge gate. Loads -// config + discovery + the rename ledger, parses every Record under -// .dv/records/, and aggregates problems (config shape, plugin -// failures, Record shape, body emptiness, Unresolved References) into -// one report. Exit code is non-zero iff at least one problem was found. +/** + * `dv validate` per `specs/cli.md` § dv validate. + * + * Safe and side-effect-free — runnable in CI as a pre-merge gate. Loads + * config + discovery + the rename ledger, parses every Record under + * `.dv/records/`, and aggregates problems (config shape, plugin + * failures, Record shape, body emptiness, Unresolved References) into + * one report. Exit code is non-zero iff at least one problem was found. + */ +/** A single problem found by `dv validate`. */ export interface ValidationProblem { + /** Stable machine-readable problem code. */ code: string; + /** Human-readable description of the problem. */ message: string; + /** Originating path or identifier (e.g. the offending Record or config), when known. */ source?: string; } +/** The `--json` report shape for `dv validate`; one {@link ValidationProblem} per issue. */ export interface ValidationReport { + /** Versioned schema URN identifying this report shape. */ schema: "urn:dv:schema:v1:validation-report"; + /** `true` iff no problems were found. */ ok: boolean; + /** Number of Records parsed during the run. */ recordsChecked: number; + /** All problems found, empty when `ok`. */ problems: ValidationProblem[]; } +/** Runtime options controlling a {@link runValidate} call. */ export interface RunValidateOptions { + /** Emit the {@link ValidationReport} as JSON instead of human-readable output. */ emitJson: boolean; + /** Whether ANSI color is enabled for human-readable output. */ colorEnabled: boolean; + /** Emit plugin tracing to stderr. */ debug?: boolean; } +/** Outcome of a {@link runValidate} call: the {@link ValidationReport} plus a process exit code. */ export interface RunValidateResult { + /** The aggregated validation report. */ report: ValidationReport; + /** Process exit code; non-zero iff problems were found. */ exitCode: number; } +/** + * Run `dv validate`: load config, discovery, and the rename ledger, parse + * every Record, and aggregate any problems into a {@link RunValidateResult}. + */ export async function runValidate( options: RunValidateOptions, ): Promise { diff --git a/apps/cli/src/cli/version.ts b/apps/cli/src/cli/version.ts index 2d59221..f0a467d 100644 --- a/apps/cli/src/cli/version.ts +++ b/apps/cli/src/cli/version.ts @@ -61,61 +61,96 @@ import { makeStyler } from "./styler.ts"; const DEFAULT_FAST_OP_TIMEOUT_MS = 60_000; +/** Inputs to {@link runVersion} — the flags and config-derived knobs for one `dv version` run. */ export interface RunVersionOptions { + /** Preview without side effects; falls back to `safety.dry-run-by-default` config when unset. */ dryRun?: boolean; + /** Compute and write changes but skip staging them into the version commit. */ noCommit: boolean; + /** Drop Unresolved References instead of halting on them (`--prune`). */ prune: boolean; + /** Emit the {@link Plan} and result as the versioned `--json` machine format. */ emitJson: boolean; + /** Whether ANSI color is enabled for human-readable output. */ colorEnabled: boolean; - // `--yes` is accepted as a no-op in M3 (dv version doesn't prompt); - // wired here so the flag is forward-compatible with later milestones. + /** + * `--yes` is accepted as a no-op in M3 (`dv version` doesn't prompt); + * wired here so the flag is forward-compatible with later milestones. + */ yes: boolean; - // Tri-state: undefined → honor `git.require-clean-tree` config (the - // default). true → skip the check regardless. false → force the - // check on even when config says otherwise. The flag pair on the - // command line is `--allow-dirty` / `--no-allow-dirty`. + /** + * Tri-state clean-tree override. `undefined` → honor `git.require-clean-tree` + * config (the default). `true` → skip the check regardless. `false` → force + * the check on even when config says otherwise. The command-line flag pair is + * `--allow-dirty` / `--no-allow-dirty`. + */ allowDirty?: boolean; - // True if the tool-wide `--debug` was set; the runner builds a - // stderr tracing reporter and threads it through every plugin - // invocation so authors can see exactly what dv asked the plugin. - // Optional with a default of false so test callers don't have to - // opt out of tracing every time. + /** + * True if the tool-wide `--debug` was set; the runner builds a stderr tracing + * reporter and threads it through every plugin invocation so authors can see + * exactly what `dv` asked the plugin. Optional with a default of false so test + * callers don't have to opt out of tracing every time. + */ debug?: boolean; } +/** Outcome of {@link runVersion} — the executed (or, in dry-run, previewed) {@link Plan} plus summary counts. */ export interface RunVersionResult { + /** The {@link Plan} that was built and executed (or previewed in dry-run). */ plan: Plan; + /** SHA of the version commit, or `null` when nothing was committed (dry-run or `--no-commit`). */ commitSha: string | null; + /** How many Packages received a Bump. */ bumpedPackageCount: number; + /** How many pending Records were consumed by this run. */ consumedRecordCount: number; + /** The constraint rewrites the cascade pass executed. */ cascadedUpdates: CascadedUpdate[]; + /** Extra files plugins staged into the version commit (e.g. refreshed lockfiles). */ finalizedFiles: FinalizedFile[]; } -// One additional file the finalize pass staged into the version -// commit. Grouped by the plugin that produced it so the human -// summary can name the source ("refreshed deno.lock via the deno -// plugin") rather than dumping a flat list. +/** + * One additional file the finalize pass staged into the version commit. + * Grouped by the plugin that produced it so the human summary can name the + * source ("refreshed deno.lock via the deno plugin") rather than dumping a + * flat list. + */ export interface FinalizedFile { - // The canonical plugin reference key (e.g. - // "run:deno run -A ./examples/plugins/deno/main.ts"), so the - // summary can de-dupe across packages governed by the same plugin. + /** + * The canonical plugin reference key (e.g. + * `run:deno run -A ./examples/plugins/deno/main.ts`), so the summary can + * de-dupe across Packages governed by the same plugin. + */ pluginKey: string; - // Repo-relative path the plugin reported via - // additionalChangedFiles. + /** Repo-relative path the plugin reported via `additionalChangedFiles`. */ path: string; } -// A single actual constraint rewrite the cascade pass executed (the -// plugin reported changed:true). The dependentPath is relative to -// repoRootPath and gets pushed into touchedPaths so the rewrite lands -// in the version commit. +/** + * A single constraint rewrite the cascade pass executed (the plugin reported + * `changed:true`). The {@link CascadedUpdate.dependentPath} is relative to the + * repo root and gets pushed into `touchedPaths` so the rewrite lands in the + * version commit. + */ export interface CascadedUpdate { + /** Name of the Package whose Bump triggered the rewrite. */ bumpedPackage: string; + /** Name of the consumer Package whose manifest constraint was rewritten. */ dependent: string; + /** Repo-relative path to the rewritten dependent manifest. */ dependentPath: string; } +/** + * Runs `dv version`: consumes pending Records and, per Package, applies the + * aggregated Bump, rewrites the manifest Version, prepends a CHANGELOG section, + * cascades dependent constraints, deletes the consumed Records, and stages + * everything into one commit (the Release PR). Plan-then-execute is the spine + * (specs/language.md Algebra §7): the same `buildVersionPlan` call powers + * `dv status`, `--dry-run`, and the real run, with the dry-run path invoking + * zero write-side plugin Ops and touching nothing on disk. + */ export async function runVersion( options: RunVersionOptions, ): Promise { diff --git a/apps/cli/src/domain/change-type.ts b/apps/cli/src/domain/change-type.ts index 17561e6..662ebc8 100644 --- a/apps/cli/src/domain/change-type.ts +++ b/apps/cli/src/domain/change-type.ts @@ -1,17 +1,23 @@ -// dv's Record-type vocabulary (specs/language.md § Lexicon). These four -// flavors come from Record frontmatter — dv never parses commit -// messages, so this is a closed set of values dv defines. The vocabulary -// happens to match a Conventional Commits subset, which is a -// familiarity choice for teams already on CC; it is not a contract on -// contributors. The `!` denotes a breaking change. Anything outside -// these four values is outside dv's vocabulary entirely and is rejected -// at parse time (.claude/CLAUDE.md § Strong opinions; specs/design.md § -// Records over commit messages). - +/** + * `dv`'s Record-type vocabulary (specs/language.md § Lexicon). These four + * flavors come from Record frontmatter — `dv` never parses commit + * messages, so this is a closed set of values `dv` defines. The vocabulary + * happens to match a Conventional Commits subset, which is a + * familiarity choice for teams already on CC; it is not a contract on + * contributors. The `!` denotes a breaking Change Type. Anything outside + * these four values is outside `dv`'s vocabulary entirely and is rejected + * at parse time (.claude/CLAUDE.md § Strong opinions; specs/design.md § + * Records over commit messages). + */ export const CHANGE_TYPES = ["feat", "fix", "feat!", "fix!"] as const; +/** + * The closed Record-type vocabulary: `feat`, `fix`, `feat!`, `fix!`. The + * trailing `!` marks a breaking Change Type. Drawn from {@link CHANGE_TYPES}. + */ export type ChangeType = (typeof CHANGE_TYPES)[number]; +/** Type guard: `true` iff `candidate` is one of the {@link CHANGE_TYPES}. */ export function isChangeType(candidate: unknown): candidate is ChangeType { return ( typeof candidate === "string" && diff --git a/apps/cli/src/domain/config.ts b/apps/cli/src/domain/config.ts index 2883e99..034f9b5 100644 --- a/apps/cli/src/domain/config.ts +++ b/apps/cli/src/domain/config.ts @@ -5,43 +5,55 @@ // Only sections needed by milestone 1 (discovery) are fully typed. The rest // keep loose shapes that future subtools will tighten. -// Tagged plugin reference. Exactly one of `path`, `builtin`, or -// `command` is set; the YAML parser (via the discriminated Zod schema -// in subtools/config/schema.ts) guarantees that invariant before any -// runtime code sees this type. -// -// { path: "./scripts/foo" } — local file or directory -// { builtin: "cargo" } — first-party plugin (none in v1) -// { command: "my-plugin" } — binary on $PATH -// -// Replaces the old `use: string` form whose kind was inferred from -// string shape (specs/config-format.md § Plugin resolution). +/** + * Tagged Plugin reference. Exactly one of `path`, `builtin`, `command`, or + * `run` is set; the YAML parser (via the discriminated Zod schema in + * subtools/config/schema.ts) guarantees that invariant before any runtime + * code sees this type. + * + * - `{ path }` — local file or directory. + * - `{ builtin }` — first-party Plugin (none in v1). + * - `{ command }` — binary on `$PATH`. + * - `{ run }` — full invocation string. + * + * Replaces the old `use: string` form whose kind was inferred from + * string shape (specs/config-format.md § Plugin resolution). + */ export type PluginReference = | { path: string } | { builtin: string } | { command: string } | { run: string }; +/** + * One config entry mapping a glob `match` to a Plugin `use`, with an + * optional per-assignment `timeout`. + */ export interface PluginAssignment { + /** Glob (or array of globs) selecting the Packages this assignment claims. */ match: string | string[]; + /** The {@link PluginReference} to invoke for matched Packages. */ use: PluginReference; + /** Optional per-Op timeout override (e.g. `"30s"`). */ timeout?: string; } -// Canonical string key for a PluginReference. Used as a Map key when -// caching `resolvePlugin` results across the assignments in a single -// run, and as the per-Package `plugin` identifier so a Package knows -// which assignment's plugin to invoke without having to carry the -// reference object around. -// -// { path: "./foo" } → "path:./foo" -// { builtin: "cargo" } → "builtin:cargo" -// { command: "x" } → "command:x" -// { run: "deno run -A jsr:@s/p" } → "run:deno run -A jsr:@s/p" -// -// Two assignments referencing the same plugin produce the same key, -// which is what the resolve-once cache relies on. The key is not a -// stable wire format — it's an in-process identifier. +/** + * Canonical string key for a {@link PluginReference}. Used as a `Map` key + * when caching `resolvePlugin` results across the assignments in a single + * run, and as the per-Package `plugin` identifier so a Package knows which + * assignment's Plugin to invoke without having to carry the reference object + * around. + * + * - `{ path: "./foo" }` → `"path:./foo"` + * - `{ builtin: "cargo" }` → `"builtin:cargo"` + * - `{ command: "x" }` → `"command:x"` + * - `{ run: "deno run -A jsr:@s/p" }` → `"run:deno run -A jsr:@s/p"` + * + * Two assignments referencing the same Plugin produce the same key, which is + * what the resolve-once cache relies on. The key is not a stable wire format + * — it's an in-process identifier. + */ export function pluginReferenceKey(ref: PluginReference): string { if ("path" in ref) return `path:${ref.path}`; if ("builtin" in ref) return `builtin:${ref.builtin}`; diff --git a/apps/cli/src/domain/package.ts b/apps/cli/src/domain/package.ts index 7eb550f..6e3774c 100644 --- a/apps/cli/src/domain/package.ts +++ b/apps/cli/src/domain/package.ts @@ -1,16 +1,19 @@ -// A Package: a unit carrying an independent Version, managed by exactly -// one Plugin (specs/language.md § Lexicon). -// -// `plugin` is the canonical `pluginReferenceKey` of the assignment -// that claimed this Package — e.g. "path:./scripts/my-plugin", -// "builtin:cargo", "command:my-plugin", "run:deno run -A ...". -// Callers use it as a lookup -// into the Map a single command-run builds -// once and shares across phases. The actual reference object lives -// on the PluginAssignment in the config. - +/** + * A Package: a unit carrying an independent Version, managed by exactly + * one Plugin (specs/language.md § Lexicon). + */ export interface Package { + /** The Package's unique name. */ name: string; + /** Filesystem path to the Package, relative to the repo root. */ path: string; + /** + * The canonical `pluginReferenceKey` of the assignment that claimed this + * Package — e.g. `"path:./scripts/my-plugin"`, `"builtin:cargo"`, + * `"command:my-plugin"`, `"run:deno run -A ..."`. Callers use it as a + * lookup into the `Map` a single command-run + * builds once and shares across phases. The actual reference object lives + * on the `PluginAssignment` in the config. + */ plugin: string; } diff --git a/apps/cli/src/lib.ts b/apps/cli/src/lib.ts index ad8e4b4..9597469 100644 --- a/apps/cli/src/lib.ts +++ b/apps/cli/src/lib.ts @@ -62,6 +62,7 @@ export { runMigrateConfig, } from "./cli/migrate.ts"; export { + PLUGIN_OP_NAMES, type PluginOpName, type RunPluginInvokeOptions, type RunPluginInvokeResult, @@ -130,13 +131,28 @@ export { type RunVersionResult, runVersion, } from "./cli/version.ts"; +// --- Domain types referenced by the runner options/results ----------------- +// Reachable through the public option/result shapes above (e.g. +// `RunAddOptions.changeType` is a {@link ChangeType}), so they're part of the +// surface a consumer types against. Re-exported here so they can be imported +// by name rather than reached through an indexed access type. +export { CHANGE_TYPES, type ChangeType } from "./domain/change-type.ts"; +export type { PluginAssignment, PluginReference } from "./domain/config.ts"; +export type { Package } from "./domain/package.ts"; // --- Binary boundary ------------------------------------------------------- export { main } from "./main.ts"; +export type { + ConfigMigrationStepResult, + MigrationChange, +} from "./subtools/config-migrations/mod.ts"; +export type { ResolvedPlugin } from "./subtools/discovery/resolve.ts"; +export type { SlugRandomSource } from "./subtools/records/mod.ts"; // --- Contract types (the `--json` shape) ----------------------------------- export type { Plan, PlanAwaitingRelease, PlanChangeCounts, + PlanConstraintUpdate, PlanPending, PlanTracked, PlanUnresolvedReference, diff --git a/apps/cli/src/scripts/generate-schemas.ts b/apps/cli/src/scripts/generate-schemas.ts index fbcf95c..a2595d3 100644 --- a/apps/cli/src/scripts/generate-schemas.ts +++ b/apps/cli/src/scripts/generate-schemas.ts @@ -9,7 +9,11 @@ // Output is hand-deterministic: 2-space indentation, trailing newline, // keys ordered as Zod emits them. -import { rawCliErrorEnvelopeSchema } from "@dv-cli/clipc"; +// Zod schema imported from clipc's internal subpath (not the public `.` +// surface — Zod is an implementation detail there). This build script is +// the one sanctioned consumer of the raw schema, feeding it to +// `z.toJSONSchema()` to emit the committed JSON Schema artifact. +import { rawCliErrorEnvelopeSchema } from "@dv-cli/clipc/internal/error-schema"; import { resolve } from "@std/path"; import { z } from "zod"; import { rawConfigLayerSchema } from "../subtools/config/schema.ts"; diff --git a/apps/cli/src/subtools/config-migrations/mod.ts b/apps/cli/src/subtools/config-migrations/mod.ts index 7cbc13f..5fb298b 100644 --- a/apps/cli/src/subtools/config-migrations/mod.ts +++ b/apps/cli/src/subtools/config-migrations/mod.ts @@ -45,9 +45,13 @@ export interface RunConfigMigrationsResult { stepResults: ConfigMigrationStepResult[]; } +/** One migration step's outcome. */ export interface ConfigMigrationStepResult { + /** Stable identifier of the step that ran. */ stepId: string; + /** Human-readable description of what the step does. */ description: string; + /** The discrete changes the step made (see {@link MigrationChange}). */ changes: MigrationChange[]; } diff --git a/apps/cli/src/subtools/config-migrations/step-types.ts b/apps/cli/src/subtools/config-migrations/step-types.ts index fb0923a..c4fbbed 100644 --- a/apps/cli/src/subtools/config-migrations/step-types.ts +++ b/apps/cli/src/subtools/config-migrations/step-types.ts @@ -3,34 +3,47 @@ // the step registry (which would create a cycle: steps.ts imports // each step, each step imports the types). +/** Arguments to a migration step's `apply`. */ export interface ConfigMigrationStepApplyArgs { + /** The config text fed to the step. */ text: string; } +/** The result of applying one migration step. */ export interface ConfigMigrationStepApplyResult { - // The text after the step's rewrite. Equal to the input when no - // change applied — the runner uses an empty `changes` list as - // the canonical "did nothing" signal. + /** + * The text after the step's rewrite. Equal to the input when no change + * applied — the runner uses an empty `changes` list as the canonical + * "did nothing" signal. + */ rewrittenText: string; + /** The discrete changes the step made (see {@link MigrationChange}). */ changes: MigrationChange[]; } -// One discrete change a migration step made. Steps describe their -// rewrites as a list of these so consumers (the CLI's human -// renderer, the --json envelope, automation tools) can summarize -// what happened without re-diffing the text. +/** + * One discrete change a migration step made. Steps describe their rewrites + * as a list of these so consumers (the CLI's human renderer, the `--json` + * envelope, automation tools) can summarize what happened without + * re-diffing the text. + */ export interface MigrationChange { - // Dotted breadcrumb identifying the location within the config, - // e.g. "discovery.plugins[0].use" or "publishing.plugin". Stays - // stable across migration releases for the same logical field. + /** + * Dotted breadcrumb identifying the location within the config, e.g. + * `"discovery.plugins[0].use"` or `"publishing.plugin"`. Stays stable + * across migration releases for the same logical field. + */ path: string; - // The original string value (or short summary) the user had. + /** The original string value (or short summary) the user had. */ before: string; - // What the step replaced it with — kind + value pair captures - // the most common shape (a discriminated reference) without - // committing to a specific schema. Future steps can use the - // same shape ("kind" = "deprecated-key" / "value" = the new - // key, etc.) without changing this type. + /** + * What the step replaced it with — `kind` + `value` pair captures the most + * common shape (a discriminated reference) without committing to a + * specific schema. Future steps can use the same shape (`kind` = + * `"deprecated-key"` / `value` = the new key, etc.) without changing this + * type. + */ kind: string; + /** The replacement value paired with {@link MigrationChange.kind}. */ value: string; } diff --git a/apps/cli/src/subtools/discovery/resolve.ts b/apps/cli/src/subtools/discovery/resolve.ts index 3e35523..93fb621 100644 --- a/apps/cli/src/subtools/discovery/resolve.ts +++ b/apps/cli/src/subtools/discovery/resolve.ts @@ -29,6 +29,10 @@ import { PosixTokenizeError, posixTokenize } from "../shell/posix-tokenize.ts"; // are intercepted in config/parse.ts and routed to a // `config-legacy-use-shape` error pointing at `dv migrate config`. +/** + * A {@link PluginReference} resolved to an executable form: a `single` file, + * a `dir`, or an `invocation`. Produced by {@link resolvePlugin}. + */ export type ResolvedPlugin = | { kind: "single"; path: string } | { kind: "dir"; path: string } @@ -48,6 +52,7 @@ interface ResolvePluginArgs { repoRootPath: string; } +/** Resolves a {@link PluginReference} to a {@link ResolvedPlugin}, per specs/config-format.md § Plugin resolution. */ export function resolvePlugin( args: ResolvePluginArgs, ): Promise { diff --git a/apps/cli/src/subtools/records/slug.ts b/apps/cli/src/subtools/records/slug.ts index 50651c4..75ac68e 100644 --- a/apps/cli/src/subtools/records/slug.ts +++ b/apps/cli/src/subtools/records/slug.ts @@ -356,13 +356,17 @@ const VERBS = [ "yawn", ]; -// Test seam: callers can inject a deterministic RNG to make slug generation -// repeatable in tests. Default uses Math.random; the spec says nothing about -// cryptographic randomness here. +/** + * Test seam: callers can inject a deterministic RNG to make slug generation + * repeatable in tests. Default uses `Math.random`; the spec says nothing + * about cryptographic randomness here. + */ export interface SlugRandomSource { + /** Returns a float in `[0, 1)`, like `Math.random`. */ next(): number; } +/** The default {@link SlugRandomSource}, backed by `Math.random`. */ export const defaultRandomSource: SlugRandomSource = { next: () => Math.random(), }; diff --git a/apps/cli/src/subtools/versioning/mod.ts b/apps/cli/src/subtools/versioning/mod.ts index bfc767d..1dd31ae 100644 --- a/apps/cli/src/subtools/versioning/mod.ts +++ b/apps/cli/src/subtools/versioning/mod.ts @@ -35,11 +35,11 @@ export { type Plan, type PlanAwaitingRelease, type PlanChangeCounts, + type PlanConstraintUpdate, type PlanPending, type PlanTracked, type PlanUnresolvedReference, parsedPlanSchema, - type RawPlan, rawPlanSchema, } from "./plan-schema.ts"; export { invokeReadVersion } from "./read-version.ts"; diff --git a/apps/cli/src/subtools/versioning/plan-schema.ts b/apps/cli/src/subtools/versioning/plan-schema.ts index b0f2386..72961da 100644 --- a/apps/cli/src/subtools/versioning/plan-schema.ts +++ b/apps/cli/src/subtools/versioning/plan-schema.ts @@ -1,5 +1,114 @@ import { z } from "zod"; +// --- Public Plan contract (Zod-free) --------------------------------------- +// The Plan types are hand-written interfaces, not `z.infer` aliases, so the +// public surface never references Zod's internal types (`deno doc --lint` +// flags those as private-type-refs, and JSR's slow-types gate rejects the +// inferred shapes). The Zod schemas below are annotated `z.ZodType<…>` +// against these interfaces, so the two cannot drift: a schema that stops +// matching its interface fails to type-check. + +/** Per-Change-Type tallies for a {@link PlanPending} entry of a {@link Plan}. */ +export interface PlanChangeCounts { + /** Number of `feat` Records feeding the bump. */ + feat: number; + /** Number of `fix` Records feeding the bump. */ + fix: number; + /** Count of breaking Records — `feat!` and `fix!` together. */ + breaking: number; +} + +/** + * A dependent Package whose constraint on a bumped Package would be rewritten + * during the cascade. Part of a {@link PlanPending} entry of a {@link Plan}. + */ +export interface PlanConstraintUpdate { + /** Name of the dependent Package whose manifest constraint changes. */ + dependent: string; + /** The new version constraint dv would write for the bumped dependency. */ + newConstraint: string; +} + +/** A {@link Plan} entry: a Package queued for a Bump on this run. */ +export interface PlanPending { + /** Name of the Package this bump applies to. */ + package: string; + /** The Package's current Version, before the bump. */ + currentVersion: string; + /** The Version dv would write after applying the aggregated Bump. */ + projectedVersion: string; + /** The aggregated Bump across the feeding Records. */ + bump: "patch" | "minor" | "major"; + /** The Package's Stability — caps a pre-1.0 Package's Bump at minor. */ + stability: "Unstable" | "Stable"; + /** Per-Change-Type tally of the Records feeding this bump. */ + changeCounts: PlanChangeCounts; + /** Record filenames feeding this bump, sorted lexicographically. */ + records: string[]; + /** Dependents whose constraint may be rewritten when this Package bumps. */ + constraintUpdates: PlanConstraintUpdate[]; +} + +/** + * A {@link Plan} entry: a Package bumped but not yet tagged — its current + * Version has no matching git Tag, so it awaits `dv release`. + */ +export interface PlanAwaitingRelease { + /** Name of the Package awaiting a release tag. */ + package: string; + /** The current (untagged) Version. */ + version: string; + /** The Tag `dv release` would mint. */ + tag: string; + /** True if this would be the Package's first `1.0.0` (a celebrated event). */ + firstStable: boolean; +} + +/** + * A {@link Plan} entry: a Record pointing at a vanished Package with no + * rename ledger entry. Halts `dv version` unless `--prune` drops it. + */ +export interface PlanUnresolvedReference { + /** Filename of the Record carrying the reference. */ + record: string; + /** The package name as written in the Record. */ + reference: string; +} + +/** + * A {@link Plan} entry: a discovered Package with its current Version, + * independent of whether any Records are queued against it. + */ +export interface PlanTracked { + /** Name of the discovered Package. */ + package: string; + /** The Package's current Version, as the read-version Op reported it. */ + currentVersion: string; + /** The Package's directory, relative to the repo root. */ + path: string; +} + +/** + * The side-effect-free structure emitted by `dv status --json` and + * `dv version|release --dry-run --json`. A pure function of repo state — + * the same plan-building code feeds the preview, the dry-run, and the real + * run (specs/language.md Algebra §7; serializes to specs/schemas/plan.json). + */ +export interface Plan { + /** Schema id — always `urn:dv:schema:v1:plan`. */ + schema: "urn:dv:schema:v1:plan"; + /** Which command produced this Plan. */ + command: "status" | "version" | "release"; + /** Per-Package bumps that `dv version` would apply. */ + pending: PlanPending[]; + /** Packages whose current Version has no Tag (what `dv release` would tag). */ + awaitingRelease: PlanAwaitingRelease[]; + /** Records pointing at no current Package (halt `dv version` unless `--prune`). */ + unresolvedReferences: PlanUnresolvedReference[]; + /** Every Package discovery resolved, with its current Version. */ + tracked: PlanTracked[]; +} + // Zod source for the Plan emitted by `dv status --json` and // `dv version --dry-run --json` (specs/language.md Algebra §7: plan is a // pure function of repo state; the same builder runs in status, dry-run, @@ -29,11 +138,7 @@ const planStabilitySchema: z.ZodEnum<{ Stable: "Stable"; }> = z.enum(["Unstable", "Stable"]); -const planChangeCountsSchema: z.ZodType<{ - feat: number; - fix: number; - breaking: number; -}> = z +const planChangeCountsSchema: z.ZodType = z .object({ feat: z.int().nonnegative(), fix: z.int().nonnegative(), @@ -46,10 +151,7 @@ const planChangeCountsSchema: z.ZodType<{ "Per-Package tally of the Records feeding the bump. `breaking` counts feat! and fix! together (independent of the Unstable cap).", }); -const planConstraintUpdateSchema: z.ZodType<{ - dependent: string; - newConstraint: string; -}> = z +const planConstraintUpdateSchema: z.ZodType = z .object({ dependent: z.string(), newConstraint: z.string(), @@ -61,16 +163,7 @@ const planConstraintUpdateSchema: z.ZodType<{ "A dependent Package whose constraint on the bumped Package will be rewritten. When dv can resolve the dependency graph (the plugin implements `get-dependencies`) this lists only real dependents; for packages whose plugin lacks that op, dv falls back to listing them as candidates and the plugin filters at execute time via `changed: false`.", }); -const planPendingEntrySchema: z.ZodType<{ - package: string; - currentVersion: string; - projectedVersion: string; - bump: "patch" | "minor" | "major"; - stability: "Unstable" | "Stable"; - changeCounts: { feat: number; fix: number; breaking: number }; - records: string[]; - constraintUpdates: { dependent: string; newConstraint: string }[]; -}> = z +const planPendingEntrySchema: z.ZodType = z .object({ package: z.string(), currentVersion: semverStringSchema, @@ -96,12 +189,7 @@ const planPendingEntrySchema: z.ZodType<{ "What `dv version` would do for one Package: the aggregated Bump, projected Version, change counts, and the Records feeding it.", }); -const planAwaitingReleaseSchema: z.ZodType<{ - package: string; - version: string; - tag: string; - firstStable: boolean; -}> = z +const planAwaitingReleaseSchema: z.ZodType = z .object({ package: z.string(), version: semverStringSchema, @@ -119,10 +207,7 @@ const planAwaitingReleaseSchema: z.ZodType<{ "A Package whose current Version has no Tag — what `dv release` would tag. Always empty in M3 — tag-state queries are M5.", }); -const planUnresolvedReferenceSchema: z.ZodType<{ - record: string; - reference: string; -}> = z +const planUnresolvedReferenceSchema: z.ZodType = z .object({ record: z .string() @@ -138,11 +223,7 @@ const planUnresolvedReferenceSchema: z.ZodType<{ "A Record references a Package that no discovery plugin claims and no rename ledger edge maps to. Halts `dv version` unless --prune.", }); -const planTrackedPackageSchema: z.ZodType<{ - package: string; - currentVersion: string; - path: string; -}> = z +const planTrackedPackageSchema: z.ZodType = z .object({ package: z.string(), currentVersion: semverStringSchema, @@ -155,14 +236,7 @@ const planTrackedPackageSchema: z.ZodType<{ "A Package discovery resolved and whose current Version the read-version Op returned. Lists every Package dv is aware of, regardless of whether any Records are pending against it.", }); -export const rawPlanSchema: z.ZodType<{ - schema: "urn:dv:schema:v1:plan"; - command: "status" | "version" | "release"; - pending: z.infer[]; - awaitingRelease: z.infer[]; - unresolvedReferences: z.infer[]; - tracked: z.infer[]; -}> = z +export const rawPlanSchema: z.ZodType = z .object({ schema: z.literal("urn:dv:schema:v1:plan").describe("Schema id."), command: z @@ -193,18 +267,10 @@ export const rawPlanSchema: z.ZodType<{ "The side-effect-free Plan emitted by `dv status --json` and `dv version|release --dry-run --json`. A pure function of repo state (specs/language.md Algebra §7).", }); -export type RawPlan = z.infer; - -// Parser-shape mirrors the raw shape — the Plan is camelCased -// throughout (it's produced by code, never authored by humans), so the -// kebab→camel transform other schemas need is unnecessary here. The -// split is preserved for symmetry with the rest of the codebase. - -export const parsedPlanSchema = rawPlanSchema; - -export type Plan = z.infer; -export type PlanPending = Plan["pending"][number]; -export type PlanAwaitingRelease = Plan["awaitingRelease"][number]; -export type PlanUnresolvedReference = Plan["unresolvedReferences"][number]; -export type PlanChangeCounts = Plan["pending"][number]["changeCounts"]; -export type PlanTracked = Plan["tracked"][number]; +/** + * Parser-shape mirrors the raw shape — the Plan is camelCased + * throughout (it's produced by code, never authored by humans), so the + * kebab→camel transform other schemas need is unnecessary here. The + * split is preserved for symmetry with the rest of the codebase. + */ +export const parsedPlanSchema: z.ZodType = rawPlanSchema; diff --git a/apps/docs/.vitepress/config.ts b/apps/docs/.vitepress/config.ts index 75682d7..1af0b8a 100644 --- a/apps/docs/.vitepress/config.ts +++ b/apps/docs/.vitepress/config.ts @@ -109,6 +109,10 @@ export default defineConfig({ "specs/walkthrough.md", "specs/v1-scope.md", "specs/schemas/**", + // Design proposals are implementer-facing (they link freely to the + // internal specs above), so they stay off the public site like the + // rest of the internal design library. + "specs/proposals/**", ], themeConfig: { nav: [ diff --git a/packages/clipc/deno.json b/packages/clipc/deno.json index e2abeb3..c7f3e49 100644 --- a/packages/clipc/deno.json +++ b/packages/clipc/deno.json @@ -1,7 +1,10 @@ { "name": "@dv-cli/clipc", "version": "0.3.1", - "exports": "./src/mod.ts", + "exports": { + ".": "./src/mod.ts", + "./internal/error-schema": "./src/cli-error-schema.ts" + }, "license": "MIT", "description": "CLIPC (Command Line Interface Procedure Call): a typed CLI framework. Routers, leaves, flag specs, and structured error responses. The substrate dv is built on.", "tasks": { diff --git a/packages/clipc/src/cli-error-schema.ts b/packages/clipc/src/cli-error-schema.ts index 8bd3a95..3a117fc 100644 --- a/packages/clipc/src/cli-error-schema.ts +++ b/packages/clipc/src/cli-error-schema.ts @@ -39,6 +39,10 @@ export interface RawCliErrorPayload { * Zod schema validating a single {@link RawCliErrorPayload} node. Carries * no `.transform()` calls so `z.toJSONSchema()` can emit it faithfully, * and uses `z.lazy` for the recursive `subErrors` array. + * + * @internal Not part of the public surface — see + * {@link rawCliErrorEnvelopeSchema} for the rationale and the public + * {@link parseCliErrorEnvelope} entry point. */ export const rawCliErrorPayloadSchema: z.ZodType = z .object({ @@ -92,6 +96,12 @@ export interface RawCliErrorEnvelope { * Zod schema validating a {@link RawCliErrorEnvelope}. The top-level * `schema` URN lets downstream tools (shell scripts, agent fleets) * version-gate before parsing the recursive `error` tree. + * + * @internal Not part of the public surface — Zod is an implementation + * detail of `@dv-cli/clipc`. Consumers validate via + * {@link parseCliErrorEnvelope} / {@link safeParseCliErrorEnvelope}, which + * return plain (Zod-free) results. The schema stays exported only so the + * in-repo JSON Schema generator can feed it to `z.toJSONSchema()`. */ export const rawCliErrorEnvelopeSchema: z.ZodType = z .object({ @@ -112,3 +122,50 @@ export const rawCliErrorEnvelopeSchema: z.ZodType = z // the envelope; declared as an interface above (rather than via // `z.infer`) so the export carries an explicit signature for JSR's // fast-types check. + +/** + * The outcome of a Zod-free validation: either a typed value or a flat list + * of issues. Mirrors the shape of a Zod `safeParse` result without exposing + * any Zod type, so `@dv-cli/clipc`'s public surface stays free of its + * validation library. Returned by {@link safeParseCliErrorEnvelope}. + */ +export type CliErrorEnvelopeParseResult = + | { success: true; data: RawCliErrorEnvelope } + | { success: false; issues: CliErrorEnvelopeParseIssue[] }; + +/** One validation problem from {@link safeParseCliErrorEnvelope}. */ +export interface CliErrorEnvelopeParseIssue { + /** Dotted path to the offending field (e.g. `error.subErrors.0.code`). */ + path: string; + /** Human-readable description of why the value was rejected. */ + message: string; +} + +/** + * Validate an unknown value as a {@link RawCliErrorEnvelope} — the JSON shape + * a `@dv-cli/clipc`-based CLI emits on stderr under `--json` mode. Throws if + * the value doesn't match. Use {@link safeParseCliErrorEnvelope} for a + * non-throwing variant. + */ +export function parseCliErrorEnvelope(input: unknown): RawCliErrorEnvelope { + return rawCliErrorEnvelopeSchema.parse(input); +} + +/** + * Non-throwing counterpart to {@link parseCliErrorEnvelope}: validate an + * unknown value as a {@link RawCliErrorEnvelope} and return a Zod-free + * {@link CliErrorEnvelopeParseResult}. + */ +export function safeParseCliErrorEnvelope( + input: unknown, +): CliErrorEnvelopeParseResult { + const result = rawCliErrorEnvelopeSchema.safeParse(input); + if (result.success) return { success: true, data: result.data }; + return { + success: false, + issues: result.error.issues.map((issue) => ({ + path: issue.path.join("."), + message: issue.message, + })), + }; +} diff --git a/packages/clipc/src/mod.ts b/packages/clipc/src/mod.ts index 6f2958b..e9189fb 100644 --- a/packages/clipc/src/mod.ts +++ b/packages/clipc/src/mod.ts @@ -9,10 +9,12 @@ // discriminated-union error shapes for typed catch-site narrowing. export { + type CliErrorEnvelopeParseIssue, + type CliErrorEnvelopeParseResult, + parseCliErrorEnvelope, type RawCliErrorEnvelope, type RawCliErrorPayload, - rawCliErrorEnvelopeSchema, - rawCliErrorPayloadSchema, + safeParseCliErrorEnvelope, } from "./cli-error-schema.ts"; export { CliError,