@uwmd/core 1.1.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 +79 -0
- package/dist/agents/bancroft.d.ts +32 -0
- package/dist/agents/bancroft.d.ts.map +1 -0
- package/dist/agents/bancroft.js +292 -0
- package/dist/agents/bancroft.js.map +1 -0
- package/dist/agents/schemas.d.ts +13 -0
- package/dist/agents/schemas.d.ts.map +1 -0
- package/dist/agents/schemas.js +74 -0
- package/dist/agents/schemas.js.map +1 -0
- package/dist/bindings.d.ts +85 -0
- package/dist/bindings.d.ts.map +1 -0
- package/dist/bindings.js +293 -0
- package/dist/bindings.js.map +1 -0
- package/dist/browser.d.ts +66 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +53 -0
- package/dist/browser.js.map +1 -0
- package/dist/calc/builtins.d.ts +4 -0
- package/dist/calc/builtins.d.ts.map +1 -0
- package/dist/calc/builtins.js +329 -0
- package/dist/calc/builtins.js.map +1 -0
- package/dist/calc/dependencies.d.ts +34 -0
- package/dist/calc/dependencies.d.ts.map +1 -0
- package/dist/calc/dependencies.js +101 -0
- package/dist/calc/dependencies.js.map +1 -0
- package/dist/calc/errors.d.ts +8 -0
- package/dist/calc/errors.d.ts.map +1 -0
- package/dist/calc/errors.js +18 -0
- package/dist/calc/errors.js.map +1 -0
- package/dist/calc/evaluator.d.ts +5 -0
- package/dist/calc/evaluator.d.ts.map +1 -0
- package/dist/calc/evaluator.js +184 -0
- package/dist/calc/evaluator.js.map +1 -0
- package/dist/calc/index.d.ts +14 -0
- package/dist/calc/index.d.ts.map +1 -0
- package/dist/calc/index.js +56 -0
- package/dist/calc/index.js.map +1 -0
- package/dist/calc/parser.d.ts +32 -0
- package/dist/calc/parser.d.ts.map +1 -0
- package/dist/calc/parser.js +361 -0
- package/dist/calc/parser.js.map +1 -0
- package/dist/cascade.d.ts +85 -0
- package/dist/cascade.d.ts.map +1 -0
- package/dist/cascade.js +172 -0
- package/dist/cascade.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +843 -0
- package/dist/cli.js.map +1 -0
- package/dist/codec.d.ts +25 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +62 -0
- package/dist/codec.js.map +1 -0
- package/dist/codecs.d.ts +6 -0
- package/dist/codecs.d.ts.map +1 -0
- package/dist/codecs.js +12 -0
- package/dist/codecs.js.map +1 -0
- package/dist/compactor.d.ts +11 -0
- package/dist/compactor.d.ts.map +1 -0
- package/dist/compactor.js +84 -0
- package/dist/compactor.js.map +1 -0
- package/dist/context-profiles.d.ts +30 -0
- package/dist/context-profiles.d.ts.map +1 -0
- package/dist/context-profiles.js +225 -0
- package/dist/context-profiles.js.map +1 -0
- package/dist/context.d.ts +55 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +288 -0
- package/dist/context.js.map +1 -0
- package/dist/dcf.d.ts +9 -0
- package/dist/dcf.d.ts.map +1 -0
- package/dist/dcf.js +75 -0
- package/dist/dcf.js.map +1 -0
- package/dist/debt.d.ts +11 -0
- package/dist/debt.d.ts.map +1 -0
- package/dist/debt.js +85 -0
- package/dist/debt.js.map +1 -0
- package/dist/defaults.d.ts +104 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +622 -0
- package/dist/defaults.js.map +1 -0
- package/dist/derived.d.ts +17 -0
- package/dist/derived.d.ts.map +1 -0
- package/dist/derived.js +29 -0
- package/dist/derived.js.map +1 -0
- package/dist/editor.d.ts +76 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +502 -0
- package/dist/editor.js.map +1 -0
- package/dist/envelope.d.ts +51 -0
- package/dist/envelope.d.ts.map +1 -0
- package/dist/envelope.js +213 -0
- package/dist/envelope.js.map +1 -0
- package/dist/format.d.ts +60 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +115 -0
- package/dist/format.js.map +1 -0
- package/dist/gaps.d.ts +62 -0
- package/dist/gaps.d.ts.map +1 -0
- package/dist/gaps.js +272 -0
- package/dist/gaps.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +15 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +169 -0
- package/dist/init.js.map +1 -0
- package/dist/integrity-canonical.d.ts +17 -0
- package/dist/integrity-canonical.d.ts.map +1 -0
- package/dist/integrity-canonical.js +124 -0
- package/dist/integrity-canonical.js.map +1 -0
- package/dist/integrity.d.ts +54 -0
- package/dist/integrity.d.ts.map +1 -0
- package/dist/integrity.js +244 -0
- package/dist/integrity.js.map +1 -0
- package/dist/modules.d.ts +24 -0
- package/dist/modules.d.ts.map +1 -0
- package/dist/modules.js +322 -0
- package/dist/modules.js.map +1 -0
- package/dist/negotiation.d.ts +19 -0
- package/dist/negotiation.d.ts.map +1 -0
- package/dist/negotiation.js +70 -0
- package/dist/negotiation.js.map +1 -0
- package/dist/opstatement.d.ts +9 -0
- package/dist/opstatement.d.ts.map +1 -0
- package/dist/opstatement.js +80 -0
- package/dist/opstatement.js.map +1 -0
- package/dist/packs/excel-emit.d.ts +21 -0
- package/dist/packs/excel-emit.d.ts.map +1 -0
- package/dist/packs/excel-emit.js +135 -0
- package/dist/packs/excel-emit.js.map +1 -0
- package/dist/packs/index.d.ts +13 -0
- package/dist/packs/index.d.ts.map +1 -0
- package/dist/packs/index.js +37 -0
- package/dist/packs/index.js.map +1 -0
- package/dist/packs/industrial.d.ts +3 -0
- package/dist/packs/industrial.d.ts.map +1 -0
- package/dist/packs/industrial.js +115 -0
- package/dist/packs/industrial.js.map +1 -0
- package/dist/packs/multifamily.d.ts +3 -0
- package/dist/packs/multifamily.d.ts.map +1 -0
- package/dist/packs/multifamily.js +82 -0
- package/dist/packs/multifamily.js.map +1 -0
- package/dist/packs/office.d.ts +3 -0
- package/dist/packs/office.d.ts.map +1 -0
- package/dist/packs/office.js +108 -0
- package/dist/packs/office.js.map +1 -0
- package/dist/packs/retail.d.ts +3 -0
- package/dist/packs/retail.d.ts.map +1 -0
- package/dist/packs/retail.js +115 -0
- package/dist/packs/retail.js.map +1 -0
- package/dist/packs/self-storage.d.ts +3 -0
- package/dist/packs/self-storage.d.ts.map +1 -0
- package/dist/packs/self-storage.js +105 -0
- package/dist/packs/self-storage.js.map +1 -0
- package/dist/parser.d.ts +15 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +430 -0
- package/dist/parser.js.map +1 -0
- package/dist/protocol.d.ts +389 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +714 -0
- package/dist/protocol.js.map +1 -0
- package/dist/refinement.d.ts +63 -0
- package/dist/refinement.d.ts.map +1 -0
- package/dist/refinement.js +242 -0
- package/dist/refinement.js.map +1 -0
- package/dist/renderer.d.ts +22 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +478 -0
- package/dist/renderer.js.map +1 -0
- package/dist/rentroll.d.ts +21 -0
- package/dist/rentroll.d.ts.map +1 -0
- package/dist/rentroll.js +256 -0
- package/dist/rentroll.js.map +1 -0
- package/dist/report.d.ts +29 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +924 -0
- package/dist/report.js.map +1 -0
- package/dist/runner.d.ts +43 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +288 -0
- package/dist/runner.js.map +1 -0
- package/dist/sourcesuses.d.ts +14 -0
- package/dist/sourcesuses.d.ts.map +1 -0
- package/dist/sourcesuses.js +63 -0
- package/dist/sourcesuses.js.map +1 -0
- package/dist/types.d.ts +248 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/dist/uwcsv.d.ts +23 -0
- package/dist/uwcsv.d.ts.map +1 -0
- package/dist/uwcsv.js +877 -0
- package/dist/uwcsv.js.map +1 -0
- package/dist/uwjson.d.ts +19 -0
- package/dist/uwjson.d.ts.map +1 -0
- package/dist/uwjson.js +61 -0
- package/dist/uwjson.js.map +1 -0
- package/dist/uwxml.d.ts +18 -0
- package/dist/uwxml.d.ts.map +1 -0
- package/dist/uwxml.js +297 -0
- package/dist/uwxml.js.map +1 -0
- package/dist/validator.d.ts +35 -0
- package/dist/validator.d.ts.map +1 -0
- package/dist/validator.js +553 -0
- package/dist/validator.js.map +1 -0
- package/dist/valuation.d.ts +9 -0
- package/dist/valuation.d.ts.map +1 -0
- package/dist/valuation.js +34 -0
- package/dist/valuation.js.map +1 -0
- package/package.json +58 -0
package/dist/uwcsv.js
ADDED
|
@@ -0,0 +1,877 @@
|
|
|
1
|
+
import { strToU8, unzipSync, zipSync } from 'fflate';
|
|
2
|
+
import { assertUWEnvelope, stampEnvelopeDigest, verifyEnvelopeDigest, } from './envelope.js';
|
|
3
|
+
import { sha256TextHex } from './integrity.js';
|
|
4
|
+
export const UW_CSV_BUNDLE_VERSION = '1.0.0';
|
|
5
|
+
export const UW_CSV_BUNDLE_MEDIA_TYPE = 'application/vnd.uwmd.csv-bundle+zip';
|
|
6
|
+
const REQUIRED_FILES = [
|
|
7
|
+
'document.csv',
|
|
8
|
+
'frontmatter.csv',
|
|
9
|
+
'blocks.csv',
|
|
10
|
+
'block_values.csv',
|
|
11
|
+
'prose.csv',
|
|
12
|
+
];
|
|
13
|
+
const MAX_COMPRESSED_BYTES = 50 * 1024 * 1024;
|
|
14
|
+
const MAX_UNCOMPRESSED_BYTES = 200 * 1024 * 1024;
|
|
15
|
+
const MAX_FILES = 100;
|
|
16
|
+
const MAX_COMPRESSION_RATIO = 100;
|
|
17
|
+
const ZIP_EPOCH = new Date('1980-01-02T00:00:00.000Z');
|
|
18
|
+
export class UWCSVError extends Error {
|
|
19
|
+
code;
|
|
20
|
+
constructor(code, message) {
|
|
21
|
+
super(`[${code}] ${message}`);
|
|
22
|
+
this.name = 'UWCSVError';
|
|
23
|
+
this.code = code;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function encodeUWCSVBundle(envelope) {
|
|
27
|
+
const stamped = await stampEnvelopeDigest(envelope);
|
|
28
|
+
const blocks = enumerateBlocks(stamped);
|
|
29
|
+
const extra = Object.fromEntries(Object.entries(stamped)
|
|
30
|
+
.filter(([key]) => ![
|
|
31
|
+
'envelope_version',
|
|
32
|
+
'format_version',
|
|
33
|
+
'generated_at',
|
|
34
|
+
'generator',
|
|
35
|
+
'semantic_digest',
|
|
36
|
+
'frontmatter',
|
|
37
|
+
'sections',
|
|
38
|
+
'pipeline_log',
|
|
39
|
+
'custom_calculations',
|
|
40
|
+
'custom_scenarios',
|
|
41
|
+
'extensions',
|
|
42
|
+
'superseded',
|
|
43
|
+
].includes(key))
|
|
44
|
+
.sort(([left], [right]) => compareCodeUnits(left, right)));
|
|
45
|
+
const files = {};
|
|
46
|
+
files['document.csv'] = encodeCSV([
|
|
47
|
+
[
|
|
48
|
+
'envelope_version',
|
|
49
|
+
'format_version',
|
|
50
|
+
'generated_at_json',
|
|
51
|
+
'generator_json',
|
|
52
|
+
'semantic_digest',
|
|
53
|
+
'deal_id_json',
|
|
54
|
+
'deal_name_json',
|
|
55
|
+
'extra_json',
|
|
56
|
+
],
|
|
57
|
+
[
|
|
58
|
+
stamped.envelope_version,
|
|
59
|
+
stamped.format_version,
|
|
60
|
+
jsonCell(stamped.generated_at ?? null),
|
|
61
|
+
jsonCell(stamped.generator ?? null),
|
|
62
|
+
stamped.semantic_digest ?? '',
|
|
63
|
+
jsonCell(stamped.frontmatter.deal_id ?? null),
|
|
64
|
+
jsonCell(stamped.frontmatter.deal_name ?? null),
|
|
65
|
+
jsonCell(extra),
|
|
66
|
+
],
|
|
67
|
+
]);
|
|
68
|
+
files['frontmatter.csv'] = encodeFlatValues(flattenJSON(stamped.frontmatter));
|
|
69
|
+
files['blocks.csv'] = encodeCSV([
|
|
70
|
+
['block_ref', 'collection', 'section_json', 'variant_json', 'ordinal', 'state'],
|
|
71
|
+
...blocks.map((entry) => [
|
|
72
|
+
entry.block_ref,
|
|
73
|
+
entry.collection,
|
|
74
|
+
jsonCell(entry.section),
|
|
75
|
+
jsonCell(entry.variant),
|
|
76
|
+
entry.ordinal === null ? '' : String(entry.ordinal),
|
|
77
|
+
entry.state,
|
|
78
|
+
]),
|
|
79
|
+
]);
|
|
80
|
+
const blockValueRows = [
|
|
81
|
+
['block_ref', 'scope', 'pointer', 'json_type', 'value_json'],
|
|
82
|
+
];
|
|
83
|
+
const proseRows = [['prose_ref', 'scope', 'block_ref', 'prose_json']];
|
|
84
|
+
for (const entry of blocks) {
|
|
85
|
+
const content = { ...entry.block.content };
|
|
86
|
+
const meta = isRecord(content['_meta']) ? content['_meta'] : {};
|
|
87
|
+
delete content['_meta'];
|
|
88
|
+
for (const [scope, value] of [
|
|
89
|
+
['annotation', entry.block.annotation],
|
|
90
|
+
['meta', meta],
|
|
91
|
+
['content', content],
|
|
92
|
+
]) {
|
|
93
|
+
for (const row of flattenJSON(value)) {
|
|
94
|
+
blockValueRows.push([
|
|
95
|
+
entry.block_ref,
|
|
96
|
+
scope,
|
|
97
|
+
row.pointer,
|
|
98
|
+
row.json_type,
|
|
99
|
+
row.value_json,
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (entry.block.prose !== undefined) {
|
|
104
|
+
proseRows.push([
|
|
105
|
+
`${entry.block_ref}/prose`,
|
|
106
|
+
'block',
|
|
107
|
+
entry.block_ref,
|
|
108
|
+
jsonCell(entry.block.prose),
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
files['block_values.csv'] = encodeCSV(blockValueRows);
|
|
113
|
+
files['prose.csv'] = encodeCSV(proseRows);
|
|
114
|
+
const views = buildViews(stamped);
|
|
115
|
+
for (const view of views)
|
|
116
|
+
files[view.path] = view.csv;
|
|
117
|
+
const inventory = [];
|
|
118
|
+
for (const path of Object.keys(files).sort()) {
|
|
119
|
+
const view = views.find((item) => item.path === path);
|
|
120
|
+
const text = files[path] ?? '';
|
|
121
|
+
inventory.push({
|
|
122
|
+
path,
|
|
123
|
+
media_type: 'text/csv; charset=utf-8',
|
|
124
|
+
sha256: `sha256:${await sha256TextHex(text)}`,
|
|
125
|
+
bytes: strToU8(text).byteLength,
|
|
126
|
+
fidelity: view ? 'view' : 'model',
|
|
127
|
+
...(view ? { profile: view.profile } : {}),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const manifest = {
|
|
131
|
+
bundle_version: UW_CSV_BUNDLE_VERSION,
|
|
132
|
+
envelope_version: stamped.envelope_version,
|
|
133
|
+
format_version: stamped.format_version,
|
|
134
|
+
semantic_digest: stamped.semantic_digest ?? '',
|
|
135
|
+
files: inventory,
|
|
136
|
+
views: views.map(({ path, profile, sourceSections }) => ({
|
|
137
|
+
path,
|
|
138
|
+
profile,
|
|
139
|
+
fidelity: 'view',
|
|
140
|
+
source_sections: sourceSections,
|
|
141
|
+
column_schema_version: '1.0.0',
|
|
142
|
+
spreadsheet_safety: 'apostrophe-prefix',
|
|
143
|
+
})),
|
|
144
|
+
normalized_value_encoding: 'canonical-json',
|
|
145
|
+
csv_dialect: 'rfc4180',
|
|
146
|
+
};
|
|
147
|
+
files['manifest.json'] = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
148
|
+
return { files };
|
|
149
|
+
}
|
|
150
|
+
export async function decodeUWCSVBundle(bundle, options = {}) {
|
|
151
|
+
const entries = Object.entries(bundle.files);
|
|
152
|
+
const maxFiles = options.maxFiles ?? MAX_FILES;
|
|
153
|
+
if (entries.length > maxFiles) {
|
|
154
|
+
throw new UWCSVError('CSV_FILE_LIMIT', `Bundle contains ${entries.length} files; limit is ${maxFiles}.`);
|
|
155
|
+
}
|
|
156
|
+
let totalBytes = 0;
|
|
157
|
+
for (const [path, text] of entries) {
|
|
158
|
+
assertSafeBundlePath(path);
|
|
159
|
+
totalBytes += strToU8(text).byteLength;
|
|
160
|
+
}
|
|
161
|
+
const maxBytes = options.maxUncompressedBytes ?? MAX_UNCOMPRESSED_BYTES;
|
|
162
|
+
if (totalBytes > maxBytes) {
|
|
163
|
+
throw new UWCSVError('CSV_SIZE_LIMIT', `Bundle expands to ${totalBytes} bytes; limit is ${maxBytes}.`);
|
|
164
|
+
}
|
|
165
|
+
const manifestText = bundle.files['manifest.json'];
|
|
166
|
+
if (!manifestText)
|
|
167
|
+
throw new UWCSVError('CSV_MANIFEST_MISSING', 'manifest.json is required.');
|
|
168
|
+
const manifest = parseManifest(manifestText);
|
|
169
|
+
const listed = new Set();
|
|
170
|
+
for (const file of manifest.files) {
|
|
171
|
+
if (listed.has(file.path))
|
|
172
|
+
throw new UWCSVError('CSV_MANIFEST_DUPLICATE', `Manifest repeats ${file.path}.`);
|
|
173
|
+
listed.add(file.path);
|
|
174
|
+
const text = bundle.files[file.path];
|
|
175
|
+
if (text === undefined)
|
|
176
|
+
throw new UWCSVError('CSV_FILE_MISSING', `${file.path} is listed but missing.`);
|
|
177
|
+
if (strToU8(text).byteLength !== file.bytes) {
|
|
178
|
+
throw new UWCSVError('CSV_SIZE_MISMATCH', `${file.path} byte length does not match manifest.`);
|
|
179
|
+
}
|
|
180
|
+
const digest = `sha256:${await sha256TextHex(text)}`;
|
|
181
|
+
if (digest !== file.sha256)
|
|
182
|
+
throw new UWCSVError('CSV_HASH_MISMATCH', `${file.path} hash does not match manifest.`);
|
|
183
|
+
}
|
|
184
|
+
for (const path of Object.keys(bundle.files)) {
|
|
185
|
+
if (path !== 'manifest.json' && !listed.has(path)) {
|
|
186
|
+
throw new UWCSVError('CSV_FILE_UNLISTED', `${path} is not listed in manifest.`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const required of REQUIRED_FILES) {
|
|
190
|
+
if (!listed.has(required))
|
|
191
|
+
throw new UWCSVError('CSV_FILE_MISSING', `${required} is required.`);
|
|
192
|
+
}
|
|
193
|
+
const documentRows = decodeCSV(requiredFile(bundle, 'document.csv'));
|
|
194
|
+
assertHeader(documentRows, [
|
|
195
|
+
'envelope_version',
|
|
196
|
+
'format_version',
|
|
197
|
+
'generated_at_json',
|
|
198
|
+
'generator_json',
|
|
199
|
+
'semantic_digest',
|
|
200
|
+
'deal_id_json',
|
|
201
|
+
'deal_name_json',
|
|
202
|
+
'extra_json',
|
|
203
|
+
], 'document.csv');
|
|
204
|
+
if (documentRows.length !== 2)
|
|
205
|
+
throw new UWCSVError('CSV_DOCUMENT_ROWS', 'document.csv must contain one data row.');
|
|
206
|
+
const document = documentRows[1] ?? [];
|
|
207
|
+
const extras = parseJSONCell(document[7] ?? '', 'document.csv.extra_json');
|
|
208
|
+
if (!isRecord(extras))
|
|
209
|
+
throw new UWCSVError('CSV_DOCUMENT_EXTRA', 'extra_json must be an object.');
|
|
210
|
+
const envelope = Object.assign(Object.create(null), extras, {
|
|
211
|
+
envelope_version: document[0],
|
|
212
|
+
format_version: document[1],
|
|
213
|
+
generated_at: parseNullableJSONCell(document[2] ?? '', 'document.csv.generated_at_json'),
|
|
214
|
+
generator: parseNullableJSONCell(document[3] ?? '', 'document.csv.generator_json'),
|
|
215
|
+
semantic_digest: document[4],
|
|
216
|
+
frontmatter: decodeFlatValues(requiredFile(bundle, 'frontmatter.csv')),
|
|
217
|
+
sections: Object.create(null),
|
|
218
|
+
pipeline_log: [],
|
|
219
|
+
custom_calculations: [],
|
|
220
|
+
custom_scenarios: [],
|
|
221
|
+
extensions: Object.create(null),
|
|
222
|
+
superseded: Object.create(null),
|
|
223
|
+
});
|
|
224
|
+
if (envelope.generated_at === null)
|
|
225
|
+
delete envelope.generated_at;
|
|
226
|
+
if (envelope.generator === null)
|
|
227
|
+
delete envelope.generator;
|
|
228
|
+
const frontmatter = envelope.frontmatter;
|
|
229
|
+
if (parseNullableJSONCell(document[5] ?? '', 'document.csv.deal_id_json') !== (frontmatter['deal_id'] ?? null)) {
|
|
230
|
+
throw new UWCSVError('CSV_DOCUMENT_IDENTITY', 'document.csv deal_id disagrees with frontmatter.csv.');
|
|
231
|
+
}
|
|
232
|
+
if (parseNullableJSONCell(document[6] ?? '', 'document.csv.deal_name_json') !== (frontmatter['deal_name'] ?? null)) {
|
|
233
|
+
throw new UWCSVError('CSV_DOCUMENT_IDENTITY', 'document.csv deal_name disagrees with frontmatter.csv.');
|
|
234
|
+
}
|
|
235
|
+
if (manifest.envelope_version !== envelope.envelope_version || manifest.format_version !== envelope.format_version) {
|
|
236
|
+
throw new UWCSVError('CSV_MANIFEST_VERSION', 'Manifest versions disagree with document.csv.');
|
|
237
|
+
}
|
|
238
|
+
const blockRows = decodeCSV(requiredFile(bundle, 'blocks.csv'));
|
|
239
|
+
assertHeader(blockRows, ['block_ref', 'collection', 'section_json', 'variant_json', 'ordinal', 'state'], 'blocks.csv');
|
|
240
|
+
const valueRows = decodeCSV(requiredFile(bundle, 'block_values.csv'));
|
|
241
|
+
assertHeader(valueRows, ['block_ref', 'scope', 'pointer', 'json_type', 'value_json'], 'block_values.csv');
|
|
242
|
+
const proseRows = decodeCSV(requiredFile(bundle, 'prose.csv'));
|
|
243
|
+
assertHeader(proseRows, ['prose_ref', 'scope', 'block_ref', 'prose_json'], 'prose.csv');
|
|
244
|
+
const valuesByBlock = new Map();
|
|
245
|
+
for (const row of valueRows.slice(1)) {
|
|
246
|
+
if (row.length !== 5)
|
|
247
|
+
throw new UWCSVError('CSV_ROW_WIDTH', 'block_values.csv row width is invalid.');
|
|
248
|
+
const [blockRef = '', scope = '', pointer = '', type = '', valueJson = ''] = row;
|
|
249
|
+
if (!['annotation', 'meta', 'content'].includes(scope)) {
|
|
250
|
+
throw new UWCSVError('CSV_SCOPE_INVALID', `Unknown block value scope ${scope}.`);
|
|
251
|
+
}
|
|
252
|
+
const scopes = valuesByBlock.get(blockRef) ?? new Map();
|
|
253
|
+
const rows = scopes.get(scope) ?? [];
|
|
254
|
+
rows.push({ pointer, json_type: assertJSONType(type), value_json: valueJson });
|
|
255
|
+
scopes.set(scope, rows);
|
|
256
|
+
valuesByBlock.set(blockRef, scopes);
|
|
257
|
+
}
|
|
258
|
+
const proseByBlock = new Map();
|
|
259
|
+
for (const row of proseRows.slice(1)) {
|
|
260
|
+
if (row.length !== 4 || row[1] !== 'block')
|
|
261
|
+
throw new UWCSVError('CSV_PROSE_ROW', 'prose.csv row is invalid.');
|
|
262
|
+
const blockRef = row[2] ?? '';
|
|
263
|
+
if (row[0] !== `${blockRef}/prose`)
|
|
264
|
+
throw new UWCSVError('CSV_PROSE_REF', `${row[0]} does not match ${blockRef}.`);
|
|
265
|
+
if (proseByBlock.has(blockRef))
|
|
266
|
+
throw new UWCSVError('CSV_PROSE_DUPLICATE', `${blockRef} repeats prose.`);
|
|
267
|
+
const prose = parseJSONCell(row[3] ?? '', 'prose.csv.prose_json');
|
|
268
|
+
if (typeof prose !== 'string')
|
|
269
|
+
throw new UWCSVError('CSV_PROSE_TYPE', 'prose_json must contain a string.');
|
|
270
|
+
proseByBlock.set(blockRef, prose);
|
|
271
|
+
}
|
|
272
|
+
const seenBlocks = new Set();
|
|
273
|
+
for (const row of blockRows.slice(1)) {
|
|
274
|
+
if (row.length !== 6)
|
|
275
|
+
throw new UWCSVError('CSV_ROW_WIDTH', 'blocks.csv row width is invalid.');
|
|
276
|
+
const blockRef = row[0] ?? '';
|
|
277
|
+
if (seenBlocks.has(blockRef))
|
|
278
|
+
throw new UWCSVError('CSV_BLOCK_DUPLICATE', `Duplicate ${blockRef}.`);
|
|
279
|
+
seenBlocks.add(blockRef);
|
|
280
|
+
validateBlockDescriptor(row);
|
|
281
|
+
const scopes = valuesByBlock.get(blockRef);
|
|
282
|
+
if (!scopes)
|
|
283
|
+
throw new UWCSVError('CSV_BLOCK_VALUES_MISSING', `${blockRef} has no value rows.`);
|
|
284
|
+
const annotation = buildJSON(scopes.get('annotation') ?? []);
|
|
285
|
+
const meta = buildJSON(scopes.get('meta') ?? []);
|
|
286
|
+
const content = buildJSON(scopes.get('content') ?? []);
|
|
287
|
+
if (!isRecord(annotation) || !isRecord(meta) || !isRecord(content)) {
|
|
288
|
+
throw new UWCSVError('CSV_BLOCK_SHAPE', `${blockRef} scopes must decode as objects.`);
|
|
289
|
+
}
|
|
290
|
+
const block = {
|
|
291
|
+
annotation: annotation,
|
|
292
|
+
content: { ...content, _meta: meta },
|
|
293
|
+
...(proseByBlock.has(blockRef) ? { prose: proseByBlock.get(blockRef) } : {}),
|
|
294
|
+
};
|
|
295
|
+
assignBlock(envelope, row, block);
|
|
296
|
+
}
|
|
297
|
+
for (const blockRef of valuesByBlock.keys()) {
|
|
298
|
+
if (!seenBlocks.has(blockRef))
|
|
299
|
+
throw new UWCSVError('CSV_BLOCK_UNLISTED', `${blockRef} has values but no block row.`);
|
|
300
|
+
}
|
|
301
|
+
for (const blockRef of proseByBlock.keys()) {
|
|
302
|
+
if (!seenBlocks.has(blockRef))
|
|
303
|
+
throw new UWCSVError('CSV_BLOCK_UNLISTED', `${blockRef} has prose but no block row.`);
|
|
304
|
+
}
|
|
305
|
+
assertUWEnvelope(envelope);
|
|
306
|
+
const verification = await verifyEnvelopeDigest(envelope);
|
|
307
|
+
if (!verification.actual)
|
|
308
|
+
throw new UWCSVError('CSV_DIGEST_MISSING', 'semantic_digest is required.');
|
|
309
|
+
if (!verification.valid || manifest.semantic_digest !== verification.actual) {
|
|
310
|
+
throw new UWCSVError('CSV_DIGEST_MISMATCH', 'Reconstructed envelope digest does not match the bundle manifest.');
|
|
311
|
+
}
|
|
312
|
+
return envelope;
|
|
313
|
+
}
|
|
314
|
+
export async function encodeUWCSVZip(envelope) {
|
|
315
|
+
const bundle = await encodeUWCSVBundle(envelope);
|
|
316
|
+
const input = {};
|
|
317
|
+
for (const path of Object.keys(bundle.files).sort()) {
|
|
318
|
+
input[path] = [strToU8(bundle.files[path] ?? ''), { level: 6, mtime: ZIP_EPOCH }];
|
|
319
|
+
}
|
|
320
|
+
return zipSync(input);
|
|
321
|
+
}
|
|
322
|
+
export async function decodeUWCSVZip(input, options = {}) {
|
|
323
|
+
inspectZip(input, options);
|
|
324
|
+
let inflated;
|
|
325
|
+
try {
|
|
326
|
+
inflated = unzipSync(input);
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
throw new UWCSVError('CSV_ZIP_INVALID', `Could not extract ZIP: ${String(error)}`);
|
|
330
|
+
}
|
|
331
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
332
|
+
const files = {};
|
|
333
|
+
try {
|
|
334
|
+
for (const path of Object.keys(inflated).sort()) {
|
|
335
|
+
assertSafeBundlePath(path);
|
|
336
|
+
files[path] = decoder.decode(inflated[path]);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
if (error instanceof UWCSVError)
|
|
341
|
+
throw error;
|
|
342
|
+
throw new UWCSVError('CSV_UTF8_INVALID', `Bundle files must be UTF-8: ${String(error)}`);
|
|
343
|
+
}
|
|
344
|
+
return decodeUWCSVBundle({ files }, options);
|
|
345
|
+
}
|
|
346
|
+
export const UW_CSV_BUNDLE_CODEC = {
|
|
347
|
+
descriptor: {
|
|
348
|
+
id: 'uw-csv-bundle',
|
|
349
|
+
media_types: [UW_CSV_BUNDLE_MEDIA_TYPE],
|
|
350
|
+
file_extensions: ['.uw.csv.zip'],
|
|
351
|
+
directions: ['read', 'write'],
|
|
352
|
+
fidelity: 'model',
|
|
353
|
+
representation_version: UW_CSV_BUNDLE_VERSION,
|
|
354
|
+
max_bytes: MAX_COMPRESSED_BYTES,
|
|
355
|
+
},
|
|
356
|
+
encode: encodeUWCSVZip,
|
|
357
|
+
decode: decodeUWCSVZip,
|
|
358
|
+
};
|
|
359
|
+
function enumerateBlocks(envelope) {
|
|
360
|
+
const output = [];
|
|
361
|
+
for (const section of Object.keys(envelope.sections).sort()) {
|
|
362
|
+
const entry = envelope.sections[section];
|
|
363
|
+
if (isEnvelopeBlock(entry)) {
|
|
364
|
+
output.push({ block_ref: `/sections/${pointerEscape(section)}`, collection: 'sections', section, variant: null, ordinal: null, state: 'current', block: entry });
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
for (const variant of Object.keys(entry ?? {}).sort()) {
|
|
368
|
+
const block = entry?.[variant];
|
|
369
|
+
if (!isEnvelopeBlock(block))
|
|
370
|
+
throw new UWCSVError('CSV_BLOCK_SHAPE', `sections.${section}.${variant} is not a block.`);
|
|
371
|
+
output.push({ block_ref: `/sections/${pointerEscape(section)}/${pointerEscape(variant)}`, collection: 'sections', section, variant, ordinal: null, state: 'current', block });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
for (const collection of ['pipeline_log', 'custom_calculations', 'custom_scenarios']) {
|
|
376
|
+
envelope[collection].forEach((block, ordinal) => output.push({ block_ref: `/${collection}/${ordinal}`, collection, section: null, variant: null, ordinal, state: 'current', block }));
|
|
377
|
+
}
|
|
378
|
+
for (const id of Object.keys(envelope.extensions).sort()) {
|
|
379
|
+
output.push({ block_ref: `/extensions/${pointerEscape(id)}`, collection: 'extensions', section: id, variant: null, ordinal: null, state: 'current', block: envelope.extensions[id] });
|
|
380
|
+
}
|
|
381
|
+
for (const section of Object.keys(envelope.superseded).sort()) {
|
|
382
|
+
envelope.superseded[section]?.forEach((block, ordinal) => output.push({ block_ref: `/superseded/${pointerEscape(section)}/${ordinal}`, collection: 'superseded', section, variant: null, ordinal, state: 'superseded', block }));
|
|
383
|
+
}
|
|
384
|
+
return output;
|
|
385
|
+
}
|
|
386
|
+
function flattenJSON(value, pointer = '') {
|
|
387
|
+
const type = jsonType(value);
|
|
388
|
+
const row = {
|
|
389
|
+
pointer,
|
|
390
|
+
json_type: type,
|
|
391
|
+
value_json: type === 'object' || type === 'array' ? '' : jsonCell(value),
|
|
392
|
+
};
|
|
393
|
+
if (Array.isArray(value)) {
|
|
394
|
+
return [row, ...value.flatMap((item, index) => flattenJSON(item, `${pointer}/${index}`))];
|
|
395
|
+
}
|
|
396
|
+
if (isRecord(value)) {
|
|
397
|
+
return [row, ...Object.keys(value).sort().flatMap((key) => flattenJSON(value[key], `${pointer}/${pointerEscape(key)}`))];
|
|
398
|
+
}
|
|
399
|
+
return [row];
|
|
400
|
+
}
|
|
401
|
+
function encodeFlatValues(rows) {
|
|
402
|
+
return encodeCSV([
|
|
403
|
+
['pointer', 'json_type', 'value_json'],
|
|
404
|
+
...rows.map((row) => [row.pointer, row.json_type, row.value_json]),
|
|
405
|
+
]);
|
|
406
|
+
}
|
|
407
|
+
function decodeFlatValues(text) {
|
|
408
|
+
const rows = decodeCSV(text);
|
|
409
|
+
assertHeader(rows, ['pointer', 'json_type', 'value_json'], 'flat value table');
|
|
410
|
+
return buildJSON(rows.slice(1).map((row) => {
|
|
411
|
+
if (row.length !== 3)
|
|
412
|
+
throw new UWCSVError('CSV_ROW_WIDTH', 'Flat value row width is invalid.');
|
|
413
|
+
return { pointer: row[0] ?? '', json_type: assertJSONType(row[1] ?? ''), value_json: row[2] ?? '' };
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
416
|
+
function buildJSON(rows) {
|
|
417
|
+
const sorted = [...rows].sort((left, right) => comparePointers(left.pointer, right.pointer));
|
|
418
|
+
if (sorted.length === 0 || sorted[0]?.pointer !== '')
|
|
419
|
+
throw new UWCSVError('CSV_ROOT_MISSING', 'Flat values require a root row.');
|
|
420
|
+
const seen = new Set();
|
|
421
|
+
let root;
|
|
422
|
+
for (const row of sorted) {
|
|
423
|
+
if (seen.has(row.pointer))
|
|
424
|
+
throw new UWCSVError('CSV_POINTER_DUPLICATE', `Duplicate pointer ${row.pointer}.`);
|
|
425
|
+
seen.add(row.pointer);
|
|
426
|
+
const value = row.json_type === 'object' ? Object.create(null) : row.json_type === 'array' ? [] : parseJSONCell(row.value_json, row.pointer || '/');
|
|
427
|
+
if (jsonType(value) !== row.json_type)
|
|
428
|
+
throw new UWCSVError('CSV_TYPE_MISMATCH', `${row.pointer} type does not match value_json.`);
|
|
429
|
+
if (row.pointer === '')
|
|
430
|
+
root = value;
|
|
431
|
+
else
|
|
432
|
+
setJSONPointer(root, row.pointer, value);
|
|
433
|
+
}
|
|
434
|
+
return root;
|
|
435
|
+
}
|
|
436
|
+
function encodeCSV(rows) {
|
|
437
|
+
return `${rows.map((row) => row.map((cell) => `"${cell.replaceAll('"', '""')}"`).join(',')).join('\r\n')}\r\n`;
|
|
438
|
+
}
|
|
439
|
+
function decodeCSV(text) {
|
|
440
|
+
const rows = [];
|
|
441
|
+
let row = [];
|
|
442
|
+
let cell = '';
|
|
443
|
+
let index = 0;
|
|
444
|
+
let quoted = false;
|
|
445
|
+
while (index < text.length) {
|
|
446
|
+
const char = text[index];
|
|
447
|
+
if (quoted) {
|
|
448
|
+
if (char === '"') {
|
|
449
|
+
if (text[index + 1] === '"') {
|
|
450
|
+
cell += '"';
|
|
451
|
+
index += 2;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
quoted = false;
|
|
455
|
+
index++;
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
cell += char;
|
|
459
|
+
index++;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (char === '"' && cell.length === 0) {
|
|
463
|
+
quoted = true;
|
|
464
|
+
index++;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (char === ',') {
|
|
468
|
+
row.push(cell);
|
|
469
|
+
cell = '';
|
|
470
|
+
index++;
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (char === '\r' && text[index + 1] === '\n') {
|
|
474
|
+
row.push(cell);
|
|
475
|
+
rows.push(row);
|
|
476
|
+
row = [];
|
|
477
|
+
cell = '';
|
|
478
|
+
index += 2;
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (char === '\n') {
|
|
482
|
+
row.push(cell);
|
|
483
|
+
rows.push(row);
|
|
484
|
+
row = [];
|
|
485
|
+
cell = '';
|
|
486
|
+
index++;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
cell += char;
|
|
490
|
+
index++;
|
|
491
|
+
}
|
|
492
|
+
if (quoted)
|
|
493
|
+
throw new UWCSVError('CSV_QUOTE_INVALID', 'CSV ends inside a quoted field.');
|
|
494
|
+
if (cell.length > 0 || row.length > 0) {
|
|
495
|
+
row.push(cell);
|
|
496
|
+
rows.push(row);
|
|
497
|
+
}
|
|
498
|
+
return rows;
|
|
499
|
+
}
|
|
500
|
+
function buildViews(envelope) {
|
|
501
|
+
const views = [];
|
|
502
|
+
views.push({ path: 'views/deal_summary.csv', profile: 'deal_summary', sourceSections: ['frontmatter'], csv: encodeWideRows([flattenViewObject(envelope.frontmatter)]) });
|
|
503
|
+
for (const [profile, section, preferredArray] of [
|
|
504
|
+
['rent_roll', 'rent_roll', 'units'],
|
|
505
|
+
['operating_statement', 'noi_model', null],
|
|
506
|
+
['debt', 'debt_structure', null],
|
|
507
|
+
['valuation', 'valuation', null],
|
|
508
|
+
['sources_uses', 'sources_uses', null],
|
|
509
|
+
]) {
|
|
510
|
+
const block = currentSectionBlock(envelope, section);
|
|
511
|
+
if (!block)
|
|
512
|
+
continue;
|
|
513
|
+
const content = Object.fromEntries(Object.entries(block.content).filter(([key]) => key !== '_meta'));
|
|
514
|
+
let rows;
|
|
515
|
+
const list = preferredArray ? content[preferredArray] : null;
|
|
516
|
+
if (Array.isArray(list) && list.length > 0) {
|
|
517
|
+
rows = list.map((item, rowIndex) => ({ row_index: rowIndex, ...flattenViewObject(item) }));
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
rows = [flattenViewObject(content)];
|
|
521
|
+
}
|
|
522
|
+
views.push({ path: `views/${profile}.csv`, profile, sourceSections: [section], csv: encodeWideRows(rows) });
|
|
523
|
+
}
|
|
524
|
+
return views;
|
|
525
|
+
}
|
|
526
|
+
function encodeWideRows(rows) {
|
|
527
|
+
const headers = [...new Set(rows.flatMap((row) => Object.keys(row)))].sort();
|
|
528
|
+
return encodeCSV([
|
|
529
|
+
headers,
|
|
530
|
+
...rows.map((row) => headers.map((header) => safeViewCell(displayCell(row[header])))),
|
|
531
|
+
]);
|
|
532
|
+
}
|
|
533
|
+
function flattenViewObject(value, prefix = '', output = {}) {
|
|
534
|
+
if (isRecord(value)) {
|
|
535
|
+
for (const key of Object.keys(value).sort()) {
|
|
536
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
537
|
+
const child = value[key];
|
|
538
|
+
if (isRecord(child))
|
|
539
|
+
flattenViewObject(child, path, output);
|
|
540
|
+
else
|
|
541
|
+
output[path] = child;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
else if (prefix)
|
|
545
|
+
output[prefix] = value;
|
|
546
|
+
return output;
|
|
547
|
+
}
|
|
548
|
+
function safeViewCell(value) {
|
|
549
|
+
return /^[\t\r\n ]*[=+\-@]/.test(value) ? `'${value}` : value;
|
|
550
|
+
}
|
|
551
|
+
function displayCell(value) {
|
|
552
|
+
if (value === null || value === undefined)
|
|
553
|
+
return '';
|
|
554
|
+
if (typeof value === 'string')
|
|
555
|
+
return value;
|
|
556
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
557
|
+
return String(value);
|
|
558
|
+
return JSON.stringify(value);
|
|
559
|
+
}
|
|
560
|
+
function currentSectionBlock(envelope, section) {
|
|
561
|
+
const entry = envelope.sections[section];
|
|
562
|
+
if (isEnvelopeBlock(entry))
|
|
563
|
+
return entry;
|
|
564
|
+
if (isRecord(entry))
|
|
565
|
+
return Object.values(entry).find(isEnvelopeBlock) ?? null;
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
function inspectZip(input, options) {
|
|
569
|
+
const maxCompressed = options.maxCompressedBytes ?? MAX_COMPRESSED_BYTES;
|
|
570
|
+
if (input.byteLength > maxCompressed)
|
|
571
|
+
throw new UWCSVError('CSV_ZIP_SIZE_LIMIT', `ZIP is ${input.byteLength} bytes; limit is ${maxCompressed}.`);
|
|
572
|
+
const view = new DataView(input.buffer, input.byteOffset, input.byteLength);
|
|
573
|
+
let eocd = -1;
|
|
574
|
+
for (let index = input.byteLength - 22; index >= Math.max(0, input.byteLength - 65_557); index--) {
|
|
575
|
+
if (view.getUint32(index, true) === 0x06054b50) {
|
|
576
|
+
eocd = index;
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (eocd < 0)
|
|
581
|
+
throw new UWCSVError('CSV_ZIP_INVALID', 'ZIP end record is missing.');
|
|
582
|
+
const archiveCommentLength = view.getUint16(eocd + 20, true);
|
|
583
|
+
if (eocd + 22 + archiveCommentLength !== input.byteLength) {
|
|
584
|
+
throw new UWCSVError('CSV_ZIP_INVALID', 'ZIP end record has an invalid comment length or trailing data.');
|
|
585
|
+
}
|
|
586
|
+
const disk = view.getUint16(eocd + 4, true);
|
|
587
|
+
const centralDisk = view.getUint16(eocd + 6, true);
|
|
588
|
+
const diskCount = view.getUint16(eocd + 8, true);
|
|
589
|
+
const count = view.getUint16(eocd + 10, true);
|
|
590
|
+
if (disk !== 0 || centralDisk !== 0 || diskCount !== count) {
|
|
591
|
+
throw new UWCSVError('CSV_ZIP_MULTIDISK', 'Multi-disk ZIP archives are forbidden.');
|
|
592
|
+
}
|
|
593
|
+
const centralSize = view.getUint32(eocd + 12, true);
|
|
594
|
+
const centralOffset = view.getUint32(eocd + 16, true);
|
|
595
|
+
const maxFiles = options.maxFiles ?? MAX_FILES;
|
|
596
|
+
if (count > maxFiles)
|
|
597
|
+
throw new UWCSVError('CSV_FILE_LIMIT', `ZIP contains ${count} files; limit is ${maxFiles}.`);
|
|
598
|
+
if (centralOffset + centralSize > eocd)
|
|
599
|
+
throw new UWCSVError('CSV_ZIP_INVALID', 'ZIP central directory is outside the archive.');
|
|
600
|
+
let offset = centralOffset;
|
|
601
|
+
let total = 0;
|
|
602
|
+
const names = new Set();
|
|
603
|
+
const maxExpanded = options.maxUncompressedBytes ?? MAX_UNCOMPRESSED_BYTES;
|
|
604
|
+
const maxRatio = options.maxCompressionRatio ?? MAX_COMPRESSION_RATIO;
|
|
605
|
+
for (let index = 0; index < count; index++) {
|
|
606
|
+
if (offset + 46 > input.byteLength || view.getUint32(offset, true) !== 0x02014b50)
|
|
607
|
+
throw new UWCSVError('CSV_ZIP_INVALID', 'ZIP central entry is invalid.');
|
|
608
|
+
const flags = view.getUint16(offset + 8, true);
|
|
609
|
+
const compressed = view.getUint32(offset + 20, true);
|
|
610
|
+
const expanded = view.getUint32(offset + 24, true);
|
|
611
|
+
const nameLength = view.getUint16(offset + 28, true);
|
|
612
|
+
const extraLength = view.getUint16(offset + 30, true);
|
|
613
|
+
const commentLength = view.getUint16(offset + 32, true);
|
|
614
|
+
const external = view.getUint32(offset + 38, true);
|
|
615
|
+
if ((flags & 1) !== 0)
|
|
616
|
+
throw new UWCSVError('CSV_ZIP_ENCRYPTED', 'Encrypted ZIP entries are forbidden.');
|
|
617
|
+
if (compressed === 0xffffffff || expanded === 0xffffffff)
|
|
618
|
+
throw new UWCSVError('CSV_ZIP64_UNSUPPORTED', 'ZIP64 entries are not supported.');
|
|
619
|
+
const nameStart = offset + 46;
|
|
620
|
+
const nameEnd = nameStart + nameLength;
|
|
621
|
+
const entryEnd = nameEnd + extraLength + commentLength;
|
|
622
|
+
if (entryEnd > centralOffset + centralSize)
|
|
623
|
+
throw new UWCSVError('CSV_ZIP_INVALID', 'ZIP central entry is truncated.');
|
|
624
|
+
let name;
|
|
625
|
+
try {
|
|
626
|
+
name = new TextDecoder('utf-8', { fatal: true }).decode(input.subarray(nameStart, nameEnd));
|
|
627
|
+
}
|
|
628
|
+
catch (error) {
|
|
629
|
+
throw new UWCSVError('CSV_ZIP_FILENAME_UTF8', `ZIP filename is not UTF-8: ${String(error)}.`);
|
|
630
|
+
}
|
|
631
|
+
assertSafeBundlePath(name);
|
|
632
|
+
if (names.has(name))
|
|
633
|
+
throw new UWCSVError('CSV_ZIP_DUPLICATE', `ZIP repeats ${name}.`);
|
|
634
|
+
names.add(name);
|
|
635
|
+
const unixMode = external >>> 16;
|
|
636
|
+
if ((unixMode & 0xf000) === 0xa000)
|
|
637
|
+
throw new UWCSVError('CSV_ZIP_SYMLINK', 'ZIP symlinks are forbidden.');
|
|
638
|
+
total += expanded;
|
|
639
|
+
if (total > maxExpanded)
|
|
640
|
+
throw new UWCSVError('CSV_SIZE_LIMIT', `ZIP expands beyond ${maxExpanded} bytes.`);
|
|
641
|
+
if (expanded > compressed * maxRatio + 1_048_576)
|
|
642
|
+
throw new UWCSVError('CSV_ZIP_RATIO_LIMIT', `${name} exceeds compression ratio ${maxRatio}.`);
|
|
643
|
+
offset = entryEnd;
|
|
644
|
+
}
|
|
645
|
+
if (offset !== centralOffset + centralSize)
|
|
646
|
+
throw new UWCSVError('CSV_ZIP_INVALID', 'ZIP central directory size is inconsistent.');
|
|
647
|
+
}
|
|
648
|
+
function parseManifest(text) {
|
|
649
|
+
let value;
|
|
650
|
+
try {
|
|
651
|
+
value = JSON.parse(text);
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
throw new UWCSVError('CSV_MANIFEST_INVALID', `manifest.json is invalid: ${String(error)}`);
|
|
655
|
+
}
|
|
656
|
+
if (!isRecord(value) || value['bundle_version'] !== UW_CSV_BUNDLE_VERSION || !Array.isArray(value['files']) || !Array.isArray(value['views']) || typeof value['semantic_digest'] !== 'string') {
|
|
657
|
+
throw new UWCSVError('CSV_MANIFEST_INVALID', 'manifest.json has an unsupported shape or version.');
|
|
658
|
+
}
|
|
659
|
+
for (const file of value['files']) {
|
|
660
|
+
if (!isRecord(file) ||
|
|
661
|
+
typeof file['path'] !== 'string' ||
|
|
662
|
+
typeof file['sha256'] !== 'string' ||
|
|
663
|
+
!/^sha256:[0-9a-f]{64}$/.test(file['sha256']) ||
|
|
664
|
+
typeof file['bytes'] !== 'number' ||
|
|
665
|
+
!Number.isSafeInteger(file['bytes']) ||
|
|
666
|
+
file['bytes'] < 0 ||
|
|
667
|
+
(file['fidelity'] !== 'model' && file['fidelity'] !== 'view')) {
|
|
668
|
+
throw new UWCSVError('CSV_MANIFEST_INVALID', 'Manifest file entry is invalid.');
|
|
669
|
+
}
|
|
670
|
+
assertSafeBundlePath(file['path']);
|
|
671
|
+
}
|
|
672
|
+
if (value['normalized_value_encoding'] !== 'canonical-json' || value['csv_dialect'] !== 'rfc4180') {
|
|
673
|
+
throw new UWCSVError('CSV_MANIFEST_INVALID', 'Manifest encoding or CSV dialect is unsupported.');
|
|
674
|
+
}
|
|
675
|
+
return value;
|
|
676
|
+
}
|
|
677
|
+
function requiredFile(bundle, path) {
|
|
678
|
+
const text = bundle.files[path];
|
|
679
|
+
if (text === undefined)
|
|
680
|
+
throw new UWCSVError('CSV_FILE_MISSING', `${path} is required.`);
|
|
681
|
+
return text;
|
|
682
|
+
}
|
|
683
|
+
function validateBlockDescriptor(row) {
|
|
684
|
+
const [blockRef = '', collection = '', sectionJson = '', variantJson = '', ordinal = '', state = ''] = row;
|
|
685
|
+
if (!blockRef.startsWith('/') || !['sections', 'pipeline_log', 'custom_calculations', 'custom_scenarios', 'extensions', 'superseded'].includes(collection))
|
|
686
|
+
throw new UWCSVError('CSV_BLOCK_DESCRIPTOR', `${blockRef} has an invalid collection.`);
|
|
687
|
+
parseNullableJSONCell(sectionJson, `${blockRef}.section`);
|
|
688
|
+
parseNullableJSONCell(variantJson, `${blockRef}.variant`);
|
|
689
|
+
if (ordinal !== '' && !/^\d+$/.test(ordinal))
|
|
690
|
+
throw new UWCSVError('CSV_BLOCK_DESCRIPTOR', `${blockRef} ordinal is invalid.`);
|
|
691
|
+
if (state !== 'current' && state !== 'superseded')
|
|
692
|
+
throw new UWCSVError('CSV_BLOCK_DESCRIPTOR', `${blockRef} state is invalid.`);
|
|
693
|
+
}
|
|
694
|
+
function assignBlock(envelope, row, block) {
|
|
695
|
+
const [blockRef = '', collection = '', sectionJson = '', variantJson = '', ordinalText = '', state = ''] = row;
|
|
696
|
+
const section = parseNullableJSONCell(sectionJson, `${blockRef}.section`);
|
|
697
|
+
const variant = parseNullableJSONCell(variantJson, `${blockRef}.variant`);
|
|
698
|
+
if (section !== null && typeof section !== 'string')
|
|
699
|
+
throw new UWCSVError('CSV_BLOCK_DESCRIPTOR', `${blockRef} section is invalid.`);
|
|
700
|
+
if (variant !== null && typeof variant !== 'string')
|
|
701
|
+
throw new UWCSVError('CSV_BLOCK_DESCRIPTOR', `${blockRef} variant is invalid.`);
|
|
702
|
+
const ordinal = ordinalText === '' ? null : Number(ordinalText);
|
|
703
|
+
let expected;
|
|
704
|
+
if (collection === 'sections') {
|
|
705
|
+
if (typeof section !== 'string' || ordinal !== null || state !== 'current')
|
|
706
|
+
throw new UWCSVError('CSV_BLOCK_DESCRIPTOR', `${blockRef} section descriptor is invalid.`);
|
|
707
|
+
const sections = envelope['sections'];
|
|
708
|
+
if (variant === null) {
|
|
709
|
+
expected = `/sections/${pointerEscape(section)}`;
|
|
710
|
+
if (section in sections)
|
|
711
|
+
throw new UWCSVError('CSV_BLOCK_DUPLICATE', `${expected} already exists.`);
|
|
712
|
+
sections[section] = block;
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
expected = `/sections/${pointerEscape(section)}/${pointerEscape(variant)}`;
|
|
716
|
+
const variants = sections[section] ?? Object.create(null);
|
|
717
|
+
if (!isRecord(variants) || isEnvelopeBlock(variants))
|
|
718
|
+
throw new UWCSVError('CSV_BLOCK_DESCRIPTOR', `${expected} conflicts with a section block.`);
|
|
719
|
+
if (variant in variants)
|
|
720
|
+
throw new UWCSVError('CSV_BLOCK_DUPLICATE', `${expected} already exists.`);
|
|
721
|
+
variants[variant] = block;
|
|
722
|
+
sections[section] = variants;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
else if (collection === 'extensions') {
|
|
726
|
+
if (typeof section !== 'string' || variant !== null || ordinal !== null || state !== 'current')
|
|
727
|
+
throw new UWCSVError('CSV_BLOCK_DESCRIPTOR', `${blockRef} extension descriptor is invalid.`);
|
|
728
|
+
expected = `/extensions/${pointerEscape(section)}`;
|
|
729
|
+
const extensions = envelope['extensions'];
|
|
730
|
+
if (section in extensions)
|
|
731
|
+
throw new UWCSVError('CSV_BLOCK_DUPLICATE', `${expected} already exists.`);
|
|
732
|
+
extensions[section] = block;
|
|
733
|
+
}
|
|
734
|
+
else if (collection === 'superseded') {
|
|
735
|
+
if (typeof section !== 'string' || variant !== null || ordinal === null || state !== 'superseded')
|
|
736
|
+
throw new UWCSVError('CSV_BLOCK_DESCRIPTOR', `${blockRef} superseded descriptor is invalid.`);
|
|
737
|
+
expected = `/superseded/${pointerEscape(section)}/${ordinal}`;
|
|
738
|
+
const superseded = envelope['superseded'];
|
|
739
|
+
const list = superseded[section] ?? [];
|
|
740
|
+
if (ordinal !== list.length)
|
|
741
|
+
throw new UWCSVError('CSV_BLOCK_DESCRIPTOR', `${expected} ordinal is out of order.`);
|
|
742
|
+
list.push(block);
|
|
743
|
+
superseded[section] = list;
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
if (!['pipeline_log', 'custom_calculations', 'custom_scenarios'].includes(collection) || section !== null || variant !== null || ordinal === null || state !== 'current')
|
|
747
|
+
throw new UWCSVError('CSV_BLOCK_DESCRIPTOR', `${blockRef} collection descriptor is invalid.`);
|
|
748
|
+
expected = `/${collection}/${ordinal}`;
|
|
749
|
+
const list = envelope[collection];
|
|
750
|
+
if (ordinal !== list.length)
|
|
751
|
+
throw new UWCSVError('CSV_BLOCK_DESCRIPTOR', `${expected} ordinal is out of order.`);
|
|
752
|
+
list.push(block);
|
|
753
|
+
}
|
|
754
|
+
if (blockRef !== expected)
|
|
755
|
+
throw new UWCSVError('CSV_BLOCK_REF_MISMATCH', `${blockRef} should be ${expected}.`);
|
|
756
|
+
}
|
|
757
|
+
function assertHeader(rows, expected, file) {
|
|
758
|
+
if (!rows[0] || rows[0].length !== expected.length || rows[0].some((cell, index) => cell !== expected[index]))
|
|
759
|
+
throw new UWCSVError('CSV_HEADER_INVALID', `${file} header is invalid.`);
|
|
760
|
+
}
|
|
761
|
+
function assertSafeBundlePath(path) {
|
|
762
|
+
if (!path || path.includes('\\') || path.includes('\0') || path.startsWith('/') || /^[A-Za-z]:/.test(path) || path.split('/').some((part) => part === '' || part === '.' || part === '..'))
|
|
763
|
+
throw new UWCSVError('CSV_PATH_UNSAFE', `Unsafe bundle path ${JSON.stringify(path)}.`);
|
|
764
|
+
}
|
|
765
|
+
function setJSONPointer(root, pointer, value) {
|
|
766
|
+
if (!pointer.startsWith('/') || pointer === '/')
|
|
767
|
+
throw new UWCSVError('CSV_POINTER_INVALID', `Invalid JSON Pointer ${pointer}.`);
|
|
768
|
+
const parts = pointer.slice(1).split('/').map(pointerUnescape);
|
|
769
|
+
let current = root;
|
|
770
|
+
for (let index = 0; index < parts.length - 1; index++) {
|
|
771
|
+
const part = parts[index] ?? '';
|
|
772
|
+
if (Array.isArray(current)) {
|
|
773
|
+
if (!/^\d+$/.test(part))
|
|
774
|
+
throw new UWCSVError('CSV_POINTER_INVALID', `${pointer} has a non-numeric array index.`);
|
|
775
|
+
const position = Number(part);
|
|
776
|
+
if (current[position] === undefined)
|
|
777
|
+
throw new UWCSVError('CSV_POINTER_PARENT_MISSING', `${pointer} parent is missing.`);
|
|
778
|
+
current = current[position];
|
|
779
|
+
}
|
|
780
|
+
else if (isRecord(current)) {
|
|
781
|
+
if (!(part in current))
|
|
782
|
+
throw new UWCSVError('CSV_POINTER_PARENT_MISSING', `${pointer} parent is missing.`);
|
|
783
|
+
current = current[part];
|
|
784
|
+
}
|
|
785
|
+
else
|
|
786
|
+
throw new UWCSVError('CSV_POINTER_PARENT_INVALID', `${pointer} parent is not a container.`);
|
|
787
|
+
}
|
|
788
|
+
const leaf = parts.at(-1) ?? '';
|
|
789
|
+
if (Array.isArray(current)) {
|
|
790
|
+
if (!/^\d+$/.test(leaf))
|
|
791
|
+
throw new UWCSVError('CSV_POINTER_INVALID', `${pointer} has a non-numeric array index.`);
|
|
792
|
+
const position = Number(leaf);
|
|
793
|
+
if (position !== current.length)
|
|
794
|
+
throw new UWCSVError('CSV_POINTER_ORDER', `${pointer} array rows are out of order.`);
|
|
795
|
+
current.push(value);
|
|
796
|
+
}
|
|
797
|
+
else if (isRecord(current)) {
|
|
798
|
+
if (leaf in current)
|
|
799
|
+
throw new UWCSVError('CSV_POINTER_DUPLICATE', `${pointer} already exists.`);
|
|
800
|
+
current[leaf] = value;
|
|
801
|
+
}
|
|
802
|
+
else
|
|
803
|
+
throw new UWCSVError('CSV_POINTER_PARENT_INVALID', `${pointer} parent is not a container.`);
|
|
804
|
+
}
|
|
805
|
+
function pointerEscape(value) { return value.replaceAll('~', '~0').replaceAll('/', '~1'); }
|
|
806
|
+
function pointerUnescape(value) {
|
|
807
|
+
if (/~(?![01])/.test(value))
|
|
808
|
+
throw new UWCSVError('CSV_POINTER_INVALID', `Invalid JSON Pointer token ${value}.`);
|
|
809
|
+
return value.replaceAll('~1', '/').replaceAll('~0', '~');
|
|
810
|
+
}
|
|
811
|
+
function pointerDepth(pointer) { return pointer === '' ? 0 : pointer.split('/').length - 1; }
|
|
812
|
+
function comparePointers(left, right) {
|
|
813
|
+
const depth = pointerDepth(left) - pointerDepth(right);
|
|
814
|
+
if (depth !== 0)
|
|
815
|
+
return depth;
|
|
816
|
+
const leftParts = left.split('/').slice(1).map(pointerUnescape);
|
|
817
|
+
const rightParts = right.split('/').slice(1).map(pointerUnescape);
|
|
818
|
+
for (let index = 0; index < leftParts.length; index++) {
|
|
819
|
+
const leftPart = leftParts[index] ?? '';
|
|
820
|
+
const rightPart = rightParts[index] ?? '';
|
|
821
|
+
if (leftPart === rightPart)
|
|
822
|
+
continue;
|
|
823
|
+
if (/^\d+$/.test(leftPart) && /^\d+$/.test(rightPart))
|
|
824
|
+
return Number(leftPart) - Number(rightPart);
|
|
825
|
+
return compareCodeUnits(leftPart, rightPart);
|
|
826
|
+
}
|
|
827
|
+
return 0;
|
|
828
|
+
}
|
|
829
|
+
function compareCodeUnits(left, right) {
|
|
830
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
831
|
+
}
|
|
832
|
+
function jsonCell(value) {
|
|
833
|
+
const encoded = JSON.stringify(value);
|
|
834
|
+
if (encoded === undefined)
|
|
835
|
+
throw new UWCSVError('CSV_VALUE_UNSUPPORTED', 'undefined is not a JSON value.');
|
|
836
|
+
return encoded;
|
|
837
|
+
}
|
|
838
|
+
function parseJSONCell(value, pointer) {
|
|
839
|
+
try {
|
|
840
|
+
return JSON.parse(value);
|
|
841
|
+
}
|
|
842
|
+
catch (error) {
|
|
843
|
+
throw new UWCSVError('CSV_JSON_INVALID', `${pointer} contains invalid JSON: ${String(error)}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
function parseNullableJSONCell(value, pointer) {
|
|
847
|
+
const parsed = parseJSONCell(value, pointer);
|
|
848
|
+
if (parsed !== null && typeof parsed !== 'string')
|
|
849
|
+
throw new UWCSVError('CSV_JSON_TYPE', `${pointer} must contain a string or null.`);
|
|
850
|
+
return parsed;
|
|
851
|
+
}
|
|
852
|
+
function jsonType(value) {
|
|
853
|
+
if (value === null)
|
|
854
|
+
return 'null';
|
|
855
|
+
if (Array.isArray(value))
|
|
856
|
+
return 'array';
|
|
857
|
+
if (isRecord(value))
|
|
858
|
+
return 'object';
|
|
859
|
+
if (typeof value === 'string')
|
|
860
|
+
return 'string';
|
|
861
|
+
if (typeof value === 'number') {
|
|
862
|
+
if (!Number.isFinite(value))
|
|
863
|
+
throw new UWCSVError('CSV_NUMBER_INVALID', 'Non-finite numbers are not JSON values.');
|
|
864
|
+
return 'number';
|
|
865
|
+
}
|
|
866
|
+
if (typeof value === 'boolean')
|
|
867
|
+
return 'boolean';
|
|
868
|
+
throw new UWCSVError('CSV_VALUE_UNSUPPORTED', `${typeof value} is not a JSON value.`);
|
|
869
|
+
}
|
|
870
|
+
function assertJSONType(value) {
|
|
871
|
+
if (['object', 'array', 'string', 'number', 'boolean', 'null'].includes(value))
|
|
872
|
+
return value;
|
|
873
|
+
throw new UWCSVError('CSV_TYPE_INVALID', `Unsupported JSON type ${value}.`);
|
|
874
|
+
}
|
|
875
|
+
function isEnvelopeBlock(value) { return isRecord(value) && isRecord(value['annotation']) && isRecord(value['content']); }
|
|
876
|
+
function isRecord(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); }
|
|
877
|
+
//# sourceMappingURL=uwcsv.js.map
|