@drbaher/draft-cli 0.2.0 → 0.3.2
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 +66 -0
- package/PARAM_SCHEMA.md +33 -0
- package/draft-cli.mjs +259 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,72 @@ 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.3.2 — 2026-05-16
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Publish auth: restored `NODE_AUTH_TOKEN` env block.** v0.3.1's
|
|
12
|
+
hotfix bumped `publish.yml` Node from 20 to 22 on the hypothesis
|
|
13
|
+
that npm CLI 11.5.1+ would auto-detect OIDC and ignore the
|
|
14
|
+
setup-node placeholder. It didn't: Node 22's npm 11.x still sent
|
|
15
|
+
the literal `XXXXX-XXXXX-XXXXX-XXXXX` placeholder and got the
|
|
16
|
+
same 404 from npm. Root cause is *not* Node version; either
|
|
17
|
+
`setup-node@v6` always writes the placeholder env into the
|
|
18
|
+
publish step, or npm CLI prefers the `.npmrc` token over OIDC
|
|
19
|
+
even when both are available. Under investigation.
|
|
20
|
+
- **v0.3.1 tag exists on GitHub but did NOT publish to npm.** Skip
|
|
21
|
+
it. Registry latest is `0.2.0` until v0.3.2 ships.
|
|
22
|
+
|
|
23
|
+
Pragmatic call: v0.3.2 ships via the bootstrap `NPM_TOKEN` path.
|
|
24
|
+
Trusted Publisher stays configured on npm so the switch back to
|
|
25
|
+
pure OIDC is a one-line change in `publish.yml` once we understand
|
|
26
|
+
why npm CLI isn't using OIDC. `feedback_oidc_setup_node_v6_placeholder.md`
|
|
27
|
+
in memory tracks the symptom + workarounds tried.
|
|
28
|
+
|
|
29
|
+
## 0.3.1 — 2026-05-16
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **`publish.yml` now uses Node 22 instead of Node 20.** npm
|
|
34
|
+
Trusted Publishing requires npm CLI 11.5.1 or later, which ships
|
|
35
|
+
with Node 22.14+. Node 20 (npm 10.x) silently falls back to
|
|
36
|
+
`NODE_AUTH_TOKEN` when configured for a registry, and to
|
|
37
|
+
`setup-node@v6`'s placeholder value (`XXXXX-...`) when the env
|
|
38
|
+
isn't set — producing a 404 from npm masking the actual 401.
|
|
39
|
+
- **v0.3.0 tag exists on GitHub but did NOT publish to npm.** v0.3.0
|
|
40
|
+
was the first publish attempt without an `NPM_TOKEN` fallback
|
|
41
|
+
(PR #10 reverted the bootstrap); the npm-CLI-too-old issue
|
|
42
|
+
surfaced immediately. No package was uploaded. v0.3.1 is the
|
|
43
|
+
rebrand of v0.3.0's typed-parameter feature with the workflow
|
|
44
|
+
fix applied. Skip v0.3.0.
|
|
45
|
+
|
|
46
|
+
## 0.3.0 — 2026-05-16
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- **Typed parameters (`type: date | money | party`).** Long-form
|
|
51
|
+
schema entries can declare a `type`, with optional `format` (date)
|
|
52
|
+
or `currency` (money). Inputs are validated and normalized between
|
|
53
|
+
value resolution and substitution; bad inputs hard-error with a
|
|
54
|
+
per-key message (exit 4). See `PARAM_SCHEMA.md` §5 for the accepted
|
|
55
|
+
shapes per type, the rejected ambiguous forms (Q3.1: US
|
|
56
|
+
`MM/DD/YYYY` and European `DD/MM/YYYY` are rejected as ambiguous),
|
|
57
|
+
and the v2 currency scope (Q3.2: USD only).
|
|
58
|
+
- **`--validate` now catches type errors** before draft runs. With
|
|
59
|
+
`--json`, errors are emitted as a `type_errors` array on the
|
|
60
|
+
result payload.
|
|
61
|
+
- **New public API:** `parseDateValue(raw)`, `formatDateValue(date, fmt)`,
|
|
62
|
+
`parseMoneyValue(raw)` → minor units, `formatMoneyValue(minor, currency)`,
|
|
63
|
+
`normalizeTypedValue(raw, placeholder)`, `normalizeTypedValues(placeholders, resolved)`.
|
|
64
|
+
|
|
65
|
+
### Schema-contract change
|
|
66
|
+
|
|
67
|
+
`PARAM_SCHEMA.md` §5 gains a "Typed parameters" section. Long-form
|
|
68
|
+
entries can now include `type`, `format`, and `currency` fields; short
|
|
69
|
+
form is unchanged. v0.3.0 schemas are forward-compatible with v0.2.x
|
|
70
|
+
readers (which will silently ignore the new fields, since they're
|
|
71
|
+
opt-in metadata on the long-form entry).
|
|
72
|
+
|
|
7
73
|
## 0.2.0 — 2026-05-16
|
|
8
74
|
|
|
9
75
|
### Added
|
package/PARAM_SCHEMA.md
CHANGED
|
@@ -237,6 +237,39 @@ its own alias list (Q3 locked) — list it explicitly if needed.
|
|
|
237
237
|
Parser selects long form iff a top-level `_meta` key is present. Short
|
|
238
238
|
and long are not mixable within one file.
|
|
239
239
|
|
|
240
|
+
### Typed parameters (v0.3.0, opt-in)
|
|
241
|
+
|
|
242
|
+
Long-form entries can declare `type`, with optional `format` (`date`)
|
|
243
|
+
or `currency` (`money`). Inputs are validated and normalized between
|
|
244
|
+
value resolution and substitution. Bad input → exit 4
|
|
245
|
+
(`EXIT.VALIDATION`) with a per-key error message; all type errors are
|
|
246
|
+
collected before exit so the user sees every issue at once.
|
|
247
|
+
|
|
248
|
+
```json
|
|
249
|
+
{
|
|
250
|
+
"_meta": { "schema_version": 1 },
|
|
251
|
+
"effective_date": { "aliases": ["Effective Date"], "type": "date", "format": "MMMM d, yyyy" },
|
|
252
|
+
"purchase_amount": { "aliases": ["Purchase Amount"], "type": "money", "currency": "USD" },
|
|
253
|
+
"party_a": { "aliases": ["Party A"], "type": "party" }
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
| `type` | Accepts | Normalizes to | Notes |
|
|
258
|
+
| -------- | -------------------------------------------------------------------- | -------------------------------------- | ----- |
|
|
259
|
+
| `date` | ISO (`2027-01-15`) or spelled (`January 15, 2027`, `Jan 15 2027`) | `format` field (default `MMMM d, yyyy`) | Q3.1 locked: US (`MM/DD/YYYY`) and European (`DD/MM/YYYY`) numeric forms are **rejected** as ambiguous. |
|
|
260
|
+
| `money` | `$5,000`, `5000.50`, `$5M`, `2.5K`, `1B`; rejects `$$5`, `5,00`, etc. | `currency`-formatted (e.g. `$5,000,000.00`) | Q3.2 locked: v0.3.0 supports `currency: "USD"` only. |
|
|
261
|
+
| `party` | Non-empty after trim; no markdown links `[text](url)`; no trailing punctuation `.,;:!?` | Trimmed string | Q3.3 locked: hard error on bad input — typed params are opt-in. |
|
|
262
|
+
|
|
263
|
+
`format` for `date` supports `yyyy`, `MMMM`, `MM`, `d` tokens (matched
|
|
264
|
+
in a single pass — `MM` doesn't accidentally consume `MMMM`, `d`
|
|
265
|
+
doesn't leak into month names). Other literal characters pass through
|
|
266
|
+
unchanged. Other tokens (e.g. `HH:mm`, `dd`, `EEEE`) are deferred to
|
|
267
|
+
future versions.
|
|
268
|
+
|
|
269
|
+
Programmatic API for drivers: `parseDateValue`, `formatDateValue`,
|
|
270
|
+
`parseMoneyValue`, `formatMoneyValue`, `normalizeTypedValue`,
|
|
271
|
+
`normalizeTypedValues`.
|
|
272
|
+
|
|
240
273
|
### Orphan handling (Q4 locked)
|
|
241
274
|
|
|
242
275
|
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.2
|
|
73
|
+
export const VERSION = "0.3.2";
|
|
74
74
|
|
|
75
75
|
// ─── EXIT CODES ─────────────────────────────────────────────────────────────
|
|
76
76
|
/**
|
|
@@ -918,6 +918,12 @@ export function parseSchema(parsed, sourceLabel = "<schema>") {
|
|
|
918
918
|
aliases: v.aliases.slice(),
|
|
919
919
|
required: v.required !== false,
|
|
920
920
|
default: Object.prototype.hasOwnProperty.call(v, "default") ? v.default : null,
|
|
921
|
+
// v2 #3: typed parameters. `type` is one of `date|money|party` (or
|
|
922
|
+
// absent → no validation/normalization). `format` (date) and
|
|
923
|
+
// `currency` (money) are optional.
|
|
924
|
+
type: typeof v.type === "string" ? v.type : null,
|
|
925
|
+
format: typeof v.format === "string" ? v.format : null,
|
|
926
|
+
currency: typeof v.currency === "string" ? v.currency : null,
|
|
921
927
|
};
|
|
922
928
|
} else {
|
|
923
929
|
if (!Array.isArray(v)) {
|
|
@@ -925,7 +931,7 @@ export function parseSchema(parsed, sourceLabel = "<schema>") {
|
|
|
925
931
|
e.exitCode = EXIT.IO;
|
|
926
932
|
throw e;
|
|
927
933
|
}
|
|
928
|
-
entries[k] = { aliases: v.slice(), required: true, default: null };
|
|
934
|
+
entries[k] = { aliases: v.slice(), required: true, default: null, type: null, format: null, currency: null };
|
|
929
935
|
}
|
|
930
936
|
}
|
|
931
937
|
return { form: long ? "long" : "short", entries };
|
|
@@ -1066,6 +1072,9 @@ function assemble(tier, hits, schema, warnings, fromLlm = false) {
|
|
|
1066
1072
|
required: resolved.required,
|
|
1067
1073
|
default: resolved.default,
|
|
1068
1074
|
aliases: resolved.aliases,
|
|
1075
|
+
type: resolved.type,
|
|
1076
|
+
format: resolved.format,
|
|
1077
|
+
currency: resolved.currency,
|
|
1069
1078
|
hits: [],
|
|
1070
1079
|
});
|
|
1071
1080
|
}
|
|
@@ -1080,14 +1089,22 @@ function resolveKey(hit, schema, fromLlm) {
|
|
|
1080
1089
|
if (schema) {
|
|
1081
1090
|
for (const [key, entry] of Object.entries(schema.entries)) {
|
|
1082
1091
|
if (entry.aliases.includes(hit.inner)) {
|
|
1083
|
-
return {
|
|
1092
|
+
return {
|
|
1093
|
+
key,
|
|
1094
|
+
required: entry.required,
|
|
1095
|
+
default: entry.default,
|
|
1096
|
+
aliases: entry.aliases,
|
|
1097
|
+
type: entry.type || null,
|
|
1098
|
+
format: entry.format || null,
|
|
1099
|
+
currency: entry.currency || null,
|
|
1100
|
+
};
|
|
1084
1101
|
}
|
|
1085
1102
|
}
|
|
1086
1103
|
return null;
|
|
1087
1104
|
}
|
|
1088
1105
|
const key = fromLlm && hit.suggested_key ? hit.suggested_key : canonicalKey(hit.inner);
|
|
1089
1106
|
if (!validKey(key)) return null;
|
|
1090
|
-
return { key, required: true, default: null, aliases: [hit.inner] };
|
|
1107
|
+
return { key, required: true, default: null, aliases: [hit.inner], type: null, format: null, currency: null };
|
|
1091
1108
|
}
|
|
1092
1109
|
|
|
1093
1110
|
// ─── VALUE RESOLUTION (CLI > JSON > prompt > default) ───────────────────────
|
|
@@ -1165,6 +1182,218 @@ export async function resolveValues(placeholders, opts, paramsObj, { prompter =
|
|
|
1165
1182
|
return { resolved, missing, sources };
|
|
1166
1183
|
}
|
|
1167
1184
|
|
|
1185
|
+
// ─── TYPED-PARAMETER NORMALIZATION (v2 #3) ──────────────────────────────────
|
|
1186
|
+
// Schema entries can declare `type: date | money | party` with optional
|
|
1187
|
+
// `format` (date) or `currency` (money). Inputs are validated and normalized
|
|
1188
|
+
// after value resolution and before substitution. Hard error (exit 4) on
|
|
1189
|
+
// invalid input — typed params are opt-in; the user asked for validation.
|
|
1190
|
+
|
|
1191
|
+
const MONTH_NAMES = ["January", "February", "March", "April", "May", "June",
|
|
1192
|
+
"July", "August", "September", "October", "November", "December"];
|
|
1193
|
+
const MONTH_INDEX = (() => {
|
|
1194
|
+
const m = {};
|
|
1195
|
+
MONTH_NAMES.forEach((name, i) => {
|
|
1196
|
+
m[name.toLowerCase()] = i;
|
|
1197
|
+
m[name.slice(0, 3).toLowerCase()] = i;
|
|
1198
|
+
});
|
|
1199
|
+
// "Sept" is a common 4-letter abbrev.
|
|
1200
|
+
m.sept = 8;
|
|
1201
|
+
return m;
|
|
1202
|
+
})();
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* Parse a date input. Accepts ISO `YYYY-MM-DD` or spelled
|
|
1206
|
+
* `Month D, YYYY` / `Mon D YYYY`. Returns a UTC `Date` on success or `null`
|
|
1207
|
+
* on failure. Q3.1: US (`MM/DD/YYYY`) and European (`DD/MM/YYYY`) numeric
|
|
1208
|
+
* formats are NOT accepted — they're ambiguous and footgun-y. Use ISO for
|
|
1209
|
+
* machine input, spelled for human input.
|
|
1210
|
+
*
|
|
1211
|
+
* @param {string} raw
|
|
1212
|
+
* @returns {Date | null}
|
|
1213
|
+
*/
|
|
1214
|
+
export function parseDateValue(raw) {
|
|
1215
|
+
const s = String(raw).trim();
|
|
1216
|
+
const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
|
|
1217
|
+
if (iso) {
|
|
1218
|
+
const [, y, m, d] = iso;
|
|
1219
|
+
const date = new Date(Date.UTC(+y, +m - 1, +d));
|
|
1220
|
+
// Reject impossible dates (e.g. 2026-02-31 round-trips to 2026-03-03).
|
|
1221
|
+
if (date.getUTCFullYear() !== +y || date.getUTCMonth() !== +m - 1 || date.getUTCDate() !== +d) return null;
|
|
1222
|
+
return date;
|
|
1223
|
+
}
|
|
1224
|
+
const spelled = /^([A-Za-z]+)\s+(\d{1,2}),?\s+(\d{4})$/.exec(s);
|
|
1225
|
+
if (spelled) {
|
|
1226
|
+
const month = MONTH_INDEX[spelled[1].toLowerCase()];
|
|
1227
|
+
if (month === undefined) return null;
|
|
1228
|
+
const date = new Date(Date.UTC(+spelled[3], month, +spelled[2]));
|
|
1229
|
+
if (date.getUTCMonth() !== month || date.getUTCDate() !== +spelled[2]) return null;
|
|
1230
|
+
return date;
|
|
1231
|
+
}
|
|
1232
|
+
return null;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* Format a `Date` per a simple format string. Supported tokens:
|
|
1237
|
+
* `yyyy` (year), `MMMM` (full month name), `MM` (2-digit month), `d` (day).
|
|
1238
|
+
* Order doesn't matter; tokens are matched in a single pass so MMMM doesn't
|
|
1239
|
+
* accidentally consume MM, and `d` doesn't leak into month names.
|
|
1240
|
+
*
|
|
1241
|
+
* @param {Date} date
|
|
1242
|
+
* @param {string} format
|
|
1243
|
+
* @returns {string}
|
|
1244
|
+
*/
|
|
1245
|
+
export function formatDateValue(date, format) {
|
|
1246
|
+
const y = date.getUTCFullYear();
|
|
1247
|
+
const m = date.getUTCMonth();
|
|
1248
|
+
const d = date.getUTCDate();
|
|
1249
|
+
return format.replace(/yyyy|MMMM|MM|d/g, (token) => {
|
|
1250
|
+
if (token === "yyyy") return String(y);
|
|
1251
|
+
if (token === "MMMM") return MONTH_NAMES[m];
|
|
1252
|
+
if (token === "MM") return String(m + 1).padStart(2, "0");
|
|
1253
|
+
if (token === "d") return String(d);
|
|
1254
|
+
return token;
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* Parse a money input. Accepts `$5,000`, `5000.50`, `$5M`, `2.5K`, etc.
|
|
1260
|
+
* Handles `K`/`M`/`B` suffixes (case-insensitive). Returns the value in
|
|
1261
|
+
* minor units (cents for USD) as an integer, or `null` on failure.
|
|
1262
|
+
*
|
|
1263
|
+
* @param {string} raw
|
|
1264
|
+
* @returns {number | null}
|
|
1265
|
+
*/
|
|
1266
|
+
export function parseMoneyValue(raw) {
|
|
1267
|
+
const s = String(raw).trim();
|
|
1268
|
+
if (!s) return null;
|
|
1269
|
+
// Strict shape: optional minus, optional single $, digits (with optional
|
|
1270
|
+
// thousand-comma groups), optional decimal, optional K/M/B. Rejects
|
|
1271
|
+
// doubled `$`, ad-hoc comma placement, multiple decimals, words.
|
|
1272
|
+
if (!/^-?\$?(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?[KMB]?$/i.test(s)) return null;
|
|
1273
|
+
let core = s.replace(/[$,\s]/g, "");
|
|
1274
|
+
let mult = 1;
|
|
1275
|
+
if (/[KMB]$/i.test(core)) {
|
|
1276
|
+
mult = { K: 1e3, M: 1e6, B: 1e9 }[core.slice(-1).toUpperCase()];
|
|
1277
|
+
core = core.slice(0, -1);
|
|
1278
|
+
}
|
|
1279
|
+
const n = parseFloat(core);
|
|
1280
|
+
if (!isFinite(n)) return null;
|
|
1281
|
+
return Math.round(n * mult * 100);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* Format a money value (in minor units, e.g. cents for USD) per a currency.
|
|
1286
|
+
* Q3.2: v2 supports USD only. Adds thousand separators and always renders
|
|
1287
|
+
* two decimal places.
|
|
1288
|
+
*
|
|
1289
|
+
* @param {number} minor — value in minor units (cents).
|
|
1290
|
+
* @param {string} currency — currency code (only "USD" supported in v2).
|
|
1291
|
+
* @returns {string}
|
|
1292
|
+
* @throws {Error} on unsupported currency.
|
|
1293
|
+
*/
|
|
1294
|
+
export function formatMoneyValue(minor, currency) {
|
|
1295
|
+
if (currency !== "USD") {
|
|
1296
|
+
throw new Error(`only USD is supported in v0.3.0; got currency="${currency}"`);
|
|
1297
|
+
}
|
|
1298
|
+
const sign = minor < 0 ? "-" : "";
|
|
1299
|
+
const abs = Math.abs(minor);
|
|
1300
|
+
const dollars = Math.floor(abs / 100);
|
|
1301
|
+
const cents = abs % 100;
|
|
1302
|
+
const intPart = String(dollars).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
1303
|
+
return `${sign}$${intPart}.${String(cents).padStart(2, "0")}`;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* Normalize a raw value per a placeholder's schema-declared type. Returns
|
|
1308
|
+
* the normalized string. Throws on invalid input (Q3.3 → hard error).
|
|
1309
|
+
* If no `type` is declared on the placeholder, returns the raw value
|
|
1310
|
+
* unchanged.
|
|
1311
|
+
*
|
|
1312
|
+
* @param {string} rawValue
|
|
1313
|
+
* @param {{ type?: string|null, format?: string|null, currency?: string|null, key?: string }} placeholder
|
|
1314
|
+
* @returns {string}
|
|
1315
|
+
* @throws {Error} with `.exitCode = EXIT.VALIDATION` on bad input.
|
|
1316
|
+
*/
|
|
1317
|
+
export function normalizeTypedValue(rawValue, placeholder) {
|
|
1318
|
+
const type = placeholder && placeholder.type;
|
|
1319
|
+
if (!type) return rawValue;
|
|
1320
|
+
if (type === "date") {
|
|
1321
|
+
const date = parseDateValue(rawValue);
|
|
1322
|
+
if (!date) {
|
|
1323
|
+
const e = new Error(
|
|
1324
|
+
`could not parse "${rawValue}" as a date. expected ISO ` +
|
|
1325
|
+
`(2027-01-15) or spelled ("January 15, 2027"). ` +
|
|
1326
|
+
`US ("01/15/2027") and European ("15/01/2027") forms are not ` +
|
|
1327
|
+
`accepted — they're ambiguous.`
|
|
1328
|
+
);
|
|
1329
|
+
e.exitCode = EXIT.VALIDATION;
|
|
1330
|
+
throw e;
|
|
1331
|
+
}
|
|
1332
|
+
return formatDateValue(date, placeholder.format || "MMMM d, yyyy");
|
|
1333
|
+
}
|
|
1334
|
+
if (type === "money") {
|
|
1335
|
+
const minor = parseMoneyValue(rawValue);
|
|
1336
|
+
if (minor === null) {
|
|
1337
|
+
const e = new Error(
|
|
1338
|
+
`could not parse "${rawValue}" as money. expected like ` +
|
|
1339
|
+
`"$5,000", "5000.50", "$5M", "2.5K".`
|
|
1340
|
+
);
|
|
1341
|
+
e.exitCode = EXIT.VALIDATION;
|
|
1342
|
+
throw e;
|
|
1343
|
+
}
|
|
1344
|
+
return formatMoneyValue(minor, placeholder.currency || "USD");
|
|
1345
|
+
}
|
|
1346
|
+
if (type === "party") {
|
|
1347
|
+
const s = String(rawValue).trim();
|
|
1348
|
+
if (!s) {
|
|
1349
|
+
const e = new Error(`party value must be non-empty`);
|
|
1350
|
+
e.exitCode = EXIT.VALIDATION;
|
|
1351
|
+
throw e;
|
|
1352
|
+
}
|
|
1353
|
+
if (/\]\(/.test(s)) {
|
|
1354
|
+
const e = new Error(`party value "${rawValue}" contains a markdown link; pass the bare party name instead.`);
|
|
1355
|
+
e.exitCode = EXIT.VALIDATION;
|
|
1356
|
+
throw e;
|
|
1357
|
+
}
|
|
1358
|
+
if (/[.!?,;:]$/.test(s)) {
|
|
1359
|
+
const e = new Error(`party value "${rawValue}" has trailing punctuation; remove it before passing.`);
|
|
1360
|
+
e.exitCode = EXIT.VALIDATION;
|
|
1361
|
+
throw e;
|
|
1362
|
+
}
|
|
1363
|
+
return s;
|
|
1364
|
+
}
|
|
1365
|
+
const e = new Error(`unknown type "${type}" on placeholder${placeholder.key ? ` "${placeholder.key}"` : ""}. expected one of: date, money, party.`);
|
|
1366
|
+
e.exitCode = EXIT.IO;
|
|
1367
|
+
throw e;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* Run {@link normalizeTypedValue} across every resolved placeholder value.
|
|
1372
|
+
* Mutates `resolved` in place with normalized strings. Collects all errors
|
|
1373
|
+
* before returning so the user sees every type failure at once.
|
|
1374
|
+
*
|
|
1375
|
+
* @param {Placeholder[]} placeholders
|
|
1376
|
+
* @param {Object<string,string>} resolved
|
|
1377
|
+
* @returns {{ ok: boolean, errors: Array<{ key: string, message: string }>, normalized: Object<string,{from: string, to: string, type: string}> }}
|
|
1378
|
+
*/
|
|
1379
|
+
export function normalizeTypedValues(placeholders, resolved) {
|
|
1380
|
+
const errors = [];
|
|
1381
|
+
const normalized = {};
|
|
1382
|
+
for (const p of placeholders) {
|
|
1383
|
+
if (!p.type) continue;
|
|
1384
|
+
if (resolved[p.key] === undefined) continue;
|
|
1385
|
+
const raw = resolved[p.key];
|
|
1386
|
+
try {
|
|
1387
|
+
const norm = normalizeTypedValue(raw, p);
|
|
1388
|
+
if (norm !== raw) normalized[p.key] = { from: raw, to: norm, type: p.type };
|
|
1389
|
+
resolved[p.key] = norm;
|
|
1390
|
+
} catch (e) {
|
|
1391
|
+
errors.push({ key: p.key, message: e.message });
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
return { ok: errors.length === 0, errors, normalized };
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1168
1397
|
async function nodePrompter(placeholder) {
|
|
1169
1398
|
if (!process.stdin.isTTY) return null;
|
|
1170
1399
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
@@ -1403,6 +1632,21 @@ export async function cmdValidate(opts, input, schema, paramsObj, envObj, { fetc
|
|
|
1403
1632
|
}
|
|
1404
1633
|
return EXIT.VALIDATION;
|
|
1405
1634
|
}
|
|
1635
|
+
// v2 #3: typed-parameter validation. Mirror what cmdDraft does so
|
|
1636
|
+
// `--validate` catches type errors before the user runs draft.
|
|
1637
|
+
const typeCheck = normalizeTypedValues(result.placeholders, resolved);
|
|
1638
|
+
if (!typeCheck.ok) {
|
|
1639
|
+
for (const te of typeCheck.errors) {
|
|
1640
|
+
err.write(paint(`error: type validation failed for "${te.key}": ${te.message}\n`, "red", err));
|
|
1641
|
+
}
|
|
1642
|
+
if (opts.json) {
|
|
1643
|
+
out.write(JSON.stringify({
|
|
1644
|
+
ok: false,
|
|
1645
|
+
type_errors: typeCheck.errors.map(({ key, message }) => ({ key, message })),
|
|
1646
|
+
}, null, 2) + "\n");
|
|
1647
|
+
}
|
|
1648
|
+
return EXIT.VALIDATION;
|
|
1649
|
+
}
|
|
1406
1650
|
if (opts.json) {
|
|
1407
1651
|
out.write(JSON.stringify({ ok: true, resolved: Object.keys(resolved), sources }, null, 2) + "\n");
|
|
1408
1652
|
} else {
|
|
@@ -1472,6 +1716,17 @@ export async function cmdDraft(opts, input, schema, paramsObj, envObj, { fetcher
|
|
|
1472
1716
|
return EXIT.VALIDATION;
|
|
1473
1717
|
}
|
|
1474
1718
|
|
|
1719
|
+
// v2 #3: typed-parameter normalization. Schema entries can declare
|
|
1720
|
+
// `type: date | money | party`. Inputs are validated and normalized
|
|
1721
|
+
// before substitution. Hard error on bad input (Q3.3 decision).
|
|
1722
|
+
const typeCheck = normalizeTypedValues(result.placeholders, resolved);
|
|
1723
|
+
if (!typeCheck.ok) {
|
|
1724
|
+
for (const te of typeCheck.errors) {
|
|
1725
|
+
err.write(paint(`error: type validation failed for "${te.key}": ${te.message}\n`, "red", err));
|
|
1726
|
+
}
|
|
1727
|
+
return EXIT.VALIDATION;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1475
1730
|
// Diff mode: print a substitution table and exit without writing output.
|
|
1476
1731
|
if (opts.diff) {
|
|
1477
1732
|
if (opts.json) {
|