@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 +224 -0
- package/ARCHITECTURE.md +206 -0
- package/CHANGELOG.md +108 -0
- package/FAQ.md +190 -0
- package/GETTING_STARTED.md +263 -0
- package/LICENSE +21 -0
- package/PARAM_SCHEMA.md +341 -0
- package/README.md +305 -0
- package/SECURITY.md +76 -0
- package/draft-cli.mjs +1757 -0
- package/package.json +58 -0
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.
|
package/ARCHITECTURE.md
ADDED
|
@@ -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.
|