From a5079b5d1c9c378fb406f85b48290df9b7ca8839 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 9 Aug 2026 12:10:07 +0200 Subject: [PATCH 01/18] docs(architecture): requirements for the unified CLI engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records the design constraints for the consolidated prisma CLI agreed with the operator — the interface between the CLI shell and its product packages, and why each constraint matters. Two questions stay open by design: command-tree ownership, and third-party engine vs owned. Co-Authored-By: Claude Fable 5 Signed-off-by: willbot Signed-off-by: Will Madden --- docs/architecture/cli-engine-requirements.md | 180 +++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 docs/architecture/cli-engine-requirements.md diff --git a/docs/architecture/cli-engine-requirements.md b/docs/architecture/cli-engine-requirements.md new file mode 100644 index 00000000..49b12fe0 --- /dev/null +++ b/docs/architecture/cli-engine-requirements.md @@ -0,0 +1,180 @@ +# Requirements for the unified CLI engine + +Status: **Agreed** (Will Madden, 2026-08-09), except the two questions in +"Open" at the end. This document records the design constraints for the +consolidated `prisma` CLI — the interface between the CLI shell and the +product packages it hosts (Prisma ORM, Prisma Composer, Prisma Cloud) — and +why each constraint matters. It precedes and will govern the engine's design; +tool and framework choices are evaluated against this list, not the other way +around. + +## The shape being constrained + +One `prisma` binary. It owns everything user-facing — argument parsing, help, +prompts, rendering, `--json`, exit codes, loading `prisma.config.ts` — and it +gets its functionality from product packages it depends on. Each product +already exposes a typed operations API returning structured results; the +engine is the thin layer that turns argv into operation calls. These +requirements constrain that layer and the package interface around it. + +## Requirements + +### R1 — One language, directly executable + +A product authors its CLI contributions in the engine's vocabulary, and that +artifact is what runs. There is no product-side data structure that a separate +interpreter translates into commands. + +**Why:** every stage of interpretation is complexity we own forever — a schema +that grows toward being a CLI framework, an interpreter to maintain, and a +translation step contributors must understand before they can add a flag. +Complexity raises the odds something goes wrong, deters outside contributions, +and makes testing indirect. One vocabulary that is itself runnable keeps the +system learnable and the test path short. + +### R2 — Commands end in typed operation calls + +A command handler calls its product's operations client, and TypeScript +enforces the arguments. The command layer contains phrasing and wiring, never +business logic. + +**Why:** the operations layer is where correctness is enforced and where +exhaustive testing lives. If command handlers grow logic, that logic escapes +the product's own test suite and the type checker's view of the operation +contract. Keeping handlers thin means the compiler proves the CLI calls each +product correctly. + +### R3 — The engine package is the whole contract + +Products import our engine package and nothing else for CLI purposes. No +third-party type appears in a product's exports, whether or not the engine +internally wraps a third-party framework. + +**Why:** whatever sits in the public interface between our packages can only +be replaced by coordinating simultaneous releases across every repo. Keeping +third-party primitives out of that interface bounds our exposure: swapping the +engine's internals is one package's problem, not an ecosystem event. + +### R4 — Products receive a context, never the environment + +Product code does not read disk, environment variables, or the TTY. Handlers +receive one typed context object carrying their validated config section, +credentials, and the output surface. + +**Why:** three reasons. Cross-cutting state — above all authentication +credentials, which Composer needs even when the user authenticated through a +Cloud command — has to flow somewhere, and handing it in as context is +strictly simpler than sharing the read-it-off-disk logic between packages. +Products that never touch the environment are agnostic about runtime (node, +bun, deno) by construction. And a handler whose whole world arrives as one +argument is trivially fakeable in tests. + +### R5 — Products have no presentational API + +Products supply words and structure: command descriptions, flag names, +examples, structured errors. They cannot print, color, format, or exit — +the interface offers no way to express it. Help layout, ANSI and color +policy, error rendering, `--json` envelopes, streaming format, and exit +codes are implemented exactly once, in the engine. + +**Why:** our CLIs were authored in different years by different teams with +different goals, and it shows: help text, phrasing, error codes, visual +display, color handling — none of it is consistent, and convention (style +guides, review comments) demonstrably did not hold the line. Consistency has +to be structural: a product that cannot render cannot diverge. This is the +constraint the rest of the design serves. + +### R6 — Errors and results follow the settled conventions + +Every user-surfaced failure is a structured error built at its origin, with a +dotted namespace code, carried in the shared `Result` shape with the single +`ok` discriminator. The engine maps failures to the error envelope and exit +codes uniformly (0 ok; 1 bug only; 2 expected failure; 3 user abort). + +**Why:** these rules are already accepted and shipping (prisma/prisma ADR 239 +and ADR 245; Composer ADR-0043/0044). The engine is where they become visible +to users, so it must implement them once rather than trusting each product's +rendering. Machine consumers — agents, CI — branch on `ok`, `code`, and exit +codes; that only works if exactly one code space and one envelope exist. + +### R7 — Product-repo end-to-end tests are first-class + +A product can instantiate the engine with only its own commands and run real +argv-in, bytes-out tests in its own repository. Production is the same +machinery, so those tests are valid evidence about the shipped CLI. + +**Why:** piloting the operations client alone misses whatever happens in real +CLI execution — parsing, context handoff, rendering, exit codes. If products +cannot test that in-repo, every CLI regression is discovered late, in the +shell's repo, by someone who didn't make the change. The engine being +instance-based (no global state) is what makes this possible; it is a hard +requirement on any framework we adopt or build. + +### R8 — The shell's test burden is integration proof + +The shell end-to-end tests the happy path of each product's critical +operations: the mounted tree, config handoff, and auth context demonstrably +work. Exhaustive error-case testing stays in product repos. + +**Why:** the shell must prove composition works — that is the one thing only +it can test. Duplicating the products' error matrices there would rot and +would blur who owns which guarantee. Cheap for the shell, exhaustive in the +product: each test lives where its failure would be introduced. + +### R9 — Static tree, lazy guts + +Command definitions are cheap and load at startup; heavy dependencies load +inside handlers at execution time. No dynamic discovery, no runtime tree +construction. + +**Why:** a statically known tree is simpler to reason about, renders complete +help without executing product code, and fails at build time when it is wrong. +The expensive parts — driver stacks, Composer's dependency tree (which imports +the user's own modules and has crashed at import in the past) — stay out of +the startup path, so `prisma migrate` can never be taken down by a product it +isn't using. The split follows the existing design for isolating heavy +dependency subtrees behind execution-time imports. + +### R10 — One config file, validated by its products, never a crash + +The engine discovers and evaluates one `prisma.config.ts`. Each product +contributes a named section and a never-throwing validator; validation +produces per-section diagnostics, and a command fails only if a section it +needs is invalid. The config value carries a version marker written by +`defineConfig`; an evaluated file without the marker (in particular a classic +Prisma 7 config, which uses the same filename) fails early with a clear, +typed error. No best-effort reading of unmarked files. + +**Why:** the unified CLI claims a filename Prisma 7 already owns; a silently +misparsed v7 file is the worst launch bug available, so detection is a +structural marker, not a heuristic. Per-section diagnostics exist because one +product's config problem must not brick the other products' commands. And +validators that throw turn a user's typo into a stack trace instead of a +diagnostic with a fix. + +### R11 — Pinned versions, tandem releases + +The shell pins exact product versions. Shipping a product change to users +means releasing the shell with a bumped pin; release automation or workflow +glue makes that cheap, and no version ranges are used. + +**Why:** with ranges, two users on the same shell version can run different +product code — support and reproducibility poison. Exact pins mean a shell +version fully determines behavior. The cost is tandem releasing, which is +tedious but simple, and simplicity wins. + +## Open + +Two questions this document deliberately leaves undecided: + +1. **Who defines the command tree.** The tree is a whole-CLI concern + (cross-product renames and coherence argue for the shell owning it), but + product-declared paths keep in-repo e2e tests running the real + invocations, and a middle shape exists: products declare paths in their + command definitions, the shell holds the agreed tree as data and refuses + to build on any mismatch. +2. **Build the engine on a third-party framework or own it end to end.** + The preference is to reuse a wheel that fits the constraints above + (instance-based, typed, no global state, static-tree friendly), wrapped + per R3; owning the whole engine is the fallback if none fits. Candidates + are evaluated against this document. From 220807b40f1cd3aa2f10edcea3fa0f2da2ab5a73 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 9 Aug 2026 12:24:02 +0200 Subject: [PATCH 02/18] docs(architecture): rule the tree (shell-defined) and ban package-manager behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R12: the shell owns the command tree — paths are cosmetic to products (the real invocation is the command and its arguments) and structural to the shell; the tree's six months of cross-product design is the evidence. R13: the CLI never installs anything — optional peer dependencies plus a structured missing-dependency error replace the old self-installing submodule approach. Co-Authored-By: Claude Fable 5 Signed-off-by: willbot Signed-off-by: Will Madden --- docs/architecture/cli-engine-requirements.md | 48 ++++++++++++++++---- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/docs/architecture/cli-engine-requirements.md b/docs/architecture/cli-engine-requirements.md index 49b12fe0..6f2d3e7e 100644 --- a/docs/architecture/cli-engine-requirements.md +++ b/docs/architecture/cli-engine-requirements.md @@ -1,6 +1,6 @@ # Requirements for the unified CLI engine -Status: **Agreed** (Will Madden, 2026-08-09), except the two questions in +Status: **Agreed** (Will Madden, 2026-08-09), except the question in "Open" at the end. This document records the design constraints for the consolidated `prisma` CLI — the interface between the CLI shell and the product packages it hosts (Prisma ORM, Prisma Composer, Prisma Cloud) — and @@ -163,17 +163,47 @@ product code — support and reproducibility poison. Exact pins mean a shell version fully determines behavior. The cost is tandem releasing, which is tedious but simple, and simplicity wins. +### R12 — The shell defines the command tree + +Products export commands; the shell decides where each one mounts. A command's +path in the tree does not appear in the product's code or its interface. + +**Why:** the tree is a whole-CLI concern, and the evidence is its own history: +defining the consolidated tree took roughly six months of iteration by the +responsible product manager, coordinating renames and regroupings across +product lines (`app` → `service`, `database` → `postgres`, standalone +`format` folded into `contract format`) that no product would have made +locally. From a product's point of view the path is purely cosmetic — the +real invocation is the command and its arguments, and a product's in-repo +e2e tests exercise exactly that by mounting the command at any path. From +the shell's point of view the tree is structural: central definition makes +path collisions impossible and keeps the shipped tree checkable against the +agreed grammar in one place. + +### R13 — The CLI never touches a package manager + +The shell does not install, download, or vendor packages at runtime — no +self-installing command modules, no hidden `node_modules`, ever. Components +that only some commands need are declared as optional peer dependencies; a +command that requires one checks for it at execution time and, when it is +absent, returns a structured error naming the dependency and how to install +it with the user's own package manager. + +**Why:** a previous incarnation of the Prisma CLI installed command +submodules on demand into a hidden `node_modules` in the working directory. +That approach is compatible with exactly one package manager and produces +edge cases everywhere else — lockfiles that lie, deduplication that never +happens, state the user cannot see or clean. The user already has a package +manager; the CLI's job is to declare what it needs and say clearly what is +missing, not to become a second, worse package manager. The structured +"optional dependency missing" error follows R6 like every other expected +failure. + ## Open -Two questions this document deliberately leaves undecided: +One question this document deliberately leaves undecided: -1. **Who defines the command tree.** The tree is a whole-CLI concern - (cross-product renames and coherence argue for the shell owning it), but - product-declared paths keep in-repo e2e tests running the real - invocations, and a middle shape exists: products declare paths in their - command definitions, the shell holds the agreed tree as data and refuses - to build on any mismatch. -2. **Build the engine on a third-party framework or own it end to end.** +1. **Build the engine on a third-party framework or own it end to end.** The preference is to reuse a wheel that fits the constraints above (instance-based, typed, no global state, static-tree friendly), wrapped per R3; owning the whole engine is the fallback if none fits. Candidates From ced138870b047fa40ee2c177303399021e036e86 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 9 Aug 2026 12:48:20 +0200 Subject: [PATCH 03/18] =?UTF-8?q?docs(architecture):=20R5=20clarifications?= =?UTF-8?q?=20=E2=80=94=20framework=20renderers=20are=20legal;=20no=20glob?= =?UTF-8?q?al=20flags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R5 constrains who owns rendering, not how it is built: the engine may adopt its internal framework's renderer where the output meets the Style Guide. And there are no global flags — per-command declaration with a shared flag-set for uniformity. Co-Authored-By: Claude Fable 5 Signed-off-by: willbot Signed-off-by: Will Madden --- docs/architecture/cli-engine-requirements.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/architecture/cli-engine-requirements.md b/docs/architecture/cli-engine-requirements.md index 6f2d3e7e..1ccadc94 100644 --- a/docs/architecture/cli-engine-requirements.md +++ b/docs/architecture/cli-engine-requirements.md @@ -84,6 +84,16 @@ guides, review comments) demonstrably did not hold the line. Consistency has to be structural: a product that cannot render cannot diverge. This is the constraint the rest of the design serves. +Two clarifications. First, this requirement constrains *who owns* rendering, +not how it is built: the engine may adopt its internal framework's renderer +wherever that output satisfies the CLI Style Guide — custom rendering is the +escape hatch for what the framework cannot express, not the default. (The +hand-rolled help formatter in the current ORM CLI exists because its +framework rendered badly, not because owning the pixels is a goal.) Second, +there are no global flags: a flag like `--json` is declared per command — +many commands legitimately don't accept it — and the engine provides a +shared flag-set so the commands that do support it declare it uniformly. + ### R6 — Errors and results follow the settled conventions Every user-surfaced failure is a structured error built at its origin, with a From 7247cfbac276da64de1301633d7f8f89772a6c68 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 9 Aug 2026 12:52:05 +0200 Subject: [PATCH 04/18] docs(architecture): the engine wraps @stricli/core Resolves the document's one open question. Clipanion passes the rubric's nine technical criteria but fails maintenance (23 months without a publish, 4.x in RC for three years at decision time); stricli passes all ten, and its known limitations are neutralized by this document's own rules. Fully hidden per R3, so the internals remain replaceable. Co-Authored-By: Claude Fable 5 Signed-off-by: willbot Signed-off-by: Will Madden --- docs/architecture/cli-engine-requirements.md | 32 +++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/docs/architecture/cli-engine-requirements.md b/docs/architecture/cli-engine-requirements.md index 1ccadc94..7d69ec3c 100644 --- a/docs/architecture/cli-engine-requirements.md +++ b/docs/architecture/cli-engine-requirements.md @@ -1,7 +1,7 @@ # Requirements for the unified CLI engine -Status: **Agreed** (Will Madden, 2026-08-09), except the question in -"Open" at the end. This document records the design constraints for the +Status: **Agreed** (Will Madden, 2026-08-09), including the engine-internals +decision at the end. This document records the design constraints for the consolidated `prisma` CLI — the interface between the CLI shell and the product packages it hosts (Prisma ORM, Prisma Composer, Prisma Cloud) — and why each constraint matters. It precedes and will govern the engine's design; @@ -209,12 +209,22 @@ missing, not to become a second, worse package manager. The structured "optional dependency missing" error follows R6 like every other expected failure. -## Open - -One question this document deliberately leaves undecided: - -1. **Build the engine on a third-party framework or own it end to end.** - The preference is to reuse a wheel that fits the constraints above - (instance-based, typed, no global state, static-tree friendly), wrapped - per R3; owning the whole engine is the fallback if none fits. Candidates - are evaluated against this document. +## The engine's internals + +Decided (Will Madden, 2026-08-09): the engine wraps **@stricli/core**, +fully hidden per R3 — no stricli type appears in the engine's public +interface, so the internals remain replaceable. + +The decision followed the evaluation rubric recorded in prisma/prisma's +`docs/architecture docs/research/commander-friction-points.md`. Commander +was ruled out there. Clipanion, the incumbent candidate with in-house +precedent, passes the rubric's nine technical criteria but fails the tenth: +at decision time its last publish was 23 months old with its 4.x line in +release-candidate state for three years. Stricli passes all ten — zero +runtime dependencies, no `node:` imports, no `process.exit` (verified +against the published artifact), per-invocation injected context, static +route maps with lazy command loading, parse-time validation with typed +errors, active institutional maintenance — and its known limitations +(per-command flags only, fixed help layout without formatter replacement) +are neutralized or made irrelevant by this document's own rules (R5's +engine-owned rendering; the no-global-flags rule). From f7ea6e8165b446d568dec34213b101835ca808b3 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 9 Aug 2026 13:17:39 +0200 Subject: [PATCH 05/18] =?UTF-8?q?docs(architecture):=20R14=20=E2=80=94=20e?= =?UTF-8?q?ngine-defined=20event=20vocabulary=20with=20product=20extension?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Common fields are engine-owned and mandatory-first; product extension data is product-published API the engine passes through; recurring extension shapes get promoted into the vocabulary. Co-Authored-By: Claude Fable 5 Signed-off-by: willbot Signed-off-by: Will Madden --- docs/architecture/cli-engine-requirements.md | 24 ++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/architecture/cli-engine-requirements.md b/docs/architecture/cli-engine-requirements.md index 7d69ec3c..aebcc146 100644 --- a/docs/architecture/cli-engine-requirements.md +++ b/docs/architecture/cli-engine-requirements.md @@ -209,6 +209,30 @@ missing, not to become a second, worse package manager. The structured "optional dependency missing" error follows R6 like every other expected failure. + +### R14 — One event vocabulary, engine-defined, with product extensions + +Commands report progress as structured events in a vocabulary the engine +defines: generic, CLI-shaped concepts (steps, warnings, remediation, +child-process output, endpoints) with consistent fields the engine knows how +to present in both human and `--json` modes. Products must fill and +primarily use those common fields. Alongside them, an event may carry +product-populated extension data: context-dependent structures the product +defines, versions, and documents as part of its own public API — the engine +passes them through to `--json` consumers untouched and does not enforce +them. A structure recurring across commands or products is the signal that +the engine vocabulary is missing a concept and should adopt it. + +**Why:** two event dialects already evolved independently (Composer's +per-operation event unions; the ORM's progress spans), which is the machine- +surface version of the presentational drift R5 exists to kill — one +vocabulary means agents and CI learn one language for the whole CLI. But a +strictly closed vocabulary would either lose product-specific facts or grow +by fiat; the extension field keeps machine consumers fully informed, puts +the compatibility burden where the knowledge is (the product publishes its +extension interfaces), and gives the vocabulary an evidence-driven growth +path instead of speculation. + ## The engine's internals Decided (Will Madden, 2026-08-09): the engine wraps **@stricli/core**, From 485143112a0e8614509e5bdd383efb1da47d56f0 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 9 Aug 2026 18:17:48 +0200 Subject: [PATCH 06/18] =?UTF-8?q?drive(prisma-cli-v8):=20commit=20the=20pr?= =?UTF-8?q?oject=20workspace=20=E2=80=94=20spec,=20design=20record,=20brie?= =?UTF-8?q?fs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prisma-cli-v8 project's Drive artifacts, committed so parallel agent sessions across repos share one canonical record: the project spec (DoD: a publishable prisma@8.0.0-rc1 on the settled design, all three command families ported), the design-notes index, the engine interface design (v8 + full v1-v7 history + five review-round artifacts + the output-modes survey + the stricli decision record + the parked daemon-library notes), and the paused 1b/1c hand-off briefs. Co-Authored-By: Claude Fable 5 Signed-off-by: willbot Signed-off-by: Will Madden --- .../briefs/1b-leftovers-prisma-prisma.md | 39 + .../assets/briefs/1c-leftovers-composer.md | 38 + .../assets/engine/daemon-library-notes.md | 60 ++ .../engine/engine-interface-draft-v1.ts | 339 +++++++ .../engine/engine-interface-draft-v2.ts | 634 +++++++++++++ .../engine/engine-interface-draft-v3.ts | 671 +++++++++++++ .../engine/engine-interface-draft-v4.ts | 771 +++++++++++++++ .../engine/engine-interface-draft-v5.ts | 836 +++++++++++++++++ .../engine/engine-interface-draft-v6.ts | 852 +++++++++++++++++ .../engine/engine-interface-draft-v7.ts | 870 +++++++++++++++++ .../assets/engine/engine-interface-draft.ts | 884 ++++++++++++++++++ .../assets/engine/output-modes-survey.md | 652 +++++++++++++ .../assets/engine/reviews/code-review-r2.md | 459 +++++++++ .../assets/engine/reviews/code-review-r3.md | 631 +++++++++++++ .../engine/reviews/code-review-r4-closure.md | 163 ++++ .../engine/reviews/code-review-r5-delta.md | 164 ++++ .../assets/engine/reviews/code-review.md | 659 +++++++++++++ .../reviews/envelope-collections-analysis.md | 223 +++++ .../engine/reviews/system-design-review-r2.md | 443 +++++++++ .../engine/reviews/system-design-review-r3.md | 420 +++++++++ .../engine/reviews/system-design-review.md | 667 +++++++++++++ .../assets/engine/stricli-vs-clipanion.md | 238 +++++ .drive/projects/prisma-cli-v8/design-notes.md | 65 ++ .drive/projects/prisma-cli-v8/spec.md | 148 +++ 24 files changed, 10926 insertions(+) create mode 100644 .drive/projects/prisma-cli-v8/assets/briefs/1b-leftovers-prisma-prisma.md create mode 100644 .drive/projects/prisma-cli-v8/assets/briefs/1c-leftovers-composer.md create mode 100644 .drive/projects/prisma-cli-v8/assets/engine/daemon-library-notes.md create mode 100644 .drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v1.ts create mode 100644 .drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v2.ts create mode 100644 .drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v3.ts create mode 100644 .drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v4.ts create mode 100644 .drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v5.ts create mode 100644 .drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v6.ts create mode 100644 .drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v7.ts create mode 100644 .drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft.ts create mode 100644 .drive/projects/prisma-cli-v8/assets/engine/output-modes-survey.md create mode 100644 .drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r2.md create mode 100644 .drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r3.md create mode 100644 .drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r4-closure.md create mode 100644 .drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r5-delta.md create mode 100644 .drive/projects/prisma-cli-v8/assets/engine/reviews/code-review.md create mode 100644 .drive/projects/prisma-cli-v8/assets/engine/reviews/envelope-collections-analysis.md create mode 100644 .drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review-r2.md create mode 100644 .drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review-r3.md create mode 100644 .drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review.md create mode 100644 .drive/projects/prisma-cli-v8/assets/engine/stricli-vs-clipanion.md create mode 100644 .drive/projects/prisma-cli-v8/design-notes.md create mode 100644 .drive/projects/prisma-cli-v8/spec.md diff --git a/.drive/projects/prisma-cli-v8/assets/briefs/1b-leftovers-prisma-prisma.md b/.drive/projects/prisma-cli-v8/assets/briefs/1b-leftovers-prisma-prisma.md new file mode 100644 index 00000000..1deb23e3 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/briefs/1b-leftovers-prisma-prisma.md @@ -0,0 +1,39 @@ +# Brief: prisma/prisma config-contract repairs and ControlClient test double + +Repo: prisma/prisma (main). Three independent deliverables; land as separate PRs or one PR with separate commits. Operator: Will Madden. Process: verify every claim below against current code before changing it; if you hit a judgment call this brief doesn't settle, stop and report it as a numbered question with options and a recommendation — do not decide it yourself. + +## Context + +These are the remaining config/contract items from the CLI-consolidation work. The governing rules are in-repo: ADR 239 (structural error envelopes, dotted codes), ADR 245 (errors structured at origin, no catch-all codes; one `ok` discriminator on results), and `docs/CLI Style Guide.md` (exit codes). The error-code registry is `docs/reference/error-reference.md`, enforced by `pnpm run check:error-reference` — any new code or new producing site is added there in the same change. + +## Deliverable 1: config loading returns diagnostics instead of throwing + +Today `loadConfig` (`packages/1-framework/3-tooling/config-loader/src/load.ts`, validation in `packages/1-framework/1-core/config/`) throws `CONFIG.VALIDATION_FAILED` / `CONFIG.EVALUATION_FAILED`-shaped errors on the first problem. The target semantics: + +- Evaluating a config module never takes down the command wholesale. Loading returns the evaluated config **plus a diagnostics list** (each diagnostic a `CliErrorEnvelope`-shaped structured error with a `meta.section` identifying the config section it concerns). +- A command that needs section X fails (exit 2, rendering that diagnostic) only if section X has a diagnostic; commands not touching X proceed. +- A config file that cannot be evaluated at all (module threw, unparseable) yields a single evaluation diagnostic attached to no section; every command then fails early with it. +- Existing error codes are reused; genuinely new conditions get new registered codes. No behavior change to what users *see* for currently-failing configs beyond message framing — pin representative `--json` envelopes before and after. + +Design the exact return type first (the repo convention is the shared `Result` from `@internal/utils/result`, but a config-with-diagnostics is not a failure — a `{ config, diagnostics }` value inside `Ok` is the expected shape) and validate it against every `loadConfig` call site before writing code. + +## Deliverable 2: versioned `defineConfig` marker + +`defineConfig` (`packages/1-framework/1-core/config/src/config-types.ts`) stamps the object it returns with a config-format version marker (non-enumerable; survives spreads is NOT required — document that configs must return the `defineConfig` result directly). The loader then enforces: + +- Marker present and current → proceed. +- Evaluation succeeded but no marker (a plain object export, or a config produced by a different `defineConfig` — i.e. a classic Prisma 7 file once the unified filename lands) → **fail early** with a new registered code (suggested: `CONFIG.UNVERSIONED_CONFIG`) whose fix text names `defineConfig` and links the migration path. This ruling is settled: fail early; no best-effort reading of unmarked configs. + +The marker's purpose is downstream: the future unified host loader will claim `prisma.config.ts`, a filename Prisma 7 already uses, and must distinguish the two by marker rather than misparse. Build the marker and enforcement here; do not build any Prisma-7-filename discovery in this repo. + +## Deliverable 3: published fixture-backed `ControlClient` test double + +Hosts and product tests need to drive the CLI's control-api surface without a real database. Export a fixture-backed double of the control client (`packages/1-framework/3-tooling/cli/src/control-api/client.ts`) from a **published** entrypoint (decide placement against how `@prisma/orm-toolchain` composes its published surface; the double must not drag the real driver/database imports into consumers). It covers every seam operation the control API exposes, returns the shared `Result` shapes with realistic fixture payloads, and its per-operation fixtures are overridable per test. Add a conformance-style test asserting the double's surface stays in sync with the real client (compile-time: same operation names and signatures). + +## Verification (all must pass before pushing) + +`pnpm turbo build --filter=@internal/cli...`; full test + typecheck + lint in config-loader, config, cli packages; `test/integration` typecheck; `pnpm run check:error-reference` with zero failures; `pnpm lint:deps`. + +## Commit discipline + +Explicit staging only. `git commit -s --trailer "Signed-off-by: Will Madden "`, body ends with `Co-Authored-By: `. Push via the `bot` remote, never origin. Verify the PR is open before any push to an existing PR branch. diff --git a/.drive/projects/prisma-cli-v8/assets/briefs/1c-leftovers-composer.md b/.drive/projects/prisma-cli-v8/assets/briefs/1c-leftovers-composer.md new file mode 100644 index 00000000..8fe87a84 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/briefs/1c-leftovers-composer.md @@ -0,0 +1,38 @@ +# Brief: composer config-contract compliance and control-API test double + +Repo: prisma/composer (main). Three deliverables. Operator: Will Madden. Process: verify every claim against current code first; stop and report numbered questions with options and a recommendation on anything this brief doesn't settle. + +## Context + +Composer's error/result rules are recorded in its ADR-0043 and ADR-0044 (`docs/design/90-decisions/`): structured errors at origin with dotted codes from the closed registry, one `ok` discriminator, exit 1 = bug only. The registry in ADR-0044 is closed — a new subcode is an edit to that list in the same change. Constraint that must not move: the effect constellation stays pinned at `4.0.0-beta.103` via the consumer overrides block (alchemy is broken on effect >= beta.104; see `skills-contrib/upgrade-alchemy-effect/SKILL.md`). + +## Deliverable 1: config validation returns diagnostics instead of throwing + +Today `load-config.ts` / `validate-coverage.ts` throw on the first invalid field. Target semantics (the config contract all products will share): + +- Loading a config returns the evaluated value **plus a diagnostics list** (structured errors, each tagged with the config section/field it concerns via `meta`), instead of throwing per field. +- Commands fail (exit 2, rendering the diagnostic) only when a section they need is invalid. +- A config module that cannot be evaluated at all yields one evaluation diagnostic (`CONFIG.EVALUATION_FAILED` already exists) and every command fails early with it. +- No import-time side effects and no throwing from `defineConfig`-equivalent factories: constructing a config value never throws; problems surface as diagnostics at load time. +- Pin representative rendered/`--json` output before and after — user-visible behavior for currently-failing configs may only change in framing. + +## Deliverable 2: effect-resolution preflight becomes a diagnostic + +`check-effect-resolution.ts` currently detects a mismatched `effect` in the consumer's tree by throwing during import, which takes out every command — including ones that never touch the deploy executor. Target: + +- The preflight runs at config-load/command-dispatch time, not import time, and surfaces as a structured diagnostic (`DEPS.EFFECT_VERSION_CONFLICT`, already registered) carried in the diagnostics list from Deliverable 1. +- Commands that need the executor fail early rendering it; commands that don't (e.g. help, config inspection) still work. +- The lazy executor-load failure path (`DEPS.EXECUTOR_UNLOADABLE`) is unchanged — it remains the backstop when the preflight didn't fire. +- The effect-CI probe and the `npm install effect dedupe` check must still pass; do not weaken either. + +## Deliverable 3: published test double for the control API + +Hosts driving `@prisma/composer/control` (deploy/destroy/dev/log) need a double that never spawns alchemy or containers. Export a fixture-backed double from a published entrypoint (placement judged against the existing `./control` shim in `packages/9-public/composer`): same operation signatures, same `Result<…, CliStructuredError>` shapes, per-operation fixtures overridable per test, including a `DevSession` double whose lifecycle methods behave. Add a compile-time conformance check that the double's surface matches the real operations. + +## Verification (all must pass before pushing) + +`pnpm build`, `pnpm typecheck`, `pnpm lint`, `pnpm lint:casts` (delta 0), `pnpm lint:deps` (all sub-checks), `@internal/cli` and integration test suites, `pnpm run check:npm-effect-resolution`. Known pre-existing failures that are not yours to fix: the `@internal/local-target` timeout pair. + +## Commit discipline + +Explicit staging only. `git commit -s --trailer "Signed-off-by: Will Madden "`, body ends with `Co-Authored-By: `. Composer's `origin` in the operator's clones is the bot SSH alias; verify the target PR is open before pushing to an existing PR branch. diff --git a/.drive/projects/prisma-cli-v8/assets/engine/daemon-library-notes.md b/.drive/projects/prisma-cli-v8/assets/engine/daemon-library-notes.md new file mode 100644 index 00000000..627dfdb1 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/daemon-library-notes.md @@ -0,0 +1,60 @@ +# Daemon library — design conclusions, parked + +Status: **excluded from the CLI-engine scope** (Will, 2026-08-09) — it is a +runtime dependency of product control clients, orthogonal to the engine. +These notes preserve what the design conversation concluded so the work is +picked up, not re-derived. Evidence citations: `output-modes-survey.md` +(emulators/daemons section). + +## The finding that shaped the engine + +"Daemon mode" needs **zero engine surface**. Commands touching daemons are +ordinary commands: `ls` is a result command presenting a table over +`scan()`; `stop` presents a result over `stop()`; `composer dev` calls +`ensure()` during startup then runs as a normal session command. The +daemon-ness lives in what handlers do (operations layer), like spawning +alchemy already does. + +## What the library is + +The lifecycle-and-discovery primitives that Composer's +`dev-emulators/src/daemon.ts` and `@prisma/dev`'s state layer each +hand-built (convergent evolution — the evidence they're one concept): + +- **ensure(name, entry, opts)** — idempotent start: read registry entry, + probe health (identity/version-matched), adopt a healthy same-version + daemon, terminate-and-replace a stale-version one, spawn detached+unref + when absent, record `{pid, port, version, logPath}`, await health — all + serialized under a lockfile so concurrent CLI invocations can't race. +- **stop(name)** — SIGTERM, grace, SIGKILL, remove entry. +- **scan() / status(name)** — registry entries probed to + running/starting/dead. +- **logs(name)** — per-daemon stdio log file. +- Daemon-side: an entry-script harness (bind localhost port, serve + /health + the product's admin API, SIGTERM cleanup). + +Each daemon's **admin API stays product-owned**; the library owns only +lifecycle and discovery. Registry entries need a product-data extension +slot (same shape of reasoning as the engine's R14). + +## Open questions when picked up + +1. **Unified machine-wide registry vs per-product registries + an + aggregating command.** Unified (one `ls` shows Composer emulators and + dev servers; one stop semantics; the second liveness implementation + stops existing) costs a real `@prisma/dev` internal migration + (`server.json` format, its `proper-lockfile` usage) including + old-format servers; per-product costs nothing now but keeps two + liveness protocols forever and adds one per future daemon. +2. **The package's home.** +3. The management-command surface the grammar parked (`emulator` + root: ls/stop/status) — the gap that today lets Composer leave + daemons on the machine with no user-facing way to list or stop them + (`stopDaemon` is "not called by any v1 command"). + +## Effect on @prisma/dev (under unification) + +Public API (`startPrismaDevServer`, scan/status surface) unchanged; +internals swap to the shared library; its domain fields (ports, exports) +ride the registry's extension slot; migration must handle servers created +under the old on-disk format. diff --git a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v1.ts b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v1.ts new file mode 100644 index 00000000..3c6eccf1 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v1.ts @@ -0,0 +1,339 @@ +/** + * DRAFT — the unified CLI engine's public interface, for line-by-line review. + * + * Everything a product package imports for CLI purposes lives here (R3). + * Requirement references (R1–R14) point at docs/architecture/ + * cli-engine-requirements.md in prisma-cli. Nothing from stricli appears — + * it is an internal of the engine package. + * + * The one execution protocol (agreed 2026-08-09): a handler receives + * (args, context), emits zero or more events through context.report, and + * returns a Result when done. Sync commands emit nothing; progress and + * poll commands emit along the way (timeouts are the handler's business); + * session commands keep emitting until context.signal fires, then clean up + * and return. Liveness display (spinner-equivalent) is the engine's: shown + * when a command runs quietly past a threshold. Daemon management (mode 5) + * is a separate design; `lsp`-style stdio servers bypass the protocol via + * a declared raw mode (see CommandDefinition.raw). + */ + +// ———————————————————————————————————————————————————————————————————————— +// Foundation types (from the zero-dependency foundation package, not here; +// shown for reading convenience) +// ———————————————————————————————————————————————————————————————————————— + +import type { CliStructuredError, Result } from '@prisma/cli-foundation' + +// ———————————————————————————————————————————————————————————————————————— +// §1 Events — R14: one engine vocabulary, product extensions ride in `data` +// ———————————————————————————————————————————————————————————————————————— + +/** + * The engine event envelope. `kind`-specific fields are the common + * vocabulary the engine renders consistently (human mode) and frames + * (--json mode: one line per event, `{ type, command, timestamp, data }`, + * where the event body is the data). `data` is the product extension: + * passed through to machine consumers untouched, never interpreted by the + * engine, documented and versioned by the product as its own public API. + * + * Starter vocabulary, derived from the output-modes survey's recurring + * structures (occurrence-ranked). Grows only by evidence: a structure + * recurring inside `data` across commands is the promotion signal. + */ +export type EngineEvent = + /** A named phase began. Engine renders it as a step line; steps may nest. */ + | { readonly kind: 'step-started'; readonly step: string; readonly data?: unknown } + /** The phase ended. `outcome` drives the ✔/✘/⚠ glyph. */ + | { + readonly kind: 'step-finished' + readonly step: string + readonly outcome: 'ok' | 'failed' | 'skipped' | 'warning' + readonly data?: unknown + } + /** Progress inside a phase (counts, not percentages — survey: counts+summary). */ + | { + readonly kind: 'progress' + readonly step?: string + readonly completed: number + readonly total?: number + readonly data?: unknown + } + /** A condition the user should know about; never fatal (fatal = the Result). */ + | { readonly kind: 'warning'; readonly message: string; readonly data?: unknown } + /** Informational line the product wants shown (human) / framed (json). */ + | { readonly kind: 'notice'; readonly message: string; readonly data?: unknown } + /** + * Output from a child process or remote log stream, line-oriented. + * Survey: three passthrough strategies exist today; this is the typed one. + */ + | { + readonly kind: 'output' + readonly source: string + readonly stream: 'stdout' | 'stderr' + readonly line: string + readonly data?: unknown + } + /** + * A user-actionable follow-up surfaced mid-run (survey: remediation exists + * in five encodings today — this is the one). Terminal remediation goes on + * the Result's error (`fix`) or the success envelope's nextActions instead. + */ + | { + readonly kind: 'remediation' + readonly label: string + readonly command?: string + readonly data?: unknown + } + /** A reachable endpoint became available (survey: endpoints/URLs, 3 families). */ + | { + readonly kind: 'endpoint' + readonly name: string + readonly url: string + readonly data?: unknown + } + /** A state transition in a watched external process (survey: poll loops). */ + | { + readonly kind: 'status' + readonly subject: string + readonly status: string + readonly data?: unknown + } + +// ———————————————————————————————————————————————————————————————————————— +// §2 The handler context — R4: the whole world arrives as one argument +// ———————————————————————————————————————————————————————————————————————— + +export interface CommandContext { + /** The product's validated section of prisma.config.ts (R10). Absent + * section: `undefined`; invalid section: the engine already failed the + * command before the handler ran, so handlers never see diagnostics. */ + readonly config: ConfigSection | undefined + + /** Management-API credentials, however the user authenticated (R4's why). */ + readonly credentials: Credentials | undefined + + /** The one way to emit while running (§1). Safe to call after the signal + * fires (events during teardown render normally). */ + readonly report: (event: EngineEvent) => void + + /** Interactive input. Every method returns a structured error instead of + * prompting when interaction is unavailable (--json, --no-interactive, + * CI, non-TTY) — the platform CLI's canPrompt gate, engine-owned. */ + readonly prompt: PromptSurface + + /** Fires on Ctrl-C/SIGTERM (engine-owned wiring). Session commands run + * until it fires; everything else should abort in-flight work with it. */ + readonly signal: AbortSignal + + /** Where the user invoked the CLI. Products never read process.cwd(). */ + readonly cwd: string +} + +export interface Credentials { + /** Opaque to the engine; shape owned by the Cloud product's auth library. */ + readonly token: string + readonly workspaceId?: string +} + +export interface PromptSurface { + readonly confirm: (question: string) => Promise> + readonly select: ( + question: string, + options: ReadonlyArray<{ value: T; label: string }>, + ) => Promise> + readonly text: ( + question: string, + opts?: { placeholder?: string }, + ) => Promise> +} + +// ———————————————————————————————————————————————————————————————————————— +// §3 Flags and arguments — R1: directly executable, typed by inference +// ———————————————————————————————————————————————————————————————————————— + +/** Flag declarations. `flag.json()` is the shared flag-set entry for + * commands that support --json (there are no global flags). Parse-time + * validation failures become structured errors with the allowed values — + * never framework strings. */ +export declare const flag: { + string(spec: { brief: string; placeholder?: string }): FlagSpec + requiredString(spec: { brief: string; placeholder?: string }): FlagSpec + boolean(spec: { brief: string }): FlagSpec + enum(spec: { + brief: string + values: T + }): FlagSpec + repeated(spec: { brief: string; placeholder?: string }): FlagSpec + /** The shared --json declaration; presence changes rendering, not parsing. */ + json(): FlagSpec +} + +declare const FLAG: unique symbol +export interface FlagSpec { readonly [FLAG]: T } + +export declare const positional: { + string(spec: { brief: string; placeholder: string }): PositionalSpec + optionalString(spec: { brief: string; placeholder: string }): PositionalSpec +} +declare const POSITIONAL: unique symbol +export interface PositionalSpec { readonly [POSITIONAL]: T } + +/** What the handler receives: each declared flag/positional, typed. */ +export type ArgsOf = { + readonly [K in keyof D['flags']]: D['flags'][K] extends FlagSpec ? T : never +} & { + readonly [K in keyof D['positionals']]: D['positionals'][K] extends PositionalSpec + ? T + : never +} + +// ———————————————————————————————————————————————————————————————————————— +// §4 The command definition — light at startup (R9), path-free (R12) +// ———————————————————————————————————————————————————————————————————————— + +export interface CommandDefinition< + TFlags extends Record> = Record>, + TPositionals extends Record> = Record>, + TResult = unknown, + TConfig = unknown, +> { + /** One line, imperative, shown in listings. */ + readonly brief: string + /** Paragraph(s) for `--help`. Words only — the engine formats. */ + readonly description?: string + /** Copy-pastable invocations, shown verbatim in help. */ + readonly examples?: readonly string[] + + readonly flags: TFlags + readonly positionals?: TPositionals + + /** + * The heavy part, loaded only at execution (R9). The module's default + * export is the handler. + */ + readonly handler: () => Promise<{ + default: ( + args: ArgsOf>, + ctx: CommandContext, + ) => Promise> + }> + + /** + * How a success Result renders (R5). The platform CLI's proven triple: + * `human` is required prose (stderr); `stdout` is the machine-consumable + * payload lines (what --quiet leaves); `json` projects the envelope's + * `result` and defaults to the raw value. All composed from engine + * primitives — there is no way to print. + */ + readonly present: { + readonly human: (value: TResult, ui: Ui) => readonly Block[] + readonly stdout?: (value: TResult) => readonly string[] + readonly json?: (value: TResult) => unknown + } + + /** + * Escape hatch for mode 7 (stdio protocol servers, e.g. `lsp`): the + * command owns stdin/stdout wholesale; events, presenters, and --json do + * not apply and the engine enforces that nothing else is declared. + */ + readonly raw?: false | { readonly reason: string } +} + +/** Identity function; exists so TypeScript infers the generics (R1). */ +export declare function defineCommand< + TFlags extends Record>, + TPositionals extends Record>, + TResult, + TConfig, +>( + def: CommandDefinition, +): CommandDefinition + +// ———————————————————————————————————————————————————————————————————————— +// §5 Presentation primitives — the R5 vocabulary (survey: card patterns) +// ———————————————————————————————————————————————————————————————————————— + +/** Deliberately small; grows by the same evidence rule as events. */ +export type Block = + | { readonly kind: 'summary'; readonly tone: 'ok' | 'error' | 'warning' | 'info'; readonly text: string } + | { readonly kind: 'fields'; readonly rows: ReadonlyArray<{ label: string; value: string; sensitive?: boolean }> } + | { readonly kind: 'table'; readonly columns: readonly string[]; readonly rows: ReadonlyArray } + | { readonly kind: 'list'; readonly items: readonly string[] } + | { readonly kind: 'nextSteps'; readonly steps: readonly string[] } + +/** Styling helpers usable inside block text; no direct writing. */ +export interface Ui { + readonly emphasize: (text: string) => string + readonly dim: (text: string) => string + readonly code: (text: string) => string +} + +// ———————————————————————————————————————————————————————————————————————— +// §6 Product export and shell mounting — R12: the shell owns the tree +// ———————————————————————————————————————————————————————————————————————— + +/** What a product package exports: named commands, no paths. */ +export type CommandSet = Readonly> + +/** + * Shell-side construction. Paths are space-separated (`'db migrate'`); + * group help text is declared with the mount, since groups belong to the + * tree, not to products. Collisions and grammar violations fail the build. + */ +export declare function createCli(spec: { + readonly name: string + readonly version: string + readonly groups: Readonly> + readonly commands: Readonly> +}): Cli + +export interface Cli { + /** + * Parse, execute, render, return the exit code (0/1/2/3 per R6; the + * caller assigns process.exitCode — the engine never exits or writes to + * anything but the provided streams). + */ + run(argv: readonly string[], runtime: Runtime): Promise +} + +/** Everything environmental, injected once by the bin (or by a test). */ +export interface Runtime { + readonly stdout: NodeJS.WritableStream + readonly stderr: NodeJS.WritableStream + readonly stdin: NodeJS.ReadableStream + readonly cwd: string + readonly isTty: { readonly stdin: boolean; readonly stderr: boolean } + readonly signal: AbortSignal + /** Loaded config + per-section diagnostics; the shell builds this via the + * unified loader (R10). Tests hand in fixtures. */ + readonly config: LoadedConfig + readonly credentials: Credentials | undefined +} + +export interface LoadedConfig { + readonly sections: Readonly> + readonly diagnostics: ReadonlyArray<{ readonly section: string | null; readonly error: CliStructuredError }> +} + +// ———————————————————————————————————————————————————————————————————————— +// §7 The product-repo test harness — R7: same machinery, bytes out +// ———————————————————————————————————————————————————————————————————————— + +export declare function createTestCli(spec: { + readonly commands: Readonly> + readonly groups?: Readonly> + readonly config?: Readonly> + readonly credentials?: Credentials +}): TestCli + +export interface TestCli { + run(argv: readonly string[], opts?: { readonly stdin?: string }): Promise<{ + readonly exitCode: number + readonly stdout: string + readonly stderr: string + /** The parsed --json event/envelope stream, when --json was passed. */ + readonly json: readonly unknown[] + /** Every EngineEvent the handler emitted, for semantic assertions. */ + readonly events: readonly EngineEvent[] + }> +} diff --git a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v2.ts b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v2.ts new file mode 100644 index 00000000..167cb5b6 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v2.ts @@ -0,0 +1,634 @@ +/** + * DRAFT v2 — the unified CLI engine's public interface, revised after the + * round-1 architect and principal-engineer reviews (see ./reviews/). + * + * Everything a product package imports for CLI purposes lives here (R3). + * Requirement references (R1–R14) point at docs/architecture/ + * cli-engine-requirements.md in prisma-cli. Nothing from stricli appears — + * it is an internal of the engine package. + * + * The execution protocol: a handler receives (args, context), emits zero + * or more events through context.report, and returns a Result when done. + * Sync commands emit nothing; progress and poll commands emit along the + * way (timeouts are the handler's business). Session commands + * (defineSessionCommand) keep emitting until context.signal fires, then + * clean up and return. Stdio protocol servers (defineRawCommand) bypass + * the protocol entirely, by declaration. Liveness display is the engine's: + * shown when a command runs quietly past a threshold. + * + * `--json` is an ENGINE MODE, not a flag products declare or handlers see + * (round-1 ruling). A value command supports it iff `present.json` exists + * (a session command always does — its stream is the JSON surface). In + * json mode the engine switches renderers, suppresses prompts (they fail + * structurally), and frames every event as one NDJSON line. The engine + * also auto-selects json mode when stdout is not a TTY — deliberate, + * agent-facing behavior. The engine injects the shared flag family on + * every command: --json, -q/--quiet, -v/--verbose, -y/--yes, + * --interactive/--no-interactive, --color/--no-color. Products cannot + * declare flags with those names. + * + * Exit codes (R6): 0 ok; 1 bug only; 2 expected structured failure; + * 3 user abort (Ctrl-C, or declining a gate the command cannot proceed + * without); 4–99 command-specific outcome codes (declared per command via + * `exitCode`); 130/143 delivered signals — the engine owns signal wiring + * and code selection. + */ + +// ———————————————————————————————————————————————————————————————————————— +// Foundation types (from the zero-dependency foundation package, not here; +// shown for reading convenience) +// ———————————————————————————————————————————————————————————————————————— + +import type { CliStructuredError, Result } from '@prisma/cli-foundation' + +/** The one severity scale (ADR 239's, the mature shipped one). Step + * outcomes are completion states, not severities — see EngineEvent. */ +export type Severity = 'error' | 'warn' | 'info' + +// ———————————————————————————————————————————————————————————————————————— +// §1 Next actions — the one remediation shape (round-1 ruling: the +// platform CLI's shipped form, adopted whole) +// ———————————————————————————————————————————————————————————————————————— + +export interface NextAction { + readonly kind: 'run-command' | 'user-choice' | 'edit-file' | 'done' + /** Open string with a recommended starter set (R-doc: journeys are + * grouping metadata; `kind` is the machine-branched field). */ + readonly journey: string + readonly label: string + readonly command?: string + readonly commands?: readonly string[] + readonly reason?: string +} + +// ———————————————————————————————————————————————————————————————————————— +// §2 Events — R14: one engine vocabulary, product extensions in `data` +// ———————————————————————————————————————————————————————————————————————— + +/** + * The engine event envelope. `kind`-specific fields are the common + * vocabulary the engine renders consistently (human mode) and frames + * (json mode: one NDJSON line per event, `{ type, command, timestamp, + * data }`). `data` is the product extension: passed through to machine + * consumers untouched, never interpreted by the engine, documented and + * versioned by the product as its own public API. A structure recurring + * inside `data` across commands is the promotion signal (R14). + * + * Rendering destinations, human mode: `output` events with channel + * 'data' are the command's data and go to OUR stdout (they are what + * `log tail > file` captures); every other event is commentary and goes + * to stderr. In json mode everything is one framed stream on stdout. + * + * Calling report() after the handler has resolved is a bug + * (InternalError) — the engine has sealed the envelope by then. Events + * emitted during teardown (after the signal, before resolution) are + * normal. + */ +export type EngineEvent = + /** A named phase began. `id`/`parentId` express nesting (the ORM's + * span shape); omitted for flat steps. */ + | { + readonly kind: 'step-started' + readonly step: string + readonly id?: string + readonly parentId?: string + readonly data?: unknown + } + /** The phase ended. `outcome` is a completion state and drives the + * ✔/✘/⚠/− glyph; it is not a severity. */ + | { + readonly kind: 'step-finished' + readonly step: string + readonly id?: string + readonly outcome: 'ok' | 'failed' | 'skipped' | 'warning' + readonly data?: unknown + } + /** Progress inside a phase (counts, not percentages — survey evidence). */ + | { + readonly kind: 'progress' + readonly step?: string + readonly completed: number + readonly total?: number + readonly data?: unknown + } + /** + * A line of commentary with a severity (round-1 ruling: 'warning' and + * 'notice' merged onto the one scale). severity 'warn' events are + * additionally aggregated by the engine into the success envelope's + * `warnings` — emit once, appear in both places. 'error' is not valid + * here: fatal problems are the Result's failure. + */ + | { + readonly kind: 'message' + readonly severity: Exclude + readonly text: string + readonly data?: unknown + } + /** + * Line-oriented output from a child process or remote stream. + * `channel` is semantic (round-1 fix): 'data' = the command's own + * output, routed to our stdout; 'diagnostic' = commentary about the + * run, routed to stderr. `source` names the emitter (service name, + * child binary), not a pipe. + */ + | { + readonly kind: 'output' + readonly source: string + readonly channel: 'data' | 'diagnostic' + readonly line: string + readonly data?: unknown + } + /** A user-actionable follow-up surfaced mid-run; the terminal ones + * belong on `present.next` instead. */ + | { readonly kind: 'remediation'; readonly action: NextAction; readonly data?: unknown } + /** A reachable endpoint became available. */ + | { + readonly kind: 'endpoint' + readonly name: string + readonly url: string + readonly data?: unknown + } + /** A state transition in a watched external process. `from` carries the + * prior state when known (survey: transitions, not snapshots). */ + | { + readonly kind: 'status' + readonly subject: string + readonly status: string + readonly from?: string + readonly data?: unknown + } + /** A file or directory this run wrote that the user may care about. */ + | { + readonly kind: 'artifact' + readonly path: string + readonly description?: string + readonly data?: unknown + } + +// ———————————————————————————————————————————————————————————————————————— +// §3 Config sections — R10 made structural (round-1: both passes' top gap) +// ———————————————————————————————————————————————————————————————————————— + +/** + * A product's named slice of prisma.config.ts. The token couples the + * section name, its validated type, and its never-throwing validator; + * commands bind to the token, which is how the engine knows which section + * a command needs — and therefore which diagnostics fail which commands. + */ +export interface ConfigSection { + readonly name: string + /** Total: any unknown in, diagnostics out. Never throws (R10). */ + readonly validate: (raw: unknown) => SectionValidation +} + +export type SectionValidation = + | { readonly ok: true; readonly value: T; readonly diagnostics: readonly CliStructuredError[] } + | { readonly ok: false; readonly diagnostics: readonly CliStructuredError[] } + +export declare function defineConfigSection(spec: { + readonly name: string + readonly validate: (raw: unknown) => SectionValidation +}): ConfigSection + +// ———————————————————————————————————————————————————————————————————————— +// §4 The handler context — R4: the whole world arrives as one argument +// ———————————————————————————————————————————————————————————————————————— + +export interface CommandContext { + /** The validated value of the command's declared config section + * (typed via the ConfigSection token), or undefined when the config + * file has no such section. A command with no declared section gets + * `undefined`. An INVALID needed section never reaches the handler — + * the engine already failed the command with that section's + * diagnostics. */ + readonly config: TConfig | undefined + + /** Management-API credentials, resolved at call time so long-lived + * sessions survive token refresh (round-1 fix). Undefined when the + * user is not authenticated. */ + readonly getCredentials: () => Promise + + /** The one way to emit while running (§2). */ + readonly report: (event: EngineEvent) => void + + /** Interactive input. In json mode, non-interactive mode, CI, or + * without a TTY, every method returns a structured error instead of + * prompting. Distinct codes distinguish "interaction unavailable" + * (exit 2) from "user cancelled the prompt" (engine maps to exit 3). */ + readonly prompt: PromptSurface + + /** Fires on Ctrl-C/SIGTERM (engine-owned wiring; the engine records + * which signal, for the 130/143 exit). Session commands run until it + * fires; everything else aborts in-flight work with it. */ + readonly signal: AbortSignal + + /** Where the user invoked the CLI. Products never read process.cwd(). */ + readonly cwd: string + + /** R13's probe: is this optional peer dependency importable from the + * user's project? Never throws; never installs anything. */ + readonly probeDependency: (specifier: string) => Promise +} + +export interface Credentials { + /** Opaque to the engine; shape owned by the Cloud product's auth + * library (placeholder pending its design). */ + readonly token: string + readonly workspaceId?: string +} + +export interface PromptSurface { + readonly confirm: (question: string) => Promise> + readonly select: ( + question: string, + options: ReadonlyArray<{ value: T; label: string }>, + ) => Promise> + readonly text: ( + question: string, + opts?: { placeholder?: string }, + ) => Promise> +} + +// ———————————————————————————————————————————————————————————————————————— +// §5 Flags and positionals — R1: directly executable, typed by inference +// ———————————————————————————————————————————————————————————————————————— + +/** + * Product-declared flags. The shared family (--json, --quiet, --verbose, + * --yes, --interactive, --color) is engine-injected and reserved — it + * never appears here and handlers never see those values; they change + * engine behavior, not handler input. Parse-time validation failures + * (bad enum value, non-numeric --timeout) become structured errors + * carrying the allowed values — never framework strings. + */ +export declare const flag: { + string(spec: { + brief: string + placeholder?: string + alias?: string + default?: string + }): FlagSpec + requiredString(spec: { brief: string; placeholder?: string; alias?: string }): FlagSpec + number(spec: { + brief: string + placeholder?: string + alias?: string + default?: number + }): FlagSpec + boolean(spec: { brief: string; alias?: string }): FlagSpec + enum(spec: { + brief: string + values: T + alias?: string + default?: T[number] + }): FlagSpec + repeated(spec: { brief: string; placeholder?: string; alias?: string }): FlagSpec +} + +declare const FLAG: unique symbol +export interface FlagSpec { + /** Phantom carrier for inference; exported so declaration emit works. */ + readonly [FLAG]: T +} +export { FLAG } + +export declare const positional: { + string(spec: { brief: string; placeholder: string }): PositionalSpec + optionalString(spec: { brief: string; placeholder: string }): PositionalSpec + /** Zero or more trailing values; at most one, last. */ + variadic(spec: { brief: string; placeholder: string }): PositionalSpec +} +declare const POSITIONAL: unique symbol +export interface PositionalSpec { + readonly [POSITIONAL]: T +} +export { POSITIONAL } + +/** + * What a handler receives. Flags and positionals live in separate + * namespaces (round-1 fix: no silent collisions, symmetric access): + * `args.flags.to`, `args.positionals.name`. + */ +export interface Args< + TFlags extends Record>, + TPositionals extends Record>, +> { + readonly flags: { readonly [K in keyof TFlags]: TFlags[K] extends FlagSpec ? T : never } + readonly positionals: { + readonly [K in keyof TPositionals]: TPositionals[K] extends PositionalSpec ? T : never + } +} + +// ———————————————————————————————————————————————————————————————————————— +// §6 Command definitions — light at startup (R9), path-free (R12) +// ———————————————————————————————————————————————————————————————————————— + +export interface CommandDefinition< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TResult = unknown, + TConfig = undefined, +> { + /** One line, imperative, shown in listings. */ + readonly brief: string + /** Paragraph(s) for --help. Words only — the engine formats. */ + readonly description?: string + /** Copy-pastable invocations, shown verbatim in help. */ + readonly examples?: readonly string[] + + readonly flags?: TFlags + readonly positionals?: TPositionals + + /** Binds the command to its product's config section (§3). The engine + * fails the command before loading the handler if this section is + * invalid; other sections' problems don't touch this command (R10). */ + readonly configSection?: ConfigSection + + /** The heavy part, loaded only at execution (R9). The module's default + * export is the handler — annotate it with CommandHandler + * (type-only import of the light definition; no runtime cycle). */ + readonly handler: () => Promise<{ default: Handler }> + + /** + * How a success Result renders (R5). The platform CLI's proven triple: + * `human` is required prose (engine writes to stderr); `stdout` is the + * machine-consumable data lines the engine writes to stdout — what + * --quiet leaves, what a pipe receives; `json` projects the envelope's + * `result` and defaults to the raw value. Its EXISTENCE is what makes + * the command support json mode. `next` supplies the envelope's + * nextActions; the human nextSteps are derived from them, so the two + * cannot disagree. + */ + readonly present: { + readonly human: (value: TResult, ui: Ui) => readonly Block[] + readonly stdout?: (value: TResult) => readonly string[] + readonly json?: (value: TResult) => unknown + readonly next?: (value: TResult) => readonly NextAction[] + } + + /** Command-specific outcome code (4–99), a pure function of the success + * value; omit for plain 0. (`migration check` exits 4 on drift.) */ + readonly exitCode?: (value: TResult) => number +} + +export type Handler< + TFlags extends Record>, + TPositionals extends Record>, + TResult, + TConfig, +> = ( + args: Args, + ctx: CommandContext, +) => Promise> + +/** For impl files: `const run: CommandHandler = …` + * — keeps definition and handler in lockstep without a runtime cycle. */ +export type CommandHandler = D extends CommandDefinition< + infer F, + infer P, + infer R, + infer C +> + ? Handler + : never + +export declare function defineCommand< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TResult = unknown, + TConfig = undefined, +>(def: CommandDefinition): CommandDefinition + +/** + * Mode 4 — sessions (dev, log tail): the handler runs until the signal + * fires, speaks entirely through events, and returns Result. There + * is no `present` — the engine owns the standard close-out line — and no + * `exitCode` (0, or the failure's code, or the signal's). A session + * always supports json mode: the event stream is its json surface. + */ +export interface SessionCommandDefinition< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, +> { + readonly brief: string + readonly description?: string + readonly examples?: readonly string[] + readonly flags?: TFlags + readonly positionals?: TPositionals + readonly configSection?: ConfigSection + readonly handler: () => Promise<{ + default: Handler + }> +} + +export declare function defineSessionCommand< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, +>(def: SessionCommandDefinition): SessionCommandDefinition + +/** + * Mode 7 — stdio protocol servers (lsp): the command owns stdin/stdout + * wholesale. Events, presenters, json mode, and prompts do not apply; + * the handler returns the exit code directly. Flags are allowed (an lsp + * takes options); the shared flag family is NOT injected. + */ +export interface RawCommandDefinition< + TFlags extends Record> = {}, +> { + readonly brief: string + readonly description?: string + readonly flags?: TFlags + readonly handler: () => Promise<{ + default: ( + args: Args, + io: { + readonly stdin: NodeJS.ReadableStream + readonly stdout: NodeJS.WritableStream + readonly stderr: NodeJS.WritableStream + readonly signal: AbortSignal + readonly cwd: string + }, + ) => Promise + }> +} + +export declare function defineRawCommand< + TFlags extends Record> = {}, +>(def: RawCommandDefinition): RawCommandDefinition + +/** Erased union for mount maps and command sets (round-1 fix: concrete + * definitions are assignable here; the generics live on define*). */ +export type AnyCommand = + | CommandDefinition + | SessionCommandDefinition + | RawCommandDefinition + +// ———————————————————————————————————————————————————————————————————————— +// §7 Presentation primitives — the R5 vocabulary +// ———————————————————————————————————————————————————————————————————————— + +/** Deliberately small; grows by the same evidence rule as events. */ +export type Block = + | { + readonly kind: 'summary' + readonly tone: 'ok' | Severity + readonly text: string + } + | { + readonly kind: 'fields' + readonly rows: ReadonlyArray<{ label: string; value: string; sensitive?: boolean }> + } + | { + readonly kind: 'table' + readonly columns: readonly string[] + readonly rows: ReadonlyArray + } + | { readonly kind: 'list'; readonly items: readonly string[] } + /** The corpus's most-rendered human structure (migration graphs, + * service trees). */ + | { readonly kind: 'tree'; readonly roots: readonly TreeNode[] } + +export interface TreeNode { + readonly label: string + readonly children?: readonly TreeNode[] +} + +/** Styling helpers usable inside block text; no direct writing. */ +export interface Ui { + readonly emphasize: (text: string) => string + readonly dim: (text: string) => string + readonly code: (text: string) => string +} + +// ———————————————————————————————————————————————————————————————————————— +// §8 Envelopes — the json contract (Layer 6, platform-proven) +// ———————————————————————————————————————————————————————————————————————— + +export interface SuccessEnvelope { + readonly ok: true + /** Stable dotted command id derived from the mount path ('db.migrate'). */ + readonly command: string + readonly result: T + /** Aggregated from severity-'warn' message events. */ + readonly warnings: readonly string[] + /** Derived from nextActions — the human-string form. */ + readonly nextSteps: readonly string[] + readonly nextActions: readonly NextAction[] +} + +export interface ErrorEnvelope { + readonly ok: false + readonly command: string + /** The CliErrorEnvelope fields (code, severity, summary, why, fix, + * where, meta, docsUrl) — nested, per the settled envelope rule. */ + readonly error: unknown + readonly warnings: readonly string[] + readonly nextSteps: readonly string[] + readonly nextActions: readonly NextAction[] +} + +/** One NDJSON line per event in json mode. */ +export interface EventFrame { + readonly type: EngineEvent['kind'] + readonly command: string + /** ISO 8601 UTC. Injectable clock in tests (§10). */ + readonly timestamp: string + readonly data: EngineEvent +} + +// ———————————————————————————————————————————————————————————————————————— +// §9 Product export and shell mounting — R12: the shell owns the tree +// ———————————————————————————————————————————————————————————————————————— + +/** What a product package exports: NAMED commands, no paths. */ +export type CommandSet = Readonly> + +/** + * Shell-side construction. Mount keys are space-separated paths + * ('db migrate'); group help text is declared with the mount, since + * groups belong to the tree, not to products. Collisions, unknown + * groups, and grammar violations fail construction (build time, not + * run time). + */ +export declare function createCli(spec: { + readonly name: string + readonly version: string + readonly groups: Readonly> + readonly commands: Readonly> +}): Cli + +export interface Cli { + /** + * Parse, execute, render, return the exit code. The engine never calls + * process.exit and never touches streams other than the ones provided. + * Auto-selects json mode when runtime.isTty.stdout is false (deliberate + * agent-facing behavior), unless the command is raw. + */ + run(argv: readonly string[], runtime: Runtime): Promise +} + +/** Everything environmental, injected once by the bin (or by a test). */ +export interface Runtime { + readonly stdout: NodeJS.WritableStream + readonly stderr: NodeJS.WritableStream + readonly stdin: NodeJS.ReadableStream + readonly cwd: string + readonly env: Readonly> + readonly isTty: { readonly stdin: boolean; readonly stdout: boolean; readonly stderr: boolean } + readonly signal: AbortSignal + /** Loaded config + per-section diagnostics; the shell builds this via + * the unified loader (R10). Tests hand in fixtures. */ + readonly config: LoadedConfig + readonly getCredentials: () => Promise +} + +export interface LoadedConfig { + /** Raw section values by name; validation happens per command via its + * ConfigSection token. */ + readonly sections: Readonly> + /** File-level problems (unevaluable module, missing version marker) — + * section: null fails every command. */ + readonly diagnostics: ReadonlyArray<{ + readonly section: string | null + readonly error: CliStructuredError + }> +} + +// ———————————————————————————————————————————————————————————————————————— +// §10 The product-repo test harness — R7: same machinery, bytes out +// ———————————————————————————————————————————————————————————————————————— + +export declare function createTestCli(spec: { + readonly commands: Readonly> + readonly groups?: Readonly> + readonly config?: Readonly> + readonly credentials?: Credentials + /** Fixed clock for deterministic EventFrame timestamps. */ + readonly now?: () => Date +}): TestCli + +export interface TestCli { + run( + argv: readonly string[], + opts?: { + readonly stdin?: string + /** Scripted prompt answers, consumed in order; a run that prompts + * past the script fails the test. */ + readonly answers?: ReadonlyArray + /** Abort the run (session tests): fires the context signal. */ + readonly abort?: AbortSignal + readonly isTty?: { stdin?: boolean; stdout?: boolean; stderr?: boolean } + readonly env?: Readonly> + }, + ): Promise<{ + readonly exitCode: number + readonly stdout: string + readonly stderr: string + /** Parsed json output (envelope + event frames) when json mode was on. */ + readonly json: readonly unknown[] + /** Every EngineEvent the handler emitted, for semantic assertions. */ + readonly events: readonly EngineEvent[] + }> +} diff --git a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v3.ts b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v3.ts new file mode 100644 index 00000000..b6468b8c --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v3.ts @@ -0,0 +1,671 @@ +/** + * DRAFT v3 — the unified CLI engine's public interface. + * v1: initial. v2: round-1 review fixes. v3: presentation moved to the + * return site (operator ruling): handlers materialize the active mode's + * views via ctx.present at the point where the outcome is known; the + * definition carries no presenters and no result generic. Prior versions + * preserved as -v1.ts / -v2.ts; round-1 artifacts in ./reviews/. + * + * Everything a product package imports for CLI purposes lives here (R3). + * Requirement references (R1–R14) point at docs/architecture/ + * cli-engine-requirements.md in prisma-cli. Nothing from stricli appears — + * it is an internal of the engine package. + * + * The execution protocol: a handler receives (args, context), emits zero + * or more events through context.report, and returns a Result when done — + * a PresentedResult built with ctx.present, carrying pure data plus the + * materialized views the active mode needs. Sync commands emit nothing; + * progress and poll commands emit along the way (timeouts are the + * handler's business). Session commands (defineSessionCommand) keep + * emitting until context.signal fires, then clean up and return. Stdio + * protocol servers (defineRawCommand) bypass the protocol by declaration. + * Liveness display is the engine's: shown when a command runs quietly + * past a threshold. Nothing product-authored executes after the handler + * resolves — the engine receives values, never callbacks. + * + * `--json` is an ENGINE MODE, not a flag products declare or handlers + * branch on. Every value command is json-capable by construction: the + * envelope's `result` is the presented data, with `views.json` as an + * optional override. In json mode the engine switches renderers, + * suppresses prompts (they fail structurally), and frames every event as + * one NDJSON line. The engine auto-selects json mode when stdout is not + * a TTY — deliberate, agent-facing behavior. The engine injects the + * shared flag family on every non-raw command: --json, -q/--quiet, + * -v/--verbose, -y/--yes, --interactive/--no-interactive, + * --color/--no-color. Products cannot declare flags with those names. + * + * Exit codes (R6): 0 ok; 1 bug only; 2 expected structured failure; + * 3 user abort (Ctrl-C, or cancelling a gate the command cannot proceed + * without); 4–99 command-specific outcome codes (declared per command + * via `exitCode`); 130/143 delivered signals — the engine owns signal + * wiring and code selection. + */ + +// ———————————————————————————————————————————————————————————————————————— +// Foundation types (from the zero-dependency foundation package, not here; +// shown for reading convenience) +// ———————————————————————————————————————————————————————————————————————— + +import type { CliStructuredError, Result } from '@prisma/cli-foundation' + +/** The one severity scale (ADR 239's, the mature shipped one). Step + * outcomes are completion states, not severities — see EngineEvent. */ +export type Severity = 'error' | 'warn' | 'info' + +// ———————————————————————————————————————————————————————————————————————— +// §1 Next actions — the one remediation shape (the platform CLI's shipped +// form, adopted whole) +// ———————————————————————————————————————————————————————————————————————— + +export interface NextAction { + readonly kind: 'run-command' | 'user-choice' | 'edit-file' | 'done' + /** Open string with a recommended starter set (journeys are grouping + * metadata; `kind` is the machine-branched field). */ + readonly journey: string + readonly label: string + readonly command?: string + readonly commands?: readonly string[] + readonly reason?: string +} + +// ———————————————————————————————————————————————————————————————————————— +// §2 Events — R14: one engine vocabulary, product extensions in `data` +// ———————————————————————————————————————————————————————————————————————— + +/** + * The engine event envelope. `kind`-specific fields are the common + * vocabulary the engine renders consistently (human mode) and frames + * (json mode: one NDJSON line per event, `{ type, command, timestamp, + * data }`). `data` is the product extension: passed through to machine + * consumers untouched, never interpreted by the engine, documented and + * versioned by the product as its own public API. A structure recurring + * inside `data` across commands is the promotion signal (R14). + * + * Rendering destinations, human mode: `output` events with channel + * 'data' are the command's data and go to OUR stdout (they are what + * `log tail > file` captures); every other event is commentary and goes + * to stderr. In json mode everything is one framed stream on stdout. + * + * Calling report() after the handler has resolved is a bug + * (InternalError) — the engine has sealed the envelope by then. Events + * emitted during teardown (after the signal, before resolution) are + * normal. + */ +export type EngineEvent = + /** A named phase began. `id`/`parentId` express nesting (the ORM's + * span shape); omitted for flat steps. */ + | { + readonly kind: 'step-started' + readonly step: string + readonly id?: string + readonly parentId?: string + readonly data?: unknown + } + /** The phase ended. `outcome` is a completion state and drives the + * ✔/✘/⚠/− glyph; it is not a severity. */ + | { + readonly kind: 'step-finished' + readonly step: string + readonly id?: string + readonly outcome: 'ok' | 'failed' | 'skipped' | 'warning' + readonly data?: unknown + } + /** Progress inside a phase (counts, not percentages — survey evidence). */ + | { + readonly kind: 'progress' + readonly step?: string + readonly completed: number + readonly total?: number + readonly data?: unknown + } + /** + * A line of commentary with a severity ('warning' and 'notice' merged + * onto the one scale). severity 'warn' events are additionally + * aggregated by the engine into the success envelope's `warnings` — + * emit once, appear in both places. 'error' is not valid here: fatal + * problems are the Result's failure. + */ + | { + readonly kind: 'message' + readonly severity: Exclude + readonly text: string + readonly data?: unknown + } + /** + * Line-oriented output from a child process or remote stream. + * `channel` is semantic: 'data' = the command's own output, routed to + * our stdout; 'diagnostic' = commentary about the run, routed to + * stderr. `source` names the emitter (service name, child binary), + * not a pipe. + */ + | { + readonly kind: 'output' + readonly source: string + readonly channel: 'data' | 'diagnostic' + readonly line: string + readonly data?: unknown + } + /** A user-actionable follow-up surfaced mid-run; terminal ones belong + * in the presented views' `next` instead. */ + | { readonly kind: 'remediation'; readonly action: NextAction; readonly data?: unknown } + /** A reachable endpoint became available. */ + | { + readonly kind: 'endpoint' + readonly name: string + readonly url: string + readonly data?: unknown + } + /** A state transition in a watched external process. `from` carries the + * prior state when known (survey: transitions, not snapshots). */ + | { + readonly kind: 'status' + readonly subject: string + readonly status: string + readonly from?: string + readonly data?: unknown + } + /** A file or directory this run wrote that the user may care about. */ + | { + readonly kind: 'artifact' + readonly path: string + readonly description?: string + readonly data?: unknown + } + +// ———————————————————————————————————————————————————————————————————————— +// §3 Presented results — presentation materializes at the return site +// ———————————————————————————————————————————————————————————————————————— + +/** + * What a value command's handler returns inside `ok(...)`: pure data plus + * the views the ACTIVE MODE already materialized. Built exclusively by + * ctx.present — the context knows the mode, calls only the view functions + * that mode needs, and the result crossing the product→engine boundary is + * values all the way down (serializable, snapshotable, no callbacks). + * + * `data` is always present and is always what the envelope's `result` + * serializes (json view overrides when supplied). View materialization by + * mode: human mode → human + stdout + next; --quiet → stdout; + * json mode → json + next. + */ +export interface PresentedResult { + readonly data: T + readonly views: { + readonly human?: readonly Block[] + readonly stdout?: readonly string[] + readonly json?: unknown + readonly next?: readonly NextAction[] + } +} + +/** + * The view functions a handler supplies to ctx.present. Only the active + * mode's functions are invoked, at the return site, where the outcome and + * its context are live — no case-reconstruction in a distant presenter. + * `human` composes engine primitives (R5: Block is the only vocabulary); + * `stdout` is the machine-consumable data lines the engine writes to + * stdout — what --quiet leaves, what a pipe receives; `json` overrides + * the envelope's `result` (default: the data itself); `next` supplies + * nextActions — the envelope's human nextSteps derive from them. + */ +export interface Views { + readonly human: (ui: Ui) => readonly Block[] + readonly stdout?: () => readonly string[] + readonly json?: () => unknown + readonly next?: () => readonly NextAction[] +} + +// ———————————————————————————————————————————————————————————————————————— +// §4 Config sections — R10 made structural +// ———————————————————————————————————————————————————————————————————————— + +/** + * A product's named slice of prisma.config.ts. The token couples the + * section name, its validated type, and its never-throwing validator; + * commands bind to the token, which is how the engine knows which section + * a command needs — and therefore which diagnostics fail which commands. + */ +export interface ConfigSection { + readonly name: string + /** Total: any unknown in, diagnostics out. Never throws (R10). */ + readonly validate: (raw: unknown) => SectionValidation +} + +export type SectionValidation = + | { readonly ok: true; readonly value: T; readonly diagnostics: readonly CliStructuredError[] } + | { readonly ok: false; readonly diagnostics: readonly CliStructuredError[] } + +export declare function defineConfigSection(spec: { + readonly name: string + readonly validate: (raw: unknown) => SectionValidation +}): ConfigSection + +// ———————————————————————————————————————————————————————————————————————— +// §5 The handler context — R4: the whole world arrives as one argument +// ———————————————————————————————————————————————————————————————————————— + +export interface CommandContext { + /** The validated value of the command's declared config section (typed + * via the ConfigSection token), or undefined when the config file has + * no such section. An INVALID needed section never reaches the + * handler — the engine already failed the command with that section's + * diagnostics. */ + readonly config: TConfig | undefined + + /** Builds the PresentedResult for the active mode: calls only the view + * functions this mode needs, at the return site. The only constructor + * of PresentedResult. */ + readonly present: (data: T, views: Views) => PresentedResult + + /** Management-API credentials, resolved at call time so long-lived + * sessions survive token refresh. Undefined when unauthenticated. */ + readonly getCredentials: () => Promise + + /** The one way to emit while running (§2). */ + readonly report: (event: EngineEvent) => void + + /** Interactive input. In json mode, non-interactive mode, CI, or + * without a TTY, every method returns a structured error instead of + * prompting. Distinct codes distinguish "interaction unavailable" + * (exit 2) from "user cancelled the prompt" (engine maps to exit 3). */ + readonly prompt: PromptSurface + + /** Fires on Ctrl-C/SIGTERM (engine-owned wiring; the engine records + * which signal, for the 130/143 exit). Session commands run until it + * fires; everything else aborts in-flight work with it. */ + readonly signal: AbortSignal + + /** Where the user invoked the CLI. Products never read process.cwd(). */ + readonly cwd: string + + /** R13's probe: is this optional peer dependency importable from the + * user's project? Never throws; never installs anything. */ + readonly probeDependency: (specifier: string) => Promise +} + +export interface Credentials { + /** Opaque to the engine; shape owned by the Cloud product's auth + * library (placeholder pending its design). */ + readonly token: string + readonly workspaceId?: string +} + +export interface PromptSurface { + readonly confirm: (question: string) => Promise> + readonly select: ( + question: string, + options: ReadonlyArray<{ value: T; label: string }>, + ) => Promise> + readonly text: ( + question: string, + opts?: { placeholder?: string }, + ) => Promise> +} + +// ———————————————————————————————————————————————————————————————————————— +// §6 Flags and positionals — R1: directly executable, typed by inference +// ———————————————————————————————————————————————————————————————————————— + +/** + * Product-declared flags. The shared family (--json, --quiet, --verbose, + * --yes, --interactive, --color) is engine-injected and reserved — it + * never appears here and handlers never see those values; they change + * engine behavior, not handler input. Parse-time validation failures + * (bad enum value, non-numeric --timeout) become structured errors + * carrying the allowed values — never framework strings. + */ +export declare const flag: { + string(spec: { + brief: string + placeholder?: string + alias?: string + default?: string + }): FlagSpec + requiredString(spec: { brief: string; placeholder?: string; alias?: string }): FlagSpec + number(spec: { + brief: string + placeholder?: string + alias?: string + default?: number + }): FlagSpec + boolean(spec: { brief: string; alias?: string }): FlagSpec + enum(spec: { + brief: string + values: T + alias?: string + default?: T[number] + }): FlagSpec + repeated(spec: { brief: string; placeholder?: string; alias?: string }): FlagSpec +} + +declare const FLAG: unique symbol +export interface FlagSpec { + /** Phantom carrier for inference; exported so declaration emit works. */ + readonly [FLAG]: T +} +export { FLAG } + +export declare const positional: { + string(spec: { brief: string; placeholder: string }): PositionalSpec + optionalString(spec: { brief: string; placeholder: string }): PositionalSpec + /** Zero or more trailing values; at most one, last. */ + variadic(spec: { brief: string; placeholder: string }): PositionalSpec +} +declare const POSITIONAL: unique symbol +export interface PositionalSpec { + readonly [POSITIONAL]: T +} +export { POSITIONAL } + +/** + * What a handler receives. Flags and positionals live in separate + * namespaces (no silent collisions, symmetric access): `args.flags.to`, + * `args.positionals.name`. + */ +export interface Args< + TFlags extends Record>, + TPositionals extends Record>, +> { + readonly flags: { readonly [K in keyof TFlags]: TFlags[K] extends FlagSpec ? T : never } + readonly positionals: { + readonly [K in keyof TPositionals]: TPositionals[K] extends PositionalSpec ? T : never + } +} + +// ———————————————————————————————————————————————————————————————————————— +// §7 Command definitions — light at startup (R9), path-free (R12), and +// free of the result type (presentation lives at the return site) +// ———————————————————————————————————————————————————————————————————————— + +export interface CommandDefinition< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, +> { + /** One line, imperative, shown in listings. */ + readonly brief: string + /** Paragraph(s) for --help. Words only — the engine formats. */ + readonly description?: string + /** Copy-pastable invocations, shown verbatim in help. */ + readonly examples?: readonly string[] + + readonly flags?: TFlags + readonly positionals?: TPositionals + + /** Binds the command to its product's config section (§4). The engine + * fails the command before loading the handler if this section is + * invalid; other sections' problems don't touch this command (R10). */ + readonly configSection?: ConfigSection + + /** The heavy part, loaded only at execution (R9). The module's default + * export is the handler — annotate it with CommandHandler + * (type-only import of the light definition; no runtime cycle). */ + readonly handler: () => Promise<{ default: Handler }> + + /** Command-specific outcome code (4–99), a pure function of the + * presented data; omit for plain 0. (`migration check` exits 4 on + * drift.) */ + readonly exitCode?: (data: unknown) => number +} + +export type Handler< + TFlags extends Record>, + TPositionals extends Record>, + TConfig, +> = ( + args: Args, + ctx: CommandContext, +) => Promise, CliStructuredError>> + +/** For impl files: `const run: CommandHandler = …` + * — keeps definition and handler in lockstep without a runtime cycle. */ +export type CommandHandler = D extends CommandDefinition + ? Handler + : never + +export declare function defineCommand< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, +>(def: CommandDefinition): CommandDefinition + +/** + * Mode 4 — sessions (dev, log tail): the handler runs until the signal + * fires, speaks entirely through events, and returns Result. There + * is no presentation — the engine owns the standard close-out line — and + * no exitCode (0, or the failure's code, or the signal's). A session + * always supports json mode: the event stream is its json surface. + */ +export interface SessionCommandDefinition< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, +> { + readonly brief: string + readonly description?: string + readonly examples?: readonly string[] + readonly flags?: TFlags + readonly positionals?: TPositionals + readonly configSection?: ConfigSection + readonly handler: () => Promise<{ + default: ( + args: Args, + ctx: CommandContext, + ) => Promise> + }> +} + +export declare function defineSessionCommand< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, +>(def: SessionCommandDefinition): SessionCommandDefinition + +/** + * Mode 7 — stdio protocol servers (lsp): the command owns stdin/stdout + * wholesale. Events, presentation, json mode, and prompts do not apply; + * the handler returns the exit code directly. Flags are allowed (an lsp + * takes options); the shared flag family is NOT injected. + */ +export interface RawCommandDefinition< + TFlags extends Record> = {}, +> { + readonly brief: string + readonly description?: string + readonly flags?: TFlags + readonly handler: () => Promise<{ + default: ( + args: Args, + io: { + readonly stdin: NodeJS.ReadableStream + readonly stdout: NodeJS.WritableStream + readonly stderr: NodeJS.WritableStream + readonly signal: AbortSignal + readonly cwd: string + }, + ) => Promise + }> +} + +export declare function defineRawCommand< + TFlags extends Record> = {}, +>(def: RawCommandDefinition): RawCommandDefinition + +/** Erased union for mount maps and command sets (concrete definitions are + * assignable here; the generics live on define*). */ +export type AnyCommand = + | CommandDefinition + | SessionCommandDefinition + | RawCommandDefinition + +// ———————————————————————————————————————————————————————————————————————— +// §8 Presentation primitives — the R5 vocabulary +// ———————————————————————————————————————————————————————————————————————— + +/** Deliberately small; grows by the same evidence rule as events. */ +export type Block = + | { + readonly kind: 'summary' + readonly tone: 'ok' | Severity + readonly text: string + } + | { + readonly kind: 'fields' + readonly rows: ReadonlyArray<{ label: string; value: string; sensitive?: boolean }> + } + | { + readonly kind: 'table' + readonly columns: readonly string[] + readonly rows: ReadonlyArray + } + | { readonly kind: 'list'; readonly items: readonly string[] } + /** The corpus's most-rendered human structure (migration graphs, + * service trees). */ + | { readonly kind: 'tree'; readonly roots: readonly TreeNode[] } + +export interface TreeNode { + readonly label: string + readonly children?: readonly TreeNode[] +} + +/** Styling helpers usable inside block text; no direct writing. */ +export interface Ui { + readonly emphasize: (text: string) => string + readonly dim: (text: string) => string + readonly code: (text: string) => string +} + +// ———————————————————————————————————————————————————————————————————————— +// §9 Envelopes — the json contract (Layer 6, platform-proven) +// ———————————————————————————————————————————————————————————————————————— + +export interface SuccessEnvelope { + readonly ok: true + /** Stable dotted command id derived from the mount path ('db.migrate'). */ + readonly command: string + /** The presented data (json view override when supplied). */ + readonly result: T + /** Aggregated from severity-'warn' message events. */ + readonly warnings: readonly string[] + /** Derived from nextActions — the human-string form. */ + readonly nextSteps: readonly string[] + readonly nextActions: readonly NextAction[] +} + +export interface ErrorEnvelope { + readonly ok: false + readonly command: string + /** The CliErrorEnvelope fields (code, severity, summary, why, fix, + * where, meta, docsUrl) — nested, per the settled envelope rule. */ + readonly error: unknown + readonly warnings: readonly string[] + readonly nextSteps: readonly string[] + readonly nextActions: readonly NextAction[] +} + +/** One NDJSON line per event in json mode. */ +export interface EventFrame { + readonly type: EngineEvent['kind'] + readonly command: string + /** ISO 8601 UTC. Injectable clock in tests (§11). */ + readonly timestamp: string + readonly data: EngineEvent +} + +// ———————————————————————————————————————————————————————————————————————— +// §10 Product export and shell mounting — R12: the shell owns the tree +// ———————————————————————————————————————————————————————————————————————— + +/** What a product package exports: NAMED commands, no paths. */ +export type CommandSet = Readonly> + +/** + * Shell-side construction. Mount keys are space-separated paths + * ('db migrate'); group help text is declared with the mount, since + * groups belong to the tree, not to products. Collisions, unknown + * groups, and grammar violations fail construction (build time, not + * run time). + */ +export declare function createCli(spec: { + readonly name: string + readonly version: string + readonly groups: Readonly> + readonly commands: Readonly> +}): Cli + +export interface Cli { + /** + * Parse, execute, render, return the exit code. The engine never calls + * process.exit and never touches streams other than the ones provided. + * Auto-selects json mode when runtime.isTty.stdout is false (deliberate + * agent-facing behavior), unless the command is raw. + */ + run(argv: readonly string[], runtime: Runtime): Promise +} + +/** Everything environmental, injected once by the bin (or by a test). */ +export interface Runtime { + readonly stdout: NodeJS.WritableStream + readonly stderr: NodeJS.WritableStream + readonly stdin: NodeJS.ReadableStream + readonly cwd: string + readonly env: Readonly> + readonly isTty: { readonly stdin: boolean; readonly stdout: boolean; readonly stderr: boolean } + readonly signal: AbortSignal + /** Loaded config + per-section diagnostics; the shell builds this via + * the unified loader (R10). Tests hand in fixtures. */ + readonly config: LoadedConfig + readonly getCredentials: () => Promise +} + +export interface LoadedConfig { + /** Raw section values by name; validation happens per command via its + * ConfigSection token. */ + readonly sections: Readonly> + /** File-level problems (unevaluable module, missing version marker) — + * section: null fails every command. */ + readonly diagnostics: ReadonlyArray<{ + readonly section: string | null + readonly error: CliStructuredError + }> +} + +// ———————————————————————————————————————————————————————————————————————— +// §11 The product-repo test harness — R7: same machinery, bytes out +// ———————————————————————————————————————————————————————————————————————— + +export declare function createTestCli(spec: { + readonly commands: Readonly> + readonly groups?: Readonly> + readonly config?: Readonly> + readonly credentials?: Credentials + /** Fixed clock for deterministic EventFrame timestamps. */ + readonly now?: () => Date +}): TestCli + +export interface TestCli { + run( + argv: readonly string[], + opts?: { + readonly stdin?: string + /** Scripted prompt answers, consumed in order; a run that prompts + * past the script fails the test. */ + readonly answers?: ReadonlyArray + /** Abort the run (session tests): fires the context signal. */ + readonly abort?: AbortSignal + readonly isTty?: { stdin?: boolean; stdout?: boolean; stderr?: boolean } + readonly env?: Readonly> + }, + ): Promise<{ + readonly exitCode: number + readonly stdout: string + readonly stderr: string + /** Parsed json output (envelope + event frames) when json mode was on. */ + readonly json: readonly unknown[] + /** Every EngineEvent the handler emitted, for semantic assertions. */ + readonly events: readonly EngineEvent[] + /** The PresentedResult the handler returned (data + materialized + * views), for semantic assertions without byte-scraping. */ + readonly presented?: PresentedResult + }> +} diff --git a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v4.ts b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v4.ts new file mode 100644 index 00000000..e64b8e98 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v4.ts @@ -0,0 +1,771 @@ +/** + * DRAFT v4 — the unified CLI engine's public interface. + * v1 initial · v2 round-1 fixes · v3 return-site presentation · + * v4 round-2 fixes + operator rulings: completed/errored semantics, + * --format with --json alias, one log-level mechanism, prompt defaults + * under --yes, "presentations" naming, outcome-code catalogue. + * Prior versions preserved as -v1/-v2/-v3.ts; review artifacts in + * ./reviews/. + * + * Everything a product package imports for CLI purposes lives here (R3). + * Requirement references (R1–R14) point at docs/architecture/ + * cli-engine-requirements.md in prisma-cli. Nothing from stricli appears — + * it is an internal of the engine package. + * + * EXECUTION PROTOCOL. A handler receives (args, context), emits zero or + * more events through context.report, and finishes one of two ways: + * + * COMPLETED — it returns ok(ctx.present(data, presentations)): the + * command executed to its end and has a result. A completed result may + * still be bad news; it carries an outcome code from the command's + * documented catalogue (`migration check` completes, presents its + * findings like any result, and exits 4). Presentation always runs for + * completed results. + * + * ERRORED — it returns notOk(structuredError): the command did not + * complete. The engine renders the error envelope (code, summary, why, + * fix); there is no product presentation on the error path. + * `remediation` events emitted before the error are aggregated into + * the error envelope's nextActions, as `warn` messages are into + * warnings — so guidance survives without a second presentation system. + * + * Session commands (defineSessionCommand) keep emitting until + * context.signal fires, then clean up and return. Stdio protocol servers + * (defineRawCommand) bypass the protocol by declaration. Liveness display + * is the engine's (shown when a command runs quietly past a threshold). + * Nothing product-authored executes after the handler resolves — the + * engine receives values, never callbacks. + * + * FORMATS AND LEVELS. The output format is an engine mode: + * `--format `, auto-selected when unspecified (human on a + * TTY stdout, json otherwise — deliberate agent-facing behavior); + * `--json` is shorthand for `--format json`. In json mode the engine + * suppresses prompts (they fail structurally) and frames every event as + * one NDJSON line. Commentary is filtered by `--log-level + * ` (default info); `--verbose` is shorthand + * for `--log-level verbose`. The engine injects the shared flag family + * on every non-raw command: --format/--json, --log-level/-v/--verbose, + * -q/--quiet, -y/--yes, --interactive/--no-interactive, + * --color/--no-color. Products cannot declare flags with those names. + * Product flag keys are camelCase and transliterate to --kebab-case. + * + * EXIT CODES (R6): 0 completed; 1 bug only; 2 errored (expected, + * structured); 3 user abort (Ctrl-C, or cancelling a prompt the command + * cannot proceed without); 4–99 outcome codes from the command's + * catalogue; 130/143 delivered signals. The engine owns signal wiring: + * first signal fires context.signal and awaits handler teardown; a + * second signal exits immediately with the signal's code. + */ + +// ———————————————————————————————————————————————————————————————————————— +// Foundation types (from the zero-dependency foundation package — which +// also owns NextAction, so the engine and the error envelope share it +// without a package cycle). Shown for reading convenience. +// ———————————————————————————————————————————————————————————————————————— + +import type { CliStructuredError, NextAction, Result } from '@prisma/cli-foundation' + +/** The one severity scale for commentary; also the log-level axis + * (ADR 239's error|warn|info, extended with verbose for detail + * commentary). Step outcomes are completion states, not severities. */ +export type Severity = 'error' | 'warn' | 'info' | 'verbose' +export type LogLevel = Severity + +export type Format = 'human' | 'json' + +// ———————————————————————————————————————————————————————————————————————— +// §1 Events — R14: one engine vocabulary, product extensions in `data` +// ———————————————————————————————————————————————————————————————————————— + +/** + * The engine event envelope. `kind`-specific fields are the common + * vocabulary the engine renders consistently (human mode) and frames + * (json mode). `data` is the product extension: passed through to + * machine consumers untouched, never interpreted by the engine, + * documented and versioned by the product as its own public API. A + * structure recurring inside `data` across commands is the promotion + * signal (R14). + * + * Rendering, human mode: `output` events with channel 'data' are the + * command's data and go to OUR stdout (what `log tail > file` captures); + * everything else is commentary on stderr, filtered by the active log + * level (`message` events by their severity; other kinds display at + * info). In json mode everything is framed on stdout (§9). + * + * report() is synchronous fire-and-forget; the engine buffers and writes + * asynchronously (no backpressure signal — accepted trade). Calling it + * after the handler has resolved is a bug (InternalError). Events during + * teardown (after the signal, before resolution) are normal. + */ +export type EngineEvent = + /** A named phase began. `id`/`parentId` express nesting; omitted for + * flat steps. */ + | { + readonly kind: 'step-started' + readonly step: string + readonly id?: string + readonly parentId?: string + readonly data?: unknown + } + /** The phase ended. `outcome` is a completion state and drives the + * ✔/✘/⚠/− glyph; it is not a severity. */ + | { + readonly kind: 'step-finished' + readonly step: string + readonly id?: string + readonly outcome: 'ok' | 'failed' | 'skipped' | 'warning' + readonly data?: unknown + } + /** Progress inside a phase (counts, not percentages). */ + | { + readonly kind: 'progress' + readonly step?: string + readonly completed: number + readonly total?: number + readonly data?: unknown + } + /** + * A line of commentary at a severity. 'warn' messages are additionally + * aggregated into the envelope's `warnings`; 'verbose' messages render + * only at --log-level verbose. 'error' is not valid here: fatal + * problems are the Result's error. + */ + | { + readonly kind: 'message' + readonly severity: Exclude + readonly text: string + readonly data?: unknown + } + /** + * Line-oriented output from a child process or remote stream. + * `channel` is semantic: 'data' = the command's own output (our + * stdout); 'diagnostic' = commentary about the run (stderr). `source` + * names the emitter, not a pipe. + */ + | { + readonly kind: 'output' + readonly source: string + readonly channel: 'data' | 'diagnostic' + readonly line: string + readonly data?: unknown + } + /** A user-actionable follow-up surfaced mid-run. Aggregated into the + * final envelope's nextActions (completed OR errored). */ + | { readonly kind: 'remediation'; readonly action: NextAction; readonly data?: unknown } + /** A reachable endpoint became available. */ + | { + readonly kind: 'endpoint' + readonly name: string + readonly url: string + readonly data?: unknown + } + /** A state transition in a watched external process; `from` carries + * the prior state when known. */ + | { + readonly kind: 'status' + readonly subject: string + readonly status: string + readonly from?: string + readonly data?: unknown + } + /** A file or directory this run wrote that the user may care about. */ + | { + readonly kind: 'artifact' + readonly path: string + readonly description?: string + readonly data?: unknown + } + +// ———————————————————————————————————————————————————————————————————————— +// §2 Presented results — presentation materializes at the return site +// ———————————————————————————————————————————————————————————————————————— + +declare const PRESENTED: unique symbol + +/** + * What a completed command's handler returns inside `ok(...)`: pure data + * plus the presentation the ACTIVE FORMAT already materialized. Built + * exclusively by ctx.present (the brand makes hand-construction a type + * error) — the context knows the format, calls only the presentation + * functions it needs, and the value crossing the product→engine boundary + * is data all the way down. + * + * `data` is always present and is what the envelope's `result` + * serializes (json presentation overrides when supplied). Materialization + * by format: human → human + stdout + next; human+--quiet → stdout; + * json → json + next. + */ +export interface PresentedResult { + readonly [PRESENTED]: true + readonly data: T + /** The outcome code selected at the return site; must be a key of the + * definition's catalogue. Omitted = 0. */ + readonly outcomeCode?: number + readonly presentation: { + readonly human?: readonly Block[] + readonly stdout?: readonly string[] + readonly json?: unknown + readonly next?: readonly NextAction[] + } +} +export { PRESENTED } + +/** + * The per-format presentation functions a handler supplies to + * ctx.present. Only the active format's functions are invoked, at the + * return site, where the outcome and its context are live. `human` + * composes engine primitives (R5: Block is the only vocabulary); + * `stdout` is the machine-consumable data lines the engine writes to + * stdout — what --quiet leaves, what a pipe receives; `json` overrides + * the envelope's `result` (default: the data itself); `next` supplies + * nextActions — the envelope's human nextSteps derive from them. + */ +export interface Presentations { + readonly human: (ui: Ui) => readonly Block[] + readonly stdout?: () => readonly string[] + readonly json?: () => unknown + readonly next?: () => readonly NextAction[] +} + +// ———————————————————————————————————————————————————————————————————————— +// §3 Config sections — R10 made structural +// ———————————————————————————————————————————————————————————————————————— + +/** + * A product's named slice of prisma.config.ts. The token couples the + * section name, its validated type, and its never-throwing validator; + * commands bind to the token, which is how the engine knows which + * section a command needs — and therefore which diagnostics fail which + * commands. Keep validators dependency-light: they load with the + * definition tree at startup (R9), not with the handler. + */ +export interface ConfigSection { + readonly name: string + /** Total: any unknown in, diagnostics out. Never throws (R10). */ + readonly validate: (raw: unknown) => SectionValidation +} + +export type SectionValidation = + | { readonly ok: true; readonly value: T; readonly diagnostics: readonly CliStructuredError[] } + | { readonly ok: false; readonly diagnostics: readonly CliStructuredError[] } + +export declare function defineConfigSection(spec: { + readonly name: string + readonly validate: (raw: unknown) => SectionValidation +}): ConfigSection + +// ———————————————————————————————————————————————————————————————————————— +// §4 The handler context — R4: the whole world arrives as one argument +// ———————————————————————————————————————————————————————————————————————— + +export interface CommandContext { + /** The validated value of the command's declared config section, or + * undefined when the config file has no such section. An INVALID + * needed section never reaches the handler — the engine already + * failed the command with that section's diagnostics. */ + readonly config: TConfig | undefined + + /** Builds the PresentedResult for the active format: calls only the + * presentation functions this format needs, at the return site. The + * only constructor of PresentedResult. `outcomeCode` must be a key of + * the definition's catalogue (engine-verified). */ + readonly present: ( + data: T, + presentations: Presentations, + opts?: { readonly outcomeCode?: number }, + ) => PresentedResult + + /** Management-API credentials, resolved at call time so long-lived + * sessions survive token refresh. Undefined when unauthenticated. + * Commands declaring `requiresCredentials` never see undefined — the + * engine fails them early with the sign-in error. */ + readonly getCredentials: () => Promise + + /** The one way to emit while running (§1). */ + readonly report: (event: EngineEvent) => void + + /** Interactive input (§4a). */ + readonly prompt: PromptSurface + + /** Fires on Ctrl-C/SIGTERM (engine-owned; the engine records which + * signal for the 130/143 exit, and force-exits on a second signal). + * Session commands run until it fires; everything else aborts + * in-flight work with it. */ + readonly signal: AbortSignal + + /** Where the user invoked the CLI. Products never read process.cwd(). */ + readonly cwd: string + + /** R13: probe an optional peer dependency's availability from the + * user's project. Never throws; never installs. Pair with + * `packageManager` to phrase the install command in the structured + * error when it's absent. */ + readonly probeDependency: (specifier: string) => Promise + + /** The user's detected package manager, for install-command phrasing. */ + readonly packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' +} + +export interface Credentials { + /** Opaque to the engine; shape owned by the Cloud product's auth + * library (placeholder pending its design). */ + readonly token: string + readonly workspaceId?: string +} + +/** + * §4a Prompts. Every prompt may carry a product-specified `default`. + * Interactively, Enter accepts the default. Under --yes, a prompt WITH a + * default resolves to it without displaying; a prompt WITHOUT a default + * cannot be operated and the invocation halts with a structured error + * (exit 2). Destructive confirmations therefore simply declare no + * default — --yes can never blast through them; they require their + * explicit flag (--force / --confirm ) per the confirmation rule. + * In json/non-interactive/CI/non-TTY contexts the same default rule + * applies as under --yes. User cancellation (Ctrl-C at the prompt) is a + * distinct structured error the engine maps to exit 3. + */ +export interface PromptSurface { + readonly confirm: ( + question: string, + opts?: { readonly default?: boolean }, + ) => Promise> + readonly select: ( + question: string, + options: ReadonlyArray<{ value: T; label: string }>, + opts?: { readonly default?: T }, + ) => Promise> + readonly text: ( + question: string, + opts?: { readonly placeholder?: string; readonly default?: string }, + ) => Promise> +} + +// ———————————————————————————————————————————————————————————————————————— +// §5 Flags and positionals — R1: directly executable, typed by inference +// ———————————————————————————————————————————————————————————————————————— + +/** + * Product-declared flags. The shared family (§header) is engine-injected + * and reserved; handlers never see those values. Parse-time validation + * failures become structured errors carrying the allowed values — never + * framework strings. + * + * Deliberate asymmetry, matching CLI convention: flags are optional by + * default (requiredString is the exception); positionals are required by + * default (optionalString is the exception). + */ +export declare const flag: { + string(spec: { + brief: string + placeholder?: string + alias?: SingleChar + default?: string + }): FlagSpec + requiredString(spec: { brief: string; placeholder?: string; alias?: SingleChar }): FlagSpec + number(spec: { + brief: string + placeholder?: string + alias?: SingleChar + default?: number + }): FlagSpec + boolean(spec: { brief: string; alias?: SingleChar }): FlagSpec + enum(spec: { + brief: string + values: T + alias?: SingleChar + default?: T[number] + }): FlagSpec + repeated(spec: { brief: string; placeholder?: string; alias?: SingleChar }): FlagSpec +} + +/** Single-character alias; longer strings are a construction error. */ +export type SingleChar = string & { readonly length?: 1 } + +declare const FLAG: unique symbol +export interface FlagSpec { + /** Phantom carrier for inference; exported so declaration emit works. */ + readonly [FLAG]: T +} +export { FLAG } + +export declare const positional: { + string(spec: { brief: string; placeholder: string }): PositionalSpec + optionalString(spec: { brief: string; placeholder: string }): PositionalSpec + /** Zero or more trailing values; at most one, declared last (order = + * declaration order; keys must not be integer-like). */ + variadic(spec: { brief: string; placeholder: string }): PositionalSpec +} +declare const POSITIONAL: unique symbol +export interface PositionalSpec { + readonly [POSITIONAL]: T +} +export { POSITIONAL } + +/** What a handler receives: separate namespaces, symmetric access — + * `args.flags.to`, `args.positionals.name`. */ +export interface Args< + TFlags extends Record>, + TPositionals extends Record>, +> { + readonly flags: { readonly [K in keyof TFlags]: TFlags[K] extends FlagSpec ? T : never } + readonly positionals: { + readonly [K in keyof TPositionals]: TPositionals[K] extends PositionalSpec ? T : never + } +} + +// ———————————————————————————————————————————————————————————————————————— +// §6 Command definitions — light at startup (R9), path-free (R12), +// runtime-discriminated by `kind` (stamped by the define* functions) +// ———————————————————————————————————————————————————————————————————————— + +export interface CommandDefinition< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, +> { + readonly kind: 'command' + /** One line, imperative, shown in listings. */ + readonly brief: string + /** Paragraph(s) for --help. Words only — the engine formats. */ + readonly description?: string + /** Copy-pastable invocations, shown verbatim in help. */ + readonly examples?: readonly string[] + + readonly flags?: TFlags + readonly positionals?: TPositionals + + /** Binds the command to its product's config section (§3). */ + readonly configSection?: ConfigSection + + /** Fail early with the sign-in error when unauthenticated; the handler + * then always receives credentials. */ + readonly requiresCredentials?: boolean + + /** + * The command's documented outcome codes (4–99): code → meaning. + * Rendered in help without executing anything; the return site selects + * one via ctx.present's outcomeCode, which the engine verifies against + * this catalogue. Absent = the command only exits 0/1/2/3. + */ + readonly outcomeCodes?: Readonly> + + /** The heavy part, loaded only at execution (R9). The module's default + * export is the handler — annotate it with CommandHandler + * (type-only import of the light definition; no runtime cycle). */ + readonly handler: () => Promise<{ default: Handler }> +} + +export type Handler< + TFlags extends Record>, + TPositionals extends Record>, + TConfig, +> = ( + args: Args, + ctx: CommandContext, +) => Promise, CliStructuredError>> + +/** For impl files: `const run: CommandHandler = …` */ +export type CommandHandler = D extends CommandDefinition + ? Handler + : never + +export declare function defineCommand< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, +>( + def: Omit, 'kind'>, +): CommandDefinition + +/** + * Mode 4 — sessions (dev, log tail): the handler runs until the signal + * fires, speaks entirely through events, and returns Result. No + * presentation (the engine owns the close-out line), no outcome codes. + * A session always supports json mode: the event stream is its json + * surface. + */ +export interface SessionCommandDefinition< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, +> { + readonly kind: 'session' + readonly brief: string + readonly description?: string + readonly examples?: readonly string[] + readonly flags?: TFlags + readonly positionals?: TPositionals + readonly configSection?: ConfigSection + readonly requiresCredentials?: boolean + readonly handler: () => Promise<{ + default: ( + args: Args, + ctx: CommandContext, + ) => Promise> + }> +} + +export declare function defineSessionCommand< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, +>( + def: Omit, 'kind'>, +): SessionCommandDefinition + +/** + * Mode 7 — stdio protocol servers (lsp): the command owns stdin/stdout + * wholesale. Events, presentation, formats, and prompts do not apply; + * the handler returns the exit code directly. Flags and config are + * allowed; the shared flag family is NOT injected. + */ +export interface RawCommandDefinition< + TFlags extends Record> = {}, + TConfig = undefined, +> { + readonly kind: 'raw' + readonly brief: string + readonly description?: string + readonly flags?: TFlags + readonly configSection?: ConfigSection + readonly handler: () => Promise<{ + default: ( + args: Args, + io: { + readonly stdin: InputStream + readonly stdout: OutputStream + readonly stderr: OutputStream + readonly signal: AbortSignal + readonly cwd: string + readonly config: TConfig | undefined + }, + ) => Promise + }> +} + +export declare function defineRawCommand< + TFlags extends Record> = {}, + TConfig = undefined, +>( + def: Omit, 'kind'>, +): RawCommandDefinition + +/** Erased union for mount maps; `kind` is the runtime discriminant. */ +export type AnyCommand = + | CommandDefinition + | SessionCommandDefinition + | RawCommandDefinition + +// ———————————————————————————————————————————————————————————————————————— +// §7 Presentation primitives — the R5 vocabulary +// ———————————————————————————————————————————————————————————————————————— + +/** Deliberately small; grows by the same evidence rule as events. */ +export type Block = + | { + readonly kind: 'summary' + readonly tone: 'ok' | Exclude + readonly text: string + } + | { + readonly kind: 'fields' + readonly rows: ReadonlyArray<{ label: string; value: string; sensitive?: boolean }> + } + | { + readonly kind: 'table' + readonly columns: readonly string[] + readonly rows: ReadonlyArray + } + | { readonly kind: 'list'; readonly items: readonly string[] } + /** The corpus's most-rendered human structure (migration graphs, + * service trees). */ + | { readonly kind: 'tree'; readonly roots: readonly TreeNode[] } + /** + * Structured errors carried inside a COMPLETED result (drift findings, + * verification failures, config diagnostics). The engine renders each + * with the same layout it uses for top-level errors (✖ summary (CODE), + * Why, Fix) — consistent by construction; products never hand-build + * error presentation. In the data/json side, carry the same errors as + * their envelopes (`toEnvelope()`). + */ + | { readonly kind: 'errors'; readonly errors: ReadonlyArray } + +export interface TreeNode { + readonly label: string + readonly children?: readonly TreeNode[] +} + +/** Styling helpers usable inside block text; no direct writing. */ +export interface Ui { + readonly emphasize: (text: string) => string + readonly dim: (text: string) => string + readonly code: (text: string) => string +} + +// ———————————————————————————————————————————————————————————————————————— +// §8 Streams — minimal structural types; no NodeJS.* in the public +// surface (runtime-agnosticism, R4's why) +// ———————————————————————————————————————————————————————————————————————— + +export interface OutputStream { + write(text: string): void +} +export interface InputStream extends AsyncIterable {} + +// ———————————————————————————————————————————————————————————————————————— +// §9 Envelopes and frames — the json contract +// ———————————————————————————————————————————————————————————————————————— + +export interface CompletedEnvelope { + /** ok = COMPLETED (the command executed to its end). A completed + * result may still carry a non-zero outcome code — bad news is a + * result, not an error. */ + readonly ok: true + /** Stable dotted command id derived from the mount path ('db.migrate'). */ + readonly command: string + /** The presented data (json presentation override when supplied). */ + readonly result: T + /** From the outcome catalogue; 0 when absent. */ + readonly outcomeCode: number + /** Aggregated from severity-'warn' message events. */ + readonly warnings: readonly string[] + /** Derived from nextActions — the human-string form. */ + readonly nextSteps: readonly string[] + readonly nextActions: readonly NextAction[] +} + +export interface ErroredEnvelope { + /** ok = false: the command did NOT complete. */ + readonly ok: false + readonly command: string + /** The CliErrorEnvelope fields (code, severity, summary, why, fix, + * where, meta, docsUrl) — nested, per the settled envelope rule. */ + readonly error: unknown + readonly warnings: readonly string[] + /** Aggregated from remediation events + derived from the error's fix. */ + readonly nextSteps: readonly string[] + readonly nextActions: readonly NextAction[] +} + +/** json mode emits one frame per line: events while running, then + * exactly one result frame. */ +export type Frame = EventFrame | ResultFrame + +export interface EventFrame { + readonly type: 'event' + readonly command: string + /** ISO 8601 UTC. Injectable clock in tests (§11). */ + readonly timestamp: string + readonly event: EngineEvent +} + +export interface ResultFrame { + readonly type: 'result' + readonly command: string + readonly timestamp: string + readonly envelope: CompletedEnvelope | ErroredEnvelope +} + +// ———————————————————————————————————————————————————————————————————————— +// §10 Product export and shell mounting — R12: the shell owns the tree +// ———————————————————————————————————————————————————————————————————————— + +/** What a product package exports: commands by NAME. */ +export type CommandSet = Readonly> + +/** What the shell builds: commands by PATH (space-separated, + * 'db migrate'). Distinct alias so the two maps never read as one. */ +export type MountedTree = Readonly> + +/** + * Shell-side construction. Group help is declared with the mount, since + * groups belong to the tree, not to products. Collisions, unknown + * groups, reserved-flag violations, and grammar violations fail + * construction (build time, not run time). + */ +export declare function createCli(spec: { + readonly name: string + readonly version: string + readonly groups: Readonly> + readonly commands: MountedTree +}): Cli + +export interface Cli { + /** Parse, execute, render, return the exit code. Never calls + * process.exit; never touches streams other than the provided ones. */ + run(argv: readonly string[], runtime: Runtime): Promise +} + +/** Everything environmental, injected once by the bin (or by a test). */ +export interface Runtime { + readonly stdout: OutputStream + readonly stderr: OutputStream + readonly stdin: InputStream + readonly cwd: string + readonly env: Readonly> + readonly isTty: { readonly stdin: boolean; readonly stdout: boolean; readonly stderr: boolean } + readonly signal: AbortSignal + /** Loaded config + per-section diagnostics; the shell builds this via + * the unified loader (R10). Tests hand in fixtures. */ + readonly config: LoadedConfig + readonly getCredentials: () => Promise + readonly packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' +} + +export interface LoadedConfig { + /** Raw section values by name; validation happens per command via its + * ConfigSection token. */ + readonly sections: Readonly> + /** File-level problems (unevaluable module, missing version marker) — + * section: null fails every command. */ + readonly diagnostics: ReadonlyArray<{ + readonly section: string | null + readonly error: CliStructuredError + }> +} + +// ———————————————————————————————————————————————————————————————————————— +// §11 The product-repo test harness — R7: same machinery, bytes out +// ———————————————————————————————————————————————————————————————————————— + +export declare function createTestCli(spec: { + readonly commands: MountedTree + readonly groups?: Readonly> + readonly config?: Readonly> + readonly credentials?: Credentials + readonly packageManager?: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' + /** Fixed clock for deterministic frame timestamps. */ + readonly now?: () => Date +}): TestCli + +export interface TestCli { + run( + argv: readonly string[], + opts?: { + readonly stdin?: string + /** Scripted prompt answers, consumed in order; a run that prompts + * past the script fails the test. */ + readonly answers?: ReadonlyArray + /** Abort the run (session tests): fires the context signal. */ + readonly abort?: AbortSignal + /** Live event tap, for asserting mid-session behavior before + * aborting. */ + readonly onEvent?: (event: EngineEvent) => void + readonly cwd?: string + readonly isTty?: { stdin?: boolean; stdout?: boolean; stderr?: boolean } + readonly env?: Readonly> + }, + ): Promise<{ + readonly exitCode: number + readonly stdout: string + readonly stderr: string + /** Parsed frames (events + the result frame) when json mode was on. */ + readonly json: readonly Frame[] + /** Every EngineEvent the handler emitted, for semantic assertions. */ + readonly events: readonly EngineEvent[] + /** The PresentedResult the handler returned (data + materialized + * presentation), for semantic assertions without byte-scraping. */ + readonly presented?: PresentedResult + }> +} diff --git a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v5.ts b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v5.ts new file mode 100644 index 00000000..c484c85d --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v5.ts @@ -0,0 +1,836 @@ +/** + * DRAFT v5 — the unified CLI engine's public interface. + * v1 initial · v2 round-1 fixes · v3 return-site presentation · + * v4 completed/errored semantics, --format, log levels, prompt defaults · + * v5 round-3 closure: diagnostics declared once at ctx.present, typed + * outcome codes, prompt.consent, byte-capable raw stdin. + * Prior versions preserved as -v1…-v4.ts; review artifacts in ./reviews/. + * + * THE MODEL, in one analogy (operator, 2026-08-09): commands settle like + * promises. A command can COMPLETE — and its completion can be + * successful or unsuccessful, both presented through the same machinery, + * distinguished by outcome code and diagnostics — or it can ERROR, which + * aborts out of the normal process and gets its own special handling. + * + * Implementation prerequisite: ADR 239 (prisma/prisma) is amended so + * completed-but-unsuccessful command results (verify/check/runner + * findings) are carried as diagnostics with their dotted codes inside a + * completed envelope with a catalogued exit code — not as structured + * failures with exit 2, as it classifies them today. + * + * Everything a product package imports for CLI purposes lives here (R3). + * Requirement references (R1–R14) point at docs/architecture/ + * cli-engine-requirements.md in prisma-cli. Nothing from stricli appears — + * it is an internal of the engine package. + * + * EXECUTION PROTOCOL. A handler receives (args, context), emits zero or + * more events through context.report, and finishes one of two ways: + * + * COMPLETED — it returns ok(ctx.present(data, presentations)): the + * command executed to its end and has a result. A completed result may + * still be bad news; it carries an outcome code from the command's + * documented catalogue (`migration check` completes, presents its + * findings like any result, and exits 4). Presentation always runs for + * completed results. + * + * ERRORED — it returns notOk(structuredError): the command did not + * complete. The engine renders the error envelope (code, summary, why, + * fix); there is no product presentation on the error path. + * `remediation` events emitted before the error are aggregated into + * the error envelope's nextActions, as `warn` messages are into + * warnings — so guidance survives without a second presentation system. + * + * Session commands (defineSessionCommand) keep emitting until + * context.signal fires, then clean up and return. Stdio protocol servers + * (defineRawCommand) bypass the protocol by declaration. Liveness display + * is the engine's (shown when a command runs quietly past a threshold). + * Nothing product-authored executes after the handler resolves — the + * engine receives values, never callbacks. + * + * FORMATS AND LEVELS. The output format is an engine mode: + * `--format `, auto-selected when unspecified (human on a + * TTY stdout, json otherwise — deliberate agent-facing behavior); + * `--json` is shorthand for `--format json`. In json mode the engine + * suppresses prompts (they fail structurally) and frames every event as + * one NDJSON line. Commentary is filtered by `--log-level + * ` (default info); `--verbose` is shorthand + * for `--log-level verbose`. The engine injects the shared flag family + * on every non-raw command: --format/--json, --log-level/-v/--verbose, + * -q/--quiet, -y/--yes, --interactive/--no-interactive, + * --color/--no-color. Products cannot declare flags with those names. + * Product flag keys are camelCase and transliterate to --kebab-case. + * + * EXIT CODES (R6): 0 completed; 1 bug only; 2 errored (expected, + * structured); 3 user abort (Ctrl-C, or cancelling a prompt the command + * cannot proceed without); 4–99 outcome codes from the command's + * catalogue; 130/143 delivered signals. The engine owns signal wiring: + * first signal fires context.signal and awaits handler teardown; a + * second signal exits immediately with the signal's code. + */ + +// ———————————————————————————————————————————————————————————————————————— +// Foundation types (from the zero-dependency foundation package — which +// also owns NextAction, so the engine and the error envelope share it +// without a package cycle). Shown for reading convenience. +// ———————————————————————————————————————————————————————————————————————— + +import type { CliStructuredError, NextAction, Result } from '@prisma/cli-foundation' + +/** The one severity scale for commentary; also the log-level axis + * (ADR 239's error|warn|info, extended with verbose for detail + * commentary). Step outcomes are completion states, not severities. */ +export type Severity = 'error' | 'warn' | 'info' | 'verbose' +export type LogLevel = Severity + +export type Format = 'human' | 'json' + +// ———————————————————————————————————————————————————————————————————————— +// §1 Events — R14: one engine vocabulary, product extensions in `data` +// ———————————————————————————————————————————————————————————————————————— + +/** + * The engine event envelope. `kind`-specific fields are the common + * vocabulary the engine renders consistently (human mode) and frames + * (json mode). `data` is the product extension: passed through to + * machine consumers untouched, never interpreted by the engine, + * documented and versioned by the product as its own public API. A + * structure recurring inside `data` across commands is the promotion + * signal (R14). + * + * Rendering, human mode: `output` events with channel 'data' are the + * command's data and go to OUR stdout (what `log tail > file` captures); + * everything else is commentary on stderr, filtered by the active log + * level (`message` events by their severity; other kinds display at + * info). In json mode everything is framed on stdout (§9). + * + * report() is synchronous fire-and-forget; the engine buffers and writes + * asynchronously (no backpressure signal — accepted trade). Calling it + * after the handler has resolved is a bug (InternalError). Events during + * teardown (after the signal, before resolution) are normal. + */ +export type EngineEvent = + /** A named phase began. `id`/`parentId` express nesting; omitted for + * flat steps. */ + | { + readonly kind: 'step-started' + readonly step: string + readonly id?: string + readonly parentId?: string + readonly data?: unknown + } + /** The phase ended. `outcome` is a completion state and drives the + * ✔/✘/⚠/− glyph; it is not a severity. */ + | { + readonly kind: 'step-finished' + readonly step: string + readonly id?: string + readonly outcome: 'ok' | 'failed' | 'skipped' | 'warning' + readonly data?: unknown + } + /** Progress inside a phase (counts, not percentages). */ + | { + readonly kind: 'progress' + readonly step?: string + readonly completed: number + readonly total?: number + readonly data?: unknown + } + /** + * A line of commentary at a severity. 'warn' messages are additionally + * aggregated into the envelope's `warnings`; 'verbose' messages render + * only at --log-level verbose. 'error' is not valid here: fatal + * problems are the Result's error. + */ + | { + readonly kind: 'message' + readonly severity: Exclude + readonly text: string + readonly data?: unknown + } + /** + * Line-oriented output from a child process or remote stream. + * `channel` is semantic: 'data' = the command's own output (our + * stdout); 'diagnostic' = commentary about the run (stderr). `source` + * names the emitter, not a pipe. + */ + | { + readonly kind: 'output' + readonly source: string + readonly channel: 'data' | 'diagnostic' + readonly line: string + readonly data?: unknown + } + /** A user-actionable follow-up surfaced mid-run. Aggregated into the + * final envelope's nextActions (completed OR errored). */ + | { readonly kind: 'remediation'; readonly action: NextAction; readonly data?: unknown } + /** A reachable endpoint became available. */ + | { + readonly kind: 'endpoint' + readonly name: string + readonly url: string + readonly data?: unknown + } + /** A state transition in a watched external process; `from` carries + * the prior state when known. */ + | { + readonly kind: 'status' + readonly subject: string + readonly status: string + readonly from?: string + readonly data?: unknown + } + /** A file or directory this run wrote that the user may care about. */ + | { + readonly kind: 'artifact' + readonly path: string + readonly description?: string + readonly data?: unknown + } + +// ———————————————————————————————————————————————————————————————————————— +// §2 Presented results — presentation materializes at the return site +// ———————————————————————————————————————————————————————————————————————— + +declare const PRESENTED: unique symbol + +/** + * What a completed command's handler returns inside `ok(...)`: pure data + * plus the presentation the ACTIVE FORMAT already materialized. Built + * exclusively by ctx.present (the brand makes hand-construction a type + * error) — the context knows the format, calls only the presentation + * functions it needs, and the value crossing the product→engine boundary + * is data all the way down. + * + * `data` is always present and is what the envelope's `result` + * serializes (json presentation overrides when supplied). Materialization + * by format: human → human + stdout + next; human+--quiet → stdout; + * json → json + next. + */ +export interface PresentedResult { + readonly [PRESENTED]: true + readonly data: T + /** The outcome code selected at the return site; typed against the + * definition's catalogue keys. Omitted = 0. */ + readonly outcomeCode?: number + /** + * Structured findings carried by a COMPLETED result (drift, verify + * failures) — declared ONCE here; the engine renders them in human + * mode with the same layout as top-level errors (shown even under + * --quiet) AND serializes their envelopes into + * CompletedEnvelope.diagnostics. One declaration, both surfaces, + * impossible to diverge. Guardrail: any severity-'error' entry + * requires a non-zero outcomeCode — a genuine could-not-complete + * belongs in notOk, not here. The test: notOk when the command + * couldn't do its job; diagnostics when finding these WAS the job. + */ + readonly diagnostics?: readonly CliStructuredError[] + readonly presentation: { + readonly human?: readonly Block[] + readonly stdout?: readonly string[] + readonly json?: unknown + readonly next?: readonly NextAction[] + } +} +export { PRESENTED } + +/** + * The per-format presentation functions a handler supplies to + * ctx.present. Only the active format's functions are invoked, at the + * return site, where the outcome and its context are live. `human` + * composes engine primitives (R5: Block is the only vocabulary); + * `stdout` is the machine-consumable data lines the engine writes to + * stdout — what --quiet leaves, what a pipe receives; `json` overrides + * the envelope's `result` (default: the data itself); `next` supplies + * nextActions — the envelope's human nextSteps derive from them. + */ +export interface Presentations { + readonly human: (ui: Ui) => readonly Block[] + readonly stdout?: () => readonly string[] + readonly json?: () => unknown + readonly next?: () => readonly NextAction[] +} + +// ———————————————————————————————————————————————————————————————————————— +// §3 Config sections — R10 made structural +// ———————————————————————————————————————————————————————————————————————— + +/** + * A product's named slice of prisma.config.ts. The token couples the + * section name, its validated type, and its never-throwing validator; + * commands bind to the token, which is how the engine knows which + * section a command needs — and therefore which diagnostics fail which + * commands. Keep validators dependency-light: they load with the + * definition tree at startup (R9), not with the handler. + */ +export interface ConfigSection { + readonly name: string + /** Total: any unknown in, diagnostics out. Never throws (R10). */ + readonly validate: (raw: unknown) => SectionValidation +} + +export type SectionValidation = + | { readonly ok: true; readonly value: T; readonly diagnostics: readonly CliStructuredError[] } + | { readonly ok: false; readonly diagnostics: readonly CliStructuredError[] } + +export declare function defineConfigSection(spec: { + readonly name: string + readonly validate: (raw: unknown) => SectionValidation +}): ConfigSection + +// ———————————————————————————————————————————————————————————————————————— +// §4 The handler context — R4: the whole world arrives as one argument +// ———————————————————————————————————————————————————————————————————————— + +export interface CommandContext { + /** The validated value of the command's declared config section, or + * undefined when the config file has no such section. An INVALID + * needed section never reaches the handler — the engine already + * failed the command with that section's diagnostics. */ + readonly config: TConfig | undefined + + /** Builds the PresentedResult for the active format: calls only the + * presentation functions this format needs, at the return site. The + * only constructor of PresentedResult. `outcomeCode` is typed against + * the definition's catalogue keys — a wrong code is a compile error + * at the call site. `diagnostics` are the completed result's + * structured findings (see PresentedResult). */ + readonly present: ( + data: T, + presentations: Presentations, + opts?: { + readonly outcomeCode?: TCode + readonly diagnostics?: readonly CliStructuredError[] + }, + ) => PresentedResult + + /** Management-API credentials, resolved at call time so long-lived + * sessions survive token refresh. Undefined when unauthenticated. + * Commands declaring `requiresCredentials` never see undefined — the + * engine fails them early with the sign-in error. */ + readonly getCredentials: () => Promise + + /** The one way to emit while running (§1). */ + readonly report: (event: EngineEvent) => void + + /** Interactive input (§4a). */ + readonly prompt: PromptSurface + + /** Fires on Ctrl-C/SIGTERM (engine-owned; the engine records which + * signal for the 130/143 exit, and force-exits on a second signal). + * Session commands run until it fires; everything else aborts + * in-flight work with it. */ + readonly signal: AbortSignal + + /** Where the user invoked the CLI. Products never read process.cwd(). */ + readonly cwd: string + + /** R13: probe an optional peer dependency's availability from the + * user's project. Never throws; never installs. Pair with + * `packageManager` to phrase the install command in the structured + * error when it's absent. */ + readonly probeDependency: (specifier: string) => Promise + + /** The user's detected package manager, for install-command phrasing. */ + readonly packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' +} + +export interface Credentials { + /** Opaque to the engine; shape owned by the Cloud product's auth + * library (placeholder pending its design). */ + readonly token: string + readonly workspaceId?: string +} + +/** + * §4a Prompts. Every prompt may carry a product-specified `default`. + * Interactively, Enter accepts the default. Under --yes, a prompt WITH a + * default resolves to it without displaying; a prompt WITHOUT a default + * cannot be operated and the invocation halts with a structured error + * (exit 2). Destructive confirmations therefore simply declare no + * default — --yes can never blast through them; they require their + * explicit flag (--force / --confirm ) per the confirmation rule. + * In json/non-interactive/CI/non-TTY contexts the same default rule + * applies as under --yes. User cancellation (Ctrl-C at the prompt) is a + * distinct structured error the engine maps to exit 3. + */ +export interface PromptSurface { + readonly confirm: ( + question: string, + opts?: { readonly default?: boolean }, + ) => Promise> + /** + * A question requiring EXPLICIT consent — not necessarily destructive, + * but never inferable. Structurally undefaultable: no default + * parameter exists, so --yes, Enter-through, and non-interactive + * contexts can never satisfy it; without a TTY it returns the + * interaction-required structured error, and the command's fix names + * the explicit flag that grants consent non-interactively. + */ + readonly consent: (question: string) => Promise> + readonly select: ( + question: string, + options: ReadonlyArray<{ value: T; label: string }>, + opts?: { readonly default?: T }, + ) => Promise> + readonly text: ( + question: string, + opts?: { readonly placeholder?: string; readonly default?: string }, + ) => Promise> +} + +// ———————————————————————————————————————————————————————————————————————— +// §5 Flags and positionals — R1: directly executable, typed by inference +// ———————————————————————————————————————————————————————————————————————— + +/** + * Product-declared flags. The shared family (§header) is engine-injected + * and reserved; handlers never see those values. Parse-time validation + * failures become structured errors carrying the allowed values — never + * framework strings. + * + * Deliberate asymmetry, matching CLI convention: flags are optional by + * default (requiredString is the exception); positionals are required by + * default (optionalString is the exception). + */ +export declare const flag: { + string(spec: { + brief: string + placeholder?: string + alias?: A & Char + default?: string + }): FlagSpec + requiredString(spec: { + brief: string + placeholder?: string + alias?: A & Char + }): FlagSpec + number(spec: { + brief: string + placeholder?: string + alias?: A & Char + default?: number + }): FlagSpec + boolean(spec: { brief: string; alias?: A & Char }): FlagSpec + enum(spec: { + brief: string + values: T + alias?: A & Char + default?: T[number] + }): FlagSpec + repeated(spec: { + brief: string + placeholder?: string + alias?: A & Char + }): FlagSpec +} + +/** Single-character alias, enforced at the type level: `Char<'q'>` is + * 'q'; `Char<'ab'>` is never. Builder methods take a const generic so + * `alias: 'ab'` is a compile error at the declaration site. */ +export type Char = S extends `${string}${infer Rest}` + ? Rest extends '' + ? S + : never + : never + +declare const FLAG: unique symbol +export interface FlagSpec { + /** Phantom carrier for inference; exported so declaration emit works. */ + readonly [FLAG]: T +} +export { FLAG } + +export declare const positional: { + string(spec: { brief: string; placeholder: string }): PositionalSpec + optionalString(spec: { brief: string; placeholder: string }): PositionalSpec + /** Zero or more trailing values; at most one, declared last (order = + * declaration order; keys must not be integer-like). */ + variadic(spec: { brief: string; placeholder: string }): PositionalSpec +} +declare const POSITIONAL: unique symbol +export interface PositionalSpec { + readonly [POSITIONAL]: T +} +export { POSITIONAL } + +/** What a handler receives: separate namespaces, symmetric access — + * `args.flags.to`, `args.positionals.name`. */ +export interface Args< + TFlags extends Record>, + TPositionals extends Record>, +> { + readonly flags: { readonly [K in keyof TFlags]: TFlags[K] extends FlagSpec ? T : never } + readonly positionals: { + readonly [K in keyof TPositionals]: TPositionals[K] extends PositionalSpec ? T : never + } +} + +// ———————————————————————————————————————————————————————————————————————— +// §6 Command definitions — light at startup (R9), path-free (R12), +// runtime-discriminated by `kind` (stamped by the define* functions) +// ———————————————————————————————————————————————————————————————————————— + +export interface CommandDefinition< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, + TCode extends number = never, +> { + readonly kind: 'command' + /** One line, imperative, shown in listings. */ + readonly brief: string + /** Paragraph(s) for --help. Words only — the engine formats. */ + readonly description?: string + /** Copy-pastable invocations, shown verbatim in help. */ + readonly examples?: readonly string[] + + readonly flags?: TFlags + readonly positionals?: TPositionals + + /** Binds the command to its product's config section (§3). */ + readonly configSection?: ConfigSection + + /** Fail early with the sign-in error when unauthenticated; the handler + * then always receives credentials. */ + readonly requiresCredentials?: boolean + + /** + * The command's documented outcome codes (4–99): code → meaning. + * Rendered in help without executing anything; the catalogue's keys + * type ctx.present's outcomeCode, so a code outside the catalogue is + * a compile error at the return site. Absent = the command only exits + * 0/1/2/3. + */ + readonly outcomeCodes?: Readonly> + + /** The heavy part, loaded only at execution (R9). The module's default + * export is the handler — annotate it with CommandHandler + * (type-only import of the light definition; no runtime cycle). */ + readonly handler: () => Promise<{ default: Handler }> +} + +export type Handler< + TFlags extends Record>, + TPositionals extends Record>, + TConfig, + TCode extends number = never, +> = ( + args: Args, + ctx: CommandContext, +) => Promise, CliStructuredError>> + +/** For impl files: `const run: CommandHandler = …` */ +export type CommandHandler = D extends CommandDefinition + ? Handler + : never + +export declare function defineCommand< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, + TCode extends number = never, +>( + def: Omit, 'kind'>, +): CommandDefinition + +/** + * Mode 4 — sessions (dev, log tail): the handler runs until the signal + * fires, speaks entirely through events, and returns Result. No + * presentation (the engine owns the close-out line), no outcome codes. + * A session always supports json mode: the event stream is its json + * surface. + */ +export interface SessionCommandDefinition< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, +> { + readonly kind: 'session' + readonly brief: string + readonly description?: string + readonly examples?: readonly string[] + readonly flags?: TFlags + readonly positionals?: TPositionals + readonly configSection?: ConfigSection + readonly requiresCredentials?: boolean + readonly handler: () => Promise<{ + default: ( + args: Args, + ctx: CommandContext, + ) => Promise> + }> +} + +export declare function defineSessionCommand< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, +>( + def: Omit, 'kind'>, +): SessionCommandDefinition + +/** + * Mode 7 — stdio protocol servers (lsp): the command owns stdin/stdout + * wholesale. Events, presentation, formats, and prompts do not apply; + * the handler returns the exit code directly. Flags and config are + * allowed; the shared flag family is NOT injected. + */ +export interface RawCommandDefinition< + TFlags extends Record> = {}, + TConfig = undefined, +> { + readonly kind: 'raw' + readonly brief: string + readonly description?: string + readonly flags?: TFlags + readonly configSection?: ConfigSection + readonly handler: () => Promise<{ + default: ( + args: Args, + io: { + readonly stdin: InputStream + readonly stdout: OutputStream + readonly stderr: OutputStream + readonly signal: AbortSignal + readonly cwd: string + readonly config: TConfig | undefined + }, + ) => Promise + }> +} + +export declare function defineRawCommand< + TFlags extends Record> = {}, + TConfig = undefined, +>( + def: Omit, 'kind'>, +): RawCommandDefinition + +/** Erased union for mount maps; `kind` is the runtime discriminant. */ +export type AnyCommand = + | CommandDefinition + | SessionCommandDefinition + | RawCommandDefinition + +// ———————————————————————————————————————————————————————————————————————— +// §7 Presentation primitives — the R5 vocabulary +// ———————————————————————————————————————————————————————————————————————— + +/** Deliberately small; grows by the same evidence rule as events. */ +export type Block = + | { + readonly kind: 'summary' + readonly tone: 'ok' | Exclude + readonly text: string + } + | { + readonly kind: 'fields' + readonly rows: ReadonlyArray<{ label: string; value: string; sensitive?: boolean }> + } + | { + readonly kind: 'table' + readonly columns: readonly string[] + readonly rows: ReadonlyArray + } + | { readonly kind: 'list'; readonly items: readonly string[] } + /** The corpus's most-rendered human structure (migration graphs, + * service trees). */ + | { readonly kind: 'tree'; readonly roots: readonly TreeNode[] } +// NOTE: structured findings inside a completed result are NOT a Block — +// they are declared once at ctx.present (diagnostics) and the engine +// renders them with the top-level error layout and serializes them into +// the envelope, so the two surfaces cannot diverge. + +export interface TreeNode { + readonly label: string + readonly children?: readonly TreeNode[] +} + +/** Styling helpers usable inside block text; no direct writing. */ +export interface Ui { + readonly emphasize: (text: string) => string + readonly dim: (text: string) => string + readonly code: (text: string) => string +} + +// ———————————————————————————————————————————————————————————————————————— +// §8 Streams — minimal structural types; no NodeJS.* in the public +// surface (runtime-agnosticism, R4's why) +// ———————————————————————————————————————————————————————————————————————— + +export interface OutputStream { + write(text: string): void +} +/** Byte-oriented, so raw commands can implement byte-counted protocols + * (lsp's Content-Length framing). Decoding is the consumer's business; + * the engine's own prompt machinery decodes internally. setRawMode is + * present where the platform supports keypress-driven input. */ +export interface InputStream extends AsyncIterable { + readonly setRawMode?: (enabled: boolean) => void +} + +// ———————————————————————————————————————————————————————————————————————— +// §9 Envelopes and frames — the json contract +// ———————————————————————————————————————————————————————————————————————— + +export interface CompletedEnvelope { + /** ok = COMPLETED (the command executed to its end). A completed + * result may still carry a non-zero outcome code — bad news is a + * result, not an error. */ + readonly ok: true + /** Stable dotted command id derived from the mount path ('db.migrate'). */ + readonly command: string + /** The presented data (json presentation override when supplied). */ + readonly result: T + /** From the outcome catalogue; 0 when absent. */ + readonly outcomeCode: number + /** The completed result's structured findings, serialized as error + * envelopes (dotted codes intact for machine consumers) — aggregated + * by the engine from PresentedResult.diagnostics. */ + readonly diagnostics: readonly unknown[] + /** Aggregated from severity-'warn' message events. */ + readonly warnings: readonly string[] + /** Derived from nextActions — the human-string form. */ + readonly nextSteps: readonly string[] + readonly nextActions: readonly NextAction[] +} + +export interface ErroredEnvelope { + /** ok = false: the command did NOT complete. */ + readonly ok: false + readonly command: string + /** The CliErrorEnvelope fields (code, severity, summary, why, fix, + * where, meta, docsUrl) — nested, per the settled envelope rule. The + * PRIMARY error: what aborted the command. */ + readonly error: unknown + /** Accompanying structured problems when the abort had several (three + * config typos are three diagnostics, not one flattened error) — + * symmetric with CompletedEnvelope.diagnostics. */ + readonly diagnostics: readonly unknown[] + readonly warnings: readonly string[] + /** Aggregated from remediation events + derived from the error's fix. */ + readonly nextSteps: readonly string[] + readonly nextActions: readonly NextAction[] +} + +/** json mode emits one frame per line: events while running, then + * exactly one result frame. */ +export type Frame = EventFrame | ResultFrame + +export interface EventFrame { + readonly type: 'event' + readonly command: string + /** ISO 8601 UTC. Injectable clock in tests (§11). */ + readonly timestamp: string + readonly event: EngineEvent +} + +export interface ResultFrame { + readonly type: 'result' + readonly command: string + readonly timestamp: string + readonly envelope: CompletedEnvelope | ErroredEnvelope +} + +// ———————————————————————————————————————————————————————————————————————— +// §10 Product export and shell mounting — R12: the shell owns the tree +// ———————————————————————————————————————————————————————————————————————— + +/** What a product package exports: commands by NAME. */ +export type CommandSet = Readonly> + +/** What the shell builds: commands by PATH (space-separated, + * 'db migrate'). Distinct alias so the two maps never read as one. */ +export type MountedTree = Readonly> + +/** + * Shell-side construction. Group help is declared with the mount, since + * groups belong to the tree, not to products. Collisions, unknown + * groups, reserved-flag violations, and grammar violations fail + * construction (build time, not run time). + */ +export declare function createCli(spec: { + readonly name: string + readonly version: string + readonly groups: Readonly> + readonly commands: MountedTree +}): Cli + +export interface Cli { + /** Parse, execute, render, return the exit code. Never calls + * process.exit; never touches streams other than the provided ones. */ + run(argv: readonly string[], runtime: Runtime): Promise +} + +/** Everything environmental, injected once by the bin (or by a test). */ +export interface Runtime { + readonly stdout: OutputStream + readonly stderr: OutputStream + readonly stdin: InputStream + readonly cwd: string + readonly env: Readonly> + readonly isTty: { readonly stdin: boolean; readonly stdout: boolean; readonly stderr: boolean } + readonly signal: AbortSignal + /** Loaded config + per-section diagnostics; the shell builds this via + * the unified loader (R10). Tests hand in fixtures. */ + readonly config: LoadedConfig + readonly getCredentials: () => Promise + readonly packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' +} + +export interface LoadedConfig { + /** Raw section values by name; validation happens per command via its + * ConfigSection token. */ + readonly sections: Readonly> + /** File-level problems (unevaluable module, missing version marker) — + * section: null fails every command. */ + readonly diagnostics: ReadonlyArray<{ + readonly section: string | null + readonly error: CliStructuredError + }> +} + +// ———————————————————————————————————————————————————————————————————————— +// §11 The product-repo test harness — R7: same machinery, bytes out +// ———————————————————————————————————————————————————————————————————————— + +export declare function createTestCli(spec: { + readonly commands: MountedTree + readonly groups?: Readonly> + readonly config?: Readonly> + readonly credentials?: Credentials + readonly packageManager?: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' + /** Fixed clock for deterministic frame timestamps. */ + readonly now?: () => Date +}): TestCli + +export interface TestCli { + run( + argv: readonly string[], + opts?: { + readonly stdin?: string + /** Scripted prompt answers, consumed in order; a run that prompts + * past the script fails the test. */ + readonly answers?: ReadonlyArray + /** Abort the run (session tests): fires the context signal. */ + readonly abort?: AbortSignal + /** Live event tap, for asserting mid-session behavior before + * aborting. */ + readonly onEvent?: (event: EngineEvent) => void + readonly cwd?: string + readonly isTty?: { stdin?: boolean; stdout?: boolean; stderr?: boolean } + readonly env?: Readonly> + }, + ): Promise<{ + readonly exitCode: number + readonly stdout: string + readonly stderr: string + /** Parsed frames (events + the result frame) when json mode was on. */ + readonly json: readonly Frame[] + /** Every EngineEvent the handler emitted, for semantic assertions. */ + readonly events: readonly EngineEvent[] + /** The PresentedResult the handler returned (data + materialized + * presentation), for semantic assertions without byte-scraping. */ + readonly presented?: PresentedResult + }> +} diff --git a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v6.ts b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v6.ts new file mode 100644 index 00000000..20fb90bf --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v6.ts @@ -0,0 +1,852 @@ +/** + * DRAFT v6 — the unified CLI engine's public interface. + * v1 initial · v2 round-1 fixes · v3 return-site presentation · + * v4 completed/errored, --format, log levels, prompt defaults · + * v5 round-3 closure · v6 operator line review: Diagnostic as a pure + * data shape (findings are not thrown errors), warnings folded into + * diagnostics, nextSteps deleted, commandId, flattened StreamEvent, + * exitCode naming restored, result/session/server command kinds. + * Prior versions preserved as -v1…-v5.ts; review artifacts in ./reviews/. + * + * THE MODEL, in one analogy (operator, 2026-08-09): commands settle like + * promises. A command can COMPLETE — and its completion can be + * successful or unsuccessful, both presented through the same machinery, + * distinguished by exit code and diagnostics — or it can ERROR, which + * aborts out of the normal process and gets its own special handling. + * + * Implementation prerequisite: ADR 239 (prisma/prisma) is amended so + * completed-but-unsuccessful command results (verify/check/runner + * findings) are carried as diagnostics with their dotted codes inside a + * completed envelope with a documented exit code — not as structured + * failures with exit 2, as it classifies them today. The amendment also + * checks whether any shipped error uses severity 'info'; if none does, + * the severity scale of CliStructuredError and Diagnostic trims to + * error|warn — together, so the two shapes stay identical. + * + * Everything a product package imports for CLI purposes lives here (R3). + * Requirement references (R1–R14) point at docs/architecture/ + * cli-engine-requirements.md in prisma-cli. Nothing from stricli appears — + * it is an internal of the engine package. + * + * EXECUTION PROTOCOL. A handler receives (args, context), emits zero or + * more events through context.report, and finishes one of two ways: + * + * COMPLETED — it returns ok(ctx.present(data, presentations, opts)): + * the command executed to its end and has a result. A completed result + * may still be bad news; it carries diagnostics (recorded findings — + * data, not thrown errors) and an exit code from the command's + * documented set (`migration check` completes, presents its findings + * like any result, and exits 4). Presentation always runs for + * completed results. + * + * ERRORED — it returns notOk(structuredError): the command did not + * complete. The engine renders the error envelope; there is no product + * presentation on the error path. The primary error of an errored + * command is severity 'error' by definition — a warning cannot abort a + * command. `remediation` events emitted before the error are + * aggregated into the errored envelope's nextActions. + * + * Session commands run until context.signal fires, then clean up and + * return. Server commands hand the stdio conversation to a foreign + * client. Liveness display is the engine's (shown when a command runs + * quietly past a threshold). Nothing product-authored executes after the + * handler resolves — the engine receives values, never callbacks. + * + * FORMATS AND LEVELS. `--format `, auto-selected when + * unspecified (human on a TTY stdout, json otherwise — deliberate + * agent-facing behavior); `--json` is shorthand for `--format json`. In + * json mode the engine suppresses prompts (they fail structurally) and + * emits one StreamEvent per line. Commentary is filtered by `--log-level + * ` (default info); `--verbose` is shorthand + * for `--log-level verbose`. The engine injects the shared flag family + * on every non-server command: --format/--json, --log-level/-v/ + * --verbose, -q/--quiet, -y/--yes, --interactive/--no-interactive, + * --color/--no-color. Products cannot declare flags with those names. + * Product flag keys are camelCase and transliterate to --kebab-case. + * + * EXIT CODES (R6): 0 completed; 1 bug only; 2 errored (expected, + * structured); 3 user abort (Ctrl-C, or cancelling a prompt the command + * cannot proceed without); 4–99 documented per command in `exitCodes`; + * 130/143 delivered signals. The engine owns signal wiring: first signal + * fires context.signal and awaits handler teardown; a second signal + * exits immediately with the signal's code. + */ + +// ———————————————————————————————————————————————————————————————————————— +// Foundation types (from the zero-dependency foundation package — which +// owns CliStructuredError, Result, NextAction, and Diagnostic, so the +// engine and both repos share them without cycles). Shown for reading +// convenience. +// ———————————————————————————————————————————————————————————————————————— + +import type { CliStructuredError, Diagnostic, NextAction, Result } from '@prisma/cli-foundation' + +/* + * For reference — the foundation shapes this file leans on: + * + * Diagnostic — a recorded finding: pure data, never thrown, no stack. + * { code: 'NAMESPACE.SUBCODE', severity: 'error' | 'warn' | 'info', + * summary, why?, fix?, where?, meta?, docsUrl? } + * Field-for-field the settled error envelope (ADR 239) minus `ok` — + * identical scales included; the two shapes never diverge. + * CliStructuredError.toEnvelope() yields exactly this shape, so a + * thrown error and a recorded finding share one wire form. Aggregate + * operations (db verify, migration check, config validation) COLLECT + * Diagnostics; they do not construct Error instances per finding. + * + * NextAction — the typed agent-facing follow-up (platform-shipped form): + * { kind: 'run-command' | 'user-choice' | 'edit-file' | 'done', + * journey, label, command?, commands?, reason? } + */ + +/** The commentary severity scale; also the log-level axis. Distinct from + * Diagnostic severity (error|warn): 'info' and 'verbose' grade + * commentary, which never enters the envelope. Step outcomes are + * completion states, not severities. */ +export type Severity = 'error' | 'warn' | 'info' | 'verbose' +export type LogLevel = Severity + +export type Format = 'human' | 'json' + +// ———————————————————————————————————————————————————————————————————————— +// §1 Events — R14: one engine vocabulary, product extensions in `data` +// ———————————————————————————————————————————————————————————————————————— + +/** + * The engine event envelope. `kind`-specific fields are the common + * vocabulary the engine renders consistently (human mode) and streams + * (json mode, §9). `data` is the product extension: passed through to + * machine consumers untouched, never interpreted by the engine, + * documented and versioned by the product as its own public API. A + * structure recurring inside `data` across commands is the promotion + * signal (R14). + * + * Rendering, human mode: `output` events with channel 'data' are the + * command's data and go to OUR stdout (what `log tail > file` captures); + * everything else is commentary on stderr, filtered by the active log + * level (`message` events by their severity; other kinds display at + * info). Events are transcript: they are NOT aggregated into the + * envelope (the one exception is `remediation` → nextActions). Findings + * that belong in the envelope are diagnostics on the presented result, + * not events. + * + * report() is synchronous fire-and-forget; the engine buffers and writes + * asynchronously (no backpressure signal — accepted trade). Calling it + * after the handler has resolved is a bug (InternalError). Events during + * teardown (after the signal, before resolution) are normal. + */ +export type EngineEvent = + /** A named phase began. `id`/`parentId` express nesting; omitted for + * flat steps. */ + | { + readonly kind: 'step-started' + readonly step: string + readonly id?: string + readonly parentId?: string + readonly data?: unknown + } + /** The phase ended. `outcome` is a completion state and drives the + * ✔/✘/⚠/− glyph; it is not a severity. */ + | { + readonly kind: 'step-finished' + readonly step: string + readonly id?: string + readonly outcome: 'ok' | 'failed' | 'skipped' | 'warning' + readonly data?: unknown + } + /** Progress inside a phase (counts, not percentages). */ + | { + readonly kind: 'progress' + readonly step?: string + readonly completed: number + readonly total?: number + readonly data?: unknown + } + /** A line of commentary at a severity; display-filtered by log level. + * Transcript only — never enters the envelope. 'error' is not valid + * here: fatal problems are the Result's error; envelope-worthy + * findings are diagnostics. */ + | { + readonly kind: 'message' + readonly severity: Exclude + readonly text: string + readonly data?: unknown + } + /** Line-oriented output from a child process or remote stream. + * `channel` is semantic: 'data' = the command's own output (our + * stdout); 'diagnostic' = commentary about the run (stderr). `source` + * names the emitter, not a pipe. */ + | { + readonly kind: 'output' + readonly source: string + readonly channel: 'data' | 'diagnostic' + readonly line: string + readonly data?: unknown + } + /** A user-actionable follow-up surfaced mid-run. Aggregated into the + * final envelope's nextActions (completed OR errored). */ + | { readonly kind: 'remediation'; readonly action: NextAction; readonly data?: unknown } + /** A reachable endpoint became available. */ + | { + readonly kind: 'endpoint' + readonly name: string + readonly url: string + readonly data?: unknown + } + /** A state transition in a watched external process; `from` carries + * the prior state when known. */ + | { + readonly kind: 'status' + readonly subject: string + readonly status: string + readonly from?: string + readonly data?: unknown + } + /** A file or directory this run wrote that the user may care about. */ + | { + readonly kind: 'artifact' + readonly path: string + readonly description?: string + readonly data?: unknown + } + +// ———————————————————————————————————————————————————————————————————————— +// §2 Presented results — presentation materializes at the return site +// ———————————————————————————————————————————————————————————————————————— + +declare const PRESENTED: unique symbol + +/** + * What a completed command's handler returns inside `ok(...)`: pure data + * plus the presentation the ACTIVE FORMAT already materialized. Built + * exclusively by ctx.present (the brand makes hand-construction a type + * error) — the context knows the format, calls only the presentation + * functions it needs, and the value crossing the product→engine boundary + * is data all the way down. + * + * `data` is always present and is what the envelope's `result` + * serializes (json presentation overrides when supplied). Materialization + * by format: human → human + stdout + next; human+--quiet → stdout; + * json → json + next. + */ +export interface PresentedResult { + readonly [PRESENTED]: true + readonly data: T + /** The exit code selected at the return site; typed against the + * definition's documented `exitCodes`. Omitted = 0. */ + readonly exitCode?: number + /** + * The completed result's recorded findings (drift, verify failures) — + * Diagnostics: data, never thrown. Declared ONCE here; the engine + * renders them in human mode with the same layout as top-level errors + * (shown even under --quiet) AND carries them verbatim into + * CompletedEnvelope.diagnostics. One declaration, both surfaces, + * impossible to diverge. Guardrail (runtime, at the return site): any + * severity-'error' entry requires a non-zero exitCode — a genuine + * could-not-complete belongs in notOk. The test: notOk when the + * command couldn't do its job; diagnostics when finding these WAS the + * job. + */ + readonly diagnostics?: readonly Diagnostic[] + readonly presentation: { + readonly human?: readonly Block[] + readonly stdout?: readonly string[] + readonly json?: unknown + readonly next?: readonly NextAction[] + } +} +export { PRESENTED } + +/** + * The per-format presentation functions a handler supplies to + * ctx.present. Only the active format's functions are invoked, at the + * return site, where the outcome and its context are live. `human` + * composes engine primitives (R5: Block is the only vocabulary); + * `stdout` is the machine-consumable data lines the engine writes to + * stdout — what --quiet leaves, what a pipe receives; `json` overrides + * the envelope's `result` (default: the data itself); `next` supplies + * the typed nextActions — agents branch on them; the human renderer + * formats them as prose (no stored string form). + */ +export interface Presentations { + readonly human: (ui: Ui) => readonly Block[] + readonly stdout?: () => readonly string[] + readonly json?: () => unknown + readonly next?: () => readonly NextAction[] +} + +// ———————————————————————————————————————————————————————————————————————— +// §3 Config sections — R10 made structural +// ———————————————————————————————————————————————————————————————————————— + +/** + * A product's named slice of prisma.config.ts. The token couples the + * section name, its validated type, and its total validator — which + * RETURNS findings (Diagnostics); it never throws (R10). Commands bind + * to the token, which is how the engine knows which section a command + * needs — and therefore which diagnostics fail which commands. Keep + * validators dependency-light: they load with the definition tree at + * startup (R9). + */ +export interface ConfigSection { + readonly name: string + readonly validate: (raw: unknown) => SectionValidation +} + +export type SectionValidation = + | { readonly ok: true; readonly value: T; readonly diagnostics: readonly Diagnostic[] } + | { readonly ok: false; readonly diagnostics: readonly Diagnostic[] } + +export declare function defineConfigSection(spec: { + readonly name: string + readonly validate: (raw: unknown) => SectionValidation +}): ConfigSection + +// ———————————————————————————————————————————————————————————————————————— +// §4 The handler context — R4: the whole world arrives as one argument +// ———————————————————————————————————————————————————————————————————————— + +export interface CommandContext { + /** The validated value of the command's declared config section, or + * undefined when the config file has no such section. An INVALID + * needed section never reaches the handler — the engine already + * failed the command with that section's diagnostics. */ + readonly config: TConfig | undefined + + /** Builds the PresentedResult for the active format: calls only the + * presentation functions this format needs, at the return site. The + * only constructor of PresentedResult. `exitCode` is typed against + * the definition's documented `exitCodes` — a code outside them is a + * compile error at the call site. `diagnostics` are the completed + * result's recorded findings (see PresentedResult). */ + readonly present: ( + data: T, + presentations: Presentations, + opts?: { + readonly exitCode?: TCode + readonly diagnostics?: readonly Diagnostic[] + }, + ) => PresentedResult + + /** Management-API credentials, resolved at call time so long-lived + * sessions survive token refresh. Undefined when unauthenticated. + * Commands declaring `requiresCredentials` never see undefined — the + * engine fails them early with the sign-in error. */ + readonly getCredentials: () => Promise + + /** The one way to emit while running (§1). */ + readonly report: (event: EngineEvent) => void + + /** Interactive input (§4a). */ + readonly prompt: PromptSurface + + /** Fires on Ctrl-C/SIGTERM (engine-owned; the engine records which + * signal for the 130/143 exit, and force-exits on a second signal). + * Session commands run until it fires; everything else aborts + * in-flight work with it. */ + readonly signal: AbortSignal + + /** Where the user invoked the CLI. Products never read process.cwd(). */ + readonly cwd: string + + /** R13: probe an optional peer dependency's availability from the + * user's project. Never throws; never installs. Pair with + * `packageManager` to phrase the install command in the structured + * error when it's absent. */ + readonly probeDependency: (specifier: string) => Promise + + /** The user's detected package manager, for install-command phrasing. */ + readonly packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' +} + +export interface Credentials { + /** Opaque to the engine; shape owned by the Cloud product's auth + * library (placeholder pending its design). */ + readonly token: string + readonly workspaceId?: string +} + +/** + * §4a Prompts. Every prompt except `consent` may carry a + * product-specified `default`. Interactively, Enter accepts the default. + * Under --yes, a prompt WITH a default resolves to it without + * displaying; a prompt WITHOUT a default cannot be operated and the + * invocation halts with a structured error (exit 2). In + * json/non-interactive/CI/non-TTY contexts the same default rule + * applies as under --yes. User cancellation (Ctrl-C at the prompt) is a + * distinct structured error the engine maps to exit 3. + */ +export interface PromptSurface { + readonly confirm: ( + question: string, + opts?: { readonly default?: boolean }, + ) => Promise> + /** + * A question requiring EXPLICIT consent — not necessarily destructive, + * but never inferable. Structurally undefaultable: no default + * parameter exists, so --yes, Enter-through, and non-interactive + * contexts can never satisfy it; without a TTY it returns the + * interaction-required structured error, and the command's fix names + * the explicit flag that grants consent non-interactively. + */ + readonly consent: (question: string) => Promise> + readonly select: ( + question: string, + options: ReadonlyArray<{ value: T; label: string }>, + opts?: { readonly default?: T }, + ) => Promise> + readonly text: ( + question: string, + opts?: { readonly placeholder?: string; readonly default?: string }, + ) => Promise> +} + +// ———————————————————————————————————————————————————————————————————————— +// §5 Flags and positionals — R1: directly executable, typed by inference +// ———————————————————————————————————————————————————————————————————————— + +/** + * Product-declared flags. The shared family (§header) is engine-injected + * and reserved; handlers never see those values. Parse-time validation + * failures become structured errors carrying the allowed values — never + * framework strings. + * + * Deliberate asymmetry, matching CLI convention: flags are optional by + * default (requiredString is the exception); positionals are required by + * default (optionalString is the exception). + */ +export declare const flag: { + string(spec: { + brief: string + placeholder?: string + alias?: A & Char + default?: string + }): FlagSpec + requiredString(spec: { + brief: string + placeholder?: string + alias?: A & Char + }): FlagSpec + number(spec: { + brief: string + placeholder?: string + alias?: A & Char + default?: number + }): FlagSpec + boolean(spec: { brief: string; alias?: A & Char }): FlagSpec + enum(spec: { + brief: string + values: T + alias?: A & Char + default?: T[number] + }): FlagSpec + repeated(spec: { + brief: string + placeholder?: string + alias?: A & Char + }): FlagSpec +} + +/** Single-character alias, enforced at the type level: `Char<'q'>` is + * 'q'; `Char<'ab'>` is never. */ +export type Char = S extends `${string}${infer Rest}` + ? Rest extends '' + ? S + : never + : never + +declare const FLAG: unique symbol +export interface FlagSpec { + /** Phantom carrier for inference; exported so declaration emit works. */ + readonly [FLAG]: T +} +export { FLAG } + +export declare const positional: { + string(spec: { brief: string; placeholder: string }): PositionalSpec + optionalString(spec: { brief: string; placeholder: string }): PositionalSpec + /** Zero or more trailing values; at most one, declared last (order = + * declaration order; keys must not be integer-like). */ + variadic(spec: { brief: string; placeholder: string }): PositionalSpec +} +declare const POSITIONAL: unique symbol +export interface PositionalSpec { + readonly [POSITIONAL]: T +} +export { POSITIONAL } + +/** What a handler receives: separate namespaces, symmetric access — + * `args.flags.to`, `args.positionals.name`. */ +export interface Args< + TFlags extends Record>, + TPositionals extends Record>, +> { + readonly flags: { readonly [K in keyof TFlags]: TFlags[K] extends FlagSpec ? T : never } + readonly positionals: { + readonly [K in keyof TPositionals]: TPositionals[K] extends PositionalSpec ? T : never + } +} + +// ———————————————————————————————————————————————————————————————————————— +// §6 Command definitions — light at startup (R9), path-free (R12), +// runtime-discriminated by `kind` (stamped by the define* functions). +// Three modalities at equal rank: a result command runs to completion +// and presents; a session command runs until told to stop, speaking +// through events; a server command hands the stdio conversation to a +// foreign client. +// ———————————————————————————————————————————————————————————————————————— + +export interface CommandDefinition< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, + TCode extends number = never, +> { + readonly kind: 'result-command' + /** One line, imperative, shown in listings. */ + readonly brief: string + /** Paragraph(s) for --help. Words only — the engine formats. */ + readonly description?: string + /** Copy-pastable invocations, shown verbatim in help. */ + readonly examples?: readonly string[] + + readonly flags?: TFlags + readonly positionals?: TPositionals + + /** Binds the command to its product's config section (§3). */ + readonly configSection?: ConfigSection + + /** Fail early with the sign-in error when unauthenticated; the handler + * then always receives credentials. */ + readonly requiresCredentials?: boolean + + /** + * The command's documented exit codes (4–99): code → meaning. + * Rendered in help without executing anything; the keys type + * ctx.present's exitCode, so a code outside them is a compile error + * at the return site. Absent = the command only exits 0/1/2/3. + */ + readonly exitCodes?: Readonly> + + /** The heavy part, loaded only at execution (R9). The module's default + * export is the handler — annotate it with CommandHandler + * (type-only import of the light definition; no runtime cycle). */ + readonly handler: () => Promise<{ default: Handler }> +} + +export type Handler< + TFlags extends Record>, + TPositionals extends Record>, + TConfig, + TCode extends number = never, +> = ( + args: Args, + ctx: CommandContext, +) => Promise, CliStructuredError>> + +/** For impl files: `const run: CommandHandler = …` */ +export type CommandHandler = D extends CommandDefinition + ? Handler + : never + +export declare function defineCommand< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, + TCode extends number = never, +>( + def: Omit, 'kind'>, +): CommandDefinition + +/** + * A session command (dev, log tail): the handler runs until the signal + * fires, speaks entirely through events, and returns Result. No + * presentation (the engine owns the close-out line), no exit-code set. + * A session always supports json mode: the event stream is its json + * surface. + */ +export interface SessionCommandDefinition< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, +> { + readonly kind: 'session-command' + readonly brief: string + readonly description?: string + readonly examples?: readonly string[] + readonly flags?: TFlags + readonly positionals?: TPositionals + readonly configSection?: ConfigSection + readonly requiresCredentials?: boolean + readonly handler: () => Promise<{ + default: ( + args: Args, + ctx: CommandContext, + ) => Promise> + }> +} + +export declare function defineSessionCommand< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, +>( + def: Omit, 'kind'>, +): SessionCommandDefinition + +/** + * A server command (lsp): a foreign client on the other end of stdio + * owns the conversation, so the engine hands over the streams. Events, + * presentation, formats, and prompts do not apply — by definition, not + * by opt-out; the handler returns the exit code directly. Flags and + * config are allowed; the shared flag family is NOT injected. + */ +export interface ServerCommandDefinition< + TFlags extends Record> = {}, + TConfig = undefined, +> { + readonly kind: 'server-command' + readonly brief: string + readonly description?: string + readonly flags?: TFlags + readonly configSection?: ConfigSection + readonly handler: () => Promise<{ + default: ( + args: Args, + io: { + readonly stdin: InputStream + readonly stdout: OutputStream + readonly stderr: OutputStream + readonly signal: AbortSignal + readonly cwd: string + readonly config: TConfig | undefined + }, + ) => Promise + }> +} + +export declare function defineServerCommand< + TFlags extends Record> = {}, + TConfig = undefined, +>( + def: Omit, 'kind'>, +): ServerCommandDefinition + +/** Erased union for mount maps; `kind` is the runtime discriminant. */ +export type AnyCommand = + | CommandDefinition + | SessionCommandDefinition + | ServerCommandDefinition + +// ———————————————————————————————————————————————————————————————————————— +// §7 Presentation primitives — the R5 vocabulary +// ———————————————————————————————————————————————————————————————————————— + +/** Deliberately small; grows by the same evidence rule as events. */ +export type Block = + | { + readonly kind: 'summary' + readonly tone: 'ok' | 'error' | 'warn' | 'info' + readonly text: string + } + | { + readonly kind: 'fields' + readonly rows: ReadonlyArray<{ label: string; value: string; sensitive?: boolean }> + } + | { + readonly kind: 'table' + readonly columns: readonly string[] + readonly rows: ReadonlyArray + } + | { readonly kind: 'list'; readonly items: readonly string[] } + /** The corpus's most-rendered human structure (migration graphs, + * service trees). */ + | { readonly kind: 'tree'; readonly roots: readonly TreeNode[] } +// NOTE: recorded findings are NOT a Block — they are diagnostics on the +// presented result; the engine renders them with the top-level error +// layout and carries them into the envelope, so the two surfaces cannot +// diverge. + +export interface TreeNode { + readonly label: string + readonly children?: readonly TreeNode[] +} + +/** Styling helpers usable inside block text; no direct writing. */ +export interface Ui { + readonly emphasize: (text: string) => string + readonly dim: (text: string) => string + readonly code: (text: string) => string +} + +// ———————————————————————————————————————————————————————————————————————— +// §8 Streams — minimal structural types; no NodeJS.* in the public +// surface (runtime-agnosticism, R4's why) +// ———————————————————————————————————————————————————————————————————————— + +export interface OutputStream { + write(text: string): void +} +/** Byte-oriented, so server commands can implement byte-counted + * protocols (lsp's Content-Length framing). Decoding is the consumer's + * business; the engine's own prompt machinery decodes internally. + * setRawMode is present where the platform supports keypress input. */ +export interface InputStream extends AsyncIterable { + readonly setRawMode?: (enabled: boolean) => void +} + +// ———————————————————————————————————————————————————————————————————————— +// §9 Envelopes and the json stream +// ———————————————————————————————————————————————————————————————————————— + +export interface CompletedEnvelope { + /** ok = COMPLETED (the command executed to its end). A completed + * result may still carry findings and a non-zero exit code — bad news + * is a result, not an error. */ + readonly ok: true + /** The command's stable dotted identity — its full mount path + * ('project.env.add'). The schema-dispatch key for machine consumers; + * says nothing about arguments. */ + readonly commandId: string + /** The presented data (json presentation override when supplied). */ + readonly result: T + /** From the documented set; 0 when unset. */ + readonly exitCode: number + /** The recorded findings, verbatim from the presented result. */ + readonly diagnostics: readonly Diagnostic[] + readonly nextActions: readonly NextAction[] +} + +export interface ErroredEnvelope { + /** ok = false: the command did NOT complete. */ + readonly ok: false + readonly commandId: string + /** The PRIMARY error — what aborted the command. Severity 'error' by + * definition. Shape: Diagnostic (a thrown CliStructuredError + * serializes to exactly this). */ + readonly error: Diagnostic + /** Accompanying findings when the abort had several (three config + * typos are three diagnostics, not one flattened error). */ + readonly diagnostics: readonly Diagnostic[] + /** Aggregated from remediation events. */ + readonly nextActions: readonly NextAction[] +} + +/** + * json mode emits one StreamEvent per line: the handler's events, + * flattened with the stream metadata, then exactly one terminal + * 'result' member carrying the envelope. One union, one discriminant + * (`kind`). + */ +export type StreamEvent = + | (EngineEvent & StreamMeta) + | ({ readonly kind: 'result'; readonly envelope: CompletedEnvelope | ErroredEnvelope } & StreamMeta) + +export interface StreamMeta { + readonly commandId: string + /** ISO 8601 UTC. Injectable clock in tests (§11). */ + readonly timestamp: string +} + +// ———————————————————————————————————————————————————————————————————————— +// §10 Product export and shell mounting — R12: the shell owns the tree +// ———————————————————————————————————————————————————————————————————————— + +/** What a product package exports: commands by NAME. */ +export type CommandSet = Readonly> + +/** What the shell builds: commands by PATH (space-separated, + * 'db migrate'). Distinct alias so the two maps never read as one. */ +export type MountedTree = Readonly> + +/** + * Shell-side construction. Group help is declared with the mount, since + * groups belong to the tree, not to products. Collisions, unknown + * groups, reserved-flag violations, and grammar violations fail + * construction (build time, not run time). + */ +export declare function createCli(spec: { + readonly name: string + readonly version: string + readonly groups: Readonly> + readonly commands: MountedTree +}): Cli + +export interface Cli { + /** Parse, execute, render, return the exit code. Never calls + * process.exit; never touches streams other than the provided ones. */ + run(argv: readonly string[], runtime: Runtime): Promise +} + +/** Everything environmental, injected once by the bin (or by a test). */ +export interface Runtime { + readonly stdout: OutputStream + readonly stderr: OutputStream + readonly stdin: InputStream + readonly cwd: string + readonly env: Readonly> + readonly isTty: { readonly stdin: boolean; readonly stdout: boolean; readonly stderr: boolean } + readonly signal: AbortSignal + /** Loaded config + per-section diagnostics; the shell builds this via + * the unified loader (R10). Tests hand in fixtures. */ + readonly config: LoadedConfig + readonly getCredentials: () => Promise + readonly packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' +} + +export interface LoadedConfig { + /** Raw section values by name; validation happens per command via its + * ConfigSection token. */ + readonly sections: Readonly> + /** File-level problems (unevaluable module, missing version marker) — + * section: null fails every command. */ + readonly diagnostics: ReadonlyArray<{ + readonly section: string | null + readonly diagnostic: Diagnostic + }> +} + +// ———————————————————————————————————————————————————————————————————————— +// §11 The product-repo test harness — R7: same machinery, bytes out +// ———————————————————————————————————————————————————————————————————————— + +export declare function createTestCli(spec: { + readonly commands: MountedTree + readonly groups?: Readonly> + readonly config?: Readonly> + readonly credentials?: Credentials + readonly packageManager?: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' + /** Fixed clock for deterministic stream timestamps. */ + readonly now?: () => Date +}): TestCli + +export interface TestCli { + run( + argv: readonly string[], + opts?: { + readonly stdin?: string + /** Scripted prompt answers, consumed in order; a run that prompts + * past the script fails the test. */ + readonly answers?: ReadonlyArray + /** Abort the run (session tests): fires the context signal. */ + readonly abort?: AbortSignal + /** Live event tap, for asserting mid-session behavior before + * aborting. */ + readonly onEvent?: (event: EngineEvent) => void + readonly cwd?: string + readonly isTty?: { stdin?: boolean; stdout?: boolean; stderr?: boolean } + readonly env?: Readonly> + }, + ): Promise<{ + readonly exitCode: number + readonly stdout: string + readonly stderr: string + /** Parsed stream (events + the terminal result) when json mode. */ + readonly json: readonly StreamEvent[] + /** Every EngineEvent the handler emitted, for semantic assertions. */ + readonly events: readonly EngineEvent[] + /** The PresentedResult the handler returned (data + materialized + * presentation), for semantic assertions without byte-scraping. */ + readonly presented?: PresentedResult + }> +} diff --git a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v7.ts b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v7.ts new file mode 100644 index 00000000..8abb07df --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v7.ts @@ -0,0 +1,870 @@ +/** + * DRAFT v7 — the unified CLI engine's public interface. + * v1 initial · v2 round-1 fixes · v3 return-site presentation · + * v4 completed/errored, --format, log levels, prompt defaults · + * v5 round-3 closure · v6 Diagnostic, warnings fold, stream flatten · + * v7 operator line review: outcome as first-class argument (exitCode + * required iff catalogued; diagnostics never undefined), help/args/needs + * grouping, product manifests own config sections, requireDependency, + * validator-owned absence, credentials trimmed, no agent-prohibition + * property. Prior versions preserved as -v1…-v6.ts; reviews in + * ./reviews/. + * + * THE MODEL, in one analogy (operator, 2026-08-09): commands settle like + * promises. A command can COMPLETE — and its completion can be + * successful or unsuccessful, both presented through the same machinery, + * distinguished by exit code and diagnostics — or it can ERROR, which + * aborts out of the normal process and gets its own special handling. + * + * Implementation prerequisite: ADR 239 (prisma/prisma) is amended so + * completed-but-unsuccessful command results (verify/check/runner + * findings) are carried as diagnostics with their dotted codes inside a + * completed envelope with a documented exit code — not as structured + * failures with exit 2, as it classifies them today. The amendment also + * checks whether any shipped error uses severity 'info'; if none does, + * the severity scale of CliStructuredError and Diagnostic trims to + * error|warn — together, so the two shapes stay identical. + * + * Everything a product package imports for CLI purposes lives here (R3). + * Requirement references (R1–R14) point at docs/architecture/ + * cli-engine-requirements.md in prisma-cli. Nothing from stricli appears — + * it is an internal of the engine package. + * + * EXECUTION PROTOCOL. A handler receives (args, context), emits zero or + * more events through context.report, and finishes one of two ways: + * + * COMPLETED — it returns ok(ctx.present(outcome, presentations)): the + * command executed to its end. The outcome is what it concluded — + * data, diagnostics (recorded findings — data, never thrown), and, + * when the command documents exit codes, an explicit code at every + * return site. Presentation always runs for completed results. + * + * ERRORED — it returns notOk(structuredError): the command did not + * complete. The engine renders the error envelope; there is no product + * presentation on the error path. The primary error is severity + * 'error' by definition. `remediation` events emitted before the + * error are aggregated into the errored envelope's nextActions. + * + * PRECONDITIONS (a command's `needs`) are enforced by the engine BEFORE + * the handler loads: an invalid needed config section, missing + * credentials, an absent optional dependency, or a non-interactive + * context each fail the command early with the engine's own structured + * error — a handler only ever runs in a world where it can operate. + * + * Session commands run until context.signal fires, then clean up and + * return. Server commands hand the stdio conversation to a foreign + * client. Liveness display is the engine's. Nothing product-authored + * executes after the handler resolves. + * + * FORMATS AND LEVELS. `--format `, auto-selected when + * unspecified (human on a TTY stdout, json otherwise); `--json` is + * shorthand for `--format json`. In json mode the engine suppresses + * prompts (they fail structurally) and emits one StreamEvent per line. + * Commentary is filtered by `--log-level ` + * (default info); `--verbose` is shorthand for `--log-level verbose`. + * The engine injects the shared flag family on every non-server + * command: --format/--json, --log-level/-v/--verbose, -q/--quiet, + * -y/--yes, --interactive/--no-interactive, --color/--no-color. + * Products cannot declare flags with those names. Product flag keys are + * camelCase and transliterate to --kebab-case. + * + * EXIT CODES (R6): 0 completed; 1 bug only; 2 errored (expected, + * structured); 3 user abort; 4–99 documented per command in + * `exitCodes`; 130/143 delivered signals. First signal fires + * context.signal and awaits teardown; a second exits immediately. + */ + +// ———————————————————————————————————————————————————————————————————————— +// Foundation types (from the zero-dependency foundation package — which +// owns CliStructuredError, Result, NextAction, and Diagnostic). Shown +// for reading convenience. +// ———————————————————————————————————————————————————————————————————————— + +import type { CliStructuredError, Diagnostic, NextAction, Result } from '@prisma/cli-foundation' + +/* + * For reference — the foundation shapes this file leans on: + * + * Diagnostic — a recorded finding: pure data, never thrown, no stack. + * { code: 'NAMESPACE.SUBCODE', severity: 'error' | 'warn' | 'info', + * summary, why?, fix?, where?, meta?, docsUrl? } + * Field-for-field the settled error envelope (ADR 239) minus `ok` — + * identical scales included; the two shapes never diverge. + * + * NextAction — the typed agent-facing follow-up (platform-shipped form): + * { kind: 'run-command' | 'user-choice' | 'edit-file' | 'done', + * journey, label, command?, commands?, reason? } + */ + +/** The commentary severity scale; also the log-level axis. Distinct from + * Diagnostic severity: 'verbose' grades commentary, which never enters + * the envelope. Step outcomes are completion states, not severities. */ +export type Severity = 'error' | 'warn' | 'info' | 'verbose' +export type LogLevel = Severity + +export type Format = 'human' | 'json' + +// ———————————————————————————————————————————————————————————————————————— +// §1 Events — R14: one engine vocabulary, product extensions in `data` +// ———————————————————————————————————————————————————————————————————————— + +/** + * The engine event envelope. `kind`-specific fields are the common + * vocabulary the engine renders consistently (human mode) and streams + * (json mode, §9). `data` is the product extension: passed through to + * machine consumers untouched, never interpreted by the engine, + * documented and versioned by the product as its own public API. A + * structure recurring inside `data` across commands is the promotion + * signal (R14). + * + * Rendering, human mode: `output` events with channel 'data' are the + * command's data and go to OUR stdout; everything else is commentary on + * stderr, filtered by the active log level. Events are transcript: they + * are NOT aggregated into the envelope (the one exception is + * `remediation` → nextActions). Findings that belong in the envelope + * are diagnostics on the presented outcome, not events. + * + * report() is synchronous fire-and-forget; the engine buffers and writes + * asynchronously. Calling it after the handler has resolved is a bug + * (InternalError). Events during teardown (after the signal, before + * resolution) are normal. + */ +export type EngineEvent = + | { + readonly kind: 'step-started' + readonly step: string + readonly id?: string + readonly parentId?: string + readonly data?: unknown + } + | { + readonly kind: 'step-finished' + readonly step: string + readonly id?: string + readonly outcome: 'ok' | 'failed' | 'skipped' | 'warning' + readonly data?: unknown + } + | { + readonly kind: 'progress' + readonly step?: string + readonly completed: number + readonly total?: number + readonly data?: unknown + } + /** Commentary at a severity; display-filtered by log level. Transcript + * only. 'error' is not valid here: fatal problems are the Result's + * error; envelope-worthy findings are diagnostics. */ + | { + readonly kind: 'message' + readonly severity: Exclude + readonly text: string + readonly data?: unknown + } + | { + readonly kind: 'output' + readonly source: string + readonly channel: 'data' | 'diagnostic' + readonly line: string + readonly data?: unknown + } + | { readonly kind: 'remediation'; readonly action: NextAction; readonly data?: unknown } + | { + readonly kind: 'endpoint' + readonly name: string + readonly url: string + readonly data?: unknown + } + | { + readonly kind: 'status' + readonly subject: string + readonly status: string + readonly from?: string + readonly data?: unknown + } + | { + readonly kind: 'artifact' + readonly path: string + readonly description?: string + readonly data?: unknown + } + +// ———————————————————————————————————————————————————————————————————————— +// §2 Outcomes and presented results — presentation materializes at the +// return site +// ———————————————————————————————————————————————————————————————————————— + +/** + * What a command concluded, stated at the return site. `exitCode` is + * REQUIRED at every return site iff the command documents exit codes + * (`0` for the clean path, a documented code otherwise — the type makes + * forgetting impossible exactly where a decision exists) and forbidden + * otherwise. `diagnostics` may be omitted at the call site; the + * presented result always carries an array. + */ +export type Outcome = [TCode] extends [never] + ? { + readonly data: T + readonly diagnostics?: readonly Diagnostic[] + } + : { + readonly data: T + readonly exitCode: TCode | 0 + readonly diagnostics?: readonly Diagnostic[] + } + +declare const PRESENTED: unique symbol + +/** + * What a completed command's handler returns inside `ok(...)`: the + * outcome plus the presentation the ACTIVE FORMAT already materialized. + * Built exclusively by ctx.present (the brand makes hand-construction a + * type error) — the context knows the format, calls only the + * presentation functions it needs, and the value crossing the + * product→engine boundary is data all the way down. + * + * `data` is what the envelope's `result` serializes (json presentation + * overrides when supplied). Materialization by format: human → human + + * stdout + next; human+--quiet → stdout; json → json + next. + * + * Guardrail (runtime, at the return site): a severity-'error' diagnostic + * requires a non-zero exitCode — a genuine could-not-complete belongs in + * notOk. The test: notOk when the command couldn't do its job; + * diagnostics when finding these WAS the job. + */ +export interface PresentedResult { + readonly [PRESENTED]: true + readonly data: T + /** 0 unless the outcome selected a documented code. */ + readonly exitCode: number + /** Never undefined; empty when the outcome recorded no findings. */ + readonly diagnostics: readonly Diagnostic[] + readonly presentation: { + readonly human?: readonly Block[] + readonly stdout?: readonly string[] + readonly json?: unknown + readonly next?: readonly NextAction[] + } +} +export { PRESENTED } + +/** + * The per-format presentation functions a handler supplies to + * ctx.present. Only the active format's functions are invoked, at the + * return site. `human` composes engine primitives (R5); `stdout` is the + * machine-consumable data lines — what --quiet leaves, what a pipe + * receives; `json` overrides the envelope's `result` (default: the + * data); `next` supplies the typed nextActions — agents branch on them; + * the human renderer formats them as prose (no stored string form). + */ +export interface Presentations { + readonly human: (ui: Ui) => readonly Block[] + readonly stdout?: () => readonly string[] + readonly json?: () => unknown + readonly next?: () => readonly NextAction[] +} + +// ———————————————————————————————————————————————————————————————————————— +// §3 Config sections — a PRODUCT-level fact (one product, one section) +// ———————————————————————————————————————————————————————————————————————— + +/** + * A product's named slice of prisma.config.ts, declared once in the + * product's manifest (§10). The token couples the section name, its + * validated type, and its total validator. Commands that need the + * section reference the token in `needs.config`, which is how the + * engine knows which commands an invalid section fails — and how + * ctx.config gets its type. + * + * The validator OWNS absence: its input is the raw section value, or + * undefined when the config file has no such section. A product that + * wants defaults applies them here; one that requires the section emits + * a section-required diagnostic here. It returns findings; it never + * throws (R10). Keep validators dependency-light: they load with the + * definition tree at startup (R9). + */ +export interface ConfigSection { + readonly name: string + readonly validate: (raw: unknown | undefined) => SectionValidation +} + +export type SectionValidation = + | { readonly ok: true; readonly value: T; readonly diagnostics: readonly Diagnostic[] } + | { readonly ok: false; readonly diagnostics: readonly Diagnostic[] } + +export declare function defineConfigSection(spec: { + readonly name: string + readonly validate: (raw: unknown | undefined) => SectionValidation +}): ConfigSection + +// ———————————————————————————————————————————————————————————————————————— +// §4 The handler context — R4: the whole world arrives as one argument +// ———————————————————————————————————————————————————————————————————————— + +export interface CommandContext { + /** The validated value of the command's needed config section — + * exactly TConfig; absence semantics belong to the product's + * validator (§3). Commands with no config need get undefined. */ + readonly config: TConfig + + /** Builds the PresentedResult for the active format: "present this + * outcome, via these presentations." The only constructor of + * PresentedResult. */ + readonly present: ( + outcome: Outcome, + presentations: Presentations, + ) => PresentedResult + + /** Management-API credentials, resolved at call time so long-lived + * sessions survive token refresh. Undefined when unauthenticated. + * Commands with needs.credentials never see undefined — the engine + * fails them early with the sign-in error. */ + readonly getCredentials: () => Promise + + /** The one way to emit while running (§1). */ + readonly report: (event: EngineEvent) => void + + /** Interactive input (§4a). */ + readonly prompt: PromptSurface + + /** Fires on Ctrl-C/SIGTERM (engine-owned; second signal force-exits). + * Session commands run until it fires; everything else aborts + * in-flight work with it. */ + readonly signal: AbortSignal + + /** Where the user invoked the CLI. Products never read process.cwd(). */ + readonly cwd: string + + /** + * R13, the conditional form (evidence: composer needs @prisma/dev only + * when the config declares postgres resources — unconditional needs + * belong in `needs.dependencies`). Resolves when the optional peer + * dependency is importable from the user's project; otherwise returns + * the ENGINE'S structured missing-dependency error — install command + * phrased by the engine with the user's package manager — for the + * handler to pass to notOk. Products never craft install prose. + */ + readonly requireDependency: (specifier: string) => Promise> +} + +export interface Credentials { + /** Opaque to the engine; shape owned by the Cloud product's auth + * library (placeholder pending its design). Workspace selection is + * session state, not a credential — it lives with that library, not + * here. */ + readonly token: string +} + +/** + * §4a Prompts. Every prompt except `consent` may carry a + * product-specified `default`. Interactively, Enter accepts the default. + * Under --yes, a prompt WITH a default resolves to it without + * displaying; a prompt WITHOUT a default cannot be operated and the + * invocation halts with a structured error (exit 2). In + * json/non-interactive/CI/non-TTY contexts the same default rule + * applies. User cancellation (Ctrl-C at the prompt) is a distinct + * structured error the engine maps to exit 3. + */ +export interface PromptSurface { + readonly confirm: ( + question: string, + opts?: { readonly default?: boolean }, + ) => Promise> + /** + * A question requiring EXPLICIT consent — never inferable, not + * necessarily destructive. Structurally undefaultable: no default + * parameter exists, so --yes, Enter-through, and non-interactive + * contexts can never satisfy it; the command's fix names the explicit + * flag that grants consent non-interactively. + */ + readonly consent: (question: string) => Promise> + readonly select: ( + question: string, + options: ReadonlyArray<{ value: T; label: string }>, + opts?: { readonly default?: T }, + ) => Promise> + readonly text: ( + question: string, + opts?: { readonly placeholder?: string; readonly default?: string }, + ) => Promise> +} + +// ———————————————————————————————————————————————————————————————————————— +// §5 Flags and positionals — R1: directly executable, typed by inference +// ———————————————————————————————————————————————————————————————————————— + +/** + * Product-declared flags. The shared family (§header) is engine-injected + * and reserved; handlers never see those values. Parse-time validation + * failures become structured errors carrying the allowed values. + * + * Deliberate asymmetry, matching CLI convention: flags are optional by + * default (requiredString is the exception); positionals are required by + * default (optionalString is the exception). + */ +export declare const flag: { + string(spec: { + brief: string + placeholder?: string + alias?: A & Char + default?: string + }): FlagSpec + requiredString(spec: { + brief: string + placeholder?: string + alias?: A & Char + }): FlagSpec + number(spec: { + brief: string + placeholder?: string + alias?: A & Char + default?: number + }): FlagSpec + boolean(spec: { brief: string; alias?: A & Char }): FlagSpec + enum(spec: { + brief: string + values: T + alias?: A & Char + default?: T[number] + }): FlagSpec + repeated(spec: { + brief: string + placeholder?: string + alias?: A & Char + }): FlagSpec +} + +/** Single-character alias, enforced at the type level: `Char<'q'>` is + * 'q'; `Char<'ab'>` is never. */ +export type Char = S extends `${string}${infer Rest}` + ? Rest extends '' + ? S + : never + : never + +declare const FLAG: unique symbol +export interface FlagSpec { + /** Phantom carrier for inference; exported so declaration emit works. */ + readonly [FLAG]: T +} +export { FLAG } + +export declare const positional: { + string(spec: { brief: string; placeholder: string }): PositionalSpec + optionalString(spec: { brief: string; placeholder: string }): PositionalSpec + /** Zero or more trailing values; at most one, declared last (order = + * declaration order; keys must not be integer-like). */ + variadic(spec: { brief: string; placeholder: string }): PositionalSpec +} +declare const POSITIONAL: unique symbol +export interface PositionalSpec { + readonly [POSITIONAL]: T +} +export { POSITIONAL } + +/** The parse SPI: a command's argument surface, one property. */ +export interface ArgsSpec< + TFlags extends Record>, + TPositionals extends Record>, +> { + readonly flags?: TFlags + readonly positionals?: TPositionals +} + +/** What a handler receives: separate namespaces, symmetric access — + * `args.flags.to`, `args.positionals.name`. */ +export interface Args< + TFlags extends Record>, + TPositionals extends Record>, +> { + readonly flags: { readonly [K in keyof TFlags]: TFlags[K] extends FlagSpec ? T : never } + readonly positionals: { + readonly [K in keyof TPositionals]: TPositionals[K] extends PositionalSpec ? T : never + } +} + +// ———————————————————————————————————————————————————————————————————————— +// §6 Command definitions — light at startup (R9), path-free (R12), +// runtime-discriminated by `kind`. Three modalities at equal rank: +// result / session / server. +// ———————————————————————————————————————————————————————————————————————— + +/** The help SPI: how the command shows itself in help output. Words + * only — the engine formats. */ +export interface HelpSpec { + /** One line, imperative, shown in listings. */ + readonly summary: string + readonly description?: string + /** Copy-pastable invocations, shown verbatim. */ + readonly examples?: readonly string[] +} + +/** + * The preconditions SPI: everything the engine gates BEFORE the handler + * loads. Each unmet need fails the command early with the engine's own + * structured error — consistent phrasing by construction, and a handler + * never runs in a world where it can't operate. + */ +export interface NeedsSpec { + /** The product's config section token: validate it, fail me on its + * error diagnostics, hand me the value as ctx.config. */ + readonly config?: ConfigSection + /** Fail early with the sign-in error when unauthenticated. */ + readonly credentials?: true + /** Optional peer dependencies this command cannot run without; the + * engine probes resolvability and phrases the install error. + * (Conditional needs use ctx.requireDependency instead.) */ + readonly dependencies?: readonly string[] + /** + * Fail early (before execution, before side effects) in + * json/non-interactive/CI/non-TTY contexts. This is a MECHANICAL + * precondition — "an interactive terminal is required" — and + * deliberately NOT an agent barrier: the client's nature is + * unverifiable, and a flag claiming to exclude agents would be a + * false guarantee. Anything requiring a verified human belongs + * server-side, where identity actually exists. + */ + readonly interaction?: true +} + +export interface CommandDefinition< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, + TCode extends number = never, +> { + readonly kind: 'result-command' + readonly help: HelpSpec + readonly args?: ArgsSpec + readonly needs?: NeedsSpec + + /** + * The command's documented exit codes (4–99): code → meaning. + * Rendered in help without executing anything; the keys type the + * outcome's exitCode, making it REQUIRED at every return site (`0` or + * a documented code). Absent = the command only exits 0/1/2/3 and the + * outcome carries no exitCode. + */ + readonly exitCodes?: Readonly> + + /** The heavy part, loaded only at execution (R9). The module's default + * export is the handler — annotate it with CommandHandler + * (type-only import of the light definition; no runtime cycle). */ + readonly handler: () => Promise<{ default: Handler }> +} + +export type Handler< + TFlags extends Record>, + TPositionals extends Record>, + TConfig, + TCode extends number = never, +> = ( + args: Args, + ctx: CommandContext, +) => Promise, CliStructuredError>> + +/** For impl files: `const run: CommandHandler = …` */ +export type CommandHandler = D extends CommandDefinition + ? Handler + : never + +export declare function defineCommand< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, + TCode extends number = never, +>( + def: Omit, 'kind'>, +): CommandDefinition + +/** + * A session command (dev, log tail): runs until the signal fires, + * speaks entirely through events, returns Result. No + * presentation, no exit-code set. A session always supports json mode: + * the event stream is its json surface. + */ +export interface SessionCommandDefinition< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, +> { + readonly kind: 'session-command' + readonly help: HelpSpec + readonly args?: ArgsSpec + readonly needs?: NeedsSpec + readonly handler: () => Promise<{ + default: ( + args: Args, + ctx: CommandContext, + ) => Promise> + }> +} + +export declare function defineSessionCommand< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, +>( + def: Omit, 'kind'>, +): SessionCommandDefinition + +/** + * A server command (lsp): a foreign client on the other end of stdio + * owns the conversation, so the engine hands over the streams. Events, + * presentation, formats, and prompts do not apply — by definition, not + * by opt-out; the handler returns the exit code directly. The shared + * flag family is NOT injected. + */ +export interface ServerCommandDefinition< + TFlags extends Record> = {}, + TConfig = undefined, +> { + readonly kind: 'server-command' + readonly help: HelpSpec + readonly args?: ArgsSpec + readonly needs?: NeedsSpec + readonly handler: () => Promise<{ + default: ( + args: Args, + io: { + readonly stdin: InputStream + readonly stdout: OutputStream + readonly stderr: OutputStream + readonly signal: AbortSignal + readonly cwd: string + readonly config: TConfig + }, + ) => Promise + }> +} + +export declare function defineServerCommand< + TFlags extends Record> = {}, + TConfig = undefined, +>( + def: Omit, 'kind'>, +): ServerCommandDefinition + +/** Erased union for manifests and mount maps; `kind` discriminates. */ +export type AnyCommand = + | CommandDefinition + | SessionCommandDefinition + | ServerCommandDefinition + +// ———————————————————————————————————————————————————————————————————————— +// §7 Presentation primitives — the R5 vocabulary +// ———————————————————————————————————————————————————————————————————————— + +/** Deliberately small; grows by the same evidence rule as events. */ +export type Block = + | { + readonly kind: 'summary' + readonly tone: 'ok' | 'error' | 'warn' | 'info' + readonly text: string + } + | { + readonly kind: 'fields' + readonly rows: ReadonlyArray<{ label: string; value: string; sensitive?: boolean }> + } + | { + readonly kind: 'table' + readonly columns: readonly string[] + readonly rows: ReadonlyArray + } + | { readonly kind: 'list'; readonly items: readonly string[] } + | { readonly kind: 'tree'; readonly roots: readonly TreeNode[] } +// NOTE: recorded findings are NOT a Block — they are the outcome's +// diagnostics; the engine renders them with the top-level error layout +// and carries them into the envelope, so the two surfaces cannot +// diverge. + +export interface TreeNode { + readonly label: string + readonly children?: readonly TreeNode[] +} + +/** Styling helpers usable inside block text; no direct writing. */ +export interface Ui { + readonly emphasize: (text: string) => string + readonly dim: (text: string) => string + readonly code: (text: string) => string +} + +// ———————————————————————————————————————————————————————————————————————— +// §8 Streams — minimal structural types; no NodeJS.* in the public +// surface +// ———————————————————————————————————————————————————————————————————————— + +export interface OutputStream { + write(text: string): void +} +/** Byte-oriented, so server commands can implement byte-counted + * protocols (lsp's Content-Length framing). setRawMode is present + * where the platform supports keypress input. */ +export interface InputStream extends AsyncIterable { + readonly setRawMode?: (enabled: boolean) => void +} + +// ———————————————————————————————————————————————————————————————————————— +// §9 Envelopes and the json stream +// ———————————————————————————————————————————————————————————————————————— + +export interface CompletedEnvelope { + /** ok = COMPLETED (the command executed to its end). A completed + * result may still carry findings and a non-zero exit code — bad news + * is a result, not an error. */ + readonly ok: true + /** The command's stable dotted identity — its full mount path + * ('project.env.add'). The schema-dispatch key for machine consumers; + * says nothing about arguments. */ + readonly commandId: string + readonly result: T + readonly exitCode: number + /** The recorded findings, verbatim from the presented outcome. */ + readonly diagnostics: readonly Diagnostic[] + readonly nextActions: readonly NextAction[] +} + +export interface ErroredEnvelope { + /** ok = false: the command did NOT complete. */ + readonly ok: false + readonly commandId: string + /** The PRIMARY error — what aborted the command. Severity 'error' by + * definition. A thrown CliStructuredError serializes to exactly this + * shape. */ + readonly error: Diagnostic + /** Accompanying findings when the abort had several (three config + * typos are three diagnostics, not one flattened error). */ + readonly diagnostics: readonly Diagnostic[] + /** Aggregated from remediation events. */ + readonly nextActions: readonly NextAction[] +} + +/** json mode emits one StreamEvent per line: the handler's events, + * flattened with the stream metadata, then exactly one terminal + * 'result' member carrying the envelope. */ +export type StreamEvent = + | (EngineEvent & StreamMeta) + | ({ readonly kind: 'result'; readonly envelope: CompletedEnvelope | ErroredEnvelope } & StreamMeta) + +export interface StreamMeta { + readonly commandId: string + /** ISO 8601 UTC. Injectable clock in tests (§11). */ + readonly timestamp: string +} + +// ———————————————————————————————————————————————————————————————————————— +// §10 Product manifests and shell mounting — R12: the shell owns the +// tree; a product owns its section +// ———————————————————————————————————————————————————————————————————————— + +/** + * What a product package exports: its config section (declared once — + * a product-level fact) and its commands by NAME. The unified config + * loader consumes the sections; the shell mounts the commands. A + * command whose needs.config token is not its product's section is a + * construction error. + */ +export interface ProductManifest { + readonly configSection?: ConfigSection + readonly commands: Readonly> +} + +/** What the shell builds: commands by PATH (space-separated, + * 'db migrate'). */ +export type MountedTree = Readonly> + +/** + * Shell-side construction. Group help is declared with the mount, since + * groups belong to the tree, not to products. Collisions, unknown + * groups, reserved-flag violations, grammar violations, and + * foreign-section references fail construction (build time, not run + * time). + */ +export declare function createCli(spec: { + readonly name: string + readonly version: string + readonly products: readonly ProductManifest[] + readonly groups: Readonly> + readonly commands: MountedTree +}): Cli + +export interface Cli { + /** Parse, execute, render, return the exit code. Never calls + * process.exit; never touches streams other than the provided ones. */ + run(argv: readonly string[], runtime: Runtime): Promise +} + +/** Everything environmental, injected once by the bin (or by a test). */ +export interface Runtime { + readonly stdout: OutputStream + readonly stderr: OutputStream + readonly stdin: InputStream + readonly cwd: string + readonly env: Readonly> + readonly isTty: { readonly stdin: boolean; readonly stdout: boolean; readonly stderr: boolean } + readonly signal: AbortSignal + /** Loaded config + file-level diagnostics; the shell builds this via + * the unified loader (R10). Tests hand in fixtures. */ + readonly config: LoadedConfig + readonly getCredentials: () => Promise + /** Used by the ENGINE to phrase install commands (products never + * do — see needs.dependencies and ctx.requireDependency). */ + readonly packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' +} + +export interface LoadedConfig { + /** Raw section values by name; validation happens per command via its + * product's section token. */ + readonly sections: Readonly> + /** File-level problems (unevaluable module, missing version marker) — + * section: null fails every command. */ + readonly diagnostics: ReadonlyArray<{ + readonly section: string | null + readonly diagnostic: Diagnostic + }> +} + +// ———————————————————————————————————————————————————————————————————————— +// §11 The product-repo test harness — R7: same machinery, bytes out +// ———————————————————————————————————————————————————————————————————————— + +export declare function createTestCli(spec: { + readonly products?: readonly ProductManifest[] + readonly commands: MountedTree + readonly groups?: Readonly> + readonly config?: Readonly> + readonly credentials?: Credentials + readonly packageManager?: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' + /** Fixed clock for deterministic stream timestamps. */ + readonly now?: () => Date +}): TestCli + +export interface TestCli { + run( + argv: readonly string[], + opts?: { + readonly stdin?: string + /** Scripted prompt answers, consumed in order; a run that prompts + * past the script fails the test. */ + readonly answers?: ReadonlyArray + /** Abort the run (session tests): fires the context signal. */ + readonly abort?: AbortSignal + /** Live event tap, for asserting mid-session behavior. */ + readonly onEvent?: (event: EngineEvent) => void + readonly cwd?: string + readonly isTty?: { stdin?: boolean; stdout?: boolean; stderr?: boolean } + readonly env?: Readonly> + }, + ): Promise<{ + readonly exitCode: number + readonly stdout: string + readonly stderr: string + /** Parsed stream (events + the terminal result) when json mode. */ + readonly json: readonly StreamEvent[] + /** Every EngineEvent the handler emitted, for semantic assertions. */ + readonly events: readonly EngineEvent[] + /** The PresentedResult the handler returned, for semantic + * assertions without byte-scraping. */ + readonly presented?: PresentedResult + }> +} diff --git a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft.ts b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft.ts new file mode 100644 index 00000000..51f346db --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft.ts @@ -0,0 +1,884 @@ +/** + * DRAFT v8 — the unified CLI engine's public interface. + * v1 initial · v2 round-1 fixes · v3 return-site presentation · + * v4 completed/errored, --format, log levels, prompt defaults · + * v5 round-3 closure · v6 Diagnostic, warnings fold, stream flatten · + * v7 outcome-first present, help/args/needs, product manifests · + * v8 packaging and residue rulings: ONE library package + * (@prisma/cli-engine, with @stricli/core as an ordinary exact-pinned + * dependency — bundling was considered and rejected: unusual for a + * library, blinds security audit; R3's hiding is about types, which no + * dependency violates) with a ./protocol subpath for types-only + * consumers; + * NextAction.journey dropped (no consumer); docs URLs derived from a + * manifest-supplied base; committed versions for releases; auth library + * lives in the CLI repo, distinct from Prisma Cloud. Prior versions + * preserved as -v1…-v7.ts; reviews in ./reviews/. + * + * THE MODEL, in one analogy (operator, 2026-08-09): commands settle like + * promises. A command can COMPLETE — and its completion can be + * successful or unsuccessful, both presented through the same machinery, + * distinguished by exit code and diagnostics — or it can ERROR, which + * aborts out of the normal process and gets its own special handling. + * + * Implementation prerequisite: ADR 239 (prisma/prisma) is amended so + * completed-but-unsuccessful command results (verify/check/runner + * findings) are carried as diagnostics with their dotted codes inside a + * completed envelope with a documented exit code — not as structured + * failures with exit 2, as it classifies them today. The amendment also + * checks whether any shipped error uses severity 'info'; if none does, + * the severity scale of CliStructuredError and Diagnostic trims to + * error|warn — together, so the two shapes stay identical. + * + * Everything a product package imports for CLI purposes lives here (R3). + * Requirement references (R1–R14) point at docs/architecture/ + * cli-engine-requirements.md in prisma-cli. Nothing from stricli appears — + * it is an internal of the engine package. + * + * EXECUTION PROTOCOL. A handler receives (args, context), emits zero or + * more events through context.report, and finishes one of two ways: + * + * COMPLETED — it returns ok(ctx.present(outcome, presentations)): the + * command executed to its end. The outcome is what it concluded — + * data, diagnostics (recorded findings — data, never thrown), and, + * when the command documents exit codes, an explicit code at every + * return site. Presentation always runs for completed results. + * + * ERRORED — it returns notOk(structuredError): the command did not + * complete. The engine renders the error envelope; there is no product + * presentation on the error path. The primary error is severity + * 'error' by definition. `remediation` events emitted before the + * error are aggregated into the errored envelope's nextActions. + * + * PRECONDITIONS (a command's `needs`) are enforced by the engine BEFORE + * the handler loads: an invalid needed config section, missing + * credentials, an absent optional dependency, or a non-interactive + * context each fail the command early with the engine's own structured + * error — a handler only ever runs in a world where it can operate. + * + * Session commands run until context.signal fires, then clean up and + * return. Server commands hand the stdio conversation to a foreign + * client. Liveness display is the engine's. Nothing product-authored + * executes after the handler resolves. + * + * FORMATS AND LEVELS. `--format `, auto-selected when + * unspecified (human on a TTY stdout, json otherwise); `--json` is + * shorthand for `--format json`. In json mode the engine suppresses + * prompts (they fail structurally) and emits one StreamEvent per line. + * Commentary is filtered by `--log-level ` + * (default info); `--verbose` is shorthand for `--log-level verbose`. + * The engine injects the shared flag family on every non-server + * command: --format/--json, --log-level/-v/--verbose, -q/--quiet, + * -y/--yes, --interactive/--no-interactive, --color/--no-color. + * Products cannot declare flags with those names. Product flag keys are + * camelCase and transliterate to --kebab-case. + * + * EXIT CODES (R6): 0 completed; 1 bug only; 2 errored (expected, + * structured); 3 user abort; 4–99 documented per command in + * `exitCodes`; 130/143 delivered signals. First signal fires + * context.signal and awaits teardown; a second exits immediately. + */ + +// ———————————————————————————————————————————————————————————————————————— +// Protocol types — the ./protocol subpath of this same package: the +// shapes that cross package and process boundaries (CliStructuredError, +// Result, NextAction, Diagnostic). Types-only consumers (the repos' +// duplicated foundations, external tools) import the subpath and drag +// nothing else. Shown for reading convenience. +// ———————————————————————————————————————————————————————————————————————— + +import type { CliStructuredError, Diagnostic, NextAction, Result } from '@prisma/cli-engine/protocol' + +/* + * For reference — the foundation shapes this file leans on: + * + * Diagnostic — a recorded finding: pure data, never thrown, no stack. + * { code: 'NAMESPACE.SUBCODE', severity: 'error' | 'warn' | 'info', + * summary, why?, fix?, where?, meta?, docsUrl? } + * Field-for-field the settled error envelope (ADR 239) minus `ok` — + * identical scales included; the two shapes never diverge. + * + * NextAction — the typed agent-facing follow-up (platform-shipped form, + * minus its `journey` grouping label — dropped: no consumer branches on + * it; R14's evidence rule readmits it if one appears): + * { kind: 'run-command' | 'user-choice' | 'edit-file' | 'done', + * label, command?, commands?, reason? } + */ + +/** The commentary severity scale; also the log-level axis. Distinct from + * Diagnostic severity: 'verbose' grades commentary, which never enters + * the envelope. Step outcomes are completion states, not severities. */ +export type Severity = 'error' | 'warn' | 'info' | 'verbose' +export type LogLevel = Severity + +export type Format = 'human' | 'json' + +// ———————————————————————————————————————————————————————————————————————— +// §1 Events — R14: one engine vocabulary, product extensions in `data` +// ———————————————————————————————————————————————————————————————————————— + +/** + * The engine event envelope. `kind`-specific fields are the common + * vocabulary the engine renders consistently (human mode) and streams + * (json mode, §9). `data` is the product extension: passed through to + * machine consumers untouched, never interpreted by the engine, + * documented and versioned by the product as its own public API. A + * structure recurring inside `data` across commands is the promotion + * signal (R14). + * + * Rendering, human mode: `output` events with channel 'data' are the + * command's data and go to OUR stdout; everything else is commentary on + * stderr, filtered by the active log level. Events are transcript: they + * are NOT aggregated into the envelope (the one exception is + * `remediation` → nextActions). Findings that belong in the envelope + * are diagnostics on the presented outcome, not events. + * + * report() is synchronous fire-and-forget; the engine buffers and writes + * asynchronously. Calling it after the handler has resolved is a bug + * (InternalError). Events during teardown (after the signal, before + * resolution) are normal. + */ +export type EngineEvent = + | { + readonly kind: 'step-started' + readonly step: string + readonly id?: string + readonly parentId?: string + readonly data?: unknown + } + | { + readonly kind: 'step-finished' + readonly step: string + readonly id?: string + readonly outcome: 'ok' | 'failed' | 'skipped' | 'warning' + readonly data?: unknown + } + | { + readonly kind: 'progress' + readonly step?: string + readonly completed: number + readonly total?: number + readonly data?: unknown + } + /** Commentary at a severity; display-filtered by log level. Transcript + * only. 'error' is not valid here: fatal problems are the Result's + * error; envelope-worthy findings are diagnostics. */ + | { + readonly kind: 'message' + readonly severity: Exclude + readonly text: string + readonly data?: unknown + } + | { + readonly kind: 'output' + readonly source: string + readonly channel: 'data' | 'diagnostic' + readonly line: string + readonly data?: unknown + } + | { readonly kind: 'remediation'; readonly action: NextAction; readonly data?: unknown } + | { + readonly kind: 'endpoint' + readonly name: string + readonly url: string + readonly data?: unknown + } + | { + readonly kind: 'status' + readonly subject: string + readonly status: string + readonly from?: string + readonly data?: unknown + } + | { + readonly kind: 'artifact' + readonly path: string + readonly description?: string + readonly data?: unknown + } + +// ———————————————————————————————————————————————————————————————————————— +// §2 Outcomes and presented results — presentation materializes at the +// return site +// ———————————————————————————————————————————————————————————————————————— + +/** + * What a command concluded, stated at the return site. `exitCode` is + * REQUIRED at every return site iff the command documents exit codes + * (`0` for the clean path, a documented code otherwise — the type makes + * forgetting impossible exactly where a decision exists) and forbidden + * otherwise. `diagnostics` may be omitted at the call site; the + * presented result always carries an array. + */ +export type Outcome = [TCode] extends [never] + ? { + readonly data: T + readonly diagnostics?: readonly Diagnostic[] + } + : { + readonly data: T + readonly exitCode: TCode | 0 + readonly diagnostics?: readonly Diagnostic[] + } + +declare const PRESENTED: unique symbol + +/** + * What a completed command's handler returns inside `ok(...)`: the + * outcome plus the presentation the ACTIVE FORMAT already materialized. + * Built exclusively by ctx.present (the brand makes hand-construction a + * type error) — the context knows the format, calls only the + * presentation functions it needs, and the value crossing the + * product→engine boundary is data all the way down. + * + * `data` is what the envelope's `result` serializes (json presentation + * overrides when supplied). Materialization by format: human → human + + * stdout + next; human+--quiet → stdout; json → json + next. + * + * Guardrail (runtime, at the return site): a severity-'error' diagnostic + * requires a non-zero exitCode — a genuine could-not-complete belongs in + * notOk. The test: notOk when the command couldn't do its job; + * diagnostics when finding these WAS the job. + */ +export interface PresentedResult { + readonly [PRESENTED]: true + readonly data: T + /** 0 unless the outcome selected a documented code. */ + readonly exitCode: number + /** Never undefined; empty when the outcome recorded no findings. */ + readonly diagnostics: readonly Diagnostic[] + readonly presentation: { + readonly human?: readonly Block[] + readonly stdout?: readonly string[] + readonly json?: unknown + readonly next?: readonly NextAction[] + } +} +export { PRESENTED } + +/** + * The per-format presentation functions a handler supplies to + * ctx.present. Only the active format's functions are invoked, at the + * return site. `human` composes engine primitives (R5); `stdout` is the + * machine-consumable data lines — what --quiet leaves, what a pipe + * receives; `json` overrides the envelope's `result` (default: the + * data); `next` supplies the typed nextActions — agents branch on them; + * the human renderer formats them as prose (no stored string form). + */ +export interface Presentations { + readonly human: (ui: Ui) => readonly Block[] + readonly stdout?: () => readonly string[] + readonly json?: () => unknown + readonly next?: () => readonly NextAction[] +} + +// ———————————————————————————————————————————————————————————————————————— +// §3 Config sections — a PRODUCT-level fact (one product, one section) +// ———————————————————————————————————————————————————————————————————————— + +/** + * A product's named slice of prisma.config.ts, declared once in the + * product's manifest (§10). The token couples the section name, its + * validated type, and its total validator. Commands that need the + * section reference the token in `needs.config`, which is how the + * engine knows which commands an invalid section fails — and how + * ctx.config gets its type. + * + * The validator OWNS absence: its input is the raw section value, or + * undefined when the config file has no such section. A product that + * wants defaults applies them here; one that requires the section emits + * a section-required diagnostic here. It returns findings; it never + * throws (R10). Keep validators dependency-light: they load with the + * definition tree at startup (R9). + */ +export interface ConfigSection { + readonly name: string + readonly validate: (raw: unknown | undefined) => SectionValidation +} + +export type SectionValidation = + | { readonly ok: true; readonly value: T; readonly diagnostics: readonly Diagnostic[] } + | { readonly ok: false; readonly diagnostics: readonly Diagnostic[] } + +export declare function defineConfigSection(spec: { + readonly name: string + readonly validate: (raw: unknown | undefined) => SectionValidation +}): ConfigSection + +// ———————————————————————————————————————————————————————————————————————— +// §4 The handler context — R4: the whole world arrives as one argument +// ———————————————————————————————————————————————————————————————————————— + +export interface CommandContext { + /** The validated value of the command's needed config section — + * exactly TConfig; absence semantics belong to the product's + * validator (§3). Commands with no config need get undefined. */ + readonly config: TConfig + + /** Builds the PresentedResult for the active format: "present this + * outcome, via these presentations." The only constructor of + * PresentedResult. */ + readonly present: ( + outcome: Outcome, + presentations: Presentations, + ) => PresentedResult + + /** Management-API credentials, resolved at call time so long-lived + * sessions survive token refresh. Undefined when unauthenticated. + * Commands with needs.credentials never see undefined — the engine + * fails them early with the sign-in error. */ + readonly getCredentials: () => Promise + + /** The one way to emit while running (§1). */ + readonly report: (event: EngineEvent) => void + + /** Interactive input (§4a). */ + readonly prompt: PromptSurface + + /** Fires on Ctrl-C/SIGTERM (engine-owned; second signal force-exits). + * Session commands run until it fires; everything else aborts + * in-flight work with it. */ + readonly signal: AbortSignal + + /** Where the user invoked the CLI. Products never read process.cwd(). */ + readonly cwd: string + + /** + * R13, the conditional form (evidence: composer needs @prisma/dev only + * when the config declares postgres resources — unconditional needs + * belong in `needs.dependencies`). Resolves when the optional peer + * dependency is importable from the user's project; otherwise returns + * the ENGINE'S structured missing-dependency error — install command + * phrased by the engine with the user's package manager — for the + * handler to pass to notOk. Products never craft install prose. + */ + readonly requireDependency: (specifier: string) => Promise> +} + +export interface Credentials { + /** Opaque to the engine; shape owned by the Cloud product's auth + * library (placeholder pending its design). Workspace selection is + * session state, not a credential — it lives with that library, not + * here. */ + readonly token: string +} + +/** + * §4a Prompts. Every prompt except `consent` may carry a + * product-specified `default`. Interactively, Enter accepts the default. + * Under --yes, a prompt WITH a default resolves to it without + * displaying; a prompt WITHOUT a default cannot be operated and the + * invocation halts with a structured error (exit 2). In + * json/non-interactive/CI/non-TTY contexts the same default rule + * applies. User cancellation (Ctrl-C at the prompt) is a distinct + * structured error the engine maps to exit 3. + */ +export interface PromptSurface { + readonly confirm: ( + question: string, + opts?: { readonly default?: boolean }, + ) => Promise> + /** + * A question requiring EXPLICIT consent — never inferable, not + * necessarily destructive. Structurally undefaultable: no default + * parameter exists, so --yes, Enter-through, and non-interactive + * contexts can never satisfy it; the command's fix names the explicit + * flag that grants consent non-interactively. + */ + readonly consent: (question: string) => Promise> + readonly select: ( + question: string, + options: ReadonlyArray<{ value: T; label: string }>, + opts?: { readonly default?: T }, + ) => Promise> + readonly text: ( + question: string, + opts?: { readonly placeholder?: string; readonly default?: string }, + ) => Promise> +} + +// ———————————————————————————————————————————————————————————————————————— +// §5 Flags and positionals — R1: directly executable, typed by inference +// ———————————————————————————————————————————————————————————————————————— + +/** + * Product-declared flags. The shared family (§header) is engine-injected + * and reserved; handlers never see those values. Parse-time validation + * failures become structured errors carrying the allowed values. + * + * Deliberate asymmetry, matching CLI convention: flags are optional by + * default (requiredString is the exception); positionals are required by + * default (optionalString is the exception). + */ +export declare const flag: { + string(spec: { + brief: string + placeholder?: string + alias?: A & Char + default?: string + }): FlagSpec + requiredString(spec: { + brief: string + placeholder?: string + alias?: A & Char + }): FlagSpec + number(spec: { + brief: string + placeholder?: string + alias?: A & Char + default?: number + }): FlagSpec + boolean(spec: { brief: string; alias?: A & Char }): FlagSpec + enum(spec: { + brief: string + values: T + alias?: A & Char + default?: T[number] + }): FlagSpec + repeated(spec: { + brief: string + placeholder?: string + alias?: A & Char + }): FlagSpec +} + +/** Single-character alias, enforced at the type level: `Char<'q'>` is + * 'q'; `Char<'ab'>` is never. */ +export type Char = S extends `${string}${infer Rest}` + ? Rest extends '' + ? S + : never + : never + +declare const FLAG: unique symbol +export interface FlagSpec { + /** Phantom carrier for inference; exported so declaration emit works. */ + readonly [FLAG]: T +} +export { FLAG } + +export declare const positional: { + string(spec: { brief: string; placeholder: string }): PositionalSpec + optionalString(spec: { brief: string; placeholder: string }): PositionalSpec + /** Zero or more trailing values; at most one, declared last (order = + * declaration order; keys must not be integer-like). */ + variadic(spec: { brief: string; placeholder: string }): PositionalSpec +} +declare const POSITIONAL: unique symbol +export interface PositionalSpec { + readonly [POSITIONAL]: T +} +export { POSITIONAL } + +/** The parse SPI: a command's argument surface, one property. */ +export interface ArgsSpec< + TFlags extends Record>, + TPositionals extends Record>, +> { + readonly flags?: TFlags + readonly positionals?: TPositionals +} + +/** What a handler receives: separate namespaces, symmetric access — + * `args.flags.to`, `args.positionals.name`. */ +export interface Args< + TFlags extends Record>, + TPositionals extends Record>, +> { + readonly flags: { readonly [K in keyof TFlags]: TFlags[K] extends FlagSpec ? T : never } + readonly positionals: { + readonly [K in keyof TPositionals]: TPositionals[K] extends PositionalSpec ? T : never + } +} + +// ———————————————————————————————————————————————————————————————————————— +// §6 Command definitions — light at startup (R9), path-free (R12), +// runtime-discriminated by `kind`. Three modalities at equal rank: +// result / session / server. +// ———————————————————————————————————————————————————————————————————————— + +/** The help SPI: how the command shows itself in help output. Words + * only — the engine formats. */ +export interface HelpSpec { + /** One line, imperative, shown in listings. */ + readonly summary: string + readonly description?: string + /** Copy-pastable invocations, shown verbatim. */ + readonly examples?: readonly string[] +} + +/** + * The preconditions SPI: everything the engine gates BEFORE the handler + * loads. Each unmet need fails the command early with the engine's own + * structured error — consistent phrasing by construction, and a handler + * never runs in a world where it can't operate. + */ +export interface NeedsSpec { + /** The product's config section token: validate it, fail me on its + * error diagnostics, hand me the value as ctx.config. */ + readonly config?: ConfigSection + /** Fail early with the sign-in error when unauthenticated. */ + readonly credentials?: true + /** Optional peer dependencies this command cannot run without; the + * engine probes resolvability and phrases the install error. + * (Conditional needs use ctx.requireDependency instead.) */ + readonly dependencies?: readonly string[] + /** + * Fail early (before execution, before side effects) in + * json/non-interactive/CI/non-TTY contexts. This is a MECHANICAL + * precondition — "an interactive terminal is required" — and + * deliberately NOT an agent barrier: the client's nature is + * unverifiable, and a flag claiming to exclude agents would be a + * false guarantee. Anything requiring a verified human belongs + * server-side, where identity actually exists. + */ + readonly interaction?: true +} + +export interface CommandDefinition< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, + TCode extends number = never, +> { + readonly kind: 'result-command' + readonly help: HelpSpec + readonly args?: ArgsSpec + readonly needs?: NeedsSpec + + /** + * The command's documented exit codes (4–99): code → meaning. + * Rendered in help without executing anything; the keys type the + * outcome's exitCode, making it REQUIRED at every return site (`0` or + * a documented code). Absent = the command only exits 0/1/2/3 and the + * outcome carries no exitCode. + */ + readonly exitCodes?: Readonly> + + /** The heavy part, loaded only at execution (R9). The module's default + * export is the handler — annotate it with CommandHandler + * (type-only import of the light definition; no runtime cycle). */ + readonly handler: () => Promise<{ default: Handler }> +} + +export type Handler< + TFlags extends Record>, + TPositionals extends Record>, + TConfig, + TCode extends number = never, +> = ( + args: Args, + ctx: CommandContext, +) => Promise, CliStructuredError>> + +/** For impl files: `const run: CommandHandler = …` */ +export type CommandHandler = D extends CommandDefinition + ? Handler + : never + +export declare function defineCommand< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, + TCode extends number = never, +>( + def: Omit, 'kind'>, +): CommandDefinition + +/** + * A session command (dev, log tail): runs until the signal fires, + * speaks entirely through events, returns Result. No + * presentation, no exit-code set. A session always supports json mode: + * the event stream is its json surface. + */ +export interface SessionCommandDefinition< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, +> { + readonly kind: 'session-command' + readonly help: HelpSpec + readonly args?: ArgsSpec + readonly needs?: NeedsSpec + readonly handler: () => Promise<{ + default: ( + args: Args, + ctx: CommandContext, + ) => Promise> + }> +} + +export declare function defineSessionCommand< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, +>( + def: Omit, 'kind'>, +): SessionCommandDefinition + +/** + * A server command (lsp): a foreign client on the other end of stdio + * owns the conversation, so the engine hands over the streams. Events, + * presentation, formats, and prompts do not apply — by definition, not + * by opt-out; the handler returns the exit code directly. The shared + * flag family is NOT injected. + */ +export interface ServerCommandDefinition< + TFlags extends Record> = {}, + TConfig = undefined, +> { + readonly kind: 'server-command' + readonly help: HelpSpec + readonly args?: ArgsSpec + readonly needs?: NeedsSpec + readonly handler: () => Promise<{ + default: ( + args: Args, + io: { + readonly stdin: InputStream + readonly stdout: OutputStream + readonly stderr: OutputStream + readonly signal: AbortSignal + readonly cwd: string + readonly config: TConfig + }, + ) => Promise + }> +} + +export declare function defineServerCommand< + TFlags extends Record> = {}, + TConfig = undefined, +>( + def: Omit, 'kind'>, +): ServerCommandDefinition + +/** Erased union for manifests and mount maps; `kind` discriminates. */ +export type AnyCommand = + | CommandDefinition + | SessionCommandDefinition + | ServerCommandDefinition + +// ———————————————————————————————————————————————————————————————————————— +// §7 Presentation primitives — the R5 vocabulary +// ———————————————————————————————————————————————————————————————————————— + +/** Deliberately small; grows by the same evidence rule as events. */ +export type Block = + | { + readonly kind: 'summary' + readonly tone: 'ok' | 'error' | 'warn' | 'info' + readonly text: string + } + | { + readonly kind: 'fields' + readonly rows: ReadonlyArray<{ label: string; value: string; sensitive?: boolean }> + } + | { + readonly kind: 'table' + readonly columns: readonly string[] + readonly rows: ReadonlyArray + } + | { readonly kind: 'list'; readonly items: readonly string[] } + | { readonly kind: 'tree'; readonly roots: readonly TreeNode[] } +// NOTE: recorded findings are NOT a Block — they are the outcome's +// diagnostics; the engine renders them with the top-level error layout +// and carries them into the envelope, so the two surfaces cannot +// diverge. + +export interface TreeNode { + readonly label: string + readonly children?: readonly TreeNode[] +} + +/** Styling helpers usable inside block text; no direct writing. */ +export interface Ui { + readonly emphasize: (text: string) => string + readonly dim: (text: string) => string + readonly code: (text: string) => string +} + +// ———————————————————————————————————————————————————————————————————————— +// §8 Streams — minimal structural types; no NodeJS.* in the public +// surface +// ———————————————————————————————————————————————————————————————————————— + +export interface OutputStream { + write(text: string): void +} +/** Byte-oriented, so server commands can implement byte-counted + * protocols (lsp's Content-Length framing). setRawMode is present + * where the platform supports keypress input. */ +export interface InputStream extends AsyncIterable { + readonly setRawMode?: (enabled: boolean) => void +} + +// ———————————————————————————————————————————————————————————————————————— +// §9 Envelopes and the json stream +// ———————————————————————————————————————————————————————————————————————— + +export interface CompletedEnvelope { + /** ok = COMPLETED (the command executed to its end). A completed + * result may still carry findings and a non-zero exit code — bad news + * is a result, not an error. */ + readonly ok: true + /** The command's stable dotted identity — its full mount path + * ('project.env.add'). The schema-dispatch key for machine consumers; + * says nothing about arguments. */ + readonly commandId: string + readonly result: T + readonly exitCode: number + /** The recorded findings, verbatim from the presented outcome. */ + readonly diagnostics: readonly Diagnostic[] + readonly nextActions: readonly NextAction[] +} + +export interface ErroredEnvelope { + /** ok = false: the command did NOT complete. */ + readonly ok: false + readonly commandId: string + /** The PRIMARY error — what aborted the command. Severity 'error' by + * definition. A thrown CliStructuredError serializes to exactly this + * shape. */ + readonly error: Diagnostic + /** Accompanying findings when the abort had several (three config + * typos are three diagnostics, not one flattened error). */ + readonly diagnostics: readonly Diagnostic[] + /** Aggregated from remediation events. */ + readonly nextActions: readonly NextAction[] +} + +/** json mode emits one StreamEvent per line: the handler's events, + * flattened with the stream metadata, then exactly one terminal + * 'result' member carrying the envelope. */ +export type StreamEvent = + | (EngineEvent & StreamMeta) + | ({ readonly kind: 'result'; readonly envelope: CompletedEnvelope | ErroredEnvelope } & StreamMeta) + +export interface StreamMeta { + readonly commandId: string + /** ISO 8601 UTC. Injectable clock in tests (§11). */ + readonly timestamp: string +} + +// ———————————————————————————————————————————————————————————————————————— +// §10 Product manifests and shell mounting — R12: the shell owns the +// tree; a product owns its section +// ———————————————————————————————————————————————————————————————————————— + +/** + * What a product package exports: its config section (declared once — + * a product-level fact) and its commands by NAME. The unified config + * loader consumes the sections; the shell mounts the commands. A + * command whose needs.config token is not its product's section is a + * construction error. + */ +export interface ProductManifest { + readonly configSection?: ConfigSection + readonly commands: Readonly> + /** The product's documentation base URL. The engine derives each + * diagnostic's docs link from base + code; a diagnostic's own + * `docsUrl` field is the per-raise override (unused until a use case + * appears). */ + readonly docsBaseUrl?: string +} + +/** What the shell builds: commands by PATH (space-separated, + * 'db migrate'). */ +export type MountedTree = Readonly> + +/** + * Shell-side construction. Group help is declared with the mount, since + * groups belong to the tree, not to products. Collisions, unknown + * groups, reserved-flag violations, grammar violations, and + * foreign-section references fail construction (build time, not run + * time). + */ +export declare function createCli(spec: { + readonly name: string + readonly version: string + readonly products: readonly ProductManifest[] + readonly groups: Readonly> + readonly commands: MountedTree +}): Cli + +export interface Cli { + /** Parse, execute, render, return the exit code. Never calls + * process.exit; never touches streams other than the provided ones. */ + run(argv: readonly string[], runtime: Runtime): Promise +} + +/** Everything environmental, injected once by the bin (or by a test). */ +export interface Runtime { + readonly stdout: OutputStream + readonly stderr: OutputStream + readonly stdin: InputStream + readonly cwd: string + readonly env: Readonly> + readonly isTty: { readonly stdin: boolean; readonly stdout: boolean; readonly stderr: boolean } + readonly signal: AbortSignal + /** Loaded config + file-level diagnostics; the shell builds this via + * the unified loader (R10). Tests hand in fixtures. */ + readonly config: LoadedConfig + readonly getCredentials: () => Promise + /** Used by the ENGINE to phrase install commands (products never + * do — see needs.dependencies and ctx.requireDependency). */ + readonly packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' +} + +export interface LoadedConfig { + /** Raw section values by name; validation happens per command via its + * product's section token. */ + readonly sections: Readonly> + /** File-level problems (unevaluable module, missing version marker) — + * section: null fails every command. */ + readonly diagnostics: ReadonlyArray<{ + readonly section: string | null + readonly diagnostic: Diagnostic + }> +} + +// ———————————————————————————————————————————————————————————————————————— +// §11 The product-repo test harness — R7: same machinery, bytes out +// ———————————————————————————————————————————————————————————————————————— + +export declare function createTestCli(spec: { + readonly products?: readonly ProductManifest[] + readonly commands: MountedTree + readonly groups?: Readonly> + readonly config?: Readonly> + readonly credentials?: Credentials + readonly packageManager?: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' + /** Fixed clock for deterministic stream timestamps. */ + readonly now?: () => Date +}): TestCli + +export interface TestCli { + run( + argv: readonly string[], + opts?: { + readonly stdin?: string + /** Scripted prompt answers, consumed in order; a run that prompts + * past the script fails the test. */ + readonly answers?: ReadonlyArray + /** Abort the run (session tests): fires the context signal. */ + readonly abort?: AbortSignal + /** Live event tap, for asserting mid-session behavior. */ + readonly onEvent?: (event: EngineEvent) => void + readonly cwd?: string + readonly isTty?: { stdin?: boolean; stdout?: boolean; stderr?: boolean } + readonly env?: Readonly> + }, + ): Promise<{ + readonly exitCode: number + readonly stdout: string + readonly stderr: string + /** Parsed stream (events + the terminal result) when json mode. */ + readonly json: readonly StreamEvent[] + /** Every EngineEvent the handler emitted, for semantic assertions. */ + readonly events: readonly EngineEvent[] + /** The PresentedResult the handler returned, for semantic + * assertions without byte-scraping. */ + readonly presented?: PresentedResult + }> +} diff --git a/.drive/projects/prisma-cli-v8/assets/engine/output-modes-survey.md b/.drive/projects/prisma-cli-v8/assets/engine/output-modes-survey.md new file mode 100644 index 00000000..601eaa88 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/output-modes-survey.md @@ -0,0 +1,652 @@ +# Execution modes and event dialects across the three CLI families + +Snapshot: 2026-08-09. Evidence survey feeding the unified CLI engine's event +vocabulary (cli-engine-requirements.md R5, R6, R14) and its command-execution +modes. Style follows `docs/architecture docs/research/commander-friction-points.md`: +no claim without a citation. Path prefixes: + +- **ORM** = `packages/1-framework/3-tooling/cli/src` in prisma/prisma (this repo) +- **Composer** = `wip/repos/composer/packages/0-framework/3-tooling/cli/src` +- **Platform** = `wip/repos/prisma-cli/packages/cli/src` (plus `packages/compute`) +- **Emulators** = `wip/repos/composer/packages/1-prisma-cloud/0-lowering/dev-emulators/src` + +Sub-survey evidence was gathered per family and the central claims were +re-verified against the code (the span-event union, the `migrate` progress +gap, JSON auto-selection, the detached daemon spawn, the domain-wait loop). + +## A. Command inventory with execution mode + +Mode key (refined from the four-mode starting taxonomy; the final proposal is +in §F): + +- **S** — synchronous request/response: one operation call, one rendered result. +- **S+prog** — synchronous with in-flight progress reporting (spans, step + lines, or events) that ends when the operation ends. +- **P** — poll-until-terminal: a wait loop against a remote state machine, + with timeout semantics. +- **W** — long-lived session/watch: runs until signal/abort, re-emits over + its lifetime. +- **D** — daemon-coupled: the command manages or implicitly ensures a + machine-scoped process that outlives the CLI invocation. +- **X** — interactive wizard: prompts drive the flow. +- **V** — stdio protocol server: the process becomes a protocol endpoint. +- **B** — browser hand-off and/or local callback web server (a capability + layered on another mode, not a mode of its own — see §F). + +### A1. Prisma ORM (`prisma-next`) — commander main CLI + clipanion migration-file CLI + +Framework: commander for the main binary (cli.ts:71-73; commands registered +cli.ts:323-331), clipanion for the separate migration-file CLI, chosen for +in-process testability (migration-cli.ts:39-42). Global flags via +`addGlobalOptions`: `--format `, `--json`, `-q/--quiet`, +`-v/--verbose`, `--trace`, `--color/--no-color`, +`--interactive/--no-interactive`, `-y/--yes` (utils/command-helpers.ts:368-388). +Output discipline: stdout = data, stderr = decoration +(utils/terminal-ui.ts:9-23, 284-301). Uniform result funnel `handleResult` +with exit 2 for structured failure, 3 for user abort +(utils/result-handler.ts:16-44). Every command forks a detached telemetry +child at `preAction` (cli.ts:82-88; utils/telemetry.ts:160-177). + +| Command | Mode | Output pattern | `--json` | Citation | +|---|---|---|---|---| +| `init` | **X** + child processes | clack intro/outro, per-file logs, spinners for install/skills/emit, manual-steps note box | yes — arktype-validated `{ok, target, authoring, schemaPath, filesWritten[], filesDeleted[], packagesInstalled, contractEmitted, nextSteps[], warnings[]}` | commands/init/index.ts:45-113; init/output.ts:19-52; init/init.ts:113-143, 585-594 | +| `migrate` | S (DB session; **no progress adapter** — see B3 gap) | styled header; one `ui.step('Loading contract spaces…')`; final rendered block | yes — `{ok, migrationsApplied, migrationsTotal, markerHash, applied[], summary, perSpace[], pathDecision?, timings{total}, advancedRef}` | commands/migrate.ts:709-731, 777-779, 844-855, 928-938 | +| `migrate --show` | S (read-only preview) | full ASCII graph visualization with cross-space column alignment; skipped entirely under JSON | yes | commands/migrate.ts:159, 435-470, 910-925 | +| `format` | S | header; `ui.success`/`ui.info` line | yes — `{formatted, path?}` (compact) | commands/format.ts:20-74 | +| `lsp` | **V** — long-lived stdio LSP server | none (protocol on stdio); lazy-imports `@internal/language-server` | n/a (`--stdio` accepted, documented as the only transport) | commands/lsp.ts:8-31; language-server/src/start-server.ts:1-7; server.ts:628-629 | +| `contract emit` | **S+prog** (spans `resolveSource`, `emit`) | header; spinner per span; `ui.warn` | yes — `{ok, storageHash, executionHash?, profileHash?, outDir, files{json,dts}, timings{total}}` | commands/contract-emit.ts:105-130, 180-192; utils/formatters/emit.ts:55-66 | +| `contract infer` | **S+prog** + writes file | header; spans; `✔ Contract written to ` | yes — `{ok, summary, target, psl{path}, meta, timings}` | commands/contract-infer.ts:27-38, 70-93, 114-127 | +| `db verify` (full / `--marker-only` / `--schema-only`) | **S+prog** (spans `connect`, `verify`, `introspect`) | header; spinner spans; result block; drift rendered even under `--quiet`, exit **1** on drift | yes — mode-discriminated verify shape | commands/db-verify.ts:214-215, 366, 389-450, 458-499, 544-583; utils/formatters/verify.ts:38-74, 140-158 | +| `db init` | **S+prog** (spans `connect`, `introspect`, `plan`, `apply` + nested per-operation spans) | header incl. `mode: dry run`; spinner per top-level span; plan tree or apply summary | yes — `MigrationCommandResult` (see B3) | commands/db-init.ts:147-152, 194-231; utils/migration-command-scaffold.ts:79-89, 161; utils/formatters/migrations.ts:51-95 | +| `db update` | **S+prog** + interactive re-run loop: on destructive-op rejection, prompts and **re-executes the whole command** with `yes:true` | as `db init` | yes — same shape | commands/db-update.ts:170-176, 320-343, 345-357 | +| `db schema` | **S+prog** (read-only) | header; introspection tree | yes — `IntrospectSchemaResult` | commands/db-schema.ts:18-29, 48-74 | +| `db sign` | **S+prog** (spans `schemaVerify`, `sign`) | header; from→to hashes; exit **1** on verify failure | yes | commands/db-sign.ts:205-229, 293-325 | +| `migration plan` | S, offline, writes migration packages + snapshots (with cross-space seed side effects) | header; `ui.step` per seeded space; final tree/summary | yes | commands/migration-plan.ts:235-242, 382-400, 726-767 | +| `migration new` | S, offline, scaffolds files | header; success block naming dir/from/to | yes — `{ok, dir, from, to, summary}` | commands/migration-new.ts:236-302 | +| `migration show` | S, offline | header; operations + SQL preview | yes | commands/migration-show.ts:102-115, 213-258; commands/json/schemas.ts:156-177 | +| `migration status` | S; DB-connected by default, offline with `--from` | header; tree sections | yes — includes per-migration `status: 'applied'\|'pending'\|null` and `diagnostics[]` with `hints[]` | commands/migration-status.ts:383-384, 639-712; json/schemas.ts:70-121 | +| `migration log` | S, DB required | header; table render | yes | commands/migration-log.ts:67-161; json/schemas.ts:123-141 | +| `migration list` | S, offline | header + optional legend; tree | yes | commands/migration-list.ts:209-253, 304-328 | +| `migration graph` | S, offline; three output modes — `--dot` (Graphviz to stdout) **takes precedence over `--json`** | header; tree; DOT | yes | commands/migration-graph.ts:95-96, 240-270 | +| `migration check` | S, offline; own exit-code scheme (0/2/**4** = integrity failed, via `exitOverride`) | `✔ summary` or per-failure `✗ [CODE] where: why` + `fix:` | yes — `{ok, failures[{space,code,where,why,fix}], summary}` | commands/migration-check.ts:377-378, 604-698; migration-check/exit-codes.ts:1-3 | +| `ref set` / `delete` / `list` | S, offline | one line each | yes (compact) | commands/ref.ts:178-269 | +| `telemetry status` / `enable` / `disable` | S | 1–3 lines | yes (compact) | commands/telemetry/index.ts:18-83 | +| migration-file CLI (`node migration.ts`) | S; separate clipanion CLI | `--dry-run` prints framed `--- migration.json ---` / `--- ops.json ---` blobs; else writes files + one line | **no `--json`** (flags: `--help`, `--dry-run`, `--config`) | migration-cli.ts:104, 113-149, 508-529; exit codes 0/1/2 :181-186 | + +### A2. Composer (`prisma-composer`) — clipanion, 4 commands + +| Command | Mode | Output pattern | `--json` | Citation | +|---|---|---|---|---| +| `deploy ` | S + child passthrough | alchemy child inherits stdio; topology tree rendered by the report hook inside the child; failure → envelope or child-status hints | no | main.ts:258-274; run-alchemy.ts:46-53 (`stdio: 'inherit'`); render-deployment.ts:77-127 | +| `destroy ` | S with one pre-event + child passthrough | `DestroyEvent` 'no-local-deploy-state' → console.warn | no | main.ts:296-313; operations/destroy.ts:18-20 | +| `dev ` | **W** (event session) + **D** (implicitly ensures machine-scoped emulator daemons) | `DevEvent` union rendered line-by-line; session object `stop()`/`closed`; CLI owns SIGINT/SIGTERM | no | operations/dev.ts:14-52; dev/run-dev.ts:43-133; emulators below | +| `log [address]` | **W** (stream) | `AsyncIterable` printed `[service] line`; side-channel `LogEvent` union; AbortSignal ends it | no | operations/log.ts:15-58; log/run-log.ts:26-74 | + +Composer has **no** `--json` anywhere (main.ts:18-110 declares only +`--name/--stage/--production/--fresh/--tail`); the machine surface is the +programmatic operations API (`@prisma/composer/control`, +exports/control.ts:16-32) instead. + +### A3. Platform (`prisma-cli`) — commander v14, ~60 commands + +Framework: Commander v14 (`cli/package.json:47`; cli.ts:3), wrapped so all +Commander output goes to stderr with `exitOverride()` +(shell/runtime.ts:34-55). Twelve top-level `addCommand` calls +(cli.ts:107-118); a descriptor table of 72 command ids +(shell/command-meta.ts:34-700). Global flags: `--json`, `-q/--quiet`, +`-v/--verbose`, `--trace`, `-y/--yes`, `--interactive`/`--no-interactive`, +`--color`/`--no-color` (shell/global-flags.ts:23-45). + +The overwhelming default is **S** through one choke point +(shell/command-runner.ts:70-147) with a uniform `--json` envelope. +"presenter" below means that default. + +| Command | Mode | Output pattern | `--json` | Citation | +|---|---|---|---|---| +| `version` / `--version` | S (local) | presenter | yes | commands/version/index.ts:11-32; cli.ts:123-160 | +| `feedback ` | S | presenter | yes | commands/feedback/index.ts:11-43 | +| `init` | **X** | prompts + presenter | yes (prompts suppressed) | commands/init/index.ts:11-93; controllers/init.ts prompt sites :331, :762, :914, :925, :950, :1020 | +| `agent install/update/status` | S (local, shells out) | presenter | yes | commands/agent/index.ts:39-123 | +| `auth login` | S + **B** (local OAuth callback server + browser + paste race) | login progress direct to stderr; presenter at end | yes | commands/auth/index.ts:52-79; lib/auth/login.ts:39-155 (ephemeral-port server :45-53; `Promise.race` callback-vs-paste :136-146; HTML success page :373-462) | +| `auth logout` / `whoami` | S | presenter | yes | commands/auth/index.ts:81-150 | +| `auth workspace list/use/logout` | S (`use` prompts when arg omitted) | presenter | yes | commands/auth/index.ts:167-250; prompt controllers/auth.ts:585 | +| `project list/show/create/rename` | S | presenter | yes | commands/project/index.ts:64-92, 184-211, 243-291 | +| `project link` | S + prompt | presenter | yes | commands/project/index.ts:213-241; lib/project/interactive-setup.ts:44, 87 | +| `project remove/transfer` | S, typed `--confirm ` | presenter | yes | commands/project/index.ts:94-182 | +| `project env add/update/list/remove` | S | presenter | yes | commands/env.ts:45-240 | +| `git connect` | **P** + **B** (GitHub App install wait) | presenter | yes | commands/git/index.ts:31-59; poll controllers/project.ts:1780-1829; `open()` :2075 | +| `git disconnect` | S | presenter | yes | commands/git/index.ts:61-88 | +| `branch list` (the only branch cmd) | S | presenter | yes | commands/branch/index.ts:27-50 | +| `build logs ` | **W** with `--follow`, else bounded stream | NDJSON records split stdout/stderr by `source`/`level` | yes — per-record events, no envelope | commands/build/index.ts:24-58; controllers/build.ts:34-150 | +| `database list/show/create/usage/restore/remove` | S (`create` and `restore` are **single POSTs** — §A4) | presenter | yes | commands/database/index.ts:92-375; lib/database/provider.ts:309-333, 472-489 | +| `database backup list`, `connection list/create/rotate/remove` | S | presenter | yes | commands/database/index.ts:297-539 | +| `bucket list/create/delete`, `bucket key list/create/delete` | S | presenter; `bucket key create` is the one 3-way presenter (secret → stdout) | yes | commands/bucket/index.ts:64-269 | +| `app build` | S (local build) | presenter | yes | commands/app/index.ts:108-148 | +| `app run` | **W** — hosts framework dev server for the session | passthrough | **no — hard error** | commands/app/index.ts:150-197; rejection controllers/app.ts:273-281 | +| `app deploy` | **S+prog** with SDK-internal **P** | discrete step lines to stderr, off when `--json`/`--quiet` | yes; two result shapes (single vs all) | commands/app/index.ts:199-320; progress controllers/app.ts:801-812; SDK poll lib/app/app-provider.ts:507-527 | +| `app show` / `list-deploys` / `show-deploy` | S | presenter | yes | commands/app/index.ts:326-359, 675-735 | +| `app open` | S + **B** | presenter | yes | commands/app/index.ts:361-394; controllers/app.ts:1268-1272 | +| `app domain add/show/remove/retry` | S | presenter | yes | commands/app/index.ts:420-590 | +| `app domain wait ` | **P** — the one user-facing wait verb | status-transition lines with elapsed `mm:ss`; `--timeout` default 15m | yes — one NDJSON `{type:"status",...}` event per transition | commands/app/index.ts:592-633; loop controllers/app.ts:1506-1563, 2651-2687 | +| `app logs` | **W** (stream) | header + per-record write | yes | commands/app/index.ts:635-666; controllers/app.ts:1566-1633 | +| `app promote/rollback/remove` | **S+prog** with SDK **P** (120s budget) | progress lines | yes | commands/app/index.ts:737-862; lib/app/app-provider.ts:344-360, 471-479 | + +The sibling `compute` package has **zero commands** — it is a runtime +library (`KeepAwakeGuard`, `waitUntil`; `compute/src/index.ts:1-6`, no `bin` +in `compute/package.json:8-13`). + +### A4. The management-API async answer + +**The management API is overwhelmingly synchronous CRUD; asynchronous +poll-until-terminal behavior exists in exactly three places, all deliberate.** + +Async, loop in the CLI: + +1. `app domain wait` — `while (true)` at controllers/app.ts:1506, terminal on + `active`/`failed`, deadline throws `DOMAIN_VERIFICATION_TIMEOUT` + (:1516-1552), abort-aware sleep clamped to remaining budget (:1554-1557), + `--timeout 0` = check once (:1542). Real provisioning state machine: + `pending_dns | verifying | provisioning_tls | verified_routing_blocked | + active | failed` (types/app.ts:175). +2. `git connect` — `waitForInstalledRepository` + (controllers/project.ts:1780-1829) polls SCM installations until the human + finishes the GitHub App install; interval/timeout env-overridable + (:1789-1796); polls only when `canPrompt(context)` (:1708-1717) — + non-interactive callers error immediately instead. + +Async, loop delegated to `@prisma/compute-sdk` but configured by the CLI +(`timeoutSeconds: 120, pollIntervalMs: 2000`): `deploy` +(lib/app/app-provider.ts:507-527), `promote` (:471-479), `destroyApp` +(:353-358), `updateEnv`-then-promote (:563-584). Deploy progress callbacks +expose the state machine (`onStatusChange`, lib/app/deploy-progress.ts:116-118; +steps build → archive → upload → start → running → promoted, :57-89). + +Explicitly **not** async (negative findings): + +- `database create` is a single POST, no wait-for-ready + (lib/database/provider.ts:309-333); same for `database restore` (:472-489) + and branch creation (lib/app/app-provider.ts:869-895). +- No `"provisioning"`/`"ready"` state on database/branch paths; the one "wait + for the database to become ready" string is advice text inside an error + message (lib/database/provider.ts:788), not a loop. +- Most `while (true)` occurrences are cursor pagination + (lib/app/app-provider.ts:909-930; lib/database/provider.ts:244-270; + lib/bucket/provider.ts:92, 170). No `setInterval` in the package. + +### A5. Emulators and daemons (mode D evidence) + +**`@prisma/dev` (PPG-local postgres) is npm-published only** — no source in +any local clone (`wip/repos/` holds composer, create-prisma, ignite, +pdp-control-plane, prisma-cli, project-compute; no dev repo). Surveyed from +the published tarballs (0.13.0 / 0.24.14 / 0.25.1; pinned 0.25.1 in +`pnpm-workspace.yaml`). Its surface: + +- **No `bin`** in any surveyed version — a library, not a CLI. (The line + `pnpm dlx @prisma/dev start` in `examples/react-router-demo/.env.example:2` + cannot work; stale doc.) +- Programmatic API: `startPrismaDevServer(options?): Promise` + (`dist/index.d.ts:79`) returning `{database, shadowDatabase, ppg.url, + http.url, name, close()}` (:46-55). Default ports 51213–51216 (:57-60). +- **Machine-scoped management primitives** (`dist/state-CNKFAMiX.d.ts`): + `ServerState.scan()` (:206 — the "ps"), `getServerStatus` (:239), + `isServerRunning` (:240), `killServer` (:241 — SIGTERM, poll, SIGKILL), + `deleteServer` (:238), status enum `"running" | "starting_up" | + "not_running" | "no_such_server" | "unknown" | "error"` (:236), + `persistenceMode: "stateless" | "stateful"` (:178). +- On-disk state under `env-paths("prisma-dev")`: per-server dir with + `server.json` dump (pid, ports, exports), `.pglite/`, and a + `proper-lockfile` `.lock` whose held/free state plus an HTTP + `GET /health` name-match probe *is* the liveness check (decompiled + `dist/chunk-HFONW2ZS.js`). TCP loopback only, no unix socket. +- `dist/daemon.js` is a script the **consumer** `fork()`s: name in + `process.argv[2]`, reports `{type:"started"|"error"}` over Node IPC + (`dist/daemon.d.ts:14-22`), SIGTERM/SIGINT handlers close and exit. + `@prisma/dev` never detaches itself — machine- vs session-scope is the + caller's choice. +- The 0.25.1 README documents both scopes: the Vite plugin is in-process + ("There is no background daemon", README:184), while `prisma dev` servers + are machine-scoped with an ORM-CLI management surface — "invisible to + `prisma dev ls`, `stop`, and `rm`" (README:126) and "leaves it running + when Vite exits" (README:131). +- Consumers today: prisma-next test utils wrap start/close session-scoped + (`test/utils/src/exports/index.ts:30-58`); the legacy bundled prisma 6 CLI + (`packages/cli/build/index.js:4090`) uses `@prisma/dev` + + `internal/state` for `prisma init`/`prisma dev` (foreground; it never + imports `internal/daemon`); the platform CLI references it **zero** times. + +**Composer's dev emulators are the most rigorous machine-scoped daemon design +in the survey** (`@internal/dev-emulators`): + +- Every daemon is "a detached, `unref()`'d child process that outlives + whatever called `ensureDaemon`" (daemon.ts:2-6); spawn at + daemon.ts:273-291 (`detached: true`, stdio to a log file, `child.unref()`). +- Machine registry at `~/.prisma-composer/emulators/`: `.json` entry + {pid, port, version, logPath} (daemon.ts:26-31, 105-120), state dir, log + file, `proper-lockfile` lock (:327-363). +- Readiness = HTTP health poll (200 ms up to 10 s) that **requires the + health payload's version to match the caller's** so a foreign process on + the port is never adopted (daemon.ts:159-218). +- `ensureDaemon` is an idempotent adopt-or-start-or-replace lifecycle: + classify `healthy | stale-version | dead-or-unhealthy | absent` under the + lock; stale version ⇒ kill and replace; persisted port never moves + (daemon.ts:305-317, 381-478). +- Three daemons — `compute` (supervises `bun bootstrap.js` children with + crash backoff, compute-main.ts:22-28, 422, 772-832), `buckets` + (fs-backed S3, buckets-main.ts:336-415), `postgres` (hosts + `@prisma/dev` `startPrismaDevServer()` in-process, one stateful named + server per Database resource, postgres-main.ts:650-662). Admin surface is + loopback JSON APIs (client.ts:178-199, 285-293, 376-384). +- `prisma-composer dev` **implicitly ensures** them (local-target/ + emulators.ts:42-54) and its `stop()` deliberately leaves them running — + "emulators and data stay up" (operations/dev.ts:47-49; run-dev.ts:83). + `--fresh` deletes per-app records only (local-target/teardown.ts:22-37). +- **There is no user-facing stop/status/ls command**: `stopDaemon` is + documented as "Not called by any v1 command — an operator escape hatch, + exported for tests" (daemon.ts:480-489). The strongest daemon + implementation has the weakest management surface. + +Other daemon-adjacent findings: project-compute has nothing daemon-shaped +(grep for daemon/emulator/detached/unref across cli+sdk: zero hits; its only +local server is the ephemeral OAuth callback, +project-compute/cli/src/lib/auth/login.ts:34-35). The platform CLI's one +detached process is the seconds-long update-check worker that re-execs the +CLI with `detached: true` + `unref()` and an env-var worker branch in +bin.ts (shell/update-check.ts:225-237; bin.ts:7-9). The ORM CLI's telemetry +child is the same fire-and-forget shape (utils/telemetry.ts:160-177). + +## B. Event and progress dialects + +Every distinct mechanism by which command code reports progress or +intermediate state. + +### B1. Composer: per-operation typed event unions + `onEvent` callback + +Each operation input carries `onEvent?: (event: XEvent) => void`, one +discriminated union per operation, on `kind`: + +- `DevEvent` — `ready {endpoints}`, `unwatchable {address}`, + `rebuild-failed {message}`, `watch-error {message}`, + `converge-failed {stackFilePath, reproduceCommand, cwd}`, `stopping`, + `stop-error {message}`, `stopped` (operations/dev.ts:14-31). +- `DestroyEvent` — `no-local-deploy-state {cwd}` (operations/destroy.ts:18-20). +- `LogEvent` — `stream-failed {message}`, `lines-dropped {count}` + (operations/log.ts:20-25). +- `deploy` — no events; resolves to `DeploySuccess {summary?}` + (operations/deploy.ts:25-35). + +Properties: rendering lives entirely in the CLI adapter (a `switch` over +`event.kind`, run-dev.ts:54-90; run-log.ts:40-48); a throwing host `onEvent` +must not kill the session (execute-dev.ts:190-196); lifetime is a session +object (`DevSession.stop()` / `closed`, operations/dev.ts:44-52) or an +`AsyncIterable` ended by the caller's `AbortSignal` (operations/log.ts:36-58). +Failures ride the shared `Result`/`CliStructuredError` shape +(operations/shared.ts:81-126); cli.ts:17-35 maps structured → exit 2, +escape → exit 1 + report hint. + +### B2. Composer: child-process passthrough + cross-process result file + +`deploy`/`destroy` spawn an `alchemy` child with `stdio: 'inherit'` +(run-alchemy.ts:46-53) — the child's own output *is* the progress display. +The structured result crosses back via a JSON file named in +`PRISMA_COMPOSER_DEPLOYMENT_RESULT_FILE` (deployment-summary.ts:18), written +best-effort by a report hook inside the child (:45-53) and re-validated +field-by-field by the parent (:63-101). On failure the error carries +`meta.diagnostics` (`ExecutionDiagnostics {exitCode, stackFilePath, +reproduceCommand, cwd}`, operations/shared.ts:44-75); the CLI prints two +reproduce-hint lines and passes the child's exit status through +(render-error.ts:27-37). + +### B3. ORM: progress spans (`ControlProgressEvent`) + +The exact shape (ORM control-api/types.ts:91-111, verified): + +```ts +export type ControlProgressEvent = + | { readonly action: ControlActionName; readonly kind: 'spanStart'; + readonly spanId: string; readonly parentSpanId?: string; readonly label: string } + | { readonly action: ControlActionName; readonly kind: 'spanEnd'; + readonly spanId: string; readonly outcome: 'ok' | 'skipped' | 'error' }; +``` + +`ControlActionName` = `'dbInit' | 'dbUpdate' | 'dbVerify' | 'migrate' | +'verify' | 'schemaVerify' | 'sign' | 'introspect' | 'emit'` (types.ts:67-76). +Design notes in-source (types.ts:78-90): only two event kinds; all +operation-specific progress is modeled as **nested spans** via `parentSpanId` +(per-migration-operation spans are `operation:` children, +control-api/operations/migration-helpers.ts:22-48); zero overhead when the +callback is absent. + +Renderer: `createProgressAdapter` (utils/progress-adapter.ts:32-74) — no-op +under `--quiet`, `--json`, or non-interactive (:36-38); top-level span → +clack spinner (delay-gated 100 ms, terminal-ui.ts:172-224), nested span → +`ui.step` line; `spanEnd` closes with elapsed-ms suffix, `(skipped)`, or +`(failed)` (:60-72). + +Wired by: `contract emit` (contract-emit.ts:121), `db schema` + +`contract infer` (inspect-live-schema.ts:136), `db sign` (db-sign.ts:205), +`db verify` both paths (db-verify.ts:366, 472), `db init` + `db update` +(migration-command-scaffold.ts:161). Span ids in use: `connect`, `verify`, +`schemaVerify`, `sign`, `introspect`, `resolveSource`, `emit`, `plan`, +`apply` (control-api/client.ts:215-222, 245-273, 298-327, 352-377, 548-567, +618-753; operations/db-run.ts:59-62; run-migration.ts:137-173). + +**Gap:** `migrate` — the longest-running, most destructive command — never +creates a progress adapter. `client.migrate()` threads `onProgress` +(client.ts:499-532) and `runMigration` emits `apply` + nested spans, but +commands/migrate.ts calls `client.migrate({...})` with no `onProgress` +(migrate.ts:808-814, verified); its only in-flight feedback is one `ui.step` +(migrate.ts:777-779). + +### B4. Platform: presenter objects + one success choke point + +End-state presentation, not streaming: controllers return a result; +`writeCommandSuccess` (shell/command-runner.ts:105-147) picks the channel. +Presenter interface (command-runner.ts:25-37): `renderStdout?` +(machine-usable payload → stdout), `renderHuman` (prose → stderr), +`renderJson?` (envelope override). Stream discipline is strict — human to +stderr, data to stdout (shell/output.ts:185-191; command-runner.ts:164-171) +— which is what makes `--quiet` pipe-clean (command-runner.ts:124-127). +Warnings render in human mode too so degraded steps are never silent +(command-runner.ts:130-135). Reusable card patterns (list/show/mutate) pair +a human renderer with a serializer side by side (output/patterns.ts:44-107), +with secret masking built into the UI layer (ui.ts:10; patterns.ts:14, 41). + +### B5. Platform: discrete step lines for long operations — no spinners + +Repo-wide grep for spinner/ora: zero hits outside `@clack/prompts`. Long +operations print append-only step lines: `createDeployProgress` +(lib/app/deploy-progress.ts:36-91; "Building locally...", "Uploading...", +"Deploying..." + status rows via `onStatusChange` :116-118), +`createPromoteProgress` (:97-137), disabled wholesale by +`enabled = !json && !quiet` (controllers/app.ts:801-811). `app domain wait` +prints only on status transitions with elapsed `mm:ss` +(controllers/app.ts:2670-2687). Everything is line-oriented, identical piped +or interactive apart from color and headers. (Contrast: the ORM uses clack +spinners for top-level spans, B3 — the two families made opposite calls.) + +### B6. Platform: NDJSON event streams + +Where the platform CLI streams, it emits single-line JSON events via +`writeJsonEvent` (shell/output.ts:31-36), distinct from the pretty-printed +success envelope: build-log records (controllers/build.ts:88-93), domain-wait +status transitions (`{type:"status", command, timestamp, data}`, +controllers/app.ts:2651-2662), wrapper success/error events +(command-runner.ts:213-235). `build logs` sets `emitJsonSuccessEvent: false` +because the stream carries its own `terminal` record +(commands/build/index.ts:52-55; command-runner.ts:208-222). + +### B7. Direct console writes at the adapter layer + +In all three families the final human rendering is direct +`console.log`/stderr writes concentrated at one adapter layer per command: +Composer's run-dev.ts:54-90 and run-log.ts:40-48; the platform's login +progress (lib/auth/login.ts) before the presenter runs; the ORM's `ui.*` +methods over stderr (terminal-ui.ts:284-301). The structure lives one layer +down (events, results, presenters); the writes are the rendering. + +### B8. Daemon readiness: HTTP health polling, not events + +The emulator daemons report readiness by health endpoint, not by event or +IPC: `awaitHealthy` polls `GET /health` and requires a version match +(daemon.ts:159-218). `@prisma/dev`'s forkable daemon script is the one IPC +user (`process.send({type:"started"|"error"})`, `dist/daemon.d.ts:14-22`). +Transient loopback failures after heavy converges are absorbed by a retry +helper (5 × 500 ms, Composer operations/emulator-retry.ts:9-24). + +## C. Recurring structures — R14 promotion candidates + +Ranked by breadth of occurrence (families out of 3, then by site count). + +1. **Structured error with code / summary / why / fix / where / docsUrl — + 3/3 families, uniform.** ORM: `ok:false, code, severity, summary, why?, + fix?, where?{path,line}, meta?, docsUrl?` + (packages/1-framework/1-core/errors/src/control.ts:9-19; rendered + utils/formatters/errors.ts:32-122). Composer: `CliErrorEnvelope` with + summary/code/why/fix/where rendered `✖ summary (CODE)` + indented lines + (render-error.ts:9-18). Platform: `{code, domain, severity, summary, why, + fix, where, meta, docsUrl}` (shell/output.ts:38-50). Already settled by + ADR 239/245 + Composer ADR-0043/0044 per R6; the survey confirms it is + the single most uniform structure in the corpus. + +2. **Remediation / next-step, in five competing encodings — 3/3 families.** + (a) the error `fix` field everywhere (above); (b) platform envelope-level + `nextSteps` + `nextActions` on **every** success (shell/output.ts:22-29), + including a pre-filled `feedback` recover action on crashes + (shell/output.ts:104-114); (c) ORM structured `hints[]` only on + `migration status` diagnostics (migration-status.ts:316-321, 525-534; + json/schemas.ts:78-103) and a first-class `nextSteps[]` only in `init` + JSON (init/output.ts:38, 117-150); (d) ORM free-text "Next:" prose lines + (formatters/migrations.ts:326-327, 458-464; migration-plan.ts:827, + 908-910; migration-new.ts:295-297); (e) Composer's **reproduce command**: + `reproduceCommand` + `stackFilePath` + `cwd` in both the + `converge-failed` event (operations/dev.ts:22-27) and failure + `meta.diagnostics` (operations/shared.ts:44-75; render-error.ts:27-37). + This is the clearest case of one engine concept currently spelled five + ways. + +3. **Per-item outcome lists — 3/3 families.** ORM: per-space blocks + `{spaceId, kind, operations[], marker?}` (control-api/types.ts:330-352; + renderer formatters/migrations.ts:119-154), `applied[]` + (migrations.ts:276-283), per-migration `status:'applied'|'pending'|null` + (json/schemas.ts:70-76), `failures[{space,code,where,why,fix}]` + (json/schemas.ts:179-187), truncated-to-3 conflict lists with a + "re-run with -v" footer (formatters/errors.ts:54-98). Platform: the + shared list/show card patterns with paired serializers + (output/patterns.ts:57-107). Composer: `DeploymentSummary.nodes[]` each + `{address, entities[{kind, id, url?, details?}]}` + (deployment-summary.ts:22-30) rendered as a topology tree + (render-deployment.ts:77-116). + +4. **Warnings list — 3/3.** ORM `warnings[]` in results + (formatters/migrations.ts:97-107; init/output.ts:66-68; + formatters/verify.ts:104-118); platform envelope `warnings` rendered even + in human mode (shell/output.ts:22-29; command-runner.ts:130-135); + Composer's warning-severity events (`watch-error`, `stop-error`, + `stream-failed`, `lines-dropped`; operations/dev.ts:19-30, + operations/log.ts:20-25). + +5. **Endpoints / URLs — 3/3, three senses.** Service endpoints: Composer + `ServiceEndpoint {address, url}` (operations/shared.ts:19-22) in `ready` + events and `log` attachments; entity `url` in deploy summaries + (render-deployment.ts:104-106); platform `liveUrl` (`app open`, + controllers/app.ts:1268-1272) and emulator base URLs + (`http://127.0.0.1:`, dev-emulators client.ts:58). Docs URLs: ORM + headers carry `https://pris.ly/...` "Read more" links (styled.ts:55-66; + e.g. db-init.ts:129) and envelope `docsUrl` under `-v` (errors.ts:99-101); + platform envelope `docsUrl` (output.ts:38-50). Masked connection URLs: + ORM `maskConnectionUrl`/`sanitizeErrorMessage` + (command-helpers.ts:308-359); platform `URL_CREDENTIALS_PATTERN` + + `maskValue` (ui.ts:10; patterns.ts:14, 41). + +6. **Counts + one-line summary — 3/3.** ORM "Planned/Applied N operation(s) + across M contract space(s)" (migrations.ts:174-182, 433-446), "N + migration(s) applied" (migration-log.ts:142), `operationCount` fields + (json/schemas.ts:8, 130); platform `summary` strings throughout the + presenters and `renderSummaryLine` glyphs ✔/✘/⚠/ℹ (ui.ts:129-143); + Composer `lines-dropped {count}` (operations/log.ts:24-25). + +7. **File paths / artifacts written — 3/3.** ORM `files{json,dts}` + `outDir` + (emit.ts:12-19), `filesWritten[]`/`filesDeleted[]` (init/output.ts:23-31), + `dir`/`baselineDir` (migration-new.ts:238; migration-plan.ts:884-897), + relativized to cwd nearly everywhere (emit.ts:33-34; + command-helpers.ts:138-140). Composer `stackFilePath` + (operations/shared.ts:48) and the generated-stack reproduce hint. + Platform: log paths in the daemon registry (dev-emulators + daemon.ts:26-31) — thinner here. + +8. **Child-process output — 3/3, three strategies.** Passthrough: Composer + `stdio: 'inherit'` (run-alchemy.ts:46-53). Captured + redacted + carried + in the error: ORM init reads child stderr, strips credentials, surfaces + an excerpt or `meta.stderrLines` (init/init.ts:809, 834-845, 925-945; + skill-install.ts:207-241). As typed events: platform build-log NDJSON + records with `source`/`level` routing to stdout/stderr + (controllers/build.ts:34-150). An engine vocabulary needs a + child-output-line concept that all three can target. + +9. **Durations — 2/3 consistently.** ORM `timings: {total}` ms rendered only + under `-v` (emit.ts:17-19; migrations.ts:92-94, 261-263, 329-332, + 477-479; verify.ts:71-73) plus span elapsed-ms suffixes + (progress-adapter.ts:62-69); platform `--verbose` timing diagnostics + appended best-effort (command-runner.ts:136-139, 173-191) and domain-wait + elapsed `mm:ss` (controllers/app.ts:2682-2687). Composer surfaces no + durations. + +10. **Status/state-machine enums — 2/3 (+ the daemon layer).** Platform + domain statuses (types/app.ts:175) and deploy display statuses + (presenters/app.ts:790-802); ORM per-migration + `'applied'|'pending'|null`; `@prisma/dev` `ServerStatusV1.status` + six-value enum (state d.ts:236). Any engine "wait" concept needs a + from→to status-transition event (the platform already emits exactly + that, controllers/app.ts:2651-2662). + +11. **Typed confirmation for destructive operations — 2/3.** Platform + `--confirm ` (commands/project/index.ts:101-106, 136-153; + database/bucket variants); ORM `db update`'s prompt-then-re-execute with + `yes:true` (db-update.ts:320-343); Composer's flag-encoded target + (`destroy` requires `--stage` or `--production`, main.ts:276-294). + +## D. `--json` reality + +- **ORM: near-universal, per-command shapes, plus surprises.** Every main-CLI + command has `--json` via the shared flag set (command-helpers.ts:368-388); + shapes are per-command result objects (the tables in §A1), several + arktype/schema-validated (commands/json/schemas.ts; init/output.ts:19-52). + **JSON auto-selects when stdout is not a TTY even without `--json`** + (utils/global-flags.ts:67-69, verified; `--format pretty` is the escape + hatch, terminal-ui.ts:334). No streaming JSON exists — spans never reach + `--json` consumers (the progress adapter no-ops under JSON, + progress-adapter.ts:36-38). Indentation is inconsistent: most commands + pretty-print, `ref`/`format`/`telemetry` emit compact single lines + (ref.ts:203, 230, 252; format.ts:67; telemetry/index.ts:33, 56, 77). + `migration graph --dot --json` silently emits DOT + (migration-graph.ts:249-258). The migration-file CLI has no `--json`. +- **Composer: none.** No JSON flag exists (main.ts:18-110). The machine + surface is the typed programmatic API (`@prisma/composer/control`) — + events as callbacks, results as values — rather than serialized output. +- **Platform: near-total, one envelope, streaming where needed.** Success: + `{ok: true, command, result, warnings, nextSteps, nextActions}` + pretty-printed (shell/output.ts:9-29); error: `{ok: false, command, error, + warnings, nextSteps, nextActions}` (:38-50, 164-183); crashes still emit + the envelope (`UNEXPECTED_ERROR` + recover action, :120-162; cli.ts:69-71). + Streaming commands switch to single-line NDJSON events (§B6). Deviations: + `app run` rejects `--json` outright (controllers/app.ts:273-281); + `app deploy` has two result shapes (commands/app/index.ts:311-314). + +Cross-family delta worth naming: the ORM buries remediation in per-command +shapes and prose while the platform reserved envelope-level `nextSteps` / +`nextActions` on every command; and only the platform has a +crash-still-emits-JSON guarantee. + +## E. Framework support for execution modes (stricli, clipanion) + +Both are parse-and-dispatch frameworks; **neither models execution modes at +all** — no concept of long-running commands, streaming, progress, daemons, +or watch modes. + +- **stricli** (bloomberg.github.io/stricli): documented features are routing, + typed argument parsing, isolated context, lazy command loading, + autocomplete. Its "Out of Scope" page explicitly declares out of scope: + cross-argument validation, local system access (use context injection), + **logging** ("no first-party logging solution"), **enhanced formatting** + (recommends chalk etc.), and **prompting/stdin** ("interactive user input + beyond command-line arguments is not supported"; recommends + enquirer/clack). Execution model: the command function runs to completion; + `run()` writes help/errors to the injected `context.process.stdout/stderr` + and sets `context.process.exitCode`. Nothing constrains what the function + does while running — a long-lived session is just a promise that hasn't + resolved. (Full internals evaluation: + wip/designs/engine/stricli-vs-clipanion.md.) +- **clipanion** (mael.dev/clipanion): documents command paths, option types, + execution contexts (`stdin`/`stdout`/`stderr`/`env`/`colorDepth`), + validation, error handling, help. Its one stream-relevant claim is + compositional: streams live in the context "so commands can easily + intercept the output of other commands". No modeling of long-running + commands, progress, or daemons. Composer's usage confirms the division of + labor: clipanion parses (main.ts:112-160); session lifetime, signals, and + events are hand-built above it (run-dev.ts:110-132). Same in the ORM + migration-file CLI (migration-cli.ts). + +Consequence: execution modes are the engine's to define. Nothing in either +framework will be contradicted by an engine-level mode taxonomy, and nothing +can be reused for it — the frameworks end where the handler begins. + +## F. The mode taxonomy the evidence supports + +The four starting modes hold, with refinements: "event-streaming +asynchronous" splits into in-flight progress (ends on its own) versus +poll-until-terminal (a remote state machine with timeout semantics), and two +modes must be added (wizard, stdio server). Browser hand-off and detached +worker spawns are capabilities that ride on other modes, not modes. + +1. **Synchronous request/response** — the dominant mode everywhere: + ~55 platform commands, ~14 ORM commands, 2 Composer commands (§A tables). + The engine's baseline: result value in, one rendering out, uniform + envelope. + +2. **Synchronous with in-flight progress** — same lifetime as (1), plus + intermediate reporting that both human and `--json` surfaces may consume. + Three dialects to unify: ORM nested spans with outcome + elapsed (B3, 7 + wiring sites), platform discrete step lines + `onStatusChange` callbacks + (B5), Composer's single pre-event on `destroy` (B1). Today none of the + ORM's span data reaches `--json`; the platform's step lines are + human-only. R14's vocabulary should make progress events representable in + both channels, which no family does today. + +3. **Poll-until-terminal (wait)** — distinct from (2) because it carries + timeout/deadline semantics, an explicit remote status enum, and + transition events: `app domain wait` (with `--timeout`, `--timeout 0` = + probe once, NDJSON transition events), `git connect`'s + interactivity-conditional poll, and the SDK-delegated deploy/promote/ + destroy polls (§A4). Small today (one dedicated verb) but structurally + different enough — and precedented enough — to be its own engine concept: + the platform team invented a `wait` verb rather than bolting waiting onto + `add`. + +4. **Long-lived session / watch** — runs until signal or abort, re-emits + over its lifetime: Composer `dev` (session object with `stop()`/`closed` + + typed events) and `log` (AsyncIterable + AbortSignal), platform + `app run` (dev-server passthrough), `app logs`, `build logs --follow` + (§A2, §A3). Two lifetime idioms exist — session object versus + abort-signal-terminated iterable — and the engine must pick or support + both; Composer deliberately keeps signal ownership in the host + (operations/dev.ts:41-43; run-dev.ts:124-131), which matches R4/R5. + +5. **Daemon-coupled (machine-scoped)** — the controlled process outlives the + CLI invocation: Composer's three emulator daemons (implicitly ensured by + `dev`, never stoppable from the CLI) and `@prisma/dev` stateful servers + (library primitives for scan/status/kill; the legacy `prisma dev` + `ls`/`stop`/`rm` surface per its README) (§A5). Consistent mechanics + across both implementations — registry file + lockfile liveness + HTTP + health with identity check + SIGTERM-then-SIGKILL — which is effectively + a specification for the engine's daemon concept. The evidence also shows + the gap the unified CLI must not reproduce: composer daemons have **no** + user-facing ps/stop/status. + +6. **Interactive wizard** — prompts drive the flow: ORM `init` (clack, + spinners, child installs), platform `init` (1065-line controller, six + prompt sites), plus scattered single-prompt commands (`project link`, + `auth workspace use`, `db update`'s confirm-and-re-run) (§A1, §A3). Both + families gate every prompt on an interactivity check that `--json`, + CI, and non-TTY force off (platform `canPrompt`, shell/runtime.ts:105-119; + ORM progress/prompt gating, progress-adapter.ts:36-38, + global-flags interactivity flags) — an engine-level capability check, not + per-command logic. + +7. **Stdio protocol server** — ORM `lsp`: lazy-imports the language server, + hands the process to `connection.listen()`, no exit path of its own + (§A1). One instance, but structurally unlike everything else: the CLI's + own rendering machinery must get out of the way entirely. + +Cross-cutting capabilities (not modes): **browser hand-off / local callback +server** (auth login's ephemeral-port server + callback-vs-paste race, +git connect, app open — always layered on S or P); **child-process +passthrough** (Composer deploy/destroy); **detached fire-and-forget self +processes** (platform update-check worker, ORM telemetry child) — invisible +to users, but the engine should know the pattern exists because both +families independently built it. + +Straddlers, noted rather than force-fit: `composer dev` is (4)+(5) — a +session that implicitly manages daemons; `app deploy` is (2) with (3) inside +the SDK; `db update` is (2) wrapped in a (6)-style confirm loop that +re-executes the command; `migration graph` is (1) with three output +renderers (tree/DOT/JSON). diff --git a/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r2.md b/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r2.md new file mode 100644 index 00000000..1f2f7e80 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r2.md @@ -0,0 +1,459 @@ +# Code review round 2 — unified CLI engine public interface (v3) + +Reviewer pass: principal engineer. Naming, typology and system shape remain +the architect's; referrals are marked. + +Subject: `wip/designs/engine/engine-interface-draft.ts` (v3), read against v1 +and v2 (`-v1.ts`, `-v2.ts`) and my round-1 artifact `./reviews/code-review.md`. + +## Summary + +v3 is a large improvement. Of the 25 round-1 findings, 15 are fully resolved, +7 are partly resolved with a named residual, and 3 are untouched. Both round-1 +FAIL verdicts (R6 exit codes, R10 config sections) are cleared: exit codes now +span the settled table and config sections are bound to commands by a typed +token, which is what makes "a command fails only if a section it needs is +invalid" implementable rather than aspirational. The type-system defects that +made v1 unbuildable are genuinely fixed — I worked through each one and they +hold up. + +The return-site presentation ruling is, I think, right, and for a reason +stronger than the one stated. Under v1/v2 a presenter ran after the handler +had returned, which meant it had to reconstruct which case it was in from the +result value alone, and a throw inside it happened after the command had +logically succeeded. Under v3 the views close over live context at the point +the outcome is known, and a throw in a view is just a handler throw. That is +a real reduction in the number of things that can go wrong. + +Three things about the new shape need attention before this is settled. + +**The one internal contradiction is `exitCode`.** The v3 header states that +"nothing product-authored executes after the handler resolves — the engine +receives values, never callbacks." `exitCode?: (data: unknown) => number` is +precisely a product-authored callback the engine invokes after resolution. It +is also the one place where moving presentation to the return site cost type +safety that v2 had: v2's signature was `(value: TResult) => number`, correctly +typed; v3's is `(data: unknown) => number`, so every command computing a +custom exit code casts. Both problems have the same fix, and it is the fix +v3's own thesis implies: carry the exit code as a value on the presented +result. See N01 — this is the finding I would act on first. + +**The mode-to-view mapping has three unspecified interactions** that will be +decided by whoever implements first rather than by this document: what +`--verbose` does (there is no verbose view, so product-level detail under `-v` +— which both the ORM and the platform ship today — becomes unreachable); +what `--yes` does to `ctx.prompt.confirm` (handlers no longer see the flag, so +either the engine auto-answers or `-y` silently stops working in CI); and what +`--json --quiet` together mean, since the two modes select disjoint view sets. +N06 and N07. + +**`AnyCommand` has no runtime discriminant.** The three definition types are +structurally near-identical and differ only in their handler's return type, +which is erased at runtime. The engine must decide, per mounted command, +whether to expect a `PresentedResult` or `void`, whether to inject the shared +flag family, and whether auto-json applies — and it has nothing to branch on. +The `define*` functions are the natural place to stamp a tag. N03. + +Below that tier, the notable fresh items are: `PresentedResult` is +structurally constructible, so the "built exclusively by ctx.present" +invariant is documentation rather than type (N02); `NextAction` is declared in +the engine package but error envelopes carry next actions, which puts it on +the wrong side of the engine/foundation boundary (N04); flag `default`s do not +narrow the flag's type, so every defaulted flag still needs `?? default` in +the handler (N05); and positional order is object key order, which is both +implicit and, for integer-like keys, not insertion order at all (N08). + +Pressure-test results in brief. Multi-return-path handlers work well and are +better than v2 — each return site builds its own views with the case in hand. +`migration check` maps except for the `exitCode` cast. Composer `dev` now maps +cleanly onto `defineSessionCommand` (`Result`, event stream is the json +surface, `--fresh` as a boolean flag). `lsp` maps onto `defineRawCommand`. +`app deploy`'s two result shapes are no longer a problem at all, because there +is no result generic to unify — each path presents itself. That is a real +benefit of the ruling I did not anticipate. + +## Round-1 finding disposition + +| # | Round-1 finding | Disposition | Note | +|---|---|---|---| +| F01 | `ArgsOf` drops positionals | **Resolved** | `Args` takes both objects explicitly; `positionals` optionality no longer collapses the mapped type. Separate `flags`/`positionals` namespaces. | +| F02 | Handler typing is circular | **Resolved** | `CommandHandler` with a type-only import breaks the cycle; `import type` erases, so no runtime cycle. See N09 — the helper does not cover session or raw definitions. | +| F03 | Brand symbols unexported | **Resolved** | `export { FLAG }` / `export { POSITIONAL }` added; declaration emit will work. | +| F04 | Concrete command not assignable to bare `CommandDefinition` | **Resolved** | `AnyCommand` with `any` generics is assignable in both directions, so `CommandSet`, `createCli` and `createTestCli` accept real commands. Runtime discrimination is a separate problem (N03). | +| F05 | `TConfig` not inferable | **Resolved** | `configSection?: ConfigSection` is a direct inference site. Default `undefined` gives `config: undefined` when omitted. | +| F06 | No number flag, no defaults | **Partial** | `flag.number` and `default` added. But `default` does not narrow the return type — `flag.number({default: 15})` still yields `FlagSpec`, so handlers keep the `?? 15` (N05). | +| F07 | No short aliases | **Partial** | `alias?: string` added. Not constrained to one character, which is stricli's hard limit, so `alias: 'dp'` compiles and fails later (N05). | +| F08 | Positionals too thin | **Partial** | `variadic` added. Number and enum positionals still absent (acceptable). The "at most one, last" rule is unenforceable because positionals are an unordered `Record` (N08). | +| F09 | Flag/positional name collisions | **Resolved** | Separate namespaces make collision impossible by construction. | +| F10 | Exit codes contradict the settled table | **Partial** | 0/1/2/3/4–99/130/143 now stated; `exitCode` provides 4–99; the engine owns signal codes. Residuals: the callback is untyped and post-resolution (N01), the 4–99 range is not type-enforced, and whether a structured *failure* can carry a custom code is unstated. | +| F11 | Events have no defined stream | **Resolved** | `channel: 'data' \| 'diagnostic'` with routing documented — data to our stdout, everything else to stderr, all framed on stdout in json mode. This is the finding I was most concerned about and the fix is clean. | +| F12 | Session commands have no meaningful result | **Resolved** | `defineSessionCommand` returns `Result`, no presentation, event stream is the json surface. Also subsumes the platform's `emitJsonSuccessEvent: false` case. | +| F13 | `raw` contradicts the type | **Resolved** | `defineRawCommand` is its own type with its own handler shape returning an exit code directly. Raw commands cannot take positionals — presumably deliberate; worth confirming for `lsp`. | +| F14 | `Runtime` missing `env` and `isTty.stdout` | **Resolved** | Both added; auto-json now has a field to read and the engine's CI detection stays inside the injection seam. | +| F15 | No envelope slots for warnings/next steps/actions | **Resolved** | `next` view supplies `nextActions`; `nextSteps` derives from them; `warnings` aggregates from severity-`warn` message events. "Emit once, appear in both places" is a good call. Derivation rule for `nextSteps` is unstated (minor). | +| F16 | Nothing says which config section a command needs | **Resolved** | `ConfigSection` token plus `configSection` binding. Raw sections in `LoadedConfig`, validated per command, is exactly what makes the R10 rule implementable. Residual in N12. | +| F17 | Cancellation and exit-code path undefined | **Partial** | The engine now records which signal fired, and prompt failures carry distinct codes for "unavailable" (exit 2) versus "cancelled" (exit 3). **Still open:** no teardown deadline and no defined behaviour for a second Ctrl-C. A session that will not die remains unspecified. | +| F18 | `report` has no backpressure, no end of life | **Partial** | Report-after-resolution is now documented as an `InternalError`, which closes the envelope-corruption hole. Backpressure is untouched: `report` still returns `void` with no stated buffer bound, so a high-volume log tail on a slow pipe is still an unbounded-memory path. | +| F19 | Test harness cannot test session commands | **Partial** | `abort`, `answers`, `isTty`, `env`, `now` and the `presented` capture are all new and substantial — sessions are now testable. Residuals: no `cwd` knob (so commands writing artifacts relative to cwd write into the repo); events are only observable after the run, so "abort once ready" needs a timer rather than a condition; no capture of which prompts were asked (N11). | +| F20 | Steps have no identity | **Resolved** | `id`/`parentId` added, matching the ORM's span shape. | +| F21 | Credentials cannot refresh | **Resolved** | `getCredentials(): Promise` on both context and runtime. Long sessions survive expiry. | +| F22 | No way to declare a command needs auth | **Open** | Unchanged. Every handler still checks `undefined` and writes its own "not authenticated" error, so the wording drifts per product — the same failure R5 exists to prevent, one layer down. | +| F23 | `createCli` claims build-time failure it cannot deliver | **Partial** | The comment now says "build time, not run time" explicitly, which sharpens the claim but does not change the mechanism: `createCli` returns `Cli`, not `Result`, so it can only throw when called. | +| F24 | Foundation import and Node types in the public surface | **Open** | `Result`/`CliStructuredError` still come from `@prisma/cli-foundation`; `NodeJS.WritableStream` etc. still appear in `Runtime` and in the raw command's `io`. N04 adds a new instance of the same boundary problem in the opposite direction. | +| F25 | Engine flags leak into `args` | **Resolved** | The shared family is engine-injected, reserved, and never reaches handlers; `--json` is now an engine mode rather than a declared flag. This is a better answer than the one I suggested. | +| F26 | No optional-dependency probe (R13's positive half) | **Partial** | `probeDependency(specifier): Promise` added. A bare boolean means the handler still authors the "missing dependency, install it with your package manager" error, so the wording drifts per product; and a boolean cannot express the failure Composer actually hits, which is a resolvable-but-wrong-version conflict (`DEPS.EFFECT_VERSION_CONFLICT`), not absence. | + +**Counts:** resolved 15, partial 7, open 3. + +## Fresh findings + +### N01 — `exitCode` is a post-resolution callback, and it lost the typing v2 had + +**Location:** §7, `CommandDefinition.exitCode?: (data: unknown) => number` +(lines 405–408); header claim at lines 23–24. + +**Issue:** two problems in one field. First, the v3 header states that nothing +product-authored executes after the handler resolves and that the engine +receives values, never callbacks — and this is a product-authored callback the +engine invokes after resolution. It is the only exception in the file. Second, +because v3 removed the result generic, the callback receives `unknown` where +v2's received `TResult`. Every command with a custom exit code now casts: +`exitCode: (data) => (data as CheckResult).failures.length > 0 ? 4 : 0`. + +**Why it matters:** the cast is at the exact point round-1 F10 was trying to +make safe — a wrong cast produces a wrong exit code, which is the machine +surface agents and CI branch on, and no type checks it. The contradiction also +matters on its own terms: an invariant with one exception is an invariant +people stop trusting, and this one is load-bearing for the "values all the way +down, serializable, snapshotable" property v3 is selling. + +**Suggestion:** move the exit code to the return site with everything else — +`ctx.present(data, views, { exitCode: 4 })`, or a third `Views` member. The +exit code is a fact about the outcome, known exactly where the outcome is +known. This removes the cast, removes the callback, restores the invariant, +and drops a field from the definition. It also relaxes a constraint the +current shape imposes without saying so: `exitCode` as a pure function of +`data` cannot express an exit code that depends on run context rather than on +the returned value. + +While making that change, consider typing the code as a branded 4–99 value or +validating the range in the engine — a handler returning `300` or `-1` becomes +a nonsense shell status via mod 256. + +### N02 — `PresentedResult` is structurally constructible, so the mode invariant is unenforced + +**Location:** §3, `PresentedResult` (lines 191–199); "Built exclusively by +ctx.present" (line 182). + +**Issue:** `PresentedResult` is a plain interface with two public members. A +handler can return `{ data, views: { human: [...] } }` directly and satisfy the +type. Nothing marks it as engine-constructed. + +**Why it matters:** the entire correctness argument for return-site +presentation is that the *context* decides which views to materialize, because +only it knows the mode. A hand-built literal breaks that silently: it might +carry a `human` view in json mode (harmless, wasted) or omit one in human mode +(the engine has nothing to render and must invent a fallback). Neither is +caught anywhere, and both are the kind of thing that gets copied once and then +spreads. + +**Suggestion:** brand it exactly as `FlagSpec` is branded — an exported +`unique symbol` phantom member that only `ctx.present` can produce. The +mechanism is already in the file; this just applies it one more time. + +### N03 — `AnyCommand` has no runtime discriminant + +**Location:** §7, `AnyCommand` (lines 496–499); the three definition +interfaces. + +**Issue:** `CommandDefinition`, `SessionCommandDefinition` and +`RawCommandDefinition` have the same field names and differ only in their +handler's return type, which is a type-level fact erased at runtime. `createCli` +receives `Record` and must decide, per command, whether to +await a `PresentedResult` or a `void`, whether to inject the shared flag +family (raw: no), whether auto-json applies (raw: no), and which help layout +to use. + +**Why it matters:** with nothing to branch on, the engine either introspects +the loaded handler's return value at execution time — which means the decision +about flag injection and json mode, both of which must be made *before* the +handler loads, cannot be made at all — or it guesses. This is a genuine +blocker for the mounting path rather than a tidiness issue. + +**Suggestion:** have `defineCommand` / `defineSessionCommand` / +`defineRawCommand` stamp a discriminant (`readonly kind: 'value' | 'session' | +'raw'`) and make `AnyCommand` a discriminated union on it. That also lets the +shell validate mounts sensibly, and narrows correctly in the engine's own +code. + +### N04 — `NextAction` sits on the wrong side of the engine/foundation boundary + +**Location:** §1, `NextAction` (lines 60–69); §9, `ErrorEnvelope.nextActions` +(line 563). + +**Issue:** `NextAction` is declared in the engine package. But error envelopes +carry `nextActions`, and errors are raised at their origin inside product +operations, carried in `CliStructuredError` from `@prisma/cli-foundation`. For +a structured error to carry next actions, the foundation must reference +`NextAction` — which would make the foundation depend on the engine, inverting +the dependency the two-package split exists to establish. + +**Why it matters:** it is a package cycle discovered at implementation time +rather than now. The workaround people reach for — errors carry +`nextSteps: string[]` while successes carry `NextAction[]` — is exactly the +"one concept spelled several ways" the survey ranks as the second most +recurring problem in the corpus, reintroduced at the success/failure seam. + +**Suggestion:** move `NextAction` (and `Severity`, which has the same +property) into `@prisma/cli-foundation` and re-export from the engine. +Which package owns which type is an architect call; that the current +placement cannot work is not. + +### N05 — Flag defaults do not narrow, and aliases are unconstrained + +**Location:** §6, `flag.string` / `flag.number` / `flag.enum` (lines 317–339). + +**Issue:** `flag.number({ brief, default: 900 })` returns +`FlagSpec`. The whole purpose of a default is that the +value is always present, so the handler still writes `?? 900` — and now the +default lives in two places and can disagree. Separately, `alias?: string` +accepts any string, while stricli supports single-character aliases only. + +**Why it matters:** the duplicated default is a correctness trap (help text +says one thing, the handler's fallback says another) and it removes the +benefit that motivated adding defaults at all. The alias type accepts values +that cannot work. + +**Suggestion:** overload each factory so the presence of `default` produces the +non-optional spec type. Constrain `alias` to a one-character template-literal +type, or validate it when the tree is constructed and say so. + +### N06 — `--verbose` has no view, so product-level detail under `-v` is unreachable + +**Location:** §3, the mode-to-view mapping (lines 187–190). + +**Issue:** the mapping covers human, `--quiet` and json. `--verbose` is in the +injected flag family but selects no view, and handlers cannot see it (by +design, correctly). + +**Why it matters:** both shipping families put product detail behind `-v` +today — the ORM renders `timings` and expands truncated conflict lists, the +platform appends timing diagnostics. Under this shape the engine can add its +own detail under `-v` but a product can never add any. That may well be the +right ruling, but as an unstated omission it will be discovered when someone +tries to port `db verify`'s verbose conflict list and finds there is nowhere +to put it. + +**Suggestion:** either add an optional `verbose?: (ui: Ui) => readonly Block[]` +view materialized only in verbose human mode, or state explicitly that `-v` +adds engine-owned detail only and that product detail belongs in `data`. + +### N07 — `--yes` and `--json --quiet` semantics are unspecified + +**Location:** header lines 33–35 (the injected flag family); §5, +`PromptSurface`. + +**Issue:** two interactions are named nowhere. (a) Handlers no longer see +`--yes`, so what does it do? If it does not auto-answer `ctx.prompt.confirm`, +then `-y` has stopped working and every CI script that relies on it breaks. +If it does, then the engine is auto-confirming destructive operations, and the +platform's deliberately stronger pattern — typed confirmation, `--confirm +` — must be documented as something `-y` does *not* satisfy. +(b) `--json --quiet` selects two disjoint view sets (json+next versus stdout); +precedence is undefined. + +**Why it matters:** (a) is a destructive-operation safety question, which +makes it the highest-consequence unstated default in the file. (b) is minor +but will be resolved differently by different implementers. + +**Suggestion:** state that `--yes` makes `prompt.confirm` return `true` +without prompting and that it does not satisfy typed confirmation, which stays +a declared flag. State that json mode wins over `--quiet`. + +### N08 — Positional order comes from object key order + +**Location:** §6, `positional` (lines 348–353); `positionals?: TPositionals` +as a `Record`. + +**Issue:** positionals are declared in an unordered record, so argument order +is object key insertion order. That is stable for ordinary string keys but +*not* for integer-like keys, which JavaScript reorders to the front. The two +ordering rules the comments assert — variadic last, and by implication +optional after required — cannot be expressed or checked. + +**Why it matters:** an implicit ordering rule carried by object literal syntax +is a subtle source of wrong argument binding, and the failure mode (arguments +silently swapped) is quiet. + +**Suggestion:** at minimum, validate both rules when the tree is constructed +and document that declaration order is argument order. An ordered form (a +tuple, or `positionals: [named(...), named(...)]`) removes the class entirely +— that is a shape choice, so architect referral, but the current form does +need one of the two. + +### N09 — `CommandHandler` covers only value commands + +**Location:** §7, `CommandHandler` (lines 422–424). + +**Issue:** the conditional matches `CommandDefinition` only. A session +command's implementation file has no helper and must hand-write +`(args: Args, ctx: CommandContext) => Promise>`, +which is exactly the drift F02 was fixed to prevent. Raw commands likewise. + +**Suggestion:** extend the conditional to all three definition types, or ship +`SessionHandler` and `RawHandler` alongside. + +### N10 — `Views`'s type parameter is unused + +**Location:** §3, `Views` (lines 211–216). + +**Issue:** none of the four members mentions `T` — the view functions close +over the data lexically rather than receiving it. So `Views` is +structurally `Views` and `T` is inferred solely from `present`'s +first argument. + +**Why it matters:** low severity, but a phantom type parameter invites the +reader to believe a relationship is being checked when it is not. Someone will +eventually pass views that describe a different value than `data` and nothing +will complain. + +**Suggestion:** either drop the parameter, or pass the data into the view +functions (`human: (data: T, ui: Ui) => Block[]`) so the relationship is real. +The second also makes views extractable into named module-level functions, +which helps multi-return-path handlers share view logic. + +### N11 — The harness cannot observe events mid-run, control cwd, or capture prompts + +**Location:** §11, `TestCli.run` options and result. + +**Issue:** three residuals from F19. (a) `abort?: AbortSignal` requires the +test to decide *when* to abort, but events are only visible after `run()` +resolves — so the realistic session test ("come up, reach ready, then stop") +has to use a timer, which is the flaky-test pattern. (b) No `cwd` option, so a +command that writes artifacts relative to cwd writes into the product repo +during tests. (c) `answers` is consumed positionally with no capture of which +prompts were asked, so a test cannot assert that the destructive-operation +confirmation was actually shown — only that *something* consumed an answer. + +**Why it matters:** (a) and (c) between them mean the two riskiest command +classes — sessions and destructive prompts — are testable in form but weakly +in substance. + +**Suggestion:** add an `onEvent` callback (or `abortWhen: (e: EngineEvent) => +boolean`) so aborts are condition-driven; add `cwd`; add `prompts` to the +result recording each question asked and the answer given. + +Related, and worth one line: because only the active mode's views are +materialized, a single test run can never assert on both the human and the +json rendering. That is inherent to the design and fine — but it should be +stated, so product test suites are written to run both modes rather than +discovering one of them is uncovered later. + +### N12 — Successful section validation may carry diagnostics with no defined fate + +**Location:** §4, `SectionValidation`'s `ok: true` branch (line 235). + +**Issue:** the success branch carries `diagnostics: readonly +CliStructuredError[]` — non-fatal problems in an otherwise valid section. The +draft never says what the engine does with them. + +**Why it matters:** they are presumably meant to surface as warnings, which +would mean they belong in the envelope's `warnings` array alongside the +severity-`warn` message events. If unstated, they get dropped, and a user's +deprecated-config-key warning silently never appears. + +**Suggestion:** state that they render as warnings and join the envelope's +`warnings`, on the same path as `message` events. + +Separately: `LoadedConfig` still cannot distinguish "no config file exists" +from "config file exists and is empty". Both produce `sections: {}`, +`diagnostics: []`. For a command that needs a section, both produce +`config: undefined` and the handler writes the same error, so this is probably +harmless — but "you have no prisma.config.ts" and "your prisma.config.ts is +missing the composer section" deserve different fixes, and only the product +can tell them apart if the engine gives it the fact. + +### N13 — Config validators live in the eagerly loaded tree + +**Location:** §4, `ConfigSection.validate`; §7, `configSection` on the +definition. + +**Issue:** R9 keeps heavy dependencies behind the lazy `handler`. The +`ConfigSection` token — including its validator function — is referenced by +the static definition, so it and whatever it imports load at startup. A +validator built on a schema library (arktype and zod both appear in the +corpus) pulls that library into every invocation of every command, including +`prisma --help`. + +**Why it matters:** R9's stated motivation is that `prisma migrate` can never +be slowed or taken down by a product it is not using. A shared config +validator undermines that for all commands at once, and it will not be +noticed until startup time is measured. + +**Suggestion:** state the rule (validators must be dependency-free, or +hand-written predicates), or make the validator itself lazily loaded like the +handler. Worth deciding now, because it constrains how products write +validators. + +### N14 — `EventFrame` nests an event's `data` inside the frame's `data` + +**Location:** §9, `EventFrame` (lines 567–573). + +**Issue:** `EventFrame.data` is the whole `EngineEvent`, which itself has a +`data` field for product extensions. Machine consumers read +`frame.data.data` for the extension payload, and `frame.type` duplicates +`frame.data.kind`. + +**Why it matters:** minor, but this is the agent-facing wire format — the one +surface where a confusing shape is paid for by every consumer, forever, and +which is the hardest thing in the design to change later. + +**Suggestion:** flatten the event's own fields into the frame, or rename one +of the two `data` fields. The platform's shipped shape +(`{type, command, timestamp, data}` where `data` is the payload) suggests +flattening. + +## Deferred + +Unchanged from round 1, and still correctly out of scope: config-section +*registration* mechanics beyond the token (the token is what R10 needed, and +it is now present), daemon management (mode 5), autocomplete, telemetry hooks, +and duration/timing events. One addition: whether raw commands should accept +positionals (`lsp` may want a path) is a small open question rather than a +defect. + +## Acceptance-criteria verification + +Same strict verdicts as round 1. **PASS** = the interface structurally +satisfies or enforces the requirement. **WEAK** = satisfiable, but the shape +does not enforce it. **FAIL** = the shape contradicts the requirement or +cannot express it. **NOT VERIFIED** = cannot be assessed from the draft. + +| R | Requirement | R1 | R2 | Detail | +|---|---|---|---|---| +| R1 | One language, directly executable | PASS | **PASS** | Unchanged, and strengthened: `CommandHandler` removes the one path by which round 1's typing defects would have reintroduced a hand-maintained parallel description of the arguments. The declared object is still what runs. | +| R2 | Commands end in typed operation calls | WEAK | **WEAK** | Unchanged. The shape remains compatible and the thin case remains the natural one, but nothing prevents a fat handler. Return-site views arguably pull slightly the other way — presentation logic now lives in the handler file — though it is presentation, not business logic, so the requirement is not threatened. | +| R3 | The engine package is the whole contract | WEAK | **WEAK** | No stricli type appears; that goal still holds. The two round-1 leaks persist: products import `@prisma/cli-foundation` for `Result`/`CliStructuredError`, and `NodeJS.*` stream types remain in `Runtime` and in the raw command's `io`. N04 adds a third instance in the opposite direction — `NextAction` is on the engine side but is needed by foundation-owned errors, which as written is a package cycle. Fixable without design change, but no longer trivially. | +| R4 | Products receive a context, never the environment | PASS | **PASS** | Improved. `getCredentials()` replaces the static token, `probeDependency` gives the R13 check a sanctioned route that does not touch the environment directly, and `cwd` is still the only path to the working directory. Nothing in the context reaches disk, env or TTY. | +| R5 | Products have no presentational API | PASS | **PASS** | Held, and on a better footing. `Block` remains the only vocabulary, `Ui` still cannot write, and products no longer see `--json`/`--quiet`/`--verbose` at all, which removes the temptation round-1 F25 identified. Return-site materialization means no product code runs after resolution — with the single `exitCode` exception (N01). N06 records the cost: products cannot express verbose-only detail. | +| R6 | Errors and results follow the settled conventions | **FAIL** | **WEAK** | Cleared as a failure. The exit-code space now matches the settled table: 4–99 via `exitCode`, 130/143 engine-owned with the signal recorded, and prompt failures carry distinct codes so "interaction unavailable" (2) and "user cancelled" (3) are mechanically separable rather than string-matched. Not yet PASS: the custom code is computed by an untyped post-resolution callback (N01), the 4–99 range is not enforced anywhere, and whether a structured *failure* can carry a custom code is unstated. | +| R7 | Product-repo end-to-end tests are first-class | WEAK | **PASS** | Upgraded. `abort` makes session commands testable, `answers` makes prompt-bearing commands testable, `isTty`/`env`/`now` make mode selection and framing deterministic, and the `presented` capture allows semantic assertions without byte-scraping. Every command class can now be driven argv-in, bytes-out from a product repo, which is what the requirement asks. The residuals in N11 — no `cwd`, no mid-run event observation, no prompt capture — are real and worth fixing, but they narrow the quality of the tests rather than the class of commands that can be tested. | +| R8 | The shell's test burden is integration proof | NOT VERIFIED | **NOT VERIFIED** | Still an allocation-of-work requirement with no interface surface. Nothing obstructs it. Assess against the shell's test plan. | +| R9 | Static tree, lazy guts | PASS | **PASS** | The lazy `handler` is unchanged and still matches stricli's loader; help still renders from static declarations. Removing the presenters from the definition makes the static tree lighter than v2's, which is a real gain. One new leak to watch, not enough to change the verdict: `configSection.validate` is referenced from the static definition, so a schema library behind a validator lands in every invocation's startup path (N13). | +| R10 | One config file, validated by its products, never a crash | **FAIL** | **PASS** | Cleared, and this is the largest single improvement in v3. The `ConfigSection` token couples name, type and never-throwing validator; `configSection` binds a command to exactly one section; `LoadedConfig` carries raw sections validated per command. That is what makes "a command fails only if a section it needs is invalid" implementable — a bad Composer section genuinely cannot touch `prisma migrate`. File-level problems (unevaluable module, missing `defineConfig` marker) are expressible as `section: null` diagnostics that fail everything, which is R10's fail-early rule. Residuals are small and named in N12. | +| R11 | Pinned versions, tandem releases | NOT VERIFIED | **NOT VERIFIED** | Release-process requirement, no interface surface. | +| R12 | The shell defines the command tree | PASS | **PASS** | Unchanged. No path appears in any definition; mount keys and group briefs live at `createCli`. `AnyCommand` now makes the mount maps actually typecheck, which round 1 found they did not. The overstated "fails the build" claim (F23) persists as a documentation-versus-mechanism gap, not a requirement failure. | +| R13 | The CLI never touches a package manager | WEAK | **WEAK** | The prohibition still holds absolutely — nothing installs or vendors. `probeDependency` gives the positive half a sanctioned route, which is progress. Still not PASS because the requirement's positive half is a *structured error naming the dependency and how to install it*, and a `Promise` produces no error at all: each product writes its own wording, so the message drifts exactly as R5 predicts. A boolean also cannot express the failure Composer actually hits, which is a version conflict rather than absence. | +| R14 | One event vocabulary, engine-defined, with product extensions | PASS | **PASS** | Strengthened on every axis round 1 flagged: `id`/`parentId` restore the nesting the ORM's span model needs, `channel` gives data-versus-commentary routing, `artifact` and `from` were added on survey evidence, and `warning`/`notice` merged onto the one severity scale. `data?: unknown` remains the uniform extension point with the engine explicitly not interpreting it. The only blemish is the wire shape's double `data` (N14), which is a framing detail rather than a vocabulary one. | + +### Summary counts + +| Verdict | Round 1 | Round 2 | Requirements (round 2) | +|---|---|---|---| +| PASS | 6 | **8** | R1, R4, R5, R7, R9, R10, R12, R14 | +| WEAK | 4 | **4** | R2, R3, R6, R13 | +| FAIL | 2 | **0** | — | +| NOT VERIFIED | 2 | **2** | R8, R11 | +| **Total** | 14 | **14** | | + +Movement: R6 FAIL → WEAK, R10 FAIL → PASS, R7 WEAK → PASS. No regressions. diff --git a/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r3.md b/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r3.md new file mode 100644 index 00000000..f0b0380f --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r3.md @@ -0,0 +1,631 @@ +# Code review round 3 — unified CLI engine public interface (v4) + +Reviewer pass: principal engineer. Round-3 findings are numbered P01–P11 (F = +round 1, N = round 2). + +Subject: `wip/designs/engine/engine-interface-draft.ts` (v4), read against v3 +and my round-2 artifact `./reviews/code-review-r2.md`. + +Three claims in this review were checked by compiling them rather than +reasoning about them (TypeScript 5.6, `--strict`). Where that happened I say +so and give the compiler's output. + +## Summary + +**The design is settled. What remains is not design work.** Every structural +question I raised across rounds 1 and 2 is now closed, both round-1 FAILs +stayed closed, and the semantics reframe is coherent. I would stop iterating +on the shape. + +What remains is two type declarations that do not do what they say, one safety +property that is currently a convention where it could cheaply be a type, and +one under-specified stream contract. None of them changes a shape; all are +local edits to individual declarations. I am explicitly **not** calling this a +clean verdict, because two of them are defects rather than nits — but the +distance to clean is small and mechanical. + +**P01 is a hard bug.** `SingleChar = string & { readonly length?: 1 }` does not +constrain aliases to one character — it rejects *every* string, including +`'q'`. TypeScript types `'q'.length` as `number`, never as `1`, so no string +literal can satisfy `{length?: 1}`. Compiled: + +``` +t.ts(4,5): error TS2322: Type 'string' is not assignable to type 'SingleChar'. +t.ts(5,5): error TS2322: Type 'string' is not assignable to type 'SingleChar'. +``` + +Line 4 is `alias: 'q'`. The fix introduced for N05 makes the `alias` field +unusable. A working formulation exists and I verified it. + +**P02 is the one real gap left in an otherwise excellent mechanism.** The +outcome-code split — a documentable catalogue on the definition, selection at +the return site — is the right answer, better than what I proposed, because +the catalogue renders in help without executing anything. But +`opts?: { outcomeCode?: number }` is typed `number`, not against the +catalogue. So `ctx.present(data, p, { outcomeCode: 7 })` against a catalogue +of `{4, 5}` compiles, and the engine's verification fires at the return site — +after the command has done all of its work. A typo turns a successful +`migration check` into an internal error at exit. This is closable: I compiled +the fix, and TypeScript produces +`Type '7' is not assignable to type '4 | 5 | undefined'` at the call site. + +**P03 is the item I would think hardest about.** The prompt-default rule is +clever and I like it — `--yes` accepts a declared default, no default means a +structured halt, so destructive confirmations "simply declare no default" and +`--yes` can never blast through them. But nothing *enforces* that a +destructive confirm declares no default. One product writing +`confirm('Delete production database?', { default: true })` re-opens the hole +silently. R5's own rationale is that convention "demonstrably did not hold the +line", and this is the highest-consequence convention in the file. Making it +structural is cheap: a separate `confirm.destructive(question)` that accepts +no `default` parameter at all turns the rule into a type error. + +**P04 concerns the one named consumer of raw mode.** `InputStream extends +AsyncIterable` does not say whether it yields chunks or lines, and it +yields decoded strings. An LSP server over stdio reads `Content-Length: N` and +then exactly N *bytes*; a decoded string cannot be counted in bytes when the +payload is multi-byte, and if the engine helpfully splits on newlines the +framing is destroyed outright (LSP payloads contain newlines). `lsp` is the +only command `defineRawCommand` exists for, so this contract should be settled +against it. + +Two things I want to credit. Removing the `exitCode` callback makes "nothing +product-authored executes after the handler resolves" exception-free — that +invariant is now true as written, which it was not in v3. And resolving the +`--verbose` question through one log-level mechanism rather than a second +presentation member is the better of the two answers I offered; products emit +`verbose` messages as events they already had, and no new surface appeared. + +I also withdraw one round-2 grade. I marked R13 WEAK because `probeDependency` +returns a bare boolean and each product authors its own missing-dependency +error. Re-reading R13, it asks that *the command* return that error — product +authorship is what the requirement specifies, not a deviation from it. With +`packageManager` added in v4, the command now has the facts to phrase "install +it with your own package manager". That is a PASS, and my round-2 WEAK +imported an R5 concern into an R13 verdict. + +## Disposition of round-2 findings + +| # | Round-2 finding | Disposition | Note | +|---|---|---|---| +| N01 | `exitCode` is a post-resolution callback; lost v2's typing | **Partial** | The callback is gone and the "no product code after resolution" invariant is now exception-free — that half is fully resolved, and the catalogue split is better than what I proposed. The typing half is not: selection is `number`, verified against the catalogue at runtime, at the return site. See **P02**. | +| N02 | `PresentedResult` hand-constructible | **Resolved** | Branded with an exported `PRESENTED` symbol, same idiom as `FLAG`. | +| N03 | `AnyCommand` has no runtime discriminant | **Resolved** | `kind: 'command' \| 'session' \| 'raw'` stamped by the `define*` functions via the `Omit<…, 'kind'>` pattern. I compiled the inference question this raises (does `Omit<>` wrapping break generic inference from `flags`?) — it does not. No concern. | +| N04 | `NextAction` on the wrong side of the package boundary | **Resolved** | Moved to `@prisma/cli-foundation`, so the engine and the error envelope share it with no cycle. | +| N05a | Flag defaults do not narrow | **Open** | Unchanged: `flag.number({ default: 900 })` still yields `FlagSpec`, so the handler keeps `?? 900` and the default lives in two places that can disagree. See **P05**; fix verified. | +| N05b | `alias` unconstrained | **Regressed** | `SingleChar` was added and rejects every string, including one-character ones. See **P01**. | +| N06 | `--verbose` selects no view | **Resolved** | One mechanism: `--log-level error\|warn\|info\|verbose`, a `verbose` message severity, `--verbose` as shorthand. No presentation member added. This is the better answer. | +| N07 | `--yes` and `--json --quiet` unspecified | **Partial** | The `--yes` mechanism is now fully specified and thoughtfully designed; the residual is that its safety property is convention-only (**P03**). `--json --quiet` precedence is still unstated — the materialization table covers `human+--quiet` only (**P10**). | +| N08 | Positional order is object key order | **Resolved** | Documented: declaration order is argument order, variadic last, keys must not be integer-like. Convention rather than type, but the failure is now named where an author will read it. | +| N09 | `CommandHandler` covers only value commands | **Open** | Unchanged. Session and raw implementation files still hand-write the handler signature, which is the drift F02 was fixed to prevent. See **P09**. | +| N10 | `Views`'s parameter unused | **Resolved** | Renamed to `Presentations` and de-genericised. The phantom parameter is gone. | +| N11 | Harness: no cwd, no mid-run events, no prompt capture | **Mostly resolved** | `cwd` and an `onEvent` live tap added — the two that mattered. Prompt capture is still absent (nit, in **P11**). | +| N12 | Fate of `ok: true` section diagnostics unstated | **Open** | Unchanged. Nit, in **P11**. | +| N13 | Validators load at startup | **Resolved** | Documented as "keep validators dependency-light: they load with the definition tree at startup (R9), not with the handler." Convention, but stated at the point of use. | +| N14 | `EventFrame` nests `data` inside `data` | **Resolved** | `Frame = EventFrame \| ResultFrame` with `event: EngineEvent`. The added `ResultFrame` also makes the json stream self-describing, which is more than I asked for. | +| F17 | No teardown deadline / second-signal behaviour | **Resolved** | "First signal fires context.signal and awaits handler teardown; a second signal exits immediately with the signal's code." | +| F18 | `report` backpressure | **Accepted trade** | Now explicit: "synchronous fire-and-forget; the engine buffers and writes asynchronously (no backpressure signal — accepted trade)". Legitimate. Residual: the buffer has no stated bound or drop policy (**P08**). | +| F22 | No way to declare a command needs auth | **Resolved** | `requiresCredentials` on value and session definitions; the engine fails early with one canonical sign-in error. | +| F23 | `createCli` claims build-time failure | **Open** | Unchanged doc-versus-mechanism gap. Nit, in **P11**. | +| F24 | Foundation import and Node types in the surface | **Mostly resolved** | `NodeJS.*` replaced by structural `OutputStream`/`InputStream` — the runtime-agnosticism concern is fully addressed. The remaining residual is that products import two of our packages; a re-export closes it (see R3 in the table). | +| F26 | `probeDependency` returns a bare boolean | **Resolved** | `packageManager` added, which is the missing fact for phrasing the install command. Grade corrected — see R13. | + +**Counts:** resolved 14, partial 3, open 4 (three of which are nits), regressed 1. + +## Fresh findings + +### P01 — `SingleChar` rejects every string, so `alias` is unusable — MUST FIX + +**Location:** §5, `export type SingleChar = string & { readonly length?: 1 }` +(line 383), used by all six flag factories. + +**Issue:** TypeScript types the `length` property of any string, including a +one-character literal, as `number` — it does not compute literal lengths. So +no string is assignable to `{ readonly length?: 1 }`. Verified: + +``` +t.ts(4,5): error TS2322: Type 'string' is not assignable to type 'SingleChar'. + Type 'string' is not assignable to type '{ readonly length?: 1 | undefined; }'. +t.ts(5,5): error TS2322: … +``` + +Line 4 is `alias: 'q'`; line 5 is `alias: 'ab'`. Both rejected. + +**Why it matters:** short aliases were round-1 F07, and every shipping CLI in +the corpus has them. As written, no command can declare one — the feature is +not merely unenforced, it is inaccessible. + +**Suggestion:** infer the alias as a type parameter and constrain it with a +template-literal recursion. Verified working — `'q'` compiles, `'ab'` errors, +omitting the field compiles: + +```ts +type Char = S extends `${string}${infer R}` + ? (R extends '' ? S : never) + : never + +// on each factory: +boolean(spec: { brief: string; alias?: A & Char }): FlagSpec +``` + +Alternatively drop the type and validate at construction — but the type +version works, so prefer it. + +### P02 — The outcome code is not typed against its catalogue — MUST FIX + +**Location:** §4, `present`'s `opts?: { readonly outcomeCode?: number }` +(line 275); §6, `outcomeCodes?: Readonly>` (line 451). + +**Issue:** the catalogue is declared on the definition and the selection +happens at the return site, but nothing connects the two at compile time. The +engine verifies at runtime, which means at the return site — after the command +has finished all of its work. + +**Why it matters:** a mistyped or stale outcome code (catalogue edited, call +site not) turns a command that ran correctly into an internal error at the +moment it was about to report success. That is the worst available time to +discover a one-character mistake, and it lands on the exit-code surface CI +branches on. It is also the last remaining place where the v3→v4 move cost +type safety that v2 had. + +**Suggestion:** thread the catalogue's key type. Verified working: + +```ts +// definition gains a fourth parameter, inferred from the catalogue literal: +export interface CommandDefinition { + readonly outcomeCodes?: Readonly> + // … +} +// context carries it; present narrows: +readonly present: (data: T, presentations: Presentations, + opts?: { readonly outcomeCode?: TOutcome }) => PresentedResult +``` + +`defineCommand({ outcomeCodes: { 4: 'drift', 5: 'stale' } })` infers +`TOutcome = 4 | 5`, and the compiler rejects a wrong code at the call site with +a message that names the valid ones: + +``` +error TS2322: Type '7' is not assignable to type '4 | 5 | undefined'. +``` + +That is exactly the error a product author wants. The runtime verification +stays as defence in depth. + +Two smaller points in the same area. The 4–99 range is still unenforced — +worth validating the catalogue's keys at construction, where it is cheap. +And whether an explicit `{ outcomeCode: 0 }` is legal when the catalogue omits +`0` is currently ambiguous; say that 0 is always legal and always means +completed-nominally. + +### P03 — Destructive-prompt safety is a convention where it could be a type + +**Location:** §4a, `PromptSurface` and its doc comment (lines 316–342). + +**Issue:** the rule is that `--yes` resolves a prompt to its declared default, +and that a prompt with no default halts. Safety for destructive operations +therefore rests entirely on the author remembering not to declare a default. +`confirm('Delete the production database?', { default: true })` compiles, and +under `--yes` it deletes without displaying anything. + +**Why it matters:** R5 exists because "convention (style guides, review +comments) demonstrably did not hold the line" on much lower-stakes things than +this. The failure is silent, it is in the blast-radius category rather than +the annoyance category, and it will be introduced by someone adding a default +for a good local reason without knowing the rule it interacts with. + +**Suggestion:** make destructiveness explicit rather than inferred from an +absence. A second method that structurally cannot take a default: + +```ts +readonly confirm: (q: string, opts?: { readonly default?: boolean }) => Promise> +/** Never auto-answered: no default, --yes cannot satisfy it. Pairs with the + * command's own --force / --confirm flag. */ +readonly confirmDestructive: (q: string) => Promise> +``` + +Now "a destructive confirm has no default" is enforced by the signature, and +the call site is self-documenting in review. Whether it is two methods or one +method with a required `destructive: true` is a shape choice — architect +referral — but the property should not stay a convention. + +Secondary, worth one line in the doc: adding an ordinary no-default prompt to +an existing command silently breaks every CI caller passing `-y`, because the +invocation now halts at exit 2. That is the correct behaviour, but it makes +"add a prompt" a breaking change, which is worth saying out loud. + +### P04 — `InputStream` is under-specified and probably wrong for `lsp` + +**Location:** §8, `export interface InputStream extends AsyncIterable {}` +(line 605). + +**Issue:** two unresolved questions. First, chunking: nothing says whether an +iteration yields an arbitrary chunk or a line. Second, encoding: it yields +`string`, so bytes have already been decoded. + +**Why it matters:** `defineRawCommand` exists for `lsp`, and an LSP server +over stdio parses `Content-Length: N\r\n\r\n` followed by exactly N **bytes**. +Decoded strings cannot be counted in bytes once any payload contains a +multi-byte character, so the framing parser cannot be written correctly. And +if the engine were to yield lines rather than chunks, framing breaks outright, +because LSP bodies contain newlines and the header/body boundary is +byte-counted, not line-counted. The one command this escape hatch was built +for may not fit through it. + +**Why it is a real risk rather than theoretical:** the ORM `lsp` command today +hands the process to `connection.listen()` and lets the language-server +library own the raw stdio stream. Under this interface the engine interposes a +decoded string iterator between them. + +**Suggestion:** type raw stdin as `AsyncIterable` — byte-exact and +still runtime-agnostic, since `Uint8Array` is a platform primitive rather than +a Node type. Offer a decoded string view separately if anything wants one. +Either way, state chunk-not-line semantics explicitly. Worth confirming +against the actual language-server entry point during the spike, since it is a +single known consumer and the answer is cheap to obtain. + +The matching question on the output side: `OutputStream.write(text: string): +void` returns nothing, so a raw command cannot know its writes have drained +before it returns an exit code. Because the engine sets `process.exitCode` +rather than calling `process.exit`, the runtime will flush naturally — so this +is fine, but it is fine by accident and deserves a sentence. + +### P05 — Flag defaults still do not narrow the flag's type + +**Location:** §5, `flag.string` / `flag.number` / `flag.enum` (lines 358–380). +Carried over from N05a. + +**Issue:** `flag.number({ brief, default: 900 })` returns +`FlagSpec`. + +**Why it matters:** the handler still writes `?? 900`, so the default is +declared twice and the two copies can drift — help text says one thing, the +handler falls back to another. It also removes the benefit that motivated +adding defaults in round 1. + +**Suggestion:** overloads. Verified working — with `default` present the value +is `number`, without it `number | undefined`: + +```ts +number(spec: { brief: string; placeholder?: string; default: number }): FlagSpec +number(spec: { brief: string; placeholder?: string }): FlagSpec +``` + +### P06 — Sessions have no defined end in json mode + +**Location:** §9, `Frame` and `ResultFrame` (lines 642–659); §6, session +definitions returning `Result`. + +**Issue:** the comment says json mode emits "events while running, then +exactly one result frame". A session has no presentation and no result, so +whether it emits a terminal `ResultFrame` is unstated. + +**Why it matters:** a machine consumer tailing `prisma log tail --json` needs +to know whether the stream ended cleanly, was aborted, or failed. Without a +terminal frame it can only infer that from the pipe closing, which cannot +distinguish a clean stop from a crash. The platform already solved this: its +`build logs` stream carries its own `terminal` record, and it sets +`emitJsonSuccessEvent: false` precisely because the wrapper's success event +would mislabel a failed stream as succeeded. + +**Suggestion:** state that a session also emits exactly one terminal +`ResultFrame` carrying `ok` (and the error when it errored), with no `result`. +That gives every json stream in the CLI the same shape: events, then one +terminal frame. + +### P07 — `ok` now means completed, not succeeded; that is a consumer-visible change + +**Location:** §9, `CompletedEnvelope.ok` (lines 611–615) and the header's +COMPLETED/ERRORED framing. + +**Issue:** an integrity failure in `migration check` is now `ok: true` with +`outcomeCode: 4` and exit 4. R6's stated rationale is that machine consumers +"branch on `ok`, `code`, and exit codes"; the answer to "did the check pass?" +is now `ok && outcomeCode === 0`, a two-field test where every existing +consumer performs a one-field test. + +**Why it matters:** I think the semantics are right — "bad news is a result, +not an error" is a genuinely better model, and it is what lets a completed +result render through the normal presentation path. But it is a change in what +`ok` means, and the class of consumer most likely to get it wrong is an agent +writing the obvious `if (result.ok)`. The mitigation is already in place and +is the reason I am not grading this as a defect: `outcomeCode` is a +**required** field on `CompletedEnvelope`, always present, so the correct test +is always available and never `undefined`. + +**Suggestion:** no interface change. Document the meaning of `ok` explicitly +where consumers will read it (the json contract docs, not only this file), and +note the specific migration: ORM `migration check` consumers branching on `ok` +must move to `outcomeCode`. Worth one line in whatever release note covers the +json surface. + +### P08 — The event buffer has no stated bound or drop policy + +**Location:** §1, "the engine buffers and writes asynchronously (no +backpressure signal — accepted trade)" (lines 96–97). + +**Issue:** accepting the trade is reasonable — a `void` return keeps `report` +trivial to call and the alternative complicates every emit site. But an +unbounded buffer in front of a slow consumer is a memory failure mode, and +`log tail | slow-consumer` is a real invocation. + +**Why it matters:** it is the one remaining way a well-behaved command can +take down the process, and it fails at exactly the moment a user is debugging +something else. + +**Suggestion:** state a bound and what happens at the bound. There is +precedent in the corpus for the honest answer: Composer's `LogEvent` union +already carries `lines-dropped { count }`. Dropping with a visible count is +better than growing without limit, and the vocabulary for saying so exists. + +### P09 — `CommandHandler` still covers only value commands + +**Location:** §6, `CommandHandler` (lines 469–471). Carried over from N09. + +**Issue:** the conditional matches `CommandDefinition` only, so session and +raw implementation files hand-write their handler signatures. + +**Why it matters:** hand-written signatures drifting from their definitions is +exactly what F02 was fixed to prevent; the fix simply was not extended to two +of the three command kinds. + +**Suggestion:** extend the conditional to all three, or ship `SessionHandler` +and `RawHandler` alongside. + +### P10 — `--quiet`, `--log-level` and json mode have unstated interactions + +**Location:** header lines 39–49; §2's materialization table (lines 194–196). + +**Issue:** the table covers `human`, `human + --quiet`, and `json`. It does not +cover `json + --quiet`. Separately, `--quiet` and `--log-level` now overlap: +`--quiet` suppresses presentation, `--log-level error` suppresses commentary, +and `--quiet --log-level verbose` has no stated meaning. + +**Why it matters:** minor, but two people will implement it two ways, and it +is one sentence to prevent. + +**Suggestion:** state that json mode wins over `--quiet`, and that `--quiet` +governs presentation while `--log-level` governs commentary, so the two +compose rather than conflict. + +### P11 — Remaining nits + +Genuinely small; listing them so they are dispositioned rather than lost. + +1. **`CommandSet` and `MountedTree` are the same type.** Both are + `Readonly>`, so the comment's "distinct alias so + the two maps never read as one" is documentation only — a by-name map is + assignable where a by-path map is expected. Branding one would make it real; + leaving it is defensible. +2. **`SectionValidation`'s `ok: true` diagnostics have no stated fate** (N12). + Presumably they render as warnings and join the envelope's `warnings`; say + so, or they will be silently dropped. +3. **The harness does not capture which prompts were asked** (N11 residual). + With prompt defaults now load-bearing for safety, a test cannot assert that + a destructive confirmation was actually displayed — only that something + consumed an answer. +4. **`requiresCredentials` is absent from `RawCommandDefinition`.** Probably + deliberate for `lsp`; worth confirming rather than inheriting by omission. +5. **`createCli` still says "build time, not run time"** (F23) while remaining + a function that can only throw when called. +6. **`LogLevel = Severity`** makes the level axis and the item axis the same + type, so a `Severity` is assignable wherever a `LogLevel` is wanted. Harmless + today; they may diverge later. +7. **`LoadedConfig` still cannot distinguish "no config file" from "config file + present but empty"** (round-2 N12 residual). Both yield `config: undefined` + for a command that needs a section, so the handler produces the same error + for two situations with different fixes. + +## Addendum — the `errors` Block (operator amendment, landed mid-review) + +`Block` gained `{ kind: 'errors'; errors: ReadonlyArray }` +for structured errors carried inside a COMPLETED result, engine-rendered with +the top-level error layout. + +**The intent is right and the rendering half is a clear win.** "Products never +hand-build error presentation" is R5 applied to the one place it had escaped: +`migration check` today hand-formats `✗ [CODE] where: why` plus a `fix:` line, +and the survey ranks structured-error-with-code/why/fix as the single most +uniform structure in the corpus (3/3 families). Having the engine render that +layout in a completed result, identically to how it renders a top-level error, +is exactly the consistency R5 exists for. I would keep the concept. + +The mechanism has one structural problem and one coverage gap. + +### P12 — The same error list must be written two or three times, and the copies cannot be reconciled + +**Location:** §7, `Block.errors` and its doc ("In the data/json side, carry the +same errors as their envelopes (`toEnvelope()`)"). + +**Issue:** `Block` appears only in `Presentations.human`. So the human side +gets `CliStructuredError` instances via the block, and the data side must +carry the same errors again, converted by hand with `toEnvelope()`. Under +`--quiet` — where only `stdout` materializes — they must be written a third +time or they vanish. + +Nothing couples the copies, and **nothing can**: only the active format's +presentation functions run, so in json mode the `human` function is never +invoked and the block never exists. There is no moment at which both lists are +in memory to be compared, by the engine or by a test. A handler that filters, +truncates, or forgets to update one side produces a human reading and a +machine reading that disagree about what the command found, silently and +undetectably. + +Three concrete consequences: + +1. **Forgetting `toEnvelope()` fails quietly rather than loudly.** + `CliStructuredError` extends `Error`, and `message` is non-enumerable on + `Error`, so `JSON.stringify` of a raw instance drops it while keeping the + custom own fields. The output is a plausible-looking payload missing its + summary — not an obvious failure. The instruction to convert is a doc + comment with no type behind it. +2. **It breaks §2's own invariant.** `PresentedResult` is described as "data + all the way down (serializable, snapshotable, no callbacks)". A live + `CliStructuredError` is not plain data: it carries a `cause` chain that can + reference arbitrary objects and a stack. Every other `Block` is strings and + rows. The harness exposes `presented` for "semantic assertions without + byte-scraping" — snapshotting one containing error instances gives unstable + output. +3. **`db verify --quiet` regresses.** That command deliberately renders drift + even under `--quiet` (survey §A1). Under the current materialization rule + `--quiet` builds only `stdout`, so an `errors` block is never constructed + and the drift disappears — unless the handler duplicates it into the stdout + lines as well. + +**Suggestion:** let the engine own the list once, format-independently. Either + +```ts +ctx.present(data, presentations, { outcomeCode: 4, errors: findings }) +``` + +or a format-independent member on `Presentations` (`errors?: () => +readonly CliStructuredError[]`) that the engine always materializes. Either +way the engine renders them in human mode with its layout, serializes their +envelopes into the result envelope itself, and emits them under `--quiet` +according to one stated rule. Declared once, no `toEnvelope()` in product +code, no drift possible, and `PresentedResult` goes back to being plain data. + +Note that the obvious cheaper fix — "the engine scans the human blocks for an +`errors` block and serializes what it finds" — does not work, precisely +because `human` is not invoked in json mode. That asymmetry is the argument +for pulling the declaration out of the presentation functions entirely. + +**Also worth one line:** state whether the rendered error includes `meta`. The +corpus masks credentials in connection URLs deliberately +(`maskConnectionUrl`, `URL_CREDENTIALS_PATTERN`), and a drift or verification +error's `meta` is a plausible place for one to appear. `fields` already has +`sensitive`; the errors block has no equivalent, so the masking policy should +be stated as the engine's. + +### P13 — Completed-with-errors covers the non-zero cases; the ERRORED side cannot express multiple errors + +**Location:** §9, `ErroredEnvelope.error` (singular); §4, `CommandContext.config` +("the engine already failed the command with that section's diagnostics", +plural); §3, `SectionValidation.diagnostics` (an array). + +Working through what products exit non-zero for today: + +| Case | Covered? | +|---|---| +| `migration check` integrity failure (exit 4) | **Yes** — the motivating case, and a good fit: its `failures[{space, code, where, why, fix}]` already has the structured-error shape. | +| `db verify` drift (exit **1** today) | **Yes**, with a deliberate renumbering to an outcome code — exit 1 is now "bug only". Same class as the already-agreed "Compute's 1 renumbers to 2". Needs listing in migration notes; and see P12.3 for the `--quiet` interaction. | +| `db sign` verify failure (exit 1) | Yes, same renumbering. | +| Composer spawned-engine exit-status passthrough | **No.** A child exiting 137 cannot map into 4–99. The settled contract has "one documented exception" for this; v4's exit table does not mention it. Carried-over gap, not caused by this amendment. | +| A failure carrying *many* structured errors | **No** — see below. | + +**Issue:** `ErroredEnvelope.error` is singular. But the engine's own R10 path +is plural: `SectionValidation` returns `diagnostics` as an array, and the +context doc promises the engine fails the command "with that section's +diagnostics". A config file with three invalid fields has three diagnostics +and one envelope slot. + +**Why it matters:** the engine cannot express its own most likely failure. The +workarounds are both bad — pick one diagnostic and drop the rest, or nest the +others in `meta` where no consumer knows to look — and R10's whole point is +that a user's config typo becomes a good diagnostic rather than a stack trace. +Three typos should not become one diagnostic. + +It also creates a perverse incentive now that the completed path renders +errors well: a command with several genuine failures is better off returning +`ok` with an errors block and an outcome code than returning `notOk`, purely +because the completed path can show all of them. That would make `ok` mean +"had something to display" rather than "completed", which undoes P07's +semantics. + +**Suggestion:** allow the errored envelope to carry a list — either +`error` plus an optional `additionalErrors`, or make the field a non-empty +array. The human rendering already exists (it is the same layout the `errors` +block uses), so this is an envelope change rather than a rendering one. + +### P14 — Two ways to show an error in human mode + +**Location:** §7, `Block.summary` with `tone: 'error'` versus `Block.errors`. + +Minor, and I raise it only so it is dispositioned: a product can render an +error condition either as a `summary` block with an error tone or as an +`errors` block. The intent is clearly that `errors` is for structured +`CliStructuredError` values and `summary` is for prose, but nothing says so, +and the corpus's history is that two ways of expressing one thing diverge. +One sentence in the `Block` doc is enough. + +## Deferred + +Unchanged and still correctly out of scope: config-section registration +mechanics beyond the token, daemon management (mode 5), autocomplete, +telemetry hooks, and duration/timing events. Nothing new joined this list in +v4. + +## Acceptance-criteria verification + +Same strict verdicts throughout. **PASS** = the interface structurally +satisfies or enforces the requirement. **WEAK** = satisfiable, but the shape +does not enforce it. **FAIL** = the shape contradicts or cannot express it. +**NOT VERIFIED** = cannot be assessed from the draft. + +| R | Requirement | r1 | r2 | r3 | Detail | +|---|---|---|---|---|---| +| R1 | One language, directly executable | PASS | PASS | **PASS** | Unchanged. The declared object is what runs; `kind` is stamped by `define*` rather than hand-written, which removes a way to get it wrong. I compiled the question this raised — whether `Omit<…, 'kind'>` on the parameter breaks inference of `TFlags` from the `flags` literal — and it does not. | +| R2 | Commands end in typed operation calls | WEAK | WEAK | **WEAK** | Unchanged, and unchangeable from here: the shape is compatible and the thin handler is the natural one, but nothing structurally prevents business logic in a handler. This is a review-and-lint requirement, and I would stop expecting the interface to carry it. | +| R3 | The engine package is the whole contract | WEAK | WEAK | **WEAK** | Substantially improved. `NodeJS.*` is gone from the public surface, replaced by structural `OutputStream`/`InputStream`, which closes the runtime-agnosticism half; and `NextAction` moving to the foundation closes the package cycle. No third-party type appears anywhere, so R3's actual rationale — bounding third-party exposure, keeping internals replaceable — is fully satisfied. The one residual is literal rather than substantive: products still import two of *our* packages, because `Result`, `CliStructuredError` and `NextAction` live in the foundation. Re-exporting them from the engine makes this PASS and costs one line. Whether the engine should re-export or R3's wording should acknowledge the foundation is an architect call. | +| R4 | Products receive a context, never the environment | PASS | PASS | **PASS** | Improved again: `packageManager` gives handlers the one environmental fact they needed for R13 phrasing without reading the environment, and `requiresCredentials` removes the most common reason a handler would reach for anything else. Nothing in the context touches disk, env or TTY. | +| R5 | Products have no presentational API | PASS | PASS | **PASS** | Now exception-free, which it was not in v3. Removing the `exitCode` callback means the header's "nothing product-authored executes after the handler resolves" is literally true. `Block` remains the only vocabulary, `Ui` cannot write, and the `--verbose` question was answered by a log-level mechanism rather than a second presentation surface — so no new rendering authority reached products. | +| R6 | Errors and results follow the settled conventions | FAIL | WEAK | **WEAK** | Close to PASS and blocked on one thing. Everything expressible is now correct: the full exit-code table including 4–99 and 130/143, second-signal force-exit, prompt cancellation mapped to 3 versus unavailability to 2, `Result` throughout, and an outcome catalogue that renders in help without executing anything. Two things keep it WEAK. First, the code selected at the return site is typed `number` rather than against that catalogue, so validity is enforced at runtime after the work is done (**P02**); I verified the typed version compiles and produces `Type '7' is not assignable to type '4 \| 5 \| undefined'`. Second, the mid-review `errors` Block amendment exposed that `ErroredEnvelope.error` is singular while the engine's own config-diagnostics path is plural, so a multi-error failure cannot be expressed on the errored side (**P13**). Both are envelope/type edits rather than shape changes; fixing them moves R6 to PASS. | +| R7 | Product-repo end-to-end tests are first-class | WEAK | PASS | **PASS** | Held and improved: `cwd` and the `onEvent` live tap close the two residuals that mattered, so a session test can assert mid-run state and abort on a condition rather than a timer. Remaining gap is prompt capture (P11.3), which narrows what a test can assert about the new prompt-default safety rule but does not affect which commands are testable. | +| R8 | The shell's test burden is integration proof | NOT VERIFIED | NOT VERIFIED | **NOT VERIFIED** | Still an allocation-of-work requirement with no interface surface. Nothing obstructs it; assess against the shell's test plan. | +| R9 | Static tree, lazy guts | PASS | PASS | **PASS** | The lazy `handler` is unchanged. The one leak I flagged in round 2 — validators loading with the definition tree — is now documented at the point of use ("keep validators dependency-light"). Convention rather than structure, but stated where an author will read it, and the `outcomeCodes` catalogue being plain data means help still renders without executing product code. | +| R10 | One config file, validated by its products, never a crash | FAIL | PASS | **PASS** | Held. The `ConfigSection` token still couples name, type and never-throwing validator; `configSection` still binds a command to exactly one section, which is what makes "fails only if a section it needs is invalid" real. Residual nits only: the fate of non-fatal diagnostics on the success branch, and no-file versus empty-file being indistinguishable (P11.2, P11.7). | +| R11 | Pinned versions, tandem releases | NOT VERIFIED | NOT VERIFIED | **NOT VERIFIED** | Release-process requirement, no interface surface. | +| R12 | The shell defines the command tree | PASS | PASS | **PASS** | Held. No path appears in any definition; `MountedTree` names the by-path map at the mount site. The two map aliases being structurally identical (P11.1) is a documentation-versus-type nit, not a requirement gap. | +| R13 | The CLI never touches a package manager | WEAK | WEAK | **PASS** | Upgraded, partly on v4's change and partly correcting my own round-2 reading. The prohibition holds absolutely — nothing installs or vendors. R13's positive half asks that *the command* check at execution time and return a structured error naming the dependency and how to install it with the user's own package manager. `probeDependency` provides the check and `packageManager` (new in v4) provides the last missing fact for the install phrasing, so a product can now satisfy the requirement exactly as worded. My round-2 WEAK penalised product-authored error text, which is what R13 specifies rather than a deviation from it. | +| R14 | One event vocabulary, engine-defined, with product extensions | PASS | PASS | **PASS** | Held and tidied. The `Frame` union with a distinct `ResultFrame` removes the double-`data` awkwardness and makes the json stream self-describing. The vocabulary itself is unchanged from v3's already-strong state: nesting ids, data/diagnostic channel routing, `artifact`, `from`, one severity scale, and `data?: unknown` as the uniform untouched extension point. One unstated case: whether a session emits a terminal frame (**P06**). | + +### Summary counts + +| Verdict | r1 | r2 | r3 | Requirements (r3) | +|---|---|---|---|---| +| PASS | 6 | 8 | **9** | R1, R4, R5, R7, R9, R10, R12, R13, R14 | +| WEAK | 4 | 4 | **3** | R2, R3, R6 | +| FAIL | 2 | 0 | **0** | — | +| NOT VERIFIED | 2 | 2 | **2** | R8, R11 | +| **Total** | 14 | 14 | **14** | | + +Movement since round 2: R13 WEAK → PASS. No regressions. + +Of the three remaining WEAKs, one is closable by a verified edit (R6, via +P02), one is closable by a re-export or a wording decision (R3), and one is +not an interface property at all (R2). There is no requirement left that this +interface cannot express. + +## Verdict + +Not clean yet, and I want to be precise about the gap rather than round it in +either direction. + +**Two must-fix defects:** P01 (`SingleChar` rejects every string — the alias +feature is inaccessible) and P02 (the outcome code is not typed against its +catalogue, so a wrong code fails at runtime after the command has done its +work). Both are single-declaration edits, both fixes are compiled and verified +in this review, and neither changes a shape. + +**One safety property I would make structural before shipping:** P03, the +destructive-prompt convention. + +**One contract to settle against its only consumer:** P04, `InputStream` for +`lsp`. + +**One amendment to finish landing:** the `errors` Block is the right idea with +the wrong placement. Declared inside `Presentations.human`, the same error list +has to be written two or three times and the copies cannot be reconciled — +in json mode the human function never runs, so nothing can cross-check them +(P12). Moving the declaration out of the presentation functions, so the engine +renders *and* serialises one list, fixes it without giving up anything the +amendment was after. P13 is its companion on the errored side: the envelope +holds one error while the engine's own config path produces several. + +Everything else — P05 through P11 and P14 — is small, and P07 needs +documentation rather than an interface change. + +**The design is settled.** Rounds 1 and 2 found structural problems; round 3 +found two broken type declarations, a safety convention, and one misplaced +amendment. If P01, P02 and P12 are fixed and P03, P04 and P13 are ruled on, I +would sign this off without another review pass. diff --git a/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r4-closure.md b/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r4-closure.md new file mode 100644 index 00000000..5293a4c7 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r4-closure.md @@ -0,0 +1,163 @@ +# Round 4 — closure check (v5) + +Reviewer pass: principal engineer. Scope: compile-verify the two type +mechanics, read the four closure changes for regressions, state whether the +loop closes. Prior artifacts: `code-review.md`, `-r2.md`, `-r3.md`. + +## Verdict + +**Both compile-checks pass. No regressions found. The loop is closed from my +lens.** + +Every must-fix and should-rule-on item from round 3 is resolved, and two of +them are resolved better than I proposed. What remains is four small items I +had already graded as nits or documentation, listed at the end so they are +dispositioned rather than lost. None of them needs another review pass. + +## (a) Alias mechanics — PASS + +Tested against v5's declarations verbatim (`Char` plus +`alias?: A & Char` with `A extends string = never` on each builder). +TypeScript 5.6, `--strict`. Empty output — every assertion held: + +| Case | Expected | Result | +|---|---|---| +| `flag.boolean({ brief, alias: 'f' })` | compiles | ✓ | +| `flag.string({ brief, alias: 'q' })` | compiles | ✓ | +| `flag.boolean({ brief })` — no alias, `never` default | compiles | ✓ | +| `flag.enum({ brief, values: ['a','b'], alias: 'F' })` | compiles | ✓ | +| `alias: 'ab'` | rejected | ✓ | +| `alias: ''` | rejected | ✓ | +| `alias: 'data-proxy'` | rejected | ✓ | +| `FlagSpec<'a' \| 'b' \| undefined>` from the enum | inference survives the added `A` parameter | ✓ | + +The two cases I specifically wanted to confirm both hold: **omitting the alias +compiles** (the `= never` default does not poison the call, which was the +failure mode of my own first attempt in round 3), and **the empty string is +rejected** (`Char<''>` is `never`, since `''` does not match +`` `${string}${infer Rest}` ``). P01 is closed. + +## (b) `TCode` threading — PASS + +Tested through the realistic round trip, not just the direct call: catalogue on +the definition → `Omit<…, 'kind'>` in `defineCommand` → `CommandHandler` in a separate handler file → `ctx.present`. Empty output: + +| Case | Expected | Result | +|---|---|---| +| `outcomeCodes: { 4: '…', 5: '…' }` infers `TCode` | `4 \| 5`, both directions | ✓ | +| `ctx.present(…, { outcomeCode: 4 })` / `5` | compiles | ✓ | +| `ctx.present(…)` with no opts | compiles (omitted = 0) | ✓ | +| `ctx.present(…, { diagnostics: […], outcomeCode: 4 })` | compiles | ✓ | +| `ctx.present(…, { outcomeCode: 7 })` | rejected | ✓ | +| No catalogue declared → any `outcomeCode` | rejected (`TCode = never`) | ✓ | + +Inference survives both the `Omit<>` wrapper and the second inference site +created by `handler`'s mention of `TCode`, which was the thing worth checking. +The no-catalogue case falling out as `never` is a bonus: a command that never +declared outcome codes cannot select one, so the catalogue is not merely +advisory. + +**Control run** to prove the negative assertions are load-bearing rather than +vacuous: swapping the invalid `7` for a valid `5` makes the compiler report +`error TS2578: Unused '@ts-expect-error' directive` — confirming that in the +real test `outcomeCode: 7` genuinely errored. P02 is closed. + +## Regression read of the other closure changes + +**P12/C8 — diagnostics declared once at `ctx.present`.** Clean, and it fixes +more than I asked. `Block`'s `errors` member is gone (0 occurrences), +`PresentedResult.diagnostics` is the single declaration, and the engine both +renders them in human mode and serializes their envelopes into +`CompletedEnvelope.diagnostics`. The drift I flagged is now structurally +impossible rather than merely discouraged, and `PresentedResult` is plain data +again except for the diagnostics themselves, which the engine converts. The +`--quiet` regression I identified (`db verify` deliberately shows drift even +when quiet) is explicitly handled — "shown even under --quiet". The +`notOk`-versus-diagnostics test in the doc ("notOk when the command couldn't do +its job; diagnostics when finding these WAS the job") is the right line and is +stated where an author will read it. + +One observation, not a defect: the guardrail — a severity-`error` diagnostic +requires a non-zero outcome code — is a runtime check firing at the return +site. That is the same late timing as the old P02, but unlike P02 it cannot be +typed, because it depends on the severity of runtime error values. A runtime +check is correct here. Worth making the engine's message for it explicit about +which of the two fixes the author wants (raise the outcome code, or move the +finding to `notOk`), since it fires after the work is done. + +**P13 — errored diagnostics.** Symmetric with the completed side, `error` +retained as the primary. The engine can now express its own R10 failure: three +config typos serialize as three diagnostics rather than one flattened error. +The perverse incentive I flagged — returning `ok` purely to display several +problems — is gone, so `ok` keeps meaning "completed". + +**P03 — `prompt.consent`.** Structurally undefaultable: no `opts` parameter +exists, so `--yes`, Enter-through and non-interactive contexts cannot satisfy +it. The operator's reframe from "destructive" to "explicit consent" is the +better framing — the property that matters is that the answer is not +inferable, which is broader than damage and easier to apply correctly at a +call site. `confirm` keeps its default for ordinary questions, so the two are +distinguishable by name at the point of use. + +**P04 — `InputStream`.** `AsyncIterable` with an optional +`setRawMode`. Byte-exact, so `lsp`'s `Content-Length` framing can be +implemented correctly; decoding is explicitly the consumer's business; and +`setRawMode` being optional is right, since it is a platform capability rather +than a guarantee. Note the engine's own prompt machinery decodes internally, +which keeps the byte-level type from leaking into the prompt surface. + +**ADR 239 amendment.** Recording it in the header as an implementation +prerequisite is the right call — it is the one change here that lands outside +this repo, and the promise analogy states the model more clearly than the +prose around it did. Worth carrying that sentence into the ADR itself. + +## Still open (nits, no further review needed) + +Listing these only so the closure is honest about what was not touched. All +were graded nit or documentation in round 3. + +1. **P05 — flag defaults still do not narrow.** `flag.string({ default })` + still returns `FlagSpec` (no overloads present), so a + defaulted flag keeps its `?? default` in the handler and the value is + declared twice. Verified fix is in `code-review-r3.md`; worth taking when + someone is next in the file. +2. **P06 — sessions have no stated terminal frame** in json mode, so a machine + consumer cannot distinguish a clean stop from a crash except by the pipe + closing. One sentence. +3. **P09 — `CommandHandler` still matches only `CommandDefinition`**, so + session and raw implementation files hand-write their signatures. +4. **P10 — `json + --quiet` precedence** still unstated; the materialization + table covers `human + --quiet` only. + +Plus the round-3 P11 nit list (the two map aliases being structurally +identical, the fate of `ok: true` section diagnostics, prompt capture in the +harness, `requiresCredentials` absent on raw, `createCli`'s "build time" +wording, `LogLevel = Severity`, no-file versus empty-file in `LoadedConfig`). + +## Acceptance criteria + +Unchanged from round 3 except R6, which the two closures resolve. + +| Verdict | r3 | r4 | Requirements | +|---|---|---|---| +| PASS | 9 | **10** | R1, R4, R5, R6, R7, R9, R10, R12, R13, R14 | +| WEAK | 3 | **2** | R2, R3 | +| FAIL | 0 | **0** | — | +| NOT VERIFIED | 2 | **2** | R8, R11 | + +**R6 WEAK → PASS.** Both reasons it was held are gone: the outcome code is now +typed against its catalogue (compile-verified above), and the errored envelope +carries multiple diagnostics, so the engine can express its own config-failure +case. The exit-code table, prompt cancellation mapping, and second-signal +behaviour were already in place. + +The two remaining WEAKs are the same as before and neither is an interface +defect: **R2** is a review-and-lint property no interface can carry, and **R3** +is the one-line question of whether the engine re-exports the foundation's +`Result` / `CliStructuredError` / `NextAction` so products import one package +instead of two — an architect call, not an engineering gap. **R8** and **R11** +remain process requirements with no interface surface. + +No requirement remains that this interface cannot express, and no requirement +is contradicted by its shape. diff --git a/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r5-delta.md b/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r5-delta.md new file mode 100644 index 00000000..7f6631a1 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r5-delta.md @@ -0,0 +1,164 @@ +# Round 5 — v7 delta check + +Reviewer pass: principal engineer. Scope: compile-verify the three type +mechanics, regression-read the restructuring, confirm closure still holds. +Prior artifacts: `code-review.md`, `-r2.md`, `-r3.md`, `-r4-closure.md`. + +## Verdict + +**All three mechanics compile as specified, in both directions, with controls. +No regressions. Closure holds.** + +Two of the v7 changes make the interface meaningfully stronger than v5, not +merely equivalent: the `Outcome` conditional turns "did you pick an exit code?" +into a compile error at every return site of a catalogued command, and +`requireDependency` moves the missing-dependency prose from products into the +engine. Both close things I had accepted as convention or as product +responsibility in earlier rounds. + +## (1) `Outcome` — PASS, both directions + +Tested verbatim through the full round trip (`defineCommand` inference → +`CommandHandler` → `ctx.present`). TypeScript 5.6, `--strict`. +Empty output; every assertion held. + +Catalogued command (`exitCodes: { 4: '…', 5: '…' }`): + +| Case | Expected | Result | +|---|---|---| +| `TCode` inferred | `4 \| 5`, both directions | ✓ | +| `present({ data, exitCode: 4 })` / `5` / `0` | compiles | ✓ | +| `present({ data, exitCode: 5, diagnostics: […] })` | compiles | ✓ | +| `present({ data })` — **exitCode omitted** | **rejected** | ✓ | +| `present({ data, exitCode: 7 })` | rejected | ✓ | + +Uncatalogued command (no `exitCodes`): + +| Case | Expected | Result | +|---|---|---| +| `present({ data })` | compiles | ✓ | +| `present({ data, diagnostics: [] })` | compiles | ✓ | +| `present({ data, exitCode: 4 })` | **rejected** | ✓ | + +**Controls.** All three negative assertions were re-run with the offending +value corrected, and each produced `error TS2578: Unused '@ts-expect-error' +directive` — proving the assertions are load-bearing rather than vacuously +passing: + +``` +cA.ts(82,3): error TS2578: Unused '@ts-expect-error' directive. # exitCode supplied → required-ness assertion was real +cB.ts(84,3): error TS2578: Unused '@ts-expect-error' directive. # 7→5 → out-of-catalogue assertion was real +cC.ts(100,3): error TS2578: Unused '@ts-expect-error' directive. # exitCode dropped → forbidden-ness assertion was real +``` + +The `[TCode] extends [never]` guard behaves correctly for an inferred union +(`4 | 5` takes the required branch) and for the default (`never` takes the +forbidden branch). This is the strongest form of the P02 fix: v5 made a wrong +code a compile error; v7 makes a *missing* code one too, so a command that +documents outcomes cannot silently exit 0 from a path the author forgot about. + +## (2) `TConfig` through the grouped `needs` — PASS + +`needs: { config: checkSection }` with `ConfigSection` still flows to +`ctx.config`. Both `const cfg: CheckCfg = ctx.config` and +`ctx.config.strict` typecheck. Nesting the inference site one level deeper +inside an optional property does not break it. + +## (3) `ctx.config: TConfig` exactly — PASS + +The no-config case types correctly: with no `needs.config`, `TConfig` defaults +to `undefined` and `const c: undefined = ctx.config` compiles. So dropping +`| undefined` does not strand commands that need no config. + +Worth stating plainly, because it is a real shift in responsibility: absence is +now the validator's to model. A product whose section is genuinely optional +must type it as `T | undefined` and have its validator return that for absent +input. That is the right owner — the product knows whether absence is legal — +but it is a rule that lives only in the doc comment, and a validator typed `T` +that receives `undefined` will produce a confusing failure. One sentence in the +`ConfigSection` docs about the validator's input including `undefined` would +close it. Nit. + +## (4) Regression read + +**`help` / `args` / `needs` grouping.** Applied consistently across all three +kinds — `CommandDefinition`, `SessionCommandDefinition`, `ServerCommandDefinition` +each carry `help: HelpSpec`, `args?: ArgsSpec`, `needs?: NeedsSpec`. The +discriminants (`result-command`, `session-command`, `server-command`) are +intact and stamped by the `define*` functions via `Omit<…, 'kind'>`, so N03 +stays closed. `raw` → `server-command` is a rename; naming is the architect's. + +**`ProductManifest` + `createCli(products)`.** Good structural gain: the +manifest ties a product's config section to its commands, and the doc says +foreign-section references fail construction. That makes R10's "a command only +needs its own product's section" checkable at build time rather than trusted. +Note the association between a `MountedTree` entry and its manifest entry is a +construction-time check, not a type-level one — consistent with the existing +posture on collisions and grammar, so no new concern. + +**`requireDependency` replacing `probeDependency`.** This is the best change in +v7. The engine now returns its own structured missing-dependency error with the +install command phrased from the detected package manager, and the handler just +passes it to `notOk`. `packageManager` correctly disappears from +`CommandContext` (it survives only on `Runtime` and the test spec), because +products no longer need it. My round-2 F26 residual — every product authoring +its own install prose, drifting per product — is now structurally impossible. +R13 still holds: the *command* returns the error, as the requirement words it; +only the wording moved to the engine. + +**`Credentials` trimmed to `{ token }`.** No regression — nothing in the +interface consumed `workspaceId`, and treating workspace selection as session +state owned by the auth library is coherent. + +**`PresentedResult.exitCode` / `diagnostics` now required.** Correct: the value +is engine-constructed, so both are always populated, and test code reading +`presented` no longer handles `undefined`. The optionality that remains is on +the *input* (`Outcome`), which is where it belongs. + +**`Diagnostic` as a distinct foundation type.** Pure data, never thrown, no +stack — this resolves the round-3 P12 concern about live `Error` instances +crossing the boundary more cleanly than v5 did. `PresentedResult` is now plain +data throughout, so harness snapshots are stable. + +**`NeedsSpec.interaction`.** The comment explaining why this is a mechanical +precondition and deliberately *not* an agent barrier — "the client's nature is +unverifiable, and a flag claiming to exclude agents would be a false guarantee" +— is exactly right, and worth keeping verbatim; it forecloses a bad feature +request permanently. + +One nit: `NeedsSpec` is shared across all three kinds, so a +`ServerCommandDefinition` can declare `needs.interaction` even though prompts +do not apply to server commands. Harmless, unenforceable in the current shape, +and not worth restructuring for. + +## Still open + +Unchanged from the round-4 list, all previously graded nit or documentation: +P05 (flag defaults do not narrow — verified fix on file), P06 (no stated +terminal frame for sessions in json mode), P09 (`CommandHandler` matches +only `CommandDefinition`, so session and server impl files hand-write their +signatures), P10 (`json + --quiet` precedence), plus the P11 nit list. Two +small additions from this round: the validator-input-includes-`undefined` +sentence above, and `NeedsSpec.interaction` on server commands. + +## Acceptance criteria + +Unchanged from round 4. + +| Verdict | Count | Requirements | +|---|---|---| +| PASS | 10 | R1, R4, R5, R6, R7, R9, R10, R12, R13, R14 | +| WEAK | 2 | R2, R3 | +| FAIL | 0 | — | +| NOT VERIFIED | 2 | R8, R11 | + +R6 and R13 both strengthened within their existing PASS — R6 because a missing +exit code is now a compile error, R13 because the install prose moved to the +engine. R10 strengthened via manifest-checked section ownership. The two +WEAKs are the same non-defects as before: **R2** is a review-and-lint property +no interface can carry, and **R3** is the one-line question of whether the +engine re-exports the foundation types (now four: `CliStructuredError`, +`Result`, `NextAction`, `Diagnostic`) so products import one package rather +than two — an architect call. + +Closure holds. No further review pass needed from my lens. diff --git a/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review.md b/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review.md new file mode 100644 index 00000000..367f54b6 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review.md @@ -0,0 +1,659 @@ +# Code review — unified CLI engine public interface (draft) + +Reviewer pass: principal engineer (failure modes, operability, blast radius, +cost vs. benefit, constraints vs. assumptions). Naming, typology and overall +system shape are the architect's pass; where I hit one of those I say so and +move on. + +Subject: `wip/designs/engine/engine-interface-draft.ts` (snapshot 2026-08-09). + +Sources read in full: `cli-engine-requirements.md` (R1–R14), +`output-modes-survey.md`, `stricli-vs-clipanion.md`, the platform CLI shell +(`command-runner.ts`, `output.ts`, `runtime.ts`, `prompt.ts`, +`global-flags.ts`, `errors.ts`, `next-actions.ts`), Composer's +`operations/dev.ts`, `operations/shared.ts`, `dev/run-dev.ts`, and +`wip/designs/1a/design.md` (the host–product contract, whose §7.6 exit-code +table I treat as settled). + +## Summary + +The execution protocol at the centre of this draft is the right one. "Events +while running, one `Result` at the end, presenters turn the `Result` into +bytes" is a direct generalisation of the platform CLI's proven presenter +choke point, and it absorbs the ORM's progress spans and Composer's typed +event unions without contorting either. The `Block`/`Ui` pair genuinely makes +rendering impossible from a product, which is what R5 asks for. The lazy +`handler` loader is a one-to-one fit with stricli's `loader`. That core is +sound and I would build on it. + +The draft is not yet buildable as written, for three separate classes of +reason. + +First, the TypeScript does not compile in the ways it needs to. `ArgsOf` +silently drops all positionals because `positionals` is optional; a concrete +`CommandDefinition` is not assignable to the bare `CommandDefinition` that +`CommandSet` and `createCli` require; the brand symbols are unexported, which +breaks declaration emit; and typing a lazily loaded handler against its own +definition is circular. These are findings F01–F05 and they all have the same +fix: make `ArgsOf` take the flags and positionals objects rather than the +whole definition, and introduce an erased command type for mounting. + +Second, several things the shipping CLIs demonstrably do cannot be said in +this vocabulary at all. There is no number flag and no flag default, so +`app domain wait --timeout 15m` has to parse its own string, which pushes +parse-time validation into handlers and straight past R6. There are no short +aliases, so `-q`, `-y`, `-v`, `-f` all disappear. Exit codes are stated as +0/1/2/3 while the settled contract table is 0/1/2/3/4–99/130/143, and +`migration check` already ships a 4 and the platform already ships a 130. +`Runtime` has no `env` and no `isTty.stdout`, so the engine cannot implement +its own CI detection or the deliberately-kept auto-JSON-on-non-TTY behaviour +without reaching around the injection seam — which is exactly what makes R7's +in-repo tests trustworthy. The success envelope slots the platform puts on +every command (`warnings`, `nextSteps`, `nextActions`) have nowhere to live, +and remediation is the second most recurring structure in the survey. + +Third, the streaming half of the protocol is under-specified in the place +where it matters operationally. `prisma app logs` and `build logs --follow` +exist to have their output piped. The draft never says which stream an event +renders to, and the only candidate concept — the `output` event — uses +`stream` to mean the *child's* stream, not ours. If events render to stderr +(which the settled stream discipline implies), `prisma app logs > file` +produces an empty file. That is a shipped-behaviour regression hiding in an +unstated default. + +Underneath those, two design questions are genuinely open rather than +oversights: R10's config-section registration is deliberately absent and the +current `LoadedConfig` shape cannot support the "fails only if a section it +needs is invalid" rule without it (F16); and the test harness has no way to +abort a run, which makes every session command — `dev`, `app logs`, +`build logs --follow` — untestable through the harness that R7 says is the +evidence (F19). + +Pressure-test verdicts in short: `migration list` maps cleanly. `app deploy` +maps with one gap (two result shapes need a union `TResult`, which works, but +the SDK's `onStatusChange` has no matching event beyond `status`, which is +fine). Composer `dev` maps for events and `--fresh` but not for lifetime: the +handler must itself await the signal and then return a `Result` whose value is +a session that no longer exists, and `present.human` is asked to render it +(F12). `lsp` cannot be declared at all, because `raw` claims nothing else is +declared while the type still requires `flags` and `present` (F13). + +## What looks solid + +- **The one protocol.** Events during, `Result` at the end, presenters after. + It is the platform CLI's `writeCommandSuccess` generalised, and it is the + only thing in the corpus that can serve all three families. +- **`present` as a triple.** `human` to stderr, `stdout` for the machine + payload, `json` for the envelope projection is exactly the platform's + proven `renderHuman`/`renderStdout`/`renderJson`, including the property + that `--quiet` leaves a clean pipe. `bucket key create`'s secret-to-stdout + case survives this design unchanged. +- **`Block` and `Ui`.** There is no `print`, no colour function that writes, + no exit. A product that wanted to diverge could not. `fields.sensitive` + carries the secret-masking the platform built into its UI layer. +- **Lazy `handler`.** A direct match for stricli's `loader`, and it keeps + Composer's import-time crash history classified rather than fatal at + startup (R9's actual motivation). +- **Path-free commands, mounting-side tree.** `CommandSet` has no paths and + `createCli` supplies them. That is R12 structurally, and it is also what + lets a product mount a command anywhere in its own e2e tests. +- **`data?: unknown` on every event.** The R14 extension mechanism is + present on every variant, uniformly, with the engine explicitly not + interpreting it. This is the right shape. +- **Prompts return `Result` rather than throwing.** The platform's + prompt layer throws a `usageError` and then string-matches its own summary + to detect cancellation (`isPromptCancelError`, prompt.ts:98-100). Returning + a value is strictly better. + +## Findings + +### F01 — `ArgsOf` silently drops every positional + +**Location:** §3, `ArgsOf`, lines 182–188; `positionals?: TPositionals` at line 208. + +**Issue:** `positionals` is declared optional, so `D['positionals']` has type +`TPositionals | undefined`. `keyof` over a union is the intersection of the +members' keys, and `keyof undefined` is `never`, so the second half of the +intersection evaluates to `{}`. Every positional vanishes from the handler's +argument type, with no error anywhere. + +**Why it matters:** `app deploy `, `database show `, +`app domain wait ` — most of the corpus takes positionals. Handlers +would read `args.entry` and get a compile error, or worse, authors would add +an index signature to make it go away. + +**Suggestion:** stop parameterising `ArgsOf` on the definition. Make it +`ArgsOf` taking the two objects directly, defaulting +`TPositionals` to `{}`. That also fixes F02 and F04. + +### F02 — Typing a lazily loaded handler against its own definition is circular + +**Location:** §4, `handler`, lines 214–219. + +**Issue:** the handler lives in a separate module (that is the point of the +loader). To type its `args` parameter the author must write something like +`ArgsOf` — but `myCommand`'s own type depends on the +handler's type, which depends on `myCommand`. TypeScript will resolve this to +`any` or report a circularity, depending on how it is written. + +**Why it matters:** the only escape is to hand-write the args type in the +handler module, which then drifts from the flag declarations silently. That +is precisely the "separate data structure a translator interprets" that R1 +exists to prevent, reintroduced at the file boundary. + +**Suggestion:** with F01's fix, authors declare `const flags = {...}` once, +export it, and both the definition and the handler refer to +`ArgsOf`. No cycle. Consider shipping a +`Handler` alias so the handler +module has one thing to import. + +### F03 — The brand symbols are not exported, so declaration emit fails + +**Location:** §3, `declare const FLAG: unique symbol` (line 171) and +`declare const POSITIONAL: unique symbol` (line 178). + +**Issue:** `FlagSpec` and `PositionalSpec` are exported interfaces whose only +member is keyed by a non-exported `unique symbol`. Emitting `.d.ts` for this +package produces TS4033/TS4023 ("has or is using private name"). + +**Why it matters:** the engine package ships types; this is a hard build +failure the moment `declaration: true` is on. + +**Suggestion:** export the symbol declarations (they can stay undocumented), +or brand with a `declare const brand: unique symbol` exported from an +`internal` entry point that the public types reference. + +### F04 — A concrete command is not assignable to the bare `CommandDefinition` + +**Location:** §6, `CommandSet` (line 276) and `createCli`'s +`commands: Readonly>` (line 287); §7, +`createTestCli` (line 323). + +**Issue:** `CommandDefinition`'s defaults make `present.human` have signature +`(value: unknown, ui: Ui) => Block[]`. Under `strictFunctionTypes`, assigning +a command whose `present.human` takes `(value: MigrationList, ui)` requires +`unknown` to be assignable to `MigrationList`, which it is not. The same +argument applies to `handler`'s `args` parameter. So the very commands +`createCli` is supposed to receive will be rejected by it. + +**Why it matters:** this is not a corner case — it is the primary call site. +Every product's `CommandSet` export and every `createCli` call breaks, and +the usual fix people reach for (`as any`) erases the whole type story. + +**Suggestion:** introduce an explicitly erased mount-side type — e.g. +`AnyCommandDefinition = CommandDefinition` (or a +deliberately opaque `MountableCommand` the engine produces) — and type +`CommandSet`, `createCli` and `createTestCli` against that. The precise +definition stays on the authoring side where inference matters. + +### F05 — `TConfig` is not inferable, so `ctx.config` will be `unknown` in practice + +**Location:** §4, `CommandDefinition`'s `TConfig` parameter (line 198) and its +single use inside `handler`'s `ctx` parameter (line 217). + +**Issue:** `TConfig` appears only in a contravariant position inside a lazily +imported module's function parameter. `defineCommand` has nothing to infer it +from unless the handler module explicitly annotates its `ctx` parameter, +which is the thing the circularity in F02 makes awkward. + +**Why it matters:** in the common case every handler gets +`CommandContext` and casts. That defeats the point of typing the +config section at all. + +**Suggestion:** make the config section an explicit, declared part of the +definition rather than a free type parameter — see F16, which needs a runtime +section name anyway. One field solves both. + +### F06 — No number flag and no defaults, so parse-time validation leaks into handlers + +**Location:** §3, the `flag` object, lines 158–169. + +**Issue:** the vocabulary is string, requiredString, boolean, enum, repeated, +json. There is no number, no duration, and no `default` on any of them. + +**Why it matters:** `app domain wait --timeout` (default 15m, with `0` meaning +"probe once") and the SDK's `timeoutSeconds`/`pollIntervalMs` are real, +shipping, and cannot be declared here. The handler ends up parsing and +validating the string, which means the failure surfaces as a handler error +after routing rather than as the typed parse error R6 wants — and stricli's +own scanner errors, the thing the framework evaluation praised most (criterion +6), go unused for this class of flag. + +**Suggestion:** add `flag.number({ brief, default?, min?, max? })` and a +`default` option to `string`/`enum`/`boolean`. Defaults also remove the +`?? fallback` noise from every handler. + +### F07 — No short aliases + +**Location:** §3, the `flag` object. + +**Issue:** nothing declares `-q`, `-y`, `-v`, `-f`, `-n`. Stricli supports +exactly one-character aliases (per the evaluation, weakness 5); the draft +does not expose that capability. + +**Why it matters:** every shipping CLI in the corpus has them +(`global-flags.ts:13-21`). Dropping them is a user-visible regression, and +it will be discovered after the vocabulary is frozen. + +**Suggestion:** add `short?: string` to the flag specs and validate it is one +character at build time. Note that stricli cannot do long aliases, so +deprecation renames need the engine's own hidden-second-flag trick — worth +recording now. + +### F08 — Positionals are too thin: no number, no enum, no variadic + +**Location:** §3, `positional`, lines 174–177. + +**Issue:** only `string` and `optionalString`. + +**Why it matters:** variadic positionals appear in the corpus (multi-argument +commands) and enum positionals would let the "did you mean" machinery stricli +already computes apply to subjects, not just flags. + +**Suggestion:** add `rest()` (variadic) at minimum; number and enum if the +tree needs them. Flag anything not added as a deliberate exclusion so it does +not get re-litigated. + +### F09 — Flag and positional names can collide silently + +**Location:** §3, `ArgsOf`'s intersection, lines 182–188. + +**Issue:** `ArgsOf` intersects the flag keyspace with the positional keyspace. +A flag named `name` and a positional named `name` produce +`string & (string | undefined)` — one merged key, no error, and at runtime one +value overwrites the other depending on the engine's merge order. + +**Why it matters:** it is a quiet correctness bug, and the merge order is an +engine implementation detail no author will know. + +**Suggestion:** either detect the collision at the type level (a conditional +type resolving to a `never`-valued error field is enough to make it a compile +error) or separate the keyspaces (`args.flags.x` / `args.positionals.y`). +Which of those is right is a shape question — architect referral. + +### F10 — Exit codes 0/1/2/3 contradict the settled table + +**Location:** §6, `Cli.run` doc comment, lines 292–296. + +**Issue:** the settled host–product contract (`wip/designs/1a/design.md` §7.6) +is `0` ok, `1` bug, `2` expected failure, `3` user declined, `130`/`143` +signals, `4`–`99` command-specific outcome codes, plus one documented +passthrough exception for spawned-engine exit statuses. The draft names four +codes and gives a command no way to express any of the others. + +**Why it matters:** this is not hypothetical. `migration check` already exits +`4` on integrity failure via `exitOverride` +(`migration-check/exit-codes.ts:1-3`), the platform's `commandCanceledError` +already exits `130` (`errors.ts:129-139`), and Composer already passes an +alchemy child's status through (`render-error.ts:27-37`). Machine consumers +branching on exit codes is a stated R6 goal; a code space the engine cannot +express is a code space each product will re-invent behind the engine's back. + +**Suggestion:** state that `CliStructuredError` carries an optional +`exitCode` in the 4–99 range (the platform's `CliError.exitCode` already +does), define how signal-driven termination reaches 130/143 (see F17), and +name the child-passthrough exception explicitly. + +### F11 — Events have no defined stream, and the streaming-data case has no home + +**Location:** §1, the `output` event (lines 69–75); §2, `report` (line 117). + +**Issue:** the draft never says where events render in human mode. The +`output` event's `stream` field describes the *child's* stream, not ours, +so there is no way to say "this line is the data the user asked for and +belongs on our stdout". + +**Why it matters:** `prisma app logs`, `build logs --follow`, and +`app run` exist to be piped. Today `build logs` routes NDJSON records to +stdout or stderr by `source`/`level` (`controllers/build.ts:34-150`). If the +engine renders all events to stderr — which the settled stream discipline +implies — then `prisma app logs > out.txt` yields an empty file. That is a +silent regression discovered by a user, not by us. + +**Suggestion:** state the routing rule explicitly, and give a command a way to +declare that its event stream *is* its data — either a distinct event kind +(`data`, going to stdout) or a `channel: 'data' | 'progress'` discriminator on +`output`. While you are there, say that `--quiet` suppresses progress events +but never data events, matching what `--quiet` already means for `present`. + +### F12 — Session commands have no meaningful `Result`, and `present` is required anyway + +**Location:** §4, `present` (required, lines 228–232); the header's protocol +note, lines 12–14. + +**Issue:** for a session command the value *is* the session, which no longer +exists once the handler returns. Composer's `dev()` returns +`Result` and the CLI adapter then owns the lifetime +(`run-dev.ts`). Under this interface the handler must instead await the signal +itself, tear down, and return some invented `Result` whose `present.human` is +asked to render a summary of a session that has ended. + +**Why it matters:** every session command gets a fake result type and a +no-op presenter, and the shape of that fake becomes de facto convention +without ever being designed. It also removes the deliberate property Composer +records: the host owns signals and the operation never touches them. + +**Suggestion:** make `present` optional, or add an explicit +`kind: 'session'` (or `present: 'none'`) that declares "the stream is the +output; there is no end-state rendering". This is the same knob the platform +already needs — `build logs` sets `emitJsonSuccessEvent: false` for exactly +this reason (`commands/build/index.ts:52-55`). + +### F13 — `raw` contradicts the rest of the type + +**Location:** §4, `raw?: false | { reason: string }`, lines 234–239. + +**Issue:** the comment says "the engine enforces that nothing else is +declared", but `flags` and `present` are both required properties. A `raw` +command such as `lsp` cannot be written without declaring an empty `flags` +object and a presenter that will never run. + +**Why it matters:** `lsp` is a real command; the escape hatch does not +currently escape anything. + +**Suggestion:** make `CommandDefinition` a union — the normal shape versus a +`RawCommandDefinition` with `brief`, `description`, `flags` and a raw handler +taking the runtime streams. Then "nothing else is declared" is enforced by the +type rather than by a runtime check. + +### F14 — `Runtime` is missing `env` and `isTty.stdout` + +**Location:** §6, `Runtime`, lines 300–311. + +**Issue:** there is no `env`, and `isTty` covers stdin and stderr but not +stdout. + +**Why it matters:** two concrete consequences. +(a) The engine's own interactivity check needs `env.CI` — the platform's +`canPrompt` reads it directly (`runtime.ts:114`). Colour policy needs +`NO_COLOR`/`FORCE_COLOR`. With no `env` on `Runtime`, the engine reads +`process.env`, and the moment it does, R7's in-repo tests stop being able to +simulate CI or a non-colour terminal — which is the property that makes those +tests evidence. +(b) The deliberately-kept auto-JSON behaviour keys on stdout not being a TTY +(`utils/global-flags.ts:67-69`). The interface has no field to read. + +**Suggestion:** add `env: Readonly>` and +`isTty.stdout`. Both are cheap and both are already in the platform's +`CliRuntime`. + +### F15 — The success envelope has no slots for warnings, next steps, or next actions + +**Location:** §4, `present` and the handler's `Result` return. + +**Issue:** the handler returns a bare value. The platform's `CommandSuccess` +carries `warnings`, `nextSteps` and `nextActions` alongside `result` on every +command (`output.ts:9-15`), and renders warnings in human mode too so degraded +steps are never silent (`command-runner.ts:130-135`). + +**Why it matters:** remediation is the second-most-recurring structure in the +survey, currently spelled five different ways, and unifying it is a stated +R14 goal. If the only place to put it is inside `present.json`, it ends up +nested in `result` where no cross-command consumer can find it — recreating +the ORM's "remediation buried in per-command shapes" problem the survey calls +out by name. `Block.nextSteps` covers human mode only. + +**Suggestion:** let the handler return `Result, …>` where +`Success` carries `value` plus optional `warnings`/`nextSteps`/`nextActions`, +or make those a second, engine-owned channel the handler can add to. Either +way they must reach the JSON envelope without passing through `present.json`. + +### F16 — R10's central rule is inexpressible: nothing says which section a command needs + +**Location:** §2, `CommandContext.config` (lines 106–110); §6, `LoadedConfig` +(lines 313–316). + +**Issue:** `CommandContext`'s comment asserts that the engine already failed +the command when its section is invalid, but nothing in `CommandDefinition` +names a section. `LoadedConfig` is an untyped `sections` record plus +diagnostics keyed by `section: string | null`. The engine cannot match one to +the other. + +**Why it matters:** "a command fails only if a section it needs is invalid" is +the whole reason per-section diagnostics exist — one product's config typo must +not brick another product's commands. As written the engine has exactly two +implementable choices, and both are wrong: fail every command if any section +is bad, or fail none. There is also no expression of the `defineConfig` +version marker, whose absence R10 says must fail early with a typed error — +today that can only appear as a diagnostic with `section: null`, which is +indistinguishable from a general file problem. + +**Suggestion:** put the section name on the definition (`configSection: +'composer'`), which also gives `TConfig` something to infer from (F05). Give +`LoadedConfig` an explicit discriminated top-level state so "no config file", +"file present but unmarked", and "file valid, section X invalid" are three +distinguishable things rather than three shapes of the same record. Section +registration and validators are legitimately a separate design, but the +*name* has to be here or the rule cannot be enforced. + +### F17 — Cancellation, teardown, and the signal-to-exit-code path are undefined + +**Location:** §2, `signal` (lines 124–126); §6, `Runtime.signal` (line 306). + +**Issue:** three gaps sit together. The engine cannot tell which signal fired +(`AbortSignal` alone does not say SIGINT versus SIGTERM), so 130 versus 143 +is unreachable. Nothing bounds teardown — a handler that hangs after abort +leaves the CLI unresponsive, and a second Ctrl-C has no defined effect. +And `PromptSurface` returns the same `Result` failure for "no TTY available" +as for "user pressed Ctrl-C", which the engine must distinguish to choose +exit 2 versus exit 3. + +**Why it matters:** this is the 2am path. A `dev` session that will not die on +Ctrl-C is the single most common CLI complaint, and it is currently +unspecified rather than decided. + +**Suggestion:** state that `signal.reason` carries a typed cancellation value +naming the signal; define a teardown grace period after which the engine +returns the signal code regardless; define what a second signal does. Give the +prompt failures distinct, documented codes so the exit-code mapping is +mechanical rather than a string match. + +### F18 — `report` has no backpressure and no defined end of life + +**Location:** §2, `report: (event: EngineEvent) => void`, line 117. + +**Issue:** `report` returns `void`, so a handler tailing a high-volume log has +no signal to slow down; `stream.write()` returning `false` is invisible. The +draft also says `report` is safe to call after the signal fires, but says +nothing about after the handler's promise settles. + +**Why it matters:** unbounded buffering on a slow pipe is a memory failure +mode in exactly the commands that stream. And a late `report` from a detached +task — a timer, a child process that has not been awaited — writing after the +JSON envelope has been printed corrupts the `--json` contract for machine +consumers, which is the one contract agents depend on. + +**Suggestion:** either return a `boolean`/`Promise` for backpressure, or +document that the engine buffers with a stated bound and what happens when it +is hit. Separately, state that `report` becomes a no-op once the handler's +promise settles, and that the engine drains before writing the envelope. + +### F19 — The test harness cannot test any session command + +**Location:** §7, `TestCli.run`, lines 330–338. + +**Issue:** `run(argv, { stdin })` has no abort handle, no cwd, no env, and no +TTY control. For a session command `run()` simply never resolves. The `stdin` +string also cannot drive a clack prompt, which reads raw keypresses (arrow +keys for `select`), so prompt-bearing commands are untestable too. + +**Why it matters:** R7 says in-repo tests are the evidence about the shipped +CLI. Under this harness the untestable set is `dev`, `app logs`, +`build logs --follow`, `app run`, and every wizard — which is most of what +actually breaks. Nor can a test cover F14's auto-JSON behaviour, since there +is no TTY knob. + +**Suggestion:** add `signal`, `cwd`, `env`, and `isTty` to the `run` options, +and replace the `stdin` string with a scripted prompt responder (an ordered +list of answers, plus the recorded prompts in the result so tests can assert +what was asked). Both are small; both are the difference between a harness +that covers the risky commands and one that covers the easy ones. + +### F20 — Steps have no identity, so the nesting the comment promises does not work + +**Location:** §1, `step-started` / `step-finished` (lines 44–52). + +**Issue:** the pair is correlated only by the `step` string, and the comment +says "steps may nest". The one implementation in the corpus that actually +nests uses `spanId` plus `parentSpanId` (`control-api/types.ts:91-111`), +precisely because a name is not unique — the ORM's per-migration spans are +`operation:` children of `apply`. + +**Why it matters:** two concurrent steps with the same name (two contract +spaces both applying) cannot be told apart, and a renderer cannot build the +tree. This is the one place where the draft's vocabulary is strictly weaker +than the code it generalises. + +**Suggestion:** add `id` and optional `parentId`, exactly as the span model +does. `step` stays as the human label. + +### F21 — Credentials are a static token with no refresh path + +**Location:** §2, `Credentials` (lines 132–136); §6, `Runtime.credentials`. + +**Issue:** a plain `{ token, workspaceId? }` captured once at startup. + +**Why it matters:** a `dev` session runs for hours. A token captured at +minute zero expires at minute ninety, and every subsequent management-API +call fails with an auth error that looks like a bug. Nothing in this shape +allows refresh. + +**Suggestion:** make it a provider — `getToken(): Promise>` +— so refresh is possible without changing the interface later. The opacity +the comment wants is preserved. + +### F22 — Commands cannot declare that they need authentication + +**Location:** §2, `credentials: Credentials | undefined`. + +**Issue:** every handler that needs auth must check for `undefined` and build +its own "not authenticated" structured error. + +**Why it matters:** the platform already centralises this +(`authRequiredError`, `errors.ts:101-115`) with consistent wording, next +steps, and exit code. Pushing it into every handler is the presentational +drift R5 exists to kill, one layer down: the error text becomes per-product. + +**Suggestion:** `requiresAuth: true` on the definition, with the engine +producing the one canonical error. Whether that belongs on the engine at all, +given `credentials` is Cloud-specific, is a shape question — architect +referral. + +### F23 — `createCli` is documented as failing at build time but can only throw at startup + +**Location:** §6, `createCli`, lines 283–288; "Collisions and grammar +violations fail the build." + +**Issue:** `createCli` is a runtime function taking a `Record`. Duplicate +object keys are a TypeScript error, but a command mounted at `'db'` colliding +with a group named `'db'`, or a path violating the agreed grammar, can only be +detected when the function runs. Throwing there also conflicts with the stated +rule that the engine never exits and only writes to the provided streams. + +**Why it matters:** "fails the build" and "throws on every user invocation +including `--help`" are very different guarantees, and the draft claims the +first while the shape delivers the second. + +**Suggestion:** say plainly that validation happens at `createCli` time and +returns a `Result` (or is asserted by a shell-repo test that calls it), and +keep a separate lint/build step if a genuine build-time check is wanted. + +### F24 — `@prisma/cli-foundation` and `NodeJS.WritableStream` both sit in the public surface + +**Location:** line 25 (the foundation import); §6, `Runtime`'s stream fields. + +**Issue:** R3 says products import the engine package "and nothing else for +CLI purposes". `Result` and `CliStructuredError` come from a second package. +Separately, `NodeJS.WritableStream` is a Node-global type in a surface that +R4 wants runtime-agnostic; stricli deliberately uses a minimal structural +`{ write, getColorDepth? }` instead. + +**Why it matters:** the foundation split is almost certainly correct +(structured errors are raised at their origin, inside operations, which must +not depend on the engine), but then the engine should re-export so products +have one import. And the Node type quietly makes the public surface +node-only, which is one of R4's three stated reasons. + +**Suggestion:** re-export `Result` and `CliStructuredError` from the engine +package. Replace the stream types with a structural interface. Whether the +two-package split is right is an architect question; the re-export and the +stream type are not. + +### F25 — `flag.json()` is engine-behavioural but arrives in `args` + +**Location:** §3, `flag.json()`, line 168. + +**Issue:** as a `FlagSpec` in the flags record, `json` appears in +`ArgsOf` and therefore in the handler's arguments. The engine can identify it +at runtime (it produced the spec object), so switching renderers and +suppressing prompts and progress is implementable — that part works. + +**Why it matters:** a handler that can see `args.json` will eventually branch +on it, which is the R5 violation the whole design is built to prevent. It also +raises the question of the other engine-behavioural flags the platform proves +are needed and the draft omits entirely: `--quiet`, `--verbose`, `--trace`, +`--yes`, `--no-interactive`, `--color/--no-color` +(`global-flags.ts:23-45`). With no global flags, each must come from a shared +declaration, and only `json` has one. + +**Suggestion:** exclude engine-owned flags from `ArgsOf` (a `FlagSpec` variant +the mapped type filters out), and add the rest of the shared set. `--yes` is +the interesting one: it must reach `ctx.prompt` so `confirm` auto-answers, +which argues it is context state, not an argument. + +## Deferred + +These are real, but they are scope expansions rather than gaps in this draft. + +- **Config-section registration and validators (R10's other half).** The + draft says this is a separate design and I agree. What cannot be deferred + is the section *name* on the definition — without it the engine cannot + implement the "only the sections it needs" rule at all (F16). +- **Daemon management (mode 5).** Explicitly out of scope in the header. + The survey's finding that Composer's daemons have no user-facing + stop/status is worth carrying into that design, not this one. +- **Autocomplete.** Stricli ships `proposeCompletions`; nothing in this draft + touches it. Fine to leave until the tree is stable, but note that a + completion surface constrains the flag vocabulary, so decide before + freezing F06–F08. +- **Telemetry.** Both the ORM and the platform fork a detached child on every + invocation. Neither `Runtime` nor `Cli` has a hook. Deliberate, presumably — + but it should be an explicit decision rather than an omission. +- **Duration and timing events.** The survey ranks durations 2/3 and both + families render them under `--verbose`. There is no timing concept in the + event vocabulary. Adding one is speculative until a renderer needs it; the + evidence rule the draft sets for itself says wait. + +## Acceptance-criteria verification + +Verdicts for a design artifact: **PASS** = the interface structurally +satisfies or enforces the requirement. **WEAK** = satisfiable, but the shape +does not enforce it. **FAIL** = the shape contradicts the requirement or +cannot express it. **NOT VERIFIED** = cannot be assessed from the draft. + +| R | Requirement | Verdict | Detail | +|---|---|---|---| +| R1 | One language, directly executable | **PASS** | `defineCommand` is an identity function; the declared object is what the engine runs. No product-side schema, no interpreter. The typing defects (F01–F05) are bugs in the expression, not a return to a two-stage design — except that F02's workaround (hand-written arg types in the handler module) would reintroduce exactly the drift R1 forbids, so fixing it is R1 work. | +| R2 | Commands end in typed operation calls | **WEAK** | The shape is compatible: `handler` returns `Result`, which is the operations client's own return type, so the thin-wiring case is the natural one. But nothing prevents a handler containing logic, and `TResult` being free-form makes a fat handler no harder to write than a thin one. Enforcement is a review/lint concern, not an interface one. | +| R3 | The engine package is the whole contract | **WEAK** | No stricli type appears anywhere — the primary goal holds, and `FlagSpec`/`PositionalSpec` being opaque brands means even the parse vocabulary is ours. Two leaks: `Result` and `CliStructuredError` come from `@prisma/cli-foundation`, so products import two packages (F24); and `NodeJS.WritableStream` puts a Node-global type in the surface, which is a third-party type in the sense R4 cares about even if not in the sense R3 names. Both fixable without design change. | +| R4 | Products receive a context, never the environment | **PASS** | `CommandContext` carries config, credentials, `report`, `prompt`, `signal`, `cwd`, and offers no way to reach disk, env, or the TTY. `cwd` is explicitly there so products never call `process.cwd()`. Two follow-ups that do not change the verdict: the engine itself loses injectability because `Runtime` has no `env` (F14), and there is no sanctioned way to probe for an optional module (F13's R13 counterpart, F26 below). | +| R5 | Products have no presentational API | **PASS** | Structurally enforced. `present` returns `Block[]`; `Ui` returns strings and cannot write; there is no print, no colour policy, no exit. `stdout: (value) => string[]` is the one raw-bytes path and it mirrors the platform's proven `renderStdout`, which exists so `--quiet` leaves a clean pipe. The gaps I found are about what products *cannot say* (F11, F15), not about what they can render. | +| R6 | Errors and results follow the settled conventions | **FAIL** | The `Result`/`CliStructuredError` half is right and used consistently, including on the prompt surface. The exit-code half contradicts the settled table: the draft names 0/1/2/3 and gives a command no way to express 4–99 or the signal codes, while `migration check` already exits 4, the platform's cancel path already exits 130, and Composer already passes a child's status through (F10). Cancellation cannot even reach 130/143 because the interface does not say which signal fired (F17), and prompt failures do not distinguish "no TTY" (exit 2) from "user declined" (exit 3). | +| R7 | Product-repo end-to-end tests are first-class | **WEAK** | `createTestCli` is instance-based, takes only the product's own commands, returns real bytes plus exit code, and — better than the current CLIs — exposes `events` for semantic assertions without forcing `--json`. That much is a genuine match. But the harness cannot abort a run, so every session command hangs forever; cannot script keypress-driven prompts, so every wizard is untestable; and has no cwd/env/TTY knobs, so the auto-JSON and interactivity behaviours cannot be covered (F19). The untestable set is the risky set. | +| R8 | The shell's test burden is integration proof | **NOT VERIFIED** | This is an allocation-of-work requirement, not an interface one. Nothing in the draft obstructs it — `createCli` plus an injected `Runtime` gives the shell the same argv-in/bytes-out path products get. Re-assess against the shell's test plan. | +| R9 | Static tree, lazy guts | **PASS** | `handler: () => Promise<{ default }>` is exactly stricli's `loader`, and everything help needs (`brief`, `description`, `examples`, `flags`, `positionals`) is static, so full help renders without invoking a loader. One caveat that does not change the verdict: `present` also sits in the static definition, so a presenter that reaches for a heavy formatting library silently drags it into startup. Worth a documented rule. | +| R10 | One config file, validated by its products, never a crash | **FAIL** | The requirement's operative rule — "a command fails only if a section it needs is invalid" — cannot be implemented against this shape, because no command declares which section it needs and `LoadedConfig` is an untyped record keyed by strings the engine cannot match to commands (F16). The `defineConfig` version marker has no representation either; an unmarked classic Prisma 7 file can only appear as a diagnostic with `section: null`, indistinguishable from any other whole-file problem — and R10 calls a silently misparsed v7 file the worst launch bug available. Section registration is legitimately deferred; the section *name* is not. | +| R11 | Pinned versions, tandem releases | **NOT VERIFIED** | A release-process requirement with no interface surface. `createCli` takes a `version` string, which is unrelated. | +| R12 | The shell defines the command tree | **PASS** | `CommandDefinition` contains no path and `CommandSet` is a flat name→definition record; paths exist only as keys in `createCli`, alongside group briefs that are declared at the mount because groups belong to the tree. `createTestCli` taking the same records is what lets a product mount a command anywhere for its own tests, which is R12's stated escape valve. Only caveat: "collisions fail the build" overstates what a runtime `Record` can guarantee (F23). | +| R13 | The CLI never touches a package manager | **WEAK** | Nothing in the interface installs, downloads, or vendors anything, so the prohibition holds. The requirement's positive half does not: a handler needing an optional peer has no sanctioned way to probe for it. A bare `await import('x')` in a handler is arguably a permitted runtime check (the stricli evaluation says the framework does not interfere), but with no helper every product hand-rolls the try/catch and its own error wording — the same per-product drift F22 describes for auth. Suggest `ctx.optionalDependency(name): Promise>` so the "missing dependency, install it with your own package manager" error is written once. | +| R14 | One event vocabulary, engine-defined, with product extensions | **PASS** | The union is a genuine generalisation of the surveyed structures, the common fields are required rather than optional (so products must fill them), and `data?: unknown` sits on every variant as the pass-through extension with the engine explicitly not interpreting it. The occurrence-ranked derivation is exactly the evidence discipline R14 asks for. Two weaknesses that do not sink it: steps lack the ids the one nesting implementation in the corpus needs (F20), and the vocabulary has no way to mark an event as data rather than decoration, which is where the streaming commands break (F11). | + +### Summary counts + +| Verdict | Count | Requirements | +|---|---|---| +| PASS | 6 | R1, R4, R5, R9, R12, R14 | +| WEAK | 4 | R2, R3, R7, R13 | +| FAIL | 2 | R6, R10 | +| NOT VERIFIED | 2 | R8, R11 | +| **Total** | **14** | | diff --git a/.drive/projects/prisma-cli-v8/assets/engine/reviews/envelope-collections-analysis.md b/.drive/projects/prisma-cli-v8/assets/engine/reviews/envelope-collections-analysis.md new file mode 100644 index 00000000..55e7dd9e --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/reviews/envelope-collections-analysis.md @@ -0,0 +1,223 @@ +# Are `diagnostics`, `warnings`, and `nextActions` three real things? + +Adversarial analysis of the draft envelope's three collection fields +(`engine-interface-draft.ts` §1, §2, §9), against the shipped emission sites +in the output-modes survey, ADR 239, and the platform CLI source. + +**Verdict up front: the operator is right about `warnings` and wrong about +`nextActions`. Fold to two collections: `diagnostics` + `nextActions`. +Delete `warnings` from both envelopes, and stop aggregating warn-severity +message events into the result contract. `nextSteps` should go too — it is +derived data a JSON consumer can compute from `nextActions`.** + +--- + +## 1. What the shipped "warnings" actually are + +I enumerated every warning-emission site with content across the three +families. They fall into three populations, and none of them justifies an +uncoded string channel in the envelope. + +### Population A: structured findings that lose their structure at the boundary + +These are the majority, and they are the damning ones. Each site *has* +structure — a kind, a wrapped error, a why, a fix — and crushes it into +prose because the envelope's `warnings: string[]` accepts nothing else. + +| Site | What it flattens | Natural code | +|---|---|---| +| ORM planner conflicts — `MigrationPlannerConflict { kind, summary, why? }`, kinds like `'typeMismatch'`, `'nullabilityConflict'` (framework-components/control/control-migration-types.ts:262-269; rendered as a dim string list, cli/utils/formatters/migrations.ts:97-107) | A discriminated union with `kind` + `summary` + `why` — CliStructuredError minus the registry entry | `MIGRATION.PLANNER_TYPE_MISMATCH`, `MIGRATION.PLANNER_NULLABILITY_CONFLICT`, … | +| ORM `db verify` schema warnings — doc comment says it outright: "**Warn-graded finding messages** (observed-policy drift)" (cli/utils/formatters/verify.ts:50-55) | Drift findings from the same verification machinery whose error-graded findings the draft calls diagnostics | `CONTRACT.DRIFT_*` — same family as the error-graded findings | +| Platform init "Project link failed: `${error.summary}`. Link later with …" (prisma-cli controllers/init.ts:1053-1055) | A caught `CliError` — a full structured error — demoted to its `.summary` interpolated into a string | The error's own code, re-emitted at severity warn | +| Platform init "Installing X failed: `${detail}`. Install it later with `${installCommandText}`." (controllers/init.ts:365-372; same pattern :290) | summary + why (wrapped error's first line) + fix (install command) serialized into one sentence | `CLI.INIT_SKILL_INSTALL_FAILED` **already exists in ADR 239's crosswalk as an error code** (PN-CLI-5013). The same condition is an error in one flow and a warning in another — severity is an attribute of the occurrence, which is exactly what `CliStructuredError.severity` models | +| Platform local-state cleanup failures — "The app was removed remotely, but the local `${target}` state could not be cleared: `${cause}`" (controllers/app.ts:4999-5002, used :3086, :3092; same pattern in project remove/transfer, controllers/project.ts:629-632, 710-715) | A caught error swallowed into prose; a wrapper script that wanted to retry the cleanup cannot detect it | `PLATFORM.LOCAL_STATE_CLEANUP_FAILED` | +| Platform `agent status` "Could not read installed skills with `${cmd}`: `${msg}`. Falling back to `${path}`." (controllers/agent.ts:134-141) | Degraded read: failing command, cause, fallback source — three fields in one string | `PLATFORM.SKILLS_READ_DEGRADED` | +| Composer's warn-severity events — `watch-error {message}`, `rebuild-failed {message}`, `stop-error {message}`, `stream-failed {message}`, `lines-dropped {count}` (operations/dev.ts:19-30, operations/log.ts:20-25) | Already a discriminated union on `kind` — coded warnings in all but registry membership | one code per event kind | + +### Population B: advisory no-op notices a machine consumer genuinely wants to branch on + +| Site | Why a code is *more* deserved, not less | +|---|---| +| Platform promote/rollback "The selected deployment is already live for this app." (controllers/app.ts:1914-1916, 2027-2029) | An agent driving `app promote` needs to know the operation was a no-op. String-matching "already live" is the only machine handle today. `PLATFORM.DEPLOYMENT_ALREADY_LIVE` is about the clearest branch-worthy code in this whole inventory | +| Platform branch-database "prompt suppressed" warning under `--yes`/non-interactive (lib/app/branch-database-deploy.ts:135-141) | An agent needs to know a decision was skipped and why. Branch-worthy | +| Platform missing-preview-default env warnings (controllers/app-env-file.ts:64, app-env.ts:155) | Policy advisory with an exact key list — already carries `meta`-shaped content | + +### Population C: pure FYI that is really result data + +| Site | Where it belongs after the fold | +|---|---| +| ORM init ".env already exists; leaving it untouched." / "README.md already exists…" (commands/init/init.ts:223-225, 376) | The init JSON already has `filesWritten[]`/`filesDeleted[]` (init/output.ts:23-31); a `filesSkipped[]` in the **result data** says this better than any warning channel | +| ORM init "No package.json found…; created a minimal one." (init.ts:354-356) | Result data (`packageJsonSynthesized: true`) or a coded advisory — author's call | +| ORM init DB probe soft-failures in non-strict mode (init.ts:686-697) | Coded advisory: the probe outcomes are already a discriminated union (`'below-minimum' | 'no-database-url' | 'connection-failed' | 'driver-missing'`) — kinds again, flattened to `outcome.message` | + +**Answer to Q1.** Roughly 20 distinct warning texts ship across the corpus. +The large majority either already carry a discriminant (`kind`, a wrapped +error, an enum outcome) or wrap a structured error whose code exists. +Forcing registry codes onto them is not bloat: it is ~15–20 additive codes, +each declared in its owning module's union per ADR 239 (no central registry +edit), and several reuse codes that already exist (`CLI.INIT_SKILL_INSTALL_FAILED`). +The uncoded warning is a discipline failure of exactly the shape ADR 239 +killed for errors: "consumers cannot match errors by code because there is +no one code space to match against" — substitute "warnings" and every word +holds. The shipped sites prove the failure mode is not hypothetical: a +`CliError` is being demoted to its `.summary` in a string today +(init.ts:1053), and fix commands are trapped inside prose where no agent can +extract them. + +ADR 239 itself anticipated the fold. Its Consequences section keeps +`severity` while admitting "nearly every error is `error` today; the +`warn`/`info` values earn their place only for advisory … surfaces." +A separate uncoded `warnings` channel is what keeps `severity: 'warn'` dead +weight. The fold is what makes it earn its place. + +## 2. Is there consumer-visible behavior distinguishing the two channels? + +The draft gives the two channels four behavioral differences. None survives +inspection as a *consumer contract*; all four are artifacts of provenance. + +1. **Shape** (string vs envelope). Strictly less information. A + severity-`warn` envelope can carry everything the string carries + (`summary`) plus code, why, fix, meta. No consumer capability depends on + warnings being strings — the platform's human renderer just prefixes a + glyph (command-runner.ts:130-135), which an envelope renders equally well. +2. **Provenance** (aggregated from mid-run `message` events vs declared at + `ctx.present`). This is an authoring-side plumbing difference, not a + consumer distinction — and it is a *defect*: the same finding discovered + mid-run either gets buffered by the product until the return site (and + arrives structured) or gets emitted as a warn event (and arrives as a + string). Two paths, two shapes, one concept, diverging by accident of + when the code learned the fact. +3. **`--quiet` visibility** (draft: diagnostics render even under `--quiet`; + warnings, as commentary, do not). This rule can be kept per-severity + after the fold if wanted — but note the platform deliberately renders + warnings "so partial failures … are never silent" + (command-runner.ts:130-135). Either way it is a rendering policy keyed + off a field, not a reason for two collections. +4. **Log-level interaction.** The draft has warn `message` events both + filtered by `--log-level` *and* aggregated into the envelope — so does + `--log-level error` remove a warning from the result contract, or only + from the transcript? The draft doesn't say. The fold dissolves the + ambiguity: commentary is filterable and ephemeral; the envelope's + diagnostics are the contract and are never level-filtered. + +**The fold is free.** Folded shape: + +```ts +export interface CompletedEnvelope { + readonly ok: true + readonly command: string + readonly result: T + readonly outcomeCode: number + /** ALL structured findings of this run, serialized error envelopes. + * severity: 'error' entries require a non-zero outcomeCode; + * 'warn'/'info' entries are advisory. */ + readonly diagnostics: readonly unknown[] + readonly nextActions: readonly NextAction[] + // `warnings` deleted. `nextSteps` deleted (derivable — see §3). +} +``` + +`ErroredEnvelope` gets the same deletion: `error` (the primary abort) + +`diagnostics` (accompanying findings) + `nextActions`. Warn commentary +emitted before the abort is transcript, not contract; anything +contract-worthy is a diagnostic. + +Event change: `message` events keep `severity: 'warn'` for live human +rendering, but the clause "additionally aggregated into the envelope's +`warnings`" (draft §1, message event doc) is deleted. Commentary and +contract stop sharing a pipe. + +**What emitting a warning costs an author after the fold.** Three honest +options, matching the three populations: + +- *Contract-worthy finding* → one factory call at the return site: + `ctx.present(data, p, { diagnostics: [structuredError('MIGRATION.PLANNER_TYPE_MISMATCH', summary, { severity: 'warn', why, meta })] })`. + Cost over `warnings.push(string)`: one line in the owning module's code + union, and naming the thing. That naming cost is the point — it is the + same cost ADR 239 imposes on errors, for the same payoff. +- *Pure FYI* → put it in the result data, where it was always cheaper and + more queryable (`filesSkipped[]` beats a prose warning). +- *Ephemeral color* ("retrying…", "this may take a while") → a warn/info + `message` event, still one uncoded line — it just no longer leaks into + the machine contract. + +**The middle option — a shared generic `CLI.WARNING` code with structured +meta — should be rejected.** It is a fallback code: consumers cannot branch +on it, docs cannot index it, and it recreates the uncoded string with extra +ceremony. The same one-code-space argument that killed fallback error codes +kills it. + +## 3. Do `nextActions` overlap diagnostics? + +No — and the evidence is specific: **command-level follow-ups ship on fully +clean successes, where there is no finding to hang a `fix` on.** Platform +promote returns `nextSteps: ["prisma-cli app list-deploys", "app show-deploy "]` +on the happy path (controllers/app.ts:1917-1921); deploy returns +promote/show-deploy continuations (app.ts:882-890); `agent status` returns +the install command when skills are absent (agent.ts:157-159). None of +these is remediation of a finding — they are journey continuation. A +consumer (the survey's R14 case; the platform's crash envelope with its +pre-filled `feedback` recover action, shell/output.ts:147-156) branches on +`nextActions.kind`/`journey` to *drive the next invocation*; a diagnostic's +`fix` is prose explaining how to clear *that finding*. Scope is a real +distinction, exercised on both sides by shipped code: platform errors carry +`fix` AND `nextSteps`/`nextActions` simultaneously (output.ts:170-183). + +Two genuine overlaps to manage, neither fatal: + +- A diagnostic whose remediation is runnable (the "Install it later with X" + warnings) should emit **both**: the diagnostic (with `fix` prose) and a + `remediation` event / `next` entry carrying the typed command. The draft + already has the aggregation machinery; the survey's finding C2 (five + competing remediation encodings) is the argument for keeping exactly this + one typed action shape rather than re-deriving actions from fix strings. +- **`nextSteps` is redundant in the JSON envelope.** The draft defines it + as "derived from nextActions — the human-string form." A JSON consumer is + a machine; shipping the pre-derived human rendering alongside the source + of derivation is duplication in the contract. Derive `nextSteps` at the + human renderer, drop the field from both envelopes. (Lower stakes than + the warnings fold; if platform-envelope compatibility matters more than + minimality, keeping it costs only redundancy, not incoherence.) + +## 4. Recommendation + +**Fold to two: `diagnostics` + `nextActions`.** Concretely: + +1. Delete `warnings` from `CompletedEnvelope` and `ErroredEnvelope` (§9). +2. Delete the aggregation clause on the `message` event (§1); warn messages + are transcript only. +3. Keep `PresentedResult.diagnostics` / `ctx.present`'s `diagnostics` opt as + the single carrier; entries use `CliStructuredError.severity` ('warn' + for advisory, 'error' only with a non-zero outcomeCode — the existing + guardrail, unchanged). +4. Optionally key the render-under-`--quiet` rule off severity (error-graded + findings always; warn-graded findings follow the platform's + never-silent precedent). +5. Delete `nextSteps` from both envelopes; derive it in the human renderer. +6. Keep `nextActions` exactly as drafted. + +**Migration cost for the shipped sites** (all additive, all within existing +ADR 239 namespaces, each code declared in its owning module): + +| Family | Sites | Work | +|---|---|---| +| ORM planner conflicts | 1 producer type, 1 renderer | Map `kind` → `MIGRATION.PLANNER_*` codes (~4 codes); the union already exists structurally | +| ORM verify schema warnings | 1 shape, 1 renderer | Grade as `CONTRACT.*` warn-severity diagnostics; upstream verification already produces findings | +| ORM init (~6 texts) | init.ts | 2 become result data (`filesSkipped`), ~4 become coded advisories; probe outcomes already enumerate their kinds | +| Platform init/agent/app/project (~8 texts) | controllers | ~6 new codes; the link-failed case re-emits the caught error's existing code at severity warn; skill-install reuses `CLI.INIT_SKILL_INSTALL_FAILED` | +| Platform already-live / prompt-suppressed / env advisories | 4 sites | 3 codes, all genuinely branch-worthy | +| Composer warn events | already typed | No envelope today (no `--json`); under the engine their event kinds map 1:1 to codes when they need contract presence | + +Total: roughly 15–20 new codes, ~20 call-site edits, zero renames, zero +breaking changes to anything published. + +**Where the operator is right and where not.** The `warnings`/`diagnostics` +split is manufactured — it is the platform envelope's `warnings: string[]` +grandfathered into a design that simultaneously adopted a structured-error +model making it obsolete. The shipped warnings are mostly structured +findings and demoted errors being flattened to strings at the boundary; the +one thing the string channel provides that diagnostics don't is the ability +to skip naming the finding, and that is the discipline failure, not a +feature. `nextActions`, by contrast, is real: command-scoped continuation +exists on clean successes, cannot be derived from findings, and is the one +survivor the survey's five remediation encodings should collapse into. diff --git a/.drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review-r2.md b/.drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review-r2.md new file mode 100644 index 00000000..9ff36804 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review-r2.md @@ -0,0 +1,443 @@ +# System design review, round 2 — the unified CLI engine's public interface (v3) + +Subject: `wip/designs/engine/engine-interface-draft.ts` (v3). Compared against +`-v1.ts` and `-v2.ts` and against my round-1 artifact, +`./reviews/system-design-review.md`. + +Pass: **architect**. Same lens and same probes as round 1: discriminator +completeness, consumer-vs-essence, concept-vs-mechanism, symmetry, reads-cold. +Implementation mechanics and failure modes go to the principal-engineer pass. + +The six operator rulings are treated as settled. I do not re-argue them; where a +ruling creates a new consequence I say so and mark it as a consequence, not an +objection. Ruling 4 (`stdout` kept as a view name) is accepted without +qualification — see the disposition of A10. + +--- + +## Part 1 — Disposition of round-1 findings + +Legend: **resolved** / **partial** / **open** / **overruled**. + +### Events + +| # | Item | Disposition | +|---|---|---| +| A1 | `notice` vs `warning` — one axis, two kinds | **resolved.** Merged into `message` with `severity: Exclude` (lines 128–133), and `'error'` correctly excluded because fatal is the `Result`. The aggregation rule ("emit once, appear in both places", lines 122–125) is a genuine improvement over anything shipped. | +| A2 | `output` / `stream: 'stdout'\|'stderr'` names pipes | **resolved.** `channel: 'data' \| 'diagnostic'` (line 144) is semantic and lets a remote log stream map onto it honestly. The kind is still called `output`; with `channel` semantic and `source` documented as "not a pipe", the ambiguity I raised is materially gone. One new interaction with `views.stdout` — see B14. | +| A3 | `status` drops the transition | **resolved.** `from?: string` added (line 164). Kind name unchanged; acceptable now that the payload is a transition. | +| A4 | steps have no identity, nesting only in prose | **resolved.** `id`/`parentId` on `step-started`, `id` on `step-finished` (lines 100–101, 109). | +| A5 | four spellings of severity | **resolved as ruled.** One `Severity` type (line 53) reused by `message` and `Block.summary.tone`. `step-finished.outcome` deliberately stays a completion state — that is a defensible distinction and the doc now states it (lines 104–105). One residue: `outcome` still contains `'warning'`, which is a severity word inside a completion-state set. `'ok' \| 'failed' \| 'skipped' \| 'partial'` would carry the "finished, but not cleanly" meaning without borrowing from the severity scale. Minor; noting, not pressing. | +| A6 | remediation spelled three ways | **resolved.** One `NextAction` (lines 60–69, the platform's shape adopted whole), used by the `remediation` event, `Views.next`, and both envelopes. | +| A7 | events have no sensitivity marker while `Block.fields` does | **open.** `Block.fields.rows[].sensitive` still exists (line 514); `endpoint.url` and `output.line` still cannot be marked, and `Ui` still has no masking helper. The survey's §C5 masking evidence stands. | +| A8 | files-written has no home | **resolved.** `artifact` event added (lines 167–173) with `path` and `description`. No corresponding `Block`, but `tree`/`list`/`fields` cover the terminal case adequately. | +| A9 | `endpoint.name` vs Composer's `address` | **open**, cosmetic; one clarifying word in the doc comment still worth adding. | + +### Presentation + +| # | Item | Disposition | +|---|---|---| +| A10 | `present.stdout` names a file descriptor | **overruled by operator (ruling 4).** Accepted. The v3 shape strengthens the operator's case rather than weakening it: `views.stdout` is now a pure `readonly string[]`, there is no stream handle anywhere near a product, and "stdout" is genuine shared vocabulary for CLI authors. I withdraw the finding. The one consequence worth a sentence of documentation is B14. | +| A11 | `Block` missing `tree`; `list` unevidenced | **partial.** `tree` added with a recursive `TreeNode` (lines 522–529) — the important half. `list` remains with no cited evidence and still overlaps a one-column `table` and an unlabelled `fields`. | +| A12 | `Ui` reads cold; `dim` is a rendering decision; no masking or path relativization | **open.** `Ui` is byte-identical to v1 (lines 531–536). | + +### Context and runtime + +| # | Item | Disposition | +|---|---|---| +| A13 | config section unbound — round-1's top gap | **resolved.** `ConfigSection`, `SectionValidation`, `defineConfigSection`, and `CommandDefinition.configSection` (§4, line 398) make R10 structural. `SectionValidation` carrying `diagnostics` on the `ok: true` branch is a nice touch — a valid section can still warn. One residual: section-name collision has no stated home (B17). | +| A14 | `Runtime` has no `env` | **resolved** (line 612), and correctly on `Runtime` only, not on `CommandContext`. | +| A15 | `isTty` missing `stdout` | **resolved** (line 613), and now load-bearing: json auto-selection keys off it (line 601). | +| A16 | `signal` duplicated on `Runtime` and `CommandContext` | **partial.** Both still present (lines 276, 614). The context's doc now explains what it is *for* (session lifetime, 130/143 selection), which reduces the confusion, but the relationship between the two is still unstated. | +| A17 | `Credentials` owned elsewhere but declared here; name too broad | **partial.** The comment now says "placeholder pending its design" (line 288), which is honest. The `getCredentials()` change to a call-time async resolver (line 262) is a genuine improvement I did not ask for and would have. Name still `Credentials`. | +| A18 | `PromptSurface` naming; no `--yes`; no typed destructive confirmation | **partial.** `--yes` is resolved by ruling 1 — it is engine-injected and `ctx.prompt.confirm` consults it, which is the right shape. The distinct error codes for "interaction unavailable" (exit 2) versus "user cancelled" (exit 3) (lines 267–270) is a real improvement. Still open: the type name, and typed destructive confirmation (`--confirm `, survey §C11, 2/3 families). | +| A19 | no interactivity capability fact on the context | **open.** A handler still learns its environment only by attempting a prompt and reading the failure. `git connect`'s shipped "poll only when we can prompt" (`controllers/project.ts:1708-1717`) remains unexpressible. | + +### Flags, positionals, definitions + +| # | Item | Disposition | +|---|---|---| +| A20 | `flag.json()` a concept in the wrong clothes; family of seven | **resolved as ruled (ruling 1), with one omission.** `flag.json()` is gone; the family is engine-injected and reserved (lines 309–316). The omission: the injected family is `--json, --quiet, --verbose, --yes, --interactive, --color` — six of the seven shipped flags. `--trace` is missing, and the platform's error renderer literally prints "Re-run with --trace for deeper diagnostics" (`shell/output.ts`). Either fold it into `--verbose` explicitly or add it. See also B15 on the missing `--no-json`. | +| A21 | required-ness spelled with opposite defaults on the two sides | **open.** `flag.string` optional / `flag.requiredString`; `positional.string` required / `positional.optionalString` (lines 318–353). Unchanged. Still the clearest reads-cold failure in the file. | +| A22 | builder set incomplete | **resolved.** `flag.number` (line 325), `alias`, `default`, and `positional.variadic` (line 352) all added. Remaining absences (required `enum`, typed `repeated`) are defensible. | +| A23 | key→flag-name transliteration rule unstated | **open.** `alias`/`default` arrived; the rule that the record key becomes the flag name — and how `dryRun` becomes `--dry-run` — still appears nowhere. `hidden`/`deprecated` also still absent. | +| A24 | `raw` names the mechanism; three spellings for two states; `present` impossible-state | **resolved.** Split into three definition types with three `define*` functions (§7). The impossible state is now unrepresentable, which is the right fix. New findings on the split: B12, B13. | +| A25 | `handler` is really a loader | **open.** Still `handler: () => Promise<{ default: … }>` (line 403). The new `CommandHandler` helper (lines 420–424) is a good addition and makes the naming collision more visible, not less: `def.handler` is a loader, `CommandHandler` is the function type. | +| A26 | groups get a poorer declaration than commands | **open.** `groups: Record` unchanged (line 592). No `description`, no `examples`, no docs link — on either groups or commands. | +| A27 | `present` required even for `void` results | **resolved** by the session variant and by presentation moving to the return site. | + +### Mounting and testing + +| # | Item | Disposition | +|---|---|---| +| A28 | space-separated paths — right shape, needs a named type and an ordering rule | **partial.** Shape retained (correct). No `CommandPath` type, no stated grammar, no help-ordering rule. | +| A29 | `CommandSet` declared but unused; two meanings, one structure | **partial.** `CommandSet` now uses `AnyCommand` (line 580) but `createCli.commands` (line 593) and `createTestCli.commands` (line 638) are still two inline copies of the same structural type meaning *paths* rather than *names*. A reader still cannot tell the product-side map from the shell-side map. | +| A30 | shell can place a command but cannot rename it | **open.** | +| A31 | test harness is not the same machinery at the seam | **largely resolved.** `env`, `isTty`, `abort`, `answers`, and an injectable `now` all added (lines 642–658) — this is now a genuinely usable harness and the scripted-answers design ("a run that prompts past the script fails the test") is better than what I proposed. One residual: `config?: Record` is still the raw section map, not `LoadedConfig`, so a product still cannot test the invalid-section path — the exact R10 behavior the new `ConfigSection` machinery exists to produce. | +| A32 | `json: unknown[]` models the transport | **open**, and now more consequential — see B16. | + +### Missing concepts from round 1 + +| # | Item | Disposition | +|---|---|---| +| M1 | no success envelope | **resolved.** `SuccessEnvelope` / `ErrorEnvelope` (§9), with `warnings` aggregated from events and `nextSteps` derived from `nextActions` so the two cannot disagree. The strongest single improvement in the revision. | +| M2 | config version marker | **partial.** `LoadedConfig.diagnostics` now names "missing version marker" as a file-level problem and states that `section: null` fails every command (lines 626–627). The writer side (`defineConfig`) is still elsewhere by delegation, which is fine now that the reader side is explicit. | +| M3 | exit codes 4–99 | **resolved in policy, defective in typing.** The header declares 0/1/2/3, 4–99 per command, 130/143 for signals — a better answer than I asked for. The typing of `exitCode` is the subject of B7. | +| M4 | typed destructive confirmation | **open** (see A18). | +| M5 | poll timeouts | **open / accepted.** Still "the handler's business" (line 18), still with no engine-owned timeout error code or elapsed rendering. Worth recording as an accepted risk with a reason rather than leaving it as a parenthesis. | +| M6 | R13's dependency check | **resolved in placement, incomplete in shape** — `ctx.probeDependency` added (line 283). See B19. | +| M7 | telemetry / update-check | **open**, still unhomed; out of scope for this artifact. | +| M8 | durations | **open.** No `durationMs` anywhere; the engine can derive step durations now that steps have ids (A4 resolved), so this is smaller than it was. | + +### Referrals from round 1 + +R1 (variance of `CommandDefinition` in collections) — **addressed** by `AnyCommand` +with `any` generics; see B13 for what that costs. R2 (`ArgsOf` over optional +`positionals`) — **addressed** by the `Args` split with +required parameters and optional definition members; the principal-engineer pass +should still confirm `{}` defaults behave. R3 (does inference land) — still needs +a compiled example, now including `ctx.present`. R4 (`report` sync vs +backpressure) — **open**; the doc adds a sealing rule (line 89) but not a +backpressure answer. R5 (events after the signal) — **resolved in contract** +(lines 89–92). R6 (test determinism) — **resolved** (`now`, line 643). R7 +(`section: null`) — **resolved** (line 626). + +--- + +## Part 2 — Fresh findings on the v3 shape + +The move of presentation to the return site is a real improvement on the axis it +was chosen for. The v2 shape asked a presenter, declared in a different file at +startup, to reconstruct from a result value the case distinctions the handler had +already made — the classic "re-derive what you just knew" tax. Returning a +materialized view removes it, and the invariant it buys ("nothing product-authored +executes after the handler resolves — the engine receives values, never +callbacks", lines 23–24) is a strong, checkable property that also makes the +result snapshotable in tests. I would keep the ruling. + +The findings below are about what the new shape names things, what it now cannot +express, and one place where the file contradicts its own stated invariant. + +### The presented-result triple + +**B1 — the interface lost its static inventory of what a command can produce, and +nothing replaces it.** In v2 a reader (and a build step, and a docs generator) +could look at a definition and see every output shape the command has. In v3 the +views exist only inside a handler, at one or more return sites, behind a lazy +import. R5 still holds in its stated form — a product still cannot render — but +the second-order property R5 buys, *reviewable* consistency, now has no +attachment point. The platform generates help and docs from static descriptors +today (`shell/command-meta.ts`, `shell/help.ts`), and R8 makes the shell's job +"integration proof", which implies something checkable. +*Consequence of ruling 5, not an objection.* The mitigation is cheap and partly +present: `TestCli.run().presented` (line 669) gives per-command evidence, so a +product-repo conformance test can assert every command's human view is non-empty +and its stdout view is pipe-clean. Say so in the doc, so the loss is a deliberate +trade with a named replacement rather than a silent one. + +**B2 — `Views` and `PresentedResult['views']` are two different types with +the same name-word and the same member names — one is the recipe, one is the +dish.** (Lines 191–216.) `Views.human` is `(ui: Ui) => readonly Block[]`; +`views.human` is `readonly Block[]`. A reader who has learned one will misread the +other, and the compiler's error text when they are confused reads as nonsense +("Type '() => readonly Block[]' is not assignable to type 'readonly Block[]'"). +Symmetry probe fires: parallel names for non-parallel things. +*Alternative:* name the builders for what they are and the values for what they +are — `ViewBuilders` (or `Renderers`) supplied to `ctx.present`, and +`RenderedViews` inside the result. Two words, and the file reads cold correctly. + +**B3 — `Views` is generic in a parameter none of its members mention.** (Lines +211–216.) `human: (ui: Ui) => …`, `stdout: () => …`, `json: () => …`, `next: +() => …` — `T` appears nowhere. The views close over the data lexically, which is +the entire point of the ruling, so the parameter is decorative: `Views` and +`Views` are the same type, and `ctx.present(data: T, views: Views)` +gives a false impression that the views are checked against the data. +*Alternative:* drop the parameter — `Views` — and let `ctx.present(data: T, +views: Views): PresentedResult`. The relation between data and views is +lexical by design; the type should stop pretending otherwise. + +**B4 — `ctx.present` is a verb meaning "display it", on a method that displays +nothing.** (Line 258.) It selects which view functions to call and returns a +value; the engine displays. In an interface whose founding premise is "products +cannot print", the most confusable possible name is a method called `present` that +does not present. It also sits next to `ctx.report`, which *does* emit — two +sibling methods with two similar verbs and opposite effects. A handler that calls +`ctx.present(...)` and forgets to return it has silently produced nothing, and the +name actively encourages that mistake. +*Alternative:* `ctx.outcome(data, views)` or `ctx.result(data, views)` — noun-ish, +reads as construction, and pairs correctly with the returned type's name. + +**B5 — the mode→view-set mapping is prose, the mode set is not a type, and mode +combinations are undefined.** (Lines 186–189: "human mode → human + stdout + next; +`--quiet` → stdout; json mode → json + next".) Four problems, in ascending order +of importance. + (a) There is no `OutputMode` type anywhere. The engine's central dispatch — + which views get materialized, which renderer runs, whether prompts work — turns + on a union that is never declared. Discriminator-completeness fires on an + undeclared union. + (b) Combinations are undefined. `--json --quiet` is accepted today by the + platform (`resolveGlobalFlags` sets both) and resolved by json-first precedence + (`command-runner.ts:118-127`). `--json --verbose`, and non-TTY-auto-json plus + explicit `--quiet`, are the same question. The interface must name the precedence. + (c) The one **required** member of `Views` is `human` (line 212) — the one view + that json mode never materializes. The required/optional split runs opposite to + the mode mapping. + (d) `--quiet → stdout` alone means `next` is not materialized in quiet mode, so + next actions vanish. Almost certainly intended; it should be stated, because + `nextSteps`/`nextActions` are the survey's headline machine-facing asset. +*Alternative:* declare `export type OutputMode = 'human' | 'quiet' | 'json'`, put +the mapping in a table in that type's doc comment, and state the precedence when +several flags apply. + +**B6 — `--verbose` is in the injected flag family but has no view, so every +product's shipped verbose content becomes inexpressible.** (Lines 33–35 vs +211–216.) The shipped behavior is substantial and cited in the survey §C9/§D: the +ORM renders `timings` only under `-v`, truncates conflict lists to three with a +"re-run with -v" footer (`formatters/errors.ts:54-98`), and shows `docsUrl` only +under `-v` (`errors.ts:99-101`); the platform appends timing diagnostics under +`--verbose` (`command-runner.ts:136-139`). Under v3 the handler cannot see the +flag (ruling 1, correctly), `ctx.present` receives no mode information, and +`Views` has no verbose member. So the only verbose content that can exist is +engine-generated decoration. +*Alternative:* add `verbose?: (ui: Ui) => readonly Block[]` to `Views`, materialized +and appended in verbose human mode. That keeps the product supplying words and the +engine deciding whether to show them, which is exactly R5's division. This is the +most concrete gap the new shape creates. + +**B7 — `exitCode: (data: unknown) => number` is a callback the engine executes +after the handler resolves, in a file that says no such thing exists.** (Line 408 +vs lines 23–24: "Nothing product-authored executes after the handler resolves — +the engine receives values, never callbacks.") It is also the one place where the +lost result generic bites hardest: the definition is loaded at startup and cannot +name a type produced by a module loaded at execution, so the author writes +`(data: unknown) => number` and casts — a cast in the machine-facing contract R6 +exists to protect, where a wrong value silently produces a wrong exit code. +Answering the question directly: **no, `unknown` is not acceptable here, and the +fix is not to bring back `TResult`.** Apply the ruling's own argument +consistently: the outcome and its context are live at the return site, so the exit +code belongs there, where the data is typed. +*Alternative, and I think this is the clean split:* + - **Declaration of the space stays on the definition** — `exitCodes?: Readonly>`, a documented catalogue (`4: 'integrity check failed'`) that help and docs can render without executing anything, and that the engine can validate against the 4–99 range at build time. + - **Selection of the value moves to the return site** — `PresentedResult` gains `readonly exitCode?: number`, supplied through `ctx.present(data, views, { exitCode })` or as a fifth view. Typed, no cast, no post-resolution callback, and the header's invariant becomes true. + +**B8 — the erasure is total: `Handler` returns `Result, +…>`, so no command's data type survives anywhere.** (Line 418.) Three +consequences. `SuccessEnvelope` can never be instantiated with a real `T`. +`TestCli.run().presented?: PresentedResult` forces every product-repo +test to cast before asserting on its own data — in the harness R7 calls +first-class. And `CommandHandler`, the helper whose stated purpose is +keeping definition and handler "in lockstep", now locks only args and config. +The ruling requires the *definition* to be free of the result type; it does not +require the *handler* to be. +*Alternative:* `Handler` returning +`Result, …>`. An individual handler file keeps its type, +the definition stays result-free, and the erased form is only what the mount map +stores. Parameterize `TestCli.run()` the same way. + +**B9 — view functions are now ordinary closures called conditionally, which +creates a class of mode-dependent bug the old shape made impossible.** Only the +active mode's functions run (lines 202–203), so a view function with a side effect +fires in one mode and not another, and a `human` closure that computes something +the handler needs is silently skipped under `--quiet` or `--json`. The old shape +had the engine call presenters exactly once, outside the handler. Nothing in the +contract says view functions must be pure. +*Alternative:* state it in `Views`'s doc comment — "pure; called at most once; +only in the active mode" — so the rule is part of the contract rather than folklore. +(The enforcement question is a referral.) + +**B10 — failure got no presentation at all, yet `ErrorEnvelope` declares +`nextActions` with no way to populate it.** (Lines 555–564.) A failing handler +returns `notOk(error)` and the error carries ADR 239's `why`/`fix`/`docsUrl` — +one triple. But `ErrorEnvelope.nextActions: readonly NextAction[]` (line 563) has +no producer anywhere in the interface, and no failing command can render +structure. The survey's evidence is direct and load-bearing for two shipped +commands: `migration check` renders a per-failure list of `✗ [CODE] where: why` +plus a `fix:` per failure (`commands/migration-check.ts:604-698`), and `db verify` +renders a drift block on failure (`utils/formatters/verify.ts:140-158`). Neither +is portable to this interface. Symmetry probe fires as hard as it can: success +gained an entire presentation subsystem in this revision; failure lost the little +it had. +*Alternative:* `ctx.fail(error, views?)` producing a presented failure that +`notOk` carries, using the same `Views` vocabulary (`human` blocks + `next` +actions). It costs one method and makes `ErrorEnvelope`'s existing fields +truthful. + +### The three definition variants + +**B11 — the session variant contradicts two other paragraphs of the same file.** +(Lines 432–456.) (a) Lines 122–125 say severity-`warn` message events are +"aggregated by the engine into the success envelope's `warnings`"; a session has +no success envelope, so that sentence is false for sessions and the warnings a +long-running session emits have no terminal home. (b) "A session always supports +json mode: the event stream is its json surface" — but the platform's shipped +streaming runner emits a terminal `{type:'success'|'error'}` frame precisely so a +machine consumer knows the stream ended and how (`command-runner.ts:208-235`), +with exactly one command opting out because it carries its own terminal record. +The interface removes that guarantee without saying so. +*Alternative:* state that the engine emits a terminal frame for sessions in json +mode, and say where a session's warnings go (a terminal frame is the natural +answer, which resolves both halves at once). + +**B12 — the three definitions duplicate their common members by copy, and the raw +variant's omissions look accidental rather than decided.** (Lines 380–492.) +`brief`/`description`/`examples`/`flags`/`positionals`/`configSection` are written +out three times with three different subsets. `RawCommandDefinition` has no +`examples`, no `positionals`, and — the substantive one — **no `configSection`, +and its `io` object carries no config**. The one command in the corpus that +motivates this variant is `lsp`, and a language server is precisely the consumer +that must read the user's `prisma.config.ts`. As written, a raw command cannot +obtain config at all without reading disk, which R4 forbids. +*Alternative:* extract a shared `CommandCommon { brief; description?; examples?; +docsUrl? }` that all three extend (so A26's future additions are one edit, not +three), and decide `configSection` for raw deliberately — I believe it must be +allowed, with the validated value handed in on `io`. + +**B13 — `AnyCommand` is a union with no discriminant, and the engine cannot tell +its members apart at runtime.** (Lines 496–499.) `CommandDefinition` without an +`exitCode` and `SessionCommandDefinition` have *identical* runtime shapes: the +same members, and `handler` differing only in a return type that does not exist at +runtime. `RawCommandDefinition` is distinguishable only by the absence of +`positionals`/`configSection`, which are optional on the others. So `createCli` +receives a map of `AnyCommand` and has no reliable way to decide whether to inject +the shared flag family, whether to run the presentation pipeline, or whether to +hand the process's streams over. Using `any` in the erasure (the right pragmatic +fix for round-1's variance problem) removes even the type-level distinction. +*Alternative:* have the three `define*` functions stamp a discriminant — +`kind: 'value' | 'session' | 'raw'` — so `AnyCommand` is a real discriminated +union. They are identity functions today; making them not-quite-identity is a +one-line change and buys an exhaustive `switch` in the engine and in any +future tooling that walks a command set. + +### Engine modes, flags, and the machine contract + +**B14 — there are now two product-facing routes to stdout with different +vocabularies and no stated rule for choosing.** `views.stdout` (terminal, lines, +line 195) and `output` events with `channel: 'data'` (streaming, lines, documented +as "routed to OUR stdout", lines 84–87). A command that streams data lines and +also returns a payload writes to stdout through both. This is a consequence of +accepting ruling 4, not an argument against it. +*Alternative:* one sentence — streaming data uses `output`/`data`; the terminal +payload uses `views.stdout`; and state the ordering guarantee between them. + +**B15 — json mode auto-selects on a non-TTY stdout and the flag family provides +no way to turn it off.** (Lines 31–32, 601; family at lines 33–35.) Piping any +command now changes the output's shape, so `prisma … | less`, `| tee run.log`, or +`| head` all get json rather than the human rendering. The ORM does this today but +ships `--format pretty` as the escape hatch (survey §D, `terminal-ui.ts:334`); the +platform does not auto-select at all. The interface adopts the more aggressive +behavior and removes the escape hatch. It is also internally asymmetric: both +other environment-sensing flags in the family have negative forms +(`--no-interactive`, `--no-color`) and this one does not. +*Alternative:* add `--no-json` to the injected family. (Related: `--trace` is +absent from the family though it is shipped and named in shipped error output — +see A20.) + +**B16 — the json stream's frame vocabulary is ambiguous, and `EventFrame.data` +collides with the event's own `data`.** (Lines 566–573.) The doc says "in json +mode everything is one framed stream on stdout" (line 87), but `SuccessEnvelope` +and `ErrorEnvelope` carry no frame fields (no `type`, no `timestamp`), and +`EventFrame.type` is typed `EngineEvent['kind']`, which cannot express a terminal +success or error frame. The platform's shipped frames do include them +(`{type:'success'|'error', command, timestamp, …}`, `command-runner.ts:213-235`). +So a consumer cannot tell from the types whether the envelope is a frame in the +stream or a separate object after it — in the one contract that must be +unambiguous, because agents and CI parse it. Separately, `EventFrame.data: +EngineEvent` nests an event that itself has a `data` member, so a consumer reads +`frame.data.data` to reach the product extension; the platform's `data` meant the +payload. +*Alternative:* declare the frame union explicitly — `type: EngineEvent['kind'] | +'success' | 'error'` with the envelope frames included — and rename +`EventFrame.data` to `event`. + +**B17 — nothing owns config-section name collisions.** (Lines 228–241, 589–594.) +Commands carry their own tokens and `createCli` never sees a section list, which +is a better design than the registry I proposed in round 1. But two products +registering the same `name` with different validators is now silently +last-one-wins, or worse, order-dependent. `createCli` already promises build-time +failure for collisions, unknown groups, and grammar violations (lines 585–587) — +add section-name conflicts to that same sentence. + +**B18 — `probeDependency` returns a bare boolean, so the product cannot write the +error R13 requires.** (Lines 281–283.) R13 mandates "a structured error naming the +dependency and how to install it **with the user's own package manager**". Package +manager detection is environmental, and R4 forbids products from reading the +environment — so the handler literally cannot know whether to say `npm add`, +`pnpm add`, `yarn add`, or `bun add`. The probe as designed can only produce half +the required error. +*Alternative:* `requireDependency(specifier): Promise>` — the engine detects the package manager and builds R13's +error, the handler just propagates it. Keep the boolean probe as well if commands +need to branch rather than fail. + +**B19 — `PresentedResult` claims a single constructor that the type does not +enforce.** (Lines 179–199: "Built exclusively by ctx.present".) It is a public +exported interface with all-optional views, so it is hand-constructible and the +"exclusively" claim is a comment. That matters because the mode contract lives in +which views are populated: a hand-built result with a `human` view in json mode is +a silently wrong state. +*Alternative:* brand it with a private symbol, the technique the file already uses +twice for `FlagSpec` and `PositionalSpec`. `TestCli.run().presented` can still +expose it for reading. + +--- + +## Part 3 — Referrals to the principal-engineer pass + +- Does `ctx.present`'s inference land? `present: (data: T, views: Views)` + with `Views` not mentioning `T` (B3) means `T` is inferred solely from `data` + — confirm the returned `PresentedResult` narrows as intended in a real + handler, including the union-of-return-sites case. +- Runtime discrimination of `AnyCommand` members (B13) — confirm whether the + engine can in fact tell them apart today; if it can only do so by probing + optional members, that is a correctness bug, not just a typing one. +- `report()` backpressure for a session emitting thousands of `output` events into + a slow pipe (round-1 R4, still open). +- The sealing rule (line 89) says calling `report()` after resolution throws + `InternalError` — check that a `finally` block or an unawaited promise in a + handler cannot trip it as a matter of course. +- View functions invoked conditionally (B9): whether the engine can detect + impurity, and what happens if a view function throws — mid-render failure after + a successful operation is a nasty state. +- `Exclude` (line 130) in a published declaration file: confirm + it emits readably for consumers rather than as an opaque conditional type. + +--- + +## Verdict + +v3 is a large improvement over v1, and most of it is the revision doing exactly +what the reviews asked: R10 is now structural, the success envelope exists, the +severity scale is single, remediation has one shape, the impossible +`raw`-plus-presenter state is gone, and the test harness became a real one. The +round-1 items that remain open are mostly small and mostly cosmetic — `Ui`, the +required-ness asymmetry, group declarations, path typing. + +The new shape — presentation at the return site — is the right call on the axis it +was chosen for, and the invariant it buys ("the engine receives values, never +callbacks") is worth having. My substantive concerns are that the invariant is not +yet true, and that the move left two holes it did not intend to leave. + +The invariant is not yet true because `exitCode` is a product-authored callback +the engine runs after the handler resolves, in the same file that declares no such +thing exists (B7). Moving the *selection* of the exit code to the return site +where the data is typed, and leaving a documentable *catalogue* on the definition, +resolves the contradiction and the `unknown` cast at once. That is my primary +recommendation. + +The two holes are `--verbose`, which is in the injected flag family but has no +view, so every product's shipped verbose content becomes inexpressible (B6); and +failure, which gained nothing while success gained a subsystem, leaving +`ErrorEnvelope.nextActions` with no producer and two shipped commands +(`migration check`, `db verify`) unportable (B10). Both are closed by adding one +member each — `Views.verbose` and `ctx.fail(error, views)`. + +Below those, three naming problems will cost every future reader: `ctx.present` +displays nothing (B4), `Views` and `views` are the recipe and the dish under one +word (B2), and `Views` is generic in a parameter it never uses (B3). And one +defect is mechanical rather than aesthetic: `AnyCommand` is a union whose members +are runtime-indistinguishable, so the engine cannot reliably tell a session +command from a value command (B13) — a stamped `kind` discriminant fixes it in a +line. + +Close B7, B6, B10, and B13, apply the three renames, and this interface expresses +its requirements. Nothing here argues for another structural revision. diff --git a/.drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review-r3.md b/.drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review-r3.md new file mode 100644 index 00000000..9da8718e --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review-r3.md @@ -0,0 +1,420 @@ +# System design review, round 3 (final) — the unified CLI engine's public interface (v4) + +Subject: `wip/designs/engine/engine-interface-draft.ts` (v4), against `-v3.ts` and +my round-2 artifact `./reviews/system-design-review-r2.md`. + +Pass: **architect**, same probes throughout: discriminator completeness, +consumer-vs-essence, concept-vs-mechanism, symmetry, reads-cold. + +The operator rulings are settled and I do not re-argue them. Ruling 1 in +particular — completed/errored replacing success/failure, with `ctx.fail` +rejected — is not merely accepted here: it is a better answer than the one I +proposed, for reasons I set out under the disposition of B10. + +**Headline: no structural concerns remain.** One item needs reconciliation with a +settled ADR before implementation (C1, narrowed by the `errors` block amendment — +see C8), two are small capability or typing defects with one-line fixes (C3, C4), +and everything else in this document is a nit. The verdict section says so plainly. + +This round includes the operator-directed amendment that landed mid-review: the new +`Block` member `{ kind: 'errors', errors: CliStructuredError[] }`. It is probed in +**C8**, and it changes the disposition of C1 for the better. + +--- + +## Part 1 — Disposition of round-2 findings + +| # | Item | Disposition | +|---|---|---| +| B1 | The interface lost its static inventory of what a command can produce | **Largely resolved.** The half that mattered for machines is now static and documented: `outcomeCodes` is a definition-level catalogue "rendered in help without executing anything" (lines 445–451). What is still dynamic is the human/stdout presentation, and `TestCli.presented` (line 760) is the stated replacement for checking it per command. Nit-level residue only. | +| B2 | `Views` (recipe) and `views` (dish) under one word | **Resolved.** `Presentations` for the input bundle (line 223), `presentation` for the materialized field (line 204). The two now read as different things because they are named as different things. | +| B3 | `Views` generic in a parameter no member mentions | **Resolved.** `Presentations` is non-generic; `ctx.present(data: T, presentations: Presentations, …)` (lines 272–276) now says exactly what is true — `T` comes from the data, the presentations close over it lexically. | +| B4 | `ctx.present` is a verb for a method that displays nothing | **Resolved in effect.** The name is unchanged, but the vocabulary around it changed and that was the actual problem. `ctx.present(data, presentations)` producing a `presentation` field reads as construction because the noun is now on both sides of the call. I withdraw the finding. | +| B5 | Mode set undeclared; combinations undefined | **Largely resolved.** `Format = 'human' \| 'json'` and `LogLevel` are declared types (lines 71–74), and format and log level are now cleanly orthogonal axes rather than one overloaded "mode". Two residual undefined combinations, both nits: C9 (`--json --quiet`) and C10 (whether `--quiet` implies a log level). | +| B6 | `--verbose` injected but with no presentation member | **Resolved as ruled (ruling 3), and better than my proposal.** One mechanism — severity-`verbose` `message` events filtered by `--log-level` — beats a second presentation member, because it keeps the product supplying words and the engine deciding display in exactly one place. One consequence worth knowing: verbose detail must now be *emitted during the run* as commentary rather than *composed into the final result blocks*, so the ORM's "truncated to 3, re-run with -v" pattern becomes a verbose message event on stderr rather than an expanded list inside the result. That is a fine trade; it should just be a known one. | +| B7 | `exitCode: (data: unknown) => number` — a post-resolution callback in a file that says none exist | **Resolved exactly as recommended.** Catalogue on the definition (`outcomeCodes`, line 451), typed selection at the return site (`ctx.present`'s `outcomeCode`, line 275). The header's invariant "nothing product-authored executes after the handler resolves" (lines 36–37) is now true. | +| B8 | Total erasure — no command's data type survives | **Open, nit.** `Handler` still returns `Result, …>` (line 466) and `TestCli.presented` is `PresentedResult` (line 760), so product-repo tests cast before asserting on their own data. A defaulted fourth parameter (`Handler`) would fix it without touching the definition. Small enough to leave. | +| B9 | Presentation functions are conditionally-invoked closures with no purity rule | **Open, nit.** The `Presentations` doc (lines 213–222) still does not say "pure, called at most once, only for the active format". One sentence. | +| B10 | Failure got no presentation while success gained a subsystem | **Overruled by ruling 1 — and dissolved, not merely rejected.** This is the right call and I want to record why, because it is the best move in the revision. My finding rested on two shipped commands (`migration check`, `db verify`) needing to render structure on the error path. Under completed/errored semantics they are not on the error path: they executed to their end, they have a result, and bad news is a result. They present normally and carry an outcome code. The finding's premise was that "did not succeed" and "did not complete" were the same thing; the ruling separates them, which is a genuinely better model than adding a parallel presentation system for failures. The second half — `ErroredEnvelope.nextActions` having no producer — is answered too: remediation events aggregate into it, and the error's `fix` derives `nextSteps` (lines 637–639). Both halves closed. | +| B11 | The session variant contradicted two other paragraphs | **Resolved.** The result frame is now universal ("events while running, then exactly one result frame", lines 642–643), so a session does get a terminal frame and its `warn` messages do have an envelope to aggregate into. The two contradictions are gone. One typing nit remains: C8. | +| B12 | Three definitions duplicate common members; raw has no config | **Substantively resolved.** Raw gained `configSection` and `io.config` (lines 531, 541), which was the real problem — an LSP can now read the user's config without touching disk. The literal duplication of `brief`/`description`/`flags`/… across three interfaces remains, and raw still has no `examples`. Nit. | +| B13 | `AnyCommand` members are runtime-indistinguishable | **Resolved.** `kind: 'command' \| 'session' \| 'raw'` is a required member of each interface and stamped by the `define*` functions via `Omit<…, 'kind'>` (lines 427, 478, 493, 514, 527, 551). This is the cleanest possible form of the fix: authors never write it, the engine can `switch` on it, and the union is genuinely discriminated. | +| B14 | Two product-facing routes to stdout with no rule for choosing | **Partial, nit.** Both routes are documented (lines 89–90, 218–219) but the ordering guarantee between streamed `output`/`data` lines and the terminal `presentation.stdout` lines is still unstated. | +| B15 | json auto-selects on non-TTY with no escape hatch | **Resolved by ruling 2.** `--format ` gives the escape hatch (`--format human`) that `--json` alone could not, and `--json` survives as the shorthand everyone already types. Note `--trace` is now absent from the injected family, which I read as the deliberate consequence of ruling 3's one-log-mechanism decision; worth one line confirming stack traces appear at `--log-level verbose`. | +| B16 | Frame vocabulary ambiguous; `frame.data.data` | **Resolved.** `Frame = EventFrame \| ResultFrame` (line 644), `EventFrame.event` (line 651), `ResultFrame.envelope` (line 658). The machine contract now says exactly what is on the wire. | +| B17 | Config-section name collisions unowned | **Partial, nit.** `createCli`'s doc lists "collisions, unknown groups, reserved-flag violations, and grammar violations" (lines 674–676); "collisions" reads as path collisions. Name section-name conflicts explicitly in that sentence. | +| B18 | `probeDependency` cannot phrase R13's install command | **Resolved.** `ctx.packageManager` (line 306), and the `probeDependency` doc now points at it (lines 299–302). | +| B19 | `PresentedResult` hand-constructible despite the "exclusively" claim | **Resolved.** Branded with a `PRESENTED` unique symbol (lines 183, 199) — the same technique the file already used twice, so it reads consistently. | + +### Round-1 items still open in v4 + +All nit-level. `Ui` is unchanged since v1 — still no masking or path-relativization helper and `dim` is still an ANSI word (A12, A7). `list` remains the one `Block` with no cited evidence (A11). `signal` still appears on both `Runtime` and `CommandContext` without stating their relationship (A16). `handler` is still a loader named for what it returns (A25). Groups gained `description` (line 681) but still no `examples`, and neither groups nor commands carry a docs link (A26). The shell still cannot override a command's `brief` at the mount (A30). Poll timeouts remain the handler's business (M5). + +Three round-1 items closed in v4 that I should record: **A21** (required-ness asymmetry) is settled by naming it — lines 353–356 now state it is deliberate and matches CLI convention, which is a legitimate resolution of a reads-cold problem; **A23** (transliteration) is settled by line 50; **M4** (typed destructive confirmation) is settled by the prompt-defaults design (lines 316–327), and settled *better* than I proposed: "destructive confirmations simply declare no default — `--yes` can never blast through them". That inverts the problem so the safe case is the default case, which is the right shape for a rule about destructive operations. + +--- + +## Part 2 — Fresh findings on the v4 shapes + +### C1 — A completed-but-bad result carries an integer where the settled conventions carry a dotted code. This needs reconciling with ADR 239. + +Ruling 1 moves a class of outcomes off the error path: `migration check` finding 16 +integrity violations, and `db verify` finding drift, are now completed results with +outcome codes (lines 20–23). ADR 239 currently classifies exactly those outcomes the +other way. Its exit-code section reads: "Expected `StructuredError` failures (usage, +config, precondition, **verify, runner**) → **2**", and its crosswalk assigns them +dotted codes today — `CONTRACT.VERIFY_FAILED`, `MIGRATION.RUNNER_FAILED`, and the +eighteen `MIGRATION.CHECK_*` codes converted from `PN-MIG-CHECK-NNN`. + +Two consequences, one of which matters. + +The one that does not: per-item codes survive. `migration check`'s shipped json +already carries `failures[{ space, code, where, why, fix }]`, and that lives inside +`data`, so a consumer can still match individual violations by dotted code. + +The one that does: **at the envelope level, a completed-but-bad result has no +`code` at all.** R6's own justification names three keys machine consumers branch +on — "agents, CI — branch on `ok`, `code`, and exit codes; that only works if +exactly one code space and one envelope exist." Under v4, the errored path provides +all three and the completed-with-outcome path provides two: `ok: true` and an +integer whose meaning is per-command and shares a numeric space (4–99) with every +other command's unrelated outcomes. A CI job that wants "did any Prisma command +report an integrity failure" can match `MIGRATION.CHECK_*` today and cannot match +anything tomorrow without knowing which command produced the 4. + +*Alternative, one field:* let the catalogue carry the dotted code alongside the +meaning, and surface it on the envelope. + +```ts +readonly outcomeCodes?: Readonly> +``` + +with `CompletedEnvelope` gaining `readonly outcome?: { code: string; meaning: string }` +populated from the catalogue entry for the selected code. That keeps ADR 239's +single code space intact across both envelopes, costs nothing at the return site +(the handler still selects an integer), and makes the help rendering strictly +better because it can print the code next to the meaning. + +Whether ADR 239's exit-code paragraph should also be amended (it currently sends +verify and runner failures to exit 2, which v4 sends to 4–99) is a decision for the +ADR's owner, not for this interface — but the two documents currently disagree and +one of them has to move. Flagging it as the one item to settle before +implementation. + +**Narrowed by the `errors` block amendment.** The new `Block` member carries real +`CliStructuredError` values inside a completed result, so the dotted code space now +*does* have a first-class home on the completed path — which is direct evidence +that the design already agrees dotted codes belong there. What remains of C1 is +smaller than when I wrote it: the per-finding codes are handled, and only the +envelope-level outcome lacks a dotted counterpart. C8's fourth point proposes +engine-side aggregation that would close most of what is left. + +### C2 — `ok` now means three different things in this file, and the envelope union has no name. + +`Result.ok` (foundation: no error), `SectionValidation.ok` (line 249: the section +validated), and `CompletedEnvelope.ok` / `ErroredEnvelope.ok` (lines 615, 631: the +command ran to its end). The third is narrower than the first, and a reader arriving +from ADR 239 or design 1a — where `ok: true` means "succeeded" — will misread it. +The doc comments do compensate (lines 612–614, 630), and keeping the wire field +named `ok` is correct because it is the shipped, settled envelope field. + +Two small things worth doing anyway. First, state the semantic shift once, in a +prominent place — the header's EXECUTION PROTOCOL section is the natural home, and +it nearly does this already; one sentence saying "`ok` on the envelope means +completed, which is narrower than `Result.ok`" removes the trap. Second, declare +the union: `ResultFrame.envelope: CompletedEnvelope | ErroredEnvelope` (line 658) +writes it inline, so consumers of the json contract have no exported name for the +thing they parse. `export type Envelope = CompletedEnvelope | ErroredEnvelope`. + +Nit. + +### C3 — `SingleChar` does not express what it claims, and may not typecheck as intended. + +```ts +export type SingleChar = string & { readonly length?: 1 } +``` +(Lines 382–383.) `string` already carries `length: number`; intersecting an optional +`length: 1` on top does not constrain a string literal's length, because a literal's +apparent `length` is `number`, not `1`. Depending on how the checker resolves the +apparent member, this either rejects every string or constrains nothing — and the +doc comment concedes the real check is at construction ("longer strings are a +construction error"). A type that advertises a constraint it does not enforce is +worse than no type: a reader will trust it. + +*Alternative:* either drop it and use `alias?: string` with the doc sentence and the +existing construction-time check (honest, and the check already exists), or, if a +compile-time guarantee is genuinely wanted, enumerate the alphabet as a literal +union — verbose but precise and machine-generatable. I would drop it. + +Referral: the principal-engineer pass should confirm the actual checker behavior +before deciding which way to go. + +### C4 — `InputStream` as `AsyncIterable` cannot support the prompts the engine promises. + +§8 (lines 597–605) replaces `NodeJS.*` with structural types for runtime +agnosticism, which is right and serves R4's "why" directly. But `Runtime.stdin` is +the only input the bin injects (line 695), and interactive prompts as shipped — +`@clack/prompts`, used by both families — need raw-mode keypress access to draw a +`select` with arrow keys or to intercept Ctrl-C at the prompt. An +`AsyncIterable` can deliver lines; it cannot put a terminal into raw mode. +So the interface, as typed, admits only line-oriented prompts, while §4a describes +a prompt surface with `select` over labelled options and a distinct +cancel-at-the-prompt error (lines 316–342) that presumes keypress handling. + +`OutputStream.write(text): void` is fine by comparison — cursor control and the +liveness display are ANSI escapes and go through `write` — and the missing return +value is the documented accepted trade (line 96). + +*Alternative, one optional member:* extend the input type with the capability rather +than the mechanism — + +```ts +export interface InputStream extends AsyncIterable { + /** Present only on a terminal; the engine degrades prompts without it. */ + readonly setRawMode?: (raw: boolean) => void +} +``` + +— which keeps the surface runtime-agnostic (a non-TTY runtime simply omits it) and +makes the degradation path explicit rather than accidental. + +### C5 — `outcomeCode` is checked against the catalogue at runtime when it could be checked at compile time. + +(Lines 270–271, 451.) `ctx.present`'s `opts.outcomeCode?: number` is verified by the +engine against the definition's catalogue. That is a real improvement on v3's +`(data: unknown) => number` — the catalogue is static, help can render it, and the +range is checkable at construction. But this is the machine-facing exit contract, +and a typo'd `44` for `4` is currently a runtime failure in the one place where a +wrong value is silently meaningful. + +*Alternative, if it is cheap:* thread the catalogue's key union through the context — +`CommandContext`, with `TOutcome` inferred +from `keyof CommandDefinition['outcomeCodes']` and `present`'s option typed +`outcomeCode?: TOutcome`. That makes an undeclared code a compile error and keeps +everything else unchanged. If threading a second parameter through `Handler` and +`CommandHandler` proves awkward, the runtime check is acceptable — the catalogue +being static is what mattered. Referral to the principal-engineer pass for the +feasibility call; nit either way. + +### C6 — Two undefined combinations in the format/level matrix. + +Both one-line documentation fixes. + +(a) `--json --quiet` is not in the materialization table (lines 194–196: human, +human+`--quiet`, json). The platform resolves the equivalent by json-first +precedence (`command-runner.ts:118-127`); say so. + +(b) The relationship between `-q/--quiet` and `--log-level` is unstated. As written +they are orthogonal — `--quiet` selects which *presentation* materializes, +`--log-level` filters *commentary* — which is a clean split and better than the +shipped CLIs manage. But it leaves `--quiet` alone still emitting step lines and +progress at level `info`, which is probably not what a user typing `--quiet` +expects. Either state that `--quiet` implies `--log-level error`, or state +explicitly that it does not. + +Related nit: at `--log-level warn`, every non-`message` event kind is suppressed +(line 92 puts them all at `info`), so steps, progress, endpoints, artifacts, and +status transitions all vanish together. That is defensible but is a fairly blunt +grouping for six distinct event kinds; if evidence later shows users want progress +without step chatter, the per-kind level assignment is where to look. + +### C7 — Residual asymmetries and small gaps + +All nits, grouped for brevity. + +- **`createTestCli.config` is still `Record`** (line 726) while + `Runtime.config` is `LoadedConfig` (line 702). So a product repo still cannot + test the invalid-section path — the flagship behavior the new `ConfigSection` + machinery exists to produce, and the one R10 calls out. Accepting + `LoadedConfig | Record` closes it. Also `credentials?: Credentials` + (line 727) is a value where `Runtime` has a `getCredentials()` function, so token + refresh cannot be exercised. +- **A session's completed envelope shape is unstated.** `CompletedEnvelope.result: T` + is required (line 619) and a session returns `Result`; say that a session's + result frame carries `result: null, outcomeCode: 0`. +- **Two sources feed `nextActions` on the completed path** — aggregated `remediation` + events (lines 152–153) and `presentation.next` (line 227) — with no stated order or + duplicate rule. +- **`requiresCredentials`** (line 443) is a good addition, but `getCredentials()` + still returns `Credentials | undefined` even for commands that declared it, so + those handlers still handle an impossible `undefined`. Documenting the guarantee + is enough; tightening the type is not worth threading another parameter. +- **`CommandSet` and `MountedTree`** (lines 665–670) are mutually assignable aliases, + so "distinct alias so the two maps never read as one" is true for readers and not + for the compiler. That is fine and I would not brand it — the mount map is written + once, in one file, and reviewed as a literal. +- **`CompletedEnvelope` / `ErroredEnvelope`** name two different axes (completion, + erroring) for one distinction. `Completed`/`Errored` is acceptable and every + alternative I can construct on a single axis is worse. Recording that I looked. + +### C8 — The new `errors` Block member (operator amendment) + +```ts +| { readonly kind: 'errors'; readonly errors: ReadonlyArray } +``` + +**The move is right, and one part of it is the best thing in §7.** Handing the +engine real `CliStructuredError` values and letting it render them with the same +layout it uses for top-level errors is exactly R5's argument applied where it was +previously leaking: without this, `migration check` and `db verify` would have had +to hand-format `✖ summary (CODE)` / Why / Fix into `Block.list` strings, and the +two error layouts in the CLI would have drifted within a release. The block takes +no `Ui` and needs none — the engine owns the layout completely, which is the +correct consequence of "products never hand-build error presentation". Five +observations, in order of importance. + +**1. The name puts the word "error" on both sides of the file's central +distinction.** v4's whole model turns on ERRORED (did not complete) versus +COMPLETED (ran to its end, possibly with bad news). This block lives strictly on +the completed side and is called `errors`. A reader who has just learned that +distinction meets `kind: 'errors'` inside a completed result and has to un-learn it. +Reads-cold fires, and so does consumer-vs-essence: every other `Block` member names +a *layout* the engine draws (`summary`, `fields`, `table`, `list`, `tree`), while +this one names its payload's type. + +The word the rest of the system already uses for exactly this concept is +**diagnostics**: `SectionValidation.diagnostics` and `LoadedConfig.diagnostics` in +this same file (lines 249–250, 713), and the ORM's shipped `migration status` +`diagnostics[]` with per-item `hints[]` (`json/schemas.ts:78-103`). All three mean +the same thing — structured findings produced by a run that completed. +*Alternative:* `kind: 'diagnostics'`, same payload. One word, and the completed and +errored paths stop sharing a root. + +**2. It does invite misuse as a substitute for `notOk`, and the guardrail is +missing rather than weak.** A handler that hits a genuine did-not-complete condition +can now write `ok(ctx.present(data, { human: () => [{ kind: 'errors', errors: [e] }] }))` +and exit 0. It renders identically to a real error, so a human cannot tell; the only +signals that differ are the envelope's `ok` and the exit code — which are precisely +what agents and CI branch on. So the failure mode is invisible to people and wrong +for machines, which is the worst combination. + +Two fixes, both cheap and neither requiring a new concept. +- *Write the test down.* The distinguishing question is crisp and currently unstated: + use `notOk` when the command could not do its job; use this block when finding + these was the job. One sentence in the doc comment. +- *Make the engine check it.* The engine renders the block, so it can see it. Require + that a completed result containing severity-`error` entries also carries a non-zero + `outcomeCode` from the catalogue — verifiable at the same point the engine already + verifies the code against the catalogue (line 271). That turns "don't smuggle + failures through the completed path" from advice into a rule, and it costs nothing + for legitimate uses, which all have an outcome code anyway (`migration check` exits + 4, `db verify` exits on drift). + +**3. `CliStructuredError` carries ADR 239's optional `severity`, so this block is not +always errors — which reinforces point 1 and raises a second question.** Config +diagnostics and lint findings are routinely `warn` or `info` (ADR 239 keeps those +values specifically for "advisory lint/budget surfaces"). So `kind: 'errors'` will +frequently carry non-errors. And it is unstated whether a `warn`-severity entry here +aggregates into `CompletedEnvelope.warnings`, which today is fed only by +severity-`warn` `message` events (line 622). Two producers of the same concept, one +aggregating and one silent. Say which. + +**4. The data/json side is a convention where it could be structural — and this is +where the amendment can pay for itself twice.** The doc instructs the product: "In +the data/json side, carry the same errors as their envelopes (`toEnvelope()`)." +That is unenforced, and it is the second place in the file where the human and json +renderings of one result can silently disagree (the first being `presentation.json` +overriding `data`). But the engine already holds these values — and it already +performs exactly this kind of aggregation twice, pulling `warnings` from `message` +events and `nextActions` from `remediation` events. + +*Alternative:* aggregate them the same way. `CompletedEnvelope` gains +`readonly diagnostics: readonly CliErrorEnvelope[]`, populated by the engine from +the block. Consistency becomes structural rather than remembered, the instruction to +products disappears, and — the second payment — the dotted codes reach machine +consumers on the completed path automatically, which is most of what C1 was about. +I would rank this the single most valuable follow-up in this document. + +**5. Minor asymmetry with the errored path.** On the errored path the engine derives +`nextSteps` from the error's `fix` (line 638). On the completed path a block may +carry N errors each with its own `fix`, and none of them feed `nextSteps` or +`nextActions`. Almost certainly deliberate — N fixes would flood the envelope — but +it means a user gets "Fix:" lines in the human rendering that have no machine +counterpart, which inverts the usual direction of that gap. One sentence stating the +rule is enough. + +--- + +## Part 3 — Referrals to the principal-engineer pass + +- `SingleChar`'s actual checker behavior (C3) — does it reject every literal, accept + everything, or something else? +- Feasibility of threading the outcome-code union through `CommandContext` (C5). +- Whether `ctx.present`'s inference lands across a handler with several return + sites, now that `Presentations` is non-generic (B3's fix changes the inference + shape). +- `Omit, 'kind'>` as the `define*` parameter (lines 478, 514, + 551): confirm `Omit` over a generic interface preserves the mapped `flags`/ + `positionals` inference rather than widening it. +- The buffered, non-backpressuring `report()` (line 96) against a session emitting + into a slow pipe — the trade is stated; the drop or unbounded-growth behavior is + not. +- Second-signal force-exit (lines 55–57) interacting with the "calling report() + after resolution is an InternalError" rule during teardown. + +--- + +## Verdict + +**Nothing structural remains. This is a clean pass.** + +v4 closes every substantive finding from both prior rounds. The two I called +structural in round 2 are closed in the strongest available way: the outcome-code +catalogue with return-site selection makes the header's no-callbacks invariant +actually true, and the `kind` discriminant stamped by the `define*` functions turns +`AnyCommand` into a union the engine can genuinely switch on. The naming problems +are gone — `Presentations` and `presentation` are the recipe and the dish under two +words, and the surrounding vocabulary rehabilitated `ctx.present` without renaming +it. + +Three rulings deserve to be recorded as improvements on what the reviews asked for, +not merely as decisions. Completed/errored semantics dissolved my B10 rather than +overruling it: separating "did not succeed" from "did not complete" is a better +model than bolting a second presentation system onto the error path, and it makes +the bad-news commands in the corpus expressible as what they are. One log mechanism +beat my proposed `verbose` presentation member, because it keeps display policy in +one place instead of two. And the prompt-default rule — a destructive confirmation +declares no default, so `--yes` structurally cannot pass it — is a better answer to +typed destructive confirmation than the `confirmDestructive` method I proposed, +because it makes the safe case the default case rather than an opt-in. + +The late `errors` block amendment is the same kind of improvement: giving the engine +real `CliStructuredError` values to render, rather than letting products format +`✖ summary (CODE)` into strings, closes the last place where the two error layouts +in the CLI could drift. Its problems are a name and a missing check, not a shape — +call it `diagnostics` (the word the config machinery and the ORM already use, and it +stops the completed path sharing a root word with the errored path), and have the +engine require a non-zero outcome code when the block carries severity-`error` +entries, so the completed path cannot be used to smuggle failures past `ok`. + +One item to settle before implementation, and it is a reconciliation rather than a +redesign: **C1**. Moving verify, runner, and check outcomes off the error path means +a completed-but-bad result reaches machine consumers with an integer and no dotted +code at the envelope level, while ADR 239 currently classifies exactly those +outcomes as structured failures with `CONTRACT.VERIFY_FAILED` / +`MIGRATION.RUNNER_FAILED` / `MIGRATION.CHECK_*` codes and exit 2. R6 names `code` as +one of the three keys consumers branch on. The `errors` block narrows this +considerably — the per-finding codes now have a home — and the cleanest completion +is C8's fourth point: have the engine aggregate that block into a +`CompletedEnvelope.diagnostics` field the same way it already aggregates warnings +from `message` events and next actions from `remediation` events. Adding the dotted +code to the outcome catalogue entries closes the remainder. The ADR's exit-code +paragraph then needs a corresponding amendment, which is its owner's call. + +Below that, two small defects worth fixing while the file is open: `SingleChar` +claims a constraint it does not enforce (C3), and `InputStream` as a plain +`AsyncIterable` cannot support the keypress-driven prompts §4a describes +(C4) — one optional `setRawMode` member resolves it and keeps the surface +runtime-agnostic. + +Everything else in this document is a nit: undefined `--json --quiet` precedence, +the `--quiet`/`--log-level` relationship, `Ui`'s missing masking helper, the test +harness's config shape, an unnamed envelope union, whether `warn`-severity entries +in an `errors` block reach the envelope's `warnings`, and a handful of one-sentence +documentation additions. None of them should hold up implementation, and several +are better decided against real usage than in the abstract. + +I have no further architectural concerns. The loop can close. diff --git a/.drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review.md b/.drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review.md new file mode 100644 index 00000000..a64e79b7 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review.md @@ -0,0 +1,667 @@ +# System design review — the unified CLI engine's public interface + +Subject: `wip/designs/engine/engine-interface-draft.ts` (a design artifact: type +declarations and doc comments, not shipping code). + +Pass: **architect**. The lens is system shape, vocabulary, boundaries, +dependency direction, and conceptual integrity. Implementation correctness, +failure modes, and operability are the principal-engineer pass's job; where I +noticed something in that territory I list it under "Referrals" instead of +arguing it here. + +Sources read in full: `cli-engine-requirements.md` (R1–R14), +`wip/designs/engine/output-modes-survey.md`, prisma/prisma +`docs/architecture docs/adrs/ADR 239 - Errors are structural envelopes with +dotted namespace codes.md`, composer `ADR-0043`/`ADR-0044` (titles and +decisions), the sibling foundation design `wip/designs/1a/design.md`, and the +platform CLI shell layer (`wip/repos/prisma-cli/packages/cli/src/shell/`: +`output.ts`, `command-runner.ts`, `global-flags.ts`, `runtime.ts`, `ui.ts`, +`errors.ts`, `prompt.ts`, `help.ts`, `next-actions.ts`). + +Note on ADR 245: no such file exists in this repo (the ADR series stops at 243). +The `Result` conventions it is cited for are, however, recorded in +`wip/designs/1a/design.md` §"Results carry one discriminator", and the draft +matches them. The draft's use of `CliStructuredError` from +`@prisma/cli-foundation` also matches design 1a (which settles both the class +name and the package name) even though ADR 239's own example spells the +interface `StructuredError`. No action; recording it so a later reader does not +"fix" it in the wrong direction. + +--- + +## 1. What is being introduced + +In plain language, the draft proposes nine concepts. + +1. **An event vocabulary** (`EngineEvent`, nine members). A running command can + say: a phase started, a phase ended with an outcome, N of M items are done, + here is a warning, here is a note, here is a line a child process printed, + here is something you could do about it, here is a URL that now works, here + is a state change in something I am watching. Each member may carry a + product-defined `data` payload the engine never interprets (R14). + +2. **A handler's world** (`CommandContext`). One object holding the product's + config section, credentials, the function that emits events, a prompting + surface, an abort signal, and the working directory. R4's "the whole world + arrives as one argument". + +3. **A declaration vocabulary for arguments** (`flag.*`, `positional.*`). Small + builder functions whose return types carry a phantom type parameter, so that + `ArgsOf` can compute the handler's argument type by inference (R1). + +4. **A command declaration** (`CommandDefinition`): the words shown in help, the + flags and positionals, a function that lazily imports the real handler (R9), + a presenter triple, and an escape hatch for commands that take over stdin and + stdout. + +5. **A presentation vocabulary** (`Block`, `Ui`). A product returns a list of + structured blocks — a summary line, a label/value list, a table, a bullet + list, a next-steps list — plus three text-styling helpers. There is no way to + write bytes (R5). + +6. **A product's export surface** (`CommandSet`): named commands with no paths. + +7. **Shell-side mounting** (`createCli`): the shell supplies the binary's name + and version, the group headings, and a map from space-separated path to + command (R12). + +8. **The injected environment** (`Runtime`, `LoadedConfig`): streams, cwd, TTY + facts, a signal, the loaded config with per-section diagnostics, credentials. + +9. **An in-repo test harness** (`createTestCli`, `TestCli`): argv in, bytes and + events out, using the production machinery (R7). + +The overall shape is right, and the derivation discipline is visible: almost +every member of `EngineEvent` and `Block` can be traced to a numbered structure +in the survey's §C ranking. The findings below are about the places where a name +does not say what the thing is, where a set does not cover its space, where two +sides of a symmetric pair have different shapes, and where a requirement has no +type to live in. + +--- + +## 2. Subsystem fit and boundary correctness + +**Dependency direction is correct.** Products depend on the engine package and +on the zero-dependency foundation for `Result` and `CliStructuredError`; the +engine depends on neither product; the shell depends on both and owns the tree. +Nothing in the file names stricli, commander, clipanion, clack, or colorette, so +R3 holds at the level of names. + +**One stricli-shaped assumption does survive** — see finding A20 on +per-command-only flags. It is not a stricli *type* in the interface, so R3 is +not violated in the letter; but the "there are no global flags" rule that R5 +states, and that the requirements doc's own closing section says was adopted +partly because it "neutralizes" stricli's per-command-flags limitation, has been +carried into the public interface as a shape: `flag.json()` exists and +`flag.quiet()` / `flag.verbose()` / `flag.color()` do not, with no statement of +what happens to the other six flags every shipping CLI has. That is a framework +limitation showing through the contract, which is what R3 is meant to prevent. + +**The two-level split — `Runtime` (the environmental whole, injected once) and +`CommandContext` (the handler's narrow world) — is the right boundary.** The +test in favour of it: `Runtime` holds things a *process* has (streams, TTY-ness, +loaded config, cwd) and `CommandContext` holds things a *command* has (its +config section, its way of speaking, its abort signal). Products can only reach +the second, which is what makes R4's runtime-agnosticism and testability claims +true. Three things sit in the wrong layer or are absent from it; see A13–A16. + +**Where the boundary is not yet drawn at all:** the config *section* is a +first-class concept in R10 (named section, never-throwing validator, per-section +diagnostics, "a command fails only if a section it needs is invalid") and has no +representation anywhere in the interface. This is the largest structural gap and +is finding A12. + +--- + +## 3. Naming and typology findings + +Each finding names the thing, states the problem, and proposes a concrete +alternative. Line numbers refer to the draft. + +### Events + +**A1 — `notice` vs `warning`: one axis spelled two ways, and a third time +elsewhere.** (lines 61–64.) `warning` and `notice` differ only in severity, and +severity is already modelled as a *field* in two other places in the same file: +`step-finished.outcome` (line 50) and `Block.summary.tone` (line 258). So the +file encodes "how serious is this" as a kind in one place and a field in two +others. Symmetry probe fires. Also `notice` reads cold as an official +announcement ("a notice of termination"); the thing meant is an informational +line. +*Alternative:* one member, `{ kind: 'message'; tone: 'info' | 'warning'; message: string }`, +reusing the same tone vocabulary as `Block.summary`. Three concepts collapse to +one axis used consistently. + +**A2 — `output` is the most overloaded word available, and `stream` is a +mechanism.** (lines 69–75.) The concept is "one line that a child process or a +remote log stream produced". The word `output` in this same design also means +the presenter triple's job, the `--json` payload, and the survey's own title +("output modes"). A reader with no context will parse `kind: 'output'` as "the +command's output". Separately, `stream: 'stdout' | 'stderr'` names two OS pipes; +the survey's own evidence (§B6, `controllers/build.ts:34-150`) is a *remote* +build-log stream that has no pipes and routes by a `level` field instead — so +remote logs must pretend to have file descriptors. +*Alternative:* `kind: 'process-output'` (or `'log-line'`), with +`channel: 'out' | 'err'`, and document that a remote stream maps its severity +onto the channel. + +**A3 — `status` is both the kind and the field, and it drops the transition.** +(lines 95–100.) `{ kind: 'status'; subject; status }` reads awkwardly cold, and +more importantly the survey's §C10 conclusion is explicit: "Any engine 'wait' +concept needs a **from→to** status-transition event (the platform already emits +exactly that, controllers/app.ts:2651-2662)." The draft records only the new +value, so a consumer that joins the stream late cannot tell a transition from a +re-assertion, and the human renderer cannot print "pending_dns → verifying". +*Alternative:* `{ kind: 'status-changed'; subject: string; from?: string; to: string }`. + +**A4 — `step` is a display name doing duty as an identity, and nesting is +asserted in prose but absent from the type.** (lines 44–60.) The doc comment +says "steps may nest", and the ORM's shipped dialect models *all* +operation-specific progress as nested spans with `spanId` / `parentSpanId` +(survey §B3, `control-api/types.ts:91-111`). The draft has neither an id nor a +parent, so nesting cannot be expressed, concurrent steps cannot be paired +start-to-finish, and `progress.step?: string` (line 56) refers to a step by its +display string. +*Alternative:* either add `id: string` and `parentId?: string` and let `progress` +and `step-finished` reference `id`; or state in the type's doc that steps form a +strict stack (last-started is the one that finishes) and delete the nesting +claim if that is not true. + +**A5 — `step-finished.outcome: 'ok' | 'failed' | 'skipped' | 'warning'` mixes +two vocabularies.** (line 50.) `'ok' | 'failed'` is an outcome; `'warning'` is a +severity; `Block.summary.tone` spells the same space `'ok' | 'error' | +'warning' | 'info'`; ADR 239's `severity` spells it `'error' | 'warn' | 'info'`. +Four spellings of one axis across the settled conventions and this file. +*Alternative:* pick one tone vocabulary — `'ok' | 'warning' | 'error' | +'skipped'` — and use exactly it in `step-finished`, `Block.summary`, and the +message event from A1. Reconcile against ADR 239's `severity` values in the same +change (`warn` vs `warning` is a live inconsistency in the settled surface). + +**A6 — `remediation` is the third of three spellings of one concept inside one +file.** (lines 81–86.) The survey's §C2 identifies "remediation / next-step in +five competing encodings" as "the clearest case of one engine concept currently +spelled five ways." The draft reduces five to three — the error's `fix`, the +`remediation` event, and `Block.nextSteps` — and its own doc comment (lines +78–80) names a fourth, "the success envelope's nextActions", which does not +exist in the file at all (see M1). Worse, the shipping platform concept is +richer: `NextAction { kind, journey, label, command, commands, reason }` +(`shell/next-actions.ts`), and the draft's `{ label, command? }` is a silent +subset. +*Alternative:* lift one type — call it `NextAction`, matching the shipping name — +into the interface, and use that same type in the event, on the success +envelope, and as the payload of the `nextSteps` block. Then there is one concept +with one shape and three placements, instead of three concepts. + +**A7 — the event vocabulary has no sensitivity marker, while `Block` does.** +(lines 43–100 vs line 259.) `Block.fields` rows carry `sensitive?: boolean`, and +the survey's §C5 records credential masking as real, shipped policy in two +families (`maskConnectionUrl` / `sanitizeErrorMessage` in the ORM; +`URL_CREDENTIALS_PATTERN` + `maskValue` in the platform, `ui.ts:10`). An +`endpoint.url` or an `output.line` can carry a connection string with a +password, and the product has no way to say so and the engine no way to know. +Asymmetry within one file for one policy. +*Alternative:* either give the engine a masking helper on `Ui` and a +`sensitive?: boolean` on `endpoint`, or state that the engine masks credential +patterns unconditionally in every rendered string (which is the stronger, more +R5-shaped answer) and delete `Block.fields.sensitive`. + +**A8 — evidence-ranked structure #7 has no home: files written.** The survey +ranks "file paths / artifacts written" as a 3/3-family recurring structure +(§C7: ORM `files{json,dts}`, `filesWritten[]`/`filesDeleted[]`, `dir`, +`baselineDir`, all relativized to cwd; Composer `stackFilePath`). Under R14's own +promotion rule ("a structure recurring across commands or products is the signal +that the engine vocabulary is missing a concept"), this is a promotion +candidate that was not promoted, and the draft does not say why. It also has no +`Block` — a product would have to pre-format paths into `Block.list` strings, +which puts path relativization (an engine policy today) into product code. +*Alternative:* either add `{ kind: 'artifact'; path: string; action: 'written' | +'deleted' | 'unchanged' }` plus a `Ui.relativePath(p)` helper, or record in the +draft's own doc comment that artifacts are deliberately deferred and why. + +**A9 — `endpoint.name` versus the shipped vocabulary.** (lines 88–93.) Composer's +shipped type is `ServiceEndpoint { address, url }` (`operations/shared.ts:19-22`) +where `address` is the service's coordinate, not a label. `name` is fine if it +means the human label, but a reader coming from Composer will populate it with +an address. One clarifying word in the doc comment resolves it. Low priority. + +### The presenter triple, `Block`, and `Ui` + +**A10 — `present.stdout` names a file descriptor in an interface whose entire +point is that products cannot write to file descriptors.** (lines 228–232.) +`human` is named for its audience, `json` for its format, `stdout` for a stream — +and R5 says "they cannot print… the interface offers no way to express it," yet +the key is the name of a stream. It is also not even distinguishing: under +`--json` the machine payload goes to stdout as well. The essence of the three is: +prose for a person, the machine-usable payload lines that survive `--quiet`, and +the structured projection. +*Alternative:* `{ human, payload, json }` (or `{ prose, data, json }`). The +platform's own `renderHuman` / `renderStdout` / `renderJson` +(`shell/command-runner.ts:25-37`) has the same flaw; generalizing it is the +moment to fix it, not to enshrine it. + +**A11 — `Block` is missing the single most-cited human structure in the survey: +the tree.** (lines 257–262.) The survey records tree rendering across all three +families and many commands: the ORM's migration graph visualization with +cross-space column alignment (`commands/migrate.ts:435-470`), introspection trees +(`db schema`), migration list/status trees, ADR 227's "migration read commands +share one graphical renderer", ADR 229's line-plane-occlusion renderer; and +Composer's deployment topology tree (`render-deployment.ts:77-116`). With no +tree block, every one of those commands must either pre-format ASCII into +`Block.list` strings — which is product-side rendering by another name and +exactly the hole through which the drift R5 exists to kill returns — or the +engine grows a custom escape hatch per command. +Separately, `Block.list` (line 261) is the one member with no cited evidence: it +is `fields` without labels, or a one-column `table`. And `Block.nextSteps` (line +262) is data, not layout — see A6. +*Alternative:* add a `tree` block (recursive `{ label, children? }` nodes, engine +owns the glyphs and alignment); drop `list` unless evidence appears, or keep it +and delete `nextSteps` in favour of the shared `NextAction` type. + +**A12 (naming) — `Ui` reads cold as "the user interface"; it is a text-styling +helper with three verbs, one of which is a rendering decision.** (lines 265–269.) +`emphasize` and `code` are semantic (this matters; this is a literal token). +`dim` is the ANSI concept itself — a product choosing "dim" is a product making a +presentation decision, which R5 assigns to the engine. And two policies the +engine visibly owns today are absent: value masking (§C5) and path +relativization to cwd (§C7, "nearly everywhere"). +*Alternative:* rename the type `TextStyle`; replace `dim` with `deemphasize`; +add `mask(value)` and `relativePath(path)`. + +### The context / runtime split + +**A13 — `CommandContext` is an unbound type parameter: nothing +declares which section a command needs, and nothing registers a validator.** +(lines 106–110, 194–199, 313–316.) R10 requires each product to contribute "a +named section and a never-throwing validator", and requires that "a command +fails only if a section it needs is invalid." The interface has `LoadedConfig` +with `sections: Record` and per-section `diagnostics`, and it +has a `TConfig` generic that the product simply *asserts*. There is no key, no +validator, and therefore no way for the engine to know which section to hand +over or which diagnostic should fail this command. The doc comment on line 107 +promises behavior the type cannot support. +*Alternative:* make the config section a first-class concept — +`defineConfigSection({ name, validate })` returning a token carrying the +validated type; `CommandDefinition.configSection?: ConfigSectionToken` binding +`TConfig`; `createCli({ sections: [...] })` registering them. Then +`CommandContext` is derived rather than asserted, and R10's failure rule +becomes mechanical. + +**A14 — `Runtime` has no `env`, so the engine must read `process.env` itself.** +(lines 300–311.) The engine owns interactivity policy and colour policy (R5). +Both are environment-driven in every shipping implementation: `canPrompt` reads +`runtime.env.CI` (`shell/runtime.ts:113`), colour reads `NO_COLOR`/`FORCE_COLOR`. +`Runtime` is described as "everything environmental, injected once by the bin (or +by a test)" and omits the environment. The consequence is that the two behaviors +most worth testing are the two a test cannot control. +*Alternative:* `readonly env: Readonly>` on +`Runtime` (and only on `Runtime` — R4 keeps it out of `CommandContext`). + +**A15 — `Runtime.isTty` covers `stdin` and `stderr` but not `stdout`.** (line +305.) The whole stdout-is-data discipline keys on stdout, and the ORM's shipped +behavior is to auto-select JSON when *stdout* is not a TTY (survey §D, +`utils/global-flags.ts:67-69`). The set is incomplete for its own space. +*Alternative:* `isTty: { stdin, stdout, stderr }`. + +**A16 — `signal` appears on both `Runtime` (line 306) and `CommandContext` +(line 126) under the same name.** If they are the same object, one is redundant; +if the context's is a per-command child that also fires on command-level timeout, +the type should say so. Symmetry probe fires either way. Also `Runtime.credentials` +and `CommandContext.credentials` are the *same* type, whereas config narrows from +`LoadedConfig` to one section — the two cross-cutting values are handled +asymmetrically with no stated reason. + +**A17 — `Credentials` is declared in the engine but documented as owned +elsewhere, and its name is too broad.** (lines 132–136.) The comment says +"opaque to the engine; shape owned by the Cloud product's auth library" while the +engine declares two concrete fields. Either it is opaque (then type it as an +opaque carried value and let the Cloud product's library define the shape) or the +engine reads it (then the fields are the engine's and the comment is wrong). A +concept cannot live in one bounded context and be owned by another. Separately, +`Credentials` reads cold as "any credentials" — database URLs, git tokens, bucket +keys are all credentials in this product family. +*Alternative:* `ManagementApiSession` (or `PlatformSession`), with the ownership +question resolved one way or the other. + +**A18 — `PromptSurface` is engine-internal jargon, and the set is short of what +the evidence needs.** (lines 138–148.) "Surface" is a word this design team uses; +a contributor reads `ctx.prompt` and wants a type called `Prompts`. More +substantively: there is no representation of `-y/--yes` (shipped in both the ORM +and the platform), so every product will re-add it as a per-command flag and +re-implement pre-acceptance — the precise divergence R5 exists to prevent. And +the survey's §C11 records typed destructive confirmation (`--confirm `) +as a 2/3-family pattern with no home here. +*Alternative:* rename to `Prompts`; add `confirmDestructive(question, { expect })`; +and make `--yes` an engine-owned concept that `confirm` consults, not a flag each +product declares. + +**A19 — no interactivity capability fact on the context.** The survey's §F6 +conclusion is that the prompt check is "an engine-level capability check, not +per-command logic", and the shipped code branches on it *before* prompting: +`git connect` polls only when `canPrompt` and errors immediately otherwise +(`controllers/project.ts:1708-1717`). With the draft's design a handler learns +its environment only by attempting a prompt and inspecting the failure. +*Alternative:* `readonly interactive: boolean` on `CommandContext`. This is a +fact about the environment, not a rendering decision, so it does not weaken R5. + +### Flags and positionals + +**A20 — `flag.json()` is a real concept wearing the wrong clothes, and it is the +only member of a family of seven.** (lines 167–168.) Two problems. + +*Shape.* `--json` is not an argument the handler consumes; it is a declaration +that the command has a machine-readable mode. The evidence that it is different +in kind: it switches stream discipline, suppresses prompts (`canPrompt` returns +false under json, `shell/runtime.ts:106-108`), disables progress rendering +(`progress-adapter.ts:36-38`), and changes envelope behavior on crash. Typing it +as `FlagSpec` puts `args.json` in the handler's arguments and invites +the handler to branch on it — which is presentational logic re-entering product +code through the front door. It also has no `brief`, unlike every sibling +builder: a tell that it is not the same kind of thing. +*Alternative:* delete `flag.json()` and derive the capability — a command +supports `--json` exactly when it declares `present.json` (or unconditionally, +since `present.json` already defaults to the raw value). The flag then never +appears in `ArgsOf` and the handler cannot see it. + +*Completeness.* The shipping CLIs have `--json`, `-q/--quiet`, `-v/--verbose`, +`--trace`, `-y/--yes`, `--interactive`/`--no-interactive`, `--color`/`--no-color` +(`shell/global-flags.ts:23-45`; ORM `utils/command-helpers.ts:368-388`). The +draft names `--quiet`, `--no-interactive`, and `--json` in doc comments and +declares exactly one of them. Whatever the answer is — engine-injected on every +command, or a `flag.*` entry each — it must be the same answer for all seven. +Engine-injected is the right one: `--trace` changes error rendering and +`--verbose` changes diagnostics, both squarely engine concerns under R5. If they +are engine-injected, then so should `--json` be, which is the same conclusion as +above by a second route. + +**A21 — required-ness is spelled with opposite defaults and opposite naming on +the two sides of one axis.** (lines 158–177.) `flag.string()` is optional and +`flag.requiredString()` is required; `positional.string()` is *required* and +`positional.optionalString()` is optional. A reader who learns one side will read +the other wrong. This is the clearest reads-cold failure in the file. +*Alternative:* one convention. Either mark both non-defaults explicitly +(`flag.string` / `flag.requiredString`; `positional.requiredString` / +`positional.string` — no, that just moves the problem), or make required-ness a +spec field on both: `flag.string({ brief, required: true })`, +`positional.string({ brief, placeholder, required: false })`. The field form +reads correctly cold on both sides and removes four builder names. + +**A22 — the builder set is incomplete for its own space.** (lines 158–177.) No +`flag.number()` / `flag.integer()`, though the survey's one poll verb takes +`--timeout ` with `--timeout 0` meaning "probe once" +(`commands/app/index.ts:592-633`). No required variant of `enum` or `repeated`; +no enum-typed `repeated`; no optional/required distinction on `repeated` at all +(it returns `readonly string[]`, so "not passed" and "passed empty" are the same +value). Discriminator-completeness applied to a builder set rather than a union. + +**A23 — `FlagSpec` carries only a phantom type; the flag's *name* is the +record key, and the transliteration rule is unstated.** (lines 171–172, 207.) A +flag called `--dry-run` must be the key `dryRun` (or `'dry-run'`, quoted) and the +engine must transliterate. That rule is the single most likely thing a +contributor gets wrong and it appears nowhere. Also absent from the spec, though +all present in today's CLIs: short aliases (`-q`, `-v`, `-y`), `default`, +`hidden`, `deprecated`. +*Alternative:* state the key→flag-name rule in the `flags` doc comment, and add +`alias?`, `default?`, `hidden?`, `deprecated?` to the specs. + +### The command definition + +**A24 — `raw` names the mechanism, has three spellings for two states, and +contradicts `present` being required.** (lines 234–239.) The concept is the +survey's mode 7: "the process becomes a protocol endpoint" (`lsp`). `raw` names +the byte-level consequence, not the thing. The type `false | { reason: string }` +gives "no" two spellings (`undefined` and `false`) and "yes" one, so the reader +must learn that a boolean-looking field is really a presence check. And because +`present` is a *required* member of `CommandDefinition` (line 228), a raw command +must supply a presenter that can never run — the impossible state is +representable, while the doc comment says the engine will reject it at runtime. +*Alternative:* make `CommandDefinition` a union of a standard command (with +`present`, no protocol field) and a stdio-server command +(`stdioServer: { reason: string }`, no `present`, no `flags` beyond transport). +Then the engine's runtime check disappears into the type. Keep the required +`reason` — a mandatory written waiver on an escape hatch is a good idea; add one +line saying it exists for review pressure, since it is displayed nowhere. + +**A25 — `handler` is named for the thing it returns, not for what it is.** (lines +214–219.) The value is a module loader; the handler is its default export. Two +concepts, one name. +*Alternative:* `load: () => Promise<{ default: Handler }>`, with an exported +`Handler` type so products can name their handler's +type without spelling the whole signature. + +**A26 — groups get a strictly poorer declaration than commands, and groups are +the more-visited help pages.** (lines 200–205 vs 286.) A command declares +`brief`, `description`, `examples`; a group declares only `brief`. Today's +platform descriptors carry `description`, `longDescription`, `examples`, and +`docsPath` for every node including groups, and `help.ts` renders all four +(`shell/command-meta.ts`, `shell/help.ts:13-42`). `prisma db --help` will be a +one-line heading and a list. +*Alternative:* groups take `{ brief, description?, examples? }` — the same shape +as a command minus arguments. And add `docsPath?` (or `docsUrl?`) to both: +ADR 239 makes docs URLs part of the settled error surface and the platform +already renders a "Read more" row (`ui.ts` docs-path rendering). + +**A27 — `present` is required even for commands with nothing to present.** (line +228.) `app run` hosts a dev server and rejects `--json` outright +(`controllers/app.ts:273-281`); `app logs` streams; a session command's terminal +value is `void`. Each must write `human: () => []`. +*Alternative:* make `present` optional when `TResult` is `void`, or model +session/streaming commands as their own declaration variant alongside A24's +union. + +### Mounting + +**A28 — space-separated paths are the right shape; the type should say so.** +(lines 283–288.) A path key that reads exactly like the invocation (`'db +migrate'`) makes the tree reviewable in one glance, makes collisions string +equality, and keeps the grammar checkable in one place — which is precisely R12's +argument. It beats nested objects for a tree this flat. Two refinements: give it +a named type (`export type CommandPath = string`) with the grammar in its doc +comment (lowercase words, single spaces, kebab-case within a word), and state how +help ordering is determined, since object key order is currently the de facto +answer and nobody has agreed to it. + +**A29 — `CommandSet` is declared and then not used, and the two maps that share +its structure mean different things.** (lines 276, 287, 323.) `CommandSet = +Record` where the key is a *command name* (product +side, R12: no paths). `createCli`'s `commands` is the identical structural type +where the key is a *path*. `createTestCli`'s is a third inline copy. Three +spellings, two meanings, one structure — a reader cannot tell them apart, and +neither can the compiler. +*Alternative:* use `CommandSet` for the product side and introduce +`CommandMounts = Readonly>` for the shell +side, with A28's branded path type making the distinction visible. + +**A30 — the shell can place a command but cannot rename it.** (lines 283–288.) +R12's own evidence is six months of renames and regroupings across product lines +(`app` → `service`, `database` → `postgres`) that no product would have made +locally. Those renames change user-facing *words*, and `brief` — a user-facing +sentence written by a product before its final path was known — is not +overridable at the mount. The shell owns the tree but not the tree's prose. +*Alternative:* let a mount entry be either a `CommandDefinition` or +`{ command: CommandDefinition; brief?: string }`. + +### Test harness + +**A31 — `createTestCli` is not the same machinery at the seam R7 promises.** +(lines 322–339.) Production takes a whole `Runtime`; the harness takes four +loosely-typed fields and `run(argv, { stdin })`. Concretely, `config?: +Record` is a bare section map while `Runtime.config` is +`LoadedConfig` with diagnostics — so the harness *cannot construct an invalid +section*, which is the R10 behavior most worth testing in a product repo. There +is also no way to set env (A14), no way to fire the abort signal (so exit code 3 +and session teardown are untestable), and no clock (see referral R6). +*Alternative:* `createTestCli(spec)` plus `run(argv, overrides?: Partial +& { stdin?: string })`, with `config` typed as `LoadedConfig`. + +**A32 — `TestCli.run().json: readonly unknown[]` models the transport, not the +value.** (line 335.) A non-streaming command emits one envelope; an array is the +NDJSON mechanism showing through the assertion surface, and every test must write +`result.json[0]`. +*Alternative:* `envelope?: unknown` for the terminal envelope and `jsonEvents: +readonly unknown[]` for the stream, so an assertion names what it is asserting. + +--- + +## 4. Missing concepts + +Things the requirements or the survey imply that the interface has no home for. +A13 (config sections), A19 (interactivity), A20 (the other six flags), A8 +(artifacts), and A11 (trees) are already stated above and are not repeated. + +**M1 — the success envelope. This is the biggest omission.** The platform's +shipped success envelope is `{ ok, command, result, warnings, nextSteps, +nextActions }` (`shell/output.ts:9-29`), warnings are rendered in human mode too +so degraded steps are never silent (`command-runner.ts:130-135`), and the survey +calls envelope-level `nextSteps`/`nextActions` on *every* success the platform's +distinguishing asset (§D, "Cross-family delta worth naming"; §C2(b)). The draft +has no envelope type at all. Consequences: (a) a command that succeeds with +caveats has nowhere to put them except a mid-run `warning` event, which is a +different thing — a warning attached to a result is part of the result; (b) +`Block.nextSteps` is a *human rendering* block, so under `--json` the next steps +disappear entirely — a regression against today's platform CLI, and precisely the +field agents consume; (c) the draft's own doc comment at lines 78–80 refers to +"the success envelope's nextActions" as an existing home for terminal +remediation, and it does not exist. +*Alternative:* declare the envelope in the interface — +`Success = { result: T; warnings?: readonly string[]; nextActions?: readonly +NextAction[] }` — and let a handler return `Result, CliStructuredError>`, +or add `warnings`/`nextActions` as a second return channel. The `NextAction` type +is A6's shared type. + +**M2 — the config version marker (R10).** R10 requires `defineConfig` to write a +structural version marker and requires an unmarked file (in particular a classic +Prisma 7 config sharing the filename) to fail early with a typed error, calling a +silent misparse "the worst launch bug available." Nothing in the interface +mentions the marker, `defineConfig`, or the failure; `LoadedConfig` arrives +already loaded. The sibling foundation design does address it +(`wip/designs/1a/design.md` A9), so this may be a deliberate delegation — but the +engine interface should at least name where the boundary is, because the engine +is what fails the run. + +**M3 — exit codes beyond 0–3.** R6 and ADR 239 fix 0/1/2/3, and ADR 239 adds +"Commands may still return a command-specific code for finer classification." +`migration check` already ships exit **4** for integrity failure +(`migration-check/exit-codes.ts:1-3`), and `db verify` and `db sign` ship exit +**1** on drift / verify failure (survey §A1) — which under ADR 239 now means "a +bug in Prisma". The interface offers a handler no way to influence the exit code: +it returns `Result`, and `CliStructuredError` carries no +`exitCode` (today's `CliError` does, `shell/errors.ts:57`). So either the shipped +ORM behavior becomes inexpressible, or the interface needs to say so and require +those commands to change. Both are defensible; neither is stated. +*Alternative:* if finer codes stay, put an optional `exitCode` on the structured +error and state the reserved ranges; if they go, say so here and record it as a +breaking change the migration must make. + +**M4 — typed destructive confirmation.** §C11, 2/3 families: +`--confirm ` on the platform's `project remove` / `transfer` and the +database/bucket variants. `PromptSurface.confirm` is yes/no only. Covered in A18; +listed here because it is a requirement-level gap, not just a naming one. + +**M5 — timeout and deadline semantics for poll commands.** The survey elevates +poll-until-terminal to its own execution mode precisely because it "carries +timeout/deadline semantics, an explicit remote status enum, and transition +events" (§F3). The draft's header decides "timeouts are the handler's business." +That is a legitimate decision, but its consequence is that every poll command +re-implements deadline parsing, elapsed-time rendering, and the timeout error +code — the divergence R5 exists to prevent, arriving through a different door. +At minimum the engine should own the timeout error code and the elapsed +rendering. Worth recording as an accepted risk with a reason. + +**M6 — R13's optional-peer-dependency check.** R13 requires a structured error +naming the missing dependency and the user's own install command, produced by an +execution-time check. It is expressible as an ordinary `CliStructuredError`, so +this is not a hole so much as an unassigned owner: if the engine provides the +check and the error code, it belongs here; if each product hand-rolls it, R13's +"clearly say what is missing" becomes convention again. State which. + +**M7 — telemetry and update-check side processes.** Both the ORM and the platform +spawn a detached child on every invocation (ADR 217; `shell/update-check.ts:225-237`). +They are presumably engine-internal, but they are cross-cutting behavior the +engine will own, and the interface gives the shell no way to configure or +suppress them. Out of scope for this artifact, but currently unhomed anywhere. + +**M8 — durations.** §C9, 2/3 families: ORM `timings: {total}` under `-v`, span +elapsed-ms suffixes, platform `--verbose` timing diagnostics and domain-wait +`mm:ss`. The engine can measure step durations itself by pairing start/finish, so +this is mostly fine — but only if A4's identity problem is fixed, and only if +`--verbose` exists (A20). + +--- + +## 5. Referrals to the principal-engineer pass + +These are implementation-mechanics or failure-mode questions I noticed while +reading; they are not architecture findings and I have not judged them. + +- **R1 — variance of `CommandDefinition` in the collection types.** `CommandSet` + and `createCli.commands` use `CommandDefinition` with its defaults + (`TResult = unknown`). `present.human: (value: TResult, ui: Ui) => Block[]` is + contravariant in `TResult`, so a `CommandDefinition` + is probably not assignable to `CommandDefinition`. If so, `createCli` cannot + accept real commands without a cast — which would undercut R1's "directly + executable" claim. Needs a compile test. +- **R2 — `ArgsOf` when `positionals` is absent.** `positionals?` is optional + (line 208) but `ArgsOf` maps over `keyof D['positionals']` (line 185). + Behavior over `undefined` needs checking. +- **R3 — whether `defineCommand`'s inference actually lands.** The phantom-symbol + `FlagSpec` / `PositionalSpec` design is the whole basis of R1's + "typed by inference"; it needs a worked example compiled, including an enum + flag and a `const` values array. +- **R4 — `report` is synchronous and returns `void`** (line 117) while writing to + a stream is asynchronous and can apply backpressure. Behavior of a session + command emitting thousands of `output` events into a slow pipe is a failure + mode worth a look. +- **R5 — events emitted after the signal fires.** Line 117 promises they "render + normally"; whether the stream is still writable during teardown is an + operability question. +- **R6 — determinism of the test harness.** The `--json` frame carries a + `timestamp` (doc comment, lines 33–35), so `TestCli.run().json` is not + snapshot-stable without an injected clock. +- **R7 — `LoadedConfig.diagnostics[].section: string | null`** — what `null` + means (a whole-file failure?) is not stated and affects which commands fail. + +--- + +## 6. Verdict + +The overall shape is sound and the boundaries are drawn in the right places. The +product/engine/shell layering satisfies R3, R4, R9, and R12 as stated; the +handler protocol (args and context in, events along the way, a `Result` out) is +one mechanism covering six of the survey's seven execution modes with a declared +escape hatch for the seventh; and the derivation discipline is real — most +members of `EngineEvent` and `Block` trace back to a numbered, occurrence-ranked +structure in the survey, which is exactly the evidence standard R14 asks for. +The two-level `Runtime` / `CommandContext` split is the right boundary and is the +part I would change least. + +What the draft is not yet is *conceptually minimal or symmetric*. One axis — +severity — is spelled four ways across `step-finished.outcome`, the +`warning`/`notice` split, `Block.summary.tone`, and ADR 239's `severity`. One +concept — remediation — is spelled three ways inside the file and a fourth time in +a doc comment referring to a type that does not exist. One axis — +required-ness — has opposite defaults on the flag and positional sides. Three +structurally identical maps mean two different things with no way to tell them +apart. Two names take their meaning from a mechanism rather than from the thing +(`present.stdout`, `raw`), and one takes it from a file descriptor inside an +interface whose purpose is that products cannot touch file descriptors. + +Two gaps are more than naming and should be closed before this interface is +implemented against. First, **the config section has no representation at all**: +R10's named section, never-throwing validator, and "fails only if a section it +needs is invalid" rule are all promised in a doc comment that the types cannot +support (A13). Second, **there is no success envelope** (M1): warnings and next +actions attached to a successful result — shipped today on every platform +command, and the one thing the survey singles out as the platform's advantage — +have nowhere to live, and `Block.nextSteps` silently drops them from `--json`. +Behind those, the unresolved status of the other six cross-cutting flags (A20) +determines whether `flag.json()` is a concept or an accident, and the missing +tree block (A11) determines whether the most-rendered human structure in the +corpus can be expressed at all or leaks back into product code. + +None of this is a reason to restart. The draft is a good second-order artifact +being asked a first-order question, and the fixes are mostly subtractive: one +tone vocabulary instead of four, one remediation type instead of three, one +required-ness convention instead of two, `--json` derived instead of declared. +Add the config-section token and the success envelope, and the interface would +express its requirements rather than describe them. diff --git a/.drive/projects/prisma-cli-v8/assets/engine/stricli-vs-clipanion.md b/.drive/projects/prisma-cli-v8/assets/engine/stricli-vs-clipanion.md new file mode 100644 index 00000000..41fb2d85 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/stricli-vs-clipanion.md @@ -0,0 +1,238 @@ +# Stricli vs clipanion as the CLI engine's internal framework + +Snapshot: 2026-08-09. Research-only evaluation of `@stricli/core` 1.3.0 +(Bloomberg) against the 10-criterion rubric in +`docs/architecture docs/research/commander-friction-points.md` and the engine +requirements R1–R13 in +`wip/repos/prisma-cli/docs/architecture/cli-engine-requirements.md`. +Compared point-by-point with clipanion 3.x (the currently chosen internals). + +Evidence base: the published package (`npm pack @stricli/core@1.3.0`, type +declarations `dist/index.d.ts` and the built `dist/index.js` read in full), +the docs site (bloomberg.github.io/stricli), the GitHub repo via API, npm +registry data, and the composer repo's living clipanion usage +(`packages/0-framework/3-tooling/cli/src/cli.ts`). Line references below are +into the unpacked `dist/index.d.ts` of 1.3.0. + +## Verdict + +**Strong contender — the comparative prototype spike is justified.** + +Stricli scores **10/10** on the rubric (clipanion: 7/10). It passes every +criterion clipanion passes, and it passes criterion 10 (maintenance), which is +clipanion's only hard failure — and clipanion's position there has worsened +since the friction doc was written (last push 2024-09-06, now ~23 months ago; +4.0 still in RC after 3 years; stable 3.2.1 dates from June 2023). Stricli is +also a better structural fit for R9 and R12 than clipanion: its command tree +is literally a static route map built by the mounting side, with lazy handler +loading as a first-class, documented feature. The weaknesses section below +lists real warts (negative internal exit codes, no parse-only API, limited +help-layout customization, single primary maintainer), but none is +disqualifying for our wrap-everything design, and several disappear entirely +because we render output ourselves (R5). + +## Rubric score table + +Criteria abbreviated; full text in commander-friction-points.md ("Concrete +evaluation criteria for the replacement"). + +| # | Criterion | Stricli | Clipanion | Winner | +|---|-----------|---------|-----------|--------| +| 1 | Never calls `process.exit` from parse/dispatch; no message string-matching needed | **Pass.** Zero occurrences of `process.exit(` in `dist/index.js`/`index.cjs` (verified by grep). `run()` sets `context.process.exitCode ??= exitCode` on the *injected* process object (d.ts:1757; index.js `run` impl). Failure classes are typed constants in the exported `ExitCode` object (d.ts:1547–1580). | **Pass** (friction doc: `cli.run` returns exit code as a promise; caller sets `process.exitCode`). | Tie | +| 2 | Injected `{stdout, stderr, env}` per invocation, no globals | **Pass.** `run(app, inputs, context)` requires a context whose `process` is a `StricliProcess` — `{stdout, stderr, env?, exitCode?}` with a minimal structural `Writable` (`write`, optional `getColorDepth`) (d.ts:4–46). Verified in the built JS: every `process` reference is a function parameter fed from the context; no global reads. Docs ("Isolated Context"): "this indirection allows for injecting alternate implementations without hot-swapping globals." | **Pass** (friction doc; composer's `run(argv)` passes streams via clipanion context). | Tie | +| 3 | Help, errors, command output all go to the injected streams | **Pass.** All framework output (help text, parse errors, did-you-mean, integration errors) is written via `context.process.stdout/stderr` (verified in `dist/index.js`; e.g. the integration-error path writes `context.process.stderr.write(...)`). | **Pass**, with the caveat of open issue [arcanis/clipanion#176] (errors to stdout instead of stderr in some paths), which the migration-CLI swap works around with parse-only `cli.process`. | Stricli (no known stream-routing bug) | +| 4 | `parse(argv, ctx)` signals success/failure structurally | **Pass with note.** `run()` returns `Promise` and communicates the exit code by assigning `context.process.exitCode` — we read it off the injected object we own. Codes are the typed `ExitCode` constants: `0` success, `1` command threw, negative codes for framework-level failures (`-4` InvalidArgument, `-5` UnknownCommand, `-2` CommandLoadError, `-3` ContextLoadError, `-1` InternalError, `-10` IntegrationError) (d.ts:1547–1580). `determineExitCode?: (exc: unknown) => number` in `ApplicationConfiguration` (d.ts:593) maps a thrown value to a code. No string matching anywhere. | **Pass** (`cli.run` resolves to the exit code directly — slightly cleaner shape). | Tie (clipanion's return shape is nicer; stricli's codes are more finely discriminated) | +| 5 | Help generated from the same declarations the parser uses | **Pass.** One declaration per parameter: `flags: { verbose: { kind: "boolean", brief: "..." } }` etc. is both the parse spec and the help source. `Command` and `RouteMap` expose `brief`, `fullDescription`, `formatUsageLine`, `formatHelp` from those same objects (d.ts:1118–1155), and `generateHelpTextForAllCommands` walks the tree (d.ts:1356). Docs site's "No Magic" principle is exactly this: no parallel DSL. | **Pass** (friction doc: `usage = {…}` lives on the command class next to the typed option declarations). | Tie | +| 6 | Parse-time enum/constraint validation with structured errors | **Pass.** `kind: "enum"` flags with `values: readonly T[]` are validated during scanning; failure throws `EnumValidationError` carrying typed fields `externalFlagName`, `input`, `values`, `corrections` (d.ts:1443–1457). All nine scanner errors are exported subclasses of `ArgumentScannerError` with typed payloads (`FlagNotFoundError.corrections`, `UnsatisfiedPositionalError.limit`, …) plus a `formatMessageForArgumentScannerError` dispatcher (d.ts:1367–1386). This routes directly into our error envelope with no message parsing. | **Pass** (typanion validators produce structured errors at parse time). | Tie (stricli's error taxonomy is richer and flatter to consume) | +| 7 | Hook for unknown command/flag with "did you mean" | **Pass.** Built in: route scanning computes corrections via Damerau-Levenshtein with git's empirically-derived weights, configurable (`ScannerConfiguration.distanceOptions`, d.ts:428–446), and hands `{input, corrections}` to the replaceable `noCommandRegisteredForInput` formatter (d.ts:260–264). `FlagNotFoundError` also carries `corrections`. We inject our own wording; no internal error interception. | **Pass** (friction doc). | Stricli (suggestions computed for us, formatter injectable) | +| 8 | Command carries user-defined metadata without shims | **Pass with note.** `docs: { brief, fullDescription?, customUsage? }` on every command (d.ts:1612–1625) and `{ brief, fullDescription?, hideRoute? }` on route maps. No arbitrary free-form metadata bag (e.g. docs URL) — but under R1/R3 the engine builds stricli objects *from our own definition type*, which is where arbitrary metadata lives; the framework never needs to carry it. No WeakMap shims required either way. | **Pass** (class fields hold anything). | Tie in practice | +| 9 | Runtime-agnostic parser (no `node:*` imports) | **Pass, strongest possible form.** Zero runtime dependencies (`package.json`: no `dependencies` key; tagline "no dependencies"). Zero `node:` imports in the ESM build (the single grep hit in the CJS build is a tsup comment). All environment access goes through the injected context. Ships ESM + CJS + `.d.ts`; single file ~86 KB unminified. | **Pass with caveat**: clipanion needs a `platform/` shim layer and has open issue [arcanis/clipanion#178] (invalid `lib/platform/node.mjs` require). | **Stricli** | +| 10 | Healthy maintenance trajectory | **Pass.** 1.0.0 published 2024-10-01; 16 releases since, latest 1.3.0 on 2026-07-16; repo pushed 2026-08-07 (two days before this snapshot). 19 open issues, actively triaged (recent bugs like white-on-white help #140 and green-bleed help #170 fixed and released). ~699k weekly npm downloads. Bloomberg OSS project, Apache-2.0. Adopters beyond Bloomberg: **Sentry's `sentry-cli`** (getsentry/cli), MystenLabs ts-sdks, Matt Pocock's `evalite`, LaunchDarkly's MCP server, Inkeep agents, Hookdeck Outpost, xs-dev, and every Speakeasy-generated MCP CLI (which is where much of the download volume comes from). Bus factor caveat below. | **Fail** (friction doc, and worse now: last push to arcanis/clipanion 2024-09-06 — ~23 months; 4.0.0-rc.4 was the last publish, 4.0 in RC since 2023-07; stable 3.2.1 from 2023-06; 42 open issues accumulating). | **Stricli, decisively** | + +**Totals: stricli 10/10, clipanion 7/10** (clipanion per the friction doc: +passes 1–9, fails 10). The friction doc's threshold: ≥8/10 is "a clear win +over Commander". Both clear it; only stricli clears criterion 10, which the +friction doc itself flags as the criterion that becomes more important "for a +larger surface" — exactly our case. + +## Fit against R1–R13 + +- **R1 (one directly executable language).** Good fit. `buildCommand` / + `buildRouteMap` / `buildApplication` are plain functions taking plain object + literals — our engine vocabulary can be a thin typed layer whose output *is* + the runnable stricli tree, no interpreter. Note the engine still owns its own + definition type per R3, so there is one translation (ours → stricli's), same + as with clipanion. +- **R2 (handlers end in typed operation calls).** Neutral/good. A stricli + command function is `(this: CONTEXT, flags: FLAGS, ...args: ARGS) => void | + Error | Promise` (d.ts:1103) — flags and positionals arrive + fully typed; the `FLAGS` type parameter is checked against the parameter + declarations in *both* directions (`FlagParametersForType` maps every key + of the handler's flags type to a required declaration, d.ts:910). Declaring + a flag the type doesn't have, or omitting one it does, is a compile error. + This is stronger inference than clipanion's per-field `Option.String(...)` + class properties. +- **R3 (engine package is the whole contract).** Good fit. Everything is + interface-typed and generically parameterized on `CONTEXT`; the objects + `buildCommand` returns are opaque values we never re-export. Nothing forces + stricli types into our public surface. +- **R4 (context, never the environment).** Direct match. Stricli's whole + design is a context object bound as `this` in command functions, extended + with whatever we add (docs: "Isolated Context"). `StricliDynamicCommandContext` + supports a `forCommand(info) => CONTEXT | Promise` builder + (d.ts:78–88) — per-invocation, possibly async construction of the + handler-facing context (config section, credentials, output surface) after + routing but before execution. This maps one-to-one onto our handler-context + design. Parsers themselves also get the context (`InputParser`, + d.ts:987), which clipanion's typanion validators do not. +- **R5 (engine renders everything).** Good fit with one design decision to + make in the spike. Two viable postures: + 1. *Own rendering wholesale*: skip the `help`/`version` integrations + (since 1.2 they are opt-in integrations, d.ts:1728/1755) and render help + ourselves by walking the public tree — `RouteMap.getAllEntries()`, + `Command.parameters` (flags with `brief`/`default`/`values`/`hidden`, + positionals with placeholders), `brief`/`fullDescription` (d.ts:1118–1141). + All parse metadata is on public readonly properties — no + private-field spelunking like Commander's `defaultValue` (friction §8). + 2. *Own the text, let stricli lay it out*: replace the entire + `ApplicationText` object (`localization: { text }`) — every error string + and help header is a formatter we supply, receiving the *typed* error + objects (d.ts:192–319). + Parse/route errors are formatted by our functions and written to our + streams either way. The one thing stricli insists on doing is the physical + `stderr.write` of the formatted error inside `run()` — acceptable because + both the string and the stream are ours; if we ever need the error as a + value instead, see the "no parse-only API" weakness below. +- **R6 (structured errors, exit-code mapping).** Good fit. Scanner errors are + typed classes → our envelope; `ExitCode` discriminates usage errors (−4/−5 → + our 2) from bugs (−1/−2/−3 → our 1); `determineExitCode` maps handler + throws. We read `context.process.exitCode` off our own injected object and + translate before touching the real process (see weaknesses: never hand + stricli the real `process`). +- **R7 (product-repo e2e tests, instance-based).** Direct match — this is the + hard requirement, and stricli meets it structurally. `Application` is a + value; `run(app, argv, context)` is a pure-ish call over injected streams; + no module-level state anywhere in the bundle (verified). A product mounts + its commands in a throwaway route map and runs argv-in/bytes-out in its own + repo with a `{ process: fakeStreams }` context. The docs advertise exactly + this testing story. +- **R8 (shell integration proof).** Neutral — same machinery, no obstacle. +- **R9 (static tree, lazy guts).** Direct match, better than clipanion. + Route maps are built eagerly at startup from cheap declarations; each + command takes either `func` (inline) or `loader: () => Promise` (d.ts:1107–1113, 1637–1648) — the dynamic import of the + heavy handler module happens only when that command is actually executed, + *after* routing and before argument parsing. Help renders from the static + declarations without ever invoking the loader. A loader failure is its own + exit class (`CommandLoadError`, −2) with its own formatter + (`exceptionWhileLoadingCommandFunction`), so "Composer's dependency tree + crashed at import" becomes a classified, renderable failure rather than a + startup crash. Clipanion has no built-in lazy-module story (its + registration is runtime `cli.register` per class; stricli's own + "Alternatives" page criticizes it for runtime command loading). +- **R10 (config).** Out of framework scope; nothing in stricli touches config + files. No conflict. +- **R11 (pinning).** No conflict. Zero transitive dependencies makes exact + pinning trivial and audit surface minimal. +- **R12 (shell defines the tree).** Direct match. A `Command` contains no + path; paths exist only as keys in `buildRouteMap({ routes: { migrate: + cmd } })` (d.ts:1673–1703), composed by whoever mounts. The same command + value can be mounted at any path, at several paths, or under a test-only + root in a product repo. Route-level `aliases` and `hideRoute` are also + mounting-side concerns. This is structurally the R12 split. +- **R13 (no package manager).** Pass. Nothing self-installs; zero runtime + deps; nothing interferes with optional-peer-dependency detection (a handler + doing its own `import()` probe is untouched by the framework). The optional + version-check feature (`getLatestVersion`) is opt-in and purely advisory; + we simply don't enable it. + +## Honest weaknesses of stricli + +1. **Negative internal exit codes.** Framework failures set + `context.process.exitCode` to −1…−10. POSIX exit statuses are 0–255; if + the *real* `process` were passed as context, `-5` becomes exit 251. For us + this is a mapping obligation, not a bug — the engine must always inject + its own process facade and translate — but it is a trap for anyone who + follows the docs' quickstart (`run(app, argv, { process })`) and expects + sane shell-visible codes for usage errors. +2. **No parse-only public API.** `run()` is the only execution entry point + (plus `proposeCompletions`). There is no exported "parse this argv against + this command and give me the flags/route result without executing" + function — `RouteScanResult` is a type you only meet inside integration + hooks. Commander-style metadata recovery isn't needed (declarations are + public), but if the spike finds we want parse-errors-as-values rather than + parse-errors-as-formatted-strings, the workaround is a custom + `ApplicationText` whose formatters capture the typed error object onto our + context before returning a string — functional, slightly inelegant. +3. **Help layout is stricli's unless we render ourselves.** Section order, + indentation, and column layout of `formatHelp` are not configurable beyond + headers/keywords and a few booleans; issue #87 ("Customize usage/help + layout", open since 2025-05) confirms. Under R5 we intend to render help + ourselves anyway, which sidesteps this — but it means the spike must + verify that walking `Command.parameters` gives us everything our formatter + needs (it appears to: briefs, placeholders, defaults, enum values, + optional/variadic/hidden are all in the public d.ts). +4. **No user-defined global flags across commands.** A flag like `--json` + must either be declared on every command (our engine can inject it into + every definition it builds — mechanical) or expressed as an + application-level integration flag, which takes over the run like + `--help` does. Open issues #146 ("Support application-level global + flags") and #127 ("Root flags?") confirm this is a real gap upstream. +5. **Single-character flag aliases only.** `Aliases` is typed as one ASCII + letter → flag name (d.ts:998–1001). No long-form alias (`--data-proxy` + aliasing `--accelerate`); route-level aliases are unrestricted, flag-level + are not. If we need long flag aliases for deprecation migrations, we + declare a second hidden flag and merge in the handler — our engine can + abstract that. +6. **CamelCase-first flag naming.** Flag names are TypeScript object keys, so + multi-word flags are natively `camelCase`; kebab input/output is a scanner + and display mode (`allow-kebab-for-camel` / `convert-camel-to-kebab`, + d.ts:399, 453). Fine, but it is a convention the engine must set once + (Prisma's user-facing flags are kebab-case) and the "original vs + converted" duality shows up in APIs like `getOtherAliasesForInput` + returning per-case-style records. +7. **Bus factor.** Contributor stats: molisani (Michael Molisani, Bloomberg) + 125 commits; next human contributor 30; everyone else ≤4. This is a + one-primary-maintainer project with corporate backing — better than + clipanion's one-maintainer-who-moved-on, and Bloomberg has kept it staffed + for 22 months of releases, but it is not a multi-maintainer community. + Mitigation is the same wrap-per-R3 posture that lets us swap internals. +8. **API still settling.** 1.2 deprecated the whole `DocumentationConfiguration` + / `versionInfo` surface in favor of integrations (deprecation notices + throughout the d.ts). Handled gracefully (deprecate, don't break), and it + moved in a direction we like (help/version became optional), but expect + some churn inside 1.x. +9. **Mandatory `brief` on everything** (issue #92). For us a non-issue — the + engine requires descriptions anyway — noted for completeness. +10. **Docs gloss over `forCommand`.** The dynamic per-command context builder + (`StricliDynamicCommandContext.forCommand`) is in the types but + undocumented on the site (issue #126). We would rely on it for R4; the + spike should exercise it explicitly. + +## Clipanion-specific notes not in the friction doc + +- Maintenance has degraded further since the 2026-04-30 snapshot: the repo's + last push remains 2024-09-06 (now ~23 months), last npm publish is + 4.0.0-rc.4 (2024-09-06), and the stable 3.x line's last release is 3.2.1 + (2023-06-05). Open issues: 42. The friction doc's "re-evaluate criterion 10 + before adopting clipanion at larger scope" instruction, applied today, + reads as a failure for this scope. +- Composer's living usage (`wip/repos/composer/packages/0-framework/3-tooling/ + cli/src/cli.ts`) confirms the pleasant parts: `run(argv)` returns the exit + code, `UsageError` is a typed catchable, envelope mapping is a small + try/catch. Nothing in that file argues against clipanion ergonomically — + the case against it is purely criterion 10 plus the weaker R9 story. + +## Bottom line for the spike decision + +Stricli is not merely "clipanion with a pulse". It is a closer structural +match to this requirements document than clipanion on the three requirements +that shaped the engine design (R7 instance/context model, R9 static tree with +lazy loaders, R12 mounting-side route maps), it has the best +runtime-agnosticism profile of any candidate examined (zero deps, zero +`node:*`, all environment access injected), and its maintenance trajectory is +the strongest signal: 16 releases in 22 months, active triage, corporate +backing, and credible external adopters (Sentry CLI, Speakeasy, MystenLabs). +Run the comparative spike; the specific things the spike must prove are the +R5 own-rendering path (weakness 3), the `forCommand` context handoff +(weakness 10), and the exit-code translation layer (weakness 1). diff --git a/.drive/projects/prisma-cli-v8/design-notes.md b/.drive/projects/prisma-cli-v8/design-notes.md new file mode 100644 index 00000000..05c5392b --- /dev/null +++ b/.drive/projects/prisma-cli-v8/design-notes.md @@ -0,0 +1,65 @@ +# cli-host — design notes + +Orchestrator-authored index of the settled design inputs this project +builds on. The artifacts themselves live where they were produced; this +file is the map. + +## Settled (do not re-litigate without the operator) + +- **Engine interface, v8** — `assets/engine/engine-interface-draft.ts` + (v1–v7 history alongside). Settled through facilitated + line-by-line design with Will plus five adversarial review rounds + (architect + principal engineer, both closed clean; artifacts in + `assets/engine/reviews/`). Every novel typing claim + compile-verified. +- **Requirements R1–R14** — prisma-cli PR #128 + (`docs/architecture/cli-engine-requirements.md`), unmerged. +- **Packaging** — ONE library package `@prisma/cli-engine` with a + `./protocol` subpath for types-only consumers; `@stricli/core` as an + ordinary exact-pinned dependency (bundling rejected); committed + versions, bumped in PRs. +- **Model** — commands settle like promises: COMPLETED (presented + outcome: data + diagnostics + documented exitCodes) vs ERRORED + (structured error, engine-rendered). `Diagnostic` ≡ the error envelope + shape; findings are data, never thrown. +- **Framework decision record** — + `assets/engine/stricli-vs-clipanion.md` (stricli 10/10 vs + clipanion 7/10 on the repo's own rubric). +- **Evidence base** — `assets/engine/output-modes-survey.md` + (~85 commands, three families, mode taxonomy, recurring structures). +- **Auth** — an auth library (token storage, refresh, login guts) lives + in the prisma-cli repo, DISTINCT from Prisma Cloud; Cloud extraction + later leaves auth behind. `{ token }` is the engine-visible shape; + credentials resolve per-call so refresh works under long sessions. +- **Conformance** — a small 3-check tool only: import purity, validator + no-throw on garbage, published-tarball verification. +- **ADR 239 amendment (prisma/prisma) is an implementation + prerequisite** — completed-but-unsuccessful results carry dotted codes + as diagnostics inside completed envelopes; includes the + severity-'info' evidence check (trim both scales together if unused). + +## Parked (excluded from this project by ruling) + +- **Daemon library** — `assets/engine/daemon-library-notes.md`: + runtime dependency of product control clients, orthogonal to the + engine; zero engine surface needed. + +## Hand-off briefs — handed off, PAUSED by the operator + +- `assets/briefs/1b-leftovers-prisma-prisma.md` and + `assets/briefs/1c-leftovers-composer.md` are already with other agents, + but the operator paused that work until the engine lands: their config + deliverables (diagnostics-not-throw loaders, marker, validators) will + be rewritten against the engine's config API (v8 §3: + defineConfigSection tokens, validator-owned absence, Diagnostic + findings, ProductManifest). Sequencing consequence: the engine's + protocol + config-section API is upstream of resuming 1b/1c; when + resumed, the briefs need revision first. + +## Prior art / superseded + +- The consolidate-clis project (closed 2026-08-07): grammar doc, spec, + plan recoverable from prisma/prisma PR #29917's head ref + (`refs/pull/29917/head`). Its Phase 2–3 content (host build, ports, + ecosystem cutover) informs this project's plan but was never + re-ratified — treat as input, not contract. diff --git a/.drive/projects/prisma-cli-v8/spec.md b/.drive/projects/prisma-cli-v8/spec.md new file mode 100644 index 00000000..deee8d2c --- /dev/null +++ b/.drive/projects/prisma-cli-v8/spec.md @@ -0,0 +1,148 @@ +# Summary + +Ship the unified `prisma` CLI: one binary, one `prisma.config.ts`, and the +ORM, Composer, and Cloud command families mounted on the agreed grammar +tree — built on the settled engine design (interface v8, requirements +R1–R14). **Definition of done: the operator can publish the `prisma` npm +binary as `8.0.0-rc1` implementing the full design.** Publishing is the +operator's act; this project delivers the publishable state. + +# Description + +Prisma users today face three CLIs (`prisma-next`, `prisma-composer`, +`@prisma/cli`), three config files, and three unrelated help/output/error +dialects. The consolidation direction, grammar tree, and engine design +are settled (see `design-notes.md` for the authoritative map: engine +interface v8 with five review rounds closed; requirements R1–R14 on +prisma-cli PR #128; packaging, auth, config, and error-model rulings). +What remains — this project — is to build it: the engine library, the +unified config machinery, the shell, the auth library, and the port of +all three command families, ending in a release pipeline that can produce +`prisma@8.0.0-rc1`. + +Repos involved: **prisma-cli** (the shell, the engine package, the auth +library — home repo), **prisma/prisma** (ORM product integration; the +ADR 239 amendment), **prisma/composer** (Composer product integration). + +# Requirements + +## Functional Requirements + +1. **The engine library exists and is consumable**: `@prisma/cli-engine` + implements interface v8 — the execution protocol + (completed/errored), events, return-site presentation, config-section + tokens, prompts (defaults, `consent`), the three command kinds, the + envelopes and json stream, mounting, and the test harness — with a + `./protocol` subpath for types-only consumers and `@stricli/core` as + an exact-pinned internal dependency. Every deviation from the v8 + draft discovered during implementation returns to the operator as a + design question, not a silent fix. +2. **ADR 239 is amended first** (prisma/prisma): completed-but- + unsuccessful results carry dotted codes as diagnostics inside + completed envelopes with documented exit codes; includes the + severity-`info` evidence check (trim `CliStructuredError` and + `Diagnostic` scales together if unused). +3. **One config file**: the shell discovers and evaluates + `prisma.config.ts` exactly once; the `defineConfig` version marker + distinguishes v8 configs, and an evaluated file without the marker — + in particular a classic Prisma 7 config — fails early with a typed + error naming the migration path (fail-early is ruled; no best-effort + reading). Products contribute sections via manifests + (`ProductManifest`: section token + commands + docs base); validation + is per-section diagnostics; a command fails only when a section it + needs is invalid. +4. **The shell**: the `prisma` binary in the prisma-cli repo — mounts + the grammar tree (shell-owned paths, R12), injects the shared flag + family, implements formats (`--format`, `--json` alias, auto-json on + non-TTY stdout), log levels, prompts, signals, exit codes, and the + crash envelope, all through the engine. +5. **Auth**: the auth library (token storage, refresh, login flow guts — + extracted from `@prisma/cli`'s existing implementation) lives in the + prisma-cli repo, distinct from Prisma Cloud code; the shell consumes + it to supply `getCredentials`; credentials authenticated through any + command reach every product's operations through context. +6. **All three command families port onto the tree** per the grammar + doc: ORM (`contract *`, `migration *`, `db *`, `init`, `lsp`, …), + Composer (`project deploy|dev|log|destroy`, stubs where ruled), and + Cloud/platform (`auth *`, `project *`, `postgres *`, `service *`, + `bucket *`, `git`, `agent`, with the ruled renames). Parity bar: + behavior equivalent to the shipping CLIs except where a settled + ruling changed it (envelopes, exit codes, renames) — divergences are + enumerated, not discovered. +7. **Products integrate as designed**: prisma/prisma and composer export + manifests and command sets; the shell pins exact versions; tandem + releases run on committed versions with workflow glue. +8. **Product-repo e2e is real** (R7): each product runs argv-in/ + bytes-out tests against the engine's harness in its own repo; the + shell's own suite proves composition per family (R8). +9. **The conformance checker exists**: the small three-check tool — + import purity, validator no-throw on hostile input, published-tarball + verification — wired into CI where products publish. +10. **A release pipeline produces the publishable artifact**: versioned + per the committed-versions ruling, capable of emitting + `prisma@8.0.0-rc1` on demand. + +## Non-Functional Requirements + +- Requirements **R1–R14** (prisma-cli `docs/architecture/ + cli-engine-requirements.md`) govern throughout; this spec does not + restate them. +- The settled error/result conventions (prisma/prisma ADR 239 as + amended, ADR 245; composer ADR-0043/0044) hold everywhere. +- The exit-code contract: 0 completed / 1 bug only / 2 errored / + 3 abort / 4–99 documented / 130,143 signals. +- The CLI never touches a package manager (R13); optional capability = + optional peer dependency + engine-phrased error. +- Runtime-agnostic products (R4): context-not-environment discipline + throughout the ports. +- The engine design artifacts live in this project directory + (`assets/engine/`); the durable subset (the final interface record, + ADRs) migrates into `docs/` at the appropriate slices and at + close-out. + +## Non-goals + +- **Daemon library and emulator management commands** — parked by + ruling (`assets/engine/daemon-library-notes.md`); the `emulator` + root stays off this project's tree. +- **Prisma 7 config compatibility** beyond the fail-early typed error. +- **Ecosystem cutover**: codemods (`prisma-next.config.ts` → + `prisma.config.ts`), create-prisma templates, deprecation of the + three existing binaries, docs-site updates, the npm takeover + sequencing itself — follow-on work after rc1 exists. +- **`prisma.compute.ts` migration** — separate Terminal effort. +- **Resuming the paused 1b/1c briefs** — sequenced after the engine's + config API lands; their revision is the trigger to unpause, tracked + in delivery, but their content is not this project's deliverable. +- **GA (non-rc) release.** + +# Acceptance Criteria + +- [ ] `@prisma/cli-engine` published (or publishable) from the + prisma-cli repo; its `./protocol` subpath consumed type-only by at + least one product; the v8 draft's compile-verified typing claims + hold in the shipped package's tests. +- [ ] ADR 239 amendment merged in prisma/prisma before any port relies + on completed-with-diagnostics semantics. +- [ ] A v8 `prisma.config.ts` with sections for all three products + drives a real workflow end to end; a Prisma 7 config file produces + the typed fail-early error, test-pinned. +- [ ] Every command family mounted; the shipped tree checked against the + grammar doc by a build-time test; per-family parity divergence + lists reviewed by the operator. +- [ ] `prisma auth login` through to a Composer deploy consuming the + same credentials via context, e2e. +- [ ] Product-repo e2e suites exist and pass in prisma/prisma and + composer using the engine harness; shell integration proofs pass + per family. +- [ ] Conformance checker runs in CI for both products' publish paths. +- [ ] The release pipeline emits a `prisma@8.0.0-rc1` artifact from a + tagged commit; the operator can publish it with one action. + +# Cross-cutting + +- Drive process governs delivery: slices are one-PR units; deviations + from settled design return to the operator; retros land learnings in + durable memory. +- Commit/PR discipline per the operator's standing rules (bot identity, + dual sign-off, explicit staging, verify-PR-open-before-push). From 04809e8115b9c8d14823a6e862673eed4917f5ea Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 9 Aug 2026 18:22:42 +0200 Subject: [PATCH 07/18] =?UTF-8?q?drive(prisma-cli-v8):=20project=20plan=20?= =?UTF-8?q?=E2=80=94=20platform,=20then=20Composer,=20then=20ORM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seven slices: engine + one vertical command; platform family port with the auth extraction and commander-shell retirement; Composer as the first cross-repo consumer (config sections, sessions, the child-status passthrough, tandem releases); the ADR 239 amendment in parallel; ORM last (diagnostics model, lsp, exit-code reconciliation); the three-check conformance tool; the release pipeline ending at a publishable prisma@8.0.0-rc1. Includes the coverage ledger mapping every engine surface to the slice that proves it. Co-Authored-By: Claude Fable 5 Signed-off-by: willbot Signed-off-by: Will Madden --- .drive/projects/prisma-cli-v8/plan.md | 117 ++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 .drive/projects/prisma-cli-v8/plan.md diff --git a/.drive/projects/prisma-cli-v8/plan.md b/.drive/projects/prisma-cli-v8/plan.md new file mode 100644 index 00000000..d738e4ba --- /dev/null +++ b/.drive/projects/prisma-cli-v8/plan.md @@ -0,0 +1,117 @@ +# prisma-cli-v8 — project plan + +Consumer ordering (operator, 2026-08-09): **platform → Composer → ORM**. +The engine meets its first consumer in its own repo (flex is a same-PR +edit; the variable is isolated), its second across a repo boundary with +real config and sessions, and its hardest consumer last, twice-hardened. +Slices are one-PR units; each port slice ships its parity-divergence +list for operator review (spec FR6). + +Branch mechanics: slices land as PRs into the prisma-cli repo's +`cli-engine-requirements` branch lineage (PR #128 is the living decision +record) or their own repos; #128 merges when the operator says so. + +## Slices + +### S1 — Engine package + one vertical command + +Repo: prisma-cli. Implement `@prisma/cli-engine` per the v8 interface +(execution protocol, events, return-site presentation, config-section +tokens, prompts incl. consent + defaults, three command kinds, +envelopes/stream, mounting, `./protocol` subpath, the test harness; +`@stricli/core` exact-pinned). Prove it end to end with ONE ported +platform command (`project list` or `auth whoami`) mounted in a minimal +shell bin: parse → context → handler → presentation → envelope → exit +code, byte-asserted through the harness. First-contact flex on the v8 +draft returns to the operator as design questions; the draft in +`assets/engine/` is updated to match what ships. + +### S2 — Platform family port + auth extraction + +Repo: prisma-cli. Port the ~60 management-API commands onto the engine +in grouped batches (auth + project; database + bucket; app + build + +git + agent + env; init wizard last — it stresses prompts hardest). +Extract the auth library (token storage, refresh, login-flow guts) as +its own package, distinct from Prisma Cloud code, consumed by the shell +for `getCredentials`. Retire the commander shell (`exitOverride` maze, +custom help formatter, WeakMap shims — the friction-points doc is the +kill list). DoD: every platform command on the engine, old shell +deleted, per-family shell integration proofs, parity list reviewed. + +### S3 — Composer adoption (first cross-repo consumer) + +Repos: composer + prisma-cli. Composer exports a `ProductManifest` +(the `composer` config section token — its validator rewritten from the +current throwing loader per the section API — plus its command set); +ports `deploy`/`destroy` (result commands), `dev`/`log` (session +commands), preserving the alchemy child-status passthrough exception. +Consumes the PUBLISHED engine (`./protocol` from outside, exact pins); +stands up the tandem-release workflow glue on committed versions. The +paused 1c brief is superseded by this slice — close it out against +what ships here. Proves: config machinery under real product use, +sessions, cross-repo consumption. + +### S4 — ADR 239 amendment (parallel; before S5) + +Repo: prisma/prisma. Completed-but-unsuccessful results carry dotted +codes as diagnostics inside completed envelopes with documented exit +codes; the severity-'info' evidence check (trim both scales together if +unused). Small, independent; the only ordering constraint is landing +before S5 relies on the semantics. + +### S5 — ORM adoption + +Repos: prisma/prisma + prisma-cli. The `orm` section token + +manifest; port `contract *`, `migration *` (retiring the clipanion +migration-cli), `db *`, `init`, `telemetry`, `lsp` (the server +command). Proves the diagnostics model (`migration check`, `db verify` +as completed-with-findings + catalogued exit codes) and the +exit-code-4 semantics under S4's amendment. The paused 1b brief is +superseded here — close it out against the section API. The ORM's +three colliding exit-code schemes reconcile to the contract (survey +finding). + +### S6 — Conformance checker (parallel after S1) + +Repo: prisma-cli. The small three-check tool — import purity, +validator no-throw on hostile input, published-tarball verification — +wired into both products' publish CI as S3/S5 land. + +### S7 — Release pipeline + rc1 + +Repo: prisma-cli. The `prisma` binary package assembled: full grammar +tree mounted with the build-time grammar check, committed-versions +release automation, pinned product versions, and the pipeline emitting +a publishable `prisma@8.0.0-rc1` artifact from a tagged commit. Ends +when the operator can publish with one action (project DoD). + +## Dependency graph + +``` +S1 ──► S2 ──► S3 ──► S5 ──► S7 + │ ▲ ▲ + └──────┘ (published engine exists after S2's engine hardening) +S4 (prisma/prisma) ────────► S5 +S6 (after S1) ─────────────► wired in during S3/S5 +``` + +## Coverage ledger (what proves what) + +| Engine surface | Proven by | +| --- | --- | +| Sync commands, presenters, envelopes, exit codes | S1, S2 | +| Prompts (defaults, consent, wizard) | S2 (init) | +| Poll + status events; output streams | S2 (domain wait; app/build logs) | +| Auth via context, refresh under long runs | S2, S3 (deploy) | +| Config sections, manifests, validator absence | S3, S5 | +| Session commands, signal lifetime | S3 (dev, log) | +| Cross-repo/published consumption, pins, tandem releases | S3 | +| Child-status passthrough exception | S3 | +| Diagnostics model, catalogued exit codes | S5 | +| Server command (stdio) | S5 (lsp) | +| Grammar tree completeness | S7 | + +## Out of plan (per spec non-goals) + +Daemon library and `emulator` root; ecosystem cutover/codemods/ +deprecations; `prisma.compute.ts`; GA. From 03f87037692b1aa82e73ae37a24fa3e8085650f8 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 9 Aug 2026 18:24:24 +0200 Subject: [PATCH 08/18] =?UTF-8?q?drive(prisma-cli-v8):=20S1=20slice=20cont?= =?UTF-8?q?ract=20=E2=80=94=20engine=20package=20+=20auth=20whoami=20verti?= =?UTF-8?q?cal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Signed-off-by: willbot Signed-off-by: Will Madden --- .../prisma-cli-v8/specs/s1-engine-vertical.md | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 .drive/projects/prisma-cli-v8/specs/s1-engine-vertical.md diff --git a/.drive/projects/prisma-cli-v8/specs/s1-engine-vertical.md b/.drive/projects/prisma-cli-v8/specs/s1-engine-vertical.md new file mode 100644 index 00000000..0db61465 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/specs/s1-engine-vertical.md @@ -0,0 +1,84 @@ +# S1 — Engine package + one vertical command (slice contract) + +One PR into the `cli-engine-requirements` lineage (prisma-cli repo). + +## Goal + +`@prisma/cli-engine` exists, implements the v8 interface, and is proven +end to end by ONE ported platform command — `auth whoami` — running +through a minimal bin: parse → preconditions → context → handler → +presentation → envelope → exit code, byte-asserted through the +package's own test harness. + +## In scope + +1. **The engine package** (new workspace package in this repo): + - The protocol types (`CliStructuredError`, `Result`, `Diagnostic`, + `NextAction`) implemented from the prisma/prisma donor sources + (`packages/1-framework/0-foundation/utils/`, `1-core/errors/ + control.ts` lines ~9–111) with the settled adjustments (Diagnostic + as pure data ≡ envelope shape; NextAction without `journey`), + exposed at the `./protocol` subpath. + - The full v8 surface from + `.drive/projects/prisma-cli-v8/assets/engine/ + engine-interface-draft.ts`: defineConfigSection, defineCommand/ + defineSessionCommand/defineServerCommand, flag/positional builders + (Char alias typing), Args, CommandContext (present with Outcome, + report, prompt incl. consent + defaults under --yes, signal, cwd, + requireDependency, getCredentials), events + rendering rules, + Blocks + Ui, envelopes + StreamEvent framing, createCli + Runtime + + LoadedConfig, createTestCli. `@stricli/core@1.3.0` exact-pinned, + fully internal. + - The compile-verified typing claims from the review record + (`assets/engine/reviews/code-review-r4-closure.md`, + `-r5-delta.md`) become permanent type-tests in the package + (@ts-expect-error suites): Char alias accept/reject, Outcome + exitCode required-iff-catalogued both directions, needs.config → + ctx.config inference, PresentedResult brand. +2. **A minimal config loader** behind `Runtime.config`: discover + `prisma.config.ts` from cwd, evaluate it, check the `defineConfig` + version marker, produce `LoadedConfig` (raw sections + + file-level diagnostics). An evaluated file WITHOUT the marker (a + classic Prisma 7 config) yields the typed fail-early diagnostic — + test-pinned. (Full loader polish, section registration UX, and the + `defineConfig` helper's final home evolve in S3; the marker + semantics are settled and land now.) +3. **A minimal bin** (`prisma-v8` working name, not published): + createCli with one group, `auth whoami` mounted, Runtime assembled + from the real process (streams, env, TTY, signals) with + `getCredentials` backed by the EXISTING token-storage adapter + in place (extraction is S2). +4. **The `auth whoami` port**: definition + lazy handler calling the + existing controller logic as its operations layer; presentations per + the platform's current output (parity), stdout payload, json + envelope. +5. **Tests**: engine unit tests; harness e2e for whoami (human bytes, + `--json` stream + envelope, `--quiet`, exit codes incl. errored and + unauthenticated preconditions); marker fail-early; "engine never + calls process.exit and writes only to provided streams" proven by + harness construction. + +## Out of scope + +Every other command; commander-shell removal (S2); auth-library +extraction (S2); ProductManifest consumption from another repo (S3); +publishing. + +## Design authority + +The v8 draft is normative. Where implementation contradicts it, STOP +and return the question — the draft gets amended by the operator's +ruling, never silently. Requirements R1–R14 +(`docs/architecture/cli-engine-requirements.md`) govern. + +## Acceptance + +- [ ] Package builds; `./protocol` subpath importable type-only. +- [ ] Type-test suite green, including every ported compile-verified + claim (with stale-@ts-expect-error control discipline). +- [ ] `prisma-v8 auth whoami` parity with `prisma-cli auth whoami` + (documented divergences only: envelope shape, exit codes). +- [ ] Harness e2e green for human/json/quiet/errored/unauthenticated. +- [ ] Prisma 7 config file → typed fail-early error, test-pinned. +- [ ] v8 draft in `assets/engine/` updated to match any operator-ruled + amendments made during the slice. From 423cca62b57b7784aafffdb9f42afb5393b2bdb9 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 9 Aug 2026 18:39:25 +0200 Subject: [PATCH 09/18] =?UTF-8?q?drive(prisma-cli-v8):=20S1=20dispatch=20p?= =?UTF-8?q?lan=20=E2=80=94=20six=20sequential=20dispatches?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Signed-off-by: willbot Signed-off-by: Will Madden --- .../prisma-cli-v8/plans/s1-engine-vertical.md | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 .drive/projects/prisma-cli-v8/plans/s1-engine-vertical.md diff --git a/.drive/projects/prisma-cli-v8/plans/s1-engine-vertical.md b/.drive/projects/prisma-cli-v8/plans/s1-engine-vertical.md new file mode 100644 index 00000000..5b81f823 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/plans/s1-engine-vertical.md @@ -0,0 +1,133 @@ +# S1 dispatch plan — engine package + auth whoami vertical + +Slice contract: `../specs/s1-engine-vertical.md`. The v8 draft +(`../assets/engine/engine-interface-draft.ts`) is normative; any +first-contact contradiction STOPS the dispatch and returns to the +operator as a design question. + +Branch mechanics: all dispatches commit to one branch +`s1-engine-vertical` off `cli-engine-requirements`; the slice PR +targets `cli-engine-requirements`. + +Codebase grounding (2026-08-09): pnpm workspace, packages/cli + +packages/compute; @prisma/cli builds with tsdown, tests with vitest +(`packages/cli/tests/*.test.ts`); the whoami vertical is +`runAuthWhoAmI` (packages/cli/src/controllers/auth.ts:114) over +`createAuthUseCases().whoami` (packages/cli/src/use-cases/auth.ts) with +presentations in packages/cli/src/presenters/auth.ts; token storage is +packages/cli/src/adapters/token-storage.ts (+ @prisma/credentials-store); +the commander shell lives in packages/cli/src/shell/. + +## Dispatches (sequential) + +### D1 — Package scaffold + protocol subpath + +**Outcome:** `packages/cli-engine` (`@prisma/cli-engine`) exists in the +workspace, builds with tsdown, runs vitest, and exports the `./protocol` +subpath carrying the protocol types (`Diagnostic`, `CliStructuredError` +with `toEnvelope()`, `Result`, `NextAction`) ported from the +prisma/prisma donor sources with the settled adjustments (Diagnostic is +pure data ≡ envelope shape minus `ok`; NextAction has no `journey`; +severity scales identical). +**Builds on:** nothing (first dispatch). +**Hands to:** a building package whose `./protocol` import is proven +type-only (a test that imports it and a check that importing it executes +no engine code). +**Completed when:** package builds; protocol unit tests green +(`toEnvelope()` shape pinned); type-only import proven; workspace lint +passes. + +### D2 — Definition surface + type-test suite + +**Outcome:** the full v8 *type* surface compiles: defineCommand / +defineSessionCommand / defineServerCommand, flag + positional builders +with the `Char` alias typing, `Args`, `Outcome`, `Presentations` / +`PresentedResult`, `defineConfigSection` + `SectionValidation`, +`NeedsSpec` / `HelpSpec`, `ProductManifest`, `createCli` mounting types, +`Runtime` / `LoadedConfig` / `Credentials` shapes. Pure types and +constructors only — no execution. +**Builds on:** D1's package + protocol types. +**Hands to:** the definition types D3 executes against and D5 loads +config for. +**Completed when:** every compile-verified claim from the review record +(reviews/code-review-r4-closure.md, -r5-delta.md) is a permanent +type-test with stale-@ts-expect-error discipline: Char alias +accept/reject, exitCode required-iff-catalogued in both directions, +needs.config → ctx.config inference, PresentedResult brand. Suite green. + +### D3 — Execution engine + test harness (result commands) + +**Outcome:** a result command runs end to end inside the package's own +tests: `createCli` mounts the tree on `@stricli/core@1.3.0` +(exact-pinned, fully internal), parse → needs checks → context assembly +→ handler → `ctx.present` materializing only the active format → +envelope → exit code. Both settlements work: COMPLETED (presented +result, diagnostics, documented exit codes) and ERRORED +(CliStructuredError → error envelope, engine-rendered). `--format +human|json` (`--json` alias, auto-json when stdout is not a TTY), +`--log-level` (`--verbose` alias), StreamEvent framing, `createTestCli` +harness (answers, abort, onEvent, cwd, now; exitCode/stdout/stderr/ +json/events/presented). The engine never calls `process.exit` and +writes only to provided streams — proven by harness construction. +**Builds on:** D2's definition surface. +**Hands to:** an executable engine D4 completes and D6 mounts a real +command on. +**Completed when:** harness e2e for a toy in-test command byte-asserts +human, json-stream + envelope, and errored paths with correct exit +codes (0/1/2 + catalogued). + +### D4 — Prompts, events, session/server lifetimes + +**Outcome:** the remaining execution surface: `ctx.prompt` (product +defaults accepted by `--yes`/Enter; no default halts under `--yes`; +`prompt.consent` structurally undefaultable), the event vocabulary + +rendering rules (step, progress, message severities, output channels, +remediation → NextAction, endpoint/status/artifact, opaque product +`data`), `ctx.report`, `ctx.requireDependency` (engine-phrased install +error), session-command lifetime (runs until signal, no presentation) +and server-command stdio handoff, signal exit codes (3, 130, 143). +**Builds on:** D3's execution engine + harness. +**Hands to:** the complete engine surface the acceptance sweep checks. +**Completed when:** each behavior above has a harness test; prompt +default/consent semantics test-pinned; session command terminates +cleanly on abort in tests. + +### D5 — Config loader (marker fail-early) + +**Outcome:** `Runtime.config` is populated by a minimal loader: +discover `prisma.config.ts` from cwd, evaluate it, check the +`defineConfig` version marker, produce `LoadedConfig` (raw sections + +file-level diagnostics). An evaluated file WITHOUT the marker (a +Prisma 7 config) yields the settled typed fail-early diagnostic. +**Builds on:** D2's `LoadedConfig` / section-token types (not D4 — +non-linear; the loader needs no prompts/events surface). +**Hands to:** config loading D6's bin wires into its Runtime. +**Completed when:** loader tests cover found/absent/marked/unmarked +files; the Prisma 7 fail-early diagnostic is test-pinned (code + +summary + fix). + +### D6 — `prisma-v8` bin + auth whoami port + slice e2e + +**Outcome:** a minimal unpublished bin (working name `prisma-v8`) in +packages/cli: `createCli` with one group, `auth whoami` mounted; +Runtime assembled from the real process (streams, env, TTY, signals), +`getCredentials` backed by the existing token-storage adapter in place; +the whoami definition + lazy handler calling the existing +use-case/controller logic as its operations layer; presentations +matching the current `prisma-cli auth whoami` output (parity), stdout +payload, json envelope. +**Builds on:** D3 (execution), D4 (full surface), D5 (config in +Runtime). +**Hands to:** the slice DoD: the proven vertical + the parity-divergence +list for operator review. +**Completed when:** harness e2e green for whoami human bytes, `--json` +stream + envelope, `--quiet`, errored, and unauthenticated +(needs.credentials) paths; parity divergences documented (expected: +envelope shape, exit codes); every slice acceptance box checkable. + +## Completeness check + +D1+D2 → package/protocol/type-test acceptance boxes; D3+D4 → engine +behavior + never-exits box; D5 → Prisma 7 fail-early box; D6 → parity + +e2e boxes and the draft-amendment box (any operator rulings during the +slice update `assets/engine/` before the PR opens). From 6c00a6b55f936a7363fa57dfbb11747569ea7f2d Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 9 Aug 2026 20:06:34 +0200 Subject: [PATCH 10/18] drive(prisma-cli-v8): strip review artifacts and superseded interface drafts Code review artifacts are never committed; the compile-verified claims survive as the permanent type-test suite in @prisma/cli-engine. Only the current normative engine-interface-draft.ts remains. References updated. Co-Authored-By: Claude Fable 5 Signed-off-by: willbot Signed-off-by: Will Madden --- .../engine/engine-interface-draft-v1.ts | 339 ------- .../engine/engine-interface-draft-v2.ts | 634 ------------- .../engine/engine-interface-draft-v3.ts | 671 -------------- .../engine/engine-interface-draft-v4.ts | 771 ---------------- .../engine/engine-interface-draft-v5.ts | 836 ----------------- .../engine/engine-interface-draft-v6.ts | 852 ----------------- .../engine/engine-interface-draft-v7.ts | 870 ------------------ .../assets/engine/reviews/code-review-r2.md | 459 --------- .../assets/engine/reviews/code-review-r3.md | 631 ------------- .../engine/reviews/code-review-r4-closure.md | 163 ---- .../engine/reviews/code-review-r5-delta.md | 164 ---- .../assets/engine/reviews/code-review.md | 659 ------------- .../reviews/envelope-collections-analysis.md | 223 ----- .../engine/reviews/system-design-review-r2.md | 443 --------- .../engine/reviews/system-design-review-r3.md | 420 --------- .../engine/reviews/system-design-review.md | 667 -------------- .drive/projects/prisma-cli-v8/design-notes.md | 12 +- .../prisma-cli-v8/plans/s1-engine-vertical.md | 4 +- .../prisma-cli-v8/specs/s1-engine-vertical.md | 5 +- 19 files changed, 10 insertions(+), 8813 deletions(-) delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v1.ts delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v2.ts delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v3.ts delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v4.ts delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v5.ts delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v6.ts delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v7.ts delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r2.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r3.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r4-closure.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r5-delta.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/reviews/code-review.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/reviews/envelope-collections-analysis.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review-r2.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review-r3.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review.md diff --git a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v1.ts b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v1.ts deleted file mode 100644 index 3c6eccf1..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v1.ts +++ /dev/null @@ -1,339 +0,0 @@ -/** - * DRAFT — the unified CLI engine's public interface, for line-by-line review. - * - * Everything a product package imports for CLI purposes lives here (R3). - * Requirement references (R1–R14) point at docs/architecture/ - * cli-engine-requirements.md in prisma-cli. Nothing from stricli appears — - * it is an internal of the engine package. - * - * The one execution protocol (agreed 2026-08-09): a handler receives - * (args, context), emits zero or more events through context.report, and - * returns a Result when done. Sync commands emit nothing; progress and - * poll commands emit along the way (timeouts are the handler's business); - * session commands keep emitting until context.signal fires, then clean up - * and return. Liveness display (spinner-equivalent) is the engine's: shown - * when a command runs quietly past a threshold. Daemon management (mode 5) - * is a separate design; `lsp`-style stdio servers bypass the protocol via - * a declared raw mode (see CommandDefinition.raw). - */ - -// ———————————————————————————————————————————————————————————————————————— -// Foundation types (from the zero-dependency foundation package, not here; -// shown for reading convenience) -// ———————————————————————————————————————————————————————————————————————— - -import type { CliStructuredError, Result } from '@prisma/cli-foundation' - -// ———————————————————————————————————————————————————————————————————————— -// §1 Events — R14: one engine vocabulary, product extensions ride in `data` -// ———————————————————————————————————————————————————————————————————————— - -/** - * The engine event envelope. `kind`-specific fields are the common - * vocabulary the engine renders consistently (human mode) and frames - * (--json mode: one line per event, `{ type, command, timestamp, data }`, - * where the event body is the data). `data` is the product extension: - * passed through to machine consumers untouched, never interpreted by the - * engine, documented and versioned by the product as its own public API. - * - * Starter vocabulary, derived from the output-modes survey's recurring - * structures (occurrence-ranked). Grows only by evidence: a structure - * recurring inside `data` across commands is the promotion signal. - */ -export type EngineEvent = - /** A named phase began. Engine renders it as a step line; steps may nest. */ - | { readonly kind: 'step-started'; readonly step: string; readonly data?: unknown } - /** The phase ended. `outcome` drives the ✔/✘/⚠ glyph. */ - | { - readonly kind: 'step-finished' - readonly step: string - readonly outcome: 'ok' | 'failed' | 'skipped' | 'warning' - readonly data?: unknown - } - /** Progress inside a phase (counts, not percentages — survey: counts+summary). */ - | { - readonly kind: 'progress' - readonly step?: string - readonly completed: number - readonly total?: number - readonly data?: unknown - } - /** A condition the user should know about; never fatal (fatal = the Result). */ - | { readonly kind: 'warning'; readonly message: string; readonly data?: unknown } - /** Informational line the product wants shown (human) / framed (json). */ - | { readonly kind: 'notice'; readonly message: string; readonly data?: unknown } - /** - * Output from a child process or remote log stream, line-oriented. - * Survey: three passthrough strategies exist today; this is the typed one. - */ - | { - readonly kind: 'output' - readonly source: string - readonly stream: 'stdout' | 'stderr' - readonly line: string - readonly data?: unknown - } - /** - * A user-actionable follow-up surfaced mid-run (survey: remediation exists - * in five encodings today — this is the one). Terminal remediation goes on - * the Result's error (`fix`) or the success envelope's nextActions instead. - */ - | { - readonly kind: 'remediation' - readonly label: string - readonly command?: string - readonly data?: unknown - } - /** A reachable endpoint became available (survey: endpoints/URLs, 3 families). */ - | { - readonly kind: 'endpoint' - readonly name: string - readonly url: string - readonly data?: unknown - } - /** A state transition in a watched external process (survey: poll loops). */ - | { - readonly kind: 'status' - readonly subject: string - readonly status: string - readonly data?: unknown - } - -// ———————————————————————————————————————————————————————————————————————— -// §2 The handler context — R4: the whole world arrives as one argument -// ———————————————————————————————————————————————————————————————————————— - -export interface CommandContext { - /** The product's validated section of prisma.config.ts (R10). Absent - * section: `undefined`; invalid section: the engine already failed the - * command before the handler ran, so handlers never see diagnostics. */ - readonly config: ConfigSection | undefined - - /** Management-API credentials, however the user authenticated (R4's why). */ - readonly credentials: Credentials | undefined - - /** The one way to emit while running (§1). Safe to call after the signal - * fires (events during teardown render normally). */ - readonly report: (event: EngineEvent) => void - - /** Interactive input. Every method returns a structured error instead of - * prompting when interaction is unavailable (--json, --no-interactive, - * CI, non-TTY) — the platform CLI's canPrompt gate, engine-owned. */ - readonly prompt: PromptSurface - - /** Fires on Ctrl-C/SIGTERM (engine-owned wiring). Session commands run - * until it fires; everything else should abort in-flight work with it. */ - readonly signal: AbortSignal - - /** Where the user invoked the CLI. Products never read process.cwd(). */ - readonly cwd: string -} - -export interface Credentials { - /** Opaque to the engine; shape owned by the Cloud product's auth library. */ - readonly token: string - readonly workspaceId?: string -} - -export interface PromptSurface { - readonly confirm: (question: string) => Promise> - readonly select: ( - question: string, - options: ReadonlyArray<{ value: T; label: string }>, - ) => Promise> - readonly text: ( - question: string, - opts?: { placeholder?: string }, - ) => Promise> -} - -// ———————————————————————————————————————————————————————————————————————— -// §3 Flags and arguments — R1: directly executable, typed by inference -// ———————————————————————————————————————————————————————————————————————— - -/** Flag declarations. `flag.json()` is the shared flag-set entry for - * commands that support --json (there are no global flags). Parse-time - * validation failures become structured errors with the allowed values — - * never framework strings. */ -export declare const flag: { - string(spec: { brief: string; placeholder?: string }): FlagSpec - requiredString(spec: { brief: string; placeholder?: string }): FlagSpec - boolean(spec: { brief: string }): FlagSpec - enum(spec: { - brief: string - values: T - }): FlagSpec - repeated(spec: { brief: string; placeholder?: string }): FlagSpec - /** The shared --json declaration; presence changes rendering, not parsing. */ - json(): FlagSpec -} - -declare const FLAG: unique symbol -export interface FlagSpec { readonly [FLAG]: T } - -export declare const positional: { - string(spec: { brief: string; placeholder: string }): PositionalSpec - optionalString(spec: { brief: string; placeholder: string }): PositionalSpec -} -declare const POSITIONAL: unique symbol -export interface PositionalSpec { readonly [POSITIONAL]: T } - -/** What the handler receives: each declared flag/positional, typed. */ -export type ArgsOf = { - readonly [K in keyof D['flags']]: D['flags'][K] extends FlagSpec ? T : never -} & { - readonly [K in keyof D['positionals']]: D['positionals'][K] extends PositionalSpec - ? T - : never -} - -// ———————————————————————————————————————————————————————————————————————— -// §4 The command definition — light at startup (R9), path-free (R12) -// ———————————————————————————————————————————————————————————————————————— - -export interface CommandDefinition< - TFlags extends Record> = Record>, - TPositionals extends Record> = Record>, - TResult = unknown, - TConfig = unknown, -> { - /** One line, imperative, shown in listings. */ - readonly brief: string - /** Paragraph(s) for `--help`. Words only — the engine formats. */ - readonly description?: string - /** Copy-pastable invocations, shown verbatim in help. */ - readonly examples?: readonly string[] - - readonly flags: TFlags - readonly positionals?: TPositionals - - /** - * The heavy part, loaded only at execution (R9). The module's default - * export is the handler. - */ - readonly handler: () => Promise<{ - default: ( - args: ArgsOf>, - ctx: CommandContext, - ) => Promise> - }> - - /** - * How a success Result renders (R5). The platform CLI's proven triple: - * `human` is required prose (stderr); `stdout` is the machine-consumable - * payload lines (what --quiet leaves); `json` projects the envelope's - * `result` and defaults to the raw value. All composed from engine - * primitives — there is no way to print. - */ - readonly present: { - readonly human: (value: TResult, ui: Ui) => readonly Block[] - readonly stdout?: (value: TResult) => readonly string[] - readonly json?: (value: TResult) => unknown - } - - /** - * Escape hatch for mode 7 (stdio protocol servers, e.g. `lsp`): the - * command owns stdin/stdout wholesale; events, presenters, and --json do - * not apply and the engine enforces that nothing else is declared. - */ - readonly raw?: false | { readonly reason: string } -} - -/** Identity function; exists so TypeScript infers the generics (R1). */ -export declare function defineCommand< - TFlags extends Record>, - TPositionals extends Record>, - TResult, - TConfig, ->( - def: CommandDefinition, -): CommandDefinition - -// ———————————————————————————————————————————————————————————————————————— -// §5 Presentation primitives — the R5 vocabulary (survey: card patterns) -// ———————————————————————————————————————————————————————————————————————— - -/** Deliberately small; grows by the same evidence rule as events. */ -export type Block = - | { readonly kind: 'summary'; readonly tone: 'ok' | 'error' | 'warning' | 'info'; readonly text: string } - | { readonly kind: 'fields'; readonly rows: ReadonlyArray<{ label: string; value: string; sensitive?: boolean }> } - | { readonly kind: 'table'; readonly columns: readonly string[]; readonly rows: ReadonlyArray } - | { readonly kind: 'list'; readonly items: readonly string[] } - | { readonly kind: 'nextSteps'; readonly steps: readonly string[] } - -/** Styling helpers usable inside block text; no direct writing. */ -export interface Ui { - readonly emphasize: (text: string) => string - readonly dim: (text: string) => string - readonly code: (text: string) => string -} - -// ———————————————————————————————————————————————————————————————————————— -// §6 Product export and shell mounting — R12: the shell owns the tree -// ———————————————————————————————————————————————————————————————————————— - -/** What a product package exports: named commands, no paths. */ -export type CommandSet = Readonly> - -/** - * Shell-side construction. Paths are space-separated (`'db migrate'`); - * group help text is declared with the mount, since groups belong to the - * tree, not to products. Collisions and grammar violations fail the build. - */ -export declare function createCli(spec: { - readonly name: string - readonly version: string - readonly groups: Readonly> - readonly commands: Readonly> -}): Cli - -export interface Cli { - /** - * Parse, execute, render, return the exit code (0/1/2/3 per R6; the - * caller assigns process.exitCode — the engine never exits or writes to - * anything but the provided streams). - */ - run(argv: readonly string[], runtime: Runtime): Promise -} - -/** Everything environmental, injected once by the bin (or by a test). */ -export interface Runtime { - readonly stdout: NodeJS.WritableStream - readonly stderr: NodeJS.WritableStream - readonly stdin: NodeJS.ReadableStream - readonly cwd: string - readonly isTty: { readonly stdin: boolean; readonly stderr: boolean } - readonly signal: AbortSignal - /** Loaded config + per-section diagnostics; the shell builds this via the - * unified loader (R10). Tests hand in fixtures. */ - readonly config: LoadedConfig - readonly credentials: Credentials | undefined -} - -export interface LoadedConfig { - readonly sections: Readonly> - readonly diagnostics: ReadonlyArray<{ readonly section: string | null; readonly error: CliStructuredError }> -} - -// ———————————————————————————————————————————————————————————————————————— -// §7 The product-repo test harness — R7: same machinery, bytes out -// ———————————————————————————————————————————————————————————————————————— - -export declare function createTestCli(spec: { - readonly commands: Readonly> - readonly groups?: Readonly> - readonly config?: Readonly> - readonly credentials?: Credentials -}): TestCli - -export interface TestCli { - run(argv: readonly string[], opts?: { readonly stdin?: string }): Promise<{ - readonly exitCode: number - readonly stdout: string - readonly stderr: string - /** The parsed --json event/envelope stream, when --json was passed. */ - readonly json: readonly unknown[] - /** Every EngineEvent the handler emitted, for semantic assertions. */ - readonly events: readonly EngineEvent[] - }> -} diff --git a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v2.ts b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v2.ts deleted file mode 100644 index 167cb5b6..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v2.ts +++ /dev/null @@ -1,634 +0,0 @@ -/** - * DRAFT v2 — the unified CLI engine's public interface, revised after the - * round-1 architect and principal-engineer reviews (see ./reviews/). - * - * Everything a product package imports for CLI purposes lives here (R3). - * Requirement references (R1–R14) point at docs/architecture/ - * cli-engine-requirements.md in prisma-cli. Nothing from stricli appears — - * it is an internal of the engine package. - * - * The execution protocol: a handler receives (args, context), emits zero - * or more events through context.report, and returns a Result when done. - * Sync commands emit nothing; progress and poll commands emit along the - * way (timeouts are the handler's business). Session commands - * (defineSessionCommand) keep emitting until context.signal fires, then - * clean up and return. Stdio protocol servers (defineRawCommand) bypass - * the protocol entirely, by declaration. Liveness display is the engine's: - * shown when a command runs quietly past a threshold. - * - * `--json` is an ENGINE MODE, not a flag products declare or handlers see - * (round-1 ruling). A value command supports it iff `present.json` exists - * (a session command always does — its stream is the JSON surface). In - * json mode the engine switches renderers, suppresses prompts (they fail - * structurally), and frames every event as one NDJSON line. The engine - * also auto-selects json mode when stdout is not a TTY — deliberate, - * agent-facing behavior. The engine injects the shared flag family on - * every command: --json, -q/--quiet, -v/--verbose, -y/--yes, - * --interactive/--no-interactive, --color/--no-color. Products cannot - * declare flags with those names. - * - * Exit codes (R6): 0 ok; 1 bug only; 2 expected structured failure; - * 3 user abort (Ctrl-C, or declining a gate the command cannot proceed - * without); 4–99 command-specific outcome codes (declared per command via - * `exitCode`); 130/143 delivered signals — the engine owns signal wiring - * and code selection. - */ - -// ———————————————————————————————————————————————————————————————————————— -// Foundation types (from the zero-dependency foundation package, not here; -// shown for reading convenience) -// ———————————————————————————————————————————————————————————————————————— - -import type { CliStructuredError, Result } from '@prisma/cli-foundation' - -/** The one severity scale (ADR 239's, the mature shipped one). Step - * outcomes are completion states, not severities — see EngineEvent. */ -export type Severity = 'error' | 'warn' | 'info' - -// ———————————————————————————————————————————————————————————————————————— -// §1 Next actions — the one remediation shape (round-1 ruling: the -// platform CLI's shipped form, adopted whole) -// ———————————————————————————————————————————————————————————————————————— - -export interface NextAction { - readonly kind: 'run-command' | 'user-choice' | 'edit-file' | 'done' - /** Open string with a recommended starter set (R-doc: journeys are - * grouping metadata; `kind` is the machine-branched field). */ - readonly journey: string - readonly label: string - readonly command?: string - readonly commands?: readonly string[] - readonly reason?: string -} - -// ———————————————————————————————————————————————————————————————————————— -// §2 Events — R14: one engine vocabulary, product extensions in `data` -// ———————————————————————————————————————————————————————————————————————— - -/** - * The engine event envelope. `kind`-specific fields are the common - * vocabulary the engine renders consistently (human mode) and frames - * (json mode: one NDJSON line per event, `{ type, command, timestamp, - * data }`). `data` is the product extension: passed through to machine - * consumers untouched, never interpreted by the engine, documented and - * versioned by the product as its own public API. A structure recurring - * inside `data` across commands is the promotion signal (R14). - * - * Rendering destinations, human mode: `output` events with channel - * 'data' are the command's data and go to OUR stdout (they are what - * `log tail > file` captures); every other event is commentary and goes - * to stderr. In json mode everything is one framed stream on stdout. - * - * Calling report() after the handler has resolved is a bug - * (InternalError) — the engine has sealed the envelope by then. Events - * emitted during teardown (after the signal, before resolution) are - * normal. - */ -export type EngineEvent = - /** A named phase began. `id`/`parentId` express nesting (the ORM's - * span shape); omitted for flat steps. */ - | { - readonly kind: 'step-started' - readonly step: string - readonly id?: string - readonly parentId?: string - readonly data?: unknown - } - /** The phase ended. `outcome` is a completion state and drives the - * ✔/✘/⚠/− glyph; it is not a severity. */ - | { - readonly kind: 'step-finished' - readonly step: string - readonly id?: string - readonly outcome: 'ok' | 'failed' | 'skipped' | 'warning' - readonly data?: unknown - } - /** Progress inside a phase (counts, not percentages — survey evidence). */ - | { - readonly kind: 'progress' - readonly step?: string - readonly completed: number - readonly total?: number - readonly data?: unknown - } - /** - * A line of commentary with a severity (round-1 ruling: 'warning' and - * 'notice' merged onto the one scale). severity 'warn' events are - * additionally aggregated by the engine into the success envelope's - * `warnings` — emit once, appear in both places. 'error' is not valid - * here: fatal problems are the Result's failure. - */ - | { - readonly kind: 'message' - readonly severity: Exclude - readonly text: string - readonly data?: unknown - } - /** - * Line-oriented output from a child process or remote stream. - * `channel` is semantic (round-1 fix): 'data' = the command's own - * output, routed to our stdout; 'diagnostic' = commentary about the - * run, routed to stderr. `source` names the emitter (service name, - * child binary), not a pipe. - */ - | { - readonly kind: 'output' - readonly source: string - readonly channel: 'data' | 'diagnostic' - readonly line: string - readonly data?: unknown - } - /** A user-actionable follow-up surfaced mid-run; the terminal ones - * belong on `present.next` instead. */ - | { readonly kind: 'remediation'; readonly action: NextAction; readonly data?: unknown } - /** A reachable endpoint became available. */ - | { - readonly kind: 'endpoint' - readonly name: string - readonly url: string - readonly data?: unknown - } - /** A state transition in a watched external process. `from` carries the - * prior state when known (survey: transitions, not snapshots). */ - | { - readonly kind: 'status' - readonly subject: string - readonly status: string - readonly from?: string - readonly data?: unknown - } - /** A file or directory this run wrote that the user may care about. */ - | { - readonly kind: 'artifact' - readonly path: string - readonly description?: string - readonly data?: unknown - } - -// ———————————————————————————————————————————————————————————————————————— -// §3 Config sections — R10 made structural (round-1: both passes' top gap) -// ———————————————————————————————————————————————————————————————————————— - -/** - * A product's named slice of prisma.config.ts. The token couples the - * section name, its validated type, and its never-throwing validator; - * commands bind to the token, which is how the engine knows which section - * a command needs — and therefore which diagnostics fail which commands. - */ -export interface ConfigSection { - readonly name: string - /** Total: any unknown in, diagnostics out. Never throws (R10). */ - readonly validate: (raw: unknown) => SectionValidation -} - -export type SectionValidation = - | { readonly ok: true; readonly value: T; readonly diagnostics: readonly CliStructuredError[] } - | { readonly ok: false; readonly diagnostics: readonly CliStructuredError[] } - -export declare function defineConfigSection(spec: { - readonly name: string - readonly validate: (raw: unknown) => SectionValidation -}): ConfigSection - -// ———————————————————————————————————————————————————————————————————————— -// §4 The handler context — R4: the whole world arrives as one argument -// ———————————————————————————————————————————————————————————————————————— - -export interface CommandContext { - /** The validated value of the command's declared config section - * (typed via the ConfigSection token), or undefined when the config - * file has no such section. A command with no declared section gets - * `undefined`. An INVALID needed section never reaches the handler — - * the engine already failed the command with that section's - * diagnostics. */ - readonly config: TConfig | undefined - - /** Management-API credentials, resolved at call time so long-lived - * sessions survive token refresh (round-1 fix). Undefined when the - * user is not authenticated. */ - readonly getCredentials: () => Promise - - /** The one way to emit while running (§2). */ - readonly report: (event: EngineEvent) => void - - /** Interactive input. In json mode, non-interactive mode, CI, or - * without a TTY, every method returns a structured error instead of - * prompting. Distinct codes distinguish "interaction unavailable" - * (exit 2) from "user cancelled the prompt" (engine maps to exit 3). */ - readonly prompt: PromptSurface - - /** Fires on Ctrl-C/SIGTERM (engine-owned wiring; the engine records - * which signal, for the 130/143 exit). Session commands run until it - * fires; everything else aborts in-flight work with it. */ - readonly signal: AbortSignal - - /** Where the user invoked the CLI. Products never read process.cwd(). */ - readonly cwd: string - - /** R13's probe: is this optional peer dependency importable from the - * user's project? Never throws; never installs anything. */ - readonly probeDependency: (specifier: string) => Promise -} - -export interface Credentials { - /** Opaque to the engine; shape owned by the Cloud product's auth - * library (placeholder pending its design). */ - readonly token: string - readonly workspaceId?: string -} - -export interface PromptSurface { - readonly confirm: (question: string) => Promise> - readonly select: ( - question: string, - options: ReadonlyArray<{ value: T; label: string }>, - ) => Promise> - readonly text: ( - question: string, - opts?: { placeholder?: string }, - ) => Promise> -} - -// ———————————————————————————————————————————————————————————————————————— -// §5 Flags and positionals — R1: directly executable, typed by inference -// ———————————————————————————————————————————————————————————————————————— - -/** - * Product-declared flags. The shared family (--json, --quiet, --verbose, - * --yes, --interactive, --color) is engine-injected and reserved — it - * never appears here and handlers never see those values; they change - * engine behavior, not handler input. Parse-time validation failures - * (bad enum value, non-numeric --timeout) become structured errors - * carrying the allowed values — never framework strings. - */ -export declare const flag: { - string(spec: { - brief: string - placeholder?: string - alias?: string - default?: string - }): FlagSpec - requiredString(spec: { brief: string; placeholder?: string; alias?: string }): FlagSpec - number(spec: { - brief: string - placeholder?: string - alias?: string - default?: number - }): FlagSpec - boolean(spec: { brief: string; alias?: string }): FlagSpec - enum(spec: { - brief: string - values: T - alias?: string - default?: T[number] - }): FlagSpec - repeated(spec: { brief: string; placeholder?: string; alias?: string }): FlagSpec -} - -declare const FLAG: unique symbol -export interface FlagSpec { - /** Phantom carrier for inference; exported so declaration emit works. */ - readonly [FLAG]: T -} -export { FLAG } - -export declare const positional: { - string(spec: { brief: string; placeholder: string }): PositionalSpec - optionalString(spec: { brief: string; placeholder: string }): PositionalSpec - /** Zero or more trailing values; at most one, last. */ - variadic(spec: { brief: string; placeholder: string }): PositionalSpec -} -declare const POSITIONAL: unique symbol -export interface PositionalSpec { - readonly [POSITIONAL]: T -} -export { POSITIONAL } - -/** - * What a handler receives. Flags and positionals live in separate - * namespaces (round-1 fix: no silent collisions, symmetric access): - * `args.flags.to`, `args.positionals.name`. - */ -export interface Args< - TFlags extends Record>, - TPositionals extends Record>, -> { - readonly flags: { readonly [K in keyof TFlags]: TFlags[K] extends FlagSpec ? T : never } - readonly positionals: { - readonly [K in keyof TPositionals]: TPositionals[K] extends PositionalSpec ? T : never - } -} - -// ———————————————————————————————————————————————————————————————————————— -// §6 Command definitions — light at startup (R9), path-free (R12) -// ———————————————————————————————————————————————————————————————————————— - -export interface CommandDefinition< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TResult = unknown, - TConfig = undefined, -> { - /** One line, imperative, shown in listings. */ - readonly brief: string - /** Paragraph(s) for --help. Words only — the engine formats. */ - readonly description?: string - /** Copy-pastable invocations, shown verbatim in help. */ - readonly examples?: readonly string[] - - readonly flags?: TFlags - readonly positionals?: TPositionals - - /** Binds the command to its product's config section (§3). The engine - * fails the command before loading the handler if this section is - * invalid; other sections' problems don't touch this command (R10). */ - readonly configSection?: ConfigSection - - /** The heavy part, loaded only at execution (R9). The module's default - * export is the handler — annotate it with CommandHandler - * (type-only import of the light definition; no runtime cycle). */ - readonly handler: () => Promise<{ default: Handler }> - - /** - * How a success Result renders (R5). The platform CLI's proven triple: - * `human` is required prose (engine writes to stderr); `stdout` is the - * machine-consumable data lines the engine writes to stdout — what - * --quiet leaves, what a pipe receives; `json` projects the envelope's - * `result` and defaults to the raw value. Its EXISTENCE is what makes - * the command support json mode. `next` supplies the envelope's - * nextActions; the human nextSteps are derived from them, so the two - * cannot disagree. - */ - readonly present: { - readonly human: (value: TResult, ui: Ui) => readonly Block[] - readonly stdout?: (value: TResult) => readonly string[] - readonly json?: (value: TResult) => unknown - readonly next?: (value: TResult) => readonly NextAction[] - } - - /** Command-specific outcome code (4–99), a pure function of the success - * value; omit for plain 0. (`migration check` exits 4 on drift.) */ - readonly exitCode?: (value: TResult) => number -} - -export type Handler< - TFlags extends Record>, - TPositionals extends Record>, - TResult, - TConfig, -> = ( - args: Args, - ctx: CommandContext, -) => Promise> - -/** For impl files: `const run: CommandHandler = …` - * — keeps definition and handler in lockstep without a runtime cycle. */ -export type CommandHandler = D extends CommandDefinition< - infer F, - infer P, - infer R, - infer C -> - ? Handler - : never - -export declare function defineCommand< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TResult = unknown, - TConfig = undefined, ->(def: CommandDefinition): CommandDefinition - -/** - * Mode 4 — sessions (dev, log tail): the handler runs until the signal - * fires, speaks entirely through events, and returns Result. There - * is no `present` — the engine owns the standard close-out line — and no - * `exitCode` (0, or the failure's code, or the signal's). A session - * always supports json mode: the event stream is its json surface. - */ -export interface SessionCommandDefinition< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, -> { - readonly brief: string - readonly description?: string - readonly examples?: readonly string[] - readonly flags?: TFlags - readonly positionals?: TPositionals - readonly configSection?: ConfigSection - readonly handler: () => Promise<{ - default: Handler - }> -} - -export declare function defineSessionCommand< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, ->(def: SessionCommandDefinition): SessionCommandDefinition - -/** - * Mode 7 — stdio protocol servers (lsp): the command owns stdin/stdout - * wholesale. Events, presenters, json mode, and prompts do not apply; - * the handler returns the exit code directly. Flags are allowed (an lsp - * takes options); the shared flag family is NOT injected. - */ -export interface RawCommandDefinition< - TFlags extends Record> = {}, -> { - readonly brief: string - readonly description?: string - readonly flags?: TFlags - readonly handler: () => Promise<{ - default: ( - args: Args, - io: { - readonly stdin: NodeJS.ReadableStream - readonly stdout: NodeJS.WritableStream - readonly stderr: NodeJS.WritableStream - readonly signal: AbortSignal - readonly cwd: string - }, - ) => Promise - }> -} - -export declare function defineRawCommand< - TFlags extends Record> = {}, ->(def: RawCommandDefinition): RawCommandDefinition - -/** Erased union for mount maps and command sets (round-1 fix: concrete - * definitions are assignable here; the generics live on define*). */ -export type AnyCommand = - | CommandDefinition - | SessionCommandDefinition - | RawCommandDefinition - -// ———————————————————————————————————————————————————————————————————————— -// §7 Presentation primitives — the R5 vocabulary -// ———————————————————————————————————————————————————————————————————————— - -/** Deliberately small; grows by the same evidence rule as events. */ -export type Block = - | { - readonly kind: 'summary' - readonly tone: 'ok' | Severity - readonly text: string - } - | { - readonly kind: 'fields' - readonly rows: ReadonlyArray<{ label: string; value: string; sensitive?: boolean }> - } - | { - readonly kind: 'table' - readonly columns: readonly string[] - readonly rows: ReadonlyArray - } - | { readonly kind: 'list'; readonly items: readonly string[] } - /** The corpus's most-rendered human structure (migration graphs, - * service trees). */ - | { readonly kind: 'tree'; readonly roots: readonly TreeNode[] } - -export interface TreeNode { - readonly label: string - readonly children?: readonly TreeNode[] -} - -/** Styling helpers usable inside block text; no direct writing. */ -export interface Ui { - readonly emphasize: (text: string) => string - readonly dim: (text: string) => string - readonly code: (text: string) => string -} - -// ———————————————————————————————————————————————————————————————————————— -// §8 Envelopes — the json contract (Layer 6, platform-proven) -// ———————————————————————————————————————————————————————————————————————— - -export interface SuccessEnvelope { - readonly ok: true - /** Stable dotted command id derived from the mount path ('db.migrate'). */ - readonly command: string - readonly result: T - /** Aggregated from severity-'warn' message events. */ - readonly warnings: readonly string[] - /** Derived from nextActions — the human-string form. */ - readonly nextSteps: readonly string[] - readonly nextActions: readonly NextAction[] -} - -export interface ErrorEnvelope { - readonly ok: false - readonly command: string - /** The CliErrorEnvelope fields (code, severity, summary, why, fix, - * where, meta, docsUrl) — nested, per the settled envelope rule. */ - readonly error: unknown - readonly warnings: readonly string[] - readonly nextSteps: readonly string[] - readonly nextActions: readonly NextAction[] -} - -/** One NDJSON line per event in json mode. */ -export interface EventFrame { - readonly type: EngineEvent['kind'] - readonly command: string - /** ISO 8601 UTC. Injectable clock in tests (§10). */ - readonly timestamp: string - readonly data: EngineEvent -} - -// ———————————————————————————————————————————————————————————————————————— -// §9 Product export and shell mounting — R12: the shell owns the tree -// ———————————————————————————————————————————————————————————————————————— - -/** What a product package exports: NAMED commands, no paths. */ -export type CommandSet = Readonly> - -/** - * Shell-side construction. Mount keys are space-separated paths - * ('db migrate'); group help text is declared with the mount, since - * groups belong to the tree, not to products. Collisions, unknown - * groups, and grammar violations fail construction (build time, not - * run time). - */ -export declare function createCli(spec: { - readonly name: string - readonly version: string - readonly groups: Readonly> - readonly commands: Readonly> -}): Cli - -export interface Cli { - /** - * Parse, execute, render, return the exit code. The engine never calls - * process.exit and never touches streams other than the ones provided. - * Auto-selects json mode when runtime.isTty.stdout is false (deliberate - * agent-facing behavior), unless the command is raw. - */ - run(argv: readonly string[], runtime: Runtime): Promise -} - -/** Everything environmental, injected once by the bin (or by a test). */ -export interface Runtime { - readonly stdout: NodeJS.WritableStream - readonly stderr: NodeJS.WritableStream - readonly stdin: NodeJS.ReadableStream - readonly cwd: string - readonly env: Readonly> - readonly isTty: { readonly stdin: boolean; readonly stdout: boolean; readonly stderr: boolean } - readonly signal: AbortSignal - /** Loaded config + per-section diagnostics; the shell builds this via - * the unified loader (R10). Tests hand in fixtures. */ - readonly config: LoadedConfig - readonly getCredentials: () => Promise -} - -export interface LoadedConfig { - /** Raw section values by name; validation happens per command via its - * ConfigSection token. */ - readonly sections: Readonly> - /** File-level problems (unevaluable module, missing version marker) — - * section: null fails every command. */ - readonly diagnostics: ReadonlyArray<{ - readonly section: string | null - readonly error: CliStructuredError - }> -} - -// ———————————————————————————————————————————————————————————————————————— -// §10 The product-repo test harness — R7: same machinery, bytes out -// ———————————————————————————————————————————————————————————————————————— - -export declare function createTestCli(spec: { - readonly commands: Readonly> - readonly groups?: Readonly> - readonly config?: Readonly> - readonly credentials?: Credentials - /** Fixed clock for deterministic EventFrame timestamps. */ - readonly now?: () => Date -}): TestCli - -export interface TestCli { - run( - argv: readonly string[], - opts?: { - readonly stdin?: string - /** Scripted prompt answers, consumed in order; a run that prompts - * past the script fails the test. */ - readonly answers?: ReadonlyArray - /** Abort the run (session tests): fires the context signal. */ - readonly abort?: AbortSignal - readonly isTty?: { stdin?: boolean; stdout?: boolean; stderr?: boolean } - readonly env?: Readonly> - }, - ): Promise<{ - readonly exitCode: number - readonly stdout: string - readonly stderr: string - /** Parsed json output (envelope + event frames) when json mode was on. */ - readonly json: readonly unknown[] - /** Every EngineEvent the handler emitted, for semantic assertions. */ - readonly events: readonly EngineEvent[] - }> -} diff --git a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v3.ts b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v3.ts deleted file mode 100644 index b6468b8c..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v3.ts +++ /dev/null @@ -1,671 +0,0 @@ -/** - * DRAFT v3 — the unified CLI engine's public interface. - * v1: initial. v2: round-1 review fixes. v3: presentation moved to the - * return site (operator ruling): handlers materialize the active mode's - * views via ctx.present at the point where the outcome is known; the - * definition carries no presenters and no result generic. Prior versions - * preserved as -v1.ts / -v2.ts; round-1 artifacts in ./reviews/. - * - * Everything a product package imports for CLI purposes lives here (R3). - * Requirement references (R1–R14) point at docs/architecture/ - * cli-engine-requirements.md in prisma-cli. Nothing from stricli appears — - * it is an internal of the engine package. - * - * The execution protocol: a handler receives (args, context), emits zero - * or more events through context.report, and returns a Result when done — - * a PresentedResult built with ctx.present, carrying pure data plus the - * materialized views the active mode needs. Sync commands emit nothing; - * progress and poll commands emit along the way (timeouts are the - * handler's business). Session commands (defineSessionCommand) keep - * emitting until context.signal fires, then clean up and return. Stdio - * protocol servers (defineRawCommand) bypass the protocol by declaration. - * Liveness display is the engine's: shown when a command runs quietly - * past a threshold. Nothing product-authored executes after the handler - * resolves — the engine receives values, never callbacks. - * - * `--json` is an ENGINE MODE, not a flag products declare or handlers - * branch on. Every value command is json-capable by construction: the - * envelope's `result` is the presented data, with `views.json` as an - * optional override. In json mode the engine switches renderers, - * suppresses prompts (they fail structurally), and frames every event as - * one NDJSON line. The engine auto-selects json mode when stdout is not - * a TTY — deliberate, agent-facing behavior. The engine injects the - * shared flag family on every non-raw command: --json, -q/--quiet, - * -v/--verbose, -y/--yes, --interactive/--no-interactive, - * --color/--no-color. Products cannot declare flags with those names. - * - * Exit codes (R6): 0 ok; 1 bug only; 2 expected structured failure; - * 3 user abort (Ctrl-C, or cancelling a gate the command cannot proceed - * without); 4–99 command-specific outcome codes (declared per command - * via `exitCode`); 130/143 delivered signals — the engine owns signal - * wiring and code selection. - */ - -// ———————————————————————————————————————————————————————————————————————— -// Foundation types (from the zero-dependency foundation package, not here; -// shown for reading convenience) -// ———————————————————————————————————————————————————————————————————————— - -import type { CliStructuredError, Result } from '@prisma/cli-foundation' - -/** The one severity scale (ADR 239's, the mature shipped one). Step - * outcomes are completion states, not severities — see EngineEvent. */ -export type Severity = 'error' | 'warn' | 'info' - -// ———————————————————————————————————————————————————————————————————————— -// §1 Next actions — the one remediation shape (the platform CLI's shipped -// form, adopted whole) -// ———————————————————————————————————————————————————————————————————————— - -export interface NextAction { - readonly kind: 'run-command' | 'user-choice' | 'edit-file' | 'done' - /** Open string with a recommended starter set (journeys are grouping - * metadata; `kind` is the machine-branched field). */ - readonly journey: string - readonly label: string - readonly command?: string - readonly commands?: readonly string[] - readonly reason?: string -} - -// ———————————————————————————————————————————————————————————————————————— -// §2 Events — R14: one engine vocabulary, product extensions in `data` -// ———————————————————————————————————————————————————————————————————————— - -/** - * The engine event envelope. `kind`-specific fields are the common - * vocabulary the engine renders consistently (human mode) and frames - * (json mode: one NDJSON line per event, `{ type, command, timestamp, - * data }`). `data` is the product extension: passed through to machine - * consumers untouched, never interpreted by the engine, documented and - * versioned by the product as its own public API. A structure recurring - * inside `data` across commands is the promotion signal (R14). - * - * Rendering destinations, human mode: `output` events with channel - * 'data' are the command's data and go to OUR stdout (they are what - * `log tail > file` captures); every other event is commentary and goes - * to stderr. In json mode everything is one framed stream on stdout. - * - * Calling report() after the handler has resolved is a bug - * (InternalError) — the engine has sealed the envelope by then. Events - * emitted during teardown (after the signal, before resolution) are - * normal. - */ -export type EngineEvent = - /** A named phase began. `id`/`parentId` express nesting (the ORM's - * span shape); omitted for flat steps. */ - | { - readonly kind: 'step-started' - readonly step: string - readonly id?: string - readonly parentId?: string - readonly data?: unknown - } - /** The phase ended. `outcome` is a completion state and drives the - * ✔/✘/⚠/− glyph; it is not a severity. */ - | { - readonly kind: 'step-finished' - readonly step: string - readonly id?: string - readonly outcome: 'ok' | 'failed' | 'skipped' | 'warning' - readonly data?: unknown - } - /** Progress inside a phase (counts, not percentages — survey evidence). */ - | { - readonly kind: 'progress' - readonly step?: string - readonly completed: number - readonly total?: number - readonly data?: unknown - } - /** - * A line of commentary with a severity ('warning' and 'notice' merged - * onto the one scale). severity 'warn' events are additionally - * aggregated by the engine into the success envelope's `warnings` — - * emit once, appear in both places. 'error' is not valid here: fatal - * problems are the Result's failure. - */ - | { - readonly kind: 'message' - readonly severity: Exclude - readonly text: string - readonly data?: unknown - } - /** - * Line-oriented output from a child process or remote stream. - * `channel` is semantic: 'data' = the command's own output, routed to - * our stdout; 'diagnostic' = commentary about the run, routed to - * stderr. `source` names the emitter (service name, child binary), - * not a pipe. - */ - | { - readonly kind: 'output' - readonly source: string - readonly channel: 'data' | 'diagnostic' - readonly line: string - readonly data?: unknown - } - /** A user-actionable follow-up surfaced mid-run; terminal ones belong - * in the presented views' `next` instead. */ - | { readonly kind: 'remediation'; readonly action: NextAction; readonly data?: unknown } - /** A reachable endpoint became available. */ - | { - readonly kind: 'endpoint' - readonly name: string - readonly url: string - readonly data?: unknown - } - /** A state transition in a watched external process. `from` carries the - * prior state when known (survey: transitions, not snapshots). */ - | { - readonly kind: 'status' - readonly subject: string - readonly status: string - readonly from?: string - readonly data?: unknown - } - /** A file or directory this run wrote that the user may care about. */ - | { - readonly kind: 'artifact' - readonly path: string - readonly description?: string - readonly data?: unknown - } - -// ———————————————————————————————————————————————————————————————————————— -// §3 Presented results — presentation materializes at the return site -// ———————————————————————————————————————————————————————————————————————— - -/** - * What a value command's handler returns inside `ok(...)`: pure data plus - * the views the ACTIVE MODE already materialized. Built exclusively by - * ctx.present — the context knows the mode, calls only the view functions - * that mode needs, and the result crossing the product→engine boundary is - * values all the way down (serializable, snapshotable, no callbacks). - * - * `data` is always present and is always what the envelope's `result` - * serializes (json view overrides when supplied). View materialization by - * mode: human mode → human + stdout + next; --quiet → stdout; - * json mode → json + next. - */ -export interface PresentedResult { - readonly data: T - readonly views: { - readonly human?: readonly Block[] - readonly stdout?: readonly string[] - readonly json?: unknown - readonly next?: readonly NextAction[] - } -} - -/** - * The view functions a handler supplies to ctx.present. Only the active - * mode's functions are invoked, at the return site, where the outcome and - * its context are live — no case-reconstruction in a distant presenter. - * `human` composes engine primitives (R5: Block is the only vocabulary); - * `stdout` is the machine-consumable data lines the engine writes to - * stdout — what --quiet leaves, what a pipe receives; `json` overrides - * the envelope's `result` (default: the data itself); `next` supplies - * nextActions — the envelope's human nextSteps derive from them. - */ -export interface Views { - readonly human: (ui: Ui) => readonly Block[] - readonly stdout?: () => readonly string[] - readonly json?: () => unknown - readonly next?: () => readonly NextAction[] -} - -// ———————————————————————————————————————————————————————————————————————— -// §4 Config sections — R10 made structural -// ———————————————————————————————————————————————————————————————————————— - -/** - * A product's named slice of prisma.config.ts. The token couples the - * section name, its validated type, and its never-throwing validator; - * commands bind to the token, which is how the engine knows which section - * a command needs — and therefore which diagnostics fail which commands. - */ -export interface ConfigSection { - readonly name: string - /** Total: any unknown in, diagnostics out. Never throws (R10). */ - readonly validate: (raw: unknown) => SectionValidation -} - -export type SectionValidation = - | { readonly ok: true; readonly value: T; readonly diagnostics: readonly CliStructuredError[] } - | { readonly ok: false; readonly diagnostics: readonly CliStructuredError[] } - -export declare function defineConfigSection(spec: { - readonly name: string - readonly validate: (raw: unknown) => SectionValidation -}): ConfigSection - -// ———————————————————————————————————————————————————————————————————————— -// §5 The handler context — R4: the whole world arrives as one argument -// ———————————————————————————————————————————————————————————————————————— - -export interface CommandContext { - /** The validated value of the command's declared config section (typed - * via the ConfigSection token), or undefined when the config file has - * no such section. An INVALID needed section never reaches the - * handler — the engine already failed the command with that section's - * diagnostics. */ - readonly config: TConfig | undefined - - /** Builds the PresentedResult for the active mode: calls only the view - * functions this mode needs, at the return site. The only constructor - * of PresentedResult. */ - readonly present: (data: T, views: Views) => PresentedResult - - /** Management-API credentials, resolved at call time so long-lived - * sessions survive token refresh. Undefined when unauthenticated. */ - readonly getCredentials: () => Promise - - /** The one way to emit while running (§2). */ - readonly report: (event: EngineEvent) => void - - /** Interactive input. In json mode, non-interactive mode, CI, or - * without a TTY, every method returns a structured error instead of - * prompting. Distinct codes distinguish "interaction unavailable" - * (exit 2) from "user cancelled the prompt" (engine maps to exit 3). */ - readonly prompt: PromptSurface - - /** Fires on Ctrl-C/SIGTERM (engine-owned wiring; the engine records - * which signal, for the 130/143 exit). Session commands run until it - * fires; everything else aborts in-flight work with it. */ - readonly signal: AbortSignal - - /** Where the user invoked the CLI. Products never read process.cwd(). */ - readonly cwd: string - - /** R13's probe: is this optional peer dependency importable from the - * user's project? Never throws; never installs anything. */ - readonly probeDependency: (specifier: string) => Promise -} - -export interface Credentials { - /** Opaque to the engine; shape owned by the Cloud product's auth - * library (placeholder pending its design). */ - readonly token: string - readonly workspaceId?: string -} - -export interface PromptSurface { - readonly confirm: (question: string) => Promise> - readonly select: ( - question: string, - options: ReadonlyArray<{ value: T; label: string }>, - ) => Promise> - readonly text: ( - question: string, - opts?: { placeholder?: string }, - ) => Promise> -} - -// ———————————————————————————————————————————————————————————————————————— -// §6 Flags and positionals — R1: directly executable, typed by inference -// ———————————————————————————————————————————————————————————————————————— - -/** - * Product-declared flags. The shared family (--json, --quiet, --verbose, - * --yes, --interactive, --color) is engine-injected and reserved — it - * never appears here and handlers never see those values; they change - * engine behavior, not handler input. Parse-time validation failures - * (bad enum value, non-numeric --timeout) become structured errors - * carrying the allowed values — never framework strings. - */ -export declare const flag: { - string(spec: { - brief: string - placeholder?: string - alias?: string - default?: string - }): FlagSpec - requiredString(spec: { brief: string; placeholder?: string; alias?: string }): FlagSpec - number(spec: { - brief: string - placeholder?: string - alias?: string - default?: number - }): FlagSpec - boolean(spec: { brief: string; alias?: string }): FlagSpec - enum(spec: { - brief: string - values: T - alias?: string - default?: T[number] - }): FlagSpec - repeated(spec: { brief: string; placeholder?: string; alias?: string }): FlagSpec -} - -declare const FLAG: unique symbol -export interface FlagSpec { - /** Phantom carrier for inference; exported so declaration emit works. */ - readonly [FLAG]: T -} -export { FLAG } - -export declare const positional: { - string(spec: { brief: string; placeholder: string }): PositionalSpec - optionalString(spec: { brief: string; placeholder: string }): PositionalSpec - /** Zero or more trailing values; at most one, last. */ - variadic(spec: { brief: string; placeholder: string }): PositionalSpec -} -declare const POSITIONAL: unique symbol -export interface PositionalSpec { - readonly [POSITIONAL]: T -} -export { POSITIONAL } - -/** - * What a handler receives. Flags and positionals live in separate - * namespaces (no silent collisions, symmetric access): `args.flags.to`, - * `args.positionals.name`. - */ -export interface Args< - TFlags extends Record>, - TPositionals extends Record>, -> { - readonly flags: { readonly [K in keyof TFlags]: TFlags[K] extends FlagSpec ? T : never } - readonly positionals: { - readonly [K in keyof TPositionals]: TPositionals[K] extends PositionalSpec ? T : never - } -} - -// ———————————————————————————————————————————————————————————————————————— -// §7 Command definitions — light at startup (R9), path-free (R12), and -// free of the result type (presentation lives at the return site) -// ———————————————————————————————————————————————————————————————————————— - -export interface CommandDefinition< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, -> { - /** One line, imperative, shown in listings. */ - readonly brief: string - /** Paragraph(s) for --help. Words only — the engine formats. */ - readonly description?: string - /** Copy-pastable invocations, shown verbatim in help. */ - readonly examples?: readonly string[] - - readonly flags?: TFlags - readonly positionals?: TPositionals - - /** Binds the command to its product's config section (§4). The engine - * fails the command before loading the handler if this section is - * invalid; other sections' problems don't touch this command (R10). */ - readonly configSection?: ConfigSection - - /** The heavy part, loaded only at execution (R9). The module's default - * export is the handler — annotate it with CommandHandler - * (type-only import of the light definition; no runtime cycle). */ - readonly handler: () => Promise<{ default: Handler }> - - /** Command-specific outcome code (4–99), a pure function of the - * presented data; omit for plain 0. (`migration check` exits 4 on - * drift.) */ - readonly exitCode?: (data: unknown) => number -} - -export type Handler< - TFlags extends Record>, - TPositionals extends Record>, - TConfig, -> = ( - args: Args, - ctx: CommandContext, -) => Promise, CliStructuredError>> - -/** For impl files: `const run: CommandHandler = …` - * — keeps definition and handler in lockstep without a runtime cycle. */ -export type CommandHandler = D extends CommandDefinition - ? Handler - : never - -export declare function defineCommand< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, ->(def: CommandDefinition): CommandDefinition - -/** - * Mode 4 — sessions (dev, log tail): the handler runs until the signal - * fires, speaks entirely through events, and returns Result. There - * is no presentation — the engine owns the standard close-out line — and - * no exitCode (0, or the failure's code, or the signal's). A session - * always supports json mode: the event stream is its json surface. - */ -export interface SessionCommandDefinition< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, -> { - readonly brief: string - readonly description?: string - readonly examples?: readonly string[] - readonly flags?: TFlags - readonly positionals?: TPositionals - readonly configSection?: ConfigSection - readonly handler: () => Promise<{ - default: ( - args: Args, - ctx: CommandContext, - ) => Promise> - }> -} - -export declare function defineSessionCommand< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, ->(def: SessionCommandDefinition): SessionCommandDefinition - -/** - * Mode 7 — stdio protocol servers (lsp): the command owns stdin/stdout - * wholesale. Events, presentation, json mode, and prompts do not apply; - * the handler returns the exit code directly. Flags are allowed (an lsp - * takes options); the shared flag family is NOT injected. - */ -export interface RawCommandDefinition< - TFlags extends Record> = {}, -> { - readonly brief: string - readonly description?: string - readonly flags?: TFlags - readonly handler: () => Promise<{ - default: ( - args: Args, - io: { - readonly stdin: NodeJS.ReadableStream - readonly stdout: NodeJS.WritableStream - readonly stderr: NodeJS.WritableStream - readonly signal: AbortSignal - readonly cwd: string - }, - ) => Promise - }> -} - -export declare function defineRawCommand< - TFlags extends Record> = {}, ->(def: RawCommandDefinition): RawCommandDefinition - -/** Erased union for mount maps and command sets (concrete definitions are - * assignable here; the generics live on define*). */ -export type AnyCommand = - | CommandDefinition - | SessionCommandDefinition - | RawCommandDefinition - -// ———————————————————————————————————————————————————————————————————————— -// §8 Presentation primitives — the R5 vocabulary -// ———————————————————————————————————————————————————————————————————————— - -/** Deliberately small; grows by the same evidence rule as events. */ -export type Block = - | { - readonly kind: 'summary' - readonly tone: 'ok' | Severity - readonly text: string - } - | { - readonly kind: 'fields' - readonly rows: ReadonlyArray<{ label: string; value: string; sensitive?: boolean }> - } - | { - readonly kind: 'table' - readonly columns: readonly string[] - readonly rows: ReadonlyArray - } - | { readonly kind: 'list'; readonly items: readonly string[] } - /** The corpus's most-rendered human structure (migration graphs, - * service trees). */ - | { readonly kind: 'tree'; readonly roots: readonly TreeNode[] } - -export interface TreeNode { - readonly label: string - readonly children?: readonly TreeNode[] -} - -/** Styling helpers usable inside block text; no direct writing. */ -export interface Ui { - readonly emphasize: (text: string) => string - readonly dim: (text: string) => string - readonly code: (text: string) => string -} - -// ———————————————————————————————————————————————————————————————————————— -// §9 Envelopes — the json contract (Layer 6, platform-proven) -// ———————————————————————————————————————————————————————————————————————— - -export interface SuccessEnvelope { - readonly ok: true - /** Stable dotted command id derived from the mount path ('db.migrate'). */ - readonly command: string - /** The presented data (json view override when supplied). */ - readonly result: T - /** Aggregated from severity-'warn' message events. */ - readonly warnings: readonly string[] - /** Derived from nextActions — the human-string form. */ - readonly nextSteps: readonly string[] - readonly nextActions: readonly NextAction[] -} - -export interface ErrorEnvelope { - readonly ok: false - readonly command: string - /** The CliErrorEnvelope fields (code, severity, summary, why, fix, - * where, meta, docsUrl) — nested, per the settled envelope rule. */ - readonly error: unknown - readonly warnings: readonly string[] - readonly nextSteps: readonly string[] - readonly nextActions: readonly NextAction[] -} - -/** One NDJSON line per event in json mode. */ -export interface EventFrame { - readonly type: EngineEvent['kind'] - readonly command: string - /** ISO 8601 UTC. Injectable clock in tests (§11). */ - readonly timestamp: string - readonly data: EngineEvent -} - -// ———————————————————————————————————————————————————————————————————————— -// §10 Product export and shell mounting — R12: the shell owns the tree -// ———————————————————————————————————————————————————————————————————————— - -/** What a product package exports: NAMED commands, no paths. */ -export type CommandSet = Readonly> - -/** - * Shell-side construction. Mount keys are space-separated paths - * ('db migrate'); group help text is declared with the mount, since - * groups belong to the tree, not to products. Collisions, unknown - * groups, and grammar violations fail construction (build time, not - * run time). - */ -export declare function createCli(spec: { - readonly name: string - readonly version: string - readonly groups: Readonly> - readonly commands: Readonly> -}): Cli - -export interface Cli { - /** - * Parse, execute, render, return the exit code. The engine never calls - * process.exit and never touches streams other than the ones provided. - * Auto-selects json mode when runtime.isTty.stdout is false (deliberate - * agent-facing behavior), unless the command is raw. - */ - run(argv: readonly string[], runtime: Runtime): Promise -} - -/** Everything environmental, injected once by the bin (or by a test). */ -export interface Runtime { - readonly stdout: NodeJS.WritableStream - readonly stderr: NodeJS.WritableStream - readonly stdin: NodeJS.ReadableStream - readonly cwd: string - readonly env: Readonly> - readonly isTty: { readonly stdin: boolean; readonly stdout: boolean; readonly stderr: boolean } - readonly signal: AbortSignal - /** Loaded config + per-section diagnostics; the shell builds this via - * the unified loader (R10). Tests hand in fixtures. */ - readonly config: LoadedConfig - readonly getCredentials: () => Promise -} - -export interface LoadedConfig { - /** Raw section values by name; validation happens per command via its - * ConfigSection token. */ - readonly sections: Readonly> - /** File-level problems (unevaluable module, missing version marker) — - * section: null fails every command. */ - readonly diagnostics: ReadonlyArray<{ - readonly section: string | null - readonly error: CliStructuredError - }> -} - -// ———————————————————————————————————————————————————————————————————————— -// §11 The product-repo test harness — R7: same machinery, bytes out -// ———————————————————————————————————————————————————————————————————————— - -export declare function createTestCli(spec: { - readonly commands: Readonly> - readonly groups?: Readonly> - readonly config?: Readonly> - readonly credentials?: Credentials - /** Fixed clock for deterministic EventFrame timestamps. */ - readonly now?: () => Date -}): TestCli - -export interface TestCli { - run( - argv: readonly string[], - opts?: { - readonly stdin?: string - /** Scripted prompt answers, consumed in order; a run that prompts - * past the script fails the test. */ - readonly answers?: ReadonlyArray - /** Abort the run (session tests): fires the context signal. */ - readonly abort?: AbortSignal - readonly isTty?: { stdin?: boolean; stdout?: boolean; stderr?: boolean } - readonly env?: Readonly> - }, - ): Promise<{ - readonly exitCode: number - readonly stdout: string - readonly stderr: string - /** Parsed json output (envelope + event frames) when json mode was on. */ - readonly json: readonly unknown[] - /** Every EngineEvent the handler emitted, for semantic assertions. */ - readonly events: readonly EngineEvent[] - /** The PresentedResult the handler returned (data + materialized - * views), for semantic assertions without byte-scraping. */ - readonly presented?: PresentedResult - }> -} diff --git a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v4.ts b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v4.ts deleted file mode 100644 index e64b8e98..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v4.ts +++ /dev/null @@ -1,771 +0,0 @@ -/** - * DRAFT v4 — the unified CLI engine's public interface. - * v1 initial · v2 round-1 fixes · v3 return-site presentation · - * v4 round-2 fixes + operator rulings: completed/errored semantics, - * --format with --json alias, one log-level mechanism, prompt defaults - * under --yes, "presentations" naming, outcome-code catalogue. - * Prior versions preserved as -v1/-v2/-v3.ts; review artifacts in - * ./reviews/. - * - * Everything a product package imports for CLI purposes lives here (R3). - * Requirement references (R1–R14) point at docs/architecture/ - * cli-engine-requirements.md in prisma-cli. Nothing from stricli appears — - * it is an internal of the engine package. - * - * EXECUTION PROTOCOL. A handler receives (args, context), emits zero or - * more events through context.report, and finishes one of two ways: - * - * COMPLETED — it returns ok(ctx.present(data, presentations)): the - * command executed to its end and has a result. A completed result may - * still be bad news; it carries an outcome code from the command's - * documented catalogue (`migration check` completes, presents its - * findings like any result, and exits 4). Presentation always runs for - * completed results. - * - * ERRORED — it returns notOk(structuredError): the command did not - * complete. The engine renders the error envelope (code, summary, why, - * fix); there is no product presentation on the error path. - * `remediation` events emitted before the error are aggregated into - * the error envelope's nextActions, as `warn` messages are into - * warnings — so guidance survives without a second presentation system. - * - * Session commands (defineSessionCommand) keep emitting until - * context.signal fires, then clean up and return. Stdio protocol servers - * (defineRawCommand) bypass the protocol by declaration. Liveness display - * is the engine's (shown when a command runs quietly past a threshold). - * Nothing product-authored executes after the handler resolves — the - * engine receives values, never callbacks. - * - * FORMATS AND LEVELS. The output format is an engine mode: - * `--format `, auto-selected when unspecified (human on a - * TTY stdout, json otherwise — deliberate agent-facing behavior); - * `--json` is shorthand for `--format json`. In json mode the engine - * suppresses prompts (they fail structurally) and frames every event as - * one NDJSON line. Commentary is filtered by `--log-level - * ` (default info); `--verbose` is shorthand - * for `--log-level verbose`. The engine injects the shared flag family - * on every non-raw command: --format/--json, --log-level/-v/--verbose, - * -q/--quiet, -y/--yes, --interactive/--no-interactive, - * --color/--no-color. Products cannot declare flags with those names. - * Product flag keys are camelCase and transliterate to --kebab-case. - * - * EXIT CODES (R6): 0 completed; 1 bug only; 2 errored (expected, - * structured); 3 user abort (Ctrl-C, or cancelling a prompt the command - * cannot proceed without); 4–99 outcome codes from the command's - * catalogue; 130/143 delivered signals. The engine owns signal wiring: - * first signal fires context.signal and awaits handler teardown; a - * second signal exits immediately with the signal's code. - */ - -// ———————————————————————————————————————————————————————————————————————— -// Foundation types (from the zero-dependency foundation package — which -// also owns NextAction, so the engine and the error envelope share it -// without a package cycle). Shown for reading convenience. -// ———————————————————————————————————————————————————————————————————————— - -import type { CliStructuredError, NextAction, Result } from '@prisma/cli-foundation' - -/** The one severity scale for commentary; also the log-level axis - * (ADR 239's error|warn|info, extended with verbose for detail - * commentary). Step outcomes are completion states, not severities. */ -export type Severity = 'error' | 'warn' | 'info' | 'verbose' -export type LogLevel = Severity - -export type Format = 'human' | 'json' - -// ———————————————————————————————————————————————————————————————————————— -// §1 Events — R14: one engine vocabulary, product extensions in `data` -// ———————————————————————————————————————————————————————————————————————— - -/** - * The engine event envelope. `kind`-specific fields are the common - * vocabulary the engine renders consistently (human mode) and frames - * (json mode). `data` is the product extension: passed through to - * machine consumers untouched, never interpreted by the engine, - * documented and versioned by the product as its own public API. A - * structure recurring inside `data` across commands is the promotion - * signal (R14). - * - * Rendering, human mode: `output` events with channel 'data' are the - * command's data and go to OUR stdout (what `log tail > file` captures); - * everything else is commentary on stderr, filtered by the active log - * level (`message` events by their severity; other kinds display at - * info). In json mode everything is framed on stdout (§9). - * - * report() is synchronous fire-and-forget; the engine buffers and writes - * asynchronously (no backpressure signal — accepted trade). Calling it - * after the handler has resolved is a bug (InternalError). Events during - * teardown (after the signal, before resolution) are normal. - */ -export type EngineEvent = - /** A named phase began. `id`/`parentId` express nesting; omitted for - * flat steps. */ - | { - readonly kind: 'step-started' - readonly step: string - readonly id?: string - readonly parentId?: string - readonly data?: unknown - } - /** The phase ended. `outcome` is a completion state and drives the - * ✔/✘/⚠/− glyph; it is not a severity. */ - | { - readonly kind: 'step-finished' - readonly step: string - readonly id?: string - readonly outcome: 'ok' | 'failed' | 'skipped' | 'warning' - readonly data?: unknown - } - /** Progress inside a phase (counts, not percentages). */ - | { - readonly kind: 'progress' - readonly step?: string - readonly completed: number - readonly total?: number - readonly data?: unknown - } - /** - * A line of commentary at a severity. 'warn' messages are additionally - * aggregated into the envelope's `warnings`; 'verbose' messages render - * only at --log-level verbose. 'error' is not valid here: fatal - * problems are the Result's error. - */ - | { - readonly kind: 'message' - readonly severity: Exclude - readonly text: string - readonly data?: unknown - } - /** - * Line-oriented output from a child process or remote stream. - * `channel` is semantic: 'data' = the command's own output (our - * stdout); 'diagnostic' = commentary about the run (stderr). `source` - * names the emitter, not a pipe. - */ - | { - readonly kind: 'output' - readonly source: string - readonly channel: 'data' | 'diagnostic' - readonly line: string - readonly data?: unknown - } - /** A user-actionable follow-up surfaced mid-run. Aggregated into the - * final envelope's nextActions (completed OR errored). */ - | { readonly kind: 'remediation'; readonly action: NextAction; readonly data?: unknown } - /** A reachable endpoint became available. */ - | { - readonly kind: 'endpoint' - readonly name: string - readonly url: string - readonly data?: unknown - } - /** A state transition in a watched external process; `from` carries - * the prior state when known. */ - | { - readonly kind: 'status' - readonly subject: string - readonly status: string - readonly from?: string - readonly data?: unknown - } - /** A file or directory this run wrote that the user may care about. */ - | { - readonly kind: 'artifact' - readonly path: string - readonly description?: string - readonly data?: unknown - } - -// ———————————————————————————————————————————————————————————————————————— -// §2 Presented results — presentation materializes at the return site -// ———————————————————————————————————————————————————————————————————————— - -declare const PRESENTED: unique symbol - -/** - * What a completed command's handler returns inside `ok(...)`: pure data - * plus the presentation the ACTIVE FORMAT already materialized. Built - * exclusively by ctx.present (the brand makes hand-construction a type - * error) — the context knows the format, calls only the presentation - * functions it needs, and the value crossing the product→engine boundary - * is data all the way down. - * - * `data` is always present and is what the envelope's `result` - * serializes (json presentation overrides when supplied). Materialization - * by format: human → human + stdout + next; human+--quiet → stdout; - * json → json + next. - */ -export interface PresentedResult { - readonly [PRESENTED]: true - readonly data: T - /** The outcome code selected at the return site; must be a key of the - * definition's catalogue. Omitted = 0. */ - readonly outcomeCode?: number - readonly presentation: { - readonly human?: readonly Block[] - readonly stdout?: readonly string[] - readonly json?: unknown - readonly next?: readonly NextAction[] - } -} -export { PRESENTED } - -/** - * The per-format presentation functions a handler supplies to - * ctx.present. Only the active format's functions are invoked, at the - * return site, where the outcome and its context are live. `human` - * composes engine primitives (R5: Block is the only vocabulary); - * `stdout` is the machine-consumable data lines the engine writes to - * stdout — what --quiet leaves, what a pipe receives; `json` overrides - * the envelope's `result` (default: the data itself); `next` supplies - * nextActions — the envelope's human nextSteps derive from them. - */ -export interface Presentations { - readonly human: (ui: Ui) => readonly Block[] - readonly stdout?: () => readonly string[] - readonly json?: () => unknown - readonly next?: () => readonly NextAction[] -} - -// ———————————————————————————————————————————————————————————————————————— -// §3 Config sections — R10 made structural -// ———————————————————————————————————————————————————————————————————————— - -/** - * A product's named slice of prisma.config.ts. The token couples the - * section name, its validated type, and its never-throwing validator; - * commands bind to the token, which is how the engine knows which - * section a command needs — and therefore which diagnostics fail which - * commands. Keep validators dependency-light: they load with the - * definition tree at startup (R9), not with the handler. - */ -export interface ConfigSection { - readonly name: string - /** Total: any unknown in, diagnostics out. Never throws (R10). */ - readonly validate: (raw: unknown) => SectionValidation -} - -export type SectionValidation = - | { readonly ok: true; readonly value: T; readonly diagnostics: readonly CliStructuredError[] } - | { readonly ok: false; readonly diagnostics: readonly CliStructuredError[] } - -export declare function defineConfigSection(spec: { - readonly name: string - readonly validate: (raw: unknown) => SectionValidation -}): ConfigSection - -// ———————————————————————————————————————————————————————————————————————— -// §4 The handler context — R4: the whole world arrives as one argument -// ———————————————————————————————————————————————————————————————————————— - -export interface CommandContext { - /** The validated value of the command's declared config section, or - * undefined when the config file has no such section. An INVALID - * needed section never reaches the handler — the engine already - * failed the command with that section's diagnostics. */ - readonly config: TConfig | undefined - - /** Builds the PresentedResult for the active format: calls only the - * presentation functions this format needs, at the return site. The - * only constructor of PresentedResult. `outcomeCode` must be a key of - * the definition's catalogue (engine-verified). */ - readonly present: ( - data: T, - presentations: Presentations, - opts?: { readonly outcomeCode?: number }, - ) => PresentedResult - - /** Management-API credentials, resolved at call time so long-lived - * sessions survive token refresh. Undefined when unauthenticated. - * Commands declaring `requiresCredentials` never see undefined — the - * engine fails them early with the sign-in error. */ - readonly getCredentials: () => Promise - - /** The one way to emit while running (§1). */ - readonly report: (event: EngineEvent) => void - - /** Interactive input (§4a). */ - readonly prompt: PromptSurface - - /** Fires on Ctrl-C/SIGTERM (engine-owned; the engine records which - * signal for the 130/143 exit, and force-exits on a second signal). - * Session commands run until it fires; everything else aborts - * in-flight work with it. */ - readonly signal: AbortSignal - - /** Where the user invoked the CLI. Products never read process.cwd(). */ - readonly cwd: string - - /** R13: probe an optional peer dependency's availability from the - * user's project. Never throws; never installs. Pair with - * `packageManager` to phrase the install command in the structured - * error when it's absent. */ - readonly probeDependency: (specifier: string) => Promise - - /** The user's detected package manager, for install-command phrasing. */ - readonly packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' -} - -export interface Credentials { - /** Opaque to the engine; shape owned by the Cloud product's auth - * library (placeholder pending its design). */ - readonly token: string - readonly workspaceId?: string -} - -/** - * §4a Prompts. Every prompt may carry a product-specified `default`. - * Interactively, Enter accepts the default. Under --yes, a prompt WITH a - * default resolves to it without displaying; a prompt WITHOUT a default - * cannot be operated and the invocation halts with a structured error - * (exit 2). Destructive confirmations therefore simply declare no - * default — --yes can never blast through them; they require their - * explicit flag (--force / --confirm ) per the confirmation rule. - * In json/non-interactive/CI/non-TTY contexts the same default rule - * applies as under --yes. User cancellation (Ctrl-C at the prompt) is a - * distinct structured error the engine maps to exit 3. - */ -export interface PromptSurface { - readonly confirm: ( - question: string, - opts?: { readonly default?: boolean }, - ) => Promise> - readonly select: ( - question: string, - options: ReadonlyArray<{ value: T; label: string }>, - opts?: { readonly default?: T }, - ) => Promise> - readonly text: ( - question: string, - opts?: { readonly placeholder?: string; readonly default?: string }, - ) => Promise> -} - -// ———————————————————————————————————————————————————————————————————————— -// §5 Flags and positionals — R1: directly executable, typed by inference -// ———————————————————————————————————————————————————————————————————————— - -/** - * Product-declared flags. The shared family (§header) is engine-injected - * and reserved; handlers never see those values. Parse-time validation - * failures become structured errors carrying the allowed values — never - * framework strings. - * - * Deliberate asymmetry, matching CLI convention: flags are optional by - * default (requiredString is the exception); positionals are required by - * default (optionalString is the exception). - */ -export declare const flag: { - string(spec: { - brief: string - placeholder?: string - alias?: SingleChar - default?: string - }): FlagSpec - requiredString(spec: { brief: string; placeholder?: string; alias?: SingleChar }): FlagSpec - number(spec: { - brief: string - placeholder?: string - alias?: SingleChar - default?: number - }): FlagSpec - boolean(spec: { brief: string; alias?: SingleChar }): FlagSpec - enum(spec: { - brief: string - values: T - alias?: SingleChar - default?: T[number] - }): FlagSpec - repeated(spec: { brief: string; placeholder?: string; alias?: SingleChar }): FlagSpec -} - -/** Single-character alias; longer strings are a construction error. */ -export type SingleChar = string & { readonly length?: 1 } - -declare const FLAG: unique symbol -export interface FlagSpec { - /** Phantom carrier for inference; exported so declaration emit works. */ - readonly [FLAG]: T -} -export { FLAG } - -export declare const positional: { - string(spec: { brief: string; placeholder: string }): PositionalSpec - optionalString(spec: { brief: string; placeholder: string }): PositionalSpec - /** Zero or more trailing values; at most one, declared last (order = - * declaration order; keys must not be integer-like). */ - variadic(spec: { brief: string; placeholder: string }): PositionalSpec -} -declare const POSITIONAL: unique symbol -export interface PositionalSpec { - readonly [POSITIONAL]: T -} -export { POSITIONAL } - -/** What a handler receives: separate namespaces, symmetric access — - * `args.flags.to`, `args.positionals.name`. */ -export interface Args< - TFlags extends Record>, - TPositionals extends Record>, -> { - readonly flags: { readonly [K in keyof TFlags]: TFlags[K] extends FlagSpec ? T : never } - readonly positionals: { - readonly [K in keyof TPositionals]: TPositionals[K] extends PositionalSpec ? T : never - } -} - -// ———————————————————————————————————————————————————————————————————————— -// §6 Command definitions — light at startup (R9), path-free (R12), -// runtime-discriminated by `kind` (stamped by the define* functions) -// ———————————————————————————————————————————————————————————————————————— - -export interface CommandDefinition< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, -> { - readonly kind: 'command' - /** One line, imperative, shown in listings. */ - readonly brief: string - /** Paragraph(s) for --help. Words only — the engine formats. */ - readonly description?: string - /** Copy-pastable invocations, shown verbatim in help. */ - readonly examples?: readonly string[] - - readonly flags?: TFlags - readonly positionals?: TPositionals - - /** Binds the command to its product's config section (§3). */ - readonly configSection?: ConfigSection - - /** Fail early with the sign-in error when unauthenticated; the handler - * then always receives credentials. */ - readonly requiresCredentials?: boolean - - /** - * The command's documented outcome codes (4–99): code → meaning. - * Rendered in help without executing anything; the return site selects - * one via ctx.present's outcomeCode, which the engine verifies against - * this catalogue. Absent = the command only exits 0/1/2/3. - */ - readonly outcomeCodes?: Readonly> - - /** The heavy part, loaded only at execution (R9). The module's default - * export is the handler — annotate it with CommandHandler - * (type-only import of the light definition; no runtime cycle). */ - readonly handler: () => Promise<{ default: Handler }> -} - -export type Handler< - TFlags extends Record>, - TPositionals extends Record>, - TConfig, -> = ( - args: Args, - ctx: CommandContext, -) => Promise, CliStructuredError>> - -/** For impl files: `const run: CommandHandler = …` */ -export type CommandHandler = D extends CommandDefinition - ? Handler - : never - -export declare function defineCommand< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, ->( - def: Omit, 'kind'>, -): CommandDefinition - -/** - * Mode 4 — sessions (dev, log tail): the handler runs until the signal - * fires, speaks entirely through events, and returns Result. No - * presentation (the engine owns the close-out line), no outcome codes. - * A session always supports json mode: the event stream is its json - * surface. - */ -export interface SessionCommandDefinition< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, -> { - readonly kind: 'session' - readonly brief: string - readonly description?: string - readonly examples?: readonly string[] - readonly flags?: TFlags - readonly positionals?: TPositionals - readonly configSection?: ConfigSection - readonly requiresCredentials?: boolean - readonly handler: () => Promise<{ - default: ( - args: Args, - ctx: CommandContext, - ) => Promise> - }> -} - -export declare function defineSessionCommand< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, ->( - def: Omit, 'kind'>, -): SessionCommandDefinition - -/** - * Mode 7 — stdio protocol servers (lsp): the command owns stdin/stdout - * wholesale. Events, presentation, formats, and prompts do not apply; - * the handler returns the exit code directly. Flags and config are - * allowed; the shared flag family is NOT injected. - */ -export interface RawCommandDefinition< - TFlags extends Record> = {}, - TConfig = undefined, -> { - readonly kind: 'raw' - readonly brief: string - readonly description?: string - readonly flags?: TFlags - readonly configSection?: ConfigSection - readonly handler: () => Promise<{ - default: ( - args: Args, - io: { - readonly stdin: InputStream - readonly stdout: OutputStream - readonly stderr: OutputStream - readonly signal: AbortSignal - readonly cwd: string - readonly config: TConfig | undefined - }, - ) => Promise - }> -} - -export declare function defineRawCommand< - TFlags extends Record> = {}, - TConfig = undefined, ->( - def: Omit, 'kind'>, -): RawCommandDefinition - -/** Erased union for mount maps; `kind` is the runtime discriminant. */ -export type AnyCommand = - | CommandDefinition - | SessionCommandDefinition - | RawCommandDefinition - -// ———————————————————————————————————————————————————————————————————————— -// §7 Presentation primitives — the R5 vocabulary -// ———————————————————————————————————————————————————————————————————————— - -/** Deliberately small; grows by the same evidence rule as events. */ -export type Block = - | { - readonly kind: 'summary' - readonly tone: 'ok' | Exclude - readonly text: string - } - | { - readonly kind: 'fields' - readonly rows: ReadonlyArray<{ label: string; value: string; sensitive?: boolean }> - } - | { - readonly kind: 'table' - readonly columns: readonly string[] - readonly rows: ReadonlyArray - } - | { readonly kind: 'list'; readonly items: readonly string[] } - /** The corpus's most-rendered human structure (migration graphs, - * service trees). */ - | { readonly kind: 'tree'; readonly roots: readonly TreeNode[] } - /** - * Structured errors carried inside a COMPLETED result (drift findings, - * verification failures, config diagnostics). The engine renders each - * with the same layout it uses for top-level errors (✖ summary (CODE), - * Why, Fix) — consistent by construction; products never hand-build - * error presentation. In the data/json side, carry the same errors as - * their envelopes (`toEnvelope()`). - */ - | { readonly kind: 'errors'; readonly errors: ReadonlyArray } - -export interface TreeNode { - readonly label: string - readonly children?: readonly TreeNode[] -} - -/** Styling helpers usable inside block text; no direct writing. */ -export interface Ui { - readonly emphasize: (text: string) => string - readonly dim: (text: string) => string - readonly code: (text: string) => string -} - -// ———————————————————————————————————————————————————————————————————————— -// §8 Streams — minimal structural types; no NodeJS.* in the public -// surface (runtime-agnosticism, R4's why) -// ———————————————————————————————————————————————————————————————————————— - -export interface OutputStream { - write(text: string): void -} -export interface InputStream extends AsyncIterable {} - -// ———————————————————————————————————————————————————————————————————————— -// §9 Envelopes and frames — the json contract -// ———————————————————————————————————————————————————————————————————————— - -export interface CompletedEnvelope { - /** ok = COMPLETED (the command executed to its end). A completed - * result may still carry a non-zero outcome code — bad news is a - * result, not an error. */ - readonly ok: true - /** Stable dotted command id derived from the mount path ('db.migrate'). */ - readonly command: string - /** The presented data (json presentation override when supplied). */ - readonly result: T - /** From the outcome catalogue; 0 when absent. */ - readonly outcomeCode: number - /** Aggregated from severity-'warn' message events. */ - readonly warnings: readonly string[] - /** Derived from nextActions — the human-string form. */ - readonly nextSteps: readonly string[] - readonly nextActions: readonly NextAction[] -} - -export interface ErroredEnvelope { - /** ok = false: the command did NOT complete. */ - readonly ok: false - readonly command: string - /** The CliErrorEnvelope fields (code, severity, summary, why, fix, - * where, meta, docsUrl) — nested, per the settled envelope rule. */ - readonly error: unknown - readonly warnings: readonly string[] - /** Aggregated from remediation events + derived from the error's fix. */ - readonly nextSteps: readonly string[] - readonly nextActions: readonly NextAction[] -} - -/** json mode emits one frame per line: events while running, then - * exactly one result frame. */ -export type Frame = EventFrame | ResultFrame - -export interface EventFrame { - readonly type: 'event' - readonly command: string - /** ISO 8601 UTC. Injectable clock in tests (§11). */ - readonly timestamp: string - readonly event: EngineEvent -} - -export interface ResultFrame { - readonly type: 'result' - readonly command: string - readonly timestamp: string - readonly envelope: CompletedEnvelope | ErroredEnvelope -} - -// ———————————————————————————————————————————————————————————————————————— -// §10 Product export and shell mounting — R12: the shell owns the tree -// ———————————————————————————————————————————————————————————————————————— - -/** What a product package exports: commands by NAME. */ -export type CommandSet = Readonly> - -/** What the shell builds: commands by PATH (space-separated, - * 'db migrate'). Distinct alias so the two maps never read as one. */ -export type MountedTree = Readonly> - -/** - * Shell-side construction. Group help is declared with the mount, since - * groups belong to the tree, not to products. Collisions, unknown - * groups, reserved-flag violations, and grammar violations fail - * construction (build time, not run time). - */ -export declare function createCli(spec: { - readonly name: string - readonly version: string - readonly groups: Readonly> - readonly commands: MountedTree -}): Cli - -export interface Cli { - /** Parse, execute, render, return the exit code. Never calls - * process.exit; never touches streams other than the provided ones. */ - run(argv: readonly string[], runtime: Runtime): Promise -} - -/** Everything environmental, injected once by the bin (or by a test). */ -export interface Runtime { - readonly stdout: OutputStream - readonly stderr: OutputStream - readonly stdin: InputStream - readonly cwd: string - readonly env: Readonly> - readonly isTty: { readonly stdin: boolean; readonly stdout: boolean; readonly stderr: boolean } - readonly signal: AbortSignal - /** Loaded config + per-section diagnostics; the shell builds this via - * the unified loader (R10). Tests hand in fixtures. */ - readonly config: LoadedConfig - readonly getCredentials: () => Promise - readonly packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' -} - -export interface LoadedConfig { - /** Raw section values by name; validation happens per command via its - * ConfigSection token. */ - readonly sections: Readonly> - /** File-level problems (unevaluable module, missing version marker) — - * section: null fails every command. */ - readonly diagnostics: ReadonlyArray<{ - readonly section: string | null - readonly error: CliStructuredError - }> -} - -// ———————————————————————————————————————————————————————————————————————— -// §11 The product-repo test harness — R7: same machinery, bytes out -// ———————————————————————————————————————————————————————————————————————— - -export declare function createTestCli(spec: { - readonly commands: MountedTree - readonly groups?: Readonly> - readonly config?: Readonly> - readonly credentials?: Credentials - readonly packageManager?: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' - /** Fixed clock for deterministic frame timestamps. */ - readonly now?: () => Date -}): TestCli - -export interface TestCli { - run( - argv: readonly string[], - opts?: { - readonly stdin?: string - /** Scripted prompt answers, consumed in order; a run that prompts - * past the script fails the test. */ - readonly answers?: ReadonlyArray - /** Abort the run (session tests): fires the context signal. */ - readonly abort?: AbortSignal - /** Live event tap, for asserting mid-session behavior before - * aborting. */ - readonly onEvent?: (event: EngineEvent) => void - readonly cwd?: string - readonly isTty?: { stdin?: boolean; stdout?: boolean; stderr?: boolean } - readonly env?: Readonly> - }, - ): Promise<{ - readonly exitCode: number - readonly stdout: string - readonly stderr: string - /** Parsed frames (events + the result frame) when json mode was on. */ - readonly json: readonly Frame[] - /** Every EngineEvent the handler emitted, for semantic assertions. */ - readonly events: readonly EngineEvent[] - /** The PresentedResult the handler returned (data + materialized - * presentation), for semantic assertions without byte-scraping. */ - readonly presented?: PresentedResult - }> -} diff --git a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v5.ts b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v5.ts deleted file mode 100644 index c484c85d..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v5.ts +++ /dev/null @@ -1,836 +0,0 @@ -/** - * DRAFT v5 — the unified CLI engine's public interface. - * v1 initial · v2 round-1 fixes · v3 return-site presentation · - * v4 completed/errored semantics, --format, log levels, prompt defaults · - * v5 round-3 closure: diagnostics declared once at ctx.present, typed - * outcome codes, prompt.consent, byte-capable raw stdin. - * Prior versions preserved as -v1…-v4.ts; review artifacts in ./reviews/. - * - * THE MODEL, in one analogy (operator, 2026-08-09): commands settle like - * promises. A command can COMPLETE — and its completion can be - * successful or unsuccessful, both presented through the same machinery, - * distinguished by outcome code and diagnostics — or it can ERROR, which - * aborts out of the normal process and gets its own special handling. - * - * Implementation prerequisite: ADR 239 (prisma/prisma) is amended so - * completed-but-unsuccessful command results (verify/check/runner - * findings) are carried as diagnostics with their dotted codes inside a - * completed envelope with a catalogued exit code — not as structured - * failures with exit 2, as it classifies them today. - * - * Everything a product package imports for CLI purposes lives here (R3). - * Requirement references (R1–R14) point at docs/architecture/ - * cli-engine-requirements.md in prisma-cli. Nothing from stricli appears — - * it is an internal of the engine package. - * - * EXECUTION PROTOCOL. A handler receives (args, context), emits zero or - * more events through context.report, and finishes one of two ways: - * - * COMPLETED — it returns ok(ctx.present(data, presentations)): the - * command executed to its end and has a result. A completed result may - * still be bad news; it carries an outcome code from the command's - * documented catalogue (`migration check` completes, presents its - * findings like any result, and exits 4). Presentation always runs for - * completed results. - * - * ERRORED — it returns notOk(structuredError): the command did not - * complete. The engine renders the error envelope (code, summary, why, - * fix); there is no product presentation on the error path. - * `remediation` events emitted before the error are aggregated into - * the error envelope's nextActions, as `warn` messages are into - * warnings — so guidance survives without a second presentation system. - * - * Session commands (defineSessionCommand) keep emitting until - * context.signal fires, then clean up and return. Stdio protocol servers - * (defineRawCommand) bypass the protocol by declaration. Liveness display - * is the engine's (shown when a command runs quietly past a threshold). - * Nothing product-authored executes after the handler resolves — the - * engine receives values, never callbacks. - * - * FORMATS AND LEVELS. The output format is an engine mode: - * `--format `, auto-selected when unspecified (human on a - * TTY stdout, json otherwise — deliberate agent-facing behavior); - * `--json` is shorthand for `--format json`. In json mode the engine - * suppresses prompts (they fail structurally) and frames every event as - * one NDJSON line. Commentary is filtered by `--log-level - * ` (default info); `--verbose` is shorthand - * for `--log-level verbose`. The engine injects the shared flag family - * on every non-raw command: --format/--json, --log-level/-v/--verbose, - * -q/--quiet, -y/--yes, --interactive/--no-interactive, - * --color/--no-color. Products cannot declare flags with those names. - * Product flag keys are camelCase and transliterate to --kebab-case. - * - * EXIT CODES (R6): 0 completed; 1 bug only; 2 errored (expected, - * structured); 3 user abort (Ctrl-C, or cancelling a prompt the command - * cannot proceed without); 4–99 outcome codes from the command's - * catalogue; 130/143 delivered signals. The engine owns signal wiring: - * first signal fires context.signal and awaits handler teardown; a - * second signal exits immediately with the signal's code. - */ - -// ———————————————————————————————————————————————————————————————————————— -// Foundation types (from the zero-dependency foundation package — which -// also owns NextAction, so the engine and the error envelope share it -// without a package cycle). Shown for reading convenience. -// ———————————————————————————————————————————————————————————————————————— - -import type { CliStructuredError, NextAction, Result } from '@prisma/cli-foundation' - -/** The one severity scale for commentary; also the log-level axis - * (ADR 239's error|warn|info, extended with verbose for detail - * commentary). Step outcomes are completion states, not severities. */ -export type Severity = 'error' | 'warn' | 'info' | 'verbose' -export type LogLevel = Severity - -export type Format = 'human' | 'json' - -// ———————————————————————————————————————————————————————————————————————— -// §1 Events — R14: one engine vocabulary, product extensions in `data` -// ———————————————————————————————————————————————————————————————————————— - -/** - * The engine event envelope. `kind`-specific fields are the common - * vocabulary the engine renders consistently (human mode) and frames - * (json mode). `data` is the product extension: passed through to - * machine consumers untouched, never interpreted by the engine, - * documented and versioned by the product as its own public API. A - * structure recurring inside `data` across commands is the promotion - * signal (R14). - * - * Rendering, human mode: `output` events with channel 'data' are the - * command's data and go to OUR stdout (what `log tail > file` captures); - * everything else is commentary on stderr, filtered by the active log - * level (`message` events by their severity; other kinds display at - * info). In json mode everything is framed on stdout (§9). - * - * report() is synchronous fire-and-forget; the engine buffers and writes - * asynchronously (no backpressure signal — accepted trade). Calling it - * after the handler has resolved is a bug (InternalError). Events during - * teardown (after the signal, before resolution) are normal. - */ -export type EngineEvent = - /** A named phase began. `id`/`parentId` express nesting; omitted for - * flat steps. */ - | { - readonly kind: 'step-started' - readonly step: string - readonly id?: string - readonly parentId?: string - readonly data?: unknown - } - /** The phase ended. `outcome` is a completion state and drives the - * ✔/✘/⚠/− glyph; it is not a severity. */ - | { - readonly kind: 'step-finished' - readonly step: string - readonly id?: string - readonly outcome: 'ok' | 'failed' | 'skipped' | 'warning' - readonly data?: unknown - } - /** Progress inside a phase (counts, not percentages). */ - | { - readonly kind: 'progress' - readonly step?: string - readonly completed: number - readonly total?: number - readonly data?: unknown - } - /** - * A line of commentary at a severity. 'warn' messages are additionally - * aggregated into the envelope's `warnings`; 'verbose' messages render - * only at --log-level verbose. 'error' is not valid here: fatal - * problems are the Result's error. - */ - | { - readonly kind: 'message' - readonly severity: Exclude - readonly text: string - readonly data?: unknown - } - /** - * Line-oriented output from a child process or remote stream. - * `channel` is semantic: 'data' = the command's own output (our - * stdout); 'diagnostic' = commentary about the run (stderr). `source` - * names the emitter, not a pipe. - */ - | { - readonly kind: 'output' - readonly source: string - readonly channel: 'data' | 'diagnostic' - readonly line: string - readonly data?: unknown - } - /** A user-actionable follow-up surfaced mid-run. Aggregated into the - * final envelope's nextActions (completed OR errored). */ - | { readonly kind: 'remediation'; readonly action: NextAction; readonly data?: unknown } - /** A reachable endpoint became available. */ - | { - readonly kind: 'endpoint' - readonly name: string - readonly url: string - readonly data?: unknown - } - /** A state transition in a watched external process; `from` carries - * the prior state when known. */ - | { - readonly kind: 'status' - readonly subject: string - readonly status: string - readonly from?: string - readonly data?: unknown - } - /** A file or directory this run wrote that the user may care about. */ - | { - readonly kind: 'artifact' - readonly path: string - readonly description?: string - readonly data?: unknown - } - -// ———————————————————————————————————————————————————————————————————————— -// §2 Presented results — presentation materializes at the return site -// ———————————————————————————————————————————————————————————————————————— - -declare const PRESENTED: unique symbol - -/** - * What a completed command's handler returns inside `ok(...)`: pure data - * plus the presentation the ACTIVE FORMAT already materialized. Built - * exclusively by ctx.present (the brand makes hand-construction a type - * error) — the context knows the format, calls only the presentation - * functions it needs, and the value crossing the product→engine boundary - * is data all the way down. - * - * `data` is always present and is what the envelope's `result` - * serializes (json presentation overrides when supplied). Materialization - * by format: human → human + stdout + next; human+--quiet → stdout; - * json → json + next. - */ -export interface PresentedResult { - readonly [PRESENTED]: true - readonly data: T - /** The outcome code selected at the return site; typed against the - * definition's catalogue keys. Omitted = 0. */ - readonly outcomeCode?: number - /** - * Structured findings carried by a COMPLETED result (drift, verify - * failures) — declared ONCE here; the engine renders them in human - * mode with the same layout as top-level errors (shown even under - * --quiet) AND serializes their envelopes into - * CompletedEnvelope.diagnostics. One declaration, both surfaces, - * impossible to diverge. Guardrail: any severity-'error' entry - * requires a non-zero outcomeCode — a genuine could-not-complete - * belongs in notOk, not here. The test: notOk when the command - * couldn't do its job; diagnostics when finding these WAS the job. - */ - readonly diagnostics?: readonly CliStructuredError[] - readonly presentation: { - readonly human?: readonly Block[] - readonly stdout?: readonly string[] - readonly json?: unknown - readonly next?: readonly NextAction[] - } -} -export { PRESENTED } - -/** - * The per-format presentation functions a handler supplies to - * ctx.present. Only the active format's functions are invoked, at the - * return site, where the outcome and its context are live. `human` - * composes engine primitives (R5: Block is the only vocabulary); - * `stdout` is the machine-consumable data lines the engine writes to - * stdout — what --quiet leaves, what a pipe receives; `json` overrides - * the envelope's `result` (default: the data itself); `next` supplies - * nextActions — the envelope's human nextSteps derive from them. - */ -export interface Presentations { - readonly human: (ui: Ui) => readonly Block[] - readonly stdout?: () => readonly string[] - readonly json?: () => unknown - readonly next?: () => readonly NextAction[] -} - -// ———————————————————————————————————————————————————————————————————————— -// §3 Config sections — R10 made structural -// ———————————————————————————————————————————————————————————————————————— - -/** - * A product's named slice of prisma.config.ts. The token couples the - * section name, its validated type, and its never-throwing validator; - * commands bind to the token, which is how the engine knows which - * section a command needs — and therefore which diagnostics fail which - * commands. Keep validators dependency-light: they load with the - * definition tree at startup (R9), not with the handler. - */ -export interface ConfigSection { - readonly name: string - /** Total: any unknown in, diagnostics out. Never throws (R10). */ - readonly validate: (raw: unknown) => SectionValidation -} - -export type SectionValidation = - | { readonly ok: true; readonly value: T; readonly diagnostics: readonly CliStructuredError[] } - | { readonly ok: false; readonly diagnostics: readonly CliStructuredError[] } - -export declare function defineConfigSection(spec: { - readonly name: string - readonly validate: (raw: unknown) => SectionValidation -}): ConfigSection - -// ———————————————————————————————————————————————————————————————————————— -// §4 The handler context — R4: the whole world arrives as one argument -// ———————————————————————————————————————————————————————————————————————— - -export interface CommandContext { - /** The validated value of the command's declared config section, or - * undefined when the config file has no such section. An INVALID - * needed section never reaches the handler — the engine already - * failed the command with that section's diagnostics. */ - readonly config: TConfig | undefined - - /** Builds the PresentedResult for the active format: calls only the - * presentation functions this format needs, at the return site. The - * only constructor of PresentedResult. `outcomeCode` is typed against - * the definition's catalogue keys — a wrong code is a compile error - * at the call site. `diagnostics` are the completed result's - * structured findings (see PresentedResult). */ - readonly present: ( - data: T, - presentations: Presentations, - opts?: { - readonly outcomeCode?: TCode - readonly diagnostics?: readonly CliStructuredError[] - }, - ) => PresentedResult - - /** Management-API credentials, resolved at call time so long-lived - * sessions survive token refresh. Undefined when unauthenticated. - * Commands declaring `requiresCredentials` never see undefined — the - * engine fails them early with the sign-in error. */ - readonly getCredentials: () => Promise - - /** The one way to emit while running (§1). */ - readonly report: (event: EngineEvent) => void - - /** Interactive input (§4a). */ - readonly prompt: PromptSurface - - /** Fires on Ctrl-C/SIGTERM (engine-owned; the engine records which - * signal for the 130/143 exit, and force-exits on a second signal). - * Session commands run until it fires; everything else aborts - * in-flight work with it. */ - readonly signal: AbortSignal - - /** Where the user invoked the CLI. Products never read process.cwd(). */ - readonly cwd: string - - /** R13: probe an optional peer dependency's availability from the - * user's project. Never throws; never installs. Pair with - * `packageManager` to phrase the install command in the structured - * error when it's absent. */ - readonly probeDependency: (specifier: string) => Promise - - /** The user's detected package manager, for install-command phrasing. */ - readonly packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' -} - -export interface Credentials { - /** Opaque to the engine; shape owned by the Cloud product's auth - * library (placeholder pending its design). */ - readonly token: string - readonly workspaceId?: string -} - -/** - * §4a Prompts. Every prompt may carry a product-specified `default`. - * Interactively, Enter accepts the default. Under --yes, a prompt WITH a - * default resolves to it without displaying; a prompt WITHOUT a default - * cannot be operated and the invocation halts with a structured error - * (exit 2). Destructive confirmations therefore simply declare no - * default — --yes can never blast through them; they require their - * explicit flag (--force / --confirm ) per the confirmation rule. - * In json/non-interactive/CI/non-TTY contexts the same default rule - * applies as under --yes. User cancellation (Ctrl-C at the prompt) is a - * distinct structured error the engine maps to exit 3. - */ -export interface PromptSurface { - readonly confirm: ( - question: string, - opts?: { readonly default?: boolean }, - ) => Promise> - /** - * A question requiring EXPLICIT consent — not necessarily destructive, - * but never inferable. Structurally undefaultable: no default - * parameter exists, so --yes, Enter-through, and non-interactive - * contexts can never satisfy it; without a TTY it returns the - * interaction-required structured error, and the command's fix names - * the explicit flag that grants consent non-interactively. - */ - readonly consent: (question: string) => Promise> - readonly select: ( - question: string, - options: ReadonlyArray<{ value: T; label: string }>, - opts?: { readonly default?: T }, - ) => Promise> - readonly text: ( - question: string, - opts?: { readonly placeholder?: string; readonly default?: string }, - ) => Promise> -} - -// ———————————————————————————————————————————————————————————————————————— -// §5 Flags and positionals — R1: directly executable, typed by inference -// ———————————————————————————————————————————————————————————————————————— - -/** - * Product-declared flags. The shared family (§header) is engine-injected - * and reserved; handlers never see those values. Parse-time validation - * failures become structured errors carrying the allowed values — never - * framework strings. - * - * Deliberate asymmetry, matching CLI convention: flags are optional by - * default (requiredString is the exception); positionals are required by - * default (optionalString is the exception). - */ -export declare const flag: { - string(spec: { - brief: string - placeholder?: string - alias?: A & Char - default?: string - }): FlagSpec - requiredString(spec: { - brief: string - placeholder?: string - alias?: A & Char - }): FlagSpec - number(spec: { - brief: string - placeholder?: string - alias?: A & Char - default?: number - }): FlagSpec - boolean(spec: { brief: string; alias?: A & Char }): FlagSpec - enum(spec: { - brief: string - values: T - alias?: A & Char - default?: T[number] - }): FlagSpec - repeated(spec: { - brief: string - placeholder?: string - alias?: A & Char - }): FlagSpec -} - -/** Single-character alias, enforced at the type level: `Char<'q'>` is - * 'q'; `Char<'ab'>` is never. Builder methods take a const generic so - * `alias: 'ab'` is a compile error at the declaration site. */ -export type Char = S extends `${string}${infer Rest}` - ? Rest extends '' - ? S - : never - : never - -declare const FLAG: unique symbol -export interface FlagSpec { - /** Phantom carrier for inference; exported so declaration emit works. */ - readonly [FLAG]: T -} -export { FLAG } - -export declare const positional: { - string(spec: { brief: string; placeholder: string }): PositionalSpec - optionalString(spec: { brief: string; placeholder: string }): PositionalSpec - /** Zero or more trailing values; at most one, declared last (order = - * declaration order; keys must not be integer-like). */ - variadic(spec: { brief: string; placeholder: string }): PositionalSpec -} -declare const POSITIONAL: unique symbol -export interface PositionalSpec { - readonly [POSITIONAL]: T -} -export { POSITIONAL } - -/** What a handler receives: separate namespaces, symmetric access — - * `args.flags.to`, `args.positionals.name`. */ -export interface Args< - TFlags extends Record>, - TPositionals extends Record>, -> { - readonly flags: { readonly [K in keyof TFlags]: TFlags[K] extends FlagSpec ? T : never } - readonly positionals: { - readonly [K in keyof TPositionals]: TPositionals[K] extends PositionalSpec ? T : never - } -} - -// ———————————————————————————————————————————————————————————————————————— -// §6 Command definitions — light at startup (R9), path-free (R12), -// runtime-discriminated by `kind` (stamped by the define* functions) -// ———————————————————————————————————————————————————————————————————————— - -export interface CommandDefinition< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, - TCode extends number = never, -> { - readonly kind: 'command' - /** One line, imperative, shown in listings. */ - readonly brief: string - /** Paragraph(s) for --help. Words only — the engine formats. */ - readonly description?: string - /** Copy-pastable invocations, shown verbatim in help. */ - readonly examples?: readonly string[] - - readonly flags?: TFlags - readonly positionals?: TPositionals - - /** Binds the command to its product's config section (§3). */ - readonly configSection?: ConfigSection - - /** Fail early with the sign-in error when unauthenticated; the handler - * then always receives credentials. */ - readonly requiresCredentials?: boolean - - /** - * The command's documented outcome codes (4–99): code → meaning. - * Rendered in help without executing anything; the catalogue's keys - * type ctx.present's outcomeCode, so a code outside the catalogue is - * a compile error at the return site. Absent = the command only exits - * 0/1/2/3. - */ - readonly outcomeCodes?: Readonly> - - /** The heavy part, loaded only at execution (R9). The module's default - * export is the handler — annotate it with CommandHandler - * (type-only import of the light definition; no runtime cycle). */ - readonly handler: () => Promise<{ default: Handler }> -} - -export type Handler< - TFlags extends Record>, - TPositionals extends Record>, - TConfig, - TCode extends number = never, -> = ( - args: Args, - ctx: CommandContext, -) => Promise, CliStructuredError>> - -/** For impl files: `const run: CommandHandler = …` */ -export type CommandHandler = D extends CommandDefinition - ? Handler - : never - -export declare function defineCommand< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, - TCode extends number = never, ->( - def: Omit, 'kind'>, -): CommandDefinition - -/** - * Mode 4 — sessions (dev, log tail): the handler runs until the signal - * fires, speaks entirely through events, and returns Result. No - * presentation (the engine owns the close-out line), no outcome codes. - * A session always supports json mode: the event stream is its json - * surface. - */ -export interface SessionCommandDefinition< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, -> { - readonly kind: 'session' - readonly brief: string - readonly description?: string - readonly examples?: readonly string[] - readonly flags?: TFlags - readonly positionals?: TPositionals - readonly configSection?: ConfigSection - readonly requiresCredentials?: boolean - readonly handler: () => Promise<{ - default: ( - args: Args, - ctx: CommandContext, - ) => Promise> - }> -} - -export declare function defineSessionCommand< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, ->( - def: Omit, 'kind'>, -): SessionCommandDefinition - -/** - * Mode 7 — stdio protocol servers (lsp): the command owns stdin/stdout - * wholesale. Events, presentation, formats, and prompts do not apply; - * the handler returns the exit code directly. Flags and config are - * allowed; the shared flag family is NOT injected. - */ -export interface RawCommandDefinition< - TFlags extends Record> = {}, - TConfig = undefined, -> { - readonly kind: 'raw' - readonly brief: string - readonly description?: string - readonly flags?: TFlags - readonly configSection?: ConfigSection - readonly handler: () => Promise<{ - default: ( - args: Args, - io: { - readonly stdin: InputStream - readonly stdout: OutputStream - readonly stderr: OutputStream - readonly signal: AbortSignal - readonly cwd: string - readonly config: TConfig | undefined - }, - ) => Promise - }> -} - -export declare function defineRawCommand< - TFlags extends Record> = {}, - TConfig = undefined, ->( - def: Omit, 'kind'>, -): RawCommandDefinition - -/** Erased union for mount maps; `kind` is the runtime discriminant. */ -export type AnyCommand = - | CommandDefinition - | SessionCommandDefinition - | RawCommandDefinition - -// ———————————————————————————————————————————————————————————————————————— -// §7 Presentation primitives — the R5 vocabulary -// ———————————————————————————————————————————————————————————————————————— - -/** Deliberately small; grows by the same evidence rule as events. */ -export type Block = - | { - readonly kind: 'summary' - readonly tone: 'ok' | Exclude - readonly text: string - } - | { - readonly kind: 'fields' - readonly rows: ReadonlyArray<{ label: string; value: string; sensitive?: boolean }> - } - | { - readonly kind: 'table' - readonly columns: readonly string[] - readonly rows: ReadonlyArray - } - | { readonly kind: 'list'; readonly items: readonly string[] } - /** The corpus's most-rendered human structure (migration graphs, - * service trees). */ - | { readonly kind: 'tree'; readonly roots: readonly TreeNode[] } -// NOTE: structured findings inside a completed result are NOT a Block — -// they are declared once at ctx.present (diagnostics) and the engine -// renders them with the top-level error layout and serializes them into -// the envelope, so the two surfaces cannot diverge. - -export interface TreeNode { - readonly label: string - readonly children?: readonly TreeNode[] -} - -/** Styling helpers usable inside block text; no direct writing. */ -export interface Ui { - readonly emphasize: (text: string) => string - readonly dim: (text: string) => string - readonly code: (text: string) => string -} - -// ———————————————————————————————————————————————————————————————————————— -// §8 Streams — minimal structural types; no NodeJS.* in the public -// surface (runtime-agnosticism, R4's why) -// ———————————————————————————————————————————————————————————————————————— - -export interface OutputStream { - write(text: string): void -} -/** Byte-oriented, so raw commands can implement byte-counted protocols - * (lsp's Content-Length framing). Decoding is the consumer's business; - * the engine's own prompt machinery decodes internally. setRawMode is - * present where the platform supports keypress-driven input. */ -export interface InputStream extends AsyncIterable { - readonly setRawMode?: (enabled: boolean) => void -} - -// ———————————————————————————————————————————————————————————————————————— -// §9 Envelopes and frames — the json contract -// ———————————————————————————————————————————————————————————————————————— - -export interface CompletedEnvelope { - /** ok = COMPLETED (the command executed to its end). A completed - * result may still carry a non-zero outcome code — bad news is a - * result, not an error. */ - readonly ok: true - /** Stable dotted command id derived from the mount path ('db.migrate'). */ - readonly command: string - /** The presented data (json presentation override when supplied). */ - readonly result: T - /** From the outcome catalogue; 0 when absent. */ - readonly outcomeCode: number - /** The completed result's structured findings, serialized as error - * envelopes (dotted codes intact for machine consumers) — aggregated - * by the engine from PresentedResult.diagnostics. */ - readonly diagnostics: readonly unknown[] - /** Aggregated from severity-'warn' message events. */ - readonly warnings: readonly string[] - /** Derived from nextActions — the human-string form. */ - readonly nextSteps: readonly string[] - readonly nextActions: readonly NextAction[] -} - -export interface ErroredEnvelope { - /** ok = false: the command did NOT complete. */ - readonly ok: false - readonly command: string - /** The CliErrorEnvelope fields (code, severity, summary, why, fix, - * where, meta, docsUrl) — nested, per the settled envelope rule. The - * PRIMARY error: what aborted the command. */ - readonly error: unknown - /** Accompanying structured problems when the abort had several (three - * config typos are three diagnostics, not one flattened error) — - * symmetric with CompletedEnvelope.diagnostics. */ - readonly diagnostics: readonly unknown[] - readonly warnings: readonly string[] - /** Aggregated from remediation events + derived from the error's fix. */ - readonly nextSteps: readonly string[] - readonly nextActions: readonly NextAction[] -} - -/** json mode emits one frame per line: events while running, then - * exactly one result frame. */ -export type Frame = EventFrame | ResultFrame - -export interface EventFrame { - readonly type: 'event' - readonly command: string - /** ISO 8601 UTC. Injectable clock in tests (§11). */ - readonly timestamp: string - readonly event: EngineEvent -} - -export interface ResultFrame { - readonly type: 'result' - readonly command: string - readonly timestamp: string - readonly envelope: CompletedEnvelope | ErroredEnvelope -} - -// ———————————————————————————————————————————————————————————————————————— -// §10 Product export and shell mounting — R12: the shell owns the tree -// ———————————————————————————————————————————————————————————————————————— - -/** What a product package exports: commands by NAME. */ -export type CommandSet = Readonly> - -/** What the shell builds: commands by PATH (space-separated, - * 'db migrate'). Distinct alias so the two maps never read as one. */ -export type MountedTree = Readonly> - -/** - * Shell-side construction. Group help is declared with the mount, since - * groups belong to the tree, not to products. Collisions, unknown - * groups, reserved-flag violations, and grammar violations fail - * construction (build time, not run time). - */ -export declare function createCli(spec: { - readonly name: string - readonly version: string - readonly groups: Readonly> - readonly commands: MountedTree -}): Cli - -export interface Cli { - /** Parse, execute, render, return the exit code. Never calls - * process.exit; never touches streams other than the provided ones. */ - run(argv: readonly string[], runtime: Runtime): Promise -} - -/** Everything environmental, injected once by the bin (or by a test). */ -export interface Runtime { - readonly stdout: OutputStream - readonly stderr: OutputStream - readonly stdin: InputStream - readonly cwd: string - readonly env: Readonly> - readonly isTty: { readonly stdin: boolean; readonly stdout: boolean; readonly stderr: boolean } - readonly signal: AbortSignal - /** Loaded config + per-section diagnostics; the shell builds this via - * the unified loader (R10). Tests hand in fixtures. */ - readonly config: LoadedConfig - readonly getCredentials: () => Promise - readonly packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' -} - -export interface LoadedConfig { - /** Raw section values by name; validation happens per command via its - * ConfigSection token. */ - readonly sections: Readonly> - /** File-level problems (unevaluable module, missing version marker) — - * section: null fails every command. */ - readonly diagnostics: ReadonlyArray<{ - readonly section: string | null - readonly error: CliStructuredError - }> -} - -// ———————————————————————————————————————————————————————————————————————— -// §11 The product-repo test harness — R7: same machinery, bytes out -// ———————————————————————————————————————————————————————————————————————— - -export declare function createTestCli(spec: { - readonly commands: MountedTree - readonly groups?: Readonly> - readonly config?: Readonly> - readonly credentials?: Credentials - readonly packageManager?: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' - /** Fixed clock for deterministic frame timestamps. */ - readonly now?: () => Date -}): TestCli - -export interface TestCli { - run( - argv: readonly string[], - opts?: { - readonly stdin?: string - /** Scripted prompt answers, consumed in order; a run that prompts - * past the script fails the test. */ - readonly answers?: ReadonlyArray - /** Abort the run (session tests): fires the context signal. */ - readonly abort?: AbortSignal - /** Live event tap, for asserting mid-session behavior before - * aborting. */ - readonly onEvent?: (event: EngineEvent) => void - readonly cwd?: string - readonly isTty?: { stdin?: boolean; stdout?: boolean; stderr?: boolean } - readonly env?: Readonly> - }, - ): Promise<{ - readonly exitCode: number - readonly stdout: string - readonly stderr: string - /** Parsed frames (events + the result frame) when json mode was on. */ - readonly json: readonly Frame[] - /** Every EngineEvent the handler emitted, for semantic assertions. */ - readonly events: readonly EngineEvent[] - /** The PresentedResult the handler returned (data + materialized - * presentation), for semantic assertions without byte-scraping. */ - readonly presented?: PresentedResult - }> -} diff --git a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v6.ts b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v6.ts deleted file mode 100644 index 20fb90bf..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v6.ts +++ /dev/null @@ -1,852 +0,0 @@ -/** - * DRAFT v6 — the unified CLI engine's public interface. - * v1 initial · v2 round-1 fixes · v3 return-site presentation · - * v4 completed/errored, --format, log levels, prompt defaults · - * v5 round-3 closure · v6 operator line review: Diagnostic as a pure - * data shape (findings are not thrown errors), warnings folded into - * diagnostics, nextSteps deleted, commandId, flattened StreamEvent, - * exitCode naming restored, result/session/server command kinds. - * Prior versions preserved as -v1…-v5.ts; review artifacts in ./reviews/. - * - * THE MODEL, in one analogy (operator, 2026-08-09): commands settle like - * promises. A command can COMPLETE — and its completion can be - * successful or unsuccessful, both presented through the same machinery, - * distinguished by exit code and diagnostics — or it can ERROR, which - * aborts out of the normal process and gets its own special handling. - * - * Implementation prerequisite: ADR 239 (prisma/prisma) is amended so - * completed-but-unsuccessful command results (verify/check/runner - * findings) are carried as diagnostics with their dotted codes inside a - * completed envelope with a documented exit code — not as structured - * failures with exit 2, as it classifies them today. The amendment also - * checks whether any shipped error uses severity 'info'; if none does, - * the severity scale of CliStructuredError and Diagnostic trims to - * error|warn — together, so the two shapes stay identical. - * - * Everything a product package imports for CLI purposes lives here (R3). - * Requirement references (R1–R14) point at docs/architecture/ - * cli-engine-requirements.md in prisma-cli. Nothing from stricli appears — - * it is an internal of the engine package. - * - * EXECUTION PROTOCOL. A handler receives (args, context), emits zero or - * more events through context.report, and finishes one of two ways: - * - * COMPLETED — it returns ok(ctx.present(data, presentations, opts)): - * the command executed to its end and has a result. A completed result - * may still be bad news; it carries diagnostics (recorded findings — - * data, not thrown errors) and an exit code from the command's - * documented set (`migration check` completes, presents its findings - * like any result, and exits 4). Presentation always runs for - * completed results. - * - * ERRORED — it returns notOk(structuredError): the command did not - * complete. The engine renders the error envelope; there is no product - * presentation on the error path. The primary error of an errored - * command is severity 'error' by definition — a warning cannot abort a - * command. `remediation` events emitted before the error are - * aggregated into the errored envelope's nextActions. - * - * Session commands run until context.signal fires, then clean up and - * return. Server commands hand the stdio conversation to a foreign - * client. Liveness display is the engine's (shown when a command runs - * quietly past a threshold). Nothing product-authored executes after the - * handler resolves — the engine receives values, never callbacks. - * - * FORMATS AND LEVELS. `--format `, auto-selected when - * unspecified (human on a TTY stdout, json otherwise — deliberate - * agent-facing behavior); `--json` is shorthand for `--format json`. In - * json mode the engine suppresses prompts (they fail structurally) and - * emits one StreamEvent per line. Commentary is filtered by `--log-level - * ` (default info); `--verbose` is shorthand - * for `--log-level verbose`. The engine injects the shared flag family - * on every non-server command: --format/--json, --log-level/-v/ - * --verbose, -q/--quiet, -y/--yes, --interactive/--no-interactive, - * --color/--no-color. Products cannot declare flags with those names. - * Product flag keys are camelCase and transliterate to --kebab-case. - * - * EXIT CODES (R6): 0 completed; 1 bug only; 2 errored (expected, - * structured); 3 user abort (Ctrl-C, or cancelling a prompt the command - * cannot proceed without); 4–99 documented per command in `exitCodes`; - * 130/143 delivered signals. The engine owns signal wiring: first signal - * fires context.signal and awaits handler teardown; a second signal - * exits immediately with the signal's code. - */ - -// ———————————————————————————————————————————————————————————————————————— -// Foundation types (from the zero-dependency foundation package — which -// owns CliStructuredError, Result, NextAction, and Diagnostic, so the -// engine and both repos share them without cycles). Shown for reading -// convenience. -// ———————————————————————————————————————————————————————————————————————— - -import type { CliStructuredError, Diagnostic, NextAction, Result } from '@prisma/cli-foundation' - -/* - * For reference — the foundation shapes this file leans on: - * - * Diagnostic — a recorded finding: pure data, never thrown, no stack. - * { code: 'NAMESPACE.SUBCODE', severity: 'error' | 'warn' | 'info', - * summary, why?, fix?, where?, meta?, docsUrl? } - * Field-for-field the settled error envelope (ADR 239) minus `ok` — - * identical scales included; the two shapes never diverge. - * CliStructuredError.toEnvelope() yields exactly this shape, so a - * thrown error and a recorded finding share one wire form. Aggregate - * operations (db verify, migration check, config validation) COLLECT - * Diagnostics; they do not construct Error instances per finding. - * - * NextAction — the typed agent-facing follow-up (platform-shipped form): - * { kind: 'run-command' | 'user-choice' | 'edit-file' | 'done', - * journey, label, command?, commands?, reason? } - */ - -/** The commentary severity scale; also the log-level axis. Distinct from - * Diagnostic severity (error|warn): 'info' and 'verbose' grade - * commentary, which never enters the envelope. Step outcomes are - * completion states, not severities. */ -export type Severity = 'error' | 'warn' | 'info' | 'verbose' -export type LogLevel = Severity - -export type Format = 'human' | 'json' - -// ———————————————————————————————————————————————————————————————————————— -// §1 Events — R14: one engine vocabulary, product extensions in `data` -// ———————————————————————————————————————————————————————————————————————— - -/** - * The engine event envelope. `kind`-specific fields are the common - * vocabulary the engine renders consistently (human mode) and streams - * (json mode, §9). `data` is the product extension: passed through to - * machine consumers untouched, never interpreted by the engine, - * documented and versioned by the product as its own public API. A - * structure recurring inside `data` across commands is the promotion - * signal (R14). - * - * Rendering, human mode: `output` events with channel 'data' are the - * command's data and go to OUR stdout (what `log tail > file` captures); - * everything else is commentary on stderr, filtered by the active log - * level (`message` events by their severity; other kinds display at - * info). Events are transcript: they are NOT aggregated into the - * envelope (the one exception is `remediation` → nextActions). Findings - * that belong in the envelope are diagnostics on the presented result, - * not events. - * - * report() is synchronous fire-and-forget; the engine buffers and writes - * asynchronously (no backpressure signal — accepted trade). Calling it - * after the handler has resolved is a bug (InternalError). Events during - * teardown (after the signal, before resolution) are normal. - */ -export type EngineEvent = - /** A named phase began. `id`/`parentId` express nesting; omitted for - * flat steps. */ - | { - readonly kind: 'step-started' - readonly step: string - readonly id?: string - readonly parentId?: string - readonly data?: unknown - } - /** The phase ended. `outcome` is a completion state and drives the - * ✔/✘/⚠/− glyph; it is not a severity. */ - | { - readonly kind: 'step-finished' - readonly step: string - readonly id?: string - readonly outcome: 'ok' | 'failed' | 'skipped' | 'warning' - readonly data?: unknown - } - /** Progress inside a phase (counts, not percentages). */ - | { - readonly kind: 'progress' - readonly step?: string - readonly completed: number - readonly total?: number - readonly data?: unknown - } - /** A line of commentary at a severity; display-filtered by log level. - * Transcript only — never enters the envelope. 'error' is not valid - * here: fatal problems are the Result's error; envelope-worthy - * findings are diagnostics. */ - | { - readonly kind: 'message' - readonly severity: Exclude - readonly text: string - readonly data?: unknown - } - /** Line-oriented output from a child process or remote stream. - * `channel` is semantic: 'data' = the command's own output (our - * stdout); 'diagnostic' = commentary about the run (stderr). `source` - * names the emitter, not a pipe. */ - | { - readonly kind: 'output' - readonly source: string - readonly channel: 'data' | 'diagnostic' - readonly line: string - readonly data?: unknown - } - /** A user-actionable follow-up surfaced mid-run. Aggregated into the - * final envelope's nextActions (completed OR errored). */ - | { readonly kind: 'remediation'; readonly action: NextAction; readonly data?: unknown } - /** A reachable endpoint became available. */ - | { - readonly kind: 'endpoint' - readonly name: string - readonly url: string - readonly data?: unknown - } - /** A state transition in a watched external process; `from` carries - * the prior state when known. */ - | { - readonly kind: 'status' - readonly subject: string - readonly status: string - readonly from?: string - readonly data?: unknown - } - /** A file or directory this run wrote that the user may care about. */ - | { - readonly kind: 'artifact' - readonly path: string - readonly description?: string - readonly data?: unknown - } - -// ———————————————————————————————————————————————————————————————————————— -// §2 Presented results — presentation materializes at the return site -// ———————————————————————————————————————————————————————————————————————— - -declare const PRESENTED: unique symbol - -/** - * What a completed command's handler returns inside `ok(...)`: pure data - * plus the presentation the ACTIVE FORMAT already materialized. Built - * exclusively by ctx.present (the brand makes hand-construction a type - * error) — the context knows the format, calls only the presentation - * functions it needs, and the value crossing the product→engine boundary - * is data all the way down. - * - * `data` is always present and is what the envelope's `result` - * serializes (json presentation overrides when supplied). Materialization - * by format: human → human + stdout + next; human+--quiet → stdout; - * json → json + next. - */ -export interface PresentedResult { - readonly [PRESENTED]: true - readonly data: T - /** The exit code selected at the return site; typed against the - * definition's documented `exitCodes`. Omitted = 0. */ - readonly exitCode?: number - /** - * The completed result's recorded findings (drift, verify failures) — - * Diagnostics: data, never thrown. Declared ONCE here; the engine - * renders them in human mode with the same layout as top-level errors - * (shown even under --quiet) AND carries them verbatim into - * CompletedEnvelope.diagnostics. One declaration, both surfaces, - * impossible to diverge. Guardrail (runtime, at the return site): any - * severity-'error' entry requires a non-zero exitCode — a genuine - * could-not-complete belongs in notOk. The test: notOk when the - * command couldn't do its job; diagnostics when finding these WAS the - * job. - */ - readonly diagnostics?: readonly Diagnostic[] - readonly presentation: { - readonly human?: readonly Block[] - readonly stdout?: readonly string[] - readonly json?: unknown - readonly next?: readonly NextAction[] - } -} -export { PRESENTED } - -/** - * The per-format presentation functions a handler supplies to - * ctx.present. Only the active format's functions are invoked, at the - * return site, where the outcome and its context are live. `human` - * composes engine primitives (R5: Block is the only vocabulary); - * `stdout` is the machine-consumable data lines the engine writes to - * stdout — what --quiet leaves, what a pipe receives; `json` overrides - * the envelope's `result` (default: the data itself); `next` supplies - * the typed nextActions — agents branch on them; the human renderer - * formats them as prose (no stored string form). - */ -export interface Presentations { - readonly human: (ui: Ui) => readonly Block[] - readonly stdout?: () => readonly string[] - readonly json?: () => unknown - readonly next?: () => readonly NextAction[] -} - -// ———————————————————————————————————————————————————————————————————————— -// §3 Config sections — R10 made structural -// ———————————————————————————————————————————————————————————————————————— - -/** - * A product's named slice of prisma.config.ts. The token couples the - * section name, its validated type, and its total validator — which - * RETURNS findings (Diagnostics); it never throws (R10). Commands bind - * to the token, which is how the engine knows which section a command - * needs — and therefore which diagnostics fail which commands. Keep - * validators dependency-light: they load with the definition tree at - * startup (R9). - */ -export interface ConfigSection { - readonly name: string - readonly validate: (raw: unknown) => SectionValidation -} - -export type SectionValidation = - | { readonly ok: true; readonly value: T; readonly diagnostics: readonly Diagnostic[] } - | { readonly ok: false; readonly diagnostics: readonly Diagnostic[] } - -export declare function defineConfigSection(spec: { - readonly name: string - readonly validate: (raw: unknown) => SectionValidation -}): ConfigSection - -// ———————————————————————————————————————————————————————————————————————— -// §4 The handler context — R4: the whole world arrives as one argument -// ———————————————————————————————————————————————————————————————————————— - -export interface CommandContext { - /** The validated value of the command's declared config section, or - * undefined when the config file has no such section. An INVALID - * needed section never reaches the handler — the engine already - * failed the command with that section's diagnostics. */ - readonly config: TConfig | undefined - - /** Builds the PresentedResult for the active format: calls only the - * presentation functions this format needs, at the return site. The - * only constructor of PresentedResult. `exitCode` is typed against - * the definition's documented `exitCodes` — a code outside them is a - * compile error at the call site. `diagnostics` are the completed - * result's recorded findings (see PresentedResult). */ - readonly present: ( - data: T, - presentations: Presentations, - opts?: { - readonly exitCode?: TCode - readonly diagnostics?: readonly Diagnostic[] - }, - ) => PresentedResult - - /** Management-API credentials, resolved at call time so long-lived - * sessions survive token refresh. Undefined when unauthenticated. - * Commands declaring `requiresCredentials` never see undefined — the - * engine fails them early with the sign-in error. */ - readonly getCredentials: () => Promise - - /** The one way to emit while running (§1). */ - readonly report: (event: EngineEvent) => void - - /** Interactive input (§4a). */ - readonly prompt: PromptSurface - - /** Fires on Ctrl-C/SIGTERM (engine-owned; the engine records which - * signal for the 130/143 exit, and force-exits on a second signal). - * Session commands run until it fires; everything else aborts - * in-flight work with it. */ - readonly signal: AbortSignal - - /** Where the user invoked the CLI. Products never read process.cwd(). */ - readonly cwd: string - - /** R13: probe an optional peer dependency's availability from the - * user's project. Never throws; never installs. Pair with - * `packageManager` to phrase the install command in the structured - * error when it's absent. */ - readonly probeDependency: (specifier: string) => Promise - - /** The user's detected package manager, for install-command phrasing. */ - readonly packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' -} - -export interface Credentials { - /** Opaque to the engine; shape owned by the Cloud product's auth - * library (placeholder pending its design). */ - readonly token: string - readonly workspaceId?: string -} - -/** - * §4a Prompts. Every prompt except `consent` may carry a - * product-specified `default`. Interactively, Enter accepts the default. - * Under --yes, a prompt WITH a default resolves to it without - * displaying; a prompt WITHOUT a default cannot be operated and the - * invocation halts with a structured error (exit 2). In - * json/non-interactive/CI/non-TTY contexts the same default rule - * applies as under --yes. User cancellation (Ctrl-C at the prompt) is a - * distinct structured error the engine maps to exit 3. - */ -export interface PromptSurface { - readonly confirm: ( - question: string, - opts?: { readonly default?: boolean }, - ) => Promise> - /** - * A question requiring EXPLICIT consent — not necessarily destructive, - * but never inferable. Structurally undefaultable: no default - * parameter exists, so --yes, Enter-through, and non-interactive - * contexts can never satisfy it; without a TTY it returns the - * interaction-required structured error, and the command's fix names - * the explicit flag that grants consent non-interactively. - */ - readonly consent: (question: string) => Promise> - readonly select: ( - question: string, - options: ReadonlyArray<{ value: T; label: string }>, - opts?: { readonly default?: T }, - ) => Promise> - readonly text: ( - question: string, - opts?: { readonly placeholder?: string; readonly default?: string }, - ) => Promise> -} - -// ———————————————————————————————————————————————————————————————————————— -// §5 Flags and positionals — R1: directly executable, typed by inference -// ———————————————————————————————————————————————————————————————————————— - -/** - * Product-declared flags. The shared family (§header) is engine-injected - * and reserved; handlers never see those values. Parse-time validation - * failures become structured errors carrying the allowed values — never - * framework strings. - * - * Deliberate asymmetry, matching CLI convention: flags are optional by - * default (requiredString is the exception); positionals are required by - * default (optionalString is the exception). - */ -export declare const flag: { - string(spec: { - brief: string - placeholder?: string - alias?: A & Char - default?: string - }): FlagSpec - requiredString(spec: { - brief: string - placeholder?: string - alias?: A & Char - }): FlagSpec - number(spec: { - brief: string - placeholder?: string - alias?: A & Char - default?: number - }): FlagSpec - boolean(spec: { brief: string; alias?: A & Char }): FlagSpec - enum(spec: { - brief: string - values: T - alias?: A & Char - default?: T[number] - }): FlagSpec - repeated(spec: { - brief: string - placeholder?: string - alias?: A & Char - }): FlagSpec -} - -/** Single-character alias, enforced at the type level: `Char<'q'>` is - * 'q'; `Char<'ab'>` is never. */ -export type Char = S extends `${string}${infer Rest}` - ? Rest extends '' - ? S - : never - : never - -declare const FLAG: unique symbol -export interface FlagSpec { - /** Phantom carrier for inference; exported so declaration emit works. */ - readonly [FLAG]: T -} -export { FLAG } - -export declare const positional: { - string(spec: { brief: string; placeholder: string }): PositionalSpec - optionalString(spec: { brief: string; placeholder: string }): PositionalSpec - /** Zero or more trailing values; at most one, declared last (order = - * declaration order; keys must not be integer-like). */ - variadic(spec: { brief: string; placeholder: string }): PositionalSpec -} -declare const POSITIONAL: unique symbol -export interface PositionalSpec { - readonly [POSITIONAL]: T -} -export { POSITIONAL } - -/** What a handler receives: separate namespaces, symmetric access — - * `args.flags.to`, `args.positionals.name`. */ -export interface Args< - TFlags extends Record>, - TPositionals extends Record>, -> { - readonly flags: { readonly [K in keyof TFlags]: TFlags[K] extends FlagSpec ? T : never } - readonly positionals: { - readonly [K in keyof TPositionals]: TPositionals[K] extends PositionalSpec ? T : never - } -} - -// ———————————————————————————————————————————————————————————————————————— -// §6 Command definitions — light at startup (R9), path-free (R12), -// runtime-discriminated by `kind` (stamped by the define* functions). -// Three modalities at equal rank: a result command runs to completion -// and presents; a session command runs until told to stop, speaking -// through events; a server command hands the stdio conversation to a -// foreign client. -// ———————————————————————————————————————————————————————————————————————— - -export interface CommandDefinition< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, - TCode extends number = never, -> { - readonly kind: 'result-command' - /** One line, imperative, shown in listings. */ - readonly brief: string - /** Paragraph(s) for --help. Words only — the engine formats. */ - readonly description?: string - /** Copy-pastable invocations, shown verbatim in help. */ - readonly examples?: readonly string[] - - readonly flags?: TFlags - readonly positionals?: TPositionals - - /** Binds the command to its product's config section (§3). */ - readonly configSection?: ConfigSection - - /** Fail early with the sign-in error when unauthenticated; the handler - * then always receives credentials. */ - readonly requiresCredentials?: boolean - - /** - * The command's documented exit codes (4–99): code → meaning. - * Rendered in help without executing anything; the keys type - * ctx.present's exitCode, so a code outside them is a compile error - * at the return site. Absent = the command only exits 0/1/2/3. - */ - readonly exitCodes?: Readonly> - - /** The heavy part, loaded only at execution (R9). The module's default - * export is the handler — annotate it with CommandHandler - * (type-only import of the light definition; no runtime cycle). */ - readonly handler: () => Promise<{ default: Handler }> -} - -export type Handler< - TFlags extends Record>, - TPositionals extends Record>, - TConfig, - TCode extends number = never, -> = ( - args: Args, - ctx: CommandContext, -) => Promise, CliStructuredError>> - -/** For impl files: `const run: CommandHandler = …` */ -export type CommandHandler = D extends CommandDefinition - ? Handler - : never - -export declare function defineCommand< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, - TCode extends number = never, ->( - def: Omit, 'kind'>, -): CommandDefinition - -/** - * A session command (dev, log tail): the handler runs until the signal - * fires, speaks entirely through events, and returns Result. No - * presentation (the engine owns the close-out line), no exit-code set. - * A session always supports json mode: the event stream is its json - * surface. - */ -export interface SessionCommandDefinition< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, -> { - readonly kind: 'session-command' - readonly brief: string - readonly description?: string - readonly examples?: readonly string[] - readonly flags?: TFlags - readonly positionals?: TPositionals - readonly configSection?: ConfigSection - readonly requiresCredentials?: boolean - readonly handler: () => Promise<{ - default: ( - args: Args, - ctx: CommandContext, - ) => Promise> - }> -} - -export declare function defineSessionCommand< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, ->( - def: Omit, 'kind'>, -): SessionCommandDefinition - -/** - * A server command (lsp): a foreign client on the other end of stdio - * owns the conversation, so the engine hands over the streams. Events, - * presentation, formats, and prompts do not apply — by definition, not - * by opt-out; the handler returns the exit code directly. Flags and - * config are allowed; the shared flag family is NOT injected. - */ -export interface ServerCommandDefinition< - TFlags extends Record> = {}, - TConfig = undefined, -> { - readonly kind: 'server-command' - readonly brief: string - readonly description?: string - readonly flags?: TFlags - readonly configSection?: ConfigSection - readonly handler: () => Promise<{ - default: ( - args: Args, - io: { - readonly stdin: InputStream - readonly stdout: OutputStream - readonly stderr: OutputStream - readonly signal: AbortSignal - readonly cwd: string - readonly config: TConfig | undefined - }, - ) => Promise - }> -} - -export declare function defineServerCommand< - TFlags extends Record> = {}, - TConfig = undefined, ->( - def: Omit, 'kind'>, -): ServerCommandDefinition - -/** Erased union for mount maps; `kind` is the runtime discriminant. */ -export type AnyCommand = - | CommandDefinition - | SessionCommandDefinition - | ServerCommandDefinition - -// ———————————————————————————————————————————————————————————————————————— -// §7 Presentation primitives — the R5 vocabulary -// ———————————————————————————————————————————————————————————————————————— - -/** Deliberately small; grows by the same evidence rule as events. */ -export type Block = - | { - readonly kind: 'summary' - readonly tone: 'ok' | 'error' | 'warn' | 'info' - readonly text: string - } - | { - readonly kind: 'fields' - readonly rows: ReadonlyArray<{ label: string; value: string; sensitive?: boolean }> - } - | { - readonly kind: 'table' - readonly columns: readonly string[] - readonly rows: ReadonlyArray - } - | { readonly kind: 'list'; readonly items: readonly string[] } - /** The corpus's most-rendered human structure (migration graphs, - * service trees). */ - | { readonly kind: 'tree'; readonly roots: readonly TreeNode[] } -// NOTE: recorded findings are NOT a Block — they are diagnostics on the -// presented result; the engine renders them with the top-level error -// layout and carries them into the envelope, so the two surfaces cannot -// diverge. - -export interface TreeNode { - readonly label: string - readonly children?: readonly TreeNode[] -} - -/** Styling helpers usable inside block text; no direct writing. */ -export interface Ui { - readonly emphasize: (text: string) => string - readonly dim: (text: string) => string - readonly code: (text: string) => string -} - -// ———————————————————————————————————————————————————————————————————————— -// §8 Streams — minimal structural types; no NodeJS.* in the public -// surface (runtime-agnosticism, R4's why) -// ———————————————————————————————————————————————————————————————————————— - -export interface OutputStream { - write(text: string): void -} -/** Byte-oriented, so server commands can implement byte-counted - * protocols (lsp's Content-Length framing). Decoding is the consumer's - * business; the engine's own prompt machinery decodes internally. - * setRawMode is present where the platform supports keypress input. */ -export interface InputStream extends AsyncIterable { - readonly setRawMode?: (enabled: boolean) => void -} - -// ———————————————————————————————————————————————————————————————————————— -// §9 Envelopes and the json stream -// ———————————————————————————————————————————————————————————————————————— - -export interface CompletedEnvelope { - /** ok = COMPLETED (the command executed to its end). A completed - * result may still carry findings and a non-zero exit code — bad news - * is a result, not an error. */ - readonly ok: true - /** The command's stable dotted identity — its full mount path - * ('project.env.add'). The schema-dispatch key for machine consumers; - * says nothing about arguments. */ - readonly commandId: string - /** The presented data (json presentation override when supplied). */ - readonly result: T - /** From the documented set; 0 when unset. */ - readonly exitCode: number - /** The recorded findings, verbatim from the presented result. */ - readonly diagnostics: readonly Diagnostic[] - readonly nextActions: readonly NextAction[] -} - -export interface ErroredEnvelope { - /** ok = false: the command did NOT complete. */ - readonly ok: false - readonly commandId: string - /** The PRIMARY error — what aborted the command. Severity 'error' by - * definition. Shape: Diagnostic (a thrown CliStructuredError - * serializes to exactly this). */ - readonly error: Diagnostic - /** Accompanying findings when the abort had several (three config - * typos are three diagnostics, not one flattened error). */ - readonly diagnostics: readonly Diagnostic[] - /** Aggregated from remediation events. */ - readonly nextActions: readonly NextAction[] -} - -/** - * json mode emits one StreamEvent per line: the handler's events, - * flattened with the stream metadata, then exactly one terminal - * 'result' member carrying the envelope. One union, one discriminant - * (`kind`). - */ -export type StreamEvent = - | (EngineEvent & StreamMeta) - | ({ readonly kind: 'result'; readonly envelope: CompletedEnvelope | ErroredEnvelope } & StreamMeta) - -export interface StreamMeta { - readonly commandId: string - /** ISO 8601 UTC. Injectable clock in tests (§11). */ - readonly timestamp: string -} - -// ———————————————————————————————————————————————————————————————————————— -// §10 Product export and shell mounting — R12: the shell owns the tree -// ———————————————————————————————————————————————————————————————————————— - -/** What a product package exports: commands by NAME. */ -export type CommandSet = Readonly> - -/** What the shell builds: commands by PATH (space-separated, - * 'db migrate'). Distinct alias so the two maps never read as one. */ -export type MountedTree = Readonly> - -/** - * Shell-side construction. Group help is declared with the mount, since - * groups belong to the tree, not to products. Collisions, unknown - * groups, reserved-flag violations, and grammar violations fail - * construction (build time, not run time). - */ -export declare function createCli(spec: { - readonly name: string - readonly version: string - readonly groups: Readonly> - readonly commands: MountedTree -}): Cli - -export interface Cli { - /** Parse, execute, render, return the exit code. Never calls - * process.exit; never touches streams other than the provided ones. */ - run(argv: readonly string[], runtime: Runtime): Promise -} - -/** Everything environmental, injected once by the bin (or by a test). */ -export interface Runtime { - readonly stdout: OutputStream - readonly stderr: OutputStream - readonly stdin: InputStream - readonly cwd: string - readonly env: Readonly> - readonly isTty: { readonly stdin: boolean; readonly stdout: boolean; readonly stderr: boolean } - readonly signal: AbortSignal - /** Loaded config + per-section diagnostics; the shell builds this via - * the unified loader (R10). Tests hand in fixtures. */ - readonly config: LoadedConfig - readonly getCredentials: () => Promise - readonly packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' -} - -export interface LoadedConfig { - /** Raw section values by name; validation happens per command via its - * ConfigSection token. */ - readonly sections: Readonly> - /** File-level problems (unevaluable module, missing version marker) — - * section: null fails every command. */ - readonly diagnostics: ReadonlyArray<{ - readonly section: string | null - readonly diagnostic: Diagnostic - }> -} - -// ———————————————————————————————————————————————————————————————————————— -// §11 The product-repo test harness — R7: same machinery, bytes out -// ———————————————————————————————————————————————————————————————————————— - -export declare function createTestCli(spec: { - readonly commands: MountedTree - readonly groups?: Readonly> - readonly config?: Readonly> - readonly credentials?: Credentials - readonly packageManager?: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' - /** Fixed clock for deterministic stream timestamps. */ - readonly now?: () => Date -}): TestCli - -export interface TestCli { - run( - argv: readonly string[], - opts?: { - readonly stdin?: string - /** Scripted prompt answers, consumed in order; a run that prompts - * past the script fails the test. */ - readonly answers?: ReadonlyArray - /** Abort the run (session tests): fires the context signal. */ - readonly abort?: AbortSignal - /** Live event tap, for asserting mid-session behavior before - * aborting. */ - readonly onEvent?: (event: EngineEvent) => void - readonly cwd?: string - readonly isTty?: { stdin?: boolean; stdout?: boolean; stderr?: boolean } - readonly env?: Readonly> - }, - ): Promise<{ - readonly exitCode: number - readonly stdout: string - readonly stderr: string - /** Parsed stream (events + the terminal result) when json mode. */ - readonly json: readonly StreamEvent[] - /** Every EngineEvent the handler emitted, for semantic assertions. */ - readonly events: readonly EngineEvent[] - /** The PresentedResult the handler returned (data + materialized - * presentation), for semantic assertions without byte-scraping. */ - readonly presented?: PresentedResult - }> -} diff --git a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v7.ts b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v7.ts deleted file mode 100644 index 8abb07df..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft-v7.ts +++ /dev/null @@ -1,870 +0,0 @@ -/** - * DRAFT v7 — the unified CLI engine's public interface. - * v1 initial · v2 round-1 fixes · v3 return-site presentation · - * v4 completed/errored, --format, log levels, prompt defaults · - * v5 round-3 closure · v6 Diagnostic, warnings fold, stream flatten · - * v7 operator line review: outcome as first-class argument (exitCode - * required iff catalogued; diagnostics never undefined), help/args/needs - * grouping, product manifests own config sections, requireDependency, - * validator-owned absence, credentials trimmed, no agent-prohibition - * property. Prior versions preserved as -v1…-v6.ts; reviews in - * ./reviews/. - * - * THE MODEL, in one analogy (operator, 2026-08-09): commands settle like - * promises. A command can COMPLETE — and its completion can be - * successful or unsuccessful, both presented through the same machinery, - * distinguished by exit code and diagnostics — or it can ERROR, which - * aborts out of the normal process and gets its own special handling. - * - * Implementation prerequisite: ADR 239 (prisma/prisma) is amended so - * completed-but-unsuccessful command results (verify/check/runner - * findings) are carried as diagnostics with their dotted codes inside a - * completed envelope with a documented exit code — not as structured - * failures with exit 2, as it classifies them today. The amendment also - * checks whether any shipped error uses severity 'info'; if none does, - * the severity scale of CliStructuredError and Diagnostic trims to - * error|warn — together, so the two shapes stay identical. - * - * Everything a product package imports for CLI purposes lives here (R3). - * Requirement references (R1–R14) point at docs/architecture/ - * cli-engine-requirements.md in prisma-cli. Nothing from stricli appears — - * it is an internal of the engine package. - * - * EXECUTION PROTOCOL. A handler receives (args, context), emits zero or - * more events through context.report, and finishes one of two ways: - * - * COMPLETED — it returns ok(ctx.present(outcome, presentations)): the - * command executed to its end. The outcome is what it concluded — - * data, diagnostics (recorded findings — data, never thrown), and, - * when the command documents exit codes, an explicit code at every - * return site. Presentation always runs for completed results. - * - * ERRORED — it returns notOk(structuredError): the command did not - * complete. The engine renders the error envelope; there is no product - * presentation on the error path. The primary error is severity - * 'error' by definition. `remediation` events emitted before the - * error are aggregated into the errored envelope's nextActions. - * - * PRECONDITIONS (a command's `needs`) are enforced by the engine BEFORE - * the handler loads: an invalid needed config section, missing - * credentials, an absent optional dependency, or a non-interactive - * context each fail the command early with the engine's own structured - * error — a handler only ever runs in a world where it can operate. - * - * Session commands run until context.signal fires, then clean up and - * return. Server commands hand the stdio conversation to a foreign - * client. Liveness display is the engine's. Nothing product-authored - * executes after the handler resolves. - * - * FORMATS AND LEVELS. `--format `, auto-selected when - * unspecified (human on a TTY stdout, json otherwise); `--json` is - * shorthand for `--format json`. In json mode the engine suppresses - * prompts (they fail structurally) and emits one StreamEvent per line. - * Commentary is filtered by `--log-level ` - * (default info); `--verbose` is shorthand for `--log-level verbose`. - * The engine injects the shared flag family on every non-server - * command: --format/--json, --log-level/-v/--verbose, -q/--quiet, - * -y/--yes, --interactive/--no-interactive, --color/--no-color. - * Products cannot declare flags with those names. Product flag keys are - * camelCase and transliterate to --kebab-case. - * - * EXIT CODES (R6): 0 completed; 1 bug only; 2 errored (expected, - * structured); 3 user abort; 4–99 documented per command in - * `exitCodes`; 130/143 delivered signals. First signal fires - * context.signal and awaits teardown; a second exits immediately. - */ - -// ———————————————————————————————————————————————————————————————————————— -// Foundation types (from the zero-dependency foundation package — which -// owns CliStructuredError, Result, NextAction, and Diagnostic). Shown -// for reading convenience. -// ———————————————————————————————————————————————————————————————————————— - -import type { CliStructuredError, Diagnostic, NextAction, Result } from '@prisma/cli-foundation' - -/* - * For reference — the foundation shapes this file leans on: - * - * Diagnostic — a recorded finding: pure data, never thrown, no stack. - * { code: 'NAMESPACE.SUBCODE', severity: 'error' | 'warn' | 'info', - * summary, why?, fix?, where?, meta?, docsUrl? } - * Field-for-field the settled error envelope (ADR 239) minus `ok` — - * identical scales included; the two shapes never diverge. - * - * NextAction — the typed agent-facing follow-up (platform-shipped form): - * { kind: 'run-command' | 'user-choice' | 'edit-file' | 'done', - * journey, label, command?, commands?, reason? } - */ - -/** The commentary severity scale; also the log-level axis. Distinct from - * Diagnostic severity: 'verbose' grades commentary, which never enters - * the envelope. Step outcomes are completion states, not severities. */ -export type Severity = 'error' | 'warn' | 'info' | 'verbose' -export type LogLevel = Severity - -export type Format = 'human' | 'json' - -// ———————————————————————————————————————————————————————————————————————— -// §1 Events — R14: one engine vocabulary, product extensions in `data` -// ———————————————————————————————————————————————————————————————————————— - -/** - * The engine event envelope. `kind`-specific fields are the common - * vocabulary the engine renders consistently (human mode) and streams - * (json mode, §9). `data` is the product extension: passed through to - * machine consumers untouched, never interpreted by the engine, - * documented and versioned by the product as its own public API. A - * structure recurring inside `data` across commands is the promotion - * signal (R14). - * - * Rendering, human mode: `output` events with channel 'data' are the - * command's data and go to OUR stdout; everything else is commentary on - * stderr, filtered by the active log level. Events are transcript: they - * are NOT aggregated into the envelope (the one exception is - * `remediation` → nextActions). Findings that belong in the envelope - * are diagnostics on the presented outcome, not events. - * - * report() is synchronous fire-and-forget; the engine buffers and writes - * asynchronously. Calling it after the handler has resolved is a bug - * (InternalError). Events during teardown (after the signal, before - * resolution) are normal. - */ -export type EngineEvent = - | { - readonly kind: 'step-started' - readonly step: string - readonly id?: string - readonly parentId?: string - readonly data?: unknown - } - | { - readonly kind: 'step-finished' - readonly step: string - readonly id?: string - readonly outcome: 'ok' | 'failed' | 'skipped' | 'warning' - readonly data?: unknown - } - | { - readonly kind: 'progress' - readonly step?: string - readonly completed: number - readonly total?: number - readonly data?: unknown - } - /** Commentary at a severity; display-filtered by log level. Transcript - * only. 'error' is not valid here: fatal problems are the Result's - * error; envelope-worthy findings are diagnostics. */ - | { - readonly kind: 'message' - readonly severity: Exclude - readonly text: string - readonly data?: unknown - } - | { - readonly kind: 'output' - readonly source: string - readonly channel: 'data' | 'diagnostic' - readonly line: string - readonly data?: unknown - } - | { readonly kind: 'remediation'; readonly action: NextAction; readonly data?: unknown } - | { - readonly kind: 'endpoint' - readonly name: string - readonly url: string - readonly data?: unknown - } - | { - readonly kind: 'status' - readonly subject: string - readonly status: string - readonly from?: string - readonly data?: unknown - } - | { - readonly kind: 'artifact' - readonly path: string - readonly description?: string - readonly data?: unknown - } - -// ———————————————————————————————————————————————————————————————————————— -// §2 Outcomes and presented results — presentation materializes at the -// return site -// ———————————————————————————————————————————————————————————————————————— - -/** - * What a command concluded, stated at the return site. `exitCode` is - * REQUIRED at every return site iff the command documents exit codes - * (`0` for the clean path, a documented code otherwise — the type makes - * forgetting impossible exactly where a decision exists) and forbidden - * otherwise. `diagnostics` may be omitted at the call site; the - * presented result always carries an array. - */ -export type Outcome = [TCode] extends [never] - ? { - readonly data: T - readonly diagnostics?: readonly Diagnostic[] - } - : { - readonly data: T - readonly exitCode: TCode | 0 - readonly diagnostics?: readonly Diagnostic[] - } - -declare const PRESENTED: unique symbol - -/** - * What a completed command's handler returns inside `ok(...)`: the - * outcome plus the presentation the ACTIVE FORMAT already materialized. - * Built exclusively by ctx.present (the brand makes hand-construction a - * type error) — the context knows the format, calls only the - * presentation functions it needs, and the value crossing the - * product→engine boundary is data all the way down. - * - * `data` is what the envelope's `result` serializes (json presentation - * overrides when supplied). Materialization by format: human → human + - * stdout + next; human+--quiet → stdout; json → json + next. - * - * Guardrail (runtime, at the return site): a severity-'error' diagnostic - * requires a non-zero exitCode — a genuine could-not-complete belongs in - * notOk. The test: notOk when the command couldn't do its job; - * diagnostics when finding these WAS the job. - */ -export interface PresentedResult { - readonly [PRESENTED]: true - readonly data: T - /** 0 unless the outcome selected a documented code. */ - readonly exitCode: number - /** Never undefined; empty when the outcome recorded no findings. */ - readonly diagnostics: readonly Diagnostic[] - readonly presentation: { - readonly human?: readonly Block[] - readonly stdout?: readonly string[] - readonly json?: unknown - readonly next?: readonly NextAction[] - } -} -export { PRESENTED } - -/** - * The per-format presentation functions a handler supplies to - * ctx.present. Only the active format's functions are invoked, at the - * return site. `human` composes engine primitives (R5); `stdout` is the - * machine-consumable data lines — what --quiet leaves, what a pipe - * receives; `json` overrides the envelope's `result` (default: the - * data); `next` supplies the typed nextActions — agents branch on them; - * the human renderer formats them as prose (no stored string form). - */ -export interface Presentations { - readonly human: (ui: Ui) => readonly Block[] - readonly stdout?: () => readonly string[] - readonly json?: () => unknown - readonly next?: () => readonly NextAction[] -} - -// ———————————————————————————————————————————————————————————————————————— -// §3 Config sections — a PRODUCT-level fact (one product, one section) -// ———————————————————————————————————————————————————————————————————————— - -/** - * A product's named slice of prisma.config.ts, declared once in the - * product's manifest (§10). The token couples the section name, its - * validated type, and its total validator. Commands that need the - * section reference the token in `needs.config`, which is how the - * engine knows which commands an invalid section fails — and how - * ctx.config gets its type. - * - * The validator OWNS absence: its input is the raw section value, or - * undefined when the config file has no such section. A product that - * wants defaults applies them here; one that requires the section emits - * a section-required diagnostic here. It returns findings; it never - * throws (R10). Keep validators dependency-light: they load with the - * definition tree at startup (R9). - */ -export interface ConfigSection { - readonly name: string - readonly validate: (raw: unknown | undefined) => SectionValidation -} - -export type SectionValidation = - | { readonly ok: true; readonly value: T; readonly diagnostics: readonly Diagnostic[] } - | { readonly ok: false; readonly diagnostics: readonly Diagnostic[] } - -export declare function defineConfigSection(spec: { - readonly name: string - readonly validate: (raw: unknown | undefined) => SectionValidation -}): ConfigSection - -// ———————————————————————————————————————————————————————————————————————— -// §4 The handler context — R4: the whole world arrives as one argument -// ———————————————————————————————————————————————————————————————————————— - -export interface CommandContext { - /** The validated value of the command's needed config section — - * exactly TConfig; absence semantics belong to the product's - * validator (§3). Commands with no config need get undefined. */ - readonly config: TConfig - - /** Builds the PresentedResult for the active format: "present this - * outcome, via these presentations." The only constructor of - * PresentedResult. */ - readonly present: ( - outcome: Outcome, - presentations: Presentations, - ) => PresentedResult - - /** Management-API credentials, resolved at call time so long-lived - * sessions survive token refresh. Undefined when unauthenticated. - * Commands with needs.credentials never see undefined — the engine - * fails them early with the sign-in error. */ - readonly getCredentials: () => Promise - - /** The one way to emit while running (§1). */ - readonly report: (event: EngineEvent) => void - - /** Interactive input (§4a). */ - readonly prompt: PromptSurface - - /** Fires on Ctrl-C/SIGTERM (engine-owned; second signal force-exits). - * Session commands run until it fires; everything else aborts - * in-flight work with it. */ - readonly signal: AbortSignal - - /** Where the user invoked the CLI. Products never read process.cwd(). */ - readonly cwd: string - - /** - * R13, the conditional form (evidence: composer needs @prisma/dev only - * when the config declares postgres resources — unconditional needs - * belong in `needs.dependencies`). Resolves when the optional peer - * dependency is importable from the user's project; otherwise returns - * the ENGINE'S structured missing-dependency error — install command - * phrased by the engine with the user's package manager — for the - * handler to pass to notOk. Products never craft install prose. - */ - readonly requireDependency: (specifier: string) => Promise> -} - -export interface Credentials { - /** Opaque to the engine; shape owned by the Cloud product's auth - * library (placeholder pending its design). Workspace selection is - * session state, not a credential — it lives with that library, not - * here. */ - readonly token: string -} - -/** - * §4a Prompts. Every prompt except `consent` may carry a - * product-specified `default`. Interactively, Enter accepts the default. - * Under --yes, a prompt WITH a default resolves to it without - * displaying; a prompt WITHOUT a default cannot be operated and the - * invocation halts with a structured error (exit 2). In - * json/non-interactive/CI/non-TTY contexts the same default rule - * applies. User cancellation (Ctrl-C at the prompt) is a distinct - * structured error the engine maps to exit 3. - */ -export interface PromptSurface { - readonly confirm: ( - question: string, - opts?: { readonly default?: boolean }, - ) => Promise> - /** - * A question requiring EXPLICIT consent — never inferable, not - * necessarily destructive. Structurally undefaultable: no default - * parameter exists, so --yes, Enter-through, and non-interactive - * contexts can never satisfy it; the command's fix names the explicit - * flag that grants consent non-interactively. - */ - readonly consent: (question: string) => Promise> - readonly select: ( - question: string, - options: ReadonlyArray<{ value: T; label: string }>, - opts?: { readonly default?: T }, - ) => Promise> - readonly text: ( - question: string, - opts?: { readonly placeholder?: string; readonly default?: string }, - ) => Promise> -} - -// ———————————————————————————————————————————————————————————————————————— -// §5 Flags and positionals — R1: directly executable, typed by inference -// ———————————————————————————————————————————————————————————————————————— - -/** - * Product-declared flags. The shared family (§header) is engine-injected - * and reserved; handlers never see those values. Parse-time validation - * failures become structured errors carrying the allowed values. - * - * Deliberate asymmetry, matching CLI convention: flags are optional by - * default (requiredString is the exception); positionals are required by - * default (optionalString is the exception). - */ -export declare const flag: { - string(spec: { - brief: string - placeholder?: string - alias?: A & Char - default?: string - }): FlagSpec - requiredString(spec: { - brief: string - placeholder?: string - alias?: A & Char - }): FlagSpec - number(spec: { - brief: string - placeholder?: string - alias?: A & Char - default?: number - }): FlagSpec - boolean(spec: { brief: string; alias?: A & Char }): FlagSpec - enum(spec: { - brief: string - values: T - alias?: A & Char - default?: T[number] - }): FlagSpec - repeated(spec: { - brief: string - placeholder?: string - alias?: A & Char - }): FlagSpec -} - -/** Single-character alias, enforced at the type level: `Char<'q'>` is - * 'q'; `Char<'ab'>` is never. */ -export type Char = S extends `${string}${infer Rest}` - ? Rest extends '' - ? S - : never - : never - -declare const FLAG: unique symbol -export interface FlagSpec { - /** Phantom carrier for inference; exported so declaration emit works. */ - readonly [FLAG]: T -} -export { FLAG } - -export declare const positional: { - string(spec: { brief: string; placeholder: string }): PositionalSpec - optionalString(spec: { brief: string; placeholder: string }): PositionalSpec - /** Zero or more trailing values; at most one, declared last (order = - * declaration order; keys must not be integer-like). */ - variadic(spec: { brief: string; placeholder: string }): PositionalSpec -} -declare const POSITIONAL: unique symbol -export interface PositionalSpec { - readonly [POSITIONAL]: T -} -export { POSITIONAL } - -/** The parse SPI: a command's argument surface, one property. */ -export interface ArgsSpec< - TFlags extends Record>, - TPositionals extends Record>, -> { - readonly flags?: TFlags - readonly positionals?: TPositionals -} - -/** What a handler receives: separate namespaces, symmetric access — - * `args.flags.to`, `args.positionals.name`. */ -export interface Args< - TFlags extends Record>, - TPositionals extends Record>, -> { - readonly flags: { readonly [K in keyof TFlags]: TFlags[K] extends FlagSpec ? T : never } - readonly positionals: { - readonly [K in keyof TPositionals]: TPositionals[K] extends PositionalSpec ? T : never - } -} - -// ———————————————————————————————————————————————————————————————————————— -// §6 Command definitions — light at startup (R9), path-free (R12), -// runtime-discriminated by `kind`. Three modalities at equal rank: -// result / session / server. -// ———————————————————————————————————————————————————————————————————————— - -/** The help SPI: how the command shows itself in help output. Words - * only — the engine formats. */ -export interface HelpSpec { - /** One line, imperative, shown in listings. */ - readonly summary: string - readonly description?: string - /** Copy-pastable invocations, shown verbatim. */ - readonly examples?: readonly string[] -} - -/** - * The preconditions SPI: everything the engine gates BEFORE the handler - * loads. Each unmet need fails the command early with the engine's own - * structured error — consistent phrasing by construction, and a handler - * never runs in a world where it can't operate. - */ -export interface NeedsSpec { - /** The product's config section token: validate it, fail me on its - * error diagnostics, hand me the value as ctx.config. */ - readonly config?: ConfigSection - /** Fail early with the sign-in error when unauthenticated. */ - readonly credentials?: true - /** Optional peer dependencies this command cannot run without; the - * engine probes resolvability and phrases the install error. - * (Conditional needs use ctx.requireDependency instead.) */ - readonly dependencies?: readonly string[] - /** - * Fail early (before execution, before side effects) in - * json/non-interactive/CI/non-TTY contexts. This is a MECHANICAL - * precondition — "an interactive terminal is required" — and - * deliberately NOT an agent barrier: the client's nature is - * unverifiable, and a flag claiming to exclude agents would be a - * false guarantee. Anything requiring a verified human belongs - * server-side, where identity actually exists. - */ - readonly interaction?: true -} - -export interface CommandDefinition< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, - TCode extends number = never, -> { - readonly kind: 'result-command' - readonly help: HelpSpec - readonly args?: ArgsSpec - readonly needs?: NeedsSpec - - /** - * The command's documented exit codes (4–99): code → meaning. - * Rendered in help without executing anything; the keys type the - * outcome's exitCode, making it REQUIRED at every return site (`0` or - * a documented code). Absent = the command only exits 0/1/2/3 and the - * outcome carries no exitCode. - */ - readonly exitCodes?: Readonly> - - /** The heavy part, loaded only at execution (R9). The module's default - * export is the handler — annotate it with CommandHandler - * (type-only import of the light definition; no runtime cycle). */ - readonly handler: () => Promise<{ default: Handler }> -} - -export type Handler< - TFlags extends Record>, - TPositionals extends Record>, - TConfig, - TCode extends number = never, -> = ( - args: Args, - ctx: CommandContext, -) => Promise, CliStructuredError>> - -/** For impl files: `const run: CommandHandler = …` */ -export type CommandHandler = D extends CommandDefinition - ? Handler - : never - -export declare function defineCommand< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, - TCode extends number = never, ->( - def: Omit, 'kind'>, -): CommandDefinition - -/** - * A session command (dev, log tail): runs until the signal fires, - * speaks entirely through events, returns Result. No - * presentation, no exit-code set. A session always supports json mode: - * the event stream is its json surface. - */ -export interface SessionCommandDefinition< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, -> { - readonly kind: 'session-command' - readonly help: HelpSpec - readonly args?: ArgsSpec - readonly needs?: NeedsSpec - readonly handler: () => Promise<{ - default: ( - args: Args, - ctx: CommandContext, - ) => Promise> - }> -} - -export declare function defineSessionCommand< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, ->( - def: Omit, 'kind'>, -): SessionCommandDefinition - -/** - * A server command (lsp): a foreign client on the other end of stdio - * owns the conversation, so the engine hands over the streams. Events, - * presentation, formats, and prompts do not apply — by definition, not - * by opt-out; the handler returns the exit code directly. The shared - * flag family is NOT injected. - */ -export interface ServerCommandDefinition< - TFlags extends Record> = {}, - TConfig = undefined, -> { - readonly kind: 'server-command' - readonly help: HelpSpec - readonly args?: ArgsSpec - readonly needs?: NeedsSpec - readonly handler: () => Promise<{ - default: ( - args: Args, - io: { - readonly stdin: InputStream - readonly stdout: OutputStream - readonly stderr: OutputStream - readonly signal: AbortSignal - readonly cwd: string - readonly config: TConfig - }, - ) => Promise - }> -} - -export declare function defineServerCommand< - TFlags extends Record> = {}, - TConfig = undefined, ->( - def: Omit, 'kind'>, -): ServerCommandDefinition - -/** Erased union for manifests and mount maps; `kind` discriminates. */ -export type AnyCommand = - | CommandDefinition - | SessionCommandDefinition - | ServerCommandDefinition - -// ———————————————————————————————————————————————————————————————————————— -// §7 Presentation primitives — the R5 vocabulary -// ———————————————————————————————————————————————————————————————————————— - -/** Deliberately small; grows by the same evidence rule as events. */ -export type Block = - | { - readonly kind: 'summary' - readonly tone: 'ok' | 'error' | 'warn' | 'info' - readonly text: string - } - | { - readonly kind: 'fields' - readonly rows: ReadonlyArray<{ label: string; value: string; sensitive?: boolean }> - } - | { - readonly kind: 'table' - readonly columns: readonly string[] - readonly rows: ReadonlyArray - } - | { readonly kind: 'list'; readonly items: readonly string[] } - | { readonly kind: 'tree'; readonly roots: readonly TreeNode[] } -// NOTE: recorded findings are NOT a Block — they are the outcome's -// diagnostics; the engine renders them with the top-level error layout -// and carries them into the envelope, so the two surfaces cannot -// diverge. - -export interface TreeNode { - readonly label: string - readonly children?: readonly TreeNode[] -} - -/** Styling helpers usable inside block text; no direct writing. */ -export interface Ui { - readonly emphasize: (text: string) => string - readonly dim: (text: string) => string - readonly code: (text: string) => string -} - -// ———————————————————————————————————————————————————————————————————————— -// §8 Streams — minimal structural types; no NodeJS.* in the public -// surface -// ———————————————————————————————————————————————————————————————————————— - -export interface OutputStream { - write(text: string): void -} -/** Byte-oriented, so server commands can implement byte-counted - * protocols (lsp's Content-Length framing). setRawMode is present - * where the platform supports keypress input. */ -export interface InputStream extends AsyncIterable { - readonly setRawMode?: (enabled: boolean) => void -} - -// ———————————————————————————————————————————————————————————————————————— -// §9 Envelopes and the json stream -// ———————————————————————————————————————————————————————————————————————— - -export interface CompletedEnvelope { - /** ok = COMPLETED (the command executed to its end). A completed - * result may still carry findings and a non-zero exit code — bad news - * is a result, not an error. */ - readonly ok: true - /** The command's stable dotted identity — its full mount path - * ('project.env.add'). The schema-dispatch key for machine consumers; - * says nothing about arguments. */ - readonly commandId: string - readonly result: T - readonly exitCode: number - /** The recorded findings, verbatim from the presented outcome. */ - readonly diagnostics: readonly Diagnostic[] - readonly nextActions: readonly NextAction[] -} - -export interface ErroredEnvelope { - /** ok = false: the command did NOT complete. */ - readonly ok: false - readonly commandId: string - /** The PRIMARY error — what aborted the command. Severity 'error' by - * definition. A thrown CliStructuredError serializes to exactly this - * shape. */ - readonly error: Diagnostic - /** Accompanying findings when the abort had several (three config - * typos are three diagnostics, not one flattened error). */ - readonly diagnostics: readonly Diagnostic[] - /** Aggregated from remediation events. */ - readonly nextActions: readonly NextAction[] -} - -/** json mode emits one StreamEvent per line: the handler's events, - * flattened with the stream metadata, then exactly one terminal - * 'result' member carrying the envelope. */ -export type StreamEvent = - | (EngineEvent & StreamMeta) - | ({ readonly kind: 'result'; readonly envelope: CompletedEnvelope | ErroredEnvelope } & StreamMeta) - -export interface StreamMeta { - readonly commandId: string - /** ISO 8601 UTC. Injectable clock in tests (§11). */ - readonly timestamp: string -} - -// ———————————————————————————————————————————————————————————————————————— -// §10 Product manifests and shell mounting — R12: the shell owns the -// tree; a product owns its section -// ———————————————————————————————————————————————————————————————————————— - -/** - * What a product package exports: its config section (declared once — - * a product-level fact) and its commands by NAME. The unified config - * loader consumes the sections; the shell mounts the commands. A - * command whose needs.config token is not its product's section is a - * construction error. - */ -export interface ProductManifest { - readonly configSection?: ConfigSection - readonly commands: Readonly> -} - -/** What the shell builds: commands by PATH (space-separated, - * 'db migrate'). */ -export type MountedTree = Readonly> - -/** - * Shell-side construction. Group help is declared with the mount, since - * groups belong to the tree, not to products. Collisions, unknown - * groups, reserved-flag violations, grammar violations, and - * foreign-section references fail construction (build time, not run - * time). - */ -export declare function createCli(spec: { - readonly name: string - readonly version: string - readonly products: readonly ProductManifest[] - readonly groups: Readonly> - readonly commands: MountedTree -}): Cli - -export interface Cli { - /** Parse, execute, render, return the exit code. Never calls - * process.exit; never touches streams other than the provided ones. */ - run(argv: readonly string[], runtime: Runtime): Promise -} - -/** Everything environmental, injected once by the bin (or by a test). */ -export interface Runtime { - readonly stdout: OutputStream - readonly stderr: OutputStream - readonly stdin: InputStream - readonly cwd: string - readonly env: Readonly> - readonly isTty: { readonly stdin: boolean; readonly stdout: boolean; readonly stderr: boolean } - readonly signal: AbortSignal - /** Loaded config + file-level diagnostics; the shell builds this via - * the unified loader (R10). Tests hand in fixtures. */ - readonly config: LoadedConfig - readonly getCredentials: () => Promise - /** Used by the ENGINE to phrase install commands (products never - * do — see needs.dependencies and ctx.requireDependency). */ - readonly packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' -} - -export interface LoadedConfig { - /** Raw section values by name; validation happens per command via its - * product's section token. */ - readonly sections: Readonly> - /** File-level problems (unevaluable module, missing version marker) — - * section: null fails every command. */ - readonly diagnostics: ReadonlyArray<{ - readonly section: string | null - readonly diagnostic: Diagnostic - }> -} - -// ———————————————————————————————————————————————————————————————————————— -// §11 The product-repo test harness — R7: same machinery, bytes out -// ———————————————————————————————————————————————————————————————————————— - -export declare function createTestCli(spec: { - readonly products?: readonly ProductManifest[] - readonly commands: MountedTree - readonly groups?: Readonly> - readonly config?: Readonly> - readonly credentials?: Credentials - readonly packageManager?: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' - /** Fixed clock for deterministic stream timestamps. */ - readonly now?: () => Date -}): TestCli - -export interface TestCli { - run( - argv: readonly string[], - opts?: { - readonly stdin?: string - /** Scripted prompt answers, consumed in order; a run that prompts - * past the script fails the test. */ - readonly answers?: ReadonlyArray - /** Abort the run (session tests): fires the context signal. */ - readonly abort?: AbortSignal - /** Live event tap, for asserting mid-session behavior. */ - readonly onEvent?: (event: EngineEvent) => void - readonly cwd?: string - readonly isTty?: { stdin?: boolean; stdout?: boolean; stderr?: boolean } - readonly env?: Readonly> - }, - ): Promise<{ - readonly exitCode: number - readonly stdout: string - readonly stderr: string - /** Parsed stream (events + the terminal result) when json mode. */ - readonly json: readonly StreamEvent[] - /** Every EngineEvent the handler emitted, for semantic assertions. */ - readonly events: readonly EngineEvent[] - /** The PresentedResult the handler returned, for semantic - * assertions without byte-scraping. */ - readonly presented?: PresentedResult - }> -} diff --git a/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r2.md b/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r2.md deleted file mode 100644 index 1f2f7e80..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r2.md +++ /dev/null @@ -1,459 +0,0 @@ -# Code review round 2 — unified CLI engine public interface (v3) - -Reviewer pass: principal engineer. Naming, typology and system shape remain -the architect's; referrals are marked. - -Subject: `wip/designs/engine/engine-interface-draft.ts` (v3), read against v1 -and v2 (`-v1.ts`, `-v2.ts`) and my round-1 artifact `./reviews/code-review.md`. - -## Summary - -v3 is a large improvement. Of the 25 round-1 findings, 15 are fully resolved, -7 are partly resolved with a named residual, and 3 are untouched. Both round-1 -FAIL verdicts (R6 exit codes, R10 config sections) are cleared: exit codes now -span the settled table and config sections are bound to commands by a typed -token, which is what makes "a command fails only if a section it needs is -invalid" implementable rather than aspirational. The type-system defects that -made v1 unbuildable are genuinely fixed — I worked through each one and they -hold up. - -The return-site presentation ruling is, I think, right, and for a reason -stronger than the one stated. Under v1/v2 a presenter ran after the handler -had returned, which meant it had to reconstruct which case it was in from the -result value alone, and a throw inside it happened after the command had -logically succeeded. Under v3 the views close over live context at the point -the outcome is known, and a throw in a view is just a handler throw. That is -a real reduction in the number of things that can go wrong. - -Three things about the new shape need attention before this is settled. - -**The one internal contradiction is `exitCode`.** The v3 header states that -"nothing product-authored executes after the handler resolves — the engine -receives values, never callbacks." `exitCode?: (data: unknown) => number` is -precisely a product-authored callback the engine invokes after resolution. It -is also the one place where moving presentation to the return site cost type -safety that v2 had: v2's signature was `(value: TResult) => number`, correctly -typed; v3's is `(data: unknown) => number`, so every command computing a -custom exit code casts. Both problems have the same fix, and it is the fix -v3's own thesis implies: carry the exit code as a value on the presented -result. See N01 — this is the finding I would act on first. - -**The mode-to-view mapping has three unspecified interactions** that will be -decided by whoever implements first rather than by this document: what -`--verbose` does (there is no verbose view, so product-level detail under `-v` -— which both the ORM and the platform ship today — becomes unreachable); -what `--yes` does to `ctx.prompt.confirm` (handlers no longer see the flag, so -either the engine auto-answers or `-y` silently stops working in CI); and what -`--json --quiet` together mean, since the two modes select disjoint view sets. -N06 and N07. - -**`AnyCommand` has no runtime discriminant.** The three definition types are -structurally near-identical and differ only in their handler's return type, -which is erased at runtime. The engine must decide, per mounted command, -whether to expect a `PresentedResult` or `void`, whether to inject the shared -flag family, and whether auto-json applies — and it has nothing to branch on. -The `define*` functions are the natural place to stamp a tag. N03. - -Below that tier, the notable fresh items are: `PresentedResult` is -structurally constructible, so the "built exclusively by ctx.present" -invariant is documentation rather than type (N02); `NextAction` is declared in -the engine package but error envelopes carry next actions, which puts it on -the wrong side of the engine/foundation boundary (N04); flag `default`s do not -narrow the flag's type, so every defaulted flag still needs `?? default` in -the handler (N05); and positional order is object key order, which is both -implicit and, for integer-like keys, not insertion order at all (N08). - -Pressure-test results in brief. Multi-return-path handlers work well and are -better than v2 — each return site builds its own views with the case in hand. -`migration check` maps except for the `exitCode` cast. Composer `dev` now maps -cleanly onto `defineSessionCommand` (`Result`, event stream is the json -surface, `--fresh` as a boolean flag). `lsp` maps onto `defineRawCommand`. -`app deploy`'s two result shapes are no longer a problem at all, because there -is no result generic to unify — each path presents itself. That is a real -benefit of the ruling I did not anticipate. - -## Round-1 finding disposition - -| # | Round-1 finding | Disposition | Note | -|---|---|---|---| -| F01 | `ArgsOf` drops positionals | **Resolved** | `Args` takes both objects explicitly; `positionals` optionality no longer collapses the mapped type. Separate `flags`/`positionals` namespaces. | -| F02 | Handler typing is circular | **Resolved** | `CommandHandler` with a type-only import breaks the cycle; `import type` erases, so no runtime cycle. See N09 — the helper does not cover session or raw definitions. | -| F03 | Brand symbols unexported | **Resolved** | `export { FLAG }` / `export { POSITIONAL }` added; declaration emit will work. | -| F04 | Concrete command not assignable to bare `CommandDefinition` | **Resolved** | `AnyCommand` with `any` generics is assignable in both directions, so `CommandSet`, `createCli` and `createTestCli` accept real commands. Runtime discrimination is a separate problem (N03). | -| F05 | `TConfig` not inferable | **Resolved** | `configSection?: ConfigSection` is a direct inference site. Default `undefined` gives `config: undefined` when omitted. | -| F06 | No number flag, no defaults | **Partial** | `flag.number` and `default` added. But `default` does not narrow the return type — `flag.number({default: 15})` still yields `FlagSpec`, so handlers keep the `?? 15` (N05). | -| F07 | No short aliases | **Partial** | `alias?: string` added. Not constrained to one character, which is stricli's hard limit, so `alias: 'dp'` compiles and fails later (N05). | -| F08 | Positionals too thin | **Partial** | `variadic` added. Number and enum positionals still absent (acceptable). The "at most one, last" rule is unenforceable because positionals are an unordered `Record` (N08). | -| F09 | Flag/positional name collisions | **Resolved** | Separate namespaces make collision impossible by construction. | -| F10 | Exit codes contradict the settled table | **Partial** | 0/1/2/3/4–99/130/143 now stated; `exitCode` provides 4–99; the engine owns signal codes. Residuals: the callback is untyped and post-resolution (N01), the 4–99 range is not type-enforced, and whether a structured *failure* can carry a custom code is unstated. | -| F11 | Events have no defined stream | **Resolved** | `channel: 'data' \| 'diagnostic'` with routing documented — data to our stdout, everything else to stderr, all framed on stdout in json mode. This is the finding I was most concerned about and the fix is clean. | -| F12 | Session commands have no meaningful result | **Resolved** | `defineSessionCommand` returns `Result`, no presentation, event stream is the json surface. Also subsumes the platform's `emitJsonSuccessEvent: false` case. | -| F13 | `raw` contradicts the type | **Resolved** | `defineRawCommand` is its own type with its own handler shape returning an exit code directly. Raw commands cannot take positionals — presumably deliberate; worth confirming for `lsp`. | -| F14 | `Runtime` missing `env` and `isTty.stdout` | **Resolved** | Both added; auto-json now has a field to read and the engine's CI detection stays inside the injection seam. | -| F15 | No envelope slots for warnings/next steps/actions | **Resolved** | `next` view supplies `nextActions`; `nextSteps` derives from them; `warnings` aggregates from severity-`warn` message events. "Emit once, appear in both places" is a good call. Derivation rule for `nextSteps` is unstated (minor). | -| F16 | Nothing says which config section a command needs | **Resolved** | `ConfigSection` token plus `configSection` binding. Raw sections in `LoadedConfig`, validated per command, is exactly what makes the R10 rule implementable. Residual in N12. | -| F17 | Cancellation and exit-code path undefined | **Partial** | The engine now records which signal fired, and prompt failures carry distinct codes for "unavailable" (exit 2) versus "cancelled" (exit 3). **Still open:** no teardown deadline and no defined behaviour for a second Ctrl-C. A session that will not die remains unspecified. | -| F18 | `report` has no backpressure, no end of life | **Partial** | Report-after-resolution is now documented as an `InternalError`, which closes the envelope-corruption hole. Backpressure is untouched: `report` still returns `void` with no stated buffer bound, so a high-volume log tail on a slow pipe is still an unbounded-memory path. | -| F19 | Test harness cannot test session commands | **Partial** | `abort`, `answers`, `isTty`, `env`, `now` and the `presented` capture are all new and substantial — sessions are now testable. Residuals: no `cwd` knob (so commands writing artifacts relative to cwd write into the repo); events are only observable after the run, so "abort once ready" needs a timer rather than a condition; no capture of which prompts were asked (N11). | -| F20 | Steps have no identity | **Resolved** | `id`/`parentId` added, matching the ORM's span shape. | -| F21 | Credentials cannot refresh | **Resolved** | `getCredentials(): Promise` on both context and runtime. Long sessions survive expiry. | -| F22 | No way to declare a command needs auth | **Open** | Unchanged. Every handler still checks `undefined` and writes its own "not authenticated" error, so the wording drifts per product — the same failure R5 exists to prevent, one layer down. | -| F23 | `createCli` claims build-time failure it cannot deliver | **Partial** | The comment now says "build time, not run time" explicitly, which sharpens the claim but does not change the mechanism: `createCli` returns `Cli`, not `Result`, so it can only throw when called. | -| F24 | Foundation import and Node types in the public surface | **Open** | `Result`/`CliStructuredError` still come from `@prisma/cli-foundation`; `NodeJS.WritableStream` etc. still appear in `Runtime` and in the raw command's `io`. N04 adds a new instance of the same boundary problem in the opposite direction. | -| F25 | Engine flags leak into `args` | **Resolved** | The shared family is engine-injected, reserved, and never reaches handlers; `--json` is now an engine mode rather than a declared flag. This is a better answer than the one I suggested. | -| F26 | No optional-dependency probe (R13's positive half) | **Partial** | `probeDependency(specifier): Promise` added. A bare boolean means the handler still authors the "missing dependency, install it with your package manager" error, so the wording drifts per product; and a boolean cannot express the failure Composer actually hits, which is a resolvable-but-wrong-version conflict (`DEPS.EFFECT_VERSION_CONFLICT`), not absence. | - -**Counts:** resolved 15, partial 7, open 3. - -## Fresh findings - -### N01 — `exitCode` is a post-resolution callback, and it lost the typing v2 had - -**Location:** §7, `CommandDefinition.exitCode?: (data: unknown) => number` -(lines 405–408); header claim at lines 23–24. - -**Issue:** two problems in one field. First, the v3 header states that nothing -product-authored executes after the handler resolves and that the engine -receives values, never callbacks — and this is a product-authored callback the -engine invokes after resolution. It is the only exception in the file. Second, -because v3 removed the result generic, the callback receives `unknown` where -v2's received `TResult`. Every command with a custom exit code now casts: -`exitCode: (data) => (data as CheckResult).failures.length > 0 ? 4 : 0`. - -**Why it matters:** the cast is at the exact point round-1 F10 was trying to -make safe — a wrong cast produces a wrong exit code, which is the machine -surface agents and CI branch on, and no type checks it. The contradiction also -matters on its own terms: an invariant with one exception is an invariant -people stop trusting, and this one is load-bearing for the "values all the way -down, serializable, snapshotable" property v3 is selling. - -**Suggestion:** move the exit code to the return site with everything else — -`ctx.present(data, views, { exitCode: 4 })`, or a third `Views` member. The -exit code is a fact about the outcome, known exactly where the outcome is -known. This removes the cast, removes the callback, restores the invariant, -and drops a field from the definition. It also relaxes a constraint the -current shape imposes without saying so: `exitCode` as a pure function of -`data` cannot express an exit code that depends on run context rather than on -the returned value. - -While making that change, consider typing the code as a branded 4–99 value or -validating the range in the engine — a handler returning `300` or `-1` becomes -a nonsense shell status via mod 256. - -### N02 — `PresentedResult` is structurally constructible, so the mode invariant is unenforced - -**Location:** §3, `PresentedResult` (lines 191–199); "Built exclusively by -ctx.present" (line 182). - -**Issue:** `PresentedResult` is a plain interface with two public members. A -handler can return `{ data, views: { human: [...] } }` directly and satisfy the -type. Nothing marks it as engine-constructed. - -**Why it matters:** the entire correctness argument for return-site -presentation is that the *context* decides which views to materialize, because -only it knows the mode. A hand-built literal breaks that silently: it might -carry a `human` view in json mode (harmless, wasted) or omit one in human mode -(the engine has nothing to render and must invent a fallback). Neither is -caught anywhere, and both are the kind of thing that gets copied once and then -spreads. - -**Suggestion:** brand it exactly as `FlagSpec` is branded — an exported -`unique symbol` phantom member that only `ctx.present` can produce. The -mechanism is already in the file; this just applies it one more time. - -### N03 — `AnyCommand` has no runtime discriminant - -**Location:** §7, `AnyCommand` (lines 496–499); the three definition -interfaces. - -**Issue:** `CommandDefinition`, `SessionCommandDefinition` and -`RawCommandDefinition` have the same field names and differ only in their -handler's return type, which is a type-level fact erased at runtime. `createCli` -receives `Record` and must decide, per command, whether to -await a `PresentedResult` or a `void`, whether to inject the shared flag -family (raw: no), whether auto-json applies (raw: no), and which help layout -to use. - -**Why it matters:** with nothing to branch on, the engine either introspects -the loaded handler's return value at execution time — which means the decision -about flag injection and json mode, both of which must be made *before* the -handler loads, cannot be made at all — or it guesses. This is a genuine -blocker for the mounting path rather than a tidiness issue. - -**Suggestion:** have `defineCommand` / `defineSessionCommand` / -`defineRawCommand` stamp a discriminant (`readonly kind: 'value' | 'session' | -'raw'`) and make `AnyCommand` a discriminated union on it. That also lets the -shell validate mounts sensibly, and narrows correctly in the engine's own -code. - -### N04 — `NextAction` sits on the wrong side of the engine/foundation boundary - -**Location:** §1, `NextAction` (lines 60–69); §9, `ErrorEnvelope.nextActions` -(line 563). - -**Issue:** `NextAction` is declared in the engine package. But error envelopes -carry `nextActions`, and errors are raised at their origin inside product -operations, carried in `CliStructuredError` from `@prisma/cli-foundation`. For -a structured error to carry next actions, the foundation must reference -`NextAction` — which would make the foundation depend on the engine, inverting -the dependency the two-package split exists to establish. - -**Why it matters:** it is a package cycle discovered at implementation time -rather than now. The workaround people reach for — errors carry -`nextSteps: string[]` while successes carry `NextAction[]` — is exactly the -"one concept spelled several ways" the survey ranks as the second most -recurring problem in the corpus, reintroduced at the success/failure seam. - -**Suggestion:** move `NextAction` (and `Severity`, which has the same -property) into `@prisma/cli-foundation` and re-export from the engine. -Which package owns which type is an architect call; that the current -placement cannot work is not. - -### N05 — Flag defaults do not narrow, and aliases are unconstrained - -**Location:** §6, `flag.string` / `flag.number` / `flag.enum` (lines 317–339). - -**Issue:** `flag.number({ brief, default: 900 })` returns -`FlagSpec`. The whole purpose of a default is that the -value is always present, so the handler still writes `?? 900` — and now the -default lives in two places and can disagree. Separately, `alias?: string` -accepts any string, while stricli supports single-character aliases only. - -**Why it matters:** the duplicated default is a correctness trap (help text -says one thing, the handler's fallback says another) and it removes the -benefit that motivated adding defaults at all. The alias type accepts values -that cannot work. - -**Suggestion:** overload each factory so the presence of `default` produces the -non-optional spec type. Constrain `alias` to a one-character template-literal -type, or validate it when the tree is constructed and say so. - -### N06 — `--verbose` has no view, so product-level detail under `-v` is unreachable - -**Location:** §3, the mode-to-view mapping (lines 187–190). - -**Issue:** the mapping covers human, `--quiet` and json. `--verbose` is in the -injected flag family but selects no view, and handlers cannot see it (by -design, correctly). - -**Why it matters:** both shipping families put product detail behind `-v` -today — the ORM renders `timings` and expands truncated conflict lists, the -platform appends timing diagnostics. Under this shape the engine can add its -own detail under `-v` but a product can never add any. That may well be the -right ruling, but as an unstated omission it will be discovered when someone -tries to port `db verify`'s verbose conflict list and finds there is nowhere -to put it. - -**Suggestion:** either add an optional `verbose?: (ui: Ui) => readonly Block[]` -view materialized only in verbose human mode, or state explicitly that `-v` -adds engine-owned detail only and that product detail belongs in `data`. - -### N07 — `--yes` and `--json --quiet` semantics are unspecified - -**Location:** header lines 33–35 (the injected flag family); §5, -`PromptSurface`. - -**Issue:** two interactions are named nowhere. (a) Handlers no longer see -`--yes`, so what does it do? If it does not auto-answer `ctx.prompt.confirm`, -then `-y` has stopped working and every CI script that relies on it breaks. -If it does, then the engine is auto-confirming destructive operations, and the -platform's deliberately stronger pattern — typed confirmation, `--confirm -` — must be documented as something `-y` does *not* satisfy. -(b) `--json --quiet` selects two disjoint view sets (json+next versus stdout); -precedence is undefined. - -**Why it matters:** (a) is a destructive-operation safety question, which -makes it the highest-consequence unstated default in the file. (b) is minor -but will be resolved differently by different implementers. - -**Suggestion:** state that `--yes` makes `prompt.confirm` return `true` -without prompting and that it does not satisfy typed confirmation, which stays -a declared flag. State that json mode wins over `--quiet`. - -### N08 — Positional order comes from object key order - -**Location:** §6, `positional` (lines 348–353); `positionals?: TPositionals` -as a `Record`. - -**Issue:** positionals are declared in an unordered record, so argument order -is object key insertion order. That is stable for ordinary string keys but -*not* for integer-like keys, which JavaScript reorders to the front. The two -ordering rules the comments assert — variadic last, and by implication -optional after required — cannot be expressed or checked. - -**Why it matters:** an implicit ordering rule carried by object literal syntax -is a subtle source of wrong argument binding, and the failure mode (arguments -silently swapped) is quiet. - -**Suggestion:** at minimum, validate both rules when the tree is constructed -and document that declaration order is argument order. An ordered form (a -tuple, or `positionals: [named(...), named(...)]`) removes the class entirely -— that is a shape choice, so architect referral, but the current form does -need one of the two. - -### N09 — `CommandHandler` covers only value commands - -**Location:** §7, `CommandHandler` (lines 422–424). - -**Issue:** the conditional matches `CommandDefinition` only. A session -command's implementation file has no helper and must hand-write -`(args: Args, ctx: CommandContext) => Promise>`, -which is exactly the drift F02 was fixed to prevent. Raw commands likewise. - -**Suggestion:** extend the conditional to all three definition types, or ship -`SessionHandler` and `RawHandler` alongside. - -### N10 — `Views`'s type parameter is unused - -**Location:** §3, `Views` (lines 211–216). - -**Issue:** none of the four members mentions `T` — the view functions close -over the data lexically rather than receiving it. So `Views` is -structurally `Views` and `T` is inferred solely from `present`'s -first argument. - -**Why it matters:** low severity, but a phantom type parameter invites the -reader to believe a relationship is being checked when it is not. Someone will -eventually pass views that describe a different value than `data` and nothing -will complain. - -**Suggestion:** either drop the parameter, or pass the data into the view -functions (`human: (data: T, ui: Ui) => Block[]`) so the relationship is real. -The second also makes views extractable into named module-level functions, -which helps multi-return-path handlers share view logic. - -### N11 — The harness cannot observe events mid-run, control cwd, or capture prompts - -**Location:** §11, `TestCli.run` options and result. - -**Issue:** three residuals from F19. (a) `abort?: AbortSignal` requires the -test to decide *when* to abort, but events are only visible after `run()` -resolves — so the realistic session test ("come up, reach ready, then stop") -has to use a timer, which is the flaky-test pattern. (b) No `cwd` option, so a -command that writes artifacts relative to cwd writes into the product repo -during tests. (c) `answers` is consumed positionally with no capture of which -prompts were asked, so a test cannot assert that the destructive-operation -confirmation was actually shown — only that *something* consumed an answer. - -**Why it matters:** (a) and (c) between them mean the two riskiest command -classes — sessions and destructive prompts — are testable in form but weakly -in substance. - -**Suggestion:** add an `onEvent` callback (or `abortWhen: (e: EngineEvent) => -boolean`) so aborts are condition-driven; add `cwd`; add `prompts` to the -result recording each question asked and the answer given. - -Related, and worth one line: because only the active mode's views are -materialized, a single test run can never assert on both the human and the -json rendering. That is inherent to the design and fine — but it should be -stated, so product test suites are written to run both modes rather than -discovering one of them is uncovered later. - -### N12 — Successful section validation may carry diagnostics with no defined fate - -**Location:** §4, `SectionValidation`'s `ok: true` branch (line 235). - -**Issue:** the success branch carries `diagnostics: readonly -CliStructuredError[]` — non-fatal problems in an otherwise valid section. The -draft never says what the engine does with them. - -**Why it matters:** they are presumably meant to surface as warnings, which -would mean they belong in the envelope's `warnings` array alongside the -severity-`warn` message events. If unstated, they get dropped, and a user's -deprecated-config-key warning silently never appears. - -**Suggestion:** state that they render as warnings and join the envelope's -`warnings`, on the same path as `message` events. - -Separately: `LoadedConfig` still cannot distinguish "no config file exists" -from "config file exists and is empty". Both produce `sections: {}`, -`diagnostics: []`. For a command that needs a section, both produce -`config: undefined` and the handler writes the same error, so this is probably -harmless — but "you have no prisma.config.ts" and "your prisma.config.ts is -missing the composer section" deserve different fixes, and only the product -can tell them apart if the engine gives it the fact. - -### N13 — Config validators live in the eagerly loaded tree - -**Location:** §4, `ConfigSection.validate`; §7, `configSection` on the -definition. - -**Issue:** R9 keeps heavy dependencies behind the lazy `handler`. The -`ConfigSection` token — including its validator function — is referenced by -the static definition, so it and whatever it imports load at startup. A -validator built on a schema library (arktype and zod both appear in the -corpus) pulls that library into every invocation of every command, including -`prisma --help`. - -**Why it matters:** R9's stated motivation is that `prisma migrate` can never -be slowed or taken down by a product it is not using. A shared config -validator undermines that for all commands at once, and it will not be -noticed until startup time is measured. - -**Suggestion:** state the rule (validators must be dependency-free, or -hand-written predicates), or make the validator itself lazily loaded like the -handler. Worth deciding now, because it constrains how products write -validators. - -### N14 — `EventFrame` nests an event's `data` inside the frame's `data` - -**Location:** §9, `EventFrame` (lines 567–573). - -**Issue:** `EventFrame.data` is the whole `EngineEvent`, which itself has a -`data` field for product extensions. Machine consumers read -`frame.data.data` for the extension payload, and `frame.type` duplicates -`frame.data.kind`. - -**Why it matters:** minor, but this is the agent-facing wire format — the one -surface where a confusing shape is paid for by every consumer, forever, and -which is the hardest thing in the design to change later. - -**Suggestion:** flatten the event's own fields into the frame, or rename one -of the two `data` fields. The platform's shipped shape -(`{type, command, timestamp, data}` where `data` is the payload) suggests -flattening. - -## Deferred - -Unchanged from round 1, and still correctly out of scope: config-section -*registration* mechanics beyond the token (the token is what R10 needed, and -it is now present), daemon management (mode 5), autocomplete, telemetry hooks, -and duration/timing events. One addition: whether raw commands should accept -positionals (`lsp` may want a path) is a small open question rather than a -defect. - -## Acceptance-criteria verification - -Same strict verdicts as round 1. **PASS** = the interface structurally -satisfies or enforces the requirement. **WEAK** = satisfiable, but the shape -does not enforce it. **FAIL** = the shape contradicts the requirement or -cannot express it. **NOT VERIFIED** = cannot be assessed from the draft. - -| R | Requirement | R1 | R2 | Detail | -|---|---|---|---|---| -| R1 | One language, directly executable | PASS | **PASS** | Unchanged, and strengthened: `CommandHandler` removes the one path by which round 1's typing defects would have reintroduced a hand-maintained parallel description of the arguments. The declared object is still what runs. | -| R2 | Commands end in typed operation calls | WEAK | **WEAK** | Unchanged. The shape remains compatible and the thin case remains the natural one, but nothing prevents a fat handler. Return-site views arguably pull slightly the other way — presentation logic now lives in the handler file — though it is presentation, not business logic, so the requirement is not threatened. | -| R3 | The engine package is the whole contract | WEAK | **WEAK** | No stricli type appears; that goal still holds. The two round-1 leaks persist: products import `@prisma/cli-foundation` for `Result`/`CliStructuredError`, and `NodeJS.*` stream types remain in `Runtime` and in the raw command's `io`. N04 adds a third instance in the opposite direction — `NextAction` is on the engine side but is needed by foundation-owned errors, which as written is a package cycle. Fixable without design change, but no longer trivially. | -| R4 | Products receive a context, never the environment | PASS | **PASS** | Improved. `getCredentials()` replaces the static token, `probeDependency` gives the R13 check a sanctioned route that does not touch the environment directly, and `cwd` is still the only path to the working directory. Nothing in the context reaches disk, env or TTY. | -| R5 | Products have no presentational API | PASS | **PASS** | Held, and on a better footing. `Block` remains the only vocabulary, `Ui` still cannot write, and products no longer see `--json`/`--quiet`/`--verbose` at all, which removes the temptation round-1 F25 identified. Return-site materialization means no product code runs after resolution — with the single `exitCode` exception (N01). N06 records the cost: products cannot express verbose-only detail. | -| R6 | Errors and results follow the settled conventions | **FAIL** | **WEAK** | Cleared as a failure. The exit-code space now matches the settled table: 4–99 via `exitCode`, 130/143 engine-owned with the signal recorded, and prompt failures carry distinct codes so "interaction unavailable" (2) and "user cancelled" (3) are mechanically separable rather than string-matched. Not yet PASS: the custom code is computed by an untyped post-resolution callback (N01), the 4–99 range is not enforced anywhere, and whether a structured *failure* can carry a custom code is unstated. | -| R7 | Product-repo end-to-end tests are first-class | WEAK | **PASS** | Upgraded. `abort` makes session commands testable, `answers` makes prompt-bearing commands testable, `isTty`/`env`/`now` make mode selection and framing deterministic, and the `presented` capture allows semantic assertions without byte-scraping. Every command class can now be driven argv-in, bytes-out from a product repo, which is what the requirement asks. The residuals in N11 — no `cwd`, no mid-run event observation, no prompt capture — are real and worth fixing, but they narrow the quality of the tests rather than the class of commands that can be tested. | -| R8 | The shell's test burden is integration proof | NOT VERIFIED | **NOT VERIFIED** | Still an allocation-of-work requirement with no interface surface. Nothing obstructs it. Assess against the shell's test plan. | -| R9 | Static tree, lazy guts | PASS | **PASS** | The lazy `handler` is unchanged and still matches stricli's loader; help still renders from static declarations. Removing the presenters from the definition makes the static tree lighter than v2's, which is a real gain. One new leak to watch, not enough to change the verdict: `configSection.validate` is referenced from the static definition, so a schema library behind a validator lands in every invocation's startup path (N13). | -| R10 | One config file, validated by its products, never a crash | **FAIL** | **PASS** | Cleared, and this is the largest single improvement in v3. The `ConfigSection` token couples name, type and never-throwing validator; `configSection` binds a command to exactly one section; `LoadedConfig` carries raw sections validated per command. That is what makes "a command fails only if a section it needs is invalid" implementable — a bad Composer section genuinely cannot touch `prisma migrate`. File-level problems (unevaluable module, missing `defineConfig` marker) are expressible as `section: null` diagnostics that fail everything, which is R10's fail-early rule. Residuals are small and named in N12. | -| R11 | Pinned versions, tandem releases | NOT VERIFIED | **NOT VERIFIED** | Release-process requirement, no interface surface. | -| R12 | The shell defines the command tree | PASS | **PASS** | Unchanged. No path appears in any definition; mount keys and group briefs live at `createCli`. `AnyCommand` now makes the mount maps actually typecheck, which round 1 found they did not. The overstated "fails the build" claim (F23) persists as a documentation-versus-mechanism gap, not a requirement failure. | -| R13 | The CLI never touches a package manager | WEAK | **WEAK** | The prohibition still holds absolutely — nothing installs or vendors. `probeDependency` gives the positive half a sanctioned route, which is progress. Still not PASS because the requirement's positive half is a *structured error naming the dependency and how to install it*, and a `Promise` produces no error at all: each product writes its own wording, so the message drifts exactly as R5 predicts. A boolean also cannot express the failure Composer actually hits, which is a version conflict rather than absence. | -| R14 | One event vocabulary, engine-defined, with product extensions | PASS | **PASS** | Strengthened on every axis round 1 flagged: `id`/`parentId` restore the nesting the ORM's span model needs, `channel` gives data-versus-commentary routing, `artifact` and `from` were added on survey evidence, and `warning`/`notice` merged onto the one severity scale. `data?: unknown` remains the uniform extension point with the engine explicitly not interpreting it. The only blemish is the wire shape's double `data` (N14), which is a framing detail rather than a vocabulary one. | - -### Summary counts - -| Verdict | Round 1 | Round 2 | Requirements (round 2) | -|---|---|---|---| -| PASS | 6 | **8** | R1, R4, R5, R7, R9, R10, R12, R14 | -| WEAK | 4 | **4** | R2, R3, R6, R13 | -| FAIL | 2 | **0** | — | -| NOT VERIFIED | 2 | **2** | R8, R11 | -| **Total** | 14 | **14** | | - -Movement: R6 FAIL → WEAK, R10 FAIL → PASS, R7 WEAK → PASS. No regressions. diff --git a/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r3.md b/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r3.md deleted file mode 100644 index f0b0380f..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r3.md +++ /dev/null @@ -1,631 +0,0 @@ -# Code review round 3 — unified CLI engine public interface (v4) - -Reviewer pass: principal engineer. Round-3 findings are numbered P01–P11 (F = -round 1, N = round 2). - -Subject: `wip/designs/engine/engine-interface-draft.ts` (v4), read against v3 -and my round-2 artifact `./reviews/code-review-r2.md`. - -Three claims in this review were checked by compiling them rather than -reasoning about them (TypeScript 5.6, `--strict`). Where that happened I say -so and give the compiler's output. - -## Summary - -**The design is settled. What remains is not design work.** Every structural -question I raised across rounds 1 and 2 is now closed, both round-1 FAILs -stayed closed, and the semantics reframe is coherent. I would stop iterating -on the shape. - -What remains is two type declarations that do not do what they say, one safety -property that is currently a convention where it could cheaply be a type, and -one under-specified stream contract. None of them changes a shape; all are -local edits to individual declarations. I am explicitly **not** calling this a -clean verdict, because two of them are defects rather than nits — but the -distance to clean is small and mechanical. - -**P01 is a hard bug.** `SingleChar = string & { readonly length?: 1 }` does not -constrain aliases to one character — it rejects *every* string, including -`'q'`. TypeScript types `'q'.length` as `number`, never as `1`, so no string -literal can satisfy `{length?: 1}`. Compiled: - -``` -t.ts(4,5): error TS2322: Type 'string' is not assignable to type 'SingleChar'. -t.ts(5,5): error TS2322: Type 'string' is not assignable to type 'SingleChar'. -``` - -Line 4 is `alias: 'q'`. The fix introduced for N05 makes the `alias` field -unusable. A working formulation exists and I verified it. - -**P02 is the one real gap left in an otherwise excellent mechanism.** The -outcome-code split — a documentable catalogue on the definition, selection at -the return site — is the right answer, better than what I proposed, because -the catalogue renders in help without executing anything. But -`opts?: { outcomeCode?: number }` is typed `number`, not against the -catalogue. So `ctx.present(data, p, { outcomeCode: 7 })` against a catalogue -of `{4, 5}` compiles, and the engine's verification fires at the return site — -after the command has done all of its work. A typo turns a successful -`migration check` into an internal error at exit. This is closable: I compiled -the fix, and TypeScript produces -`Type '7' is not assignable to type '4 | 5 | undefined'` at the call site. - -**P03 is the item I would think hardest about.** The prompt-default rule is -clever and I like it — `--yes` accepts a declared default, no default means a -structured halt, so destructive confirmations "simply declare no default" and -`--yes` can never blast through them. But nothing *enforces* that a -destructive confirm declares no default. One product writing -`confirm('Delete production database?', { default: true })` re-opens the hole -silently. R5's own rationale is that convention "demonstrably did not hold the -line", and this is the highest-consequence convention in the file. Making it -structural is cheap: a separate `confirm.destructive(question)` that accepts -no `default` parameter at all turns the rule into a type error. - -**P04 concerns the one named consumer of raw mode.** `InputStream extends -AsyncIterable` does not say whether it yields chunks or lines, and it -yields decoded strings. An LSP server over stdio reads `Content-Length: N` and -then exactly N *bytes*; a decoded string cannot be counted in bytes when the -payload is multi-byte, and if the engine helpfully splits on newlines the -framing is destroyed outright (LSP payloads contain newlines). `lsp` is the -only command `defineRawCommand` exists for, so this contract should be settled -against it. - -Two things I want to credit. Removing the `exitCode` callback makes "nothing -product-authored executes after the handler resolves" exception-free — that -invariant is now true as written, which it was not in v3. And resolving the -`--verbose` question through one log-level mechanism rather than a second -presentation member is the better of the two answers I offered; products emit -`verbose` messages as events they already had, and no new surface appeared. - -I also withdraw one round-2 grade. I marked R13 WEAK because `probeDependency` -returns a bare boolean and each product authors its own missing-dependency -error. Re-reading R13, it asks that *the command* return that error — product -authorship is what the requirement specifies, not a deviation from it. With -`packageManager` added in v4, the command now has the facts to phrase "install -it with your own package manager". That is a PASS, and my round-2 WEAK -imported an R5 concern into an R13 verdict. - -## Disposition of round-2 findings - -| # | Round-2 finding | Disposition | Note | -|---|---|---|---| -| N01 | `exitCode` is a post-resolution callback; lost v2's typing | **Partial** | The callback is gone and the "no product code after resolution" invariant is now exception-free — that half is fully resolved, and the catalogue split is better than what I proposed. The typing half is not: selection is `number`, verified against the catalogue at runtime, at the return site. See **P02**. | -| N02 | `PresentedResult` hand-constructible | **Resolved** | Branded with an exported `PRESENTED` symbol, same idiom as `FLAG`. | -| N03 | `AnyCommand` has no runtime discriminant | **Resolved** | `kind: 'command' \| 'session' \| 'raw'` stamped by the `define*` functions via the `Omit<…, 'kind'>` pattern. I compiled the inference question this raises (does `Omit<>` wrapping break generic inference from `flags`?) — it does not. No concern. | -| N04 | `NextAction` on the wrong side of the package boundary | **Resolved** | Moved to `@prisma/cli-foundation`, so the engine and the error envelope share it with no cycle. | -| N05a | Flag defaults do not narrow | **Open** | Unchanged: `flag.number({ default: 900 })` still yields `FlagSpec`, so the handler keeps `?? 900` and the default lives in two places that can disagree. See **P05**; fix verified. | -| N05b | `alias` unconstrained | **Regressed** | `SingleChar` was added and rejects every string, including one-character ones. See **P01**. | -| N06 | `--verbose` selects no view | **Resolved** | One mechanism: `--log-level error\|warn\|info\|verbose`, a `verbose` message severity, `--verbose` as shorthand. No presentation member added. This is the better answer. | -| N07 | `--yes` and `--json --quiet` unspecified | **Partial** | The `--yes` mechanism is now fully specified and thoughtfully designed; the residual is that its safety property is convention-only (**P03**). `--json --quiet` precedence is still unstated — the materialization table covers `human+--quiet` only (**P10**). | -| N08 | Positional order is object key order | **Resolved** | Documented: declaration order is argument order, variadic last, keys must not be integer-like. Convention rather than type, but the failure is now named where an author will read it. | -| N09 | `CommandHandler` covers only value commands | **Open** | Unchanged. Session and raw implementation files still hand-write the handler signature, which is the drift F02 was fixed to prevent. See **P09**. | -| N10 | `Views`'s parameter unused | **Resolved** | Renamed to `Presentations` and de-genericised. The phantom parameter is gone. | -| N11 | Harness: no cwd, no mid-run events, no prompt capture | **Mostly resolved** | `cwd` and an `onEvent` live tap added — the two that mattered. Prompt capture is still absent (nit, in **P11**). | -| N12 | Fate of `ok: true` section diagnostics unstated | **Open** | Unchanged. Nit, in **P11**. | -| N13 | Validators load at startup | **Resolved** | Documented as "keep validators dependency-light: they load with the definition tree at startup (R9), not with the handler." Convention, but stated at the point of use. | -| N14 | `EventFrame` nests `data` inside `data` | **Resolved** | `Frame = EventFrame \| ResultFrame` with `event: EngineEvent`. The added `ResultFrame` also makes the json stream self-describing, which is more than I asked for. | -| F17 | No teardown deadline / second-signal behaviour | **Resolved** | "First signal fires context.signal and awaits handler teardown; a second signal exits immediately with the signal's code." | -| F18 | `report` backpressure | **Accepted trade** | Now explicit: "synchronous fire-and-forget; the engine buffers and writes asynchronously (no backpressure signal — accepted trade)". Legitimate. Residual: the buffer has no stated bound or drop policy (**P08**). | -| F22 | No way to declare a command needs auth | **Resolved** | `requiresCredentials` on value and session definitions; the engine fails early with one canonical sign-in error. | -| F23 | `createCli` claims build-time failure | **Open** | Unchanged doc-versus-mechanism gap. Nit, in **P11**. | -| F24 | Foundation import and Node types in the surface | **Mostly resolved** | `NodeJS.*` replaced by structural `OutputStream`/`InputStream` — the runtime-agnosticism concern is fully addressed. The remaining residual is that products import two of our packages; a re-export closes it (see R3 in the table). | -| F26 | `probeDependency` returns a bare boolean | **Resolved** | `packageManager` added, which is the missing fact for phrasing the install command. Grade corrected — see R13. | - -**Counts:** resolved 14, partial 3, open 4 (three of which are nits), regressed 1. - -## Fresh findings - -### P01 — `SingleChar` rejects every string, so `alias` is unusable — MUST FIX - -**Location:** §5, `export type SingleChar = string & { readonly length?: 1 }` -(line 383), used by all six flag factories. - -**Issue:** TypeScript types the `length` property of any string, including a -one-character literal, as `number` — it does not compute literal lengths. So -no string is assignable to `{ readonly length?: 1 }`. Verified: - -``` -t.ts(4,5): error TS2322: Type 'string' is not assignable to type 'SingleChar'. - Type 'string' is not assignable to type '{ readonly length?: 1 | undefined; }'. -t.ts(5,5): error TS2322: … -``` - -Line 4 is `alias: 'q'`; line 5 is `alias: 'ab'`. Both rejected. - -**Why it matters:** short aliases were round-1 F07, and every shipping CLI in -the corpus has them. As written, no command can declare one — the feature is -not merely unenforced, it is inaccessible. - -**Suggestion:** infer the alias as a type parameter and constrain it with a -template-literal recursion. Verified working — `'q'` compiles, `'ab'` errors, -omitting the field compiles: - -```ts -type Char = S extends `${string}${infer R}` - ? (R extends '' ? S : never) - : never - -// on each factory: -boolean(spec: { brief: string; alias?: A & Char }): FlagSpec -``` - -Alternatively drop the type and validate at construction — but the type -version works, so prefer it. - -### P02 — The outcome code is not typed against its catalogue — MUST FIX - -**Location:** §4, `present`'s `opts?: { readonly outcomeCode?: number }` -(line 275); §6, `outcomeCodes?: Readonly>` (line 451). - -**Issue:** the catalogue is declared on the definition and the selection -happens at the return site, but nothing connects the two at compile time. The -engine verifies at runtime, which means at the return site — after the command -has finished all of its work. - -**Why it matters:** a mistyped or stale outcome code (catalogue edited, call -site not) turns a command that ran correctly into an internal error at the -moment it was about to report success. That is the worst available time to -discover a one-character mistake, and it lands on the exit-code surface CI -branches on. It is also the last remaining place where the v3→v4 move cost -type safety that v2 had. - -**Suggestion:** thread the catalogue's key type. Verified working: - -```ts -// definition gains a fourth parameter, inferred from the catalogue literal: -export interface CommandDefinition { - readonly outcomeCodes?: Readonly> - // … -} -// context carries it; present narrows: -readonly present: (data: T, presentations: Presentations, - opts?: { readonly outcomeCode?: TOutcome }) => PresentedResult -``` - -`defineCommand({ outcomeCodes: { 4: 'drift', 5: 'stale' } })` infers -`TOutcome = 4 | 5`, and the compiler rejects a wrong code at the call site with -a message that names the valid ones: - -``` -error TS2322: Type '7' is not assignable to type '4 | 5 | undefined'. -``` - -That is exactly the error a product author wants. The runtime verification -stays as defence in depth. - -Two smaller points in the same area. The 4–99 range is still unenforced — -worth validating the catalogue's keys at construction, where it is cheap. -And whether an explicit `{ outcomeCode: 0 }` is legal when the catalogue omits -`0` is currently ambiguous; say that 0 is always legal and always means -completed-nominally. - -### P03 — Destructive-prompt safety is a convention where it could be a type - -**Location:** §4a, `PromptSurface` and its doc comment (lines 316–342). - -**Issue:** the rule is that `--yes` resolves a prompt to its declared default, -and that a prompt with no default halts. Safety for destructive operations -therefore rests entirely on the author remembering not to declare a default. -`confirm('Delete the production database?', { default: true })` compiles, and -under `--yes` it deletes without displaying anything. - -**Why it matters:** R5 exists because "convention (style guides, review -comments) demonstrably did not hold the line" on much lower-stakes things than -this. The failure is silent, it is in the blast-radius category rather than -the annoyance category, and it will be introduced by someone adding a default -for a good local reason without knowing the rule it interacts with. - -**Suggestion:** make destructiveness explicit rather than inferred from an -absence. A second method that structurally cannot take a default: - -```ts -readonly confirm: (q: string, opts?: { readonly default?: boolean }) => Promise> -/** Never auto-answered: no default, --yes cannot satisfy it. Pairs with the - * command's own --force / --confirm flag. */ -readonly confirmDestructive: (q: string) => Promise> -``` - -Now "a destructive confirm has no default" is enforced by the signature, and -the call site is self-documenting in review. Whether it is two methods or one -method with a required `destructive: true` is a shape choice — architect -referral — but the property should not stay a convention. - -Secondary, worth one line in the doc: adding an ordinary no-default prompt to -an existing command silently breaks every CI caller passing `-y`, because the -invocation now halts at exit 2. That is the correct behaviour, but it makes -"add a prompt" a breaking change, which is worth saying out loud. - -### P04 — `InputStream` is under-specified and probably wrong for `lsp` - -**Location:** §8, `export interface InputStream extends AsyncIterable {}` -(line 605). - -**Issue:** two unresolved questions. First, chunking: nothing says whether an -iteration yields an arbitrary chunk or a line. Second, encoding: it yields -`string`, so bytes have already been decoded. - -**Why it matters:** `defineRawCommand` exists for `lsp`, and an LSP server -over stdio parses `Content-Length: N\r\n\r\n` followed by exactly N **bytes**. -Decoded strings cannot be counted in bytes once any payload contains a -multi-byte character, so the framing parser cannot be written correctly. And -if the engine were to yield lines rather than chunks, framing breaks outright, -because LSP bodies contain newlines and the header/body boundary is -byte-counted, not line-counted. The one command this escape hatch was built -for may not fit through it. - -**Why it is a real risk rather than theoretical:** the ORM `lsp` command today -hands the process to `connection.listen()` and lets the language-server -library own the raw stdio stream. Under this interface the engine interposes a -decoded string iterator between them. - -**Suggestion:** type raw stdin as `AsyncIterable` — byte-exact and -still runtime-agnostic, since `Uint8Array` is a platform primitive rather than -a Node type. Offer a decoded string view separately if anything wants one. -Either way, state chunk-not-line semantics explicitly. Worth confirming -against the actual language-server entry point during the spike, since it is a -single known consumer and the answer is cheap to obtain. - -The matching question on the output side: `OutputStream.write(text: string): -void` returns nothing, so a raw command cannot know its writes have drained -before it returns an exit code. Because the engine sets `process.exitCode` -rather than calling `process.exit`, the runtime will flush naturally — so this -is fine, but it is fine by accident and deserves a sentence. - -### P05 — Flag defaults still do not narrow the flag's type - -**Location:** §5, `flag.string` / `flag.number` / `flag.enum` (lines 358–380). -Carried over from N05a. - -**Issue:** `flag.number({ brief, default: 900 })` returns -`FlagSpec`. - -**Why it matters:** the handler still writes `?? 900`, so the default is -declared twice and the two copies can drift — help text says one thing, the -handler falls back to another. It also removes the benefit that motivated -adding defaults in round 1. - -**Suggestion:** overloads. Verified working — with `default` present the value -is `number`, without it `number | undefined`: - -```ts -number(spec: { brief: string; placeholder?: string; default: number }): FlagSpec -number(spec: { brief: string; placeholder?: string }): FlagSpec -``` - -### P06 — Sessions have no defined end in json mode - -**Location:** §9, `Frame` and `ResultFrame` (lines 642–659); §6, session -definitions returning `Result`. - -**Issue:** the comment says json mode emits "events while running, then -exactly one result frame". A session has no presentation and no result, so -whether it emits a terminal `ResultFrame` is unstated. - -**Why it matters:** a machine consumer tailing `prisma log tail --json` needs -to know whether the stream ended cleanly, was aborted, or failed. Without a -terminal frame it can only infer that from the pipe closing, which cannot -distinguish a clean stop from a crash. The platform already solved this: its -`build logs` stream carries its own `terminal` record, and it sets -`emitJsonSuccessEvent: false` precisely because the wrapper's success event -would mislabel a failed stream as succeeded. - -**Suggestion:** state that a session also emits exactly one terminal -`ResultFrame` carrying `ok` (and the error when it errored), with no `result`. -That gives every json stream in the CLI the same shape: events, then one -terminal frame. - -### P07 — `ok` now means completed, not succeeded; that is a consumer-visible change - -**Location:** §9, `CompletedEnvelope.ok` (lines 611–615) and the header's -COMPLETED/ERRORED framing. - -**Issue:** an integrity failure in `migration check` is now `ok: true` with -`outcomeCode: 4` and exit 4. R6's stated rationale is that machine consumers -"branch on `ok`, `code`, and exit codes"; the answer to "did the check pass?" -is now `ok && outcomeCode === 0`, a two-field test where every existing -consumer performs a one-field test. - -**Why it matters:** I think the semantics are right — "bad news is a result, -not an error" is a genuinely better model, and it is what lets a completed -result render through the normal presentation path. But it is a change in what -`ok` means, and the class of consumer most likely to get it wrong is an agent -writing the obvious `if (result.ok)`. The mitigation is already in place and -is the reason I am not grading this as a defect: `outcomeCode` is a -**required** field on `CompletedEnvelope`, always present, so the correct test -is always available and never `undefined`. - -**Suggestion:** no interface change. Document the meaning of `ok` explicitly -where consumers will read it (the json contract docs, not only this file), and -note the specific migration: ORM `migration check` consumers branching on `ok` -must move to `outcomeCode`. Worth one line in whatever release note covers the -json surface. - -### P08 — The event buffer has no stated bound or drop policy - -**Location:** §1, "the engine buffers and writes asynchronously (no -backpressure signal — accepted trade)" (lines 96–97). - -**Issue:** accepting the trade is reasonable — a `void` return keeps `report` -trivial to call and the alternative complicates every emit site. But an -unbounded buffer in front of a slow consumer is a memory failure mode, and -`log tail | slow-consumer` is a real invocation. - -**Why it matters:** it is the one remaining way a well-behaved command can -take down the process, and it fails at exactly the moment a user is debugging -something else. - -**Suggestion:** state a bound and what happens at the bound. There is -precedent in the corpus for the honest answer: Composer's `LogEvent` union -already carries `lines-dropped { count }`. Dropping with a visible count is -better than growing without limit, and the vocabulary for saying so exists. - -### P09 — `CommandHandler` still covers only value commands - -**Location:** §6, `CommandHandler` (lines 469–471). Carried over from N09. - -**Issue:** the conditional matches `CommandDefinition` only, so session and -raw implementation files hand-write their handler signatures. - -**Why it matters:** hand-written signatures drifting from their definitions is -exactly what F02 was fixed to prevent; the fix simply was not extended to two -of the three command kinds. - -**Suggestion:** extend the conditional to all three, or ship `SessionHandler` -and `RawHandler` alongside. - -### P10 — `--quiet`, `--log-level` and json mode have unstated interactions - -**Location:** header lines 39–49; §2's materialization table (lines 194–196). - -**Issue:** the table covers `human`, `human + --quiet`, and `json`. It does not -cover `json + --quiet`. Separately, `--quiet` and `--log-level` now overlap: -`--quiet` suppresses presentation, `--log-level error` suppresses commentary, -and `--quiet --log-level verbose` has no stated meaning. - -**Why it matters:** minor, but two people will implement it two ways, and it -is one sentence to prevent. - -**Suggestion:** state that json mode wins over `--quiet`, and that `--quiet` -governs presentation while `--log-level` governs commentary, so the two -compose rather than conflict. - -### P11 — Remaining nits - -Genuinely small; listing them so they are dispositioned rather than lost. - -1. **`CommandSet` and `MountedTree` are the same type.** Both are - `Readonly>`, so the comment's "distinct alias so - the two maps never read as one" is documentation only — a by-name map is - assignable where a by-path map is expected. Branding one would make it real; - leaving it is defensible. -2. **`SectionValidation`'s `ok: true` diagnostics have no stated fate** (N12). - Presumably they render as warnings and join the envelope's `warnings`; say - so, or they will be silently dropped. -3. **The harness does not capture which prompts were asked** (N11 residual). - With prompt defaults now load-bearing for safety, a test cannot assert that - a destructive confirmation was actually displayed — only that something - consumed an answer. -4. **`requiresCredentials` is absent from `RawCommandDefinition`.** Probably - deliberate for `lsp`; worth confirming rather than inheriting by omission. -5. **`createCli` still says "build time, not run time"** (F23) while remaining - a function that can only throw when called. -6. **`LogLevel = Severity`** makes the level axis and the item axis the same - type, so a `Severity` is assignable wherever a `LogLevel` is wanted. Harmless - today; they may diverge later. -7. **`LoadedConfig` still cannot distinguish "no config file" from "config file - present but empty"** (round-2 N12 residual). Both yield `config: undefined` - for a command that needs a section, so the handler produces the same error - for two situations with different fixes. - -## Addendum — the `errors` Block (operator amendment, landed mid-review) - -`Block` gained `{ kind: 'errors'; errors: ReadonlyArray }` -for structured errors carried inside a COMPLETED result, engine-rendered with -the top-level error layout. - -**The intent is right and the rendering half is a clear win.** "Products never -hand-build error presentation" is R5 applied to the one place it had escaped: -`migration check` today hand-formats `✗ [CODE] where: why` plus a `fix:` line, -and the survey ranks structured-error-with-code/why/fix as the single most -uniform structure in the corpus (3/3 families). Having the engine render that -layout in a completed result, identically to how it renders a top-level error, -is exactly the consistency R5 exists for. I would keep the concept. - -The mechanism has one structural problem and one coverage gap. - -### P12 — The same error list must be written two or three times, and the copies cannot be reconciled - -**Location:** §7, `Block.errors` and its doc ("In the data/json side, carry the -same errors as their envelopes (`toEnvelope()`)"). - -**Issue:** `Block` appears only in `Presentations.human`. So the human side -gets `CliStructuredError` instances via the block, and the data side must -carry the same errors again, converted by hand with `toEnvelope()`. Under -`--quiet` — where only `stdout` materializes — they must be written a third -time or they vanish. - -Nothing couples the copies, and **nothing can**: only the active format's -presentation functions run, so in json mode the `human` function is never -invoked and the block never exists. There is no moment at which both lists are -in memory to be compared, by the engine or by a test. A handler that filters, -truncates, or forgets to update one side produces a human reading and a -machine reading that disagree about what the command found, silently and -undetectably. - -Three concrete consequences: - -1. **Forgetting `toEnvelope()` fails quietly rather than loudly.** - `CliStructuredError` extends `Error`, and `message` is non-enumerable on - `Error`, so `JSON.stringify` of a raw instance drops it while keeping the - custom own fields. The output is a plausible-looking payload missing its - summary — not an obvious failure. The instruction to convert is a doc - comment with no type behind it. -2. **It breaks §2's own invariant.** `PresentedResult` is described as "data - all the way down (serializable, snapshotable, no callbacks)". A live - `CliStructuredError` is not plain data: it carries a `cause` chain that can - reference arbitrary objects and a stack. Every other `Block` is strings and - rows. The harness exposes `presented` for "semantic assertions without - byte-scraping" — snapshotting one containing error instances gives unstable - output. -3. **`db verify --quiet` regresses.** That command deliberately renders drift - even under `--quiet` (survey §A1). Under the current materialization rule - `--quiet` builds only `stdout`, so an `errors` block is never constructed - and the drift disappears — unless the handler duplicates it into the stdout - lines as well. - -**Suggestion:** let the engine own the list once, format-independently. Either - -```ts -ctx.present(data, presentations, { outcomeCode: 4, errors: findings }) -``` - -or a format-independent member on `Presentations` (`errors?: () => -readonly CliStructuredError[]`) that the engine always materializes. Either -way the engine renders them in human mode with its layout, serializes their -envelopes into the result envelope itself, and emits them under `--quiet` -according to one stated rule. Declared once, no `toEnvelope()` in product -code, no drift possible, and `PresentedResult` goes back to being plain data. - -Note that the obvious cheaper fix — "the engine scans the human blocks for an -`errors` block and serializes what it finds" — does not work, precisely -because `human` is not invoked in json mode. That asymmetry is the argument -for pulling the declaration out of the presentation functions entirely. - -**Also worth one line:** state whether the rendered error includes `meta`. The -corpus masks credentials in connection URLs deliberately -(`maskConnectionUrl`, `URL_CREDENTIALS_PATTERN`), and a drift or verification -error's `meta` is a plausible place for one to appear. `fields` already has -`sensitive`; the errors block has no equivalent, so the masking policy should -be stated as the engine's. - -### P13 — Completed-with-errors covers the non-zero cases; the ERRORED side cannot express multiple errors - -**Location:** §9, `ErroredEnvelope.error` (singular); §4, `CommandContext.config` -("the engine already failed the command with that section's diagnostics", -plural); §3, `SectionValidation.diagnostics` (an array). - -Working through what products exit non-zero for today: - -| Case | Covered? | -|---|---| -| `migration check` integrity failure (exit 4) | **Yes** — the motivating case, and a good fit: its `failures[{space, code, where, why, fix}]` already has the structured-error shape. | -| `db verify` drift (exit **1** today) | **Yes**, with a deliberate renumbering to an outcome code — exit 1 is now "bug only". Same class as the already-agreed "Compute's 1 renumbers to 2". Needs listing in migration notes; and see P12.3 for the `--quiet` interaction. | -| `db sign` verify failure (exit 1) | Yes, same renumbering. | -| Composer spawned-engine exit-status passthrough | **No.** A child exiting 137 cannot map into 4–99. The settled contract has "one documented exception" for this; v4's exit table does not mention it. Carried-over gap, not caused by this amendment. | -| A failure carrying *many* structured errors | **No** — see below. | - -**Issue:** `ErroredEnvelope.error` is singular. But the engine's own R10 path -is plural: `SectionValidation` returns `diagnostics` as an array, and the -context doc promises the engine fails the command "with that section's -diagnostics". A config file with three invalid fields has three diagnostics -and one envelope slot. - -**Why it matters:** the engine cannot express its own most likely failure. The -workarounds are both bad — pick one diagnostic and drop the rest, or nest the -others in `meta` where no consumer knows to look — and R10's whole point is -that a user's config typo becomes a good diagnostic rather than a stack trace. -Three typos should not become one diagnostic. - -It also creates a perverse incentive now that the completed path renders -errors well: a command with several genuine failures is better off returning -`ok` with an errors block and an outcome code than returning `notOk`, purely -because the completed path can show all of them. That would make `ok` mean -"had something to display" rather than "completed", which undoes P07's -semantics. - -**Suggestion:** allow the errored envelope to carry a list — either -`error` plus an optional `additionalErrors`, or make the field a non-empty -array. The human rendering already exists (it is the same layout the `errors` -block uses), so this is an envelope change rather than a rendering one. - -### P14 — Two ways to show an error in human mode - -**Location:** §7, `Block.summary` with `tone: 'error'` versus `Block.errors`. - -Minor, and I raise it only so it is dispositioned: a product can render an -error condition either as a `summary` block with an error tone or as an -`errors` block. The intent is clearly that `errors` is for structured -`CliStructuredError` values and `summary` is for prose, but nothing says so, -and the corpus's history is that two ways of expressing one thing diverge. -One sentence in the `Block` doc is enough. - -## Deferred - -Unchanged and still correctly out of scope: config-section registration -mechanics beyond the token, daemon management (mode 5), autocomplete, -telemetry hooks, and duration/timing events. Nothing new joined this list in -v4. - -## Acceptance-criteria verification - -Same strict verdicts throughout. **PASS** = the interface structurally -satisfies or enforces the requirement. **WEAK** = satisfiable, but the shape -does not enforce it. **FAIL** = the shape contradicts or cannot express it. -**NOT VERIFIED** = cannot be assessed from the draft. - -| R | Requirement | r1 | r2 | r3 | Detail | -|---|---|---|---|---|---| -| R1 | One language, directly executable | PASS | PASS | **PASS** | Unchanged. The declared object is what runs; `kind` is stamped by `define*` rather than hand-written, which removes a way to get it wrong. I compiled the question this raised — whether `Omit<…, 'kind'>` on the parameter breaks inference of `TFlags` from the `flags` literal — and it does not. | -| R2 | Commands end in typed operation calls | WEAK | WEAK | **WEAK** | Unchanged, and unchangeable from here: the shape is compatible and the thin handler is the natural one, but nothing structurally prevents business logic in a handler. This is a review-and-lint requirement, and I would stop expecting the interface to carry it. | -| R3 | The engine package is the whole contract | WEAK | WEAK | **WEAK** | Substantially improved. `NodeJS.*` is gone from the public surface, replaced by structural `OutputStream`/`InputStream`, which closes the runtime-agnosticism half; and `NextAction` moving to the foundation closes the package cycle. No third-party type appears anywhere, so R3's actual rationale — bounding third-party exposure, keeping internals replaceable — is fully satisfied. The one residual is literal rather than substantive: products still import two of *our* packages, because `Result`, `CliStructuredError` and `NextAction` live in the foundation. Re-exporting them from the engine makes this PASS and costs one line. Whether the engine should re-export or R3's wording should acknowledge the foundation is an architect call. | -| R4 | Products receive a context, never the environment | PASS | PASS | **PASS** | Improved again: `packageManager` gives handlers the one environmental fact they needed for R13 phrasing without reading the environment, and `requiresCredentials` removes the most common reason a handler would reach for anything else. Nothing in the context touches disk, env or TTY. | -| R5 | Products have no presentational API | PASS | PASS | **PASS** | Now exception-free, which it was not in v3. Removing the `exitCode` callback means the header's "nothing product-authored executes after the handler resolves" is literally true. `Block` remains the only vocabulary, `Ui` cannot write, and the `--verbose` question was answered by a log-level mechanism rather than a second presentation surface — so no new rendering authority reached products. | -| R6 | Errors and results follow the settled conventions | FAIL | WEAK | **WEAK** | Close to PASS and blocked on one thing. Everything expressible is now correct: the full exit-code table including 4–99 and 130/143, second-signal force-exit, prompt cancellation mapped to 3 versus unavailability to 2, `Result` throughout, and an outcome catalogue that renders in help without executing anything. Two things keep it WEAK. First, the code selected at the return site is typed `number` rather than against that catalogue, so validity is enforced at runtime after the work is done (**P02**); I verified the typed version compiles and produces `Type '7' is not assignable to type '4 \| 5 \| undefined'`. Second, the mid-review `errors` Block amendment exposed that `ErroredEnvelope.error` is singular while the engine's own config-diagnostics path is plural, so a multi-error failure cannot be expressed on the errored side (**P13**). Both are envelope/type edits rather than shape changes; fixing them moves R6 to PASS. | -| R7 | Product-repo end-to-end tests are first-class | WEAK | PASS | **PASS** | Held and improved: `cwd` and the `onEvent` live tap close the two residuals that mattered, so a session test can assert mid-run state and abort on a condition rather than a timer. Remaining gap is prompt capture (P11.3), which narrows what a test can assert about the new prompt-default safety rule but does not affect which commands are testable. | -| R8 | The shell's test burden is integration proof | NOT VERIFIED | NOT VERIFIED | **NOT VERIFIED** | Still an allocation-of-work requirement with no interface surface. Nothing obstructs it; assess against the shell's test plan. | -| R9 | Static tree, lazy guts | PASS | PASS | **PASS** | The lazy `handler` is unchanged. The one leak I flagged in round 2 — validators loading with the definition tree — is now documented at the point of use ("keep validators dependency-light"). Convention rather than structure, but stated where an author will read it, and the `outcomeCodes` catalogue being plain data means help still renders without executing product code. | -| R10 | One config file, validated by its products, never a crash | FAIL | PASS | **PASS** | Held. The `ConfigSection` token still couples name, type and never-throwing validator; `configSection` still binds a command to exactly one section, which is what makes "fails only if a section it needs is invalid" real. Residual nits only: the fate of non-fatal diagnostics on the success branch, and no-file versus empty-file being indistinguishable (P11.2, P11.7). | -| R11 | Pinned versions, tandem releases | NOT VERIFIED | NOT VERIFIED | **NOT VERIFIED** | Release-process requirement, no interface surface. | -| R12 | The shell defines the command tree | PASS | PASS | **PASS** | Held. No path appears in any definition; `MountedTree` names the by-path map at the mount site. The two map aliases being structurally identical (P11.1) is a documentation-versus-type nit, not a requirement gap. | -| R13 | The CLI never touches a package manager | WEAK | WEAK | **PASS** | Upgraded, partly on v4's change and partly correcting my own round-2 reading. The prohibition holds absolutely — nothing installs or vendors. R13's positive half asks that *the command* check at execution time and return a structured error naming the dependency and how to install it with the user's own package manager. `probeDependency` provides the check and `packageManager` (new in v4) provides the last missing fact for the install phrasing, so a product can now satisfy the requirement exactly as worded. My round-2 WEAK penalised product-authored error text, which is what R13 specifies rather than a deviation from it. | -| R14 | One event vocabulary, engine-defined, with product extensions | PASS | PASS | **PASS** | Held and tidied. The `Frame` union with a distinct `ResultFrame` removes the double-`data` awkwardness and makes the json stream self-describing. The vocabulary itself is unchanged from v3's already-strong state: nesting ids, data/diagnostic channel routing, `artifact`, `from`, one severity scale, and `data?: unknown` as the uniform untouched extension point. One unstated case: whether a session emits a terminal frame (**P06**). | - -### Summary counts - -| Verdict | r1 | r2 | r3 | Requirements (r3) | -|---|---|---|---|---| -| PASS | 6 | 8 | **9** | R1, R4, R5, R7, R9, R10, R12, R13, R14 | -| WEAK | 4 | 4 | **3** | R2, R3, R6 | -| FAIL | 2 | 0 | **0** | — | -| NOT VERIFIED | 2 | 2 | **2** | R8, R11 | -| **Total** | 14 | 14 | **14** | | - -Movement since round 2: R13 WEAK → PASS. No regressions. - -Of the three remaining WEAKs, one is closable by a verified edit (R6, via -P02), one is closable by a re-export or a wording decision (R3), and one is -not an interface property at all (R2). There is no requirement left that this -interface cannot express. - -## Verdict - -Not clean yet, and I want to be precise about the gap rather than round it in -either direction. - -**Two must-fix defects:** P01 (`SingleChar` rejects every string — the alias -feature is inaccessible) and P02 (the outcome code is not typed against its -catalogue, so a wrong code fails at runtime after the command has done its -work). Both are single-declaration edits, both fixes are compiled and verified -in this review, and neither changes a shape. - -**One safety property I would make structural before shipping:** P03, the -destructive-prompt convention. - -**One contract to settle against its only consumer:** P04, `InputStream` for -`lsp`. - -**One amendment to finish landing:** the `errors` Block is the right idea with -the wrong placement. Declared inside `Presentations.human`, the same error list -has to be written two or three times and the copies cannot be reconciled — -in json mode the human function never runs, so nothing can cross-check them -(P12). Moving the declaration out of the presentation functions, so the engine -renders *and* serialises one list, fixes it without giving up anything the -amendment was after. P13 is its companion on the errored side: the envelope -holds one error while the engine's own config path produces several. - -Everything else — P05 through P11 and P14 — is small, and P07 needs -documentation rather than an interface change. - -**The design is settled.** Rounds 1 and 2 found structural problems; round 3 -found two broken type declarations, a safety convention, and one misplaced -amendment. If P01, P02 and P12 are fixed and P03, P04 and P13 are ruled on, I -would sign this off without another review pass. diff --git a/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r4-closure.md b/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r4-closure.md deleted file mode 100644 index 5293a4c7..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r4-closure.md +++ /dev/null @@ -1,163 +0,0 @@ -# Round 4 — closure check (v5) - -Reviewer pass: principal engineer. Scope: compile-verify the two type -mechanics, read the four closure changes for regressions, state whether the -loop closes. Prior artifacts: `code-review.md`, `-r2.md`, `-r3.md`. - -## Verdict - -**Both compile-checks pass. No regressions found. The loop is closed from my -lens.** - -Every must-fix and should-rule-on item from round 3 is resolved, and two of -them are resolved better than I proposed. What remains is four small items I -had already graded as nits or documentation, listed at the end so they are -dispositioned rather than lost. None of them needs another review pass. - -## (a) Alias mechanics — PASS - -Tested against v5's declarations verbatim (`Char` plus -`alias?: A & Char` with `A extends string = never` on each builder). -TypeScript 5.6, `--strict`. Empty output — every assertion held: - -| Case | Expected | Result | -|---|---|---| -| `flag.boolean({ brief, alias: 'f' })` | compiles | ✓ | -| `flag.string({ brief, alias: 'q' })` | compiles | ✓ | -| `flag.boolean({ brief })` — no alias, `never` default | compiles | ✓ | -| `flag.enum({ brief, values: ['a','b'], alias: 'F' })` | compiles | ✓ | -| `alias: 'ab'` | rejected | ✓ | -| `alias: ''` | rejected | ✓ | -| `alias: 'data-proxy'` | rejected | ✓ | -| `FlagSpec<'a' \| 'b' \| undefined>` from the enum | inference survives the added `A` parameter | ✓ | - -The two cases I specifically wanted to confirm both hold: **omitting the alias -compiles** (the `= never` default does not poison the call, which was the -failure mode of my own first attempt in round 3), and **the empty string is -rejected** (`Char<''>` is `never`, since `''` does not match -`` `${string}${infer Rest}` ``). P01 is closed. - -## (b) `TCode` threading — PASS - -Tested through the realistic round trip, not just the direct call: catalogue on -the definition → `Omit<…, 'kind'>` in `defineCommand` → `CommandHandler` in a separate handler file → `ctx.present`. Empty output: - -| Case | Expected | Result | -|---|---|---| -| `outcomeCodes: { 4: '…', 5: '…' }` infers `TCode` | `4 \| 5`, both directions | ✓ | -| `ctx.present(…, { outcomeCode: 4 })` / `5` | compiles | ✓ | -| `ctx.present(…)` with no opts | compiles (omitted = 0) | ✓ | -| `ctx.present(…, { diagnostics: […], outcomeCode: 4 })` | compiles | ✓ | -| `ctx.present(…, { outcomeCode: 7 })` | rejected | ✓ | -| No catalogue declared → any `outcomeCode` | rejected (`TCode = never`) | ✓ | - -Inference survives both the `Omit<>` wrapper and the second inference site -created by `handler`'s mention of `TCode`, which was the thing worth checking. -The no-catalogue case falling out as `never` is a bonus: a command that never -declared outcome codes cannot select one, so the catalogue is not merely -advisory. - -**Control run** to prove the negative assertions are load-bearing rather than -vacuous: swapping the invalid `7` for a valid `5` makes the compiler report -`error TS2578: Unused '@ts-expect-error' directive` — confirming that in the -real test `outcomeCode: 7` genuinely errored. P02 is closed. - -## Regression read of the other closure changes - -**P12/C8 — diagnostics declared once at `ctx.present`.** Clean, and it fixes -more than I asked. `Block`'s `errors` member is gone (0 occurrences), -`PresentedResult.diagnostics` is the single declaration, and the engine both -renders them in human mode and serializes their envelopes into -`CompletedEnvelope.diagnostics`. The drift I flagged is now structurally -impossible rather than merely discouraged, and `PresentedResult` is plain data -again except for the diagnostics themselves, which the engine converts. The -`--quiet` regression I identified (`db verify` deliberately shows drift even -when quiet) is explicitly handled — "shown even under --quiet". The -`notOk`-versus-diagnostics test in the doc ("notOk when the command couldn't do -its job; diagnostics when finding these WAS the job") is the right line and is -stated where an author will read it. - -One observation, not a defect: the guardrail — a severity-`error` diagnostic -requires a non-zero outcome code — is a runtime check firing at the return -site. That is the same late timing as the old P02, but unlike P02 it cannot be -typed, because it depends on the severity of runtime error values. A runtime -check is correct here. Worth making the engine's message for it explicit about -which of the two fixes the author wants (raise the outcome code, or move the -finding to `notOk`), since it fires after the work is done. - -**P13 — errored diagnostics.** Symmetric with the completed side, `error` -retained as the primary. The engine can now express its own R10 failure: three -config typos serialize as three diagnostics rather than one flattened error. -The perverse incentive I flagged — returning `ok` purely to display several -problems — is gone, so `ok` keeps meaning "completed". - -**P03 — `prompt.consent`.** Structurally undefaultable: no `opts` parameter -exists, so `--yes`, Enter-through and non-interactive contexts cannot satisfy -it. The operator's reframe from "destructive" to "explicit consent" is the -better framing — the property that matters is that the answer is not -inferable, which is broader than damage and easier to apply correctly at a -call site. `confirm` keeps its default for ordinary questions, so the two are -distinguishable by name at the point of use. - -**P04 — `InputStream`.** `AsyncIterable` with an optional -`setRawMode`. Byte-exact, so `lsp`'s `Content-Length` framing can be -implemented correctly; decoding is explicitly the consumer's business; and -`setRawMode` being optional is right, since it is a platform capability rather -than a guarantee. Note the engine's own prompt machinery decodes internally, -which keeps the byte-level type from leaking into the prompt surface. - -**ADR 239 amendment.** Recording it in the header as an implementation -prerequisite is the right call — it is the one change here that lands outside -this repo, and the promise analogy states the model more clearly than the -prose around it did. Worth carrying that sentence into the ADR itself. - -## Still open (nits, no further review needed) - -Listing these only so the closure is honest about what was not touched. All -were graded nit or documentation in round 3. - -1. **P05 — flag defaults still do not narrow.** `flag.string({ default })` - still returns `FlagSpec` (no overloads present), so a - defaulted flag keeps its `?? default` in the handler and the value is - declared twice. Verified fix is in `code-review-r3.md`; worth taking when - someone is next in the file. -2. **P06 — sessions have no stated terminal frame** in json mode, so a machine - consumer cannot distinguish a clean stop from a crash except by the pipe - closing. One sentence. -3. **P09 — `CommandHandler` still matches only `CommandDefinition`**, so - session and raw implementation files hand-write their signatures. -4. **P10 — `json + --quiet` precedence** still unstated; the materialization - table covers `human + --quiet` only. - -Plus the round-3 P11 nit list (the two map aliases being structurally -identical, the fate of `ok: true` section diagnostics, prompt capture in the -harness, `requiresCredentials` absent on raw, `createCli`'s "build time" -wording, `LogLevel = Severity`, no-file versus empty-file in `LoadedConfig`). - -## Acceptance criteria - -Unchanged from round 3 except R6, which the two closures resolve. - -| Verdict | r3 | r4 | Requirements | -|---|---|---|---| -| PASS | 9 | **10** | R1, R4, R5, R6, R7, R9, R10, R12, R13, R14 | -| WEAK | 3 | **2** | R2, R3 | -| FAIL | 0 | **0** | — | -| NOT VERIFIED | 2 | **2** | R8, R11 | - -**R6 WEAK → PASS.** Both reasons it was held are gone: the outcome code is now -typed against its catalogue (compile-verified above), and the errored envelope -carries multiple diagnostics, so the engine can express its own config-failure -case. The exit-code table, prompt cancellation mapping, and second-signal -behaviour were already in place. - -The two remaining WEAKs are the same as before and neither is an interface -defect: **R2** is a review-and-lint property no interface can carry, and **R3** -is the one-line question of whether the engine re-exports the foundation's -`Result` / `CliStructuredError` / `NextAction` so products import one package -instead of two — an architect call, not an engineering gap. **R8** and **R11** -remain process requirements with no interface surface. - -No requirement remains that this interface cannot express, and no requirement -is contradicted by its shape. diff --git a/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r5-delta.md b/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r5-delta.md deleted file mode 100644 index 7f6631a1..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review-r5-delta.md +++ /dev/null @@ -1,164 +0,0 @@ -# Round 5 — v7 delta check - -Reviewer pass: principal engineer. Scope: compile-verify the three type -mechanics, regression-read the restructuring, confirm closure still holds. -Prior artifacts: `code-review.md`, `-r2.md`, `-r3.md`, `-r4-closure.md`. - -## Verdict - -**All three mechanics compile as specified, in both directions, with controls. -No regressions. Closure holds.** - -Two of the v7 changes make the interface meaningfully stronger than v5, not -merely equivalent: the `Outcome` conditional turns "did you pick an exit code?" -into a compile error at every return site of a catalogued command, and -`requireDependency` moves the missing-dependency prose from products into the -engine. Both close things I had accepted as convention or as product -responsibility in earlier rounds. - -## (1) `Outcome` — PASS, both directions - -Tested verbatim through the full round trip (`defineCommand` inference → -`CommandHandler` → `ctx.present`). TypeScript 5.6, `--strict`. -Empty output; every assertion held. - -Catalogued command (`exitCodes: { 4: '…', 5: '…' }`): - -| Case | Expected | Result | -|---|---|---| -| `TCode` inferred | `4 \| 5`, both directions | ✓ | -| `present({ data, exitCode: 4 })` / `5` / `0` | compiles | ✓ | -| `present({ data, exitCode: 5, diagnostics: […] })` | compiles | ✓ | -| `present({ data })` — **exitCode omitted** | **rejected** | ✓ | -| `present({ data, exitCode: 7 })` | rejected | ✓ | - -Uncatalogued command (no `exitCodes`): - -| Case | Expected | Result | -|---|---|---| -| `present({ data })` | compiles | ✓ | -| `present({ data, diagnostics: [] })` | compiles | ✓ | -| `present({ data, exitCode: 4 })` | **rejected** | ✓ | - -**Controls.** All three negative assertions were re-run with the offending -value corrected, and each produced `error TS2578: Unused '@ts-expect-error' -directive` — proving the assertions are load-bearing rather than vacuously -passing: - -``` -cA.ts(82,3): error TS2578: Unused '@ts-expect-error' directive. # exitCode supplied → required-ness assertion was real -cB.ts(84,3): error TS2578: Unused '@ts-expect-error' directive. # 7→5 → out-of-catalogue assertion was real -cC.ts(100,3): error TS2578: Unused '@ts-expect-error' directive. # exitCode dropped → forbidden-ness assertion was real -``` - -The `[TCode] extends [never]` guard behaves correctly for an inferred union -(`4 | 5` takes the required branch) and for the default (`never` takes the -forbidden branch). This is the strongest form of the P02 fix: v5 made a wrong -code a compile error; v7 makes a *missing* code one too, so a command that -documents outcomes cannot silently exit 0 from a path the author forgot about. - -## (2) `TConfig` through the grouped `needs` — PASS - -`needs: { config: checkSection }` with `ConfigSection` still flows to -`ctx.config`. Both `const cfg: CheckCfg = ctx.config` and -`ctx.config.strict` typecheck. Nesting the inference site one level deeper -inside an optional property does not break it. - -## (3) `ctx.config: TConfig` exactly — PASS - -The no-config case types correctly: with no `needs.config`, `TConfig` defaults -to `undefined` and `const c: undefined = ctx.config` compiles. So dropping -`| undefined` does not strand commands that need no config. - -Worth stating plainly, because it is a real shift in responsibility: absence is -now the validator's to model. A product whose section is genuinely optional -must type it as `T | undefined` and have its validator return that for absent -input. That is the right owner — the product knows whether absence is legal — -but it is a rule that lives only in the doc comment, and a validator typed `T` -that receives `undefined` will produce a confusing failure. One sentence in the -`ConfigSection` docs about the validator's input including `undefined` would -close it. Nit. - -## (4) Regression read - -**`help` / `args` / `needs` grouping.** Applied consistently across all three -kinds — `CommandDefinition`, `SessionCommandDefinition`, `ServerCommandDefinition` -each carry `help: HelpSpec`, `args?: ArgsSpec`, `needs?: NeedsSpec`. The -discriminants (`result-command`, `session-command`, `server-command`) are -intact and stamped by the `define*` functions via `Omit<…, 'kind'>`, so N03 -stays closed. `raw` → `server-command` is a rename; naming is the architect's. - -**`ProductManifest` + `createCli(products)`.** Good structural gain: the -manifest ties a product's config section to its commands, and the doc says -foreign-section references fail construction. That makes R10's "a command only -needs its own product's section" checkable at build time rather than trusted. -Note the association between a `MountedTree` entry and its manifest entry is a -construction-time check, not a type-level one — consistent with the existing -posture on collisions and grammar, so no new concern. - -**`requireDependency` replacing `probeDependency`.** This is the best change in -v7. The engine now returns its own structured missing-dependency error with the -install command phrased from the detected package manager, and the handler just -passes it to `notOk`. `packageManager` correctly disappears from -`CommandContext` (it survives only on `Runtime` and the test spec), because -products no longer need it. My round-2 F26 residual — every product authoring -its own install prose, drifting per product — is now structurally impossible. -R13 still holds: the *command* returns the error, as the requirement words it; -only the wording moved to the engine. - -**`Credentials` trimmed to `{ token }`.** No regression — nothing in the -interface consumed `workspaceId`, and treating workspace selection as session -state owned by the auth library is coherent. - -**`PresentedResult.exitCode` / `diagnostics` now required.** Correct: the value -is engine-constructed, so both are always populated, and test code reading -`presented` no longer handles `undefined`. The optionality that remains is on -the *input* (`Outcome`), which is where it belongs. - -**`Diagnostic` as a distinct foundation type.** Pure data, never thrown, no -stack — this resolves the round-3 P12 concern about live `Error` instances -crossing the boundary more cleanly than v5 did. `PresentedResult` is now plain -data throughout, so harness snapshots are stable. - -**`NeedsSpec.interaction`.** The comment explaining why this is a mechanical -precondition and deliberately *not* an agent barrier — "the client's nature is -unverifiable, and a flag claiming to exclude agents would be a false guarantee" -— is exactly right, and worth keeping verbatim; it forecloses a bad feature -request permanently. - -One nit: `NeedsSpec` is shared across all three kinds, so a -`ServerCommandDefinition` can declare `needs.interaction` even though prompts -do not apply to server commands. Harmless, unenforceable in the current shape, -and not worth restructuring for. - -## Still open - -Unchanged from the round-4 list, all previously graded nit or documentation: -P05 (flag defaults do not narrow — verified fix on file), P06 (no stated -terminal frame for sessions in json mode), P09 (`CommandHandler` matches -only `CommandDefinition`, so session and server impl files hand-write their -signatures), P10 (`json + --quiet` precedence), plus the P11 nit list. Two -small additions from this round: the validator-input-includes-`undefined` -sentence above, and `NeedsSpec.interaction` on server commands. - -## Acceptance criteria - -Unchanged from round 4. - -| Verdict | Count | Requirements | -|---|---|---| -| PASS | 10 | R1, R4, R5, R6, R7, R9, R10, R12, R13, R14 | -| WEAK | 2 | R2, R3 | -| FAIL | 0 | — | -| NOT VERIFIED | 2 | R8, R11 | - -R6 and R13 both strengthened within their existing PASS — R6 because a missing -exit code is now a compile error, R13 because the install prose moved to the -engine. R10 strengthened via manifest-checked section ownership. The two -WEAKs are the same non-defects as before: **R2** is a review-and-lint property -no interface can carry, and **R3** is the one-line question of whether the -engine re-exports the foundation types (now four: `CliStructuredError`, -`Result`, `NextAction`, `Diagnostic`) so products import one package rather -than two — an architect call. - -Closure holds. No further review pass needed from my lens. diff --git a/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review.md b/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review.md deleted file mode 100644 index 367f54b6..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/reviews/code-review.md +++ /dev/null @@ -1,659 +0,0 @@ -# Code review — unified CLI engine public interface (draft) - -Reviewer pass: principal engineer (failure modes, operability, blast radius, -cost vs. benefit, constraints vs. assumptions). Naming, typology and overall -system shape are the architect's pass; where I hit one of those I say so and -move on. - -Subject: `wip/designs/engine/engine-interface-draft.ts` (snapshot 2026-08-09). - -Sources read in full: `cli-engine-requirements.md` (R1–R14), -`output-modes-survey.md`, `stricli-vs-clipanion.md`, the platform CLI shell -(`command-runner.ts`, `output.ts`, `runtime.ts`, `prompt.ts`, -`global-flags.ts`, `errors.ts`, `next-actions.ts`), Composer's -`operations/dev.ts`, `operations/shared.ts`, `dev/run-dev.ts`, and -`wip/designs/1a/design.md` (the host–product contract, whose §7.6 exit-code -table I treat as settled). - -## Summary - -The execution protocol at the centre of this draft is the right one. "Events -while running, one `Result` at the end, presenters turn the `Result` into -bytes" is a direct generalisation of the platform CLI's proven presenter -choke point, and it absorbs the ORM's progress spans and Composer's typed -event unions without contorting either. The `Block`/`Ui` pair genuinely makes -rendering impossible from a product, which is what R5 asks for. The lazy -`handler` loader is a one-to-one fit with stricli's `loader`. That core is -sound and I would build on it. - -The draft is not yet buildable as written, for three separate classes of -reason. - -First, the TypeScript does not compile in the ways it needs to. `ArgsOf` -silently drops all positionals because `positionals` is optional; a concrete -`CommandDefinition` is not assignable to the bare `CommandDefinition` that -`CommandSet` and `createCli` require; the brand symbols are unexported, which -breaks declaration emit; and typing a lazily loaded handler against its own -definition is circular. These are findings F01–F05 and they all have the same -fix: make `ArgsOf` take the flags and positionals objects rather than the -whole definition, and introduce an erased command type for mounting. - -Second, several things the shipping CLIs demonstrably do cannot be said in -this vocabulary at all. There is no number flag and no flag default, so -`app domain wait --timeout 15m` has to parse its own string, which pushes -parse-time validation into handlers and straight past R6. There are no short -aliases, so `-q`, `-y`, `-v`, `-f` all disappear. Exit codes are stated as -0/1/2/3 while the settled contract table is 0/1/2/3/4–99/130/143, and -`migration check` already ships a 4 and the platform already ships a 130. -`Runtime` has no `env` and no `isTty.stdout`, so the engine cannot implement -its own CI detection or the deliberately-kept auto-JSON-on-non-TTY behaviour -without reaching around the injection seam — which is exactly what makes R7's -in-repo tests trustworthy. The success envelope slots the platform puts on -every command (`warnings`, `nextSteps`, `nextActions`) have nowhere to live, -and remediation is the second most recurring structure in the survey. - -Third, the streaming half of the protocol is under-specified in the place -where it matters operationally. `prisma app logs` and `build logs --follow` -exist to have their output piped. The draft never says which stream an event -renders to, and the only candidate concept — the `output` event — uses -`stream` to mean the *child's* stream, not ours. If events render to stderr -(which the settled stream discipline implies), `prisma app logs > file` -produces an empty file. That is a shipped-behaviour regression hiding in an -unstated default. - -Underneath those, two design questions are genuinely open rather than -oversights: R10's config-section registration is deliberately absent and the -current `LoadedConfig` shape cannot support the "fails only if a section it -needs is invalid" rule without it (F16); and the test harness has no way to -abort a run, which makes every session command — `dev`, `app logs`, -`build logs --follow` — untestable through the harness that R7 says is the -evidence (F19). - -Pressure-test verdicts in short: `migration list` maps cleanly. `app deploy` -maps with one gap (two result shapes need a union `TResult`, which works, but -the SDK's `onStatusChange` has no matching event beyond `status`, which is -fine). Composer `dev` maps for events and `--fresh` but not for lifetime: the -handler must itself await the signal and then return a `Result` whose value is -a session that no longer exists, and `present.human` is asked to render it -(F12). `lsp` cannot be declared at all, because `raw` claims nothing else is -declared while the type still requires `flags` and `present` (F13). - -## What looks solid - -- **The one protocol.** Events during, `Result` at the end, presenters after. - It is the platform CLI's `writeCommandSuccess` generalised, and it is the - only thing in the corpus that can serve all three families. -- **`present` as a triple.** `human` to stderr, `stdout` for the machine - payload, `json` for the envelope projection is exactly the platform's - proven `renderHuman`/`renderStdout`/`renderJson`, including the property - that `--quiet` leaves a clean pipe. `bucket key create`'s secret-to-stdout - case survives this design unchanged. -- **`Block` and `Ui`.** There is no `print`, no colour function that writes, - no exit. A product that wanted to diverge could not. `fields.sensitive` - carries the secret-masking the platform built into its UI layer. -- **Lazy `handler`.** A direct match for stricli's `loader`, and it keeps - Composer's import-time crash history classified rather than fatal at - startup (R9's actual motivation). -- **Path-free commands, mounting-side tree.** `CommandSet` has no paths and - `createCli` supplies them. That is R12 structurally, and it is also what - lets a product mount a command anywhere in its own e2e tests. -- **`data?: unknown` on every event.** The R14 extension mechanism is - present on every variant, uniformly, with the engine explicitly not - interpreting it. This is the right shape. -- **Prompts return `Result` rather than throwing.** The platform's - prompt layer throws a `usageError` and then string-matches its own summary - to detect cancellation (`isPromptCancelError`, prompt.ts:98-100). Returning - a value is strictly better. - -## Findings - -### F01 — `ArgsOf` silently drops every positional - -**Location:** §3, `ArgsOf`, lines 182–188; `positionals?: TPositionals` at line 208. - -**Issue:** `positionals` is declared optional, so `D['positionals']` has type -`TPositionals | undefined`. `keyof` over a union is the intersection of the -members' keys, and `keyof undefined` is `never`, so the second half of the -intersection evaluates to `{}`. Every positional vanishes from the handler's -argument type, with no error anywhere. - -**Why it matters:** `app deploy `, `database show `, -`app domain wait ` — most of the corpus takes positionals. Handlers -would read `args.entry` and get a compile error, or worse, authors would add -an index signature to make it go away. - -**Suggestion:** stop parameterising `ArgsOf` on the definition. Make it -`ArgsOf` taking the two objects directly, defaulting -`TPositionals` to `{}`. That also fixes F02 and F04. - -### F02 — Typing a lazily loaded handler against its own definition is circular - -**Location:** §4, `handler`, lines 214–219. - -**Issue:** the handler lives in a separate module (that is the point of the -loader). To type its `args` parameter the author must write something like -`ArgsOf` — but `myCommand`'s own type depends on the -handler's type, which depends on `myCommand`. TypeScript will resolve this to -`any` or report a circularity, depending on how it is written. - -**Why it matters:** the only escape is to hand-write the args type in the -handler module, which then drifts from the flag declarations silently. That -is precisely the "separate data structure a translator interprets" that R1 -exists to prevent, reintroduced at the file boundary. - -**Suggestion:** with F01's fix, authors declare `const flags = {...}` once, -export it, and both the definition and the handler refer to -`ArgsOf`. No cycle. Consider shipping a -`Handler` alias so the handler -module has one thing to import. - -### F03 — The brand symbols are not exported, so declaration emit fails - -**Location:** §3, `declare const FLAG: unique symbol` (line 171) and -`declare const POSITIONAL: unique symbol` (line 178). - -**Issue:** `FlagSpec` and `PositionalSpec` are exported interfaces whose only -member is keyed by a non-exported `unique symbol`. Emitting `.d.ts` for this -package produces TS4033/TS4023 ("has or is using private name"). - -**Why it matters:** the engine package ships types; this is a hard build -failure the moment `declaration: true` is on. - -**Suggestion:** export the symbol declarations (they can stay undocumented), -or brand with a `declare const brand: unique symbol` exported from an -`internal` entry point that the public types reference. - -### F04 — A concrete command is not assignable to the bare `CommandDefinition` - -**Location:** §6, `CommandSet` (line 276) and `createCli`'s -`commands: Readonly>` (line 287); §7, -`createTestCli` (line 323). - -**Issue:** `CommandDefinition`'s defaults make `present.human` have signature -`(value: unknown, ui: Ui) => Block[]`. Under `strictFunctionTypes`, assigning -a command whose `present.human` takes `(value: MigrationList, ui)` requires -`unknown` to be assignable to `MigrationList`, which it is not. The same -argument applies to `handler`'s `args` parameter. So the very commands -`createCli` is supposed to receive will be rejected by it. - -**Why it matters:** this is not a corner case — it is the primary call site. -Every product's `CommandSet` export and every `createCli` call breaks, and -the usual fix people reach for (`as any`) erases the whole type story. - -**Suggestion:** introduce an explicitly erased mount-side type — e.g. -`AnyCommandDefinition = CommandDefinition` (or a -deliberately opaque `MountableCommand` the engine produces) — and type -`CommandSet`, `createCli` and `createTestCli` against that. The precise -definition stays on the authoring side where inference matters. - -### F05 — `TConfig` is not inferable, so `ctx.config` will be `unknown` in practice - -**Location:** §4, `CommandDefinition`'s `TConfig` parameter (line 198) and its -single use inside `handler`'s `ctx` parameter (line 217). - -**Issue:** `TConfig` appears only in a contravariant position inside a lazily -imported module's function parameter. `defineCommand` has nothing to infer it -from unless the handler module explicitly annotates its `ctx` parameter, -which is the thing the circularity in F02 makes awkward. - -**Why it matters:** in the common case every handler gets -`CommandContext` and casts. That defeats the point of typing the -config section at all. - -**Suggestion:** make the config section an explicit, declared part of the -definition rather than a free type parameter — see F16, which needs a runtime -section name anyway. One field solves both. - -### F06 — No number flag and no defaults, so parse-time validation leaks into handlers - -**Location:** §3, the `flag` object, lines 158–169. - -**Issue:** the vocabulary is string, requiredString, boolean, enum, repeated, -json. There is no number, no duration, and no `default` on any of them. - -**Why it matters:** `app domain wait --timeout` (default 15m, with `0` meaning -"probe once") and the SDK's `timeoutSeconds`/`pollIntervalMs` are real, -shipping, and cannot be declared here. The handler ends up parsing and -validating the string, which means the failure surfaces as a handler error -after routing rather than as the typed parse error R6 wants — and stricli's -own scanner errors, the thing the framework evaluation praised most (criterion -6), go unused for this class of flag. - -**Suggestion:** add `flag.number({ brief, default?, min?, max? })` and a -`default` option to `string`/`enum`/`boolean`. Defaults also remove the -`?? fallback` noise from every handler. - -### F07 — No short aliases - -**Location:** §3, the `flag` object. - -**Issue:** nothing declares `-q`, `-y`, `-v`, `-f`, `-n`. Stricli supports -exactly one-character aliases (per the evaluation, weakness 5); the draft -does not expose that capability. - -**Why it matters:** every shipping CLI in the corpus has them -(`global-flags.ts:13-21`). Dropping them is a user-visible regression, and -it will be discovered after the vocabulary is frozen. - -**Suggestion:** add `short?: string` to the flag specs and validate it is one -character at build time. Note that stricli cannot do long aliases, so -deprecation renames need the engine's own hidden-second-flag trick — worth -recording now. - -### F08 — Positionals are too thin: no number, no enum, no variadic - -**Location:** §3, `positional`, lines 174–177. - -**Issue:** only `string` and `optionalString`. - -**Why it matters:** variadic positionals appear in the corpus (multi-argument -commands) and enum positionals would let the "did you mean" machinery stricli -already computes apply to subjects, not just flags. - -**Suggestion:** add `rest()` (variadic) at minimum; number and enum if the -tree needs them. Flag anything not added as a deliberate exclusion so it does -not get re-litigated. - -### F09 — Flag and positional names can collide silently - -**Location:** §3, `ArgsOf`'s intersection, lines 182–188. - -**Issue:** `ArgsOf` intersects the flag keyspace with the positional keyspace. -A flag named `name` and a positional named `name` produce -`string & (string | undefined)` — one merged key, no error, and at runtime one -value overwrites the other depending on the engine's merge order. - -**Why it matters:** it is a quiet correctness bug, and the merge order is an -engine implementation detail no author will know. - -**Suggestion:** either detect the collision at the type level (a conditional -type resolving to a `never`-valued error field is enough to make it a compile -error) or separate the keyspaces (`args.flags.x` / `args.positionals.y`). -Which of those is right is a shape question — architect referral. - -### F10 — Exit codes 0/1/2/3 contradict the settled table - -**Location:** §6, `Cli.run` doc comment, lines 292–296. - -**Issue:** the settled host–product contract (`wip/designs/1a/design.md` §7.6) -is `0` ok, `1` bug, `2` expected failure, `3` user declined, `130`/`143` -signals, `4`–`99` command-specific outcome codes, plus one documented -passthrough exception for spawned-engine exit statuses. The draft names four -codes and gives a command no way to express any of the others. - -**Why it matters:** this is not hypothetical. `migration check` already exits -`4` on integrity failure via `exitOverride` -(`migration-check/exit-codes.ts:1-3`), the platform's `commandCanceledError` -already exits `130` (`errors.ts:129-139`), and Composer already passes an -alchemy child's status through (`render-error.ts:27-37`). Machine consumers -branching on exit codes is a stated R6 goal; a code space the engine cannot -express is a code space each product will re-invent behind the engine's back. - -**Suggestion:** state that `CliStructuredError` carries an optional -`exitCode` in the 4–99 range (the platform's `CliError.exitCode` already -does), define how signal-driven termination reaches 130/143 (see F17), and -name the child-passthrough exception explicitly. - -### F11 — Events have no defined stream, and the streaming-data case has no home - -**Location:** §1, the `output` event (lines 69–75); §2, `report` (line 117). - -**Issue:** the draft never says where events render in human mode. The -`output` event's `stream` field describes the *child's* stream, not ours, -so there is no way to say "this line is the data the user asked for and -belongs on our stdout". - -**Why it matters:** `prisma app logs`, `build logs --follow`, and -`app run` exist to be piped. Today `build logs` routes NDJSON records to -stdout or stderr by `source`/`level` (`controllers/build.ts:34-150`). If the -engine renders all events to stderr — which the settled stream discipline -implies — then `prisma app logs > out.txt` yields an empty file. That is a -silent regression discovered by a user, not by us. - -**Suggestion:** state the routing rule explicitly, and give a command a way to -declare that its event stream *is* its data — either a distinct event kind -(`data`, going to stdout) or a `channel: 'data' | 'progress'` discriminator on -`output`. While you are there, say that `--quiet` suppresses progress events -but never data events, matching what `--quiet` already means for `present`. - -### F12 — Session commands have no meaningful `Result`, and `present` is required anyway - -**Location:** §4, `present` (required, lines 228–232); the header's protocol -note, lines 12–14. - -**Issue:** for a session command the value *is* the session, which no longer -exists once the handler returns. Composer's `dev()` returns -`Result` and the CLI adapter then owns the lifetime -(`run-dev.ts`). Under this interface the handler must instead await the signal -itself, tear down, and return some invented `Result` whose `present.human` is -asked to render a summary of a session that has ended. - -**Why it matters:** every session command gets a fake result type and a -no-op presenter, and the shape of that fake becomes de facto convention -without ever being designed. It also removes the deliberate property Composer -records: the host owns signals and the operation never touches them. - -**Suggestion:** make `present` optional, or add an explicit -`kind: 'session'` (or `present: 'none'`) that declares "the stream is the -output; there is no end-state rendering". This is the same knob the platform -already needs — `build logs` sets `emitJsonSuccessEvent: false` for exactly -this reason (`commands/build/index.ts:52-55`). - -### F13 — `raw` contradicts the rest of the type - -**Location:** §4, `raw?: false | { reason: string }`, lines 234–239. - -**Issue:** the comment says "the engine enforces that nothing else is -declared", but `flags` and `present` are both required properties. A `raw` -command such as `lsp` cannot be written without declaring an empty `flags` -object and a presenter that will never run. - -**Why it matters:** `lsp` is a real command; the escape hatch does not -currently escape anything. - -**Suggestion:** make `CommandDefinition` a union — the normal shape versus a -`RawCommandDefinition` with `brief`, `description`, `flags` and a raw handler -taking the runtime streams. Then "nothing else is declared" is enforced by the -type rather than by a runtime check. - -### F14 — `Runtime` is missing `env` and `isTty.stdout` - -**Location:** §6, `Runtime`, lines 300–311. - -**Issue:** there is no `env`, and `isTty` covers stdin and stderr but not -stdout. - -**Why it matters:** two concrete consequences. -(a) The engine's own interactivity check needs `env.CI` — the platform's -`canPrompt` reads it directly (`runtime.ts:114`). Colour policy needs -`NO_COLOR`/`FORCE_COLOR`. With no `env` on `Runtime`, the engine reads -`process.env`, and the moment it does, R7's in-repo tests stop being able to -simulate CI or a non-colour terminal — which is the property that makes those -tests evidence. -(b) The deliberately-kept auto-JSON behaviour keys on stdout not being a TTY -(`utils/global-flags.ts:67-69`). The interface has no field to read. - -**Suggestion:** add `env: Readonly>` and -`isTty.stdout`. Both are cheap and both are already in the platform's -`CliRuntime`. - -### F15 — The success envelope has no slots for warnings, next steps, or next actions - -**Location:** §4, `present` and the handler's `Result` return. - -**Issue:** the handler returns a bare value. The platform's `CommandSuccess` -carries `warnings`, `nextSteps` and `nextActions` alongside `result` on every -command (`output.ts:9-15`), and renders warnings in human mode too so degraded -steps are never silent (`command-runner.ts:130-135`). - -**Why it matters:** remediation is the second-most-recurring structure in the -survey, currently spelled five different ways, and unifying it is a stated -R14 goal. If the only place to put it is inside `present.json`, it ends up -nested in `result` where no cross-command consumer can find it — recreating -the ORM's "remediation buried in per-command shapes" problem the survey calls -out by name. `Block.nextSteps` covers human mode only. - -**Suggestion:** let the handler return `Result, …>` where -`Success` carries `value` plus optional `warnings`/`nextSteps`/`nextActions`, -or make those a second, engine-owned channel the handler can add to. Either -way they must reach the JSON envelope without passing through `present.json`. - -### F16 — R10's central rule is inexpressible: nothing says which section a command needs - -**Location:** §2, `CommandContext.config` (lines 106–110); §6, `LoadedConfig` -(lines 313–316). - -**Issue:** `CommandContext`'s comment asserts that the engine already failed -the command when its section is invalid, but nothing in `CommandDefinition` -names a section. `LoadedConfig` is an untyped `sections` record plus -diagnostics keyed by `section: string | null`. The engine cannot match one to -the other. - -**Why it matters:** "a command fails only if a section it needs is invalid" is -the whole reason per-section diagnostics exist — one product's config typo must -not brick another product's commands. As written the engine has exactly two -implementable choices, and both are wrong: fail every command if any section -is bad, or fail none. There is also no expression of the `defineConfig` -version marker, whose absence R10 says must fail early with a typed error — -today that can only appear as a diagnostic with `section: null`, which is -indistinguishable from a general file problem. - -**Suggestion:** put the section name on the definition (`configSection: -'composer'`), which also gives `TConfig` something to infer from (F05). Give -`LoadedConfig` an explicit discriminated top-level state so "no config file", -"file present but unmarked", and "file valid, section X invalid" are three -distinguishable things rather than three shapes of the same record. Section -registration and validators are legitimately a separate design, but the -*name* has to be here or the rule cannot be enforced. - -### F17 — Cancellation, teardown, and the signal-to-exit-code path are undefined - -**Location:** §2, `signal` (lines 124–126); §6, `Runtime.signal` (line 306). - -**Issue:** three gaps sit together. The engine cannot tell which signal fired -(`AbortSignal` alone does not say SIGINT versus SIGTERM), so 130 versus 143 -is unreachable. Nothing bounds teardown — a handler that hangs after abort -leaves the CLI unresponsive, and a second Ctrl-C has no defined effect. -And `PromptSurface` returns the same `Result` failure for "no TTY available" -as for "user pressed Ctrl-C", which the engine must distinguish to choose -exit 2 versus exit 3. - -**Why it matters:** this is the 2am path. A `dev` session that will not die on -Ctrl-C is the single most common CLI complaint, and it is currently -unspecified rather than decided. - -**Suggestion:** state that `signal.reason` carries a typed cancellation value -naming the signal; define a teardown grace period after which the engine -returns the signal code regardless; define what a second signal does. Give the -prompt failures distinct, documented codes so the exit-code mapping is -mechanical rather than a string match. - -### F18 — `report` has no backpressure and no defined end of life - -**Location:** §2, `report: (event: EngineEvent) => void`, line 117. - -**Issue:** `report` returns `void`, so a handler tailing a high-volume log has -no signal to slow down; `stream.write()` returning `false` is invisible. The -draft also says `report` is safe to call after the signal fires, but says -nothing about after the handler's promise settles. - -**Why it matters:** unbounded buffering on a slow pipe is a memory failure -mode in exactly the commands that stream. And a late `report` from a detached -task — a timer, a child process that has not been awaited — writing after the -JSON envelope has been printed corrupts the `--json` contract for machine -consumers, which is the one contract agents depend on. - -**Suggestion:** either return a `boolean`/`Promise` for backpressure, or -document that the engine buffers with a stated bound and what happens when it -is hit. Separately, state that `report` becomes a no-op once the handler's -promise settles, and that the engine drains before writing the envelope. - -### F19 — The test harness cannot test any session command - -**Location:** §7, `TestCli.run`, lines 330–338. - -**Issue:** `run(argv, { stdin })` has no abort handle, no cwd, no env, and no -TTY control. For a session command `run()` simply never resolves. The `stdin` -string also cannot drive a clack prompt, which reads raw keypresses (arrow -keys for `select`), so prompt-bearing commands are untestable too. - -**Why it matters:** R7 says in-repo tests are the evidence about the shipped -CLI. Under this harness the untestable set is `dev`, `app logs`, -`build logs --follow`, `app run`, and every wizard — which is most of what -actually breaks. Nor can a test cover F14's auto-JSON behaviour, since there -is no TTY knob. - -**Suggestion:** add `signal`, `cwd`, `env`, and `isTty` to the `run` options, -and replace the `stdin` string with a scripted prompt responder (an ordered -list of answers, plus the recorded prompts in the result so tests can assert -what was asked). Both are small; both are the difference between a harness -that covers the risky commands and one that covers the easy ones. - -### F20 — Steps have no identity, so the nesting the comment promises does not work - -**Location:** §1, `step-started` / `step-finished` (lines 44–52). - -**Issue:** the pair is correlated only by the `step` string, and the comment -says "steps may nest". The one implementation in the corpus that actually -nests uses `spanId` plus `parentSpanId` (`control-api/types.ts:91-111`), -precisely because a name is not unique — the ORM's per-migration spans are -`operation:` children of `apply`. - -**Why it matters:** two concurrent steps with the same name (two contract -spaces both applying) cannot be told apart, and a renderer cannot build the -tree. This is the one place where the draft's vocabulary is strictly weaker -than the code it generalises. - -**Suggestion:** add `id` and optional `parentId`, exactly as the span model -does. `step` stays as the human label. - -### F21 — Credentials are a static token with no refresh path - -**Location:** §2, `Credentials` (lines 132–136); §6, `Runtime.credentials`. - -**Issue:** a plain `{ token, workspaceId? }` captured once at startup. - -**Why it matters:** a `dev` session runs for hours. A token captured at -minute zero expires at minute ninety, and every subsequent management-API -call fails with an auth error that looks like a bug. Nothing in this shape -allows refresh. - -**Suggestion:** make it a provider — `getToken(): Promise>` -— so refresh is possible without changing the interface later. The opacity -the comment wants is preserved. - -### F22 — Commands cannot declare that they need authentication - -**Location:** §2, `credentials: Credentials | undefined`. - -**Issue:** every handler that needs auth must check for `undefined` and build -its own "not authenticated" structured error. - -**Why it matters:** the platform already centralises this -(`authRequiredError`, `errors.ts:101-115`) with consistent wording, next -steps, and exit code. Pushing it into every handler is the presentational -drift R5 exists to kill, one layer down: the error text becomes per-product. - -**Suggestion:** `requiresAuth: true` on the definition, with the engine -producing the one canonical error. Whether that belongs on the engine at all, -given `credentials` is Cloud-specific, is a shape question — architect -referral. - -### F23 — `createCli` is documented as failing at build time but can only throw at startup - -**Location:** §6, `createCli`, lines 283–288; "Collisions and grammar -violations fail the build." - -**Issue:** `createCli` is a runtime function taking a `Record`. Duplicate -object keys are a TypeScript error, but a command mounted at `'db'` colliding -with a group named `'db'`, or a path violating the agreed grammar, can only be -detected when the function runs. Throwing there also conflicts with the stated -rule that the engine never exits and only writes to the provided streams. - -**Why it matters:** "fails the build" and "throws on every user invocation -including `--help`" are very different guarantees, and the draft claims the -first while the shape delivers the second. - -**Suggestion:** say plainly that validation happens at `createCli` time and -returns a `Result` (or is asserted by a shell-repo test that calls it), and -keep a separate lint/build step if a genuine build-time check is wanted. - -### F24 — `@prisma/cli-foundation` and `NodeJS.WritableStream` both sit in the public surface - -**Location:** line 25 (the foundation import); §6, `Runtime`'s stream fields. - -**Issue:** R3 says products import the engine package "and nothing else for -CLI purposes". `Result` and `CliStructuredError` come from a second package. -Separately, `NodeJS.WritableStream` is a Node-global type in a surface that -R4 wants runtime-agnostic; stricli deliberately uses a minimal structural -`{ write, getColorDepth? }` instead. - -**Why it matters:** the foundation split is almost certainly correct -(structured errors are raised at their origin, inside operations, which must -not depend on the engine), but then the engine should re-export so products -have one import. And the Node type quietly makes the public surface -node-only, which is one of R4's three stated reasons. - -**Suggestion:** re-export `Result` and `CliStructuredError` from the engine -package. Replace the stream types with a structural interface. Whether the -two-package split is right is an architect question; the re-export and the -stream type are not. - -### F25 — `flag.json()` is engine-behavioural but arrives in `args` - -**Location:** §3, `flag.json()`, line 168. - -**Issue:** as a `FlagSpec` in the flags record, `json` appears in -`ArgsOf` and therefore in the handler's arguments. The engine can identify it -at runtime (it produced the spec object), so switching renderers and -suppressing prompts and progress is implementable — that part works. - -**Why it matters:** a handler that can see `args.json` will eventually branch -on it, which is the R5 violation the whole design is built to prevent. It also -raises the question of the other engine-behavioural flags the platform proves -are needed and the draft omits entirely: `--quiet`, `--verbose`, `--trace`, -`--yes`, `--no-interactive`, `--color/--no-color` -(`global-flags.ts:23-45`). With no global flags, each must come from a shared -declaration, and only `json` has one. - -**Suggestion:** exclude engine-owned flags from `ArgsOf` (a `FlagSpec` variant -the mapped type filters out), and add the rest of the shared set. `--yes` is -the interesting one: it must reach `ctx.prompt` so `confirm` auto-answers, -which argues it is context state, not an argument. - -## Deferred - -These are real, but they are scope expansions rather than gaps in this draft. - -- **Config-section registration and validators (R10's other half).** The - draft says this is a separate design and I agree. What cannot be deferred - is the section *name* on the definition — without it the engine cannot - implement the "only the sections it needs" rule at all (F16). -- **Daemon management (mode 5).** Explicitly out of scope in the header. - The survey's finding that Composer's daemons have no user-facing - stop/status is worth carrying into that design, not this one. -- **Autocomplete.** Stricli ships `proposeCompletions`; nothing in this draft - touches it. Fine to leave until the tree is stable, but note that a - completion surface constrains the flag vocabulary, so decide before - freezing F06–F08. -- **Telemetry.** Both the ORM and the platform fork a detached child on every - invocation. Neither `Runtime` nor `Cli` has a hook. Deliberate, presumably — - but it should be an explicit decision rather than an omission. -- **Duration and timing events.** The survey ranks durations 2/3 and both - families render them under `--verbose`. There is no timing concept in the - event vocabulary. Adding one is speculative until a renderer needs it; the - evidence rule the draft sets for itself says wait. - -## Acceptance-criteria verification - -Verdicts for a design artifact: **PASS** = the interface structurally -satisfies or enforces the requirement. **WEAK** = satisfiable, but the shape -does not enforce it. **FAIL** = the shape contradicts the requirement or -cannot express it. **NOT VERIFIED** = cannot be assessed from the draft. - -| R | Requirement | Verdict | Detail | -|---|---|---|---| -| R1 | One language, directly executable | **PASS** | `defineCommand` is an identity function; the declared object is what the engine runs. No product-side schema, no interpreter. The typing defects (F01–F05) are bugs in the expression, not a return to a two-stage design — except that F02's workaround (hand-written arg types in the handler module) would reintroduce exactly the drift R1 forbids, so fixing it is R1 work. | -| R2 | Commands end in typed operation calls | **WEAK** | The shape is compatible: `handler` returns `Result`, which is the operations client's own return type, so the thin-wiring case is the natural one. But nothing prevents a handler containing logic, and `TResult` being free-form makes a fat handler no harder to write than a thin one. Enforcement is a review/lint concern, not an interface one. | -| R3 | The engine package is the whole contract | **WEAK** | No stricli type appears anywhere — the primary goal holds, and `FlagSpec`/`PositionalSpec` being opaque brands means even the parse vocabulary is ours. Two leaks: `Result` and `CliStructuredError` come from `@prisma/cli-foundation`, so products import two packages (F24); and `NodeJS.WritableStream` puts a Node-global type in the surface, which is a third-party type in the sense R4 cares about even if not in the sense R3 names. Both fixable without design change. | -| R4 | Products receive a context, never the environment | **PASS** | `CommandContext` carries config, credentials, `report`, `prompt`, `signal`, `cwd`, and offers no way to reach disk, env, or the TTY. `cwd` is explicitly there so products never call `process.cwd()`. Two follow-ups that do not change the verdict: the engine itself loses injectability because `Runtime` has no `env` (F14), and there is no sanctioned way to probe for an optional module (F13's R13 counterpart, F26 below). | -| R5 | Products have no presentational API | **PASS** | Structurally enforced. `present` returns `Block[]`; `Ui` returns strings and cannot write; there is no print, no colour policy, no exit. `stdout: (value) => string[]` is the one raw-bytes path and it mirrors the platform's proven `renderStdout`, which exists so `--quiet` leaves a clean pipe. The gaps I found are about what products *cannot say* (F11, F15), not about what they can render. | -| R6 | Errors and results follow the settled conventions | **FAIL** | The `Result`/`CliStructuredError` half is right and used consistently, including on the prompt surface. The exit-code half contradicts the settled table: the draft names 0/1/2/3 and gives a command no way to express 4–99 or the signal codes, while `migration check` already exits 4, the platform's cancel path already exits 130, and Composer already passes a child's status through (F10). Cancellation cannot even reach 130/143 because the interface does not say which signal fired (F17), and prompt failures do not distinguish "no TTY" (exit 2) from "user declined" (exit 3). | -| R7 | Product-repo end-to-end tests are first-class | **WEAK** | `createTestCli` is instance-based, takes only the product's own commands, returns real bytes plus exit code, and — better than the current CLIs — exposes `events` for semantic assertions without forcing `--json`. That much is a genuine match. But the harness cannot abort a run, so every session command hangs forever; cannot script keypress-driven prompts, so every wizard is untestable; and has no cwd/env/TTY knobs, so the auto-JSON and interactivity behaviours cannot be covered (F19). The untestable set is the risky set. | -| R8 | The shell's test burden is integration proof | **NOT VERIFIED** | This is an allocation-of-work requirement, not an interface one. Nothing in the draft obstructs it — `createCli` plus an injected `Runtime` gives the shell the same argv-in/bytes-out path products get. Re-assess against the shell's test plan. | -| R9 | Static tree, lazy guts | **PASS** | `handler: () => Promise<{ default }>` is exactly stricli's `loader`, and everything help needs (`brief`, `description`, `examples`, `flags`, `positionals`) is static, so full help renders without invoking a loader. One caveat that does not change the verdict: `present` also sits in the static definition, so a presenter that reaches for a heavy formatting library silently drags it into startup. Worth a documented rule. | -| R10 | One config file, validated by its products, never a crash | **FAIL** | The requirement's operative rule — "a command fails only if a section it needs is invalid" — cannot be implemented against this shape, because no command declares which section it needs and `LoadedConfig` is an untyped record keyed by strings the engine cannot match to commands (F16). The `defineConfig` version marker has no representation either; an unmarked classic Prisma 7 file can only appear as a diagnostic with `section: null`, indistinguishable from any other whole-file problem — and R10 calls a silently misparsed v7 file the worst launch bug available. Section registration is legitimately deferred; the section *name* is not. | -| R11 | Pinned versions, tandem releases | **NOT VERIFIED** | A release-process requirement with no interface surface. `createCli` takes a `version` string, which is unrelated. | -| R12 | The shell defines the command tree | **PASS** | `CommandDefinition` contains no path and `CommandSet` is a flat name→definition record; paths exist only as keys in `createCli`, alongside group briefs that are declared at the mount because groups belong to the tree. `createTestCli` taking the same records is what lets a product mount a command anywhere for its own tests, which is R12's stated escape valve. Only caveat: "collisions fail the build" overstates what a runtime `Record` can guarantee (F23). | -| R13 | The CLI never touches a package manager | **WEAK** | Nothing in the interface installs, downloads, or vendors anything, so the prohibition holds. The requirement's positive half does not: a handler needing an optional peer has no sanctioned way to probe for it. A bare `await import('x')` in a handler is arguably a permitted runtime check (the stricli evaluation says the framework does not interfere), but with no helper every product hand-rolls the try/catch and its own error wording — the same per-product drift F22 describes for auth. Suggest `ctx.optionalDependency(name): Promise>` so the "missing dependency, install it with your own package manager" error is written once. | -| R14 | One event vocabulary, engine-defined, with product extensions | **PASS** | The union is a genuine generalisation of the surveyed structures, the common fields are required rather than optional (so products must fill them), and `data?: unknown` sits on every variant as the pass-through extension with the engine explicitly not interpreting it. The occurrence-ranked derivation is exactly the evidence discipline R14 asks for. Two weaknesses that do not sink it: steps lack the ids the one nesting implementation in the corpus needs (F20), and the vocabulary has no way to mark an event as data rather than decoration, which is where the streaming commands break (F11). | - -### Summary counts - -| Verdict | Count | Requirements | -|---|---|---| -| PASS | 6 | R1, R4, R5, R9, R12, R14 | -| WEAK | 4 | R2, R3, R7, R13 | -| FAIL | 2 | R6, R10 | -| NOT VERIFIED | 2 | R8, R11 | -| **Total** | **14** | | diff --git a/.drive/projects/prisma-cli-v8/assets/engine/reviews/envelope-collections-analysis.md b/.drive/projects/prisma-cli-v8/assets/engine/reviews/envelope-collections-analysis.md deleted file mode 100644 index 55e7dd9e..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/reviews/envelope-collections-analysis.md +++ /dev/null @@ -1,223 +0,0 @@ -# Are `diagnostics`, `warnings`, and `nextActions` three real things? - -Adversarial analysis of the draft envelope's three collection fields -(`engine-interface-draft.ts` §1, §2, §9), against the shipped emission sites -in the output-modes survey, ADR 239, and the platform CLI source. - -**Verdict up front: the operator is right about `warnings` and wrong about -`nextActions`. Fold to two collections: `diagnostics` + `nextActions`. -Delete `warnings` from both envelopes, and stop aggregating warn-severity -message events into the result contract. `nextSteps` should go too — it is -derived data a JSON consumer can compute from `nextActions`.** - ---- - -## 1. What the shipped "warnings" actually are - -I enumerated every warning-emission site with content across the three -families. They fall into three populations, and none of them justifies an -uncoded string channel in the envelope. - -### Population A: structured findings that lose their structure at the boundary - -These are the majority, and they are the damning ones. Each site *has* -structure — a kind, a wrapped error, a why, a fix — and crushes it into -prose because the envelope's `warnings: string[]` accepts nothing else. - -| Site | What it flattens | Natural code | -|---|---|---| -| ORM planner conflicts — `MigrationPlannerConflict { kind, summary, why? }`, kinds like `'typeMismatch'`, `'nullabilityConflict'` (framework-components/control/control-migration-types.ts:262-269; rendered as a dim string list, cli/utils/formatters/migrations.ts:97-107) | A discriminated union with `kind` + `summary` + `why` — CliStructuredError minus the registry entry | `MIGRATION.PLANNER_TYPE_MISMATCH`, `MIGRATION.PLANNER_NULLABILITY_CONFLICT`, … | -| ORM `db verify` schema warnings — doc comment says it outright: "**Warn-graded finding messages** (observed-policy drift)" (cli/utils/formatters/verify.ts:50-55) | Drift findings from the same verification machinery whose error-graded findings the draft calls diagnostics | `CONTRACT.DRIFT_*` — same family as the error-graded findings | -| Platform init "Project link failed: `${error.summary}`. Link later with …" (prisma-cli controllers/init.ts:1053-1055) | A caught `CliError` — a full structured error — demoted to its `.summary` interpolated into a string | The error's own code, re-emitted at severity warn | -| Platform init "Installing X failed: `${detail}`. Install it later with `${installCommandText}`." (controllers/init.ts:365-372; same pattern :290) | summary + why (wrapped error's first line) + fix (install command) serialized into one sentence | `CLI.INIT_SKILL_INSTALL_FAILED` **already exists in ADR 239's crosswalk as an error code** (PN-CLI-5013). The same condition is an error in one flow and a warning in another — severity is an attribute of the occurrence, which is exactly what `CliStructuredError.severity` models | -| Platform local-state cleanup failures — "The app was removed remotely, but the local `${target}` state could not be cleared: `${cause}`" (controllers/app.ts:4999-5002, used :3086, :3092; same pattern in project remove/transfer, controllers/project.ts:629-632, 710-715) | A caught error swallowed into prose; a wrapper script that wanted to retry the cleanup cannot detect it | `PLATFORM.LOCAL_STATE_CLEANUP_FAILED` | -| Platform `agent status` "Could not read installed skills with `${cmd}`: `${msg}`. Falling back to `${path}`." (controllers/agent.ts:134-141) | Degraded read: failing command, cause, fallback source — three fields in one string | `PLATFORM.SKILLS_READ_DEGRADED` | -| Composer's warn-severity events — `watch-error {message}`, `rebuild-failed {message}`, `stop-error {message}`, `stream-failed {message}`, `lines-dropped {count}` (operations/dev.ts:19-30, operations/log.ts:20-25) | Already a discriminated union on `kind` — coded warnings in all but registry membership | one code per event kind | - -### Population B: advisory no-op notices a machine consumer genuinely wants to branch on - -| Site | Why a code is *more* deserved, not less | -|---|---| -| Platform promote/rollback "The selected deployment is already live for this app." (controllers/app.ts:1914-1916, 2027-2029) | An agent driving `app promote` needs to know the operation was a no-op. String-matching "already live" is the only machine handle today. `PLATFORM.DEPLOYMENT_ALREADY_LIVE` is about the clearest branch-worthy code in this whole inventory | -| Platform branch-database "prompt suppressed" warning under `--yes`/non-interactive (lib/app/branch-database-deploy.ts:135-141) | An agent needs to know a decision was skipped and why. Branch-worthy | -| Platform missing-preview-default env warnings (controllers/app-env-file.ts:64, app-env.ts:155) | Policy advisory with an exact key list — already carries `meta`-shaped content | - -### Population C: pure FYI that is really result data - -| Site | Where it belongs after the fold | -|---|---| -| ORM init ".env already exists; leaving it untouched." / "README.md already exists…" (commands/init/init.ts:223-225, 376) | The init JSON already has `filesWritten[]`/`filesDeleted[]` (init/output.ts:23-31); a `filesSkipped[]` in the **result data** says this better than any warning channel | -| ORM init "No package.json found…; created a minimal one." (init.ts:354-356) | Result data (`packageJsonSynthesized: true`) or a coded advisory — author's call | -| ORM init DB probe soft-failures in non-strict mode (init.ts:686-697) | Coded advisory: the probe outcomes are already a discriminated union (`'below-minimum' | 'no-database-url' | 'connection-failed' | 'driver-missing'`) — kinds again, flattened to `outcome.message` | - -**Answer to Q1.** Roughly 20 distinct warning texts ship across the corpus. -The large majority either already carry a discriminant (`kind`, a wrapped -error, an enum outcome) or wrap a structured error whose code exists. -Forcing registry codes onto them is not bloat: it is ~15–20 additive codes, -each declared in its owning module's union per ADR 239 (no central registry -edit), and several reuse codes that already exist (`CLI.INIT_SKILL_INSTALL_FAILED`). -The uncoded warning is a discipline failure of exactly the shape ADR 239 -killed for errors: "consumers cannot match errors by code because there is -no one code space to match against" — substitute "warnings" and every word -holds. The shipped sites prove the failure mode is not hypothetical: a -`CliError` is being demoted to its `.summary` in a string today -(init.ts:1053), and fix commands are trapped inside prose where no agent can -extract them. - -ADR 239 itself anticipated the fold. Its Consequences section keeps -`severity` while admitting "nearly every error is `error` today; the -`warn`/`info` values earn their place only for advisory … surfaces." -A separate uncoded `warnings` channel is what keeps `severity: 'warn'` dead -weight. The fold is what makes it earn its place. - -## 2. Is there consumer-visible behavior distinguishing the two channels? - -The draft gives the two channels four behavioral differences. None survives -inspection as a *consumer contract*; all four are artifacts of provenance. - -1. **Shape** (string vs envelope). Strictly less information. A - severity-`warn` envelope can carry everything the string carries - (`summary`) plus code, why, fix, meta. No consumer capability depends on - warnings being strings — the platform's human renderer just prefixes a - glyph (command-runner.ts:130-135), which an envelope renders equally well. -2. **Provenance** (aggregated from mid-run `message` events vs declared at - `ctx.present`). This is an authoring-side plumbing difference, not a - consumer distinction — and it is a *defect*: the same finding discovered - mid-run either gets buffered by the product until the return site (and - arrives structured) or gets emitted as a warn event (and arrives as a - string). Two paths, two shapes, one concept, diverging by accident of - when the code learned the fact. -3. **`--quiet` visibility** (draft: diagnostics render even under `--quiet`; - warnings, as commentary, do not). This rule can be kept per-severity - after the fold if wanted — but note the platform deliberately renders - warnings "so partial failures … are never silent" - (command-runner.ts:130-135). Either way it is a rendering policy keyed - off a field, not a reason for two collections. -4. **Log-level interaction.** The draft has warn `message` events both - filtered by `--log-level` *and* aggregated into the envelope — so does - `--log-level error` remove a warning from the result contract, or only - from the transcript? The draft doesn't say. The fold dissolves the - ambiguity: commentary is filterable and ephemeral; the envelope's - diagnostics are the contract and are never level-filtered. - -**The fold is free.** Folded shape: - -```ts -export interface CompletedEnvelope { - readonly ok: true - readonly command: string - readonly result: T - readonly outcomeCode: number - /** ALL structured findings of this run, serialized error envelopes. - * severity: 'error' entries require a non-zero outcomeCode; - * 'warn'/'info' entries are advisory. */ - readonly diagnostics: readonly unknown[] - readonly nextActions: readonly NextAction[] - // `warnings` deleted. `nextSteps` deleted (derivable — see §3). -} -``` - -`ErroredEnvelope` gets the same deletion: `error` (the primary abort) + -`diagnostics` (accompanying findings) + `nextActions`. Warn commentary -emitted before the abort is transcript, not contract; anything -contract-worthy is a diagnostic. - -Event change: `message` events keep `severity: 'warn'` for live human -rendering, but the clause "additionally aggregated into the envelope's -`warnings`" (draft §1, message event doc) is deleted. Commentary and -contract stop sharing a pipe. - -**What emitting a warning costs an author after the fold.** Three honest -options, matching the three populations: - -- *Contract-worthy finding* → one factory call at the return site: - `ctx.present(data, p, { diagnostics: [structuredError('MIGRATION.PLANNER_TYPE_MISMATCH', summary, { severity: 'warn', why, meta })] })`. - Cost over `warnings.push(string)`: one line in the owning module's code - union, and naming the thing. That naming cost is the point — it is the - same cost ADR 239 imposes on errors, for the same payoff. -- *Pure FYI* → put it in the result data, where it was always cheaper and - more queryable (`filesSkipped[]` beats a prose warning). -- *Ephemeral color* ("retrying…", "this may take a while") → a warn/info - `message` event, still one uncoded line — it just no longer leaks into - the machine contract. - -**The middle option — a shared generic `CLI.WARNING` code with structured -meta — should be rejected.** It is a fallback code: consumers cannot branch -on it, docs cannot index it, and it recreates the uncoded string with extra -ceremony. The same one-code-space argument that killed fallback error codes -kills it. - -## 3. Do `nextActions` overlap diagnostics? - -No — and the evidence is specific: **command-level follow-ups ship on fully -clean successes, where there is no finding to hang a `fix` on.** Platform -promote returns `nextSteps: ["prisma-cli app list-deploys", "app show-deploy "]` -on the happy path (controllers/app.ts:1917-1921); deploy returns -promote/show-deploy continuations (app.ts:882-890); `agent status` returns -the install command when skills are absent (agent.ts:157-159). None of -these is remediation of a finding — they are journey continuation. A -consumer (the survey's R14 case; the platform's crash envelope with its -pre-filled `feedback` recover action, shell/output.ts:147-156) branches on -`nextActions.kind`/`journey` to *drive the next invocation*; a diagnostic's -`fix` is prose explaining how to clear *that finding*. Scope is a real -distinction, exercised on both sides by shipped code: platform errors carry -`fix` AND `nextSteps`/`nextActions` simultaneously (output.ts:170-183). - -Two genuine overlaps to manage, neither fatal: - -- A diagnostic whose remediation is runnable (the "Install it later with X" - warnings) should emit **both**: the diagnostic (with `fix` prose) and a - `remediation` event / `next` entry carrying the typed command. The draft - already has the aggregation machinery; the survey's finding C2 (five - competing remediation encodings) is the argument for keeping exactly this - one typed action shape rather than re-deriving actions from fix strings. -- **`nextSteps` is redundant in the JSON envelope.** The draft defines it - as "derived from nextActions — the human-string form." A JSON consumer is - a machine; shipping the pre-derived human rendering alongside the source - of derivation is duplication in the contract. Derive `nextSteps` at the - human renderer, drop the field from both envelopes. (Lower stakes than - the warnings fold; if platform-envelope compatibility matters more than - minimality, keeping it costs only redundancy, not incoherence.) - -## 4. Recommendation - -**Fold to two: `diagnostics` + `nextActions`.** Concretely: - -1. Delete `warnings` from `CompletedEnvelope` and `ErroredEnvelope` (§9). -2. Delete the aggregation clause on the `message` event (§1); warn messages - are transcript only. -3. Keep `PresentedResult.diagnostics` / `ctx.present`'s `diagnostics` opt as - the single carrier; entries use `CliStructuredError.severity` ('warn' - for advisory, 'error' only with a non-zero outcomeCode — the existing - guardrail, unchanged). -4. Optionally key the render-under-`--quiet` rule off severity (error-graded - findings always; warn-graded findings follow the platform's - never-silent precedent). -5. Delete `nextSteps` from both envelopes; derive it in the human renderer. -6. Keep `nextActions` exactly as drafted. - -**Migration cost for the shipped sites** (all additive, all within existing -ADR 239 namespaces, each code declared in its owning module): - -| Family | Sites | Work | -|---|---|---| -| ORM planner conflicts | 1 producer type, 1 renderer | Map `kind` → `MIGRATION.PLANNER_*` codes (~4 codes); the union already exists structurally | -| ORM verify schema warnings | 1 shape, 1 renderer | Grade as `CONTRACT.*` warn-severity diagnostics; upstream verification already produces findings | -| ORM init (~6 texts) | init.ts | 2 become result data (`filesSkipped`), ~4 become coded advisories; probe outcomes already enumerate their kinds | -| Platform init/agent/app/project (~8 texts) | controllers | ~6 new codes; the link-failed case re-emits the caught error's existing code at severity warn; skill-install reuses `CLI.INIT_SKILL_INSTALL_FAILED` | -| Platform already-live / prompt-suppressed / env advisories | 4 sites | 3 codes, all genuinely branch-worthy | -| Composer warn events | already typed | No envelope today (no `--json`); under the engine their event kinds map 1:1 to codes when they need contract presence | - -Total: roughly 15–20 new codes, ~20 call-site edits, zero renames, zero -breaking changes to anything published. - -**Where the operator is right and where not.** The `warnings`/`diagnostics` -split is manufactured — it is the platform envelope's `warnings: string[]` -grandfathered into a design that simultaneously adopted a structured-error -model making it obsolete. The shipped warnings are mostly structured -findings and demoted errors being flattened to strings at the boundary; the -one thing the string channel provides that diagnostics don't is the ability -to skip naming the finding, and that is the discipline failure, not a -feature. `nextActions`, by contrast, is real: command-scoped continuation -exists on clean successes, cannot be derived from findings, and is the one -survivor the survey's five remediation encodings should collapse into. diff --git a/.drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review-r2.md b/.drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review-r2.md deleted file mode 100644 index 9ff36804..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review-r2.md +++ /dev/null @@ -1,443 +0,0 @@ -# System design review, round 2 — the unified CLI engine's public interface (v3) - -Subject: `wip/designs/engine/engine-interface-draft.ts` (v3). Compared against -`-v1.ts` and `-v2.ts` and against my round-1 artifact, -`./reviews/system-design-review.md`. - -Pass: **architect**. Same lens and same probes as round 1: discriminator -completeness, consumer-vs-essence, concept-vs-mechanism, symmetry, reads-cold. -Implementation mechanics and failure modes go to the principal-engineer pass. - -The six operator rulings are treated as settled. I do not re-argue them; where a -ruling creates a new consequence I say so and mark it as a consequence, not an -objection. Ruling 4 (`stdout` kept as a view name) is accepted without -qualification — see the disposition of A10. - ---- - -## Part 1 — Disposition of round-1 findings - -Legend: **resolved** / **partial** / **open** / **overruled**. - -### Events - -| # | Item | Disposition | -|---|---|---| -| A1 | `notice` vs `warning` — one axis, two kinds | **resolved.** Merged into `message` with `severity: Exclude` (lines 128–133), and `'error'` correctly excluded because fatal is the `Result`. The aggregation rule ("emit once, appear in both places", lines 122–125) is a genuine improvement over anything shipped. | -| A2 | `output` / `stream: 'stdout'\|'stderr'` names pipes | **resolved.** `channel: 'data' \| 'diagnostic'` (line 144) is semantic and lets a remote log stream map onto it honestly. The kind is still called `output`; with `channel` semantic and `source` documented as "not a pipe", the ambiguity I raised is materially gone. One new interaction with `views.stdout` — see B14. | -| A3 | `status` drops the transition | **resolved.** `from?: string` added (line 164). Kind name unchanged; acceptable now that the payload is a transition. | -| A4 | steps have no identity, nesting only in prose | **resolved.** `id`/`parentId` on `step-started`, `id` on `step-finished` (lines 100–101, 109). | -| A5 | four spellings of severity | **resolved as ruled.** One `Severity` type (line 53) reused by `message` and `Block.summary.tone`. `step-finished.outcome` deliberately stays a completion state — that is a defensible distinction and the doc now states it (lines 104–105). One residue: `outcome` still contains `'warning'`, which is a severity word inside a completion-state set. `'ok' \| 'failed' \| 'skipped' \| 'partial'` would carry the "finished, but not cleanly" meaning without borrowing from the severity scale. Minor; noting, not pressing. | -| A6 | remediation spelled three ways | **resolved.** One `NextAction` (lines 60–69, the platform's shape adopted whole), used by the `remediation` event, `Views.next`, and both envelopes. | -| A7 | events have no sensitivity marker while `Block.fields` does | **open.** `Block.fields.rows[].sensitive` still exists (line 514); `endpoint.url` and `output.line` still cannot be marked, and `Ui` still has no masking helper. The survey's §C5 masking evidence stands. | -| A8 | files-written has no home | **resolved.** `artifact` event added (lines 167–173) with `path` and `description`. No corresponding `Block`, but `tree`/`list`/`fields` cover the terminal case adequately. | -| A9 | `endpoint.name` vs Composer's `address` | **open**, cosmetic; one clarifying word in the doc comment still worth adding. | - -### Presentation - -| # | Item | Disposition | -|---|---|---| -| A10 | `present.stdout` names a file descriptor | **overruled by operator (ruling 4).** Accepted. The v3 shape strengthens the operator's case rather than weakening it: `views.stdout` is now a pure `readonly string[]`, there is no stream handle anywhere near a product, and "stdout" is genuine shared vocabulary for CLI authors. I withdraw the finding. The one consequence worth a sentence of documentation is B14. | -| A11 | `Block` missing `tree`; `list` unevidenced | **partial.** `tree` added with a recursive `TreeNode` (lines 522–529) — the important half. `list` remains with no cited evidence and still overlaps a one-column `table` and an unlabelled `fields`. | -| A12 | `Ui` reads cold; `dim` is a rendering decision; no masking or path relativization | **open.** `Ui` is byte-identical to v1 (lines 531–536). | - -### Context and runtime - -| # | Item | Disposition | -|---|---|---| -| A13 | config section unbound — round-1's top gap | **resolved.** `ConfigSection`, `SectionValidation`, `defineConfigSection`, and `CommandDefinition.configSection` (§4, line 398) make R10 structural. `SectionValidation` carrying `diagnostics` on the `ok: true` branch is a nice touch — a valid section can still warn. One residual: section-name collision has no stated home (B17). | -| A14 | `Runtime` has no `env` | **resolved** (line 612), and correctly on `Runtime` only, not on `CommandContext`. | -| A15 | `isTty` missing `stdout` | **resolved** (line 613), and now load-bearing: json auto-selection keys off it (line 601). | -| A16 | `signal` duplicated on `Runtime` and `CommandContext` | **partial.** Both still present (lines 276, 614). The context's doc now explains what it is *for* (session lifetime, 130/143 selection), which reduces the confusion, but the relationship between the two is still unstated. | -| A17 | `Credentials` owned elsewhere but declared here; name too broad | **partial.** The comment now says "placeholder pending its design" (line 288), which is honest. The `getCredentials()` change to a call-time async resolver (line 262) is a genuine improvement I did not ask for and would have. Name still `Credentials`. | -| A18 | `PromptSurface` naming; no `--yes`; no typed destructive confirmation | **partial.** `--yes` is resolved by ruling 1 — it is engine-injected and `ctx.prompt.confirm` consults it, which is the right shape. The distinct error codes for "interaction unavailable" (exit 2) versus "user cancelled" (exit 3) (lines 267–270) is a real improvement. Still open: the type name, and typed destructive confirmation (`--confirm `, survey §C11, 2/3 families). | -| A19 | no interactivity capability fact on the context | **open.** A handler still learns its environment only by attempting a prompt and reading the failure. `git connect`'s shipped "poll only when we can prompt" (`controllers/project.ts:1708-1717`) remains unexpressible. | - -### Flags, positionals, definitions - -| # | Item | Disposition | -|---|---|---| -| A20 | `flag.json()` a concept in the wrong clothes; family of seven | **resolved as ruled (ruling 1), with one omission.** `flag.json()` is gone; the family is engine-injected and reserved (lines 309–316). The omission: the injected family is `--json, --quiet, --verbose, --yes, --interactive, --color` — six of the seven shipped flags. `--trace` is missing, and the platform's error renderer literally prints "Re-run with --trace for deeper diagnostics" (`shell/output.ts`). Either fold it into `--verbose` explicitly or add it. See also B15 on the missing `--no-json`. | -| A21 | required-ness spelled with opposite defaults on the two sides | **open.** `flag.string` optional / `flag.requiredString`; `positional.string` required / `positional.optionalString` (lines 318–353). Unchanged. Still the clearest reads-cold failure in the file. | -| A22 | builder set incomplete | **resolved.** `flag.number` (line 325), `alias`, `default`, and `positional.variadic` (line 352) all added. Remaining absences (required `enum`, typed `repeated`) are defensible. | -| A23 | key→flag-name transliteration rule unstated | **open.** `alias`/`default` arrived; the rule that the record key becomes the flag name — and how `dryRun` becomes `--dry-run` — still appears nowhere. `hidden`/`deprecated` also still absent. | -| A24 | `raw` names the mechanism; three spellings for two states; `present` impossible-state | **resolved.** Split into three definition types with three `define*` functions (§7). The impossible state is now unrepresentable, which is the right fix. New findings on the split: B12, B13. | -| A25 | `handler` is really a loader | **open.** Still `handler: () => Promise<{ default: … }>` (line 403). The new `CommandHandler` helper (lines 420–424) is a good addition and makes the naming collision more visible, not less: `def.handler` is a loader, `CommandHandler` is the function type. | -| A26 | groups get a poorer declaration than commands | **open.** `groups: Record` unchanged (line 592). No `description`, no `examples`, no docs link — on either groups or commands. | -| A27 | `present` required even for `void` results | **resolved** by the session variant and by presentation moving to the return site. | - -### Mounting and testing - -| # | Item | Disposition | -|---|---|---| -| A28 | space-separated paths — right shape, needs a named type and an ordering rule | **partial.** Shape retained (correct). No `CommandPath` type, no stated grammar, no help-ordering rule. | -| A29 | `CommandSet` declared but unused; two meanings, one structure | **partial.** `CommandSet` now uses `AnyCommand` (line 580) but `createCli.commands` (line 593) and `createTestCli.commands` (line 638) are still two inline copies of the same structural type meaning *paths* rather than *names*. A reader still cannot tell the product-side map from the shell-side map. | -| A30 | shell can place a command but cannot rename it | **open.** | -| A31 | test harness is not the same machinery at the seam | **largely resolved.** `env`, `isTty`, `abort`, `answers`, and an injectable `now` all added (lines 642–658) — this is now a genuinely usable harness and the scripted-answers design ("a run that prompts past the script fails the test") is better than what I proposed. One residual: `config?: Record` is still the raw section map, not `LoadedConfig`, so a product still cannot test the invalid-section path — the exact R10 behavior the new `ConfigSection` machinery exists to produce. | -| A32 | `json: unknown[]` models the transport | **open**, and now more consequential — see B16. | - -### Missing concepts from round 1 - -| # | Item | Disposition | -|---|---|---| -| M1 | no success envelope | **resolved.** `SuccessEnvelope` / `ErrorEnvelope` (§9), with `warnings` aggregated from events and `nextSteps` derived from `nextActions` so the two cannot disagree. The strongest single improvement in the revision. | -| M2 | config version marker | **partial.** `LoadedConfig.diagnostics` now names "missing version marker" as a file-level problem and states that `section: null` fails every command (lines 626–627). The writer side (`defineConfig`) is still elsewhere by delegation, which is fine now that the reader side is explicit. | -| M3 | exit codes 4–99 | **resolved in policy, defective in typing.** The header declares 0/1/2/3, 4–99 per command, 130/143 for signals — a better answer than I asked for. The typing of `exitCode` is the subject of B7. | -| M4 | typed destructive confirmation | **open** (see A18). | -| M5 | poll timeouts | **open / accepted.** Still "the handler's business" (line 18), still with no engine-owned timeout error code or elapsed rendering. Worth recording as an accepted risk with a reason rather than leaving it as a parenthesis. | -| M6 | R13's dependency check | **resolved in placement, incomplete in shape** — `ctx.probeDependency` added (line 283). See B19. | -| M7 | telemetry / update-check | **open**, still unhomed; out of scope for this artifact. | -| M8 | durations | **open.** No `durationMs` anywhere; the engine can derive step durations now that steps have ids (A4 resolved), so this is smaller than it was. | - -### Referrals from round 1 - -R1 (variance of `CommandDefinition` in collections) — **addressed** by `AnyCommand` -with `any` generics; see B13 for what that costs. R2 (`ArgsOf` over optional -`positionals`) — **addressed** by the `Args` split with -required parameters and optional definition members; the principal-engineer pass -should still confirm `{}` defaults behave. R3 (does inference land) — still needs -a compiled example, now including `ctx.present`. R4 (`report` sync vs -backpressure) — **open**; the doc adds a sealing rule (line 89) but not a -backpressure answer. R5 (events after the signal) — **resolved in contract** -(lines 89–92). R6 (test determinism) — **resolved** (`now`, line 643). R7 -(`section: null`) — **resolved** (line 626). - ---- - -## Part 2 — Fresh findings on the v3 shape - -The move of presentation to the return site is a real improvement on the axis it -was chosen for. The v2 shape asked a presenter, declared in a different file at -startup, to reconstruct from a result value the case distinctions the handler had -already made — the classic "re-derive what you just knew" tax. Returning a -materialized view removes it, and the invariant it buys ("nothing product-authored -executes after the handler resolves — the engine receives values, never -callbacks", lines 23–24) is a strong, checkable property that also makes the -result snapshotable in tests. I would keep the ruling. - -The findings below are about what the new shape names things, what it now cannot -express, and one place where the file contradicts its own stated invariant. - -### The presented-result triple - -**B1 — the interface lost its static inventory of what a command can produce, and -nothing replaces it.** In v2 a reader (and a build step, and a docs generator) -could look at a definition and see every output shape the command has. In v3 the -views exist only inside a handler, at one or more return sites, behind a lazy -import. R5 still holds in its stated form — a product still cannot render — but -the second-order property R5 buys, *reviewable* consistency, now has no -attachment point. The platform generates help and docs from static descriptors -today (`shell/command-meta.ts`, `shell/help.ts`), and R8 makes the shell's job -"integration proof", which implies something checkable. -*Consequence of ruling 5, not an objection.* The mitigation is cheap and partly -present: `TestCli.run().presented` (line 669) gives per-command evidence, so a -product-repo conformance test can assert every command's human view is non-empty -and its stdout view is pipe-clean. Say so in the doc, so the loss is a deliberate -trade with a named replacement rather than a silent one. - -**B2 — `Views` and `PresentedResult['views']` are two different types with -the same name-word and the same member names — one is the recipe, one is the -dish.** (Lines 191–216.) `Views.human` is `(ui: Ui) => readonly Block[]`; -`views.human` is `readonly Block[]`. A reader who has learned one will misread the -other, and the compiler's error text when they are confused reads as nonsense -("Type '() => readonly Block[]' is not assignable to type 'readonly Block[]'"). -Symmetry probe fires: parallel names for non-parallel things. -*Alternative:* name the builders for what they are and the values for what they -are — `ViewBuilders` (or `Renderers`) supplied to `ctx.present`, and -`RenderedViews` inside the result. Two words, and the file reads cold correctly. - -**B3 — `Views` is generic in a parameter none of its members mention.** (Lines -211–216.) `human: (ui: Ui) => …`, `stdout: () => …`, `json: () => …`, `next: -() => …` — `T` appears nowhere. The views close over the data lexically, which is -the entire point of the ruling, so the parameter is decorative: `Views` and -`Views` are the same type, and `ctx.present(data: T, views: Views)` -gives a false impression that the views are checked against the data. -*Alternative:* drop the parameter — `Views` — and let `ctx.present(data: T, -views: Views): PresentedResult`. The relation between data and views is -lexical by design; the type should stop pretending otherwise. - -**B4 — `ctx.present` is a verb meaning "display it", on a method that displays -nothing.** (Line 258.) It selects which view functions to call and returns a -value; the engine displays. In an interface whose founding premise is "products -cannot print", the most confusable possible name is a method called `present` that -does not present. It also sits next to `ctx.report`, which *does* emit — two -sibling methods with two similar verbs and opposite effects. A handler that calls -`ctx.present(...)` and forgets to return it has silently produced nothing, and the -name actively encourages that mistake. -*Alternative:* `ctx.outcome(data, views)` or `ctx.result(data, views)` — noun-ish, -reads as construction, and pairs correctly with the returned type's name. - -**B5 — the mode→view-set mapping is prose, the mode set is not a type, and mode -combinations are undefined.** (Lines 186–189: "human mode → human + stdout + next; -`--quiet` → stdout; json mode → json + next".) Four problems, in ascending order -of importance. - (a) There is no `OutputMode` type anywhere. The engine's central dispatch — - which views get materialized, which renderer runs, whether prompts work — turns - on a union that is never declared. Discriminator-completeness fires on an - undeclared union. - (b) Combinations are undefined. `--json --quiet` is accepted today by the - platform (`resolveGlobalFlags` sets both) and resolved by json-first precedence - (`command-runner.ts:118-127`). `--json --verbose`, and non-TTY-auto-json plus - explicit `--quiet`, are the same question. The interface must name the precedence. - (c) The one **required** member of `Views` is `human` (line 212) — the one view - that json mode never materializes. The required/optional split runs opposite to - the mode mapping. - (d) `--quiet → stdout` alone means `next` is not materialized in quiet mode, so - next actions vanish. Almost certainly intended; it should be stated, because - `nextSteps`/`nextActions` are the survey's headline machine-facing asset. -*Alternative:* declare `export type OutputMode = 'human' | 'quiet' | 'json'`, put -the mapping in a table in that type's doc comment, and state the precedence when -several flags apply. - -**B6 — `--verbose` is in the injected flag family but has no view, so every -product's shipped verbose content becomes inexpressible.** (Lines 33–35 vs -211–216.) The shipped behavior is substantial and cited in the survey §C9/§D: the -ORM renders `timings` only under `-v`, truncates conflict lists to three with a -"re-run with -v" footer (`formatters/errors.ts:54-98`), and shows `docsUrl` only -under `-v` (`errors.ts:99-101`); the platform appends timing diagnostics under -`--verbose` (`command-runner.ts:136-139`). Under v3 the handler cannot see the -flag (ruling 1, correctly), `ctx.present` receives no mode information, and -`Views` has no verbose member. So the only verbose content that can exist is -engine-generated decoration. -*Alternative:* add `verbose?: (ui: Ui) => readonly Block[]` to `Views`, materialized -and appended in verbose human mode. That keeps the product supplying words and the -engine deciding whether to show them, which is exactly R5's division. This is the -most concrete gap the new shape creates. - -**B7 — `exitCode: (data: unknown) => number` is a callback the engine executes -after the handler resolves, in a file that says no such thing exists.** (Line 408 -vs lines 23–24: "Nothing product-authored executes after the handler resolves — -the engine receives values, never callbacks.") It is also the one place where the -lost result generic bites hardest: the definition is loaded at startup and cannot -name a type produced by a module loaded at execution, so the author writes -`(data: unknown) => number` and casts — a cast in the machine-facing contract R6 -exists to protect, where a wrong value silently produces a wrong exit code. -Answering the question directly: **no, `unknown` is not acceptable here, and the -fix is not to bring back `TResult`.** Apply the ruling's own argument -consistently: the outcome and its context are live at the return site, so the exit -code belongs there, where the data is typed. -*Alternative, and I think this is the clean split:* - - **Declaration of the space stays on the definition** — `exitCodes?: Readonly>`, a documented catalogue (`4: 'integrity check failed'`) that help and docs can render without executing anything, and that the engine can validate against the 4–99 range at build time. - - **Selection of the value moves to the return site** — `PresentedResult` gains `readonly exitCode?: number`, supplied through `ctx.present(data, views, { exitCode })` or as a fifth view. Typed, no cast, no post-resolution callback, and the header's invariant becomes true. - -**B8 — the erasure is total: `Handler` returns `Result, -…>`, so no command's data type survives anywhere.** (Line 418.) Three -consequences. `SuccessEnvelope` can never be instantiated with a real `T`. -`TestCli.run().presented?: PresentedResult` forces every product-repo -test to cast before asserting on its own data — in the harness R7 calls -first-class. And `CommandHandler`, the helper whose stated purpose is -keeping definition and handler "in lockstep", now locks only args and config. -The ruling requires the *definition* to be free of the result type; it does not -require the *handler* to be. -*Alternative:* `Handler` returning -`Result, …>`. An individual handler file keeps its type, -the definition stays result-free, and the erased form is only what the mount map -stores. Parameterize `TestCli.run()` the same way. - -**B9 — view functions are now ordinary closures called conditionally, which -creates a class of mode-dependent bug the old shape made impossible.** Only the -active mode's functions run (lines 202–203), so a view function with a side effect -fires in one mode and not another, and a `human` closure that computes something -the handler needs is silently skipped under `--quiet` or `--json`. The old shape -had the engine call presenters exactly once, outside the handler. Nothing in the -contract says view functions must be pure. -*Alternative:* state it in `Views`'s doc comment — "pure; called at most once; -only in the active mode" — so the rule is part of the contract rather than folklore. -(The enforcement question is a referral.) - -**B10 — failure got no presentation at all, yet `ErrorEnvelope` declares -`nextActions` with no way to populate it.** (Lines 555–564.) A failing handler -returns `notOk(error)` and the error carries ADR 239's `why`/`fix`/`docsUrl` — -one triple. But `ErrorEnvelope.nextActions: readonly NextAction[]` (line 563) has -no producer anywhere in the interface, and no failing command can render -structure. The survey's evidence is direct and load-bearing for two shipped -commands: `migration check` renders a per-failure list of `✗ [CODE] where: why` -plus a `fix:` per failure (`commands/migration-check.ts:604-698`), and `db verify` -renders a drift block on failure (`utils/formatters/verify.ts:140-158`). Neither -is portable to this interface. Symmetry probe fires as hard as it can: success -gained an entire presentation subsystem in this revision; failure lost the little -it had. -*Alternative:* `ctx.fail(error, views?)` producing a presented failure that -`notOk` carries, using the same `Views` vocabulary (`human` blocks + `next` -actions). It costs one method and makes `ErrorEnvelope`'s existing fields -truthful. - -### The three definition variants - -**B11 — the session variant contradicts two other paragraphs of the same file.** -(Lines 432–456.) (a) Lines 122–125 say severity-`warn` message events are -"aggregated by the engine into the success envelope's `warnings`"; a session has -no success envelope, so that sentence is false for sessions and the warnings a -long-running session emits have no terminal home. (b) "A session always supports -json mode: the event stream is its json surface" — but the platform's shipped -streaming runner emits a terminal `{type:'success'|'error'}` frame precisely so a -machine consumer knows the stream ended and how (`command-runner.ts:208-235`), -with exactly one command opting out because it carries its own terminal record. -The interface removes that guarantee without saying so. -*Alternative:* state that the engine emits a terminal frame for sessions in json -mode, and say where a session's warnings go (a terminal frame is the natural -answer, which resolves both halves at once). - -**B12 — the three definitions duplicate their common members by copy, and the raw -variant's omissions look accidental rather than decided.** (Lines 380–492.) -`brief`/`description`/`examples`/`flags`/`positionals`/`configSection` are written -out three times with three different subsets. `RawCommandDefinition` has no -`examples`, no `positionals`, and — the substantive one — **no `configSection`, -and its `io` object carries no config**. The one command in the corpus that -motivates this variant is `lsp`, and a language server is precisely the consumer -that must read the user's `prisma.config.ts`. As written, a raw command cannot -obtain config at all without reading disk, which R4 forbids. -*Alternative:* extract a shared `CommandCommon { brief; description?; examples?; -docsUrl? }` that all three extend (so A26's future additions are one edit, not -three), and decide `configSection` for raw deliberately — I believe it must be -allowed, with the validated value handed in on `io`. - -**B13 — `AnyCommand` is a union with no discriminant, and the engine cannot tell -its members apart at runtime.** (Lines 496–499.) `CommandDefinition` without an -`exitCode` and `SessionCommandDefinition` have *identical* runtime shapes: the -same members, and `handler` differing only in a return type that does not exist at -runtime. `RawCommandDefinition` is distinguishable only by the absence of -`positionals`/`configSection`, which are optional on the others. So `createCli` -receives a map of `AnyCommand` and has no reliable way to decide whether to inject -the shared flag family, whether to run the presentation pipeline, or whether to -hand the process's streams over. Using `any` in the erasure (the right pragmatic -fix for round-1's variance problem) removes even the type-level distinction. -*Alternative:* have the three `define*` functions stamp a discriminant — -`kind: 'value' | 'session' | 'raw'` — so `AnyCommand` is a real discriminated -union. They are identity functions today; making them not-quite-identity is a -one-line change and buys an exhaustive `switch` in the engine and in any -future tooling that walks a command set. - -### Engine modes, flags, and the machine contract - -**B14 — there are now two product-facing routes to stdout with different -vocabularies and no stated rule for choosing.** `views.stdout` (terminal, lines, -line 195) and `output` events with `channel: 'data'` (streaming, lines, documented -as "routed to OUR stdout", lines 84–87). A command that streams data lines and -also returns a payload writes to stdout through both. This is a consequence of -accepting ruling 4, not an argument against it. -*Alternative:* one sentence — streaming data uses `output`/`data`; the terminal -payload uses `views.stdout`; and state the ordering guarantee between them. - -**B15 — json mode auto-selects on a non-TTY stdout and the flag family provides -no way to turn it off.** (Lines 31–32, 601; family at lines 33–35.) Piping any -command now changes the output's shape, so `prisma … | less`, `| tee run.log`, or -`| head` all get json rather than the human rendering. The ORM does this today but -ships `--format pretty` as the escape hatch (survey §D, `terminal-ui.ts:334`); the -platform does not auto-select at all. The interface adopts the more aggressive -behavior and removes the escape hatch. It is also internally asymmetric: both -other environment-sensing flags in the family have negative forms -(`--no-interactive`, `--no-color`) and this one does not. -*Alternative:* add `--no-json` to the injected family. (Related: `--trace` is -absent from the family though it is shipped and named in shipped error output — -see A20.) - -**B16 — the json stream's frame vocabulary is ambiguous, and `EventFrame.data` -collides with the event's own `data`.** (Lines 566–573.) The doc says "in json -mode everything is one framed stream on stdout" (line 87), but `SuccessEnvelope` -and `ErrorEnvelope` carry no frame fields (no `type`, no `timestamp`), and -`EventFrame.type` is typed `EngineEvent['kind']`, which cannot express a terminal -success or error frame. The platform's shipped frames do include them -(`{type:'success'|'error', command, timestamp, …}`, `command-runner.ts:213-235`). -So a consumer cannot tell from the types whether the envelope is a frame in the -stream or a separate object after it — in the one contract that must be -unambiguous, because agents and CI parse it. Separately, `EventFrame.data: -EngineEvent` nests an event that itself has a `data` member, so a consumer reads -`frame.data.data` to reach the product extension; the platform's `data` meant the -payload. -*Alternative:* declare the frame union explicitly — `type: EngineEvent['kind'] | -'success' | 'error'` with the envelope frames included — and rename -`EventFrame.data` to `event`. - -**B17 — nothing owns config-section name collisions.** (Lines 228–241, 589–594.) -Commands carry their own tokens and `createCli` never sees a section list, which -is a better design than the registry I proposed in round 1. But two products -registering the same `name` with different validators is now silently -last-one-wins, or worse, order-dependent. `createCli` already promises build-time -failure for collisions, unknown groups, and grammar violations (lines 585–587) — -add section-name conflicts to that same sentence. - -**B18 — `probeDependency` returns a bare boolean, so the product cannot write the -error R13 requires.** (Lines 281–283.) R13 mandates "a structured error naming the -dependency and how to install it **with the user's own package manager**". Package -manager detection is environmental, and R4 forbids products from reading the -environment — so the handler literally cannot know whether to say `npm add`, -`pnpm add`, `yarn add`, or `bun add`. The probe as designed can only produce half -the required error. -*Alternative:* `requireDependency(specifier): Promise>` — the engine detects the package manager and builds R13's -error, the handler just propagates it. Keep the boolean probe as well if commands -need to branch rather than fail. - -**B19 — `PresentedResult` claims a single constructor that the type does not -enforce.** (Lines 179–199: "Built exclusively by ctx.present".) It is a public -exported interface with all-optional views, so it is hand-constructible and the -"exclusively" claim is a comment. That matters because the mode contract lives in -which views are populated: a hand-built result with a `human` view in json mode is -a silently wrong state. -*Alternative:* brand it with a private symbol, the technique the file already uses -twice for `FlagSpec` and `PositionalSpec`. `TestCli.run().presented` can still -expose it for reading. - ---- - -## Part 3 — Referrals to the principal-engineer pass - -- Does `ctx.present`'s inference land? `present: (data: T, views: Views)` - with `Views` not mentioning `T` (B3) means `T` is inferred solely from `data` - — confirm the returned `PresentedResult` narrows as intended in a real - handler, including the union-of-return-sites case. -- Runtime discrimination of `AnyCommand` members (B13) — confirm whether the - engine can in fact tell them apart today; if it can only do so by probing - optional members, that is a correctness bug, not just a typing one. -- `report()` backpressure for a session emitting thousands of `output` events into - a slow pipe (round-1 R4, still open). -- The sealing rule (line 89) says calling `report()` after resolution throws - `InternalError` — check that a `finally` block or an unawaited promise in a - handler cannot trip it as a matter of course. -- View functions invoked conditionally (B9): whether the engine can detect - impurity, and what happens if a view function throws — mid-render failure after - a successful operation is a nasty state. -- `Exclude` (line 130) in a published declaration file: confirm - it emits readably for consumers rather than as an opaque conditional type. - ---- - -## Verdict - -v3 is a large improvement over v1, and most of it is the revision doing exactly -what the reviews asked: R10 is now structural, the success envelope exists, the -severity scale is single, remediation has one shape, the impossible -`raw`-plus-presenter state is gone, and the test harness became a real one. The -round-1 items that remain open are mostly small and mostly cosmetic — `Ui`, the -required-ness asymmetry, group declarations, path typing. - -The new shape — presentation at the return site — is the right call on the axis it -was chosen for, and the invariant it buys ("the engine receives values, never -callbacks") is worth having. My substantive concerns are that the invariant is not -yet true, and that the move left two holes it did not intend to leave. - -The invariant is not yet true because `exitCode` is a product-authored callback -the engine runs after the handler resolves, in the same file that declares no such -thing exists (B7). Moving the *selection* of the exit code to the return site -where the data is typed, and leaving a documentable *catalogue* on the definition, -resolves the contradiction and the `unknown` cast at once. That is my primary -recommendation. - -The two holes are `--verbose`, which is in the injected flag family but has no -view, so every product's shipped verbose content becomes inexpressible (B6); and -failure, which gained nothing while success gained a subsystem, leaving -`ErrorEnvelope.nextActions` with no producer and two shipped commands -(`migration check`, `db verify`) unportable (B10). Both are closed by adding one -member each — `Views.verbose` and `ctx.fail(error, views)`. - -Below those, three naming problems will cost every future reader: `ctx.present` -displays nothing (B4), `Views` and `views` are the recipe and the dish under one -word (B2), and `Views` is generic in a parameter it never uses (B3). And one -defect is mechanical rather than aesthetic: `AnyCommand` is a union whose members -are runtime-indistinguishable, so the engine cannot reliably tell a session -command from a value command (B13) — a stamped `kind` discriminant fixes it in a -line. - -Close B7, B6, B10, and B13, apply the three renames, and this interface expresses -its requirements. Nothing here argues for another structural revision. diff --git a/.drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review-r3.md b/.drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review-r3.md deleted file mode 100644 index 9da8718e..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review-r3.md +++ /dev/null @@ -1,420 +0,0 @@ -# System design review, round 3 (final) — the unified CLI engine's public interface (v4) - -Subject: `wip/designs/engine/engine-interface-draft.ts` (v4), against `-v3.ts` and -my round-2 artifact `./reviews/system-design-review-r2.md`. - -Pass: **architect**, same probes throughout: discriminator completeness, -consumer-vs-essence, concept-vs-mechanism, symmetry, reads-cold. - -The operator rulings are settled and I do not re-argue them. Ruling 1 in -particular — completed/errored replacing success/failure, with `ctx.fail` -rejected — is not merely accepted here: it is a better answer than the one I -proposed, for reasons I set out under the disposition of B10. - -**Headline: no structural concerns remain.** One item needs reconciliation with a -settled ADR before implementation (C1, narrowed by the `errors` block amendment — -see C8), two are small capability or typing defects with one-line fixes (C3, C4), -and everything else in this document is a nit. The verdict section says so plainly. - -This round includes the operator-directed amendment that landed mid-review: the new -`Block` member `{ kind: 'errors', errors: CliStructuredError[] }`. It is probed in -**C8**, and it changes the disposition of C1 for the better. - ---- - -## Part 1 — Disposition of round-2 findings - -| # | Item | Disposition | -|---|---|---| -| B1 | The interface lost its static inventory of what a command can produce | **Largely resolved.** The half that mattered for machines is now static and documented: `outcomeCodes` is a definition-level catalogue "rendered in help without executing anything" (lines 445–451). What is still dynamic is the human/stdout presentation, and `TestCli.presented` (line 760) is the stated replacement for checking it per command. Nit-level residue only. | -| B2 | `Views` (recipe) and `views` (dish) under one word | **Resolved.** `Presentations` for the input bundle (line 223), `presentation` for the materialized field (line 204). The two now read as different things because they are named as different things. | -| B3 | `Views` generic in a parameter no member mentions | **Resolved.** `Presentations` is non-generic; `ctx.present(data: T, presentations: Presentations, …)` (lines 272–276) now says exactly what is true — `T` comes from the data, the presentations close over it lexically. | -| B4 | `ctx.present` is a verb for a method that displays nothing | **Resolved in effect.** The name is unchanged, but the vocabulary around it changed and that was the actual problem. `ctx.present(data, presentations)` producing a `presentation` field reads as construction because the noun is now on both sides of the call. I withdraw the finding. | -| B5 | Mode set undeclared; combinations undefined | **Largely resolved.** `Format = 'human' \| 'json'` and `LogLevel` are declared types (lines 71–74), and format and log level are now cleanly orthogonal axes rather than one overloaded "mode". Two residual undefined combinations, both nits: C9 (`--json --quiet`) and C10 (whether `--quiet` implies a log level). | -| B6 | `--verbose` injected but with no presentation member | **Resolved as ruled (ruling 3), and better than my proposal.** One mechanism — severity-`verbose` `message` events filtered by `--log-level` — beats a second presentation member, because it keeps the product supplying words and the engine deciding display in exactly one place. One consequence worth knowing: verbose detail must now be *emitted during the run* as commentary rather than *composed into the final result blocks*, so the ORM's "truncated to 3, re-run with -v" pattern becomes a verbose message event on stderr rather than an expanded list inside the result. That is a fine trade; it should just be a known one. | -| B7 | `exitCode: (data: unknown) => number` — a post-resolution callback in a file that says none exist | **Resolved exactly as recommended.** Catalogue on the definition (`outcomeCodes`, line 451), typed selection at the return site (`ctx.present`'s `outcomeCode`, line 275). The header's invariant "nothing product-authored executes after the handler resolves" (lines 36–37) is now true. | -| B8 | Total erasure — no command's data type survives | **Open, nit.** `Handler` still returns `Result, …>` (line 466) and `TestCli.presented` is `PresentedResult` (line 760), so product-repo tests cast before asserting on their own data. A defaulted fourth parameter (`Handler`) would fix it without touching the definition. Small enough to leave. | -| B9 | Presentation functions are conditionally-invoked closures with no purity rule | **Open, nit.** The `Presentations` doc (lines 213–222) still does not say "pure, called at most once, only for the active format". One sentence. | -| B10 | Failure got no presentation while success gained a subsystem | **Overruled by ruling 1 — and dissolved, not merely rejected.** This is the right call and I want to record why, because it is the best move in the revision. My finding rested on two shipped commands (`migration check`, `db verify`) needing to render structure on the error path. Under completed/errored semantics they are not on the error path: they executed to their end, they have a result, and bad news is a result. They present normally and carry an outcome code. The finding's premise was that "did not succeed" and "did not complete" were the same thing; the ruling separates them, which is a genuinely better model than adding a parallel presentation system for failures. The second half — `ErroredEnvelope.nextActions` having no producer — is answered too: remediation events aggregate into it, and the error's `fix` derives `nextSteps` (lines 637–639). Both halves closed. | -| B11 | The session variant contradicted two other paragraphs | **Resolved.** The result frame is now universal ("events while running, then exactly one result frame", lines 642–643), so a session does get a terminal frame and its `warn` messages do have an envelope to aggregate into. The two contradictions are gone. One typing nit remains: C8. | -| B12 | Three definitions duplicate common members; raw has no config | **Substantively resolved.** Raw gained `configSection` and `io.config` (lines 531, 541), which was the real problem — an LSP can now read the user's config without touching disk. The literal duplication of `brief`/`description`/`flags`/… across three interfaces remains, and raw still has no `examples`. Nit. | -| B13 | `AnyCommand` members are runtime-indistinguishable | **Resolved.** `kind: 'command' \| 'session' \| 'raw'` is a required member of each interface and stamped by the `define*` functions via `Omit<…, 'kind'>` (lines 427, 478, 493, 514, 527, 551). This is the cleanest possible form of the fix: authors never write it, the engine can `switch` on it, and the union is genuinely discriminated. | -| B14 | Two product-facing routes to stdout with no rule for choosing | **Partial, nit.** Both routes are documented (lines 89–90, 218–219) but the ordering guarantee between streamed `output`/`data` lines and the terminal `presentation.stdout` lines is still unstated. | -| B15 | json auto-selects on non-TTY with no escape hatch | **Resolved by ruling 2.** `--format ` gives the escape hatch (`--format human`) that `--json` alone could not, and `--json` survives as the shorthand everyone already types. Note `--trace` is now absent from the injected family, which I read as the deliberate consequence of ruling 3's one-log-mechanism decision; worth one line confirming stack traces appear at `--log-level verbose`. | -| B16 | Frame vocabulary ambiguous; `frame.data.data` | **Resolved.** `Frame = EventFrame \| ResultFrame` (line 644), `EventFrame.event` (line 651), `ResultFrame.envelope` (line 658). The machine contract now says exactly what is on the wire. | -| B17 | Config-section name collisions unowned | **Partial, nit.** `createCli`'s doc lists "collisions, unknown groups, reserved-flag violations, and grammar violations" (lines 674–676); "collisions" reads as path collisions. Name section-name conflicts explicitly in that sentence. | -| B18 | `probeDependency` cannot phrase R13's install command | **Resolved.** `ctx.packageManager` (line 306), and the `probeDependency` doc now points at it (lines 299–302). | -| B19 | `PresentedResult` hand-constructible despite the "exclusively" claim | **Resolved.** Branded with a `PRESENTED` unique symbol (lines 183, 199) — the same technique the file already used twice, so it reads consistently. | - -### Round-1 items still open in v4 - -All nit-level. `Ui` is unchanged since v1 — still no masking or path-relativization helper and `dim` is still an ANSI word (A12, A7). `list` remains the one `Block` with no cited evidence (A11). `signal` still appears on both `Runtime` and `CommandContext` without stating their relationship (A16). `handler` is still a loader named for what it returns (A25). Groups gained `description` (line 681) but still no `examples`, and neither groups nor commands carry a docs link (A26). The shell still cannot override a command's `brief` at the mount (A30). Poll timeouts remain the handler's business (M5). - -Three round-1 items closed in v4 that I should record: **A21** (required-ness asymmetry) is settled by naming it — lines 353–356 now state it is deliberate and matches CLI convention, which is a legitimate resolution of a reads-cold problem; **A23** (transliteration) is settled by line 50; **M4** (typed destructive confirmation) is settled by the prompt-defaults design (lines 316–327), and settled *better* than I proposed: "destructive confirmations simply declare no default — `--yes` can never blast through them". That inverts the problem so the safe case is the default case, which is the right shape for a rule about destructive operations. - ---- - -## Part 2 — Fresh findings on the v4 shapes - -### C1 — A completed-but-bad result carries an integer where the settled conventions carry a dotted code. This needs reconciling with ADR 239. - -Ruling 1 moves a class of outcomes off the error path: `migration check` finding 16 -integrity violations, and `db verify` finding drift, are now completed results with -outcome codes (lines 20–23). ADR 239 currently classifies exactly those outcomes the -other way. Its exit-code section reads: "Expected `StructuredError` failures (usage, -config, precondition, **verify, runner**) → **2**", and its crosswalk assigns them -dotted codes today — `CONTRACT.VERIFY_FAILED`, `MIGRATION.RUNNER_FAILED`, and the -eighteen `MIGRATION.CHECK_*` codes converted from `PN-MIG-CHECK-NNN`. - -Two consequences, one of which matters. - -The one that does not: per-item codes survive. `migration check`'s shipped json -already carries `failures[{ space, code, where, why, fix }]`, and that lives inside -`data`, so a consumer can still match individual violations by dotted code. - -The one that does: **at the envelope level, a completed-but-bad result has no -`code` at all.** R6's own justification names three keys machine consumers branch -on — "agents, CI — branch on `ok`, `code`, and exit codes; that only works if -exactly one code space and one envelope exist." Under v4, the errored path provides -all three and the completed-with-outcome path provides two: `ok: true` and an -integer whose meaning is per-command and shares a numeric space (4–99) with every -other command's unrelated outcomes. A CI job that wants "did any Prisma command -report an integrity failure" can match `MIGRATION.CHECK_*` today and cannot match -anything tomorrow without knowing which command produced the 4. - -*Alternative, one field:* let the catalogue carry the dotted code alongside the -meaning, and surface it on the envelope. - -```ts -readonly outcomeCodes?: Readonly> -``` - -with `CompletedEnvelope` gaining `readonly outcome?: { code: string; meaning: string }` -populated from the catalogue entry for the selected code. That keeps ADR 239's -single code space intact across both envelopes, costs nothing at the return site -(the handler still selects an integer), and makes the help rendering strictly -better because it can print the code next to the meaning. - -Whether ADR 239's exit-code paragraph should also be amended (it currently sends -verify and runner failures to exit 2, which v4 sends to 4–99) is a decision for the -ADR's owner, not for this interface — but the two documents currently disagree and -one of them has to move. Flagging it as the one item to settle before -implementation. - -**Narrowed by the `errors` block amendment.** The new `Block` member carries real -`CliStructuredError` values inside a completed result, so the dotted code space now -*does* have a first-class home on the completed path — which is direct evidence -that the design already agrees dotted codes belong there. What remains of C1 is -smaller than when I wrote it: the per-finding codes are handled, and only the -envelope-level outcome lacks a dotted counterpart. C8's fourth point proposes -engine-side aggregation that would close most of what is left. - -### C2 — `ok` now means three different things in this file, and the envelope union has no name. - -`Result.ok` (foundation: no error), `SectionValidation.ok` (line 249: the section -validated), and `CompletedEnvelope.ok` / `ErroredEnvelope.ok` (lines 615, 631: the -command ran to its end). The third is narrower than the first, and a reader arriving -from ADR 239 or design 1a — where `ok: true` means "succeeded" — will misread it. -The doc comments do compensate (lines 612–614, 630), and keeping the wire field -named `ok` is correct because it is the shipped, settled envelope field. - -Two small things worth doing anyway. First, state the semantic shift once, in a -prominent place — the header's EXECUTION PROTOCOL section is the natural home, and -it nearly does this already; one sentence saying "`ok` on the envelope means -completed, which is narrower than `Result.ok`" removes the trap. Second, declare -the union: `ResultFrame.envelope: CompletedEnvelope | ErroredEnvelope` (line 658) -writes it inline, so consumers of the json contract have no exported name for the -thing they parse. `export type Envelope = CompletedEnvelope | ErroredEnvelope`. - -Nit. - -### C3 — `SingleChar` does not express what it claims, and may not typecheck as intended. - -```ts -export type SingleChar = string & { readonly length?: 1 } -``` -(Lines 382–383.) `string` already carries `length: number`; intersecting an optional -`length: 1` on top does not constrain a string literal's length, because a literal's -apparent `length` is `number`, not `1`. Depending on how the checker resolves the -apparent member, this either rejects every string or constrains nothing — and the -doc comment concedes the real check is at construction ("longer strings are a -construction error"). A type that advertises a constraint it does not enforce is -worse than no type: a reader will trust it. - -*Alternative:* either drop it and use `alias?: string` with the doc sentence and the -existing construction-time check (honest, and the check already exists), or, if a -compile-time guarantee is genuinely wanted, enumerate the alphabet as a literal -union — verbose but precise and machine-generatable. I would drop it. - -Referral: the principal-engineer pass should confirm the actual checker behavior -before deciding which way to go. - -### C4 — `InputStream` as `AsyncIterable` cannot support the prompts the engine promises. - -§8 (lines 597–605) replaces `NodeJS.*` with structural types for runtime -agnosticism, which is right and serves R4's "why" directly. But `Runtime.stdin` is -the only input the bin injects (line 695), and interactive prompts as shipped — -`@clack/prompts`, used by both families — need raw-mode keypress access to draw a -`select` with arrow keys or to intercept Ctrl-C at the prompt. An -`AsyncIterable` can deliver lines; it cannot put a terminal into raw mode. -So the interface, as typed, admits only line-oriented prompts, while §4a describes -a prompt surface with `select` over labelled options and a distinct -cancel-at-the-prompt error (lines 316–342) that presumes keypress handling. - -`OutputStream.write(text): void` is fine by comparison — cursor control and the -liveness display are ANSI escapes and go through `write` — and the missing return -value is the documented accepted trade (line 96). - -*Alternative, one optional member:* extend the input type with the capability rather -than the mechanism — - -```ts -export interface InputStream extends AsyncIterable { - /** Present only on a terminal; the engine degrades prompts without it. */ - readonly setRawMode?: (raw: boolean) => void -} -``` - -— which keeps the surface runtime-agnostic (a non-TTY runtime simply omits it) and -makes the degradation path explicit rather than accidental. - -### C5 — `outcomeCode` is checked against the catalogue at runtime when it could be checked at compile time. - -(Lines 270–271, 451.) `ctx.present`'s `opts.outcomeCode?: number` is verified by the -engine against the definition's catalogue. That is a real improvement on v3's -`(data: unknown) => number` — the catalogue is static, help can render it, and the -range is checkable at construction. But this is the machine-facing exit contract, -and a typo'd `44` for `4` is currently a runtime failure in the one place where a -wrong value is silently meaningful. - -*Alternative, if it is cheap:* thread the catalogue's key union through the context — -`CommandContext`, with `TOutcome` inferred -from `keyof CommandDefinition['outcomeCodes']` and `present`'s option typed -`outcomeCode?: TOutcome`. That makes an undeclared code a compile error and keeps -everything else unchanged. If threading a second parameter through `Handler` and -`CommandHandler` proves awkward, the runtime check is acceptable — the catalogue -being static is what mattered. Referral to the principal-engineer pass for the -feasibility call; nit either way. - -### C6 — Two undefined combinations in the format/level matrix. - -Both one-line documentation fixes. - -(a) `--json --quiet` is not in the materialization table (lines 194–196: human, -human+`--quiet`, json). The platform resolves the equivalent by json-first -precedence (`command-runner.ts:118-127`); say so. - -(b) The relationship between `-q/--quiet` and `--log-level` is unstated. As written -they are orthogonal — `--quiet` selects which *presentation* materializes, -`--log-level` filters *commentary* — which is a clean split and better than the -shipped CLIs manage. But it leaves `--quiet` alone still emitting step lines and -progress at level `info`, which is probably not what a user typing `--quiet` -expects. Either state that `--quiet` implies `--log-level error`, or state -explicitly that it does not. - -Related nit: at `--log-level warn`, every non-`message` event kind is suppressed -(line 92 puts them all at `info`), so steps, progress, endpoints, artifacts, and -status transitions all vanish together. That is defensible but is a fairly blunt -grouping for six distinct event kinds; if evidence later shows users want progress -without step chatter, the per-kind level assignment is where to look. - -### C7 — Residual asymmetries and small gaps - -All nits, grouped for brevity. - -- **`createTestCli.config` is still `Record`** (line 726) while - `Runtime.config` is `LoadedConfig` (line 702). So a product repo still cannot - test the invalid-section path — the flagship behavior the new `ConfigSection` - machinery exists to produce, and the one R10 calls out. Accepting - `LoadedConfig | Record` closes it. Also `credentials?: Credentials` - (line 727) is a value where `Runtime` has a `getCredentials()` function, so token - refresh cannot be exercised. -- **A session's completed envelope shape is unstated.** `CompletedEnvelope.result: T` - is required (line 619) and a session returns `Result`; say that a session's - result frame carries `result: null, outcomeCode: 0`. -- **Two sources feed `nextActions` on the completed path** — aggregated `remediation` - events (lines 152–153) and `presentation.next` (line 227) — with no stated order or - duplicate rule. -- **`requiresCredentials`** (line 443) is a good addition, but `getCredentials()` - still returns `Credentials | undefined` even for commands that declared it, so - those handlers still handle an impossible `undefined`. Documenting the guarantee - is enough; tightening the type is not worth threading another parameter. -- **`CommandSet` and `MountedTree`** (lines 665–670) are mutually assignable aliases, - so "distinct alias so the two maps never read as one" is true for readers and not - for the compiler. That is fine and I would not brand it — the mount map is written - once, in one file, and reviewed as a literal. -- **`CompletedEnvelope` / `ErroredEnvelope`** name two different axes (completion, - erroring) for one distinction. `Completed`/`Errored` is acceptable and every - alternative I can construct on a single axis is worse. Recording that I looked. - -### C8 — The new `errors` Block member (operator amendment) - -```ts -| { readonly kind: 'errors'; readonly errors: ReadonlyArray } -``` - -**The move is right, and one part of it is the best thing in §7.** Handing the -engine real `CliStructuredError` values and letting it render them with the same -layout it uses for top-level errors is exactly R5's argument applied where it was -previously leaking: without this, `migration check` and `db verify` would have had -to hand-format `✖ summary (CODE)` / Why / Fix into `Block.list` strings, and the -two error layouts in the CLI would have drifted within a release. The block takes -no `Ui` and needs none — the engine owns the layout completely, which is the -correct consequence of "products never hand-build error presentation". Five -observations, in order of importance. - -**1. The name puts the word "error" on both sides of the file's central -distinction.** v4's whole model turns on ERRORED (did not complete) versus -COMPLETED (ran to its end, possibly with bad news). This block lives strictly on -the completed side and is called `errors`. A reader who has just learned that -distinction meets `kind: 'errors'` inside a completed result and has to un-learn it. -Reads-cold fires, and so does consumer-vs-essence: every other `Block` member names -a *layout* the engine draws (`summary`, `fields`, `table`, `list`, `tree`), while -this one names its payload's type. - -The word the rest of the system already uses for exactly this concept is -**diagnostics**: `SectionValidation.diagnostics` and `LoadedConfig.diagnostics` in -this same file (lines 249–250, 713), and the ORM's shipped `migration status` -`diagnostics[]` with per-item `hints[]` (`json/schemas.ts:78-103`). All three mean -the same thing — structured findings produced by a run that completed. -*Alternative:* `kind: 'diagnostics'`, same payload. One word, and the completed and -errored paths stop sharing a root. - -**2. It does invite misuse as a substitute for `notOk`, and the guardrail is -missing rather than weak.** A handler that hits a genuine did-not-complete condition -can now write `ok(ctx.present(data, { human: () => [{ kind: 'errors', errors: [e] }] }))` -and exit 0. It renders identically to a real error, so a human cannot tell; the only -signals that differ are the envelope's `ok` and the exit code — which are precisely -what agents and CI branch on. So the failure mode is invisible to people and wrong -for machines, which is the worst combination. - -Two fixes, both cheap and neither requiring a new concept. -- *Write the test down.* The distinguishing question is crisp and currently unstated: - use `notOk` when the command could not do its job; use this block when finding - these was the job. One sentence in the doc comment. -- *Make the engine check it.* The engine renders the block, so it can see it. Require - that a completed result containing severity-`error` entries also carries a non-zero - `outcomeCode` from the catalogue — verifiable at the same point the engine already - verifies the code against the catalogue (line 271). That turns "don't smuggle - failures through the completed path" from advice into a rule, and it costs nothing - for legitimate uses, which all have an outcome code anyway (`migration check` exits - 4, `db verify` exits on drift). - -**3. `CliStructuredError` carries ADR 239's optional `severity`, so this block is not -always errors — which reinforces point 1 and raises a second question.** Config -diagnostics and lint findings are routinely `warn` or `info` (ADR 239 keeps those -values specifically for "advisory lint/budget surfaces"). So `kind: 'errors'` will -frequently carry non-errors. And it is unstated whether a `warn`-severity entry here -aggregates into `CompletedEnvelope.warnings`, which today is fed only by -severity-`warn` `message` events (line 622). Two producers of the same concept, one -aggregating and one silent. Say which. - -**4. The data/json side is a convention where it could be structural — and this is -where the amendment can pay for itself twice.** The doc instructs the product: "In -the data/json side, carry the same errors as their envelopes (`toEnvelope()`)." -That is unenforced, and it is the second place in the file where the human and json -renderings of one result can silently disagree (the first being `presentation.json` -overriding `data`). But the engine already holds these values — and it already -performs exactly this kind of aggregation twice, pulling `warnings` from `message` -events and `nextActions` from `remediation` events. - -*Alternative:* aggregate them the same way. `CompletedEnvelope` gains -`readonly diagnostics: readonly CliErrorEnvelope[]`, populated by the engine from -the block. Consistency becomes structural rather than remembered, the instruction to -products disappears, and — the second payment — the dotted codes reach machine -consumers on the completed path automatically, which is most of what C1 was about. -I would rank this the single most valuable follow-up in this document. - -**5. Minor asymmetry with the errored path.** On the errored path the engine derives -`nextSteps` from the error's `fix` (line 638). On the completed path a block may -carry N errors each with its own `fix`, and none of them feed `nextSteps` or -`nextActions`. Almost certainly deliberate — N fixes would flood the envelope — but -it means a user gets "Fix:" lines in the human rendering that have no machine -counterpart, which inverts the usual direction of that gap. One sentence stating the -rule is enough. - ---- - -## Part 3 — Referrals to the principal-engineer pass - -- `SingleChar`'s actual checker behavior (C3) — does it reject every literal, accept - everything, or something else? -- Feasibility of threading the outcome-code union through `CommandContext` (C5). -- Whether `ctx.present`'s inference lands across a handler with several return - sites, now that `Presentations` is non-generic (B3's fix changes the inference - shape). -- `Omit, 'kind'>` as the `define*` parameter (lines 478, 514, - 551): confirm `Omit` over a generic interface preserves the mapped `flags`/ - `positionals` inference rather than widening it. -- The buffered, non-backpressuring `report()` (line 96) against a session emitting - into a slow pipe — the trade is stated; the drop or unbounded-growth behavior is - not. -- Second-signal force-exit (lines 55–57) interacting with the "calling report() - after resolution is an InternalError" rule during teardown. - ---- - -## Verdict - -**Nothing structural remains. This is a clean pass.** - -v4 closes every substantive finding from both prior rounds. The two I called -structural in round 2 are closed in the strongest available way: the outcome-code -catalogue with return-site selection makes the header's no-callbacks invariant -actually true, and the `kind` discriminant stamped by the `define*` functions turns -`AnyCommand` into a union the engine can genuinely switch on. The naming problems -are gone — `Presentations` and `presentation` are the recipe and the dish under two -words, and the surrounding vocabulary rehabilitated `ctx.present` without renaming -it. - -Three rulings deserve to be recorded as improvements on what the reviews asked for, -not merely as decisions. Completed/errored semantics dissolved my B10 rather than -overruling it: separating "did not succeed" from "did not complete" is a better -model than bolting a second presentation system onto the error path, and it makes -the bad-news commands in the corpus expressible as what they are. One log mechanism -beat my proposed `verbose` presentation member, because it keeps display policy in -one place instead of two. And the prompt-default rule — a destructive confirmation -declares no default, so `--yes` structurally cannot pass it — is a better answer to -typed destructive confirmation than the `confirmDestructive` method I proposed, -because it makes the safe case the default case rather than an opt-in. - -The late `errors` block amendment is the same kind of improvement: giving the engine -real `CliStructuredError` values to render, rather than letting products format -`✖ summary (CODE)` into strings, closes the last place where the two error layouts -in the CLI could drift. Its problems are a name and a missing check, not a shape — -call it `diagnostics` (the word the config machinery and the ORM already use, and it -stops the completed path sharing a root word with the errored path), and have the -engine require a non-zero outcome code when the block carries severity-`error` -entries, so the completed path cannot be used to smuggle failures past `ok`. - -One item to settle before implementation, and it is a reconciliation rather than a -redesign: **C1**. Moving verify, runner, and check outcomes off the error path means -a completed-but-bad result reaches machine consumers with an integer and no dotted -code at the envelope level, while ADR 239 currently classifies exactly those -outcomes as structured failures with `CONTRACT.VERIFY_FAILED` / -`MIGRATION.RUNNER_FAILED` / `MIGRATION.CHECK_*` codes and exit 2. R6 names `code` as -one of the three keys consumers branch on. The `errors` block narrows this -considerably — the per-finding codes now have a home — and the cleanest completion -is C8's fourth point: have the engine aggregate that block into a -`CompletedEnvelope.diagnostics` field the same way it already aggregates warnings -from `message` events and next actions from `remediation` events. Adding the dotted -code to the outcome catalogue entries closes the remainder. The ADR's exit-code -paragraph then needs a corresponding amendment, which is its owner's call. - -Below that, two small defects worth fixing while the file is open: `SingleChar` -claims a constraint it does not enforce (C3), and `InputStream` as a plain -`AsyncIterable` cannot support the keypress-driven prompts §4a describes -(C4) — one optional `setRawMode` member resolves it and keeps the surface -runtime-agnostic. - -Everything else in this document is a nit: undefined `--json --quiet` precedence, -the `--quiet`/`--log-level` relationship, `Ui`'s missing masking helper, the test -harness's config shape, an unnamed envelope union, whether `warn`-severity entries -in an `errors` block reach the envelope's `warnings`, and a handful of one-sentence -documentation additions. None of them should hold up implementation, and several -are better decided against real usage than in the abstract. - -I have no further architectural concerns. The loop can close. diff --git a/.drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review.md b/.drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review.md deleted file mode 100644 index a64e79b7..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/reviews/system-design-review.md +++ /dev/null @@ -1,667 +0,0 @@ -# System design review — the unified CLI engine's public interface - -Subject: `wip/designs/engine/engine-interface-draft.ts` (a design artifact: type -declarations and doc comments, not shipping code). - -Pass: **architect**. The lens is system shape, vocabulary, boundaries, -dependency direction, and conceptual integrity. Implementation correctness, -failure modes, and operability are the principal-engineer pass's job; where I -noticed something in that territory I list it under "Referrals" instead of -arguing it here. - -Sources read in full: `cli-engine-requirements.md` (R1–R14), -`wip/designs/engine/output-modes-survey.md`, prisma/prisma -`docs/architecture docs/adrs/ADR 239 - Errors are structural envelopes with -dotted namespace codes.md`, composer `ADR-0043`/`ADR-0044` (titles and -decisions), the sibling foundation design `wip/designs/1a/design.md`, and the -platform CLI shell layer (`wip/repos/prisma-cli/packages/cli/src/shell/`: -`output.ts`, `command-runner.ts`, `global-flags.ts`, `runtime.ts`, `ui.ts`, -`errors.ts`, `prompt.ts`, `help.ts`, `next-actions.ts`). - -Note on ADR 245: no such file exists in this repo (the ADR series stops at 243). -The `Result` conventions it is cited for are, however, recorded in -`wip/designs/1a/design.md` §"Results carry one discriminator", and the draft -matches them. The draft's use of `CliStructuredError` from -`@prisma/cli-foundation` also matches design 1a (which settles both the class -name and the package name) even though ADR 239's own example spells the -interface `StructuredError`. No action; recording it so a later reader does not -"fix" it in the wrong direction. - ---- - -## 1. What is being introduced - -In plain language, the draft proposes nine concepts. - -1. **An event vocabulary** (`EngineEvent`, nine members). A running command can - say: a phase started, a phase ended with an outcome, N of M items are done, - here is a warning, here is a note, here is a line a child process printed, - here is something you could do about it, here is a URL that now works, here - is a state change in something I am watching. Each member may carry a - product-defined `data` payload the engine never interprets (R14). - -2. **A handler's world** (`CommandContext`). One object holding the product's - config section, credentials, the function that emits events, a prompting - surface, an abort signal, and the working directory. R4's "the whole world - arrives as one argument". - -3. **A declaration vocabulary for arguments** (`flag.*`, `positional.*`). Small - builder functions whose return types carry a phantom type parameter, so that - `ArgsOf` can compute the handler's argument type by inference (R1). - -4. **A command declaration** (`CommandDefinition`): the words shown in help, the - flags and positionals, a function that lazily imports the real handler (R9), - a presenter triple, and an escape hatch for commands that take over stdin and - stdout. - -5. **A presentation vocabulary** (`Block`, `Ui`). A product returns a list of - structured blocks — a summary line, a label/value list, a table, a bullet - list, a next-steps list — plus three text-styling helpers. There is no way to - write bytes (R5). - -6. **A product's export surface** (`CommandSet`): named commands with no paths. - -7. **Shell-side mounting** (`createCli`): the shell supplies the binary's name - and version, the group headings, and a map from space-separated path to - command (R12). - -8. **The injected environment** (`Runtime`, `LoadedConfig`): streams, cwd, TTY - facts, a signal, the loaded config with per-section diagnostics, credentials. - -9. **An in-repo test harness** (`createTestCli`, `TestCli`): argv in, bytes and - events out, using the production machinery (R7). - -The overall shape is right, and the derivation discipline is visible: almost -every member of `EngineEvent` and `Block` can be traced to a numbered structure -in the survey's §C ranking. The findings below are about the places where a name -does not say what the thing is, where a set does not cover its space, where two -sides of a symmetric pair have different shapes, and where a requirement has no -type to live in. - ---- - -## 2. Subsystem fit and boundary correctness - -**Dependency direction is correct.** Products depend on the engine package and -on the zero-dependency foundation for `Result` and `CliStructuredError`; the -engine depends on neither product; the shell depends on both and owns the tree. -Nothing in the file names stricli, commander, clipanion, clack, or colorette, so -R3 holds at the level of names. - -**One stricli-shaped assumption does survive** — see finding A20 on -per-command-only flags. It is not a stricli *type* in the interface, so R3 is -not violated in the letter; but the "there are no global flags" rule that R5 -states, and that the requirements doc's own closing section says was adopted -partly because it "neutralizes" stricli's per-command-flags limitation, has been -carried into the public interface as a shape: `flag.json()` exists and -`flag.quiet()` / `flag.verbose()` / `flag.color()` do not, with no statement of -what happens to the other six flags every shipping CLI has. That is a framework -limitation showing through the contract, which is what R3 is meant to prevent. - -**The two-level split — `Runtime` (the environmental whole, injected once) and -`CommandContext` (the handler's narrow world) — is the right boundary.** The -test in favour of it: `Runtime` holds things a *process* has (streams, TTY-ness, -loaded config, cwd) and `CommandContext` holds things a *command* has (its -config section, its way of speaking, its abort signal). Products can only reach -the second, which is what makes R4's runtime-agnosticism and testability claims -true. Three things sit in the wrong layer or are absent from it; see A13–A16. - -**Where the boundary is not yet drawn at all:** the config *section* is a -first-class concept in R10 (named section, never-throwing validator, per-section -diagnostics, "a command fails only if a section it needs is invalid") and has no -representation anywhere in the interface. This is the largest structural gap and -is finding A12. - ---- - -## 3. Naming and typology findings - -Each finding names the thing, states the problem, and proposes a concrete -alternative. Line numbers refer to the draft. - -### Events - -**A1 — `notice` vs `warning`: one axis spelled two ways, and a third time -elsewhere.** (lines 61–64.) `warning` and `notice` differ only in severity, and -severity is already modelled as a *field* in two other places in the same file: -`step-finished.outcome` (line 50) and `Block.summary.tone` (line 258). So the -file encodes "how serious is this" as a kind in one place and a field in two -others. Symmetry probe fires. Also `notice` reads cold as an official -announcement ("a notice of termination"); the thing meant is an informational -line. -*Alternative:* one member, `{ kind: 'message'; tone: 'info' | 'warning'; message: string }`, -reusing the same tone vocabulary as `Block.summary`. Three concepts collapse to -one axis used consistently. - -**A2 — `output` is the most overloaded word available, and `stream` is a -mechanism.** (lines 69–75.) The concept is "one line that a child process or a -remote log stream produced". The word `output` in this same design also means -the presenter triple's job, the `--json` payload, and the survey's own title -("output modes"). A reader with no context will parse `kind: 'output'` as "the -command's output". Separately, `stream: 'stdout' | 'stderr'` names two OS pipes; -the survey's own evidence (§B6, `controllers/build.ts:34-150`) is a *remote* -build-log stream that has no pipes and routes by a `level` field instead — so -remote logs must pretend to have file descriptors. -*Alternative:* `kind: 'process-output'` (or `'log-line'`), with -`channel: 'out' | 'err'`, and document that a remote stream maps its severity -onto the channel. - -**A3 — `status` is both the kind and the field, and it drops the transition.** -(lines 95–100.) `{ kind: 'status'; subject; status }` reads awkwardly cold, and -more importantly the survey's §C10 conclusion is explicit: "Any engine 'wait' -concept needs a **from→to** status-transition event (the platform already emits -exactly that, controllers/app.ts:2651-2662)." The draft records only the new -value, so a consumer that joins the stream late cannot tell a transition from a -re-assertion, and the human renderer cannot print "pending_dns → verifying". -*Alternative:* `{ kind: 'status-changed'; subject: string; from?: string; to: string }`. - -**A4 — `step` is a display name doing duty as an identity, and nesting is -asserted in prose but absent from the type.** (lines 44–60.) The doc comment -says "steps may nest", and the ORM's shipped dialect models *all* -operation-specific progress as nested spans with `spanId` / `parentSpanId` -(survey §B3, `control-api/types.ts:91-111`). The draft has neither an id nor a -parent, so nesting cannot be expressed, concurrent steps cannot be paired -start-to-finish, and `progress.step?: string` (line 56) refers to a step by its -display string. -*Alternative:* either add `id: string` and `parentId?: string` and let `progress` -and `step-finished` reference `id`; or state in the type's doc that steps form a -strict stack (last-started is the one that finishes) and delete the nesting -claim if that is not true. - -**A5 — `step-finished.outcome: 'ok' | 'failed' | 'skipped' | 'warning'` mixes -two vocabularies.** (line 50.) `'ok' | 'failed'` is an outcome; `'warning'` is a -severity; `Block.summary.tone` spells the same space `'ok' | 'error' | -'warning' | 'info'`; ADR 239's `severity` spells it `'error' | 'warn' | 'info'`. -Four spellings of one axis across the settled conventions and this file. -*Alternative:* pick one tone vocabulary — `'ok' | 'warning' | 'error' | -'skipped'` — and use exactly it in `step-finished`, `Block.summary`, and the -message event from A1. Reconcile against ADR 239's `severity` values in the same -change (`warn` vs `warning` is a live inconsistency in the settled surface). - -**A6 — `remediation` is the third of three spellings of one concept inside one -file.** (lines 81–86.) The survey's §C2 identifies "remediation / next-step in -five competing encodings" as "the clearest case of one engine concept currently -spelled five ways." The draft reduces five to three — the error's `fix`, the -`remediation` event, and `Block.nextSteps` — and its own doc comment (lines -78–80) names a fourth, "the success envelope's nextActions", which does not -exist in the file at all (see M1). Worse, the shipping platform concept is -richer: `NextAction { kind, journey, label, command, commands, reason }` -(`shell/next-actions.ts`), and the draft's `{ label, command? }` is a silent -subset. -*Alternative:* lift one type — call it `NextAction`, matching the shipping name — -into the interface, and use that same type in the event, on the success -envelope, and as the payload of the `nextSteps` block. Then there is one concept -with one shape and three placements, instead of three concepts. - -**A7 — the event vocabulary has no sensitivity marker, while `Block` does.** -(lines 43–100 vs line 259.) `Block.fields` rows carry `sensitive?: boolean`, and -the survey's §C5 records credential masking as real, shipped policy in two -families (`maskConnectionUrl` / `sanitizeErrorMessage` in the ORM; -`URL_CREDENTIALS_PATTERN` + `maskValue` in the platform, `ui.ts:10`). An -`endpoint.url` or an `output.line` can carry a connection string with a -password, and the product has no way to say so and the engine no way to know. -Asymmetry within one file for one policy. -*Alternative:* either give the engine a masking helper on `Ui` and a -`sensitive?: boolean` on `endpoint`, or state that the engine masks credential -patterns unconditionally in every rendered string (which is the stronger, more -R5-shaped answer) and delete `Block.fields.sensitive`. - -**A8 — evidence-ranked structure #7 has no home: files written.** The survey -ranks "file paths / artifacts written" as a 3/3-family recurring structure -(§C7: ORM `files{json,dts}`, `filesWritten[]`/`filesDeleted[]`, `dir`, -`baselineDir`, all relativized to cwd; Composer `stackFilePath`). Under R14's own -promotion rule ("a structure recurring across commands or products is the signal -that the engine vocabulary is missing a concept"), this is a promotion -candidate that was not promoted, and the draft does not say why. It also has no -`Block` — a product would have to pre-format paths into `Block.list` strings, -which puts path relativization (an engine policy today) into product code. -*Alternative:* either add `{ kind: 'artifact'; path: string; action: 'written' | -'deleted' | 'unchanged' }` plus a `Ui.relativePath(p)` helper, or record in the -draft's own doc comment that artifacts are deliberately deferred and why. - -**A9 — `endpoint.name` versus the shipped vocabulary.** (lines 88–93.) Composer's -shipped type is `ServiceEndpoint { address, url }` (`operations/shared.ts:19-22`) -where `address` is the service's coordinate, not a label. `name` is fine if it -means the human label, but a reader coming from Composer will populate it with -an address. One clarifying word in the doc comment resolves it. Low priority. - -### The presenter triple, `Block`, and `Ui` - -**A10 — `present.stdout` names a file descriptor in an interface whose entire -point is that products cannot write to file descriptors.** (lines 228–232.) -`human` is named for its audience, `json` for its format, `stdout` for a stream — -and R5 says "they cannot print… the interface offers no way to express it," yet -the key is the name of a stream. It is also not even distinguishing: under -`--json` the machine payload goes to stdout as well. The essence of the three is: -prose for a person, the machine-usable payload lines that survive `--quiet`, and -the structured projection. -*Alternative:* `{ human, payload, json }` (or `{ prose, data, json }`). The -platform's own `renderHuman` / `renderStdout` / `renderJson` -(`shell/command-runner.ts:25-37`) has the same flaw; generalizing it is the -moment to fix it, not to enshrine it. - -**A11 — `Block` is missing the single most-cited human structure in the survey: -the tree.** (lines 257–262.) The survey records tree rendering across all three -families and many commands: the ORM's migration graph visualization with -cross-space column alignment (`commands/migrate.ts:435-470`), introspection trees -(`db schema`), migration list/status trees, ADR 227's "migration read commands -share one graphical renderer", ADR 229's line-plane-occlusion renderer; and -Composer's deployment topology tree (`render-deployment.ts:77-116`). With no -tree block, every one of those commands must either pre-format ASCII into -`Block.list` strings — which is product-side rendering by another name and -exactly the hole through which the drift R5 exists to kill returns — or the -engine grows a custom escape hatch per command. -Separately, `Block.list` (line 261) is the one member with no cited evidence: it -is `fields` without labels, or a one-column `table`. And `Block.nextSteps` (line -262) is data, not layout — see A6. -*Alternative:* add a `tree` block (recursive `{ label, children? }` nodes, engine -owns the glyphs and alignment); drop `list` unless evidence appears, or keep it -and delete `nextSteps` in favour of the shared `NextAction` type. - -**A12 (naming) — `Ui` reads cold as "the user interface"; it is a text-styling -helper with three verbs, one of which is a rendering decision.** (lines 265–269.) -`emphasize` and `code` are semantic (this matters; this is a literal token). -`dim` is the ANSI concept itself — a product choosing "dim" is a product making a -presentation decision, which R5 assigns to the engine. And two policies the -engine visibly owns today are absent: value masking (§C5) and path -relativization to cwd (§C7, "nearly everywhere"). -*Alternative:* rename the type `TextStyle`; replace `dim` with `deemphasize`; -add `mask(value)` and `relativePath(path)`. - -### The context / runtime split - -**A13 — `CommandContext` is an unbound type parameter: nothing -declares which section a command needs, and nothing registers a validator.** -(lines 106–110, 194–199, 313–316.) R10 requires each product to contribute "a -named section and a never-throwing validator", and requires that "a command -fails only if a section it needs is invalid." The interface has `LoadedConfig` -with `sections: Record` and per-section `diagnostics`, and it -has a `TConfig` generic that the product simply *asserts*. There is no key, no -validator, and therefore no way for the engine to know which section to hand -over or which diagnostic should fail this command. The doc comment on line 107 -promises behavior the type cannot support. -*Alternative:* make the config section a first-class concept — -`defineConfigSection({ name, validate })` returning a token carrying the -validated type; `CommandDefinition.configSection?: ConfigSectionToken` binding -`TConfig`; `createCli({ sections: [...] })` registering them. Then -`CommandContext` is derived rather than asserted, and R10's failure rule -becomes mechanical. - -**A14 — `Runtime` has no `env`, so the engine must read `process.env` itself.** -(lines 300–311.) The engine owns interactivity policy and colour policy (R5). -Both are environment-driven in every shipping implementation: `canPrompt` reads -`runtime.env.CI` (`shell/runtime.ts:113`), colour reads `NO_COLOR`/`FORCE_COLOR`. -`Runtime` is described as "everything environmental, injected once by the bin (or -by a test)" and omits the environment. The consequence is that the two behaviors -most worth testing are the two a test cannot control. -*Alternative:* `readonly env: Readonly>` on -`Runtime` (and only on `Runtime` — R4 keeps it out of `CommandContext`). - -**A15 — `Runtime.isTty` covers `stdin` and `stderr` but not `stdout`.** (line -305.) The whole stdout-is-data discipline keys on stdout, and the ORM's shipped -behavior is to auto-select JSON when *stdout* is not a TTY (survey §D, -`utils/global-flags.ts:67-69`). The set is incomplete for its own space. -*Alternative:* `isTty: { stdin, stdout, stderr }`. - -**A16 — `signal` appears on both `Runtime` (line 306) and `CommandContext` -(line 126) under the same name.** If they are the same object, one is redundant; -if the context's is a per-command child that also fires on command-level timeout, -the type should say so. Symmetry probe fires either way. Also `Runtime.credentials` -and `CommandContext.credentials` are the *same* type, whereas config narrows from -`LoadedConfig` to one section — the two cross-cutting values are handled -asymmetrically with no stated reason. - -**A17 — `Credentials` is declared in the engine but documented as owned -elsewhere, and its name is too broad.** (lines 132–136.) The comment says -"opaque to the engine; shape owned by the Cloud product's auth library" while the -engine declares two concrete fields. Either it is opaque (then type it as an -opaque carried value and let the Cloud product's library define the shape) or the -engine reads it (then the fields are the engine's and the comment is wrong). A -concept cannot live in one bounded context and be owned by another. Separately, -`Credentials` reads cold as "any credentials" — database URLs, git tokens, bucket -keys are all credentials in this product family. -*Alternative:* `ManagementApiSession` (or `PlatformSession`), with the ownership -question resolved one way or the other. - -**A18 — `PromptSurface` is engine-internal jargon, and the set is short of what -the evidence needs.** (lines 138–148.) "Surface" is a word this design team uses; -a contributor reads `ctx.prompt` and wants a type called `Prompts`. More -substantively: there is no representation of `-y/--yes` (shipped in both the ORM -and the platform), so every product will re-add it as a per-command flag and -re-implement pre-acceptance — the precise divergence R5 exists to prevent. And -the survey's §C11 records typed destructive confirmation (`--confirm `) -as a 2/3-family pattern with no home here. -*Alternative:* rename to `Prompts`; add `confirmDestructive(question, { expect })`; -and make `--yes` an engine-owned concept that `confirm` consults, not a flag each -product declares. - -**A19 — no interactivity capability fact on the context.** The survey's §F6 -conclusion is that the prompt check is "an engine-level capability check, not -per-command logic", and the shipped code branches on it *before* prompting: -`git connect` polls only when `canPrompt` and errors immediately otherwise -(`controllers/project.ts:1708-1717`). With the draft's design a handler learns -its environment only by attempting a prompt and inspecting the failure. -*Alternative:* `readonly interactive: boolean` on `CommandContext`. This is a -fact about the environment, not a rendering decision, so it does not weaken R5. - -### Flags and positionals - -**A20 — `flag.json()` is a real concept wearing the wrong clothes, and it is the -only member of a family of seven.** (lines 167–168.) Two problems. - -*Shape.* `--json` is not an argument the handler consumes; it is a declaration -that the command has a machine-readable mode. The evidence that it is different -in kind: it switches stream discipline, suppresses prompts (`canPrompt` returns -false under json, `shell/runtime.ts:106-108`), disables progress rendering -(`progress-adapter.ts:36-38`), and changes envelope behavior on crash. Typing it -as `FlagSpec` puts `args.json` in the handler's arguments and invites -the handler to branch on it — which is presentational logic re-entering product -code through the front door. It also has no `brief`, unlike every sibling -builder: a tell that it is not the same kind of thing. -*Alternative:* delete `flag.json()` and derive the capability — a command -supports `--json` exactly when it declares `present.json` (or unconditionally, -since `present.json` already defaults to the raw value). The flag then never -appears in `ArgsOf` and the handler cannot see it. - -*Completeness.* The shipping CLIs have `--json`, `-q/--quiet`, `-v/--verbose`, -`--trace`, `-y/--yes`, `--interactive`/`--no-interactive`, `--color`/`--no-color` -(`shell/global-flags.ts:23-45`; ORM `utils/command-helpers.ts:368-388`). The -draft names `--quiet`, `--no-interactive`, and `--json` in doc comments and -declares exactly one of them. Whatever the answer is — engine-injected on every -command, or a `flag.*` entry each — it must be the same answer for all seven. -Engine-injected is the right one: `--trace` changes error rendering and -`--verbose` changes diagnostics, both squarely engine concerns under R5. If they -are engine-injected, then so should `--json` be, which is the same conclusion as -above by a second route. - -**A21 — required-ness is spelled with opposite defaults and opposite naming on -the two sides of one axis.** (lines 158–177.) `flag.string()` is optional and -`flag.requiredString()` is required; `positional.string()` is *required* and -`positional.optionalString()` is optional. A reader who learns one side will read -the other wrong. This is the clearest reads-cold failure in the file. -*Alternative:* one convention. Either mark both non-defaults explicitly -(`flag.string` / `flag.requiredString`; `positional.requiredString` / -`positional.string` — no, that just moves the problem), or make required-ness a -spec field on both: `flag.string({ brief, required: true })`, -`positional.string({ brief, placeholder, required: false })`. The field form -reads correctly cold on both sides and removes four builder names. - -**A22 — the builder set is incomplete for its own space.** (lines 158–177.) No -`flag.number()` / `flag.integer()`, though the survey's one poll verb takes -`--timeout ` with `--timeout 0` meaning "probe once" -(`commands/app/index.ts:592-633`). No required variant of `enum` or `repeated`; -no enum-typed `repeated`; no optional/required distinction on `repeated` at all -(it returns `readonly string[]`, so "not passed" and "passed empty" are the same -value). Discriminator-completeness applied to a builder set rather than a union. - -**A23 — `FlagSpec` carries only a phantom type; the flag's *name* is the -record key, and the transliteration rule is unstated.** (lines 171–172, 207.) A -flag called `--dry-run` must be the key `dryRun` (or `'dry-run'`, quoted) and the -engine must transliterate. That rule is the single most likely thing a -contributor gets wrong and it appears nowhere. Also absent from the spec, though -all present in today's CLIs: short aliases (`-q`, `-v`, `-y`), `default`, -`hidden`, `deprecated`. -*Alternative:* state the key→flag-name rule in the `flags` doc comment, and add -`alias?`, `default?`, `hidden?`, `deprecated?` to the specs. - -### The command definition - -**A24 — `raw` names the mechanism, has three spellings for two states, and -contradicts `present` being required.** (lines 234–239.) The concept is the -survey's mode 7: "the process becomes a protocol endpoint" (`lsp`). `raw` names -the byte-level consequence, not the thing. The type `false | { reason: string }` -gives "no" two spellings (`undefined` and `false`) and "yes" one, so the reader -must learn that a boolean-looking field is really a presence check. And because -`present` is a *required* member of `CommandDefinition` (line 228), a raw command -must supply a presenter that can never run — the impossible state is -representable, while the doc comment says the engine will reject it at runtime. -*Alternative:* make `CommandDefinition` a union of a standard command (with -`present`, no protocol field) and a stdio-server command -(`stdioServer: { reason: string }`, no `present`, no `flags` beyond transport). -Then the engine's runtime check disappears into the type. Keep the required -`reason` — a mandatory written waiver on an escape hatch is a good idea; add one -line saying it exists for review pressure, since it is displayed nowhere. - -**A25 — `handler` is named for the thing it returns, not for what it is.** (lines -214–219.) The value is a module loader; the handler is its default export. Two -concepts, one name. -*Alternative:* `load: () => Promise<{ default: Handler }>`, with an exported -`Handler` type so products can name their handler's -type without spelling the whole signature. - -**A26 — groups get a strictly poorer declaration than commands, and groups are -the more-visited help pages.** (lines 200–205 vs 286.) A command declares -`brief`, `description`, `examples`; a group declares only `brief`. Today's -platform descriptors carry `description`, `longDescription`, `examples`, and -`docsPath` for every node including groups, and `help.ts` renders all four -(`shell/command-meta.ts`, `shell/help.ts:13-42`). `prisma db --help` will be a -one-line heading and a list. -*Alternative:* groups take `{ brief, description?, examples? }` — the same shape -as a command minus arguments. And add `docsPath?` (or `docsUrl?`) to both: -ADR 239 makes docs URLs part of the settled error surface and the platform -already renders a "Read more" row (`ui.ts` docs-path rendering). - -**A27 — `present` is required even for commands with nothing to present.** (line -228.) `app run` hosts a dev server and rejects `--json` outright -(`controllers/app.ts:273-281`); `app logs` streams; a session command's terminal -value is `void`. Each must write `human: () => []`. -*Alternative:* make `present` optional when `TResult` is `void`, or model -session/streaming commands as their own declaration variant alongside A24's -union. - -### Mounting - -**A28 — space-separated paths are the right shape; the type should say so.** -(lines 283–288.) A path key that reads exactly like the invocation (`'db -migrate'`) makes the tree reviewable in one glance, makes collisions string -equality, and keeps the grammar checkable in one place — which is precisely R12's -argument. It beats nested objects for a tree this flat. Two refinements: give it -a named type (`export type CommandPath = string`) with the grammar in its doc -comment (lowercase words, single spaces, kebab-case within a word), and state how -help ordering is determined, since object key order is currently the de facto -answer and nobody has agreed to it. - -**A29 — `CommandSet` is declared and then not used, and the two maps that share -its structure mean different things.** (lines 276, 287, 323.) `CommandSet = -Record` where the key is a *command name* (product -side, R12: no paths). `createCli`'s `commands` is the identical structural type -where the key is a *path*. `createTestCli`'s is a third inline copy. Three -spellings, two meanings, one structure — a reader cannot tell them apart, and -neither can the compiler. -*Alternative:* use `CommandSet` for the product side and introduce -`CommandMounts = Readonly>` for the shell -side, with A28's branded path type making the distinction visible. - -**A30 — the shell can place a command but cannot rename it.** (lines 283–288.) -R12's own evidence is six months of renames and regroupings across product lines -(`app` → `service`, `database` → `postgres`) that no product would have made -locally. Those renames change user-facing *words*, and `brief` — a user-facing -sentence written by a product before its final path was known — is not -overridable at the mount. The shell owns the tree but not the tree's prose. -*Alternative:* let a mount entry be either a `CommandDefinition` or -`{ command: CommandDefinition; brief?: string }`. - -### Test harness - -**A31 — `createTestCli` is not the same machinery at the seam R7 promises.** -(lines 322–339.) Production takes a whole `Runtime`; the harness takes four -loosely-typed fields and `run(argv, { stdin })`. Concretely, `config?: -Record` is a bare section map while `Runtime.config` is -`LoadedConfig` with diagnostics — so the harness *cannot construct an invalid -section*, which is the R10 behavior most worth testing in a product repo. There -is also no way to set env (A14), no way to fire the abort signal (so exit code 3 -and session teardown are untestable), and no clock (see referral R6). -*Alternative:* `createTestCli(spec)` plus `run(argv, overrides?: Partial -& { stdin?: string })`, with `config` typed as `LoadedConfig`. - -**A32 — `TestCli.run().json: readonly unknown[]` models the transport, not the -value.** (line 335.) A non-streaming command emits one envelope; an array is the -NDJSON mechanism showing through the assertion surface, and every test must write -`result.json[0]`. -*Alternative:* `envelope?: unknown` for the terminal envelope and `jsonEvents: -readonly unknown[]` for the stream, so an assertion names what it is asserting. - ---- - -## 4. Missing concepts - -Things the requirements or the survey imply that the interface has no home for. -A13 (config sections), A19 (interactivity), A20 (the other six flags), A8 -(artifacts), and A11 (trees) are already stated above and are not repeated. - -**M1 — the success envelope. This is the biggest omission.** The platform's -shipped success envelope is `{ ok, command, result, warnings, nextSteps, -nextActions }` (`shell/output.ts:9-29`), warnings are rendered in human mode too -so degraded steps are never silent (`command-runner.ts:130-135`), and the survey -calls envelope-level `nextSteps`/`nextActions` on *every* success the platform's -distinguishing asset (§D, "Cross-family delta worth naming"; §C2(b)). The draft -has no envelope type at all. Consequences: (a) a command that succeeds with -caveats has nowhere to put them except a mid-run `warning` event, which is a -different thing — a warning attached to a result is part of the result; (b) -`Block.nextSteps` is a *human rendering* block, so under `--json` the next steps -disappear entirely — a regression against today's platform CLI, and precisely the -field agents consume; (c) the draft's own doc comment at lines 78–80 refers to -"the success envelope's nextActions" as an existing home for terminal -remediation, and it does not exist. -*Alternative:* declare the envelope in the interface — -`Success = { result: T; warnings?: readonly string[]; nextActions?: readonly -NextAction[] }` — and let a handler return `Result, CliStructuredError>`, -or add `warnings`/`nextActions` as a second return channel. The `NextAction` type -is A6's shared type. - -**M2 — the config version marker (R10).** R10 requires `defineConfig` to write a -structural version marker and requires an unmarked file (in particular a classic -Prisma 7 config sharing the filename) to fail early with a typed error, calling a -silent misparse "the worst launch bug available." Nothing in the interface -mentions the marker, `defineConfig`, or the failure; `LoadedConfig` arrives -already loaded. The sibling foundation design does address it -(`wip/designs/1a/design.md` A9), so this may be a deliberate delegation — but the -engine interface should at least name where the boundary is, because the engine -is what fails the run. - -**M3 — exit codes beyond 0–3.** R6 and ADR 239 fix 0/1/2/3, and ADR 239 adds -"Commands may still return a command-specific code for finer classification." -`migration check` already ships exit **4** for integrity failure -(`migration-check/exit-codes.ts:1-3`), and `db verify` and `db sign` ship exit -**1** on drift / verify failure (survey §A1) — which under ADR 239 now means "a -bug in Prisma". The interface offers a handler no way to influence the exit code: -it returns `Result`, and `CliStructuredError` carries no -`exitCode` (today's `CliError` does, `shell/errors.ts:57`). So either the shipped -ORM behavior becomes inexpressible, or the interface needs to say so and require -those commands to change. Both are defensible; neither is stated. -*Alternative:* if finer codes stay, put an optional `exitCode` on the structured -error and state the reserved ranges; if they go, say so here and record it as a -breaking change the migration must make. - -**M4 — typed destructive confirmation.** §C11, 2/3 families: -`--confirm ` on the platform's `project remove` / `transfer` and the -database/bucket variants. `PromptSurface.confirm` is yes/no only. Covered in A18; -listed here because it is a requirement-level gap, not just a naming one. - -**M5 — timeout and deadline semantics for poll commands.** The survey elevates -poll-until-terminal to its own execution mode precisely because it "carries -timeout/deadline semantics, an explicit remote status enum, and transition -events" (§F3). The draft's header decides "timeouts are the handler's business." -That is a legitimate decision, but its consequence is that every poll command -re-implements deadline parsing, elapsed-time rendering, and the timeout error -code — the divergence R5 exists to prevent, arriving through a different door. -At minimum the engine should own the timeout error code and the elapsed -rendering. Worth recording as an accepted risk with a reason. - -**M6 — R13's optional-peer-dependency check.** R13 requires a structured error -naming the missing dependency and the user's own install command, produced by an -execution-time check. It is expressible as an ordinary `CliStructuredError`, so -this is not a hole so much as an unassigned owner: if the engine provides the -check and the error code, it belongs here; if each product hand-rolls it, R13's -"clearly say what is missing" becomes convention again. State which. - -**M7 — telemetry and update-check side processes.** Both the ORM and the platform -spawn a detached child on every invocation (ADR 217; `shell/update-check.ts:225-237`). -They are presumably engine-internal, but they are cross-cutting behavior the -engine will own, and the interface gives the shell no way to configure or -suppress them. Out of scope for this artifact, but currently unhomed anywhere. - -**M8 — durations.** §C9, 2/3 families: ORM `timings: {total}` under `-v`, span -elapsed-ms suffixes, platform `--verbose` timing diagnostics and domain-wait -`mm:ss`. The engine can measure step durations itself by pairing start/finish, so -this is mostly fine — but only if A4's identity problem is fixed, and only if -`--verbose` exists (A20). - ---- - -## 5. Referrals to the principal-engineer pass - -These are implementation-mechanics or failure-mode questions I noticed while -reading; they are not architecture findings and I have not judged them. - -- **R1 — variance of `CommandDefinition` in the collection types.** `CommandSet` - and `createCli.commands` use `CommandDefinition` with its defaults - (`TResult = unknown`). `present.human: (value: TResult, ui: Ui) => Block[]` is - contravariant in `TResult`, so a `CommandDefinition` - is probably not assignable to `CommandDefinition`. If so, `createCli` cannot - accept real commands without a cast — which would undercut R1's "directly - executable" claim. Needs a compile test. -- **R2 — `ArgsOf` when `positionals` is absent.** `positionals?` is optional - (line 208) but `ArgsOf` maps over `keyof D['positionals']` (line 185). - Behavior over `undefined` needs checking. -- **R3 — whether `defineCommand`'s inference actually lands.** The phantom-symbol - `FlagSpec` / `PositionalSpec` design is the whole basis of R1's - "typed by inference"; it needs a worked example compiled, including an enum - flag and a `const` values array. -- **R4 — `report` is synchronous and returns `void`** (line 117) while writing to - a stream is asynchronous and can apply backpressure. Behavior of a session - command emitting thousands of `output` events into a slow pipe is a failure - mode worth a look. -- **R5 — events emitted after the signal fires.** Line 117 promises they "render - normally"; whether the stream is still writable during teardown is an - operability question. -- **R6 — determinism of the test harness.** The `--json` frame carries a - `timestamp` (doc comment, lines 33–35), so `TestCli.run().json` is not - snapshot-stable without an injected clock. -- **R7 — `LoadedConfig.diagnostics[].section: string | null`** — what `null` - means (a whole-file failure?) is not stated and affects which commands fail. - ---- - -## 6. Verdict - -The overall shape is sound and the boundaries are drawn in the right places. The -product/engine/shell layering satisfies R3, R4, R9, and R12 as stated; the -handler protocol (args and context in, events along the way, a `Result` out) is -one mechanism covering six of the survey's seven execution modes with a declared -escape hatch for the seventh; and the derivation discipline is real — most -members of `EngineEvent` and `Block` trace back to a numbered, occurrence-ranked -structure in the survey, which is exactly the evidence standard R14 asks for. -The two-level `Runtime` / `CommandContext` split is the right boundary and is the -part I would change least. - -What the draft is not yet is *conceptually minimal or symmetric*. One axis — -severity — is spelled four ways across `step-finished.outcome`, the -`warning`/`notice` split, `Block.summary.tone`, and ADR 239's `severity`. One -concept — remediation — is spelled three ways inside the file and a fourth time in -a doc comment referring to a type that does not exist. One axis — -required-ness — has opposite defaults on the flag and positional sides. Three -structurally identical maps mean two different things with no way to tell them -apart. Two names take their meaning from a mechanism rather than from the thing -(`present.stdout`, `raw`), and one takes it from a file descriptor inside an -interface whose purpose is that products cannot touch file descriptors. - -Two gaps are more than naming and should be closed before this interface is -implemented against. First, **the config section has no representation at all**: -R10's named section, never-throwing validator, and "fails only if a section it -needs is invalid" rule are all promised in a doc comment that the types cannot -support (A13). Second, **there is no success envelope** (M1): warnings and next -actions attached to a successful result — shipped today on every platform -command, and the one thing the survey singles out as the platform's advantage — -have nowhere to live, and `Block.nextSteps` silently drops them from `--json`. -Behind those, the unresolved status of the other six cross-cutting flags (A20) -determines whether `flag.json()` is a concept or an accident, and the missing -tree block (A11) determines whether the most-rendered human structure in the -corpus can be expressed at all or leaks back into product code. - -None of this is a reason to restart. The draft is a good second-order artifact -being asked a first-order question, and the fixes are mostly subtractive: one -tone vocabulary instead of four, one remediation type instead of three, one -required-ness convention instead of two, `--json` derived instead of declared. -Add the config-section token and the success envelope, and the interface would -express its requirements rather than describe them. diff --git a/.drive/projects/prisma-cli-v8/design-notes.md b/.drive/projects/prisma-cli-v8/design-notes.md index 05c5392b..7158421b 100644 --- a/.drive/projects/prisma-cli-v8/design-notes.md +++ b/.drive/projects/prisma-cli-v8/design-notes.md @@ -6,12 +6,12 @@ file is the map. ## Settled (do not re-litigate without the operator) -- **Engine interface, v8** — `assets/engine/engine-interface-draft.ts` - (v1–v7 history alongside). Settled through facilitated - line-by-line design with Will plus five adversarial review rounds - (architect + principal engineer, both closed clean; artifacts in - `assets/engine/reviews/`). Every novel typing claim - compile-verified. +- **Engine interface, v8** — `assets/engine/engine-interface-draft.ts`. + Settled through facilitated line-by-line design with Will plus five + adversarial review rounds (architect + principal engineer, both + closed clean). Every novel typing claim compile-verified; the claims + live on as the permanent type-test suite in `@prisma/cli-engine` + (review artifacts and superseded draft versions are not committed). - **Requirements R1–R14** — prisma-cli PR #128 (`docs/architecture/cli-engine-requirements.md`), unmerged. - **Packaging** — ONE library package `@prisma/cli-engine` with a diff --git a/.drive/projects/prisma-cli-v8/plans/s1-engine-vertical.md b/.drive/projects/prisma-cli-v8/plans/s1-engine-vertical.md index 5b81f823..ffd70d09 100644 --- a/.drive/projects/prisma-cli-v8/plans/s1-engine-vertical.md +++ b/.drive/projects/prisma-cli-v8/plans/s1-engine-vertical.md @@ -49,8 +49,8 @@ constructors only — no execution. **Builds on:** D1's package + protocol types. **Hands to:** the definition types D3 executes against and D5 loads config for. -**Completed when:** every compile-verified claim from the review record -(reviews/code-review-r4-closure.md, -r5-delta.md) is a permanent +**Completed when:** every compile-verified claim from the design +review rounds is a permanent type-test with stale-@ts-expect-error discipline: Char alias accept/reject, exitCode required-iff-catalogued in both directions, needs.config → ctx.config inference, PresentedResult brand. Suite green. diff --git a/.drive/projects/prisma-cli-v8/specs/s1-engine-vertical.md b/.drive/projects/prisma-cli-v8/specs/s1-engine-vertical.md index 0db61465..83dc6706 100644 --- a/.drive/projects/prisma-cli-v8/specs/s1-engine-vertical.md +++ b/.drive/projects/prisma-cli-v8/specs/s1-engine-vertical.md @@ -29,9 +29,8 @@ package's own test harness. Blocks + Ui, envelopes + StreamEvent framing, createCli + Runtime + LoadedConfig, createTestCli. `@stricli/core@1.3.0` exact-pinned, fully internal. - - The compile-verified typing claims from the review record - (`assets/engine/reviews/code-review-r4-closure.md`, - `-r5-delta.md`) become permanent type-tests in the package + - The compile-verified typing claims from the design review rounds + become permanent type-tests in the package (@ts-expect-error suites): Char alias accept/reject, Outcome exitCode required-iff-catalogued both directions, needs.config → ctx.config inference, PresentedResult brand. From 5e3da1fc39ee326adc25ea282e6d9670af0695c3 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 9 Aug 2026 23:22:19 +0200 Subject: [PATCH 11/18] docs(architecture): scope R4's prohibition and define the output surface R4 read as a blanket ban on reading disk, which R9 (Composer importing the user's modules at execution time) contradicts. Limit the prohibition to engine-owned state reached around the context, state that the output surface is a typed result and event sink with no writable streams or exit, and say product-domain disk access inside handlers is allowed. Co-Authored-By: Claude Fable 5 Signed-off-by: willbot Signed-off-by: Will Madden --- docs/architecture/cli-engine-requirements.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/architecture/cli-engine-requirements.md b/docs/architecture/cli-engine-requirements.md index aebcc146..793c1a11 100644 --- a/docs/architecture/cli-engine-requirements.md +++ b/docs/architecture/cli-engine-requirements.md @@ -57,9 +57,21 @@ engine's internals is one package's problem, not an ecosystem event. ### R4 — Products receive a context, never the environment -Product code does not read disk, environment variables, or the TTY. Handlers -receive one typed context object carrying their validated config section, -credentials, and the output surface. +Product code never reads engine-owned state directly: the process +environment, the process streams, TTY state, and the CLI's configuration +reach a handler only through one typed context object, which carries its +validated config section, credentials, the invocation's `cwd` and `env`, +and the output surface. The output surface is a typed result and event +sink — it exposes no writable streams and no way to exit the process; +rendering and exit codes stay in the engine (R5), and the process ends +only through the runtime's exit proxy, which the engine alone calls. + +This does not prohibit product-domain disk access. Reading the user's +project — including Composer importing the user's own modules at +execution time under R9 — is a product's job, done inside the handler +and anchored at the context's `cwd` (with `requireDependency` on the +context resolving optional dependencies from the user's project per +R13). What is banned is reaching around the context to process globals. **Why:** three reasons. Cross-cutting state — above all authentication credentials, which Composer needs even when the user authenticated through a From e8c99afc1fadf97e002cde7159c06f6bcc5b6f45 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 9 Aug 2026 23:22:53 +0200 Subject: [PATCH 12/18] docs(architecture): clarify R9 forbids discovery, not startup instantiation 'No runtime tree construction' could be read as forbidding building the tree at startup, which is exactly what createCli does. Say instead: no dynamic or discovery-driven tree construction; each engine instance builds its tree once at startup from statically defined structure. Co-Authored-By: Claude Fable 5 Signed-off-by: willbot Signed-off-by: Will Madden --- docs/architecture/cli-engine-requirements.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/architecture/cli-engine-requirements.md b/docs/architecture/cli-engine-requirements.md index 793c1a11..6b7ed654 100644 --- a/docs/architecture/cli-engine-requirements.md +++ b/docs/architecture/cli-engine-requirements.md @@ -146,8 +146,10 @@ product: each test lives where its failure would be introduced. ### R9 — Static tree, lazy guts Command definitions are cheap and load at startup; heavy dependencies load -inside handlers at execution time. No dynamic discovery, no runtime tree -construction. +inside handlers at execution time. No dynamic or discovery-driven tree +construction: each engine instance builds its tree once at startup from +statically defined structure — command definitions are direct function +references, and nothing about the tree is discovered at run time. **Why:** a statically known tree is simpler to reason about, renders complete help without executing product code, and fails at build time when it is wrong. From 911f452a66fad75b2236f9fa92205f984daa3d64 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 9 Aug 2026 23:23:45 +0200 Subject: [PATCH 13/18] docs(architecture): R10 covers config load and evaluation failures R10 promised 'never a crash' but only constrained validators; a config that fails to import or evaluate crashes earlier. Require the engine to catch load and evaluation failures and surface them as typed file-level diagnostics naming the config path, matching the shipped loader. Co-Authored-By: Claude Fable 5 Signed-off-by: willbot Signed-off-by: Will Madden --- docs/architecture/cli-engine-requirements.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/architecture/cli-engine-requirements.md b/docs/architecture/cli-engine-requirements.md index 6b7ed654..1a027a74 100644 --- a/docs/architecture/cli-engine-requirements.md +++ b/docs/architecture/cli-engine-requirements.md @@ -164,10 +164,13 @@ dependency subtrees behind execution-time imports. The engine discovers and evaluates one `prisma.config.ts`. Each product contributes a named section and a never-throwing validator; validation produces per-section diagnostics, and a command fails only if a section it -needs is invalid. The config value carries a version marker written by -`defineConfig`; an evaluated file without the marker (in particular a classic -Prisma 7 config, which uses the same filename) fails early with a clear, -typed error. No best-effort reading of unmarked files. +needs is invalid. "Never a crash" covers loading too: a file that fails to +import or evaluate — a syntax error, a throwing top-level statement — is +caught by the engine and surfaced as a typed file-level diagnostic naming +the config path, never a stack trace. The config value carries a version +marker written by `defineConfig`; an evaluated file without the marker (in +particular a classic Prisma 7 config, which uses the same filename) fails +early with a clear, typed error. No best-effort reading of unmarked files. **Why:** the unified CLI claims a filename Prisma 7 already owns; a silently misparsed v7 file is the worst launch bug available, so detection is a From 7c9b1839a9e19a803db531964aca7c37b05fd4c3 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 9 Aug 2026 23:24:27 +0200 Subject: [PATCH 14/18] docs(architecture): state the R10 version-marker contract exactly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Name the mechanism instead of gesturing at it: the engine package owns both sides — defineConfig stamps the $prismaConfig field, the loader checks it first — and each failure mode has its typed diagnostic: CLI.CONFIG_MISSING_MARKER for unmarked files, CLI.CONFIG_INVALID for unsupported (including future) versions, CLI.CONFIG_UNREADABLE for files that fail to evaluate. Matches the shipped loader. Co-Authored-By: Claude Fable 5 Signed-off-by: willbot Signed-off-by: Will Madden --- docs/architecture/cli-engine-requirements.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/architecture/cli-engine-requirements.md b/docs/architecture/cli-engine-requirements.md index 1a027a74..8b6085ce 100644 --- a/docs/architecture/cli-engine-requirements.md +++ b/docs/architecture/cli-engine-requirements.md @@ -167,10 +167,20 @@ produces per-section diagnostics, and a command fails only if a section it needs is invalid. "Never a crash" covers loading too: a file that fails to import or evaluate — a syntax error, a throwing top-level statement — is caught by the engine and surfaced as a typed file-level diagnostic naming -the config path, never a stack trace. The config value carries a version -marker written by `defineConfig`; an evaluated file without the marker (in -particular a classic Prisma 7 config, which uses the same filename) fails -early with a clear, typed error. No best-effort reading of unmarked files. +the config path, never a stack trace. + +The version-marker contract is exact. The engine package owns both sides +of it: its `defineConfig` stamps the exported config value with a +`$prismaConfig` field carrying the config contract version, and its +loader checks that field before interpreting anything else. Each failure +mode has its own typed, file-level diagnostic carrying the config path: +an evaluated file without the marker (in particular a classic Prisma 7 +config, which uses the same filename) fails early with +`CLI.CONFIG_MISSING_MARKER`; a marker declaring a version this CLI does +not support — older or newer, future markers included — fails with +`CLI.CONFIG_INVALID`; a file that cannot be evaluated at all fails with +`CLI.CONFIG_UNREADABLE`, per the previous paragraph. No best-effort +reading of unmarked files. **Why:** the unified CLI claims a filename Prisma 7 already owns; a silently misparsed v7 file is the worst launch bug available, so detection is a From 0c34b788c82f1759a5cce5a5ef33186ab50e1e0c Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 9 Aug 2026 23:24:56 +0200 Subject: [PATCH 15/18] docs(architecture): make the friction-points reference followable The path looked like a typo but is real: prisma/prisma has a directory literally named 'architecture docs'. Link the explicit GitHub URL and note the space so readers stop tripping over it. Co-Authored-By: Claude Fable 5 Signed-off-by: willbot Signed-off-by: Will Madden --- docs/architecture/cli-engine-requirements.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/architecture/cli-engine-requirements.md b/docs/architecture/cli-engine-requirements.md index 8b6085ce..80bd9056 100644 --- a/docs/architecture/cli-engine-requirements.md +++ b/docs/architecture/cli-engine-requirements.md @@ -266,8 +266,9 @@ Decided (Will Madden, 2026-08-09): the engine wraps **@stricli/core**, fully hidden per R3 — no stricli type appears in the engine's public interface, so the internals remain replaceable. -The decision followed the evaluation rubric recorded in prisma/prisma's -`docs/architecture docs/research/commander-friction-points.md`. Commander +The decision followed the evaluation rubric recorded in prisma/prisma at +[`docs/architecture docs/research/commander-friction-points.md`](https://github.com/prisma/prisma/blob/main/docs/architecture%20docs/research/commander-friction-points.md) +(the directory really is named `architecture docs`, space included). Commander was ruled out there. Clipanion, the incumbent candidate with in-house precedent, passes the rubric's nine technical criteria but fails the tenth: at decision time its last publish was 23 months old with its 4.x line in From 1899416b57d7f9d8b97b18a12aa64c96533b02fe Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 9 Aug 2026 23:25:39 +0200 Subject: [PATCH 16/18] docs(architecture): record evaluated framework versions in the decision The zero-dependency, no-process.exit and stale-release-line claims are version-specific. Record them: @stricli/core 1.3.0 (published 2026-07-16, now exact-pinned by the engine) versus Clipanion 4.0.0-rc.4 (2024-09-06; latest stable 3.2.1, June 2023). Co-Authored-By: Claude Fable 5 Signed-off-by: willbot Signed-off-by: Will Madden --- docs/architecture/cli-engine-requirements.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/architecture/cli-engine-requirements.md b/docs/architecture/cli-engine-requirements.md index 80bd9056..0a9bfc58 100644 --- a/docs/architecture/cli-engine-requirements.md +++ b/docs/architecture/cli-engine-requirements.md @@ -271,10 +271,13 @@ The decision followed the evaluation rubric recorded in prisma/prisma at (the directory really is named `architecture docs`, space included). Commander was ruled out there. Clipanion, the incumbent candidate with in-house precedent, passes the rubric's nine technical criteria but fails the tenth: -at decision time its last publish was 23 months old with its 4.x line in -release-candidate state for three years. Stricli passes all ten — zero -runtime dependencies, no `node:` imports, no `process.exit` (verified -against the published artifact), per-invocation injected context, static +at decision time its last publish, 4.0.0-rc.4 (2024-09-06), was 23 months +old with its 4.x line in release-candidate state for three years, and its +latest stable release, 3.2.1, dated from June 2023. Stricli — evaluated at +`@stricli/core` 1.3.0 (published 2026-07-16), the version the engine now +pins exactly — passes all ten: zero runtime dependencies, no `node:` +imports, no `process.exit` (verified against the published 1.3.0 +artifact), per-invocation injected context, static route maps with lazy command loading, parse-time validation with typed errors, active institutional maintenance — and its known limitations (per-command flags only, fixed help layout without formatter replacement) From a920cc086661ffe3ee86815ed9021e0dd1d8b466 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 9 Aug 2026 23:26:12 +0200 Subject: [PATCH 17/18] docs(plan): tag the dependency-graph fence as text markdownlint MD040 flags fences without a language. Co-Authored-By: Claude Fable 5 Signed-off-by: willbot Signed-off-by: Will Madden --- .drive/projects/prisma-cli-v8/plan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drive/projects/prisma-cli-v8/plan.md b/.drive/projects/prisma-cli-v8/plan.md index d738e4ba..819979db 100644 --- a/.drive/projects/prisma-cli-v8/plan.md +++ b/.drive/projects/prisma-cli-v8/plan.md @@ -87,7 +87,7 @@ when the operator can publish with one action (project DoD). ## Dependency graph -``` +```text S1 ──► S2 ──► S3 ──► S5 ──► S7 │ ▲ ▲ └──────┘ (published engine exists after S2's engine hardening) From 22877f75a3689f05f689c20d0d5b4981afd116c2 Mon Sep 17 00:00:00 2001 From: willbot Date: Mon, 10 Aug 2026 00:23:33 +0200 Subject: [PATCH 18/18] chore: exclude .drive design artifacts from biome The committed design drafts deliberately use erased types that trip error-severity lint rules; they are design prose, not shipped code. Same exclusion the s1-engine-vertical branch already carries. Co-Authored-By: Claude Fable 5 Signed-off-by: willbot Signed-off-by: Will Madden --- biome.jsonc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/biome.jsonc b/biome.jsonc index 9d7b8c00..dea2743e 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -6,7 +6,8 @@ "useIgnoreFile": true }, "files": { - "ignoreUnknown": false + "ignoreUnknown": false, + "includes": ["**", "!.drive"] }, "formatter": { "enabled": true,