@drbaher/draft-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,224 @@
1
+ # Agents
2
+
3
+ Drive `draft-cli` from an LLM agent or non-interactive client. Same shape as the rest of the contract-operations suite.
4
+
5
+ ## Output contract
6
+
7
+ - **Success**: substituted document body to **stdout** (or to `--output PATH` if given), exit `0`. With `--json`, **stdout** is a single JSON object instead.
8
+ - **Failure**: human-readable error to **stderr**, non-zero exit. With `--json`, **stdout** is `{ok: false, missing: [...]}` or similar; the error message still goes to stderr.
9
+ - Diagnostic text (`--why` block, warnings, `note:` lines, color) always goes to **stderr**. Stdout is reserved for the substituted document or the JSON report. This separation is the contract — pipelines can safely compose `template-vault get … | draft - | nda-review …` without stderr poisoning.
10
+ - `--silent` (`-q`) suppresses stderr completely after argument parsing. Use this for fully-quiet pipelines where you only want the stdout artifact.
11
+
12
+ ## Exit codes
13
+
14
+ | Code | Meaning |
15
+ |------|---------|
16
+ | `0` | Success |
17
+ | `1` | I/O error — template not found, unreadable, malformed `.docx`, write failure on `--output` |
18
+ | `2` | Validation — missing required parameters, orphan schema declarations, mixed-syntax without `--syntax`, no placeholders detected |
19
+ | `3` | `template-vault get` subprocess failed (vault ref like `nda/house-mutual`) |
20
+ | `4` | LLM tier failure — provider unreachable, auth rejected, non-JSON response, unsupported provider |
21
+
22
+ ## Discovery
23
+
24
+ ```sh
25
+ draft --help # human-readable help
26
+ draft --version # → draft-cli <semver>
27
+ draft --check-llm # one-token roundtrip to verify env-configured LLM provider
28
+ draft <template> --list-placeholders --json
29
+ # placeholders the agent will need to supply, machine-readable
30
+ draft <template> --validate --params deal.json
31
+ # completeness check; exit 2 with `--json` `{ok:false, missing:[...]}` on failure
32
+ draft <template> --diff --params deal.json --json
33
+ # structured diff (substitution table); never writes output
34
+ ```
35
+
36
+ Discovery flow for a fresh template:
37
+
38
+ 1. `draft <template> --list-placeholders --json` to see what parameters exist, which tier matched, and the alias map.
39
+ 2. Resolve each `key` to a value from the deal context.
40
+ 3. `draft <template> --params deal.json --validate --json` to confirm everything resolves before generating output.
41
+ 4. `draft <template> --params deal.json --output draft.md --json` to substitute and capture a structured report.
42
+
43
+ ## JSON shapes
44
+
45
+ ### `--list-placeholders --json`
46
+
47
+ ```json
48
+ {
49
+ "template": "nda/house-mutual",
50
+ "tier": "bracket",
51
+ "placeholders": [
52
+ {
53
+ "key": "party_a",
54
+ "first_seen_as": "Party A",
55
+ "aliases": ["Party A", "Disclosing Party"],
56
+ "required": true,
57
+ "occurrences": 4,
58
+ "tier": "bracket"
59
+ }
60
+ ],
61
+ "warnings": [],
62
+ "unmapped": []
63
+ }
64
+ ```
65
+
66
+ ### `--validate --json`
67
+
68
+ On success:
69
+
70
+ ```json
71
+ { "ok": true, "resolved": ["party_a", "party_b"], "sources": { "party_a": "params", "party_b": "cli" } }
72
+ ```
73
+
74
+ On failure:
75
+
76
+ ```json
77
+ { "ok": false, "missing": ["party_b"] }
78
+ ```
79
+
80
+ ### `--diff --json`
81
+
82
+ ```json
83
+ {
84
+ "ok": true,
85
+ "tier": "bracket",
86
+ "diff": [
87
+ { "key": "party_a", "from": "[Party A]", "to": "Acme Corporation", "occurrences": 2 },
88
+ { "key": "effective_date", "from": "[Effective Date]", "to": null, "occurrences": 1 }
89
+ ]
90
+ }
91
+ ```
92
+
93
+ `to: null` means the placeholder is unresolved — `--diff` doesn't error on missing values; it shows them so the caller can decide.
94
+
95
+ ### Main draft with `--json`
96
+
97
+ ```json
98
+ {
99
+ "ok": true,
100
+ "tier": "bracket",
101
+ "output_path": null,
102
+ "output": "<substituted document body>",
103
+ "placeholders": [ /* same shape as --list-placeholders */ ],
104
+ "sources": { "party_a": "cli", "party_b": "params" },
105
+ "warnings": [],
106
+ "unmapped": [
107
+ { "phrase": "See Section 4", "tier": "bracket" }
108
+ ]
109
+ }
110
+ ```
111
+
112
+ If `--output PATH` is set, `output_path` is the path and `output` is `null` (the document was written to disk, not embedded in the JSON).
113
+
114
+ ## Tier names
115
+
116
+ The `tier` field on JSON output indicates which detection strategy matched. Stable across minor versions:
117
+
118
+ | Tier value | Meaning |
119
+ |------------|---------|
120
+ | `"bracket"` | `[Title Case]` literal match |
121
+ | `"mustache"` | `{{...}}` literal match (only when `--syntax mustache`) |
122
+ | `"docx-highlight"` | Yellow / green / cyan / magenta highlighted runs in `.docx` |
123
+ | `"heuristic"` | Bundled generic-name dictionary (warn-only by default) |
124
+ | `"llm"` | LLM-suggested placeholders (only when env-configured) |
125
+ | `"none"` | Cascade found zero placeholders |
126
+
127
+ ## Failure → recovery
128
+
129
+ | Symptom | Diagnose | Recover |
130
+ |---|---|---|
131
+ | exit 1, `template not found` | Check the path; if it looks like a vault ref (`nda/house-mutual`), make sure `template-vault` is on `PATH`. | Pipe the body via stdin: `template-vault get … \| draft -` |
132
+ | exit 2, `missing required parameter(s)` | `draft <template> --list-placeholders --json` | Supply the missing keys via `--<key>` flags or `--params`. Note typo warnings in `warnings[]`. |
133
+ | exit 2, `mixed placeholder conventions` | Inspect the template for both `[X]` and `{{Y}}` | Pass `--syntax bracket` or `--syntax mustache` to pick one family. The other is left untouched. |
134
+ | exit 2, `schema declares … but no matching phrase` | Schema-template drift. The schema declares a key whose alias list doesn't appear in the template body. | Remove the orphan from the schema, or add the phrase to the template. |
135
+ | exit 2, `no placeholders detected by deterministic tiers` | Template has no markup; T1–T4 all empty. | Either ship a schema with explicit aliases, opt into `.docx` highlights (already auto), or configure `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` in `.env` to enable T5 LLM detection. |
136
+ | exit 3, `template-vault get … failed` | The vault subprocess returned non-zero. Stderr surfaces the vault's error. | Fix the vault ref or fall back to a local file. |
137
+ | exit 4, `LLM call failed: 401` | Provider auth rejected | Rotate `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` in `.env`. |
138
+ | exit 4, `LLM returned non-JSON response` | Model didn't follow the prompt format | Try a stronger model via `DRAFT_LLM_MODEL=…`. Or pass `--no-llm` to stay deterministic. |
139
+
140
+ ## Recommended defaults for agent invocations
141
+
142
+ ```sh
143
+ # Discovery
144
+ draft "$TEMPLATE" --list-placeholders --json
145
+
146
+ # Validate before committing to substitution
147
+ draft "$TEMPLATE" --validate --params deal.json --json
148
+
149
+ # Substitute with structured output for downstream pipelining
150
+ draft "$TEMPLATE" --params deal.json --output draft.md --json --why
151
+
152
+ # Pipe via the suite
153
+ template-vault get nda/house-mutual \
154
+ | draft - --params deal.json --json --no-llm \
155
+ | jq -r '.output' \
156
+ | nda-review review - --playbook house --json
157
+ ```
158
+
159
+ `--no-llm` is recommended for agent-driven pipelines unless the agent has explicit license to invoke a network call. `draft-cli`'s T5 auto-runs only when env configures a provider; passing `--no-llm` disables it even then.
160
+
161
+ ## LLM safety
162
+
163
+ - **No network by default.** T5 LLM tier runs only when `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `DRAFT_LLM_*` is configured in `.env` or the process environment.
164
+ - **Template text only.** T5 sends the template body to the provider. It does **not** send `--params` values, schema contents, `.env` contents, or any other env variables.
165
+ - **Explicit kill-switch.** `--no-llm` disables T5 even when configured. Use this in any pipeline that handles privileged contracts.
166
+ - **Fail-fast assertion.** `--llm` asserts the provider is configured; exits `4` immediately if not. Use this in scripts that *require* T5 so they don't silently fall back to a deterministic-only cascade.
167
+
168
+ Documented in [SECURITY.md](SECURITY.md).
169
+
170
+ ## Heuristic safety gate
171
+
172
+ T4 (heuristic dictionary) has a higher false-positive risk than the other deterministic tiers — substituting over a real party name that happens to be `Acme Corporation` would be embarrassing. By default in non-TTY contexts T4 matches are **listed but not substituted**; the run exits `2` with a warning. Bypass with `--yes-heuristic` (the user has reviewed and accepts the risk) or disable T4 entirely with `--no-heuristic`.
173
+
174
+ Agents should default to `--no-heuristic` unless the calling context has explicitly cleared the template for heuristic substitution.
175
+
176
+ ## Library use (Node.js)
177
+
178
+ `draft-cli.mjs` is an ESM module. Every meaningful function is exported:
179
+
180
+ ```js
181
+ import {
182
+ detectBracket, detectMustache, detectDocxHighlight, detectHeuristic, detectLlm,
183
+ parseSchema, loadSchema, runCascade, substitute, resolveValues,
184
+ parseArgs, main, completionScript, VERSION, EXIT
185
+ } from "@drbaher/draft-cli";
186
+
187
+ // Programmatic substitution
188
+ const args = parseArgs(["x.md", "--party-a", "Acme"]);
189
+ const input = { kind: "text", body: "Between [Party A] and [Party B]", path: null };
190
+ const result = await runCascade(input, args, /*schema=*/null, /*env=*/{});
191
+ // → { tier: "bracket", placeholders: [...], warnings: [], unmapped: [] }
192
+ const { resolved, missing } = await resolveValues(result.placeholders, args, /*paramsJson=*/{});
193
+ if (missing.length === 0) {
194
+ const out = substitute(input.body, result.placeholders, resolved, result.tier);
195
+ // out === "Between Acme and [Party B]"
196
+ }
197
+ ```
198
+
199
+ Or invoke the full CLI in-process with captured I/O:
200
+
201
+ ```js
202
+ import { Writable } from "node:stream";
203
+ import { main } from "@drbaher/draft-cli";
204
+
205
+ class Capture extends Writable {
206
+ constructor() { super(); this.s = ""; }
207
+ _write(c, _e, cb) { this.s += c; cb(); }
208
+ }
209
+ const out = new Capture(), err = new Capture();
210
+ const code = await main(["x.md", "--party-a", "Acme", "--json"], { out, err });
211
+ // out.s is the JSON report; err.s has any --why block or warnings.
212
+ ```
213
+
214
+ Test fixtures (mock spawners for `template-vault`, mock fetchers for LLM, synthesized `.docx` files) are in `tests/_helpers.mjs` if you want to drive the CLI in tests of your own.
215
+
216
+ ## See also
217
+
218
+ - [README.md](README.md) — install, 30-second first run, command reference.
219
+ - [GETTING_STARTED.md](GETTING_STARTED.md) — 10-minute walkthrough.
220
+ - [PARAM_SCHEMA.md](PARAM_SCHEMA.md) — locked parameter contract: cascade, schema file, precedence.
221
+ - [ARCHITECTURE.md](ARCHITECTURE.md) — single-file rationale, substitution model, `.docx` parsing.
222
+ - [SECURITY.md](SECURITY.md) — threat model + LLM data-flow disclosure.
223
+ - [FAQ.md](FAQ.md) — design questions and trade-offs.
224
+ - [CHANGELOG.md](CHANGELOG.md) — release notes + the v2 "Deferred" list.
@@ -0,0 +1,206 @@
1
+ # Architecture
2
+
3
+ A walk-through of how `draft-cli` is shaped and why. Read this before
4
+ contributing — it explains the constraints that drove the design.
5
+
6
+ ## Single-file CLI
7
+
8
+ `draft-cli.mjs` is one file. Helpers, tiers, command dispatchers, and
9
+ the main entry point all live in it. There is no `src/` directory, no
10
+ build step, no compiled output. Run it directly:
11
+
12
+ ```sh
13
+ node draft-cli.mjs --demo
14
+ ```
15
+
16
+ The file is exported as ESM so the test suite can `import` individual
17
+ functions. The `if (isMain)` block at the bottom runs `main()` only
18
+ when the file is invoked directly, not when imported.
19
+
20
+ Trade-off: the file is large (≈ 1000 LOC) and you have to scroll. The
21
+ upside is that the entire CLI is in one place, has one set of imports,
22
+ and can be vendored or audited as a single artifact.
23
+
24
+ ## The cascade
25
+
26
+ `runCascade()` in `draft-cli.mjs` orchestrates the five detection tiers
27
+ in this order:
28
+
29
+ ```
30
+ T1 bracket [Title Case] ──► hits? stop.
31
+ else
32
+ T2 mustache {{X}} (only if --syntax mustache; else skip)
33
+ hits? stop.
34
+ else
35
+ T3 .docx highlight runs (only if input is .docx)
36
+ hits? stop.
37
+ else
38
+ T4 heuristic dictionary (skipped by --no-heuristic)
39
+ hits? stop. Gate output behind confirmation.
40
+ else
41
+ T5 LLM (skipped by --no-llm or no env provider)
42
+ hits? stop.
43
+ else
44
+ done with zero placeholders. Caller decides whether that's an error.
45
+ ```
46
+
47
+ **Sequential-with-stop** is the locked semantic. A non-empty tier wins
48
+ and the others are skipped. This is predictable; it means a bracketed
49
+ template never accidentally invokes the LLM. The alternative — union
50
+ all tiers — was rejected during the design review because the conflict
51
+ resolution between tiers (same canonical key from two tiers with
52
+ different match texts) was a hidden complexity.
53
+
54
+ ## Substitution model
55
+
56
+ `substitute()` does byte-level replacement on the original template
57
+ body. It does **not** parse the body, build an AST, and re-emit. This
58
+ means:
59
+
60
+ - Whitespace, line endings, and Markdown structure are preserved exactly.
61
+ - The output is the input with placeholder runs swapped out — nothing
62
+ else changes.
63
+ - `.docx` input is the one exception: we extract text from
64
+ `word/document.xml` first, then substitute on the extracted text.
65
+ The output is plain markdown, not a re-written `.docx` (that's v2).
66
+
67
+ For T1/T2 (bracket/mustache), substitution uses literal string
68
+ replacement of the full match (`[Party A]` → `Acme`). For T3/T4/T5
69
+ (text-based tiers), substitution uses a whole-word regex
70
+ (`(?<![A-Za-z0-9])Acme Corporation(?![A-Za-z0-9])`) so we don't
71
+ overlap-substitute into adjacent words.
72
+
73
+ ## Schema file handling
74
+
75
+ `loadSchema()` looks for a sibling file next to the template:
76
+ `<template>.params.json` or `<template_basename>.params.json`. If
77
+ neither exists, returns `null` and the cascade uses inferred keys.
78
+
79
+ If the parsed JSON has a top-level `_meta` key, it's long form
80
+ (`{ aliases, required, default }` per entry). Otherwise short form
81
+ (`key: [aliases…]`). The two forms are not mixable within one file —
82
+ the parser commits to one shape on the first call.
83
+
84
+ `findOrphans()` checks that every schema-declared key has a matching
85
+ detected placeholder. Orphans are exit-2 errors (locked decision Q4).
86
+
87
+ ## Value resolution precedence
88
+
89
+ `resolveValues()` walks placeholders in order and assigns a value from
90
+ the first source that has one:
91
+
92
+ ```
93
+ CLI flag (--key-name VALUE)
94
+ → --params JSON file
95
+ → --interactive prompt (only if --interactive set)
96
+ → schema default (only if long form with "default")
97
+ → error (exit 2 with a listing of missing keys)
98
+ ```
99
+
100
+ The empty string is a valid CLI value (`--party-a ""`). Only **absence**
101
+ falls through to the next source.
102
+
103
+ ## Why we shell out to template-vault
104
+
105
+ `resolveInput()` detects `<category>/<name>[@version]`-shaped args and
106
+ runs `template-vault get` as a subprocess. We do NOT import
107
+ template-vault-cli as a library, because:
108
+
109
+ 1. template-vault-cli is Python; draft-cli is Node. No shared runtime.
110
+ 2. Even if we re-implemented the vault read path in Node, we'd duplicate
111
+ the lookup semantics (default sources, hash pinning, version
112
+ resolution). The vault is the source of truth for its own data.
113
+ 3. Subprocess isolation is a feature: a draft-cli bug can't corrupt a
114
+ vault, and a vault bug can't crash draft-cli without a clear exit
115
+ code (`3` for vault failure).
116
+
117
+ The `spawnSync` call is injectable via the `spawner` option on
118
+ `resolveInput()`, which is how the tests mock it without invoking a
119
+ real template-vault binary.
120
+
121
+ ## `.docx` parsing
122
+
123
+ T3 uses `jszip` to unzip the `.docx`, reads `word/document.xml`, and
124
+ regex-extracts highlight runs (`<w:r>` containing
125
+ `<w:highlight w:val="..."/>`). The XML structure of a Word document is
126
+ well-known enough that regex is robust:
127
+
128
+ ```js
129
+ const runRe = /<w:r\b[\s\S]*?<\/w:r>/g;
130
+ // inside each run: <w:rPr><w:highlight w:val="yellow"/></w:rPr><w:t>text</w:t>
131
+ ```
132
+
133
+ We don't take a full XML parser dependency (`@xmldom/xmldom` or
134
+ similar) because the surface we care about is narrow and the regex is
135
+ under 10 lines.
136
+
137
+ Output for `.docx` input is plain markdown — extracted text in document
138
+ order, paragraphs joined with `\n`. Round-tripping back into `.docx`
139
+ (preserving styles, numbering, run formatting) is intentionally out of
140
+ scope for v1.
141
+
142
+ ## LLM tier
143
+
144
+ `detectLlm()` is invoked only at the bottom of the cascade. It accepts
145
+ a `fetcher` injection so tests can mock the HTTP call without a
146
+ network. The prompt is fixed at the top of `callLlm()`:
147
+
148
+ > Given the document text below, identify spans that look like
149
+ > placeholders — names, dates, or party-identifier text that a drafter
150
+ > would replace before sending. Do NOT detect cross-references or
151
+ > section labels. Output JSON ONLY in this exact shape: …
152
+
153
+ Response parsing is permissive: we look for a balanced `{…}` substring,
154
+ parse it, validate each entry has a string `text` and a snake_case
155
+ `suggested_key`. Anything else is dropped silently.
156
+
157
+ ## ANSI color
158
+
159
+ `paint()` and `colorEnabled()` honor:
160
+
161
+ - `NO_COLOR` env (any non-empty value → off, per https://no-color.org/)
162
+ - `FORCE_COLOR` env (any non-empty value → on)
163
+ - Otherwise: on iff the target stream `isTTY`.
164
+
165
+ Color codes go to **stderr** only. Stdout is always plain so it pipes
166
+ cleanly into downstream tools.
167
+
168
+ ## Test layout
169
+
170
+ ```
171
+ tests/
172
+ _helpers.mjs — Shared fixtures, CaptureStream, mock fetchers, .docx synthesis.
173
+ fixtures/ — Template + schema files used by tests.
174
+ test_args.mjs — parseArgs and UsageError.
175
+ test_cascade.mjs — runCascade orchestration & tier-stop semantics.
176
+ test_env.mjs — .env reader, llmProviderFromEnv, color.
177
+ test_modes.mjs — Main 'draft', --list-placeholders, --validate end-to-end.
178
+ test_output.mjs — --why, --json, --output PATH, --demo.
179
+ test_schema.mjs — Short vs long form, orphans, key validity.
180
+ test_substitution.mjs — substitute(), resolveValues(), precedence.
181
+ test_t1_bracket.mjs — T1 detection rule + real Common Paper template.
182
+ test_t2_mustache.mjs — T2 detection.
183
+ test_t3_docx.mjs — T3 detection, jszip-synthesized .docx.
184
+ test_t4_heuristic.mjs — T4 detection + dictionary override.
185
+ test_t5_llm.mjs — T5 detection with mocked HTTP.
186
+ test_template_vault.mjs — Subprocess spawning with mock spawner.
187
+ ```
188
+
189
+ One concern per file, modeled on template-vault-cli's test layout.
190
+ Run with `node --test tests/test_*.mjs`. Coverage with
191
+ `node --test --experimental-test-coverage tests/test_*.mjs`. Target:
192
+ ≥ 80% line on `draft-cli.mjs`. Current: 87.2%.
193
+
194
+ ## Forward-compatibility hooks
195
+
196
+ The locked schema reserves field names for v2:
197
+
198
+ - Long-form entries can grow `"type": "date" | "money" | "party" | ...`
199
+ for typed parameters.
200
+ - Long-form entries can grow `"computed": "..."` for computed values
201
+ (`[Effective Date + 2 years]`).
202
+ - Long-form entries can grow `"detect": "highlight" | "literal" | "bracket"`
203
+ for tier-specific detection preferences.
204
+
205
+ These are reserved but unused in v1. Adding them in v2 will not break
206
+ existing v1 schema files.
package/CHANGELOG.md ADDED
@@ -0,0 +1,108 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file. The
4
+ format is loosely based on [Keep a Changelog](https://keepachangelog.com/),
5
+ and the project adheres to semantic versioning once it leaves 0.x.
6
+
7
+ ## 0.1.0 — 2026-05-16
8
+
9
+ Initial release. Single-file Node.js CLI for deterministic placeholder
10
+ substitution in legal-document templates. Part of the contract-operations
11
+ suite ([cli.drbaher.com](https://cli.drbaher.com)).
12
+
13
+ ### Added
14
+
15
+ - **Five-tier sequential-with-stop detection cascade.** First non-empty
16
+ tier wins.
17
+ - T1: `[Title Case]` brackets. Common Paper / YC SAFE / Bonterms.
18
+ - T2: `{{Title Case}}` or `{{snake_case}}` mustache (opt-in via
19
+ `--syntax mustache`).
20
+ - T3: `.docx` highlight runs (yellow / green / cyan / magenta) via
21
+ `jszip` + regex on `word/document.xml`.
22
+ - T4: Heuristic dictionary (`Acme Corporation`, `John Doe`,
23
+ `example@example.com`, `MM/DD/YYYY`, etc.). Warn-only by default;
24
+ requires interactive confirmation or `--yes-heuristic` to substitute.
25
+ - T5: LLM (Anthropic / OpenAI / explicit `DRAFT_LLM_*`). Auto-runs only
26
+ when `.env` or process env configures a provider. `--no-llm` disables.
27
+ - **Schema file `<template>.params.json`** in short or long form.
28
+ Auto-selected by presence of a top-level `_meta` key. Short form is
29
+ `{ key: [aliases…] }`; long form supports `required` and `default`.
30
+ - **Value resolution precedence**: CLI flag > `--params` JSON >
31
+ `--interactive` prompt > schema `default` > error.
32
+ - **Three modes**: main `draft`, `--list-placeholders`, `--validate`.
33
+ All three support `--json` and `--why` structured explanation.
34
+ - **Composable I/O**: stdin (`-`), stdout default, `--output PATH`,
35
+ `template-vault get` integration for `<category>/<name>[@version]` refs.
36
+ - **ANSI color** honors `NO_COLOR` and `FORCE_COLOR`; auto-disables off-TTY.
37
+ - **`--demo`** flag for a zero-file 30-second first run (`npx @drbaher/draft-cli@latest --demo`).
38
+ - **`--completion bash|zsh`** flag that emits a hand-rolled shell completion
39
+ script to stdout. Completes top-level flags, the `--syntax` value
40
+ (`bracket`/`mustache`), the `--completion` shell name, and file paths
41
+ for `--params`/`--output`/`--dictionary`. No third-party generator.
42
+ - **`--check-llm`** runs a one-token roundtrip against the configured LLM
43
+ provider — verifies env, auth, and reachability without sending any
44
+ template content. Exits `0` on success, `1` if no provider is configured,
45
+ `4` on provider error. Useful for CI / startup health checks in agent
46
+ pipelines.
47
+ - **`--diff`** prints a per-placeholder substitution table to stdout and
48
+ exits — never writes output. With `--json`, emits a structured `diff`
49
+ array. Unresolved placeholders appear as `(unresolved)` / `to: null`
50
+ rather than erroring, so the caller can decide what to do.
51
+ - **`-q` / `--silent`** suppresses all stderr (warnings, `--why` block,
52
+ notes, heuristic confirmations) for fully-quiet pipeline use. Argument-
53
+ parse errors still surface on the real stderr.
54
+ - **Schema-rescue for T1/T2 detection.** Bracketed runs whose inner text
55
+ matches a schema-declared alias are admitted by detection even when
56
+ the heuristic rule would reject them. Lets all-caps signature-block
57
+ markers (`[COMPANY]`) and fill-in markers (`[_____________]`) be
58
+ brought into the alias map without loosening the heuristic itself.
59
+ - **Typo guard** on `--<param-name>` flags. Unused flags are surfaced
60
+ as warnings and named in the missing-required error, so a typo'd
61
+ `--party-bb` doesn't silently fall through to a "missing party_b"
62
+ error with no connection.
63
+ - **Exit codes**: `0` ok, `1` i/o, `2` validation, `3` template-vault failure,
64
+ `4` LLM failure.
65
+ - **GitHub Actions CI**: Ubuntu × macOS × Node 18 / 20 / 22 test matrix,
66
+ coverage gate at 80% line, and smoke job that packs + installs + runs
67
+ `--version` + `--demo`.
68
+ - **GitHub Actions publish**: npm Trusted Publishing on `v*` tag push,
69
+ with version-vs-tag check and `--provenance` attestation.
70
+ - **Test suite**: 106 tests across 13 files (`unittest`-style per concern),
71
+ 87.2% line coverage on `draft-cli.mjs`.
72
+
73
+ ### Notes
74
+
75
+ - One runtime dependency only: `jszip` (MIT, zero transitive deps).
76
+ - The LLM tier sends template text only — no params, no `.env` contents,
77
+ no other data. No network call by default.
78
+ - Configuration contract is captured in
79
+ [PARAM_SCHEMA.md](PARAM_SCHEMA.md), reviewed and locked before code.
80
+ - **T1 bracket rule is permissive**, not strict Title-Case. Real
81
+ Common Paper / YC SAFE / Bonterms templates use sentence-shaped
82
+ placeholders with full punctuation (`[Today’s date]`, `[1 year(s)]`,
83
+ `[Fill in city or county and state, i.e. "courts located in New Castle, DE"]`).
84
+ The rule rejects markdown links (`[label](url)`), checkbox markers
85
+ (`[x]`, `[ ]`), pure section refs (`[3.1]`), all-caps headings, and
86
+ punctuation-only brackets — but otherwise admits anything bracketed
87
+ that contains at least one letter. False positives are filtered with
88
+ the schema file; false negatives in this domain are higher-cost.
89
+
90
+ ## Deferred (v2 candidates)
91
+
92
+ - **`.docx` output round-trip.** v1 writes plain markdown even from a
93
+ `.docx` input. Re-writing back into a `.docx` (preserving styles,
94
+ numbering, and run formatting) is a separate problem.
95
+ - **Computed placeholders** (`[Effective Date + 2 years]`). The long-form
96
+ schema reserves a future `"computed"` field.
97
+ - **Typed parameters** (`party`, `date`, `money` with format validation).
98
+ Schema reserves a future `"type"` field.
99
+ - **LLM-assisted parameter inference from a deal description.** v1's T5
100
+ only suggests placeholders from template text — not from external prose
101
+ describing the deal.
102
+ - **Cross-template parameter registry** (`parties.json` remembering
103
+ addresses, e-signature contacts, etc.). Additive — would layer
104
+ underneath `--params` in precedence.
105
+ - **Multi-document bundles** (MSA + SOW sharing parameters in one call).
106
+ v1 is one document per invocation.
107
+ - **`.docx` highlight detection beyond yellow/green/cyan/magenta.** v1
108
+ ignores other colors (black/white/none) by design.