@drbaher/draft-cli 0.4.0 → 0.5.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 +73 -18
- package/PARAM_SCHEMA.md +57 -0
- package/draft-cli.mjs +136 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,58 @@ 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.5.0 — 2026-05-16
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Positional addressing** for same-text placeholders with different
|
|
12
|
+
semantic roles. Long-form schema entries can declare a `positions`
|
|
13
|
+
array; each position gets its own canonical key (via `role`), so the
|
|
14
|
+
CLI uses standard `--<role>` flags. Validated against the YC SAFE
|
|
15
|
+
`$[_____________] × 2` case (valuation cap vs. purchase amount).
|
|
16
|
+
```json
|
|
17
|
+
"blank": {
|
|
18
|
+
"aliases": ["_____________"],
|
|
19
|
+
"type": "money", "currency": "USD",
|
|
20
|
+
"positions": [
|
|
21
|
+
{ "role": "valuation_cap" },
|
|
22
|
+
{ "role": "purchase_amount" }
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
```sh
|
|
27
|
+
draft safe.docx \
|
|
28
|
+
--valuation-cap 5000000 \
|
|
29
|
+
--purchase-amount 100000
|
|
30
|
+
```
|
|
31
|
+
Decisions locked (V2_BRIEFS_REMAINING Q1.1–Q1.3):
|
|
32
|
+
- **Q1.1 Index base**: schema positions are 0-indexed internally;
|
|
33
|
+
the CLI uses role names, not numeric indices.
|
|
34
|
+
- **Q1.2 Length mismatch**: schema declares N positions but
|
|
35
|
+
detection finds M ≠ N occurrences → hard error (exit 4).
|
|
36
|
+
- **Q1.3 Bare-key CLI**: a `--<role>` flag targets its specific
|
|
37
|
+
position; values still flow through `--params` JSON or
|
|
38
|
+
`--interactive` normally.
|
|
39
|
+
|
|
40
|
+
### Constraints
|
|
41
|
+
|
|
42
|
+
- Positional addressing only works at tier T1 (bracket) and T2
|
|
43
|
+
(mustache) — those tiers carry per-hit byte indices needed for
|
|
44
|
+
position-specific substitution. T3 (docx-highlight), T4 (heuristic),
|
|
45
|
+
T5 (LLM) raise a positional error if a positional schema entry's
|
|
46
|
+
aliases are matched by them. `.docx` templates with `[X]` brackets
|
|
47
|
+
that fire T1 still work; `.docx` templates that rely on T3 highlights
|
|
48
|
+
for the same alias do not.
|
|
49
|
+
|
|
50
|
+
### Schema-contract change
|
|
51
|
+
|
|
52
|
+
`PARAM_SCHEMA.md` §5 gains a "Positional addressing" subsection. Long-
|
|
53
|
+
form entries can now include a `positions` array; short form is
|
|
54
|
+
unchanged. Forward-compatible with v0.4.x readers — they'll ignore the
|
|
55
|
+
unknown field and treat the entry as a regular non-positional
|
|
56
|
+
placeholder (which means the first detected occurrence wins for
|
|
57
|
+
substitution, and ambiguity is unresolved).
|
|
58
|
+
|
|
7
59
|
## 0.4.0 — 2026-05-16
|
|
8
60
|
|
|
9
61
|
### Added
|
|
@@ -263,22 +315,25 @@ suite ([cli.drbaher.com](https://cli.drbaher.com)).
|
|
|
263
315
|
that contains at least one letter. False positives are filtered with
|
|
264
316
|
the schema file; false negatives in this domain are higher-cost.
|
|
265
317
|
|
|
266
|
-
## Deferred (
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
318
|
+
## Deferred (post-v0.4.0 candidates)
|
|
319
|
+
|
|
320
|
+
Three of the original seven v1 "Deferred" entries shipped in v0.2.0,
|
|
321
|
+
v0.3.2, and v0.4.0 (see entries above). The four remaining items are
|
|
322
|
+
the next chunk of design work, with briefs in `V2_BRIEFS_REMAINING.md`:
|
|
323
|
+
|
|
324
|
+
- **Positional addressing.** Disambiguate same-text placeholders by
|
|
325
|
+
index in the schema. The validated case: YC SAFE has
|
|
326
|
+
`$[_____________]` twice — once for the valuation cap, once for
|
|
327
|
+
the purchase amount. Smallest of the four (~150 LOC).
|
|
328
|
+
- **Cross-template `parties.json` registry.** Declare parties once
|
|
329
|
+
with `ref:parties.<key>.<field>` references from schemas. Eliminates
|
|
330
|
+
duplicating party metadata across every template (~250 LOC).
|
|
331
|
+
- **Multi-document bundles.** Resolve placeholders once and emit
|
|
332
|
+
multiple documents in one call (MSA + Order Form + DPA with shared
|
|
333
|
+
parameter values) (~250 LOC).
|
|
334
|
+
- **LLM inference from a deal description.** `--from-deal <path>`
|
|
335
|
+
reads free-form deal text and asks the T5 LLM provider to fill the
|
|
336
|
+
schema's parameters. Inverse of the existing T5 detection (~250 LOC).
|
|
283
337
|
- **`.docx` highlight detection beyond yellow/green/cyan/magenta.** v1
|
|
284
|
-
ignores other colors (black/white/none) by design.
|
|
338
|
+
ignores other colors (black/white/none) by design. Backlog, not in
|
|
339
|
+
V2_BRIEFS_REMAINING (low priority).
|
package/PARAM_SCHEMA.md
CHANGED
|
@@ -331,6 +331,63 @@ that format).
|
|
|
331
331
|
Programmatic API for drivers: `parseDuration`, `addDuration`,
|
|
332
332
|
`computeValues`.
|
|
333
333
|
|
|
334
|
+
### Positional addressing (v0.5.0, opt-in)
|
|
335
|
+
|
|
336
|
+
Some templates have the same placeholder text appearing multiple times
|
|
337
|
+
with *different* semantic roles. The validated YC SAFE case:
|
|
338
|
+
`$[_____________]` appears twice — once as valuation cap, once as
|
|
339
|
+
purchase amount. Long-form entries can declare a `positions` array
|
|
340
|
+
that splits each occurrence into its own canonical-keyed placeholder.
|
|
341
|
+
|
|
342
|
+
```json
|
|
343
|
+
{
|
|
344
|
+
"_meta": { "schema_version": 1 },
|
|
345
|
+
"blank": {
|
|
346
|
+
"aliases": ["_____________"],
|
|
347
|
+
"type": "money", "currency": "USD",
|
|
348
|
+
"positions": [
|
|
349
|
+
{ "role": "valuation_cap" },
|
|
350
|
+
{ "role": "purchase_amount" }
|
|
351
|
+
]
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
CLI uses standard `--<role>` flags (no special `@N` grammar):
|
|
357
|
+
|
|
358
|
+
```sh
|
|
359
|
+
draft safe.docx --valuation-cap 5000000 --purchase-amount 100000
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**Q1.1 locked:** index base is 0 internally; the CLI uses role names,
|
|
363
|
+
not numeric indices.
|
|
364
|
+
|
|
365
|
+
**Q1.2 locked:** count mismatch (schema declares N positions but
|
|
366
|
+
detection finds M ≠ N occurrences of the alias) is a **hard error**
|
|
367
|
+
(exit 4). The schema and the template are out of sync; silently filling
|
|
368
|
+
or trimming hides the bug.
|
|
369
|
+
|
|
370
|
+
**Q1.3 locked:** there is no bare-key CLI variant (`--<parent-key>
|
|
371
|
+
VALUE` with no role). The CLI uses role-named flags. Bare `--<role>`
|
|
372
|
+
targets that role's position; values can also come from `--params`
|
|
373
|
+
JSON keyed by role, or `--interactive`.
|
|
374
|
+
|
|
375
|
+
**Tier constraint:** positional addressing only works at T1 (bracket)
|
|
376
|
+
or T2 (mustache). T3/T4/T5 detection paths don't carry per-hit byte
|
|
377
|
+
indices needed for position-specific substitution; if a positional
|
|
378
|
+
schema entry's aliases are matched by those tiers, the command exits 4
|
|
379
|
+
with a clear error.
|
|
380
|
+
|
|
381
|
+
**Validation:** at schema parse time, positions must be a non-empty
|
|
382
|
+
array of `{role: string}` objects; roles must be valid snake_case keys
|
|
383
|
+
and unique within the entry.
|
|
384
|
+
|
|
385
|
+
Programmatic API: positions flow through detection and resolution as
|
|
386
|
+
normal `Placeholder` objects with `position_parent` (parent schema key)
|
|
387
|
+
and `position_index` (0-based) fields. `substitute` switches to
|
|
388
|
+
byte-index substitution for these, which `substituteDocxXml` does not
|
|
389
|
+
currently support.
|
|
390
|
+
|
|
334
391
|
### Orphan handling (Q4 locked)
|
|
335
392
|
|
|
336
393
|
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.5.0";
|
|
74
74
|
|
|
75
75
|
// ─── EXIT CODES ─────────────────────────────────────────────────────────────
|
|
76
76
|
/**
|
|
@@ -914,6 +914,40 @@ export function parseSchema(parsed, sourceLabel = "<schema>") {
|
|
|
914
914
|
e.exitCode = EXIT.IO;
|
|
915
915
|
throw e;
|
|
916
916
|
}
|
|
917
|
+
// v2 #7: positional addressing. Optional `positions` array; each
|
|
918
|
+
// element declares a role (its own canonical key) for the Nth detected
|
|
919
|
+
// occurrence of this entry's aliases. Roles must be valid keys and
|
|
920
|
+
// unique within the entry.
|
|
921
|
+
let positions = null;
|
|
922
|
+
if (v.positions !== undefined) {
|
|
923
|
+
if (!Array.isArray(v.positions) || v.positions.length === 0) {
|
|
924
|
+
const e = new Error(`${sourceLabel}: long-form entry '${k}' positions must be a non-empty array`);
|
|
925
|
+
e.exitCode = EXIT.IO;
|
|
926
|
+
throw e;
|
|
927
|
+
}
|
|
928
|
+
const roleSet = new Set();
|
|
929
|
+
positions = [];
|
|
930
|
+
for (let pi = 0; pi < v.positions.length; pi++) {
|
|
931
|
+
const pos = v.positions[pi];
|
|
932
|
+
if (!pos || typeof pos !== "object" || Array.isArray(pos)) {
|
|
933
|
+
const e = new Error(`${sourceLabel}: '${k}'.positions[${pi}] must be an object with a 'role' string`);
|
|
934
|
+
e.exitCode = EXIT.IO;
|
|
935
|
+
throw e;
|
|
936
|
+
}
|
|
937
|
+
if (typeof pos.role !== "string" || !validKey(pos.role)) {
|
|
938
|
+
const e = new Error(`${sourceLabel}: '${k}'.positions[${pi}].role must be a valid snake_case key`);
|
|
939
|
+
e.exitCode = EXIT.IO;
|
|
940
|
+
throw e;
|
|
941
|
+
}
|
|
942
|
+
if (roleSet.has(pos.role)) {
|
|
943
|
+
const e = new Error(`${sourceLabel}: '${k}'.positions has duplicate role '${pos.role}'`);
|
|
944
|
+
e.exitCode = EXIT.IO;
|
|
945
|
+
throw e;
|
|
946
|
+
}
|
|
947
|
+
roleSet.add(pos.role);
|
|
948
|
+
positions.push({ role: pos.role });
|
|
949
|
+
}
|
|
950
|
+
}
|
|
917
951
|
// v2 #2: computed placeholders. Optional `computed` block on long-form
|
|
918
952
|
// entries; { from: <other-key>, op: "+"|"-", value: "<n> <unit>" }.
|
|
919
953
|
let computed = null;
|
|
@@ -951,6 +985,7 @@ export function parseSchema(parsed, sourceLabel = "<schema>") {
|
|
|
951
985
|
format: typeof v.format === "string" ? v.format : null,
|
|
952
986
|
currency: typeof v.currency === "string" ? v.currency : null,
|
|
953
987
|
computed,
|
|
988
|
+
positions,
|
|
954
989
|
};
|
|
955
990
|
} else {
|
|
956
991
|
if (!Array.isArray(v)) {
|
|
@@ -958,7 +993,7 @@ export function parseSchema(parsed, sourceLabel = "<schema>") {
|
|
|
958
993
|
e.exitCode = EXIT.IO;
|
|
959
994
|
throw e;
|
|
960
995
|
}
|
|
961
|
-
entries[k] = { aliases: v.slice(), required: true, default: null, type: null, format: null, currency: null, computed: null };
|
|
996
|
+
entries[k] = { aliases: v.slice(), required: true, default: null, type: null, format: null, currency: null, computed: null, positions: null };
|
|
962
997
|
}
|
|
963
998
|
}
|
|
964
999
|
// v2 #2: validate computed references (point to existing keys; no cycles).
|
|
@@ -1126,6 +1161,7 @@ function assemble(tier, hits, schema, warnings, fromLlm = false) {
|
|
|
1126
1161
|
format: resolved.format,
|
|
1127
1162
|
currency: resolved.currency,
|
|
1128
1163
|
computed: resolved.computed,
|
|
1164
|
+
positions: resolved.positions,
|
|
1129
1165
|
hits: [],
|
|
1130
1166
|
});
|
|
1131
1167
|
}
|
|
@@ -1133,7 +1169,52 @@ function assemble(tier, hits, schema, warnings, fromLlm = false) {
|
|
|
1133
1169
|
entry.occurrences += 1;
|
|
1134
1170
|
entry.hits.push(h);
|
|
1135
1171
|
}
|
|
1136
|
-
|
|
1172
|
+
// v2 #7: expand positional entries. Each detected occurrence becomes a
|
|
1173
|
+
// separate role-keyed placeholder. Count mismatch → positional_errors;
|
|
1174
|
+
// tier T3/T4/T5 (no per-hit index) → positional_errors (not supported).
|
|
1175
|
+
const placeholders = [];
|
|
1176
|
+
const positional_errors = [];
|
|
1177
|
+
const detected_schema_keys = [...byKey.keys()];
|
|
1178
|
+
for (const p of byKey.values()) {
|
|
1179
|
+
if (!p.positions) {
|
|
1180
|
+
placeholders.push(p);
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
if (tier !== "bracket" && tier !== "mustache") {
|
|
1184
|
+
positional_errors.push({
|
|
1185
|
+
key: p.key,
|
|
1186
|
+
reason: `tier '${tier}' does not carry per-hit index info; positional addressing requires T1 (bracket) or T2 (mustache) detection`,
|
|
1187
|
+
});
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
if (p.hits.length !== p.positions.length) {
|
|
1191
|
+
positional_errors.push({
|
|
1192
|
+
key: p.key,
|
|
1193
|
+
reason: `schema declares ${p.positions.length} position(s) but detected ${p.hits.length} occurrence(s) of "${p.aliases[0] || p.key}"`,
|
|
1194
|
+
});
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
for (let i = 0; i < p.positions.length; i++) {
|
|
1198
|
+
placeholders.push({
|
|
1199
|
+
key: p.positions[i].role,
|
|
1200
|
+
first_seen_as: p.hits[i].inner,
|
|
1201
|
+
occurrences: 1,
|
|
1202
|
+
tier,
|
|
1203
|
+
required: true,
|
|
1204
|
+
default: null,
|
|
1205
|
+
aliases: p.aliases.slice(),
|
|
1206
|
+
type: p.type,
|
|
1207
|
+
format: p.format,
|
|
1208
|
+
currency: p.currency,
|
|
1209
|
+
computed: null,
|
|
1210
|
+
positions: null, // expanded; no further re-expansion
|
|
1211
|
+
hits: [p.hits[i]],
|
|
1212
|
+
position_parent: p.key,
|
|
1213
|
+
position_index: i,
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
return { tier, placeholders, warnings, unmapped, positional_errors, detected_schema_keys };
|
|
1137
1218
|
}
|
|
1138
1219
|
|
|
1139
1220
|
function resolveKey(hit, schema, fromLlm) {
|
|
@@ -1149,6 +1230,7 @@ function resolveKey(hit, schema, fromLlm) {
|
|
|
1149
1230
|
format: entry.format || null,
|
|
1150
1231
|
currency: entry.currency || null,
|
|
1151
1232
|
computed: entry.computed || null,
|
|
1233
|
+
positions: entry.positions || null,
|
|
1152
1234
|
};
|
|
1153
1235
|
}
|
|
1154
1236
|
}
|
|
@@ -1156,7 +1238,7 @@ function resolveKey(hit, schema, fromLlm) {
|
|
|
1156
1238
|
}
|
|
1157
1239
|
const key = fromLlm && hit.suggested_key ? hit.suggested_key : canonicalKey(hit.inner);
|
|
1158
1240
|
if (!validKey(key)) return null;
|
|
1159
|
-
return { key, required: true, default: null, aliases: [hit.inner], type: null, format: null, currency: null, computed: null };
|
|
1241
|
+
return { key, required: true, default: null, aliases: [hit.inner], type: null, format: null, currency: null, computed: null, positions: null };
|
|
1160
1242
|
}
|
|
1161
1243
|
|
|
1162
1244
|
// ─── VALUE RESOLUTION (CLI > JSON > prompt > default) ───────────────────────
|
|
@@ -1592,9 +1674,17 @@ async function nodePrompter(placeholder) {
|
|
|
1592
1674
|
* @param {Placeholder[]} placeholders
|
|
1593
1675
|
* @returns {Array<{key: string, aliases: string[]}>}
|
|
1594
1676
|
*/
|
|
1595
|
-
export function findOrphans(schema, placeholders) {
|
|
1677
|
+
export function findOrphans(schema, placeholders, detectedSchemaKeys = null) {
|
|
1596
1678
|
if (!schema) return [];
|
|
1597
|
-
|
|
1679
|
+
// v2 #7: for positional entries we check `detectedSchemaKeys` (the
|
|
1680
|
+
// pre-expansion key set) since the placeholders list shows role keys,
|
|
1681
|
+
// not the parent positional key. When detectedSchemaKeys is not given
|
|
1682
|
+
// (older callers / no schema-expansion path), fall back to the
|
|
1683
|
+
// placeholders list — same behavior as before v0.5.0.
|
|
1684
|
+
const presentForPositional = detectedSchemaKeys
|
|
1685
|
+
? new Set(detectedSchemaKeys)
|
|
1686
|
+
: new Set(placeholders.map((p) => p.key));
|
|
1687
|
+
const presentForRegular = new Set(placeholders.map((p) => p.key));
|
|
1598
1688
|
// v2 #2: an entry that another entry's `computed.from` points at is
|
|
1599
1689
|
// legitimately not in the template — it's a "feeder" used only for
|
|
1600
1690
|
// computation. Exempt those from the orphan check.
|
|
@@ -1604,8 +1694,9 @@ export function findOrphans(schema, placeholders) {
|
|
|
1604
1694
|
}
|
|
1605
1695
|
const orphans = [];
|
|
1606
1696
|
for (const [key, entry] of Object.entries(schema.entries)) {
|
|
1607
|
-
if (present.has(key)) continue;
|
|
1608
1697
|
if (computedFromTargets.has(key)) continue;
|
|
1698
|
+
const present = entry.positions ? presentForPositional : presentForRegular;
|
|
1699
|
+
if (present.has(key)) continue;
|
|
1609
1700
|
orphans.push({ key, aliases: entry.aliases.slice() });
|
|
1610
1701
|
}
|
|
1611
1702
|
return orphans;
|
|
@@ -1625,8 +1716,30 @@ export function findOrphans(schema, placeholders) {
|
|
|
1625
1716
|
* @returns {string} the substituted body.
|
|
1626
1717
|
*/
|
|
1627
1718
|
export function substitute(body, placeholders, values, tier) {
|
|
1719
|
+
// v2 #7: positional placeholders (`position_index !== undefined`) substitute
|
|
1720
|
+
// at a specific byte index, not by global replace. Collect them first,
|
|
1721
|
+
// apply in reverse-index order so earlier hits' indices stay stable. Then
|
|
1722
|
+
// the remaining (non-positional) placeholders use the original
|
|
1723
|
+
// replaceAll/regex logic, which is safe because positional hits all share
|
|
1724
|
+
// the same alias text — and after the index-based substitution, only the
|
|
1725
|
+
// exact bytes at each position have been replaced.
|
|
1628
1726
|
let out = body;
|
|
1727
|
+
const positionalSubs = [];
|
|
1728
|
+
for (const p of placeholders) {
|
|
1729
|
+
if (p.position_index === undefined) continue;
|
|
1730
|
+
const v = values[p.key];
|
|
1731
|
+
if (v === undefined) continue;
|
|
1732
|
+
for (const h of p.hits) {
|
|
1733
|
+
if (typeof h.index !== "number") continue;
|
|
1734
|
+
positionalSubs.push({ index: h.index, length: h.match.length, value: v });
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
positionalSubs.sort((a, b) => b.index - a.index);
|
|
1738
|
+
for (const s of positionalSubs) {
|
|
1739
|
+
out = out.slice(0, s.index) + s.value + out.slice(s.index + s.length);
|
|
1740
|
+
}
|
|
1629
1741
|
for (const p of placeholders) {
|
|
1742
|
+
if (p.position_index !== undefined) continue; // already handled above
|
|
1630
1743
|
const v = values[p.key];
|
|
1631
1744
|
if (v === undefined) continue;
|
|
1632
1745
|
for (const h of p.hits) {
|
|
@@ -1805,7 +1918,14 @@ export async function cmdValidate(opts, input, schema, paramsObj, envObj, { fetc
|
|
|
1805
1918
|
err.write(paint("error: no placeholders detected by any tier\n", "red", err));
|
|
1806
1919
|
return EXIT.VALIDATION;
|
|
1807
1920
|
}
|
|
1808
|
-
|
|
1921
|
+
// v2 #7: positional addressing errors (count mismatch, unsupported tier).
|
|
1922
|
+
if (result.positional_errors && result.positional_errors.length > 0) {
|
|
1923
|
+
for (const pe of result.positional_errors) {
|
|
1924
|
+
err.write(paint(`error: positional placeholder "${pe.key}": ${pe.reason}\n`, "red", err));
|
|
1925
|
+
}
|
|
1926
|
+
return EXIT.VALIDATION;
|
|
1927
|
+
}
|
|
1928
|
+
const orphans = findOrphans(schema, result.placeholders, result.detected_schema_keys);
|
|
1809
1929
|
if (orphans.length > 0) {
|
|
1810
1930
|
for (const o of orphans) {
|
|
1811
1931
|
err.write(paint(`error: schema declares "${o.key}" with aliases [${o.aliases.map(a => `"${a}"`).join(",")}], but no matching phrase was detected by tier '${result.tier}'.\n`, "red", err));
|
|
@@ -1891,8 +2011,15 @@ export async function cmdDraft(opts, input, schema, paramsObj, envObj, { fetcher
|
|
|
1891
2011
|
}
|
|
1892
2012
|
}
|
|
1893
2013
|
|
|
2014
|
+
// v2 #7: positional addressing errors (count mismatch, unsupported tier).
|
|
2015
|
+
if (result.positional_errors && result.positional_errors.length > 0) {
|
|
2016
|
+
for (const pe of result.positional_errors) {
|
|
2017
|
+
err.write(paint(`error: positional placeholder "${pe.key}": ${pe.reason}\n`, "red", err));
|
|
2018
|
+
}
|
|
2019
|
+
return EXIT.VALIDATION;
|
|
2020
|
+
}
|
|
1894
2021
|
// Orphan check.
|
|
1895
|
-
const orphans = findOrphans(schema, result.placeholders);
|
|
2022
|
+
const orphans = findOrphans(schema, result.placeholders, result.detected_schema_keys);
|
|
1896
2023
|
if (orphans.length > 0) {
|
|
1897
2024
|
for (const o of orphans) {
|
|
1898
2025
|
err.write(paint(`error: schema declares "${o.key}" with aliases [${o.aliases.map(a => `"${a}"`).join(",")}], but no matching phrase was detected by tier '${result.tier}'.\n`, "red", err));
|