@drbaher/draft-cli 0.3.2 → 0.4.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 +44 -0
- package/PARAM_SCHEMA.md +61 -0
- package/draft-cli.mjs +220 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,50 @@ 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.4.0 — 2026-05-16
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Computed placeholders.** Long-form schema entries can declare a
|
|
12
|
+
`computed` block referencing another key:
|
|
13
|
+
```json
|
|
14
|
+
"term_end": {
|
|
15
|
+
"aliases": ["Term End"],
|
|
16
|
+
"type": "date",
|
|
17
|
+
"format": "MMMM d, yyyy",
|
|
18
|
+
"computed": { "from": "effective_date", "op": "+", "value": "2 years" }
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
At substitution time, if no value was supplied via CLI / `--params`
|
|
22
|
+
/ interactive / default, the computed entry's value is derived from
|
|
23
|
+
the `from` placeholder. Explicit CLI / `--params` values still win —
|
|
24
|
+
computed only fills the gap. Q2.1 locked: expression syntax lives
|
|
25
|
+
in the schema only, not in template text — keeps T1 detection
|
|
26
|
+
unchanged. Q2.2 locked: v0.4.0 supports date arithmetic only
|
|
27
|
+
(`+` / `-` with `<n> day|week|month|year[s]` durations). Money
|
|
28
|
+
math and string concat deferred to a future release.
|
|
29
|
+
- **Schema-time cycle detection.** `parseSchema` throws if any
|
|
30
|
+
`computed.from` chain revisits a key (e.g. `a → b → a`), or if
|
|
31
|
+
`computed.from` references a key that doesn't exist in the same
|
|
32
|
+
schema. Catches misconfiguration before substitution starts.
|
|
33
|
+
- **Orphan-check exemption.** Schema entries that are referenced only
|
|
34
|
+
as another entry's `computed.from` source (and never appear as
|
|
35
|
+
detected aliases in the template) are no longer reported as
|
|
36
|
+
orphans. They're "feeders" — declared so a computed entry can
|
|
37
|
+
reference them, even though the template doesn't show them.
|
|
38
|
+
- **New public API:** `parseDuration(raw)`, `addDuration(date, op, dur)`,
|
|
39
|
+
`computeValues(placeholders, resolved)`.
|
|
40
|
+
|
|
41
|
+
### Schema-contract change
|
|
42
|
+
|
|
43
|
+
`PARAM_SCHEMA.md` §5 gains a "Computed placeholders" section. Long-
|
|
44
|
+
form entries can now include a `computed: { from, op, value }` block;
|
|
45
|
+
short form is unchanged. v0.4.0 schemas are forward-compatible with
|
|
46
|
+
v0.3.x readers (which will silently ignore the `computed` field as
|
|
47
|
+
unrecognized long-form metadata, treating the entry as a regular
|
|
48
|
+
placeholder — but then the user has to supply a value, since v0.3.x
|
|
49
|
+
won't compute one).
|
|
50
|
+
|
|
7
51
|
## 0.3.2 — 2026-05-16
|
|
8
52
|
|
|
9
53
|
### Fixed
|
package/PARAM_SCHEMA.md
CHANGED
|
@@ -270,6 +270,67 @@ Programmatic API for drivers: `parseDateValue`, `formatDateValue`,
|
|
|
270
270
|
`parseMoneyValue`, `formatMoneyValue`, `normalizeTypedValue`,
|
|
271
271
|
`normalizeTypedValues`.
|
|
272
272
|
|
|
273
|
+
### Computed placeholders (v0.4.0, opt-in)
|
|
274
|
+
|
|
275
|
+
Long-form entries can declare a `computed` block referencing another
|
|
276
|
+
key in the same schema. At substitution time, if no value was
|
|
277
|
+
supplied via CLI / `--params` / interactive / default, the computed
|
|
278
|
+
entry's value is derived from the `from` placeholder via simple
|
|
279
|
+
date arithmetic. Explicit user-supplied values still win — computed
|
|
280
|
+
only fills the gap.
|
|
281
|
+
|
|
282
|
+
```json
|
|
283
|
+
{
|
|
284
|
+
"_meta": { "schema_version": 1 },
|
|
285
|
+
"effective_date": {
|
|
286
|
+
"aliases": ["Effective Date"],
|
|
287
|
+
"type": "date", "format": "MMMM d, yyyy"
|
|
288
|
+
},
|
|
289
|
+
"term_end": {
|
|
290
|
+
"aliases": ["Term End"],
|
|
291
|
+
"type": "date", "format": "MMMM d, yyyy",
|
|
292
|
+
"computed": { "from": "effective_date", "op": "+", "value": "2 years" }
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
| Field | Type | Required | Notes |
|
|
298
|
+
| ------- | -------- | -------- | ----- |
|
|
299
|
+
| `from` | string | yes | Key of another entry in the same schema. Schema validation rejects unknown references. |
|
|
300
|
+
| `op` | `"+"`/`"-"` | yes | Add or subtract the duration from the `from` value. |
|
|
301
|
+
| `value` | string | yes | Duration in `<n> <unit>` form, where `<unit>` is `day`, `week`, `month`, or `year` (singular or plural; case-insensitive). |
|
|
302
|
+
|
|
303
|
+
**Q2.1 locked:** Expression syntax lives in the schema, **not** in
|
|
304
|
+
template text. T1 bracket detection treats `[Term End]` as an
|
|
305
|
+
ordinary placeholder; the schema-level `computed` block decides how
|
|
306
|
+
its value is derived.
|
|
307
|
+
|
|
308
|
+
**Q2.2 locked:** v0.4.0 supports **date arithmetic only**. Money
|
|
309
|
+
math (`+ 10%`) and string concat (`Party A + " Inc."`) are deferred
|
|
310
|
+
to a future release once the date-arithmetic design is proven against
|
|
311
|
+
real templates.
|
|
312
|
+
|
|
313
|
+
**Resolution order:** value resolution → typed-parameter normalization
|
|
314
|
+
(§ above) → computed-placeholder evaluation → substitution.
|
|
315
|
+
|
|
316
|
+
**Cycle and reference safety:** `parseSchema` walks every
|
|
317
|
+
`computed.from` chain and throws at load time if a chain revisits a
|
|
318
|
+
key (e.g. `a → b → a`) or references a non-existent key. Caught
|
|
319
|
+
before substitution.
|
|
320
|
+
|
|
321
|
+
**Orphan-check exemption:** an entry that's referenced only as
|
|
322
|
+
another entry's `computed.from` (never appears in the template) is
|
|
323
|
+
**not** an orphan. It's a feeder used solely for computation.
|
|
324
|
+
|
|
325
|
+
**Format inheritance:** the computed entry's `format` field is used
|
|
326
|
+
to render the result. If the computed entry doesn't declare `format`,
|
|
327
|
+
the default `MMMM d, yyyy` applies. The `from` entry's `format` is
|
|
328
|
+
used for parsing the source value (since by then it's normalized to
|
|
329
|
+
that format).
|
|
330
|
+
|
|
331
|
+
Programmatic API for drivers: `parseDuration`, `addDuration`,
|
|
332
|
+
`computeValues`.
|
|
333
|
+
|
|
273
334
|
### Orphan handling (Q4 locked)
|
|
274
335
|
|
|
275
336
|
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.
|
|
73
|
+
export const VERSION = "0.4.0";
|
|
74
74
|
|
|
75
75
|
// ─── EXIT CODES ─────────────────────────────────────────────────────────────
|
|
76
76
|
/**
|
|
@@ -914,6 +914,32 @@ export function parseSchema(parsed, sourceLabel = "<schema>") {
|
|
|
914
914
|
e.exitCode = EXIT.IO;
|
|
915
915
|
throw e;
|
|
916
916
|
}
|
|
917
|
+
// v2 #2: computed placeholders. Optional `computed` block on long-form
|
|
918
|
+
// entries; { from: <other-key>, op: "+"|"-", value: "<n> <unit>" }.
|
|
919
|
+
let computed = null;
|
|
920
|
+
if (v.computed !== undefined) {
|
|
921
|
+
if (!v.computed || typeof v.computed !== "object" || Array.isArray(v.computed)) {
|
|
922
|
+
const e = new Error(`${sourceLabel}: long-form entry '${k}' has invalid 'computed' (must be an object)`);
|
|
923
|
+
e.exitCode = EXIT.IO;
|
|
924
|
+
throw e;
|
|
925
|
+
}
|
|
926
|
+
if (typeof v.computed.from !== "string") {
|
|
927
|
+
const e = new Error(`${sourceLabel}: long-form entry '${k}' computed.from must be a string (key of another schema entry)`);
|
|
928
|
+
e.exitCode = EXIT.IO;
|
|
929
|
+
throw e;
|
|
930
|
+
}
|
|
931
|
+
if (v.computed.op !== "+" && v.computed.op !== "-") {
|
|
932
|
+
const e = new Error(`${sourceLabel}: long-form entry '${k}' computed.op must be "+" or "-"`);
|
|
933
|
+
e.exitCode = EXIT.IO;
|
|
934
|
+
throw e;
|
|
935
|
+
}
|
|
936
|
+
if (typeof v.computed.value !== "string") {
|
|
937
|
+
const e = new Error(`${sourceLabel}: long-form entry '${k}' computed.value must be a string (duration like "2 years")`);
|
|
938
|
+
e.exitCode = EXIT.IO;
|
|
939
|
+
throw e;
|
|
940
|
+
}
|
|
941
|
+
computed = { from: v.computed.from, op: v.computed.op, value: v.computed.value };
|
|
942
|
+
}
|
|
917
943
|
entries[k] = {
|
|
918
944
|
aliases: v.aliases.slice(),
|
|
919
945
|
required: v.required !== false,
|
|
@@ -924,6 +950,7 @@ export function parseSchema(parsed, sourceLabel = "<schema>") {
|
|
|
924
950
|
type: typeof v.type === "string" ? v.type : null,
|
|
925
951
|
format: typeof v.format === "string" ? v.format : null,
|
|
926
952
|
currency: typeof v.currency === "string" ? v.currency : null,
|
|
953
|
+
computed,
|
|
927
954
|
};
|
|
928
955
|
} else {
|
|
929
956
|
if (!Array.isArray(v)) {
|
|
@@ -931,7 +958,30 @@ export function parseSchema(parsed, sourceLabel = "<schema>") {
|
|
|
931
958
|
e.exitCode = EXIT.IO;
|
|
932
959
|
throw e;
|
|
933
960
|
}
|
|
934
|
-
entries[k] = { aliases: v.slice(), required: true, default: null, type: null, format: null, currency: null };
|
|
961
|
+
entries[k] = { aliases: v.slice(), required: true, default: null, type: null, format: null, currency: null, computed: null };
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
// v2 #2: validate computed references (point to existing keys; no cycles).
|
|
965
|
+
for (const [key, entry] of Object.entries(entries)) {
|
|
966
|
+
if (!entry.computed) continue;
|
|
967
|
+
if (!entries[entry.computed.from]) {
|
|
968
|
+
const e = new Error(`${sourceLabel}: '${key}'.computed.from = "${entry.computed.from}" does not match any other key in this schema`);
|
|
969
|
+
e.exitCode = EXIT.IO;
|
|
970
|
+
throw e;
|
|
971
|
+
}
|
|
972
|
+
// Walk the computed.from chain from this key; bail if we revisit.
|
|
973
|
+
const visited = [key];
|
|
974
|
+
let cursor = entry.computed.from;
|
|
975
|
+
while (cursor) {
|
|
976
|
+
if (visited.includes(cursor)) {
|
|
977
|
+
const e = new Error(`${sourceLabel}: computed cycle detected: ${[...visited, cursor].join(" → ")}`);
|
|
978
|
+
e.exitCode = EXIT.IO;
|
|
979
|
+
throw e;
|
|
980
|
+
}
|
|
981
|
+
visited.push(cursor);
|
|
982
|
+
const next = entries[cursor];
|
|
983
|
+
if (!next || !next.computed) break;
|
|
984
|
+
cursor = next.computed.from;
|
|
935
985
|
}
|
|
936
986
|
}
|
|
937
987
|
return { form: long ? "long" : "short", entries };
|
|
@@ -1075,6 +1125,7 @@ function assemble(tier, hits, schema, warnings, fromLlm = false) {
|
|
|
1075
1125
|
type: resolved.type,
|
|
1076
1126
|
format: resolved.format,
|
|
1077
1127
|
currency: resolved.currency,
|
|
1128
|
+
computed: resolved.computed,
|
|
1078
1129
|
hits: [],
|
|
1079
1130
|
});
|
|
1080
1131
|
}
|
|
@@ -1097,6 +1148,7 @@ function resolveKey(hit, schema, fromLlm) {
|
|
|
1097
1148
|
type: entry.type || null,
|
|
1098
1149
|
format: entry.format || null,
|
|
1099
1150
|
currency: entry.currency || null,
|
|
1151
|
+
computed: entry.computed || null,
|
|
1100
1152
|
};
|
|
1101
1153
|
}
|
|
1102
1154
|
}
|
|
@@ -1104,7 +1156,7 @@ function resolveKey(hit, schema, fromLlm) {
|
|
|
1104
1156
|
}
|
|
1105
1157
|
const key = fromLlm && hit.suggested_key ? hit.suggested_key : canonicalKey(hit.inner);
|
|
1106
1158
|
if (!validKey(key)) return null;
|
|
1107
|
-
return { key, required: true, default: null, aliases: [hit.inner], type: null, format: null, currency: null };
|
|
1159
|
+
return { key, required: true, default: null, aliases: [hit.inner], type: null, format: null, currency: null, computed: null };
|
|
1108
1160
|
}
|
|
1109
1161
|
|
|
1110
1162
|
// ─── VALUE RESOLUTION (CLI > JSON > prompt > default) ───────────────────────
|
|
@@ -1177,7 +1229,9 @@ export async function resolveValues(placeholders, opts, paramsObj, { prompter =
|
|
|
1177
1229
|
sources[p.key] = "default";
|
|
1178
1230
|
continue;
|
|
1179
1231
|
}
|
|
1180
|
-
|
|
1232
|
+
// v2 #2: computed placeholders auto-resolve later via `computeValues`.
|
|
1233
|
+
// Don't count them as missing here even though no source supplied a value.
|
|
1234
|
+
if (p.required && !p.computed) missing.push(p);
|
|
1181
1235
|
}
|
|
1182
1236
|
return { resolved, missing, sources };
|
|
1183
1237
|
}
|
|
@@ -1255,6 +1309,46 @@ export function formatDateValue(date, format) {
|
|
|
1255
1309
|
});
|
|
1256
1310
|
}
|
|
1257
1311
|
|
|
1312
|
+
/**
|
|
1313
|
+
* Parse a duration string for computed placeholders (v2 #2).
|
|
1314
|
+
* Accepts `<n> <unit>` where unit is one of `day | week | month | year`
|
|
1315
|
+
* (singular or plural). Returns an object with the unit as plural key.
|
|
1316
|
+
* Returns `null` on parse failure.
|
|
1317
|
+
*
|
|
1318
|
+
* @param {string} raw
|
|
1319
|
+
* @returns {{ days?: number, weeks?: number, months?: number, years?: number } | null}
|
|
1320
|
+
*/
|
|
1321
|
+
export function parseDuration(raw) {
|
|
1322
|
+
const m = /^(\d+)\s+(day|week|month|year)s?$/i.exec(String(raw).trim());
|
|
1323
|
+
if (!m) return null;
|
|
1324
|
+
const n = parseInt(m[1], 10);
|
|
1325
|
+
if (!isFinite(n) || n < 0) return null;
|
|
1326
|
+
const unit = m[2].toLowerCase();
|
|
1327
|
+
return { [`${unit}s`]: n };
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/**
|
|
1331
|
+
* Add or subtract a duration from a Date. Uses UTC field manipulation
|
|
1332
|
+
* via `setUTC*` methods, so day/month/year overflow follows JavaScript's
|
|
1333
|
+
* default behavior (e.g. Jan 31 + 1 month = Mar 3, not Feb 28). For
|
|
1334
|
+
* legal-doc use cases ("2 years from effective date") this is the
|
|
1335
|
+
* expected behavior; anniversary dates are unambiguous.
|
|
1336
|
+
*
|
|
1337
|
+
* @param {Date} date
|
|
1338
|
+
* @param {"+"|"-"} op
|
|
1339
|
+
* @param {{ days?: number, weeks?: number, months?: number, years?: number }} dur
|
|
1340
|
+
* @returns {Date}
|
|
1341
|
+
*/
|
|
1342
|
+
export function addDuration(date, op, dur) {
|
|
1343
|
+
const sign = op === "-" ? -1 : 1;
|
|
1344
|
+
const d = new Date(date.getTime());
|
|
1345
|
+
if (dur.years) d.setUTCFullYear(d.getUTCFullYear() + sign * dur.years);
|
|
1346
|
+
if (dur.months) d.setUTCMonth(d.getUTCMonth() + sign * dur.months);
|
|
1347
|
+
if (dur.weeks) d.setUTCDate(d.getUTCDate() + sign * dur.weeks * 7);
|
|
1348
|
+
if (dur.days) d.setUTCDate(d.getUTCDate() + sign * dur.days);
|
|
1349
|
+
return d;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1258
1352
|
/**
|
|
1259
1353
|
* Parse a money input. Accepts `$5,000`, `5000.50`, `$5M`, `2.5K`, etc.
|
|
1260
1354
|
* Handles `K`/`M`/`B` suffixes (case-insensitive). Returns the value in
|
|
@@ -1394,6 +1488,91 @@ export function normalizeTypedValues(placeholders, resolved) {
|
|
|
1394
1488
|
return { ok: errors.length === 0, errors, normalized };
|
|
1395
1489
|
}
|
|
1396
1490
|
|
|
1491
|
+
// ─── COMPUTED PLACEHOLDERS (v2 #2) ──────────────────────────────────────────
|
|
1492
|
+
// Schema entries can declare a `computed` block referencing another key in
|
|
1493
|
+
// the same schema:
|
|
1494
|
+
//
|
|
1495
|
+
// "term_end": { "aliases": ["Term End"], "type": "date",
|
|
1496
|
+
// "computed": { "from": "effective_date", "op": "+", "value": "2 years" } }
|
|
1497
|
+
//
|
|
1498
|
+
// At substitution time, if no value was supplied via CLI/--params/interactive/
|
|
1499
|
+
// default, the computed entry's value is derived from its `from` placeholder.
|
|
1500
|
+
// CLI/--params explicit values still win — computed only fills the gap.
|
|
1501
|
+
//
|
|
1502
|
+
// Cycles in `from` references are detected at parseSchema time. Missing-`from`
|
|
1503
|
+
// errors and bad-duration errors surface at compute time with a per-key
|
|
1504
|
+
// message; like typed-param errors, all errors are collected before returning
|
|
1505
|
+
// so the user sees every failure at once.
|
|
1506
|
+
|
|
1507
|
+
/**
|
|
1508
|
+
* Run computed-placeholder evaluation on already-resolved values. Mutates
|
|
1509
|
+
* `resolved` in place with computed values for any placeholder that has a
|
|
1510
|
+
* `computed` block and no existing value. Iterative — handles chains
|
|
1511
|
+
* (B from A, C from B) without an explicit topological sort.
|
|
1512
|
+
*
|
|
1513
|
+
* @param {Placeholder[]} placeholders
|
|
1514
|
+
* @param {Object<string,string>} resolved
|
|
1515
|
+
* @returns {{ ok: boolean, errors: Array<{ key: string, message: string }>, computed: Object<string,{from: string, op: string, value: string, to: string}> }}
|
|
1516
|
+
*/
|
|
1517
|
+
export function computeValues(placeholders, resolved) {
|
|
1518
|
+
const errors = [];
|
|
1519
|
+
const computed = {};
|
|
1520
|
+
const pending = placeholders.filter(
|
|
1521
|
+
(p) => p.computed && resolved[p.key] === undefined
|
|
1522
|
+
);
|
|
1523
|
+
if (pending.length === 0) return { ok: true, errors: [], computed: {} };
|
|
1524
|
+
|
|
1525
|
+
let progress = true;
|
|
1526
|
+
while (progress && pending.length > 0) {
|
|
1527
|
+
progress = false;
|
|
1528
|
+
for (let i = pending.length - 1; i >= 0; i--) {
|
|
1529
|
+
const p = pending[i];
|
|
1530
|
+
const fromKey = p.computed.from;
|
|
1531
|
+
const fromValue = resolved[fromKey];
|
|
1532
|
+
if (fromValue === undefined) continue;
|
|
1533
|
+
try {
|
|
1534
|
+
const result = computeOneValue(p, fromValue);
|
|
1535
|
+
resolved[p.key] = result;
|
|
1536
|
+
computed[p.key] = { from: fromKey, op: p.computed.op, value: p.computed.value, to: result };
|
|
1537
|
+
pending.splice(i, 1);
|
|
1538
|
+
progress = true;
|
|
1539
|
+
} catch (e) {
|
|
1540
|
+
errors.push({ key: p.key, message: e.message });
|
|
1541
|
+
pending.splice(i, 1);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
for (const p of pending) {
|
|
1546
|
+
errors.push({
|
|
1547
|
+
key: p.key,
|
|
1548
|
+
message: `cannot compute: depends on "${p.computed.from}" which is unresolved`,
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
return { ok: errors.length === 0, errors, computed };
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
function computeOneValue(p, fromValue) {
|
|
1555
|
+
// v2: dates only. Future expansions (money math, string concat) would
|
|
1556
|
+
// dispatch on placeholder type here.
|
|
1557
|
+
const date = parseDateValue(fromValue);
|
|
1558
|
+
if (!date) {
|
|
1559
|
+
throw new Error(
|
|
1560
|
+
`cannot parse "${fromValue}" as a date (from "${p.computed.from}"). ` +
|
|
1561
|
+
`Computed placeholders need a date-shaped source value.`
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
const dur = parseDuration(p.computed.value);
|
|
1565
|
+
if (!dur) {
|
|
1566
|
+
throw new Error(
|
|
1567
|
+
`cannot parse duration "${p.computed.value}". Expected ` +
|
|
1568
|
+
`"<n> <unit>" where unit is day, week, month, or year (singular ` +
|
|
1569
|
+
`or plural).`
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
const result = addDuration(date, p.computed.op, dur);
|
|
1573
|
+
return formatDateValue(result, p.format || "MMMM d, yyyy");
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1397
1576
|
async function nodePrompter(placeholder) {
|
|
1398
1577
|
if (!process.stdin.isTTY) return null;
|
|
1399
1578
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
@@ -1416,9 +1595,18 @@ async function nodePrompter(placeholder) {
|
|
|
1416
1595
|
export function findOrphans(schema, placeholders) {
|
|
1417
1596
|
if (!schema) return [];
|
|
1418
1597
|
const present = new Set(placeholders.map((p) => p.key));
|
|
1598
|
+
// v2 #2: an entry that another entry's `computed.from` points at is
|
|
1599
|
+
// legitimately not in the template — it's a "feeder" used only for
|
|
1600
|
+
// computation. Exempt those from the orphan check.
|
|
1601
|
+
const computedFromTargets = new Set();
|
|
1602
|
+
for (const entry of Object.values(schema.entries)) {
|
|
1603
|
+
if (entry.computed && entry.computed.from) computedFromTargets.add(entry.computed.from);
|
|
1604
|
+
}
|
|
1419
1605
|
const orphans = [];
|
|
1420
1606
|
for (const [key, entry] of Object.entries(schema.entries)) {
|
|
1421
|
-
if (
|
|
1607
|
+
if (present.has(key)) continue;
|
|
1608
|
+
if (computedFromTargets.has(key)) continue;
|
|
1609
|
+
orphans.push({ key, aliases: entry.aliases.slice() });
|
|
1422
1610
|
}
|
|
1423
1611
|
return orphans;
|
|
1424
1612
|
}
|
|
@@ -1647,6 +1835,20 @@ export async function cmdValidate(opts, input, schema, paramsObj, envObj, { fetc
|
|
|
1647
1835
|
}
|
|
1648
1836
|
return EXIT.VALIDATION;
|
|
1649
1837
|
}
|
|
1838
|
+
// v2 #2: computed-placeholder validation (same gate as cmdDraft).
|
|
1839
|
+
const computeCheck = computeValues(result.placeholders, resolved);
|
|
1840
|
+
if (!computeCheck.ok) {
|
|
1841
|
+
for (const ce of computeCheck.errors) {
|
|
1842
|
+
err.write(paint(`error: computed value failed for "${ce.key}": ${ce.message}\n`, "red", err));
|
|
1843
|
+
}
|
|
1844
|
+
if (opts.json) {
|
|
1845
|
+
out.write(JSON.stringify({
|
|
1846
|
+
ok: false,
|
|
1847
|
+
computed_errors: computeCheck.errors.map(({ key, message }) => ({ key, message })),
|
|
1848
|
+
}, null, 2) + "\n");
|
|
1849
|
+
}
|
|
1850
|
+
return EXIT.VALIDATION;
|
|
1851
|
+
}
|
|
1650
1852
|
if (opts.json) {
|
|
1651
1853
|
out.write(JSON.stringify({ ok: true, resolved: Object.keys(resolved), sources }, null, 2) + "\n");
|
|
1652
1854
|
} else {
|
|
@@ -1727,6 +1929,19 @@ export async function cmdDraft(opts, input, schema, paramsObj, envObj, { fetcher
|
|
|
1727
1929
|
return EXIT.VALIDATION;
|
|
1728
1930
|
}
|
|
1729
1931
|
|
|
1932
|
+
// v2 #2: computed placeholders. Fill any computed entries whose value
|
|
1933
|
+
// wasn't already supplied via CLI / --params / --interactive / default.
|
|
1934
|
+
// Runs after typed normalization so the source values are in canonical
|
|
1935
|
+
// form (e.g. a "date" type is already in the format string before we
|
|
1936
|
+
// parse it back for arithmetic).
|
|
1937
|
+
const computeCheck = computeValues(result.placeholders, resolved);
|
|
1938
|
+
if (!computeCheck.ok) {
|
|
1939
|
+
for (const ce of computeCheck.errors) {
|
|
1940
|
+
err.write(paint(`error: computed value failed for "${ce.key}": ${ce.message}\n`, "red", err));
|
|
1941
|
+
}
|
|
1942
|
+
return EXIT.VALIDATION;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1730
1945
|
// Diff mode: print a substitution table and exit without writing output.
|
|
1731
1946
|
if (opts.diff) {
|
|
1732
1947
|
if (opts.json) {
|