@uwmd/core 1.3.0 → 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 (47) hide show
  1. package/dist/browser.d.ts +10 -4
  2. package/dist/browser.d.ts.map +1 -1
  3. package/dist/browser.js +5 -2
  4. package/dist/browser.js.map +1 -1
  5. package/dist/cascade.d.ts +60 -6
  6. package/dist/cascade.d.ts.map +1 -1
  7. package/dist/cascade.js +93 -14
  8. package/dist/cascade.js.map +1 -1
  9. package/dist/cli.js +269 -11
  10. package/dist/cli.js.map +1 -1
  11. package/dist/composition.d.ts +182 -0
  12. package/dist/composition.d.ts.map +1 -0
  13. package/dist/composition.js +536 -0
  14. package/dist/composition.js.map +1 -0
  15. package/dist/context.js +23 -23
  16. package/dist/index.d.ts +10 -4
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +5 -2
  19. package/dist/index.js.map +1 -1
  20. package/dist/lite-bridge.d.ts +9 -0
  21. package/dist/lite-bridge.d.ts.map +1 -1
  22. package/dist/lite-bridge.js +44 -5
  23. package/dist/lite-bridge.js.map +1 -1
  24. package/dist/lite.js +37 -1
  25. package/dist/lite.js.map +1 -1
  26. package/dist/market-data.d.ts +134 -0
  27. package/dist/market-data.d.ts.map +1 -0
  28. package/dist/market-data.js +292 -0
  29. package/dist/market-data.js.map +1 -0
  30. package/dist/protocol.d.ts +57 -8
  31. package/dist/protocol.d.ts.map +1 -1
  32. package/dist/protocol.js +74 -4
  33. package/dist/protocol.js.map +1 -1
  34. package/dist/receipts.d.ts +189 -9
  35. package/dist/receipts.d.ts.map +1 -1
  36. package/dist/receipts.js +373 -14
  37. package/dist/receipts.js.map +1 -1
  38. package/dist/report.js +165 -165
  39. package/dist/types.d.ts +54 -1
  40. package/dist/types.d.ts.map +1 -1
  41. package/dist/types.js.map +1 -1
  42. package/dist/validator.d.ts.map +1 -1
  43. package/dist/validator.js +17 -0
  44. package/dist/validator.js.map +1 -1
  45. package/dist/version.d.ts +1 -1
  46. package/dist/version.js +1 -1
  47. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2,8 +2,8 @@
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';
@@ -24,6 +24,8 @@ import { runBancroftAgent } from './agents/bancroft.js';
24
24
  import { buildContext } from './context-profiles.js';
25
25
  import { rankGaps } from './refinement.js';
26
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';
27
29
  import { getAssetClassDefaults } from './defaults.js';
28
30
  import { MULTIFAMILY_PACK, getPackForAssetClass } from './packs/index.js';
29
31
  import { issueReceipt, verifyReceipt, assertUWReceipt } from './receipts.js';
@@ -171,7 +173,9 @@ function cmdValidate(file, flags) {
171
173
  }
172
174
  async function cmdVerify(file, flags) {
173
175
  const content = readFile(file);
174
- 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);
175
179
  // No flag set → run all three.
176
180
  const onlyValidate = flags['validate'] === true;
177
181
  const onlyIntegrity = flags['integrity'] === true;
@@ -329,7 +333,11 @@ function cmdSummary(file) {
329
333
  }
330
334
  async function cmdExport(file, flags) {
331
335
  const includeSuperseded = !(flags['no-superseded'] === true || flags['compact'] === true);
332
- const loaded = await loadEnvelope(file);
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);
333
341
  const envelope = includeSuperseded ? loaded : { ...loaded, superseded: {} };
334
342
  const text = stringifyUWEnvelope(await stampEnvelopeDigest(envelope));
335
343
  // Default output path: swap the .uw.md suffix for .uw.json (fall back to appending).
@@ -419,9 +427,15 @@ async function cmdConvert(file, flags) {
419
427
  const projection = projectUWEnvelopeToLite(envelope);
420
428
  encoded = projection.content;
421
429
  extension = '.uw.md';
422
- if (projection.report.lossy) {
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) {
423
434
  console.warn(`Warning: Lite projection omitted ${projection.report.omitted_paths.length} advanced path(s).`);
424
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
+ }
425
439
  if (typeof flags['projection-report'] === 'string') {
426
440
  writeFileSync(resolve(flags['projection-report']), JSON.stringify(projection.report, null, 2), 'utf-8');
427
441
  }
@@ -644,6 +658,65 @@ function cmdReport(file, flags) {
644
658
  console.log(`Rendered ${label} → ${basename(outPath)} (${result.sectionsRendered.length} sections${result.sectionsSkipped.length ? `, skipped: ${result.sectionsSkipped.join(', ')}` : ''})`);
645
659
  console.log('For PDF output, use @uwmd/report (uwmd-report) or print this HTML from a browser.');
646
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
+ }
647
720
  function cmdScope(file, flags) {
648
721
  const content = readFile(file);
649
722
  const parsed = parseUWFile(content);
@@ -655,12 +728,19 @@ function cmdScope(file, flags) {
655
728
  console.error(`No published asset-class default table for '${assetClass}'.`);
656
729
  process.exit(1);
657
730
  }
731
+ const market = loadMarketData(flags);
658
732
  const out = {};
659
733
  for (const path of Object.keys(table.fields)) {
660
- const r = resolveValue(path, parsed);
734
+ const r = resolveValue(path, parsed, {
735
+ asset_class: assetClass,
736
+ ...(market ? { market: market.lookup } : {}),
737
+ });
661
738
  out[path] = {
662
739
  value: r.value,
663
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,
664
744
  range: r.range,
665
745
  resolved_from: r.resolved_from,
666
746
  };
@@ -670,6 +750,7 @@ function cmdScope(file, flags) {
670
750
  deal_stage_target: 'scope',
671
751
  asset_class: assetClass,
672
752
  defaults_table: `${assetClass}@${table.version}`,
753
+ ...(market ? { market_data: market.identity } : {}),
673
754
  resolved: out,
674
755
  };
675
756
  const text = JSON.stringify(payload, null, 2);
@@ -692,7 +773,16 @@ function cmdRefine(file, flags) {
692
773
  parsed.frontmatter.asset_class ??
693
774
  'multifamily';
694
775
  const pack = getPackForAssetClass(assetClass) ?? MULTIFAMILY_PACK;
695
- 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
+ });
696
786
  if (flags['json']) {
697
787
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
698
788
  return;
@@ -729,6 +819,137 @@ for (let _i = 0; _i < args.length; _i++) {
729
819
  } // skip flag + its value
730
820
  positional.push(a);
731
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
+ }
732
953
  // Top-level async wrapper so `run --live` can use await
733
954
  (async () => {
734
955
  switch (command) {
@@ -748,7 +969,7 @@ for (let _i = 0; _i < args.length; _i++) {
748
969
  break;
749
970
  case 'verify':
750
971
  if (!positional[0]) {
751
- console.error('Usage: uwmd verify <file> [--validate] [--integrity] [--policy] [--json]');
972
+ console.error('Usage: uwmd verify <file> [--validate] [--integrity] [--policy] [--resolved] [--json]');
752
973
  process.exit(1);
753
974
  }
754
975
  await cmdVerify(positional[0], flags);
@@ -1020,7 +1241,7 @@ for (let _i = 0; _i < args.length; _i++) {
1020
1241
  break;
1021
1242
  case 'export':
1022
1243
  if (!positional[0]) {
1023
- 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]');
1024
1245
  process.exit(1);
1025
1246
  }
1026
1247
  await cmdExport(positional[0], flags);
@@ -1044,18 +1265,47 @@ for (let _i = 0; _i < args.length; _i++) {
1044
1265
  break;
1045
1266
  case 'scope':
1046
1267
  if (!positional[0]) {
1047
- 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>]');
1048
1269
  process.exit(1);
1049
1270
  }
1050
1271
  cmdScope(positional[0], flags);
1051
1272
  break;
1052
1273
  case 'refine':
1053
1274
  if (!positional[0]) {
1054
- 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]');
1055
1276
  process.exit(1);
1056
1277
  }
1057
1278
  cmdRefine(positional[0], flags);
1058
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
+ }
1059
1309
  case 'lease': {
1060
1310
  const sub = positional[0];
1061
1311
  if (sub === 'validate') {
@@ -1191,6 +1441,8 @@ Commands:
1191
1441
  init Generate a blank .uwx.md file
1192
1442
  summary <file> Print quick metrics to terminal
1193
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)
1194
1446
  formats List registered machine representations
1195
1447
  convert <file> Convert Lite/UWX/JSON/XML/CSV bundle representations
1196
1448
  report <file> Render the lender package / credit memo HTML (§7.1/§7.2)
@@ -1209,6 +1461,12 @@ Options:
1209
1461
  --strict Throw on parse errors instead of collecting
1210
1462
  --format <f> Render format: json|csv|chat|summary (render, default: summary)
1211
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)
1212
1470
  --stdout Write export/convert output to stdout
1213
1471
  --to <format> Target: lite|uwx|uw-json|uw-xml|uw-csv-bundle (convert)
1214
1472
  --projection-report Write the UWX-to-Lite omission report as JSON (convert)