@@ -70,7 +70,7 @@ import { fileURLToPath } from "node:url";
7070 */
7171
7272/** @type {string } */
73- export const VERSION = "0.7 .0" ;
73+ export const VERSION = "0.8 .0" ;
7474
7575// ─── EXIT CODES ─────────────────────────────────────────────────────────────
7676/**
@@ -279,6 +279,7 @@ export function parseArgs(argv) {
279279 if ( a === "--params" ) { opts . params = argv [ ++ i ] ; continue ; }
280280 if ( a === "--parties" ) { opts . parties = argv [ ++ i ] ; continue ; }
281281 if ( a === "--bundle" ) { opts . bundle = argv [ ++ i ] ; continue ; }
282+ if ( a === "--from-deal" ) { opts . fromDeal = argv [ ++ i ] ; continue ; }
282283 if ( a === "--output" || a === "-o" ) { opts . output = argv [ ++ i ] ; continue ; }
283284 if ( a === "--syntax" ) {
284285 const v = argv [ ++ i ] ;
@@ -808,6 +809,95 @@ ${body.slice(0, 12000)}`;
808809 return out ;
809810}
810811
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+
811901async function callLlm ( cfg , prompt , fetcher ) {
812902 if ( cfg . provider === "anthropic" ) {
813903 const r = await fetcher ( "https://api.anthropic.com/v1/messages" , {
@@ -1460,7 +1550,7 @@ export function loadBundle(path) {
14601550 * @param {{ prompter?: (p: Placeholder) => Promise<string|null> } } [io]
14611551 * @returns {Promise<ResolvedValues> }
14621552 */
1463- export async function resolveValues ( placeholders , opts , paramsObj , { prompter = nodePrompter } = { } ) {
1553+ export async function resolveValues ( placeholders , opts , paramsObj , { prompter = nodePrompter , inferred = null } = { } ) {
14641554 const resolved = { } ;
14651555 const missing = [ ] ;
14661556 const sources = { } ;
@@ -1475,6 +1565,12 @@ export async function resolveValues(placeholders, opts, paramsObj, { prompter =
14751565 sources [ p . key ] = "params" ;
14761566 continue ;
14771567 }
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+ }
14781574 if ( opts . interactive ) {
14791575 const v = await prompter ( p ) ;
14801576 if ( v !== null && v !== undefined && v !== "" ) {
@@ -2089,7 +2185,7 @@ export async function cmdListPlaceholders(opts, input, schema, envObj, { fetcher
20892185 return EXIT . OK ;
20902186}
20912187
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 } = { } ) {
20932189 const result = await runCascade ( input , opts , schema , envObj , { fetcher } ) ;
20942190 if ( result . tier === "none" ) {
20952191 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
21092205 }
21102206 return EXIT . VALIDATION ;
21112207 }
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 } ) ;
21132226 if ( missing . length > 0 ) {
21142227 printMissing ( missing , err ) ;
21152228 if ( opts . json ) {
@@ -2170,7 +2283,7 @@ export async function cmdValidate(opts, input, schema, paramsObj, envObj, { fetc
21702283 return EXIT . OK ;
21712284}
21722285
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 } = { } ) {
21742287 const result = await runCascade ( input , opts , schema , envObj , { fetcher } ) ;
21752288 if ( result . tier === "none" ) {
21762289 const hasProvider = Boolean ( llmProviderFromEnv ( envObj ) ) ;
@@ -2221,7 +2334,25 @@ export async function cmdDraft(opts, input, schema, paramsObj, envObj, { fetcher
22212334 return EXIT . VALIDATION ;
22222335 }
22232336
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 } ) ;
22252356 // Footgun guard: flag --typo'd-key VALUE that didn't match any detected
22262357 // placeholder. Without this warning, a typo'd flag is silently dropped and
22272358 // the user sees only a "missing required" error without the connection.
@@ -2839,13 +2970,22 @@ export async function main(argv, io = {}) {
28392970 return EXIT . IO ;
28402971 }
28412972
2842- let input , schema , paramsObj , envObj , parties ;
2973+ let input , schema , paramsObj , envObj , parties , dealText ;
28432974 try {
28442975 input = await resolveInput ( opts . positional [ 0 ] , { spawner, stdinReader } ) ;
28452976 schema = loadSchema ( input . path ) ;
28462977 paramsObj = loadParamsFile ( opts . params ) ;
28472978 envObj = effectiveEnv ( cwd , processEnv ) ;
28482979 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+ }
28492989 } catch ( e ) {
28502990 err . write ( paint ( `error: ${ e . message } \n` , "red" , err ) ) ;
28512991 return e . exitCode || EXIT . IO ;
@@ -2856,9 +2996,9 @@ export async function main(argv, io = {}) {
28562996 return await cmdListPlaceholders ( opts , input , schema , envObj , { fetcher, out, err } ) ;
28572997 }
28582998 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 } ) ;
28603000 }
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 } ) ;
28623002 } catch ( e ) {
28633003 err . write ( paint ( `error: ${ e . message } \n` , "red" , err ) ) ;
28643004 return e . exitCode || EXIT . IO ;
0 commit comments