Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .dv/records/olive-crabs-wade.md
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions .dv/records/patient-lamps-soar.md
Original file line number Diff line number Diff line change
@@ -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
12 changes: 5 additions & 7 deletions .github/workflows/dv-validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ===
Expand Down
39 changes: 31 additions & 8 deletions apps/cli/src/cli/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<RunAddResult> {
const repoRootPath = await requireRepoRoot();
const configFilePath = configPath(repoRootPath);
Expand Down
10 changes: 10 additions & 0 deletions apps/cli/src/cli/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<InitResult> {
const repoRoot = await requireRepoRoot();
const cfgPath = configPath(repoRoot);
Expand Down
32 changes: 21 additions & 11 deletions apps/cli/src/cli/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<RunMigrateConfigResult> {
Expand Down
44 changes: 39 additions & 5 deletions apps/cli/src/cli/plugin-invoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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<string, string>;
/** 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<RunPluginInvokeResult> {
Expand Down
25 changes: 25 additions & 0 deletions apps/cli/src/cli/plugin-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,33 +29,58 @@ import { makeStyler } from "./styler.ts";
// `dv plugin verify <plugin>` 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<RunPluginListResult> {
Expand Down
Loading