@drbaher/draft-cli 0.7.0 → 0.8.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/CHANGELOG.md CHANGED
@@ -4,6 +4,55 @@ All notable changes to this project will be documented in this file. The
4
4
  format is loosely based on [Keep a Changelog](https://keepachangelog.com/),
5
5
  and the project adheres to semantic versioning once it leaves 0.x.
6
6
 
7
+ ## 0.8.0 — 2026-05-17
8
+
9
+ ### Added
10
+
11
+ - **LLM inference from a deal description** (last v2 item). New
12
+ `--from-deal PATH` flag reads a free-form deal description and
13
+ asks the configured T5 LLM provider to extract values for the
14
+ schema's declared placeholders:
15
+ ```sh
16
+ draft nda.md --from-deal deal-notes.txt --output draft.md
17
+ ```
18
+ Where `deal-notes.txt` is unstructured prose:
19
+ ```
20
+ Mutual NDA between Acme Corporation (DE) and Globex (UK), effective
21
+ June 1, 2026, for a 2-year term.
22
+ ```
23
+ The LLM is asked to fill `party_a`, `party_a_state`, `party_b`,
24
+ `effective_date`, etc. — only the keys already detected as
25
+ placeholders are extracted.
26
+ - **Value-resolution precedence updated:**
27
+ `CLI flag > --params JSON > --from-deal (LLM) > --interactive > schema default > error`.
28
+ CLI / --params always win, so users can fix or override anything
29
+ the LLM got wrong.
30
+ - **New public API:** `inferFromDeal(dealText, placeholders, providerCfg, { fetcher })`.
31
+
32
+ ### Decisions locked (V2_BRIEFS_REMAINING Q4.1–Q4.3)
33
+
34
+ - **Q4.1 Provider:** same T5 provider config — `ANTHROPIC_API_KEY`,
35
+ `OPENAI_API_KEY`, or explicit `DRAFT_LLM_*`. No separate inference
36
+ provider; one network surface, one set of env vars.
37
+ - **Q4.2 Extra keys:** keys the LLM emits that aren't in the
38
+ detected placeholders are **warned** to stderr (not dropped
39
+ silently). The LLM gets a fresh list of allowed keys in the
40
+ prompt so this is rare in practice.
41
+ - **Q4.3 Auto-LLM:** `--from-deal` does **not** require an
42
+ explicit `--llm` flag — the inference is implicit. `--no-llm`
43
+ still disables it (the user can opt out of the network call).
44
+
45
+ ### Notes
46
+
47
+ - `--from-deal` errors are fatal (`EXIT.LLM` for provider /
48
+ network / parse failures). Users with bad provider configs see
49
+ the issue immediately rather than silently running with no
50
+ inferred values.
51
+ - Bundle mode (v0.7.0) does not yet thread `--from-deal` through
52
+ per-template inference. Deferred to a follow-up; the shared
53
+ parameter resolution makes the single-doc API already useful
54
+ for bundles via `--params`.
55
+
7
56
  ## 0.7.0 — 2026-05-17
8
57
 
9
58
  ### Added
package/PARAM_SCHEMA.md CHANGED
@@ -515,6 +515,71 @@ keys and their sources.
515
515
  Programmatic API: `loadBundle(path)` parses + validates; `cmdBundle`
516
516
  runs the orchestration with the same IO contract as `cmdDraft`.
517
517
 
518
+ ### LLM inference from a deal description (v0.8.0, opt-in)
519
+
520
+ `--from-deal PATH` reads a free-form deal description and asks the
521
+ configured T5 LLM provider to extract values for the schema's
522
+ declared placeholders. The inverse of T5 detection — instead of
523
+ inferring *where* placeholders are in a template, infer *what
524
+ values* they should take from the deal prose:
525
+
526
+ ```sh
527
+ draft nda.md --from-deal deal-notes.txt --output draft.md
528
+ ```
529
+
530
+ ```
531
+ # deal-notes.txt
532
+ Mutual NDA between Acme Corporation (DE) and Globex (UK),
533
+ effective June 1, 2026, for a 2-year term.
534
+ ```
535
+
536
+ Then `[Party A]` → `Acme Corporation`, `[Effective Date]` →
537
+ `June 1, 2026`, etc., without any `--party-a` / `--effective-date`
538
+ flags.
539
+
540
+ **Value-resolution precedence** with `--from-deal`:
541
+
542
+ ```
543
+ CLI flag > --params JSON > --from-deal (LLM) > --interactive > schema default > error
544
+ ```
545
+
546
+ CLI / --params always win, so users can fix or override anything the
547
+ LLM got wrong without re-running inference.
548
+
549
+ **Q4.1 locked:** same T5 provider config (`ANTHROPIC_API_KEY`,
550
+ `OPENAI_API_KEY`, or explicit `DRAFT_LLM_*`). One network surface,
551
+ one set of env vars.
552
+
553
+ **Q4.2 locked:** extra keys (LLM emits keys not in the detected
554
+ placeholder list) are **warned** to stderr, not silently dropped.
555
+ The LLM gets the allowed-key list in the prompt so this is rare in
556
+ practice.
557
+
558
+ **Q4.3 locked:** `--from-deal` does **not** require explicit
559
+ `--llm` — the inference is implicit when the flag is present.
560
+ `--no-llm` still disables the inference call (the user can opt
561
+ out of the network).
562
+
563
+ **Provider missing:** if no LLM provider is configured (no
564
+ `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `DRAFT_LLM_*` in env),
565
+ `--from-deal` errors immediately with `EXIT.LLM` (exit 5) and a
566
+ clear message. Same for network / HTTP errors / non-JSON LLM
567
+ responses.
568
+
569
+ **Resolution interaction with typed parameters:** inferred values
570
+ go through the same typed-normalization step as user-supplied
571
+ values. So an LLM that returns `"June 1, 2026"` for a `type: date`
572
+ parameter with `format: yyyy-MM-d` gets normalized to `2026-06-1`
573
+ before substitution.
574
+
575
+ **Bundle mode (v0.7.0) interaction:** bundles do not currently
576
+ thread `--from-deal` through per-template inference. The shared
577
+ parameter resolution already accepts `--params` JSON, which is the
578
+ simpler structured-data path for bundle workflows. Deferred to a
579
+ future release.
580
+
581
+ Programmatic API: `inferFromDeal(dealText, placeholders, providerCfg, { fetcher })`.
582
+
518
583
  ### Orphan handling (Q4 locked)
519
584
 
520
585
  Schema declares a key whose alias list matches no detected phrase →
package/draft-cli.mjs CHANGED
@@ -70,7 +70,7 @@ import { fileURLToPath } from "node:url";
70
70
  */
71
71
 
72
72
  /** @type {string} */
73
- export const VERSION = "0.7.0";
73
+ export const VERSION = "0.8.0";
74
74
 
75
75
  // ─── EXIT CODES ─────────────────────────────────────────────────────────────
76
76
  /**
@@ -279,6 +279,7 @@ export function parseArgs(argv) {
279
279
  if (a === "--params") { opts.params = argv[++i]; continue; }
280
280
  if (a === "--parties") { opts.parties = argv[++i]; continue; }
281
281
  if (a === "--bundle") { opts.bundle = argv[++i]; continue; }
282
+ if (a === "--from-deal") { opts.fromDeal = argv[++i]; continue; }
282
283
  if (a === "--output" || a === "-o") { opts.output = argv[++i]; continue; }
283
284
  if (a === "--syntax") {
284
285
  const v = argv[++i];
@@ -808,6 +809,95 @@ ${body.slice(0, 12000)}`;
808
809
  return out;
809
810
  }
810
811
 
812
+ /**
813
+ * v2 #4: LLM inference from a free-form deal description.
814
+ *
815
+ * Takes the prose deal description (the user's notes about parties, dates,
816
+ * amounts, etc.) and asks the configured T5 LLM provider to extract values
817
+ * for the placeholders the cascade has already detected. Returns
818
+ * `{values, extraKeys, warnings}`:
819
+ *
820
+ * - values: `{key: string}` for every placeholder key the LLM filled
821
+ * - extraKeys: any keys the LLM emitted that aren't in the placeholders list (Q4.2 → warn)
822
+ * - warnings: human-readable messages for malformed entries
823
+ *
824
+ * Throws on missing provider config, missing `fetch`, network/HTTP error, or
825
+ * non-JSON LLM response — same failure boundaries as `detectLlm`.
826
+ *
827
+ * @param {string} dealText — free-form deal description
828
+ * @param {Placeholder[]} placeholders — the post-detection placeholder list
829
+ * @param {ReturnType<llmProviderFromEnv>} providerCfg
830
+ * @param {{ fetcher?: typeof fetch | null }} [opts]
831
+ * @returns {Promise<{ values: Object<string,string>, extraKeys: string[], warnings: string[] }>}
832
+ */
833
+ export async function inferFromDeal(dealText, placeholders, providerCfg, { fetcher = (typeof fetch !== "undefined" ? fetch : null) } = {}) {
834
+ if (!fetcher) {
835
+ const e = new Error("fetch is not available; Node 18+ is required for --from-deal");
836
+ e.exitCode = EXIT.LLM;
837
+ throw e;
838
+ }
839
+ if (!providerCfg) {
840
+ const e = new Error("--from-deal requires an LLM provider; set ANTHROPIC_API_KEY / OPENAI_API_KEY / DRAFT_LLM_* in .env");
841
+ e.exitCode = EXIT.LLM;
842
+ throw e;
843
+ }
844
+ const wantedKeys = placeholders.map((p) => ({
845
+ key: p.key,
846
+ aliases: (p.aliases || []).slice(0, 4),
847
+ first_seen_as: p.first_seen_as,
848
+ }));
849
+ if (wantedKeys.length === 0) {
850
+ return { values: {}, extraKeys: [], warnings: [] };
851
+ }
852
+ const fieldList = wantedKeys.map((w) =>
853
+ ` - ${w.key} (template placeholder: "${w.first_seen_as}"${w.aliases.length > 1 ? `; aliases: ${w.aliases.join(", ")}` : ""})`
854
+ ).join("\n");
855
+ const prompt = `You are filling parameters for a legal-document drafting tool.
856
+ A user has written prose describing a deal. Extract values for the following
857
+ fields from the deal description. Output JSON ONLY in this exact shape, with
858
+ no commentary:
859
+
860
+ {"values":{"<key>":"<extracted_value>",...}}
861
+
862
+ If a field can't be confidently extracted from the description, omit it (do
863
+ NOT guess). Do not invent additional fields not in the list. Match the deal's
864
+ language verbatim — don't reformat dates, currencies, or names.
865
+
866
+ FIELDS:
867
+ ${fieldList}
868
+
869
+ DEAL DESCRIPTION:
870
+ ${dealText.slice(0, 12000)}`;
871
+ const raw = await callLlm(providerCfg, prompt, fetcher);
872
+ let parsed;
873
+ try {
874
+ const jsonMatch = raw.match(/\{[\s\S]*\}/);
875
+ parsed = JSON.parse(jsonMatch ? jsonMatch[0] : raw);
876
+ } catch {
877
+ const e = new Error(`LLM returned non-JSON response for --from-deal`);
878
+ e.exitCode = EXIT.LLM;
879
+ throw e;
880
+ }
881
+ const rawValues = (parsed && typeof parsed.values === "object" && parsed.values) ? parsed.values : {};
882
+ const knownKeys = new Set(placeholders.map((p) => p.key));
883
+ const values = {};
884
+ const extraKeys = [];
885
+ const warnings = [];
886
+ for (const [k, v] of Object.entries(rawValues)) {
887
+ if (!knownKeys.has(k)) {
888
+ extraKeys.push(k);
889
+ continue;
890
+ }
891
+ if (v === null || v === undefined) continue;
892
+ if (typeof v !== "string" && typeof v !== "number") {
893
+ warnings.push(`--from-deal: value for "${k}" was ${typeof v}, expected string; skipped`);
894
+ continue;
895
+ }
896
+ values[k] = String(v);
897
+ }
898
+ return { values, extraKeys, warnings };
899
+ }
900
+
811
901
  async function callLlm(cfg, prompt, fetcher) {
812
902
  if (cfg.provider === "anthropic") {
813
903
  const r = await fetcher("https://api.anthropic.com/v1/messages", {
@@ -1460,7 +1550,7 @@ export function loadBundle(path) {
1460
1550
  * @param {{ prompter?: (p: Placeholder) => Promise<string|null> }} [io]
1461
1551
  * @returns {Promise<ResolvedValues>}
1462
1552
  */
1463
- export async function resolveValues(placeholders, opts, paramsObj, { prompter = nodePrompter } = {}) {
1553
+ export async function resolveValues(placeholders, opts, paramsObj, { prompter = nodePrompter, inferred = null } = {}) {
1464
1554
  const resolved = {};
1465
1555
  const missing = [];
1466
1556
  const sources = {};
@@ -1475,6 +1565,12 @@ export async function resolveValues(placeholders, opts, paramsObj, { prompter =
1475
1565
  sources[p.key] = "params";
1476
1566
  continue;
1477
1567
  }
1568
+ // v2 #4: --from-deal LLM-inferred values, between --params and --interactive.
1569
+ if (inferred && Object.prototype.hasOwnProperty.call(inferred, p.key)) {
1570
+ resolved[p.key] = String(inferred[p.key]);
1571
+ sources[p.key] = "deal-llm";
1572
+ continue;
1573
+ }
1478
1574
  if (opts.interactive) {
1479
1575
  const v = await prompter(p);
1480
1576
  if (v !== null && v !== undefined && v !== "") {
@@ -2089,7 +2185,7 @@ export async function cmdListPlaceholders(opts, input, schema, envObj, { fetcher
2089
2185
  return EXIT.OK;
2090
2186
  }
2091
2187
 
2092
- export async function cmdValidate(opts, input, schema, paramsObj, envObj, { fetcher, out, err, parties = null } = {}) {
2188
+ export async function cmdValidate(opts, input, schema, paramsObj, envObj, { fetcher, out, err, parties = null, dealText = null } = {}) {
2093
2189
  const result = await runCascade(input, opts, schema, envObj, { fetcher });
2094
2190
  if (result.tier === "none") {
2095
2191
  err.write(paint("error: no placeholders detected by any tier\n", "red", err));
@@ -2109,7 +2205,24 @@ export async function cmdValidate(opts, input, schema, paramsObj, envObj, { fetc
2109
2205
  }
2110
2206
  return EXIT.VALIDATION;
2111
2207
  }
2112
- const { resolved, missing, sources } = await resolveValues(result.placeholders, opts, paramsObj);
2208
+ // v2 #4: --from-deal LLM inference (when dealText is present and
2209
+ // --no-llm not set). Provider config comes from env. Errors are fatal
2210
+ // to keep the user from running with partial inferred values.
2211
+ let inferred = null;
2212
+ if (dealText && !opts.noLlm) {
2213
+ try {
2214
+ const r = await inferFromDeal(dealText, result.placeholders, llmProviderFromEnv(envObj), { fetcher });
2215
+ inferred = r.values;
2216
+ for (const k of r.extraKeys) {
2217
+ err.write(paint(`warning: --from-deal LLM emitted unknown key "${k}" (not in template/schema)\n`, "yellow", err));
2218
+ }
2219
+ for (const w of r.warnings) err.write(paint(`warning: ${w}\n`, "yellow", err));
2220
+ } catch (e) {
2221
+ err.write(paint(`error: ${e.message}\n`, "red", err));
2222
+ return e.exitCode || EXIT.LLM;
2223
+ }
2224
+ }
2225
+ const { resolved, missing, sources } = await resolveValues(result.placeholders, opts, paramsObj, { inferred });
2113
2226
  if (missing.length > 0) {
2114
2227
  printMissing(missing, err);
2115
2228
  if (opts.json) {
@@ -2170,7 +2283,7 @@ export async function cmdValidate(opts, input, schema, paramsObj, envObj, { fetc
2170
2283
  return EXIT.OK;
2171
2284
  }
2172
2285
 
2173
- export async function cmdDraft(opts, input, schema, paramsObj, envObj, { fetcher, out, err, parties = null } = {}) {
2286
+ export async function cmdDraft(opts, input, schema, paramsObj, envObj, { fetcher, out, err, parties = null, dealText = null } = {}) {
2174
2287
  const result = await runCascade(input, opts, schema, envObj, { fetcher });
2175
2288
  if (result.tier === "none") {
2176
2289
  const hasProvider = Boolean(llmProviderFromEnv(envObj));
@@ -2221,7 +2334,25 @@ export async function cmdDraft(opts, input, schema, paramsObj, envObj, { fetcher
2221
2334
  return EXIT.VALIDATION;
2222
2335
  }
2223
2336
 
2224
- const { resolved, missing, sources } = await resolveValues(result.placeholders, opts, paramsObj);
2337
+ // v2 #4: --from-deal LLM inference (when dealText is present and
2338
+ // --no-llm not set). Provider config comes from env. Errors are fatal
2339
+ // to keep the user from running with partial inferred values.
2340
+ let inferred = null;
2341
+ if (dealText && !opts.noLlm) {
2342
+ try {
2343
+ const r = await inferFromDeal(dealText, result.placeholders, llmProviderFromEnv(envObj), { fetcher });
2344
+ inferred = r.values;
2345
+ for (const k of r.extraKeys) {
2346
+ err.write(paint(`warning: --from-deal LLM emitted unknown key "${k}" (not in template/schema)\n`, "yellow", err));
2347
+ }
2348
+ for (const w of r.warnings) err.write(paint(`warning: ${w}\n`, "yellow", err));
2349
+ } catch (e) {
2350
+ err.write(paint(`error: ${e.message}\n`, "red", err));
2351
+ return e.exitCode || EXIT.LLM;
2352
+ }
2353
+ }
2354
+
2355
+ const { resolved, missing, sources } = await resolveValues(result.placeholders, opts, paramsObj, { inferred });
2225
2356
  // Footgun guard: flag --typo'd-key VALUE that didn't match any detected
2226
2357
  // placeholder. Without this warning, a typo'd flag is silently dropped and
2227
2358
  // the user sees only a "missing required" error without the connection.
@@ -2839,13 +2970,22 @@ export async function main(argv, io = {}) {
2839
2970
  return EXIT.IO;
2840
2971
  }
2841
2972
 
2842
- let input, schema, paramsObj, envObj, parties;
2973
+ let input, schema, paramsObj, envObj, parties, dealText;
2843
2974
  try {
2844
2975
  input = await resolveInput(opts.positional[0], { spawner, stdinReader });
2845
2976
  schema = loadSchema(input.path);
2846
2977
  paramsObj = loadParamsFile(opts.params);
2847
2978
  envObj = effectiveEnv(cwd, processEnv);
2848
2979
  parties = loadParties(opts.parties || null);
2980
+ // v2 #4: --from-deal PATH reads a free-form deal description.
2981
+ if (opts.fromDeal) {
2982
+ if (!existsSync(opts.fromDeal)) {
2983
+ const e = new Error(`deal description file not found: ${opts.fromDeal}`);
2984
+ e.exitCode = EXIT.IO;
2985
+ throw e;
2986
+ }
2987
+ dealText = readFileSync(opts.fromDeal, "utf8");
2988
+ }
2849
2989
  } catch (e) {
2850
2990
  err.write(paint(`error: ${e.message}\n`, "red", err));
2851
2991
  return e.exitCode || EXIT.IO;
@@ -2856,9 +2996,9 @@ export async function main(argv, io = {}) {
2856
2996
  return await cmdListPlaceholders(opts, input, schema, envObj, { fetcher, out, err });
2857
2997
  }
2858
2998
  if (opts.validate) {
2859
- return await cmdValidate(opts, input, schema, paramsObj, envObj, { fetcher, out, err, parties });
2999
+ return await cmdValidate(opts, input, schema, paramsObj, envObj, { fetcher, out, err, parties, dealText });
2860
3000
  }
2861
- return await cmdDraft(opts, input, schema, paramsObj, envObj, { fetcher, out, err, parties });
3001
+ return await cmdDraft(opts, input, schema, paramsObj, envObj, { fetcher, out, err, parties, dealText });
2862
3002
  } catch (e) {
2863
3003
  err.write(paint(`error: ${e.message}\n`, "red", err));
2864
3004
  return e.exitCode || EXIT.IO;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drbaher/draft-cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Fill placeholders in a legal-document template with parameter values. Part of the contract-operations suite.",
5
5
  "type": "module",
6
6
  "bin": {