@uwmd/batch 0.2.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,3 +16,23 @@ returns deterministic asset-class, stage, and flag summaries. `projectUnderwriti
16
16
  returns every matching candidate ordered by `blocking_flags`, then error count,
17
17
  warning count, and path. Invalid candidates stay visible in the index, summary,
18
18
  and unfiltered queue; none of these helpers changes a deal file.
19
+
20
+ ## Corpus fact table (`--facts`)
21
+
22
+ ```bash
23
+ npx @uwmd/batch deals --out batch-output --facts
24
+ ```
25
+
26
+ Adds `uwmd-facts.jsonl` — one JSON object per line, one line per JSON fact in
27
+ every parseable deal — plus `uwmd-facts-manifest.json` with the counts and the
28
+ skip list. Each row is a normative `block_values` row (UW CSV Bundle spec §3,
29
+ produced by `@uwmd/core`'s `flattenEnvelopeBlockValues`, never re-implemented
30
+ here) prefixed with the deal's identity: `path`, `deal_id`, `asset_class`,
31
+ `semantic_digest`, and `valid`. Deals that parse but fail validation are
32
+ included with `valid: false`; files that cannot produce an envelope are listed
33
+ in the manifest's `deals_skipped` — the fact table never silently drops a deal.
34
+
35
+ The JSONL loads directly into DuckDB (`read_json('batch-output/uwmd-facts.jsonl')`),
36
+ Snowflake, ClickHouse, or Pandas. The durable key for a fact is
37
+ `(semantic_digest, block_ref, scope, pointer)`. See the
38
+ [data-lake guide](../../docs/DATA_LAKE.md) for the full pipeline.
@@ -1,6 +1,23 @@
1
1
  #!/usr/bin/env node
2
- import { indexUWMDDirectory, writeUWMDCollectionIndex } from '../dist/index.js';
3
- const [input, ...args] = process.argv.slice(2);
4
- const out = args[0] === '--out' ? args[1] : undefined;
5
- if (!input || !out) { console.error('Usage: uwmd-batch <input-directory> --out <output-directory>'); process.exitCode = 2; }
6
- else { const index = await indexUWMDDirectory(input); const files = await writeUWMDCollectionIndex(index, out); console.log(JSON.stringify({ ...index, outputs: files }, null, 2)); if (index.invalid_deals > 0) process.exitCode = 1; }
2
+ import { buildUWMDFactTable, indexUWMDDirectory, writeUWMDCollectionIndex, writeUWMDFactTable } from '../dist/index.js';
3
+ const argv = process.argv.slice(2);
4
+ const facts = argv.includes('--facts');
5
+ const args = argv.filter((arg) => arg !== '--facts');
6
+ const [input, ...rest] = args;
7
+ const out = rest[0] === '--out' ? rest[1] : undefined;
8
+ if (!input || !out) {
9
+ console.error('Usage: uwmd-batch <input-directory> --out <output-directory> [--facts]');
10
+ process.exitCode = 2;
11
+ } else {
12
+ const index = await indexUWMDDirectory(input);
13
+ const files = await writeUWMDCollectionIndex(index, out);
14
+ const outputs = { ...files };
15
+ if (facts) {
16
+ const table = await buildUWMDFactTable(input);
17
+ const factFiles = await writeUWMDFactTable(table, out);
18
+ outputs.facts_jsonl = factFiles.jsonl;
19
+ outputs.facts_manifest = factFiles.manifest;
20
+ }
21
+ console.log(JSON.stringify({ ...index, outputs }, null, 2));
22
+ if (index.invalid_deals > 0) process.exitCode = 1;
23
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
+ import { type UWBlockValueRow } from '@uwmd/core';
1
2
  export declare const UWMD_BATCH_INDEX_VERSION: "0.2";
3
+ export declare const UWMD_FACT_TABLE_VERSION: "0.1";
2
4
  export type MetricComparison = 'gt' | 'gte' | 'lt' | 'lte' | 'eq';
3
5
  export interface BatchMetricFilter {
4
6
  metric: string;
@@ -77,3 +79,47 @@ export declare function summarizeUWMDCollection(index: UWMDCollectionIndex): UWM
77
79
  * retained: they sort with the highest error count instead of disappearing.
78
80
  */
79
81
  export declare function projectUnderwritingQueue(index: UWMDCollectionIndex, filters?: BatchDealFilters): UnderwritingQueueProjection;
82
+ /**
83
+ * One row of the corpus-level fact table: a normative `block_values` row
84
+ * (UW CSV Bundle spec §3, via `flattenEnvelopeBlockValues`) prefixed with the
85
+ * deal's identity — path, `deal_id`, `asset_class`, semantic digest, and
86
+ * validation verdict. The durable key for a fact is
87
+ * `(semantic_digest, block_ref, scope, pointer)`; `path` and `deal_id` are
88
+ * conveniences and `block_ref` alone is not durable across encodings.
89
+ */
90
+ export interface UWMDFactRow extends UWBlockValueRow {
91
+ path: string;
92
+ deal_id: string | null;
93
+ asset_class: string | null;
94
+ semantic_digest: string;
95
+ valid: boolean;
96
+ }
97
+ export interface UWMDFactTable {
98
+ fact_table_version: typeof UWMD_FACT_TABLE_VERSION;
99
+ files_scanned: number;
100
+ deals_included: number;
101
+ /** Files that could not be parsed into an envelope, with the refusal reason. */
102
+ deals_skipped: Array<{
103
+ path: string;
104
+ error: string;
105
+ }>;
106
+ rows: UWMDFactRow[];
107
+ }
108
+ /**
109
+ * Walks a directory of deal files and returns every JSON fact of every
110
+ * parseable deal as one flat table. Deals that parse but fail validation are
111
+ * INCLUDED with `valid: false` (mirroring the collection index, where invalid
112
+ * candidates stay visible); files that cannot produce an envelope at all are
113
+ * listed in `deals_skipped` — a fact table never silently drops a deal.
114
+ * Ordering is deterministic: files sorted by path, rows in envelope order.
115
+ */
116
+ export declare function buildUWMDFactTable(inputDirectory: string): Promise<UWMDFactTable>;
117
+ /**
118
+ * Writes the fact table as `uwmd-facts.jsonl` (one JSON object per line — the
119
+ * shape DuckDB's `read_json`, Snowflake, and ClickHouse ingest directly) plus
120
+ * `uwmd-facts-manifest.json` carrying the counts and skip list.
121
+ */
122
+ export declare function writeUWMDFactTable(table: UWMDFactTable, outputDirectory: string): Promise<{
123
+ jsonl: string;
124
+ manifest: string;
125
+ }>;
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import { relative, resolve, sep } from 'node:path';
3
- import { computeEnvelopeDigest, parseUWFile, toUWEnvelope, validateUWFile } from '@uwmd/core';
3
+ import { computeEnvelopeDigest, flattenEnvelopeBlockValues, parseUWFile, toUWEnvelope, validateUWFile, } from '@uwmd/core';
4
4
  export const UWMD_BATCH_INDEX_VERSION = '0.2';
5
+ export const UWMD_FACT_TABLE_VERSION = '0.1';
5
6
  export class BatchError extends Error {
6
7
  code;
7
8
  constructor(code, message) {
@@ -69,13 +70,73 @@ export function projectUnderwritingQueue(index, filters = {}) {
69
70
  deals,
70
71
  };
71
72
  }
73
+ /**
74
+ * Walks a directory of deal files and returns every JSON fact of every
75
+ * parseable deal as one flat table. Deals that parse but fail validation are
76
+ * INCLUDED with `valid: false` (mirroring the collection index, where invalid
77
+ * candidates stay visible); files that cannot produce an envelope at all are
78
+ * listed in `deals_skipped` — a fact table never silently drops a deal.
79
+ * Ordering is deterministic: files sorted by path, rows in envelope order.
80
+ */
81
+ export async function buildUWMDFactTable(inputDirectory) {
82
+ const root = resolve(inputDirectory);
83
+ const files = (await discoverUWMD(root)).sort();
84
+ const rows = [];
85
+ const skipped = [];
86
+ let included = 0;
87
+ for (const file of files) {
88
+ const path = relative(root, file).split(sep).join('/');
89
+ try {
90
+ const parsed = parseUWFile(await readFile(file, 'utf8'));
91
+ if (!hasUWEnvelope(parsed.frontmatter)) {
92
+ throw new BatchError('MISSING_UW_ENVELOPE', 'File does not contain the required UW Markdown frontmatter envelope.');
93
+ }
94
+ const envelope = toUWEnvelope(parsed);
95
+ const digest = await computeEnvelopeDigest(envelope);
96
+ const validation = validateUWFile(parsed);
97
+ const valid = !validation.issues.some((issue) => issue.severity === 'error');
98
+ const frontmatter = parsed.frontmatter;
99
+ const deal_id = stringOrNull(frontmatter.deal_id);
100
+ const asset_class = stringOrNull(frontmatter.asset_class);
101
+ for (const fact of flattenEnvelopeBlockValues(envelope)) {
102
+ rows.push({ path, deal_id, asset_class, semantic_digest: digest, valid, ...fact });
103
+ }
104
+ included += 1;
105
+ }
106
+ catch (error) {
107
+ skipped.push({ path, error: error instanceof Error ? error.message : String(error) });
108
+ }
109
+ }
110
+ return {
111
+ fact_table_version: UWMD_FACT_TABLE_VERSION,
112
+ files_scanned: files.length,
113
+ deals_included: included,
114
+ deals_skipped: skipped,
115
+ rows,
116
+ };
117
+ }
118
+ /**
119
+ * Writes the fact table as `uwmd-facts.jsonl` (one JSON object per line — the
120
+ * shape DuckDB's `read_json`, Snowflake, and ClickHouse ingest directly) plus
121
+ * `uwmd-facts-manifest.json` carrying the counts and skip list.
122
+ */
123
+ export async function writeUWMDFactTable(table, outputDirectory) {
124
+ await mkdir(outputDirectory, { recursive: true });
125
+ const jsonl = resolve(outputDirectory, 'uwmd-facts.jsonl');
126
+ const manifest = resolve(outputDirectory, 'uwmd-facts-manifest.json');
127
+ const lines = table.rows.map((row) => JSON.stringify(row));
128
+ await writeFile(jsonl, lines.length === 0 ? '' : `${lines.join('\n')}\n`, 'utf8');
129
+ const { rows: _rows, ...head } = table;
130
+ await writeFile(manifest, `${JSON.stringify({ ...head, row_count: table.rows.length }, null, 2)}\n`, 'utf8');
131
+ return { jsonl, manifest };
132
+ }
72
133
  async function discoverUWMD(directory) {
73
134
  const entries = await readdir(directory, { withFileTypes: true });
74
135
  const found = await Promise.all(entries.map(async (entry) => {
75
136
  const path = resolve(directory, entry.name);
76
137
  if (entry.isDirectory())
77
138
  return discoverUWMD(path);
78
- return entry.isFile() && entry.name.endsWith('.uw.md') ? [path] : [];
139
+ return entry.isFile() && (entry.name.endsWith('.uw.md') || entry.name.endsWith('.uwx.md')) ? [path] : [];
79
140
  }));
80
141
  return found.flat();
81
142
  }
package/package.json CHANGED
@@ -1,7 +1,12 @@
1
1
  {
2
2
  "name": "@uwmd/batch",
3
- "version": "0.2.0",
3
+ "version": "0.8.0",
4
4
  "description": "Deterministic local batch indexing for UW Markdown deal files.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/UWMD-OSP/UW-Markdown.git",
8
+ "directory": "packages/uwmd-batch"
9
+ },
5
10
  "type": "module",
6
11
  "main": "./dist/index.js",
7
12
  "types": "./dist/index.d.ts",
@@ -13,7 +18,7 @@
13
18
  "test": "vitest run",
14
19
  "typecheck:tests": "tsc -p tsconfig.test.json"
15
20
  },
16
- "dependencies": { "@uwmd/core": "1.3.0" },
21
+ "dependencies": { "@uwmd/core": "2.3.0" },
17
22
  "devDependencies": { "@types/node": "^20.0.0", "typescript": "^5.4.0", "vitest": "^3.2.6" },
18
23
  "engines": { "node": ">=18.0.0" },
19
24
  "license": "MIT"