@uwmd/core 1.1.2 → 1.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.
Files changed (164) hide show
  1. package/README.md +3 -3
  2. package/dist/agents/bancroft.d.ts +22 -1
  3. package/dist/agents/bancroft.d.ts.map +1 -1
  4. package/dist/agents/bancroft.js +86 -60
  5. package/dist/agents/bancroft.js.map +1 -1
  6. package/dist/agents/provider.d.ts +62 -0
  7. package/dist/agents/provider.d.ts.map +1 -0
  8. package/dist/agents/provider.js +28 -0
  9. package/dist/agents/provider.js.map +1 -0
  10. package/dist/agents/providers/anthropic.d.ts +20 -0
  11. package/dist/agents/providers/anthropic.d.ts.map +1 -0
  12. package/dist/agents/providers/anthropic.js +104 -0
  13. package/dist/agents/providers/anthropic.js.map +1 -0
  14. package/dist/agents/providers/replay.d.ts +46 -0
  15. package/dist/agents/providers/replay.d.ts.map +1 -0
  16. package/dist/agents/providers/replay.js +162 -0
  17. package/dist/agents/providers/replay.js.map +1 -0
  18. package/dist/agents/schemas.d.ts +3 -3
  19. package/dist/agents/schemas.d.ts.map +1 -1
  20. package/dist/agents/schemas.js.map +1 -1
  21. package/dist/browser.d.ts +33 -4
  22. package/dist/browser.d.ts.map +1 -1
  23. package/dist/browser.js +26 -3
  24. package/dist/browser.js.map +1 -1
  25. package/dist/calc/builtins.d.ts +10 -0
  26. package/dist/calc/builtins.d.ts.map +1 -1
  27. package/dist/calc/builtins.js +64 -42
  28. package/dist/calc/builtins.js.map +1 -1
  29. package/dist/calc/errors.js +1 -1
  30. package/dist/calc/evaluator.js +6 -2
  31. package/dist/calc/evaluator.js.map +1 -1
  32. package/dist/calc/index.d.ts +7 -0
  33. package/dist/calc/index.d.ts.map +1 -1
  34. package/dist/calc/index.js +13 -1
  35. package/dist/calc/index.js.map +1 -1
  36. package/dist/calc/quantize.d.ts +56 -0
  37. package/dist/calc/quantize.d.ts.map +1 -0
  38. package/dist/calc/quantize.js +116 -0
  39. package/dist/calc/quantize.js.map +1 -0
  40. package/dist/cascade.d.ts +60 -6
  41. package/dist/cascade.d.ts.map +1 -1
  42. package/dist/cascade.js +93 -14
  43. package/dist/cascade.js.map +1 -1
  44. package/dist/cli-packages.d.ts +11 -0
  45. package/dist/cli-packages.d.ts.map +1 -0
  46. package/dist/cli-packages.js +177 -0
  47. package/dist/cli-packages.js.map +1 -0
  48. package/dist/cli.js +691 -37
  49. package/dist/cli.js.map +1 -1
  50. package/dist/composition.d.ts +182 -0
  51. package/dist/composition.d.ts.map +1 -0
  52. package/dist/composition.js +536 -0
  53. package/dist/composition.js.map +1 -0
  54. package/dist/deal-package-context.d.ts +52 -0
  55. package/dist/deal-package-context.d.ts.map +1 -0
  56. package/dist/deal-package-context.js +132 -0
  57. package/dist/deal-package-context.js.map +1 -0
  58. package/dist/deal-package-zip.d.ts +36 -0
  59. package/dist/deal-package-zip.d.ts.map +1 -0
  60. package/dist/deal-package-zip.js +132 -0
  61. package/dist/deal-package-zip.js.map +1 -0
  62. package/dist/deal-package.d.ts +82 -0
  63. package/dist/deal-package.d.ts.map +1 -0
  64. package/dist/deal-package.js +189 -0
  65. package/dist/deal-package.js.map +1 -0
  66. package/dist/defaults.d.ts +48 -0
  67. package/dist/defaults.d.ts.map +1 -1
  68. package/dist/defaults.js +512 -0
  69. package/dist/defaults.js.map +1 -1
  70. package/dist/index.d.ts +39 -4
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +30 -3
  73. package/dist/index.js.map +1 -1
  74. package/dist/init.js +1 -1
  75. package/dist/init.js.map +1 -1
  76. package/dist/integrity.d.ts +9 -0
  77. package/dist/integrity.d.ts.map +1 -1
  78. package/dist/integrity.js +12 -2
  79. package/dist/integrity.js.map +1 -1
  80. package/dist/lease-abstract.d.ts +82 -0
  81. package/dist/lease-abstract.d.ts.map +1 -0
  82. package/dist/lease-abstract.js +183 -0
  83. package/dist/lease-abstract.js.map +1 -0
  84. package/dist/lite-bridge.d.ts +67 -0
  85. package/dist/lite-bridge.d.ts.map +1 -0
  86. package/dist/lite-bridge.js +564 -0
  87. package/dist/lite-bridge.js.map +1 -0
  88. package/dist/lite.d.ts +63 -0
  89. package/dist/lite.d.ts.map +1 -0
  90. package/dist/lite.js +398 -0
  91. package/dist/lite.js.map +1 -0
  92. package/dist/market-data.d.ts +134 -0
  93. package/dist/market-data.d.ts.map +1 -0
  94. package/dist/market-data.js +292 -0
  95. package/dist/market-data.js.map +1 -0
  96. package/dist/modules.d.ts.map +1 -1
  97. package/dist/modules.js +278 -13
  98. package/dist/modules.js.map +1 -1
  99. package/dist/packs/excel-emit.d.ts +18 -1
  100. package/dist/packs/excel-emit.d.ts.map +1 -1
  101. package/dist/packs/excel-emit.js +18 -1
  102. package/dist/packs/excel-emit.js.map +1 -1
  103. package/dist/packs/hospitality.d.ts +3 -0
  104. package/dist/packs/hospitality.d.ts.map +1 -0
  105. package/dist/packs/hospitality.js +122 -0
  106. package/dist/packs/hospitality.js.map +1 -0
  107. package/dist/packs/index.d.ts +5 -1
  108. package/dist/packs/index.d.ts.map +1 -1
  109. package/dist/packs/index.js +13 -1
  110. package/dist/packs/index.js.map +1 -1
  111. package/dist/packs/land.d.ts +3 -0
  112. package/dist/packs/land.d.ts.map +1 -0
  113. package/dist/packs/land.js +124 -0
  114. package/dist/packs/land.js.map +1 -0
  115. package/dist/packs/senior-housing.d.ts +3 -0
  116. package/dist/packs/senior-housing.d.ts.map +1 -0
  117. package/dist/packs/senior-housing.js +123 -0
  118. package/dist/packs/senior-housing.js.map +1 -0
  119. package/dist/packs/student-housing.d.ts +3 -0
  120. package/dist/packs/student-housing.d.ts.map +1 -0
  121. package/dist/packs/student-housing.js +124 -0
  122. package/dist/packs/student-housing.js.map +1 -0
  123. package/dist/parser.d.ts +9 -0
  124. package/dist/parser.d.ts.map +1 -1
  125. package/dist/parser.js +40 -6
  126. package/dist/parser.js.map +1 -1
  127. package/dist/protocol.d.ts +125 -9
  128. package/dist/protocol.d.ts.map +1 -1
  129. package/dist/protocol.js +182 -4
  130. package/dist/protocol.js.map +1 -1
  131. package/dist/receipts.d.ts +396 -0
  132. package/dist/receipts.d.ts.map +1 -0
  133. package/dist/receipts.js +913 -0
  134. package/dist/receipts.js.map +1 -0
  135. package/dist/runner.d.ts +13 -0
  136. package/dist/runner.d.ts.map +1 -1
  137. package/dist/runner.js +5 -5
  138. package/dist/runner.js.map +1 -1
  139. package/dist/source-representation.d.ts +40 -0
  140. package/dist/source-representation.d.ts.map +1 -0
  141. package/dist/source-representation.js +114 -0
  142. package/dist/source-representation.js.map +1 -0
  143. package/dist/types.d.ts +63 -1
  144. package/dist/types.d.ts.map +1 -1
  145. package/dist/types.js +34 -0
  146. package/dist/types.js.map +1 -1
  147. package/dist/uwcsv.d.ts.map +1 -1
  148. package/dist/uwcsv.js +10 -78
  149. package/dist/uwcsv.js.map +1 -1
  150. package/dist/uwxml.d.ts.map +1 -1
  151. package/dist/uwxml.js +10 -2
  152. package/dist/uwxml.js.map +1 -1
  153. package/dist/validator.d.ts.map +1 -1
  154. package/dist/validator.js +27 -10
  155. package/dist/validator.js.map +1 -1
  156. package/dist/version.d.ts +3 -0
  157. package/dist/version.d.ts.map +1 -0
  158. package/dist/version.js +8 -0
  159. package/dist/version.js.map +1 -0
  160. package/dist/zip-safety.d.ts +15 -0
  161. package/dist/zip-safety.d.ts.map +1 -0
  162. package/dist/zip-safety.js +109 -0
  163. package/dist/zip-safety.js.map +1 -0
  164. package/package.json +12 -3
package/dist/cli.js CHANGED
@@ -2,28 +2,38 @@
2
2
  // uwmd CLI — command-line interface for .uw.md files
3
3
  // Commands: parse, validate, compact, diff, init, summary, render
4
4
  // Usage: uwmd <command> <file> [options]
5
- import { readFileSync, writeFileSync, existsSync } from 'node:fs';
6
- import { resolve, basename } from 'node:path';
5
+ import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from 'node:fs';
6
+ import { resolve, basename, dirname } from 'node:path';
7
7
  import { parseUWFile } from './parser.js';
8
8
  import { validateUWFile } from './validator.js';
9
9
  import { compact, diff } from './compactor.js';
10
10
  import { generateBlankUWFile } from './init.js';
11
11
  import { render } from './renderer.js';
12
12
  import { renderReportHtml } from './report.js';
13
- import { stringifyUWJsonWithDigest } from './uwjson.js';
14
- import { toUWEnvelope } from './envelope.js';
13
+ import { stringifyUWEnvelope } from './uwjson.js';
14
+ import { stampEnvelopeDigest, toUWEnvelope, } from './envelope.js';
15
15
  import { CORE_CODEC_REGISTRY } from './codecs.js';
16
16
  import { writeAgentBlock } from './runner.js';
17
17
  import { applyEdit } from './editor.js';
18
18
  import { verifyChain, verifyProvenance } from './integrity.js';
19
+ import { loadModuleManifest, createModuleRegistry, ModuleRegistryError } from './modules.js';
20
+ import { cmdLeaseValidate, cmdLeaseProject, cmdPackageCreate, cmdPackageVerify, cmdPackageList, cmdPackageToContext, cmdPackageContextValidate, cmdPackageEdges, } from './cli-packages.js';
19
21
  import { evaluateCalc } from './calc/index.js';
20
22
  import { buildAgentContext, buildAgentPrompt, isContextReady, BANCROFT_LAYERS } from './context.js';
21
23
  import { runBancroftAgent } from './agents/bancroft.js';
22
24
  import { buildContext } from './context-profiles.js';
23
25
  import { rankGaps } from './refinement.js';
24
26
  import { resolveValue } from './cascade.js';
27
+ import { parseUWPart, resolveComposition, externalizeSection, stringifyUWPart, UWPART_EXTENSION, } from './composition.js';
28
+ import { createDocumentMarketData, parseMarketDataDocument } from './market-data.js';
25
29
  import { getAssetClassDefaults } from './defaults.js';
26
30
  import { MULTIFAMILY_PACK, getPackForAssetClass } from './packs/index.js';
31
+ import { issueReceipt, verifyReceipt, assertUWReceipt } from './receipts.js';
32
+ import { CORE_VERSION } from './version.js';
33
+ import { detectUWSourceRepresentation, UWX_REPRESENTATION_ID, UW_LITE_SOURCE_DESCRIPTOR, UWX_SOURCE_DESCRIPTOR, UWX_EXTENSION, } from './source-representation.js';
34
+ import { parseUWLite } from './lite.js';
35
+ import { compileUWLite, projectUWEnvelopeToLite, stringifyUWX, } from './lite-bridge.js';
36
+ import { UW_LITE_REPRESENTATION_ID } from './source-representation.js';
27
37
  const [, , command, ...args] = process.argv;
28
38
  function readFile(path) {
29
39
  const resolved = resolve(path);
@@ -71,6 +81,20 @@ function parseFlags(rawArgs) {
71
81
  // ─── Commands ─────────────────────────────────────────────────────────────────
72
82
  function cmdParse(file, flags) {
73
83
  const content = readFile(file);
84
+ const detection = detectUWSourceRepresentation(content, file);
85
+ if (detection.representation !== UWX_REPRESENTATION_ID) {
86
+ const lite = parseUWLite(content);
87
+ console.log(JSON.stringify({
88
+ representation: lite.representation,
89
+ representation_version: lite.representation_version,
90
+ frontmatter: lite.frontmatter,
91
+ fields: lite.fields,
92
+ issues: lite.issues,
93
+ }, null, flags['compact'] ? 0 : 2));
94
+ return;
95
+ }
96
+ for (const warning of detection.warnings)
97
+ console.warn(`Warning: ${warning}`);
74
98
  const parsed = parseUWFile(content, { strict: flags['strict'] === true });
75
99
  const output = {
76
100
  frontmatter: parsed.frontmatter,
@@ -83,6 +107,38 @@ function cmdParse(file, flags) {
83
107
  }
84
108
  function cmdValidate(file, flags) {
85
109
  const content = readFile(file);
110
+ const detection = detectUWSourceRepresentation(content, file);
111
+ if (detection.representation !== UWX_REPRESENTATION_ID) {
112
+ const lite = parseUWLite(content);
113
+ const errors = lite.issues.filter((issue) => issue.severity === 'error');
114
+ const warnings = lite.issues.filter((issue) => issue.severity === 'warning');
115
+ const result = {
116
+ representation: lite.representation,
117
+ overall_status: errors.length > 0 ? 'errors' : warnings.length > 0 ? 'warnings' : 'clean',
118
+ issues: lite.issues,
119
+ errors,
120
+ warnings,
121
+ };
122
+ if (flags['json']) {
123
+ console.log(JSON.stringify(result, null, 2));
124
+ return;
125
+ }
126
+ console.log(`\n${result.overall_status.toUpperCase()} - ${basename(file)}\n`);
127
+ if (result.issues.length === 0) {
128
+ console.log('No Lite syntax issues found.');
129
+ }
130
+ else {
131
+ for (const issue of result.issues) {
132
+ const location = issue.line ? ` line ${issue.line}` : '';
133
+ const field = issue.field_path ? ` [${issue.field_path}]` : '';
134
+ console.log(` [${issue.severity.toUpperCase()}]${location}${field} ${issue.code}: ${issue.message}`);
135
+ }
136
+ }
137
+ console.log('');
138
+ process.exit(errors.length > 0 ? 1 : 0);
139
+ }
140
+ for (const warning of detection.warnings)
141
+ console.warn(`Warning: ${warning}`);
86
142
  const parsed = parseUWFile(content);
87
143
  const instCfg = loadInstitutionConfig(flags['institution']);
88
144
  const result = validateUWFile(parsed, instCfg?.thresholds);
@@ -117,7 +173,9 @@ function cmdValidate(file, flags) {
117
173
  }
118
174
  async function cmdVerify(file, flags) {
119
175
  const content = readFile(file);
120
- const parsed = parseUWFile(content);
176
+ // --resolved verifies the assembled record. Without it an externalized
177
+ // section verifies as a directive, which is not the document anyone means.
178
+ const parsed = withResolved(parseUWFile(content), file, flags);
121
179
  // No flag set → run all three.
122
180
  const onlyValidate = flags['validate'] === true;
123
181
  const onlyIntegrity = flags['integrity'] === true;
@@ -229,8 +287,13 @@ function cmdInit(flags) {
229
287
  dealStage: flags['stage'],
230
288
  tier: flags['tier'] ?? 'screener',
231
289
  });
290
+ // generateBlankUWFile() emits structured UWX content, so the default filename
291
+ // must be .uwx.md. Writing it as .uw.md produced a file the format spec
292
+ // forbids and detectUWSourceRepresentation() flags as legacy on the next load.
232
293
  const filename = flags['output']
233
- ?? (flags['name'] ? `${flags['name'].replace(/\s+/g, '-').toLowerCase()}.uw.md` : 'new-deal.uw.md');
294
+ ?? (flags['name']
295
+ ? `${flags['name'].replace(/\s+/g, '-').toLowerCase()}${UWX_EXTENSION}`
296
+ : `new-deal${UWX_EXTENSION}`);
234
297
  const outPath = resolve(filename);
235
298
  writeFileSync(outPath, content, 'utf-8');
236
299
  console.log(`Created: ${outPath}`);
@@ -269,16 +332,18 @@ function cmdSummary(file) {
269
332
  `);
270
333
  }
271
334
  async function cmdExport(file, flags) {
272
- const content = readFile(file);
273
- const parsed = parseUWFile(content);
274
- const text = await stringifyUWJsonWithDigest(parsed, {
275
- // --no-superseded or --compact drops the append-only history.
276
- includeSuperseded: !(flags['no-superseded'] === true || flags['compact'] === true),
277
- });
335
+ const includeSuperseded = !(flags['no-superseded'] === true || flags['compact'] === true);
336
+ // --resolved exports the assembled record. Exporting the directive instead
337
+ // would ship a document whose rent roll is a list of filenames.
338
+ const loaded = flags['resolved']
339
+ ? toUWEnvelope(withResolved(parseUWFile(readFile(file)), file, flags))
340
+ : await loadEnvelope(file);
341
+ const envelope = includeSuperseded ? loaded : { ...loaded, superseded: {} };
342
+ const text = stringifyUWEnvelope(await stampEnvelopeDigest(envelope));
278
343
  // Default output path: swap the .uw.md suffix for .uw.json (fall back to appending).
279
344
  const outPath = flags['output']
280
345
  ? resolve(flags['output'])
281
- : resolve(file.endsWith('.uw.md') ? `${file.slice(0, -'.uw.md'.length)}.uw.json` : `${file}.uw.json`);
346
+ : resolve(replaceUWExtension(file, '.uw.json'));
282
347
  if (flags['stdout']) {
283
348
  process.stdout.write(text);
284
349
  return;
@@ -292,12 +357,33 @@ const FORMAT_ALIASES = {
292
357
  csv: 'uw-csv-bundle',
293
358
  'csv-bundle': 'uw-csv-bundle',
294
359
  'uw-json': 'uw-json',
360
+ lite: 'uw-lite-markdown',
361
+ uw: 'uw-lite-markdown',
362
+ uwx: 'uwx-markdown',
363
+ 'uw-lite': 'uw-lite-markdown',
364
+ 'uw-lite-markdown': 'uw-lite-markdown',
365
+ 'uwx-markdown': 'uwx-markdown',
295
366
  'uw-xml': 'uw-xml',
296
367
  'uw-csv-bundle': 'uw-csv-bundle',
297
368
  };
298
369
  async function loadEnvelope(file) {
299
- if (file.toLowerCase().endsWith('.uw.md')) {
300
- return toUWEnvelope(parseUWFile(readFile(file)));
370
+ const lower = file.toLowerCase();
371
+ if (lower.endsWith('.uw.md') || lower.endsWith(UWX_EXTENSION)) {
372
+ const content = readFile(file);
373
+ const detection = detectUWSourceRepresentation(content, file);
374
+ if (detection.representation !== UWX_REPRESENTATION_ID) {
375
+ const compilation = compileUWLite(parseUWLite(content));
376
+ if (!compilation.ok) {
377
+ const issues = compilation.report.issues
378
+ .map((issue) => `${issue.code}: ${issue.message}`)
379
+ .join('; ');
380
+ throw new Error(`UW Lite compilation failed: ${issues}`);
381
+ }
382
+ return compilation.envelope;
383
+ }
384
+ for (const warning of detection.warnings)
385
+ console.warn(`Warning: ${warning}`);
386
+ return toUWEnvelope(parseUWFile(content));
301
387
  }
302
388
  const sourceCodec = CORE_CODEC_REGISTRY.findByFileName(file);
303
389
  if (!sourceCodec || !sourceCodec.descriptor.directions.includes('read')) {
@@ -308,9 +394,17 @@ async function loadEnvelope(file) {
308
394
  : readFile(file);
309
395
  return sourceCodec.decode(input);
310
396
  }
397
+ /**
398
+ * Swap a known UW source extension for `extension`, appending if none matches.
399
+ *
400
+ * `.uwx.md` must be in this list. Without it a UWX input fell through to the
401
+ * append branch, so `uwmd export deal.uwx.md` wrote `deal.uwx.md.uw.json`
402
+ * rather than `deal.uw.json`. (`.uw.md` does not match a `.uwx.md` filename —
403
+ * the suffixes genuinely differ — so it was silently a no-op, not a mis-match.)
404
+ */
311
405
  function replaceUWExtension(file, extension) {
312
406
  const lower = file.toLowerCase();
313
- for (const suffix of ['.uw.csv.zip', '.uw.md', '.uw.json', '.uw.xml']) {
407
+ for (const suffix of [UWX_EXTENSION, '.uw.csv.zip', '.uw.md', '.uw.json', '.uw.xml']) {
314
408
  if (lower.endsWith(suffix))
315
409
  return `${file.slice(0, -suffix.length)}${extension}`;
316
410
  }
@@ -319,15 +413,45 @@ function replaceUWExtension(file, extension) {
319
413
  async function cmdConvert(file, flags) {
320
414
  const requested = flags['to'];
321
415
  if (typeof requested !== 'string') {
322
- throw new Error('convert requires --to uw-json|uw-xml|uw-csv-bundle.');
416
+ throw new Error('convert requires --to uw-lite-markdown|uwx-markdown|uw-json|uw-xml|uw-csv-bundle.');
323
417
  }
324
418
  const targetId = FORMAT_ALIASES[requested.toLowerCase()] ?? requested;
325
- const target = CORE_CODEC_REGISTRY.get(targetId);
326
- if (!target.descriptor.directions.includes('write')) {
327
- throw new Error(`Representation ${targetId} is not writable.`);
328
- }
329
419
  const envelope = await loadEnvelope(file);
330
- const encoded = await target.encode(envelope);
420
+ let encoded;
421
+ let extension;
422
+ if (targetId === UWX_REPRESENTATION_ID) {
423
+ encoded = stringifyUWX(envelope);
424
+ extension = UWX_EXTENSION;
425
+ }
426
+ else if (targetId === UW_LITE_REPRESENTATION_ID) {
427
+ const projection = projectUWEnvelopeToLite(envelope);
428
+ encoded = projection.content;
429
+ extension = '.uw.md';
430
+ // Two independent losses, reported separately. A record whose only loss is
431
+ // an externalized section omits zero *paths*, so folding these together
432
+ // would print "omitted 0 advanced path(s)" over a missing rent roll.
433
+ if (projection.report.omitted_paths.length > 0) {
434
+ console.warn(`Warning: Lite projection omitted ${projection.report.omitted_paths.length} advanced path(s).`);
435
+ }
436
+ if (projection.report.externalized_sections.length > 0) {
437
+ console.warn(`Warning: Lite projection omitted ${projection.report.externalized_sections.length} externalized section(s), unresolved here: ${projection.report.externalized_sections.join(', ')}.`);
438
+ }
439
+ if (typeof flags['projection-report'] === 'string') {
440
+ writeFileSync(resolve(flags['projection-report']), JSON.stringify(projection.report, null, 2), 'utf-8');
441
+ }
442
+ }
443
+ else {
444
+ const target = CORE_CODEC_REGISTRY.get(targetId);
445
+ if (!target.descriptor.directions.includes('write')) {
446
+ throw new Error(`Representation ${targetId} is not writable.`);
447
+ }
448
+ const targetEncoded = await target.encode(envelope);
449
+ if (typeof targetEncoded !== 'string' && !(targetEncoded instanceof Uint8Array)) {
450
+ throw new Error(`Representation ${targetId} did not produce file-compatible output.`);
451
+ }
452
+ encoded = targetEncoded;
453
+ extension = target.descriptor.file_extensions[0];
454
+ }
331
455
  if (typeof encoded !== 'string' && !(encoded instanceof Uint8Array)) {
332
456
  throw new Error(`Representation ${targetId} did not produce file-compatible output.`);
333
457
  }
@@ -335,15 +459,174 @@ async function cmdConvert(file, flags) {
335
459
  process.stdout.write(typeof encoded === 'string' ? encoded : Buffer.from(encoded));
336
460
  return;
337
461
  }
338
- const extension = target.descriptor.file_extensions[0];
339
462
  const outPath = flags['output']
340
463
  ? resolve(flags['output'])
341
464
  : resolve(replaceUWExtension(file, extension));
342
465
  writeFileSync(outPath, encoded, 'utf-8');
343
466
  console.log(`Converted ${basename(file)} → ${basename(outPath)} (${targetId})`);
344
467
  }
468
+ // ─── Receipts (RFC 0016 / spec/UW_RECEIPT_v1.md) ─────────────────────────────
469
+ async function cmdReceiptIssue(file, flags) {
470
+ const receipt = await issueReceipt(readFile(file), {
471
+ filename: file,
472
+ issuer: `uwmd-cli@${CORE_VERSION}`,
473
+ ...(typeof flags['issued-at'] === 'string' ? { issued_at: flags['issued-at'] } : {}),
474
+ });
475
+ const serialized = `${JSON.stringify(receipt, null, 2)}\n`;
476
+ if (flags['stdout']) {
477
+ process.stdout.write(serialized);
478
+ return;
479
+ }
480
+ const outPath = flags['output']
481
+ ? resolve(flags['output'])
482
+ : resolve(`${file.replace(/\.(uw|uwx)\.md$/i, '')}.receipt.json`);
483
+ writeFileSync(outPath, serialized, 'utf-8');
484
+ const computed = receipt.computation.results.filter((r) => r.computed).length;
485
+ console.log(`Issued receipt for ${basename(file)} → ${basename(outPath)} ` +
486
+ `(${receipt.computation.pack}@${receipt.computation.pack_version}, ` +
487
+ `${computed}/${receipt.computation.results.length} outputs computed)`);
488
+ console.log('A receipt attests that these outputs follow from this record. It does not attest that the inputs are true.');
489
+ }
490
+ // ─── Modules ──────────────────────────────────────────────────────────────────
491
+ function readManifestFile(path) {
492
+ try {
493
+ return { ok: true, manifest: JSON.parse(readFileSync(resolve(path), 'utf-8')) };
494
+ }
495
+ catch (e) {
496
+ return { ok: false, message: e instanceof Error ? e.message : String(e) };
497
+ }
498
+ }
499
+ function hostTierFlag(flags) {
500
+ const tier = flags['tier'];
501
+ return typeof tier === 'string' ? tier : undefined;
502
+ }
503
+ function cmdModulesValidate(files, flags) {
504
+ const hostTier = hostTierFlag(flags);
505
+ const reports = files.map((file) => {
506
+ const read = readManifestFile(file);
507
+ if (!read.ok) {
508
+ return { file, ok: false, errors: [{ category: 'module', code: 'CLI-MOD-READ', message: read.message }] };
509
+ }
510
+ const result = loadModuleManifest(read.manifest, hostTier ? { hostTier } : {});
511
+ return { file, ok: result.ok, errors: result.errors };
512
+ });
513
+ if (flags['json']) {
514
+ console.log(JSON.stringify({ modules: reports }, null, 2));
515
+ process.exit(reports.every((r) => r.ok) ? 0 : 1);
516
+ }
517
+ for (const report of reports) {
518
+ if (report.ok) {
519
+ console.log(`\nOK - ${basename(report.file)}`);
520
+ continue;
521
+ }
522
+ console.log(`\nERRORS - ${basename(report.file)}`);
523
+ for (const error of report.errors) {
524
+ // The pointer is what makes this actionable: it names the exact
525
+ // declaration that failed, not just the manifest.
526
+ const at = 'pointer' in error && error.pointer ? ` [${error.pointer}]` : '';
527
+ console.log(` ${error.code}${at} ${error.message}`);
528
+ }
529
+ }
530
+ const failed = reports.filter((r) => !r.ok).length;
531
+ console.log(`\n${reports.length - failed}/${reports.length} manifest(s) loaded${hostTier ? ` against ${hostTier}` : ''}.\n`);
532
+ process.exit(failed > 0 ? 1 : 0);
533
+ }
534
+ function cmdModulesList(files, flags) {
535
+ const hostTier = hostTierFlag(flags);
536
+ const manifests = [];
537
+ for (const file of files) {
538
+ const read = readManifestFile(file);
539
+ if (!read.ok) {
540
+ console.error(`Cannot read ${basename(file)}: ${read.message}`);
541
+ process.exit(1);
542
+ }
543
+ manifests.push(read.manifest);
544
+ }
545
+ let registry;
546
+ try {
547
+ registry = createModuleRegistry({ modules: manifests, ...(hostTier ? { hostTier } : {}) });
548
+ }
549
+ catch (e) {
550
+ // Registry construction is all-or-nothing: a bad manifest means there is no
551
+ // registry to list, so report why rather than printing a partial one.
552
+ if (e instanceof ModuleRegistryError) {
553
+ console.error('Registry refused to load:');
554
+ for (const error of e.errors)
555
+ console.error(` ${error.code} ${error.message}`);
556
+ }
557
+ else {
558
+ console.error(e instanceof Error ? e.message : String(e));
559
+ }
560
+ process.exit(1);
561
+ }
562
+ const rows = registry.modules.map((m) => ({
563
+ id: m.id,
564
+ name: m.name,
565
+ version: m.version,
566
+ requires_tier: m.requires_tier,
567
+ asset_classes: m.asset_classes ?? [],
568
+ calculations: (m.calculations ?? []).length,
569
+ validations: (m.validations ?? []).length,
570
+ sections: (m.sections ?? []).length,
571
+ agent_layers: (m.agent_layers ?? []).length,
572
+ }));
573
+ if (flags['json']) {
574
+ console.log(JSON.stringify({ modules: rows }, null, 2));
575
+ return;
576
+ }
577
+ console.log(`\n${rows.length} module(s) loaded\n`);
578
+ for (const row of rows) {
579
+ console.log(` ${row.id}@${row.version} — ${row.name}`);
580
+ console.log(` tier: ${row.requires_tier}`);
581
+ console.log(` asset classes: ${row.asset_classes.length ? row.asset_classes.join(', ') : 'class-agnostic'}`);
582
+ console.log(` contributes: ${row.calculations} calc, ${row.validations} validation, ` +
583
+ `${row.sections} section, ${row.agent_layers} agent layer`);
584
+ }
585
+ console.log('');
586
+ }
587
+ async function cmdReceiptVerify(file, receiptPath, flags) {
588
+ let receipt;
589
+ try {
590
+ const raw = JSON.parse(readFileSync(resolve(receiptPath), 'utf-8'));
591
+ assertUWReceipt(raw);
592
+ receipt = raw;
593
+ }
594
+ catch (e) {
595
+ console.error(`Not a readable receipt: ${e instanceof Error ? e.message : String(e)}`);
596
+ process.exit(2);
597
+ }
598
+ const result = await verifyReceipt(receipt, readFile(file), { filename: file });
599
+ if (flags['json']) {
600
+ console.log(JSON.stringify(result, null, 2));
601
+ }
602
+ else {
603
+ console.log(`Verdict: ${result.verdict.toUpperCase()}`);
604
+ for (const issue of result.issues) {
605
+ const detail = issue.expected !== undefined || issue.actual !== undefined
606
+ ? ` (expected ${issue.expected ?? '—'}, got ${issue.actual ?? '—'})`
607
+ : '';
608
+ console.log(` [${issue.code}] ${issue.message}${detail}`);
609
+ }
610
+ if (result.verdict === 'verified') {
611
+ console.log('\nThis record is unchanged since issuance and its stated outputs follow deterministically\n' +
612
+ 'from its contents. It is NOT a statement that the inputs are true, complete, or approved.');
613
+ }
614
+ else if (result.verdict === 'unverifiable') {
615
+ console.log('\nThis verifier could not decide. That is not a negative result.');
616
+ }
617
+ }
618
+ // 0 verified, 1 failed, 3 unverifiable — so scripts can tell the third state apart.
619
+ if (result.verdict === 'failed')
620
+ process.exit(1);
621
+ if (result.verdict === 'unverifiable')
622
+ process.exit(3);
623
+ }
345
624
  function cmdFormats(flags) {
346
- const descriptors = CORE_CODEC_REGISTRY.list();
625
+ const descriptors = [
626
+ UW_LITE_SOURCE_DESCRIPTOR,
627
+ UWX_SOURCE_DESCRIPTOR,
628
+ ...CORE_CODEC_REGISTRY.list(),
629
+ ];
347
630
  if (flags['json']) {
348
631
  console.log(JSON.stringify(descriptors, null, 2));
349
632
  return;
@@ -360,10 +643,12 @@ function cmdReport(file, flags) {
360
643
  tier: flags['tier'],
361
644
  preparedBy: flags['prepared-by'],
362
645
  });
363
- // Default output path: swap the .uw.md suffix for .report.html.
646
+ // Default output path: swap the source extension for .report.html. Routed
647
+ // through replaceUWExtension so every UW source extension is handled in one
648
+ // place — the inline .uw.md check this replaced missed .uwx.md entirely.
364
649
  const outPath = flags['output']
365
650
  ? resolve(flags['output'])
366
- : resolve(file.endsWith('.uw.md') ? `${file.slice(0, -'.uw.md'.length)}.report.html` : `${file}.report.html`);
651
+ : resolve(replaceUWExtension(file, '.report.html'));
367
652
  if (flags['stdout']) {
368
653
  process.stdout.write(result.html);
369
654
  return;
@@ -373,6 +658,65 @@ function cmdReport(file, flags) {
373
658
  console.log(`Rendered ${label} → ${basename(outPath)} (${result.sectionsRendered.length} sections${result.sectionsSkipped.length ? `, skipped: ${result.sectionsSkipped.join(', ')}` : ''})`);
374
659
  console.log('For PDF output, use @uwmd/report (uwmd-report) or print this HTML from a browser.');
375
660
  }
661
+ /**
662
+ * Load `--market-data <file>` into a cascade-ready lookup, or return null when
663
+ * the flag is absent. Exits with a message rather than throwing, so a bad
664
+ * observation set is a usage error rather than a stack trace.
665
+ */
666
+ function loadMarketData(flags) {
667
+ const path = flags['market-data'];
668
+ if (!path)
669
+ return null;
670
+ try {
671
+ const doc = parseMarketDataDocument(parseUWFile(readFile(path)));
672
+ return {
673
+ lookup: createDocumentMarketData(doc),
674
+ identity: {
675
+ document_id: doc.document_id,
676
+ as_of: doc.as_of,
677
+ provider: doc.provider,
678
+ geo: doc.geo,
679
+ },
680
+ };
681
+ }
682
+ catch (e) {
683
+ console.error(`Market data: ${e instanceof Error ? e.message : String(e)}`);
684
+ process.exit(1);
685
+ }
686
+ }
687
+ function cmdMarketDataValidate(file, flags) {
688
+ const parsed = parseUWFile(readFile(file));
689
+ try {
690
+ const doc = parseMarketDataDocument(parsed);
691
+ if (flags['json']) {
692
+ process.stdout.write(`${JSON.stringify({ valid: true, ...doc }, null, 2)}\n`);
693
+ }
694
+ else {
695
+ console.log(`OK ${doc.document_id}`);
696
+ console.log(` as of ${doc.as_of} — ${doc.provider}`);
697
+ console.log(` ${doc.geo}${doc.asset_class ? ` — ${doc.asset_class}` : ''}`);
698
+ console.log(` ${doc.observations.length} observation(s):`);
699
+ for (const o of doc.observations) {
700
+ console.log(` ${o.field_path} = ${String(o.value)} ${o.unit}${o.confidence ? ` (${o.confidence})` : ''}`);
701
+ console.log(` basis: ${o.basis}`);
702
+ }
703
+ // Restated on every run because it is the thing people forget.
704
+ console.log('\n Attributable, not verified: this says the observations');
705
+ console.log(' are traceable, not that they are accurate or current.');
706
+ }
707
+ }
708
+ catch (e) {
709
+ const message = e instanceof Error ? e.message : String(e);
710
+ if (flags['json']) {
711
+ process.stdout.write(`${JSON.stringify({ valid: false, error: message }, null, 2)}\n`);
712
+ }
713
+ else {
714
+ console.error(`INVALID ${file}`);
715
+ console.error(` ${message}`);
716
+ }
717
+ process.exit(1);
718
+ }
719
+ }
376
720
  function cmdScope(file, flags) {
377
721
  const content = readFile(file);
378
722
  const parsed = parseUWFile(content);
@@ -384,12 +728,19 @@ function cmdScope(file, flags) {
384
728
  console.error(`No published asset-class default table for '${assetClass}'.`);
385
729
  process.exit(1);
386
730
  }
731
+ const market = loadMarketData(flags);
387
732
  const out = {};
388
733
  for (const path of Object.keys(table.fields)) {
389
- const r = resolveValue(path, parsed);
734
+ const r = resolveValue(path, parsed, {
735
+ asset_class: assetClass,
736
+ ...(market ? { market: market.lookup } : {}),
737
+ });
390
738
  out[path] = {
391
739
  value: r.value,
392
740
  step: r.step,
741
+ // Surfaced so a promoted value stays distinguishable in the output: it
742
+ // resolves at the `user_input` step but is tagged `market_data_accepted`.
743
+ source: r.source,
393
744
  range: r.range,
394
745
  resolved_from: r.resolved_from,
395
746
  };
@@ -399,6 +750,7 @@ function cmdScope(file, flags) {
399
750
  deal_stage_target: 'scope',
400
751
  asset_class: assetClass,
401
752
  defaults_table: `${assetClass}@${table.version}`,
753
+ ...(market ? { market_data: market.identity } : {}),
402
754
  resolved: out,
403
755
  };
404
756
  const text = JSON.stringify(payload, null, 2);
@@ -421,7 +773,16 @@ function cmdRefine(file, flags) {
421
773
  parsed.frontmatter.asset_class ??
422
774
  'multifamily';
423
775
  const pack = getPackForAssetClass(assetClass) ?? MULTIFAMILY_PACK;
424
- const result = rankGaps(parsed, { targets, top, packs: [pack] });
776
+ // With `--market-data`, a field the observation set covers is no longer a
777
+ // gap the cascade cannot fill, so it drops out of the VOI ranking. That is
778
+ // the point: the ranking should tell you what is still worth diligencing.
779
+ const market = loadMarketData(flags);
780
+ const result = rankGaps(parsed, {
781
+ targets,
782
+ top,
783
+ packs: [pack],
784
+ ...(market ? { cascadeContext: { asset_class: assetClass, market: market.lookup } } : {}),
785
+ });
425
786
  if (flags['json']) {
426
787
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
427
788
  return;
@@ -458,6 +819,137 @@ for (let _i = 0; _i < args.length; _i++) {
458
819
  } // skip flag + its value
459
820
  positional.push(a);
460
821
  }
822
+ // ─── Composition (RFC 0021) ──────────────────────────────────────────────────
823
+ /** Load every `.uwpart.md` in a directory, keyed by `part_id`. */
824
+ function loadPartsDir(dir) {
825
+ const parts = new Map();
826
+ if (!existsSync(dir))
827
+ return parts;
828
+ for (const file of readdirSync(dir).filter((f) => f.endsWith(UWPART_EXTENSION)).sort()) {
829
+ const path = resolve(dir, file);
830
+ const part = parseUWPart(parseUWFile(readFileSync(path, 'utf-8')), { filename: file });
831
+ if (parts.has(part.part_id)) {
832
+ console.error(`Duplicate part_id '${part.part_id}' in ${dir}`);
833
+ process.exit(1);
834
+ }
835
+ parts.set(part.part_id, part);
836
+ }
837
+ return parts;
838
+ }
839
+ /** Where fragments live for a record, unless told otherwise. */
840
+ function defaultPartsDir(file) {
841
+ return resolve(dirname(resolve(file)), 'parts');
842
+ }
843
+ function cmdCompose(file, flags) {
844
+ const section = flags['externalize'];
845
+ if (typeof section !== 'string') {
846
+ console.error('Usage: uwmd compose <file> --externalize <section> [--collection-key <k>] [--collection-path <p>]');
847
+ process.exit(1);
848
+ }
849
+ const collectionKey = flags['collection-key'] ?? 'unit_id';
850
+ const collectionPath = flags['collection-path'] ?? 'units';
851
+ const parsed = parseUWFile(readFile(file));
852
+ let result;
853
+ try {
854
+ result = externalizeSection(parsed, {
855
+ section,
856
+ collectionKey,
857
+ collectionPath,
858
+ ...(flags['part-prefix'] ? { partIdPrefix: flags['part-prefix'] } : {}),
859
+ });
860
+ }
861
+ catch (e) {
862
+ console.error(e instanceof Error ? e.message : String(e));
863
+ process.exit(1);
864
+ }
865
+ const outDir = flags['out-dir'] ? resolve(flags['out-dir']) : defaultPartsDir(file);
866
+ const recordPath = flags['in-place']
867
+ ? resolve(file)
868
+ : flags['output']
869
+ ? resolve(flags['output'])
870
+ : resolve(replaceUWExtension(file, '.externalized.uwx.md'));
871
+ const recordText = stringifyUWX(toUWEnvelope(result.document));
872
+ if (flags['dry-run']) {
873
+ console.log(`Would write ${result.parts.length} fragment(s) to ${outDir}:`);
874
+ for (const part of result.parts)
875
+ console.log(` ${part.part_id}${UWPART_EXTENSION}`);
876
+ console.log(`Would write the record to ${recordPath}`);
877
+ return;
878
+ }
879
+ mkdirSync(outDir, { recursive: true });
880
+ for (const part of result.parts) {
881
+ writeFileSync(resolve(outDir, `${part.part_id}${UWPART_EXTENSION}`), stringifyUWPart(part), 'utf-8');
882
+ }
883
+ writeFileSync(recordPath, recordText, 'utf-8');
884
+ console.log(`Externalized ${section} → ${result.parts.length} fragment(s) in ${basename(outDir)}/`);
885
+ console.log(`Record written to ${basename(recordPath)}`);
886
+ // Externalizing is a packaging decision, not a model change — worth saying,
887
+ // because the file looks dramatically different afterwards.
888
+ console.log('The resolved record has the same semantic digest as the original.');
889
+ }
890
+ function cmdResolve(file, flags) {
891
+ const partsDir = flags['parts'] ? resolve(flags['parts']) : defaultPartsDir(file);
892
+ const parsed = parseUWFile(readFile(file));
893
+ let parts;
894
+ try {
895
+ parts = loadPartsDir(partsDir);
896
+ }
897
+ catch (e) {
898
+ console.error(e instanceof Error ? e.message : String(e));
899
+ process.exit(1);
900
+ }
901
+ const resolution = resolveComposition(parsed, { parts });
902
+ if (flags['json']) {
903
+ process.stdout.write(`${JSON.stringify({
904
+ status: resolution.status,
905
+ externalized: resolution.externalized,
906
+ parts_available: parts.size,
907
+ issues: resolution.issues,
908
+ }, null, 2)}\n`);
909
+ }
910
+ else {
911
+ console.log(`${resolution.status === 'resolved' ? 'RESOLVED' : 'UNRESOLVED'} ${basename(file)}`);
912
+ console.log(` ${parts.size} fragment(s) available in ${basename(partsDir)}/`);
913
+ console.log(` externalized section(s): ${resolution.externalized.join(', ') || '(none)'}`);
914
+ for (const issue of resolution.issues) {
915
+ console.log(` [${issue.code}] ${issue.message}`);
916
+ }
917
+ if (resolution.status === 'unresolved') {
918
+ // The rule worth restating: under-resolution is never a smaller answer.
919
+ console.log('\n Sections stay externalized rather than resolving partially —');
920
+ console.log(' a collection missing rows would still total and still validate.');
921
+ }
922
+ }
923
+ if (flags['output'] && resolution.status === 'resolved') {
924
+ const outPath = resolve(flags['output']);
925
+ writeFileSync(outPath, stringifyUWX(toUWEnvelope(resolution.document)), 'utf-8');
926
+ if (!flags['json'])
927
+ console.log(` resolved record → ${basename(outPath)}`);
928
+ }
929
+ if (resolution.status !== 'resolved')
930
+ process.exit(1);
931
+ }
932
+ /**
933
+ * Resolve a record's externalized sections before another command reads it.
934
+ *
935
+ * Returns the record unchanged when `--resolved` was not requested, so callers
936
+ * can apply it unconditionally. Exits when resolution fails: verifying or
937
+ * exporting a partially-resolved record would report on a document that does
938
+ * not exist.
939
+ */
940
+ function withResolved(parsed, file, flags) {
941
+ if (!flags['resolved'])
942
+ return parsed;
943
+ const partsDir = flags['parts'] ? resolve(flags['parts']) : defaultPartsDir(file);
944
+ const resolution = resolveComposition(parsed, { parts: loadPartsDir(partsDir) });
945
+ if (resolution.status !== 'resolved') {
946
+ console.error(`Cannot resolve ${basename(file)} against ${basename(partsDir)}/:`);
947
+ for (const issue of resolution.issues)
948
+ console.error(` [${issue.code}] ${issue.message}`);
949
+ process.exit(1);
950
+ }
951
+ return resolution.document;
952
+ }
461
953
  // Top-level async wrapper so `run --live` can use await
462
954
  (async () => {
463
955
  switch (command) {
@@ -477,7 +969,7 @@ for (let _i = 0; _i < args.length; _i++) {
477
969
  break;
478
970
  case 'verify':
479
971
  if (!positional[0]) {
480
- console.error('Usage: uwmd verify <file> [--validate] [--integrity] [--policy] [--json]');
972
+ console.error('Usage: uwmd verify <file> [--validate] [--integrity] [--policy] [--resolved] [--json]');
481
973
  process.exit(1);
482
974
  }
483
975
  await cmdVerify(positional[0], flags);
@@ -749,7 +1241,7 @@ for (let _i = 0; _i < args.length; _i++) {
749
1241
  break;
750
1242
  case 'export':
751
1243
  if (!positional[0]) {
752
- console.error('Usage: uwmd export <file.uw.md> [--output <file.uw.json>] [--no-superseded] [--stdout]');
1244
+ console.error('Usage: uwmd export <file.uw.md> [--output <file.uw.json>] [--no-superseded] [--resolved] [--stdout]');
753
1245
  process.exit(1);
754
1246
  }
755
1247
  await cmdExport(positional[0], flags);
@@ -759,7 +1251,7 @@ for (let _i = 0; _i < args.length; _i++) {
759
1251
  break;
760
1252
  case 'convert':
761
1253
  if (!positional[0]) {
762
- console.error('Usage: uwmd convert <file.uw.md|file.uw.json|file.uw.xml|file.uw.csv.zip> --to uw-json|uw-xml|uw-csv-bundle [--output <file>] [--stdout]');
1254
+ console.error('Usage: uwmd convert <file.uw.md|file.uwx.md|file.uw.json|file.uw.xml|file.uw.csv.zip> --to lite|uwx|uw-json|uw-xml|uw-csv-bundle [--projection-report <file>] [--output <file>] [--stdout]');
763
1255
  process.exit(1);
764
1256
  }
765
1257
  await cmdConvert(positional[0], flags);
@@ -773,21 +1265,168 @@ for (let _i = 0; _i < args.length; _i++) {
773
1265
  break;
774
1266
  case 'scope':
775
1267
  if (!positional[0]) {
776
- console.error('Usage: uwmd scope <file> [--asset-class multifamily] [--output <file>]');
1268
+ console.error('Usage: uwmd scope <file> [--asset-class multifamily] [--market-data <file>] [--output <file>]');
777
1269
  process.exit(1);
778
1270
  }
779
1271
  cmdScope(positional[0], flags);
780
1272
  break;
781
1273
  case 'refine':
782
1274
  if (!positional[0]) {
783
- console.error('Usage: uwmd refine <file> [--targets dscr,debt_yield] [--top 5] [--json]');
1275
+ console.error('Usage: uwmd refine <file> [--targets dscr,debt_yield] [--top 5] [--market-data <file>] [--json]');
784
1276
  process.exit(1);
785
1277
  }
786
1278
  cmdRefine(positional[0], flags);
787
1279
  break;
1280
+ case 'compose':
1281
+ if (!positional[0]) {
1282
+ console.error('Usage: uwmd compose <file> --externalize <section> [--collection-key <k>] [--collection-path <p>] [--out-dir <dir>] [--in-place] [--dry-run]');
1283
+ process.exit(1);
1284
+ }
1285
+ cmdCompose(positional[0], flags);
1286
+ break;
1287
+ case 'resolve':
1288
+ if (!positional[0]) {
1289
+ console.error('Usage: uwmd resolve <file> [--parts <dir>] [--output <file>] [--json]');
1290
+ process.exit(1);
1291
+ }
1292
+ cmdResolve(positional[0], flags);
1293
+ break;
1294
+ case 'market-data': {
1295
+ const sub = positional[0];
1296
+ if (sub === 'validate') {
1297
+ if (!positional[1]) {
1298
+ console.error('Usage: uwmd market-data validate <file> [--json]');
1299
+ process.exit(1);
1300
+ }
1301
+ cmdMarketDataValidate(positional[1], flags);
1302
+ }
1303
+ else {
1304
+ console.error('Usage: uwmd market-data validate <file> [--json]');
1305
+ process.exit(1);
1306
+ }
1307
+ break;
1308
+ }
1309
+ case 'lease': {
1310
+ const sub = positional[0];
1311
+ if (sub === 'validate') {
1312
+ if (!positional[1]) {
1313
+ console.error('Usage: uwmd lease validate <abstract.json> [--json]');
1314
+ process.exit(1);
1315
+ }
1316
+ cmdLeaseValidate(positional[1], flags);
1317
+ }
1318
+ else if (sub === 'project') {
1319
+ if (!positional[1]) {
1320
+ console.error('Usage: uwmd lease project <abstract.json> [--compact]');
1321
+ process.exit(1);
1322
+ }
1323
+ cmdLeaseProject(positional[1], flags);
1324
+ }
1325
+ else {
1326
+ console.error('Usage: uwmd lease <validate|project> <abstract.json>');
1327
+ process.exit(1);
1328
+ }
1329
+ break;
1330
+ }
1331
+ case 'package': {
1332
+ const sub = positional[0];
1333
+ const target = positional[1];
1334
+ if (sub === 'create') {
1335
+ if (!target) {
1336
+ console.error('Usage: uwmd package create <manifest.json> [--output <file.uwpkg.zip>]');
1337
+ process.exit(1);
1338
+ }
1339
+ await cmdPackageCreate(target, flags);
1340
+ }
1341
+ else if (sub === 'verify') {
1342
+ if (!target) {
1343
+ console.error('Usage: uwmd package verify <file.uwpkg.zip> [--json]');
1344
+ process.exit(1);
1345
+ }
1346
+ await cmdPackageVerify(target, flags);
1347
+ }
1348
+ else if (sub === 'list') {
1349
+ if (!target) {
1350
+ console.error('Usage: uwmd package list <file.uwpkg.zip> [--json]');
1351
+ process.exit(1);
1352
+ }
1353
+ cmdPackageList(target, flags);
1354
+ }
1355
+ else if (sub === 'to-context') {
1356
+ if (!target) {
1357
+ console.error('Usage: uwmd package to-context <file.uwpkg.zip> [--output <file.json>] [--stdout]');
1358
+ process.exit(1);
1359
+ }
1360
+ cmdPackageToContext(target, flags);
1361
+ }
1362
+ else if (sub === 'validate-context') {
1363
+ if (!target) {
1364
+ console.error('Usage: uwmd package validate-context <file.uwpkg.context.json> [--json]');
1365
+ process.exit(1);
1366
+ }
1367
+ cmdPackageContextValidate(target, flags);
1368
+ }
1369
+ else if (sub === 'edges') {
1370
+ if (!target) {
1371
+ console.error('Usage: uwmd package edges <file.uwpkg.zip>');
1372
+ process.exit(1);
1373
+ }
1374
+ cmdPackageEdges(target);
1375
+ }
1376
+ else {
1377
+ console.error('Usage: uwmd package <create|verify|list|to-context|validate-context|edges> ...');
1378
+ process.exit(1);
1379
+ }
1380
+ break;
1381
+ }
1382
+ case 'modules': {
1383
+ const sub = positional[0];
1384
+ const files = positional.slice(1);
1385
+ if (sub === 'validate') {
1386
+ if (files.length === 0) {
1387
+ console.error('Usage: uwmd modules validate <manifest.json...> [--tier <viewer-tier>] [--json]');
1388
+ process.exit(1);
1389
+ }
1390
+ cmdModulesValidate(files, flags);
1391
+ }
1392
+ else if (sub === 'list') {
1393
+ if (files.length === 0) {
1394
+ console.error('Usage: uwmd modules list <manifest.json...> [--tier <viewer-tier>] [--json]');
1395
+ process.exit(1);
1396
+ }
1397
+ cmdModulesList(files, flags);
1398
+ }
1399
+ else {
1400
+ console.error('Usage: uwmd modules <validate|list> <manifest.json...>');
1401
+ process.exit(1);
1402
+ }
1403
+ break;
1404
+ }
1405
+ case 'receipt': {
1406
+ const sub = positional[0];
1407
+ if (sub === 'issue') {
1408
+ if (!positional[1]) {
1409
+ console.error('Usage: uwmd receipt issue <file> [--output <file.receipt.json>] [--issued-at <iso>] [--stdout]');
1410
+ process.exit(1);
1411
+ }
1412
+ await cmdReceiptIssue(positional[1], flags);
1413
+ }
1414
+ else if (sub === 'verify') {
1415
+ if (!positional[1] || !positional[2]) {
1416
+ console.error('Usage: uwmd receipt verify <file> <receipt.json> [--json]');
1417
+ process.exit(1);
1418
+ }
1419
+ await cmdReceiptVerify(positional[1], positional[2], flags);
1420
+ }
1421
+ else {
1422
+ console.error('Usage: uwmd receipt <issue|verify> ...');
1423
+ process.exit(1);
1424
+ }
1425
+ break;
1426
+ }
788
1427
  default:
789
1428
  console.log(`
790
- uwmd — .uw.md underwriting file toolkit
1429
+ uwmd — UW Markdown underwriting file toolkit
791
1430
 
792
1431
  Commands:
793
1432
  parse <file> Parse file and output JSON
@@ -799,14 +1438,20 @@ Commands:
799
1438
  compact <file> Strip superseded blocks
800
1439
  diff <file-a> <file-b> Compare two files section by section
801
1440
  calc <file> <calc.json> Evaluate a calc declaration or inline formula (Tier-3)
802
- init Generate a blank .uw.md file
1441
+ init Generate a blank .uwx.md file
803
1442
  summary <file> Print quick metrics to terminal
804
1443
  export <file> Export a digested UW JSON 1.0 document
1444
+ compose <file> Externalize a section into .uwpart.md fragments (RFC 0021)
1445
+ resolve <file> Resolve externalized sections from a parts directory (RFC 0021)
805
1446
  formats List registered machine representations
806
- convert <file> Convert Markdown/JSON/XML/CSV bundle representations
1447
+ convert <file> Convert Lite/UWX/JSON/XML/CSV bundle representations
807
1448
  report <file> Render the lender package / credit memo HTML (§7.1/§7.2)
808
1449
  scope <file> Resolve every required input via the fallback cascade and emit a triage view
809
1450
  refine <file> Rank gaps by value-of-information for stated calc targets
1451
+ receipt issue|verify Issue or verify a detached verification receipt (RFC 0016)
1452
+ modules validate|list Validate module manifest files, or list the registry they form
1453
+ lease validate|project Validate a lease abstract, or project it to a rent-roll row (RFC 0018)
1454
+ package create|verify|... Build, verify, list, or project a UW Deal Package (RFC 0018)
810
1455
  layers List Bancroft agent layers
811
1456
 
812
1457
  Options:
@@ -816,8 +1461,17 @@ Options:
816
1461
  --strict Throw on parse errors instead of collecting
817
1462
  --format <f> Render format: json|csv|chat|summary (render, default: summary)
818
1463
  --no-superseded Drop append-only history from the .uw.json export (export)
1464
+ --resolved Resolve externalized sections first (verify, export)
1465
+ --parts <dir> Fragment directory (compose, resolve, --resolved; default: ./parts)
1466
+ --externalize <s> Section to split into fragments (compose)
1467
+ --collection-key <k> Row identity field (compose, default: unit_id)
1468
+ --collection-path <p> Field the rows occupy (compose, default: units)
1469
+ --in-place Overwrite the input record (compose)
819
1470
  --stdout Write export/convert output to stdout
820
- --to <format> Target representation: uw-json|uw-xml|uw-csv-bundle (convert)
1471
+ --to <format> Target: lite|uwx|uw-json|uw-xml|uw-csv-bundle (convert)
1472
+ --projection-report Write the UWX-to-Lite omission report as JSON (convert)
1473
+ --issued-at <iso> Fix the receipt issuance timestamp (receipt issue)
1474
+ --tier <tier> Host viewer tier to load modules against (modules)
821
1475
  --max-tokens <n> Max tokens for chat render (default: 12000)
822
1476
  --institution <f> Path to .uw.institution.json config (validate)
823
1477
  --name <n> Deal name (init)