@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 +20 -0
- package/bin/uwmd-batch.mjs +22 -5
- package/dist/index.d.ts +46 -0
- package/dist/index.js +63 -2
- package/package.json +7 -2
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.
|
package/bin/uwmd-batch.mjs
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { indexUWMDDirectory, writeUWMDCollectionIndex } from '../dist/index.js';
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
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.
|
|
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": "
|
|
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"
|