structural-codes-viewer 0.1.0-alpha.1

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.
@@ -0,0 +1,235 @@
1
+ export type DocumentId = "ntc2018" | "circ2019";
2
+ export type ViewerMode = "ntc" | "circ" | "combined";
3
+ export interface Evidence {
4
+ sourceId: string;
5
+ pdfPage: number;
6
+ printedPage: string | null;
7
+ region: {
8
+ x: number;
9
+ y: number;
10
+ width: number;
11
+ height: number;
12
+ };
13
+ transformations?: Array<{
14
+ operation: string;
15
+ note: string;
16
+ }>;
17
+ rawSha256: string;
18
+ normalizedSha256: string;
19
+ }
20
+ export interface CorpusBlock {
21
+ blockId: string;
22
+ kind: string;
23
+ origin: string;
24
+ text?: {
25
+ raw: string;
26
+ normalized: string;
27
+ normalizationVersion: string;
28
+ inline?: Array<{
29
+ kind: "text";
30
+ value: string;
31
+ } | {
32
+ kind: "math";
33
+ value: string;
34
+ latex: string;
35
+ }>;
36
+ };
37
+ assetId?: string;
38
+ evidence?: Evidence;
39
+ }
40
+ export interface FormulaAsset {
41
+ id: string;
42
+ officialNumber: string | null;
43
+ pdfPage: number;
44
+ latex: string;
45
+ }
46
+ export interface TableCell {
47
+ text: string;
48
+ latex?: string;
49
+ colSpan?: number;
50
+ rowSpan?: number;
51
+ }
52
+ export interface TableAsset {
53
+ id: string;
54
+ officialNumber: string | null;
55
+ pdfPage: number;
56
+ caption: string | null;
57
+ headers: TableCell[][];
58
+ rows: TableCell[][];
59
+ notes: string[];
60
+ }
61
+ export interface FigureAsset {
62
+ id: string;
63
+ officialNumber: string;
64
+ pdfPage: number;
65
+ caption: string;
66
+ alt: string;
67
+ imagePath: string;
68
+ region?: {
69
+ width: number;
70
+ height: number;
71
+ };
72
+ }
73
+ export interface AssetBundle {
74
+ formulas: Record<string, FormulaAsset>;
75
+ tables: Record<string, TableAsset>;
76
+ figures: Record<string, FigureAsset>;
77
+ }
78
+ export interface OpenIssue {
79
+ issueId: string;
80
+ type: string;
81
+ severity: "blocking" | "warning" | "info";
82
+ note: string;
83
+ }
84
+ export interface Relation {
85
+ relationId: string;
86
+ type: string;
87
+ targetUnitId: string;
88
+ basis: string;
89
+ rationale: string;
90
+ review: {
91
+ status: string;
92
+ };
93
+ }
94
+ export interface CorpusUnit {
95
+ id: string;
96
+ document: DocumentId;
97
+ kind: string;
98
+ numbering: {
99
+ official: string;
100
+ sortKey: string;
101
+ };
102
+ title: string;
103
+ hierarchy: {
104
+ parentId: string | null;
105
+ ancestorIds: string[];
106
+ position: number;
107
+ };
108
+ validity: {
109
+ from: string | null;
110
+ to: string | null;
111
+ status: string;
112
+ asOf: string;
113
+ };
114
+ blocks: CorpusBlock[];
115
+ relations: Relation[];
116
+ workflow: {
117
+ status: string;
118
+ openIssues: OpenIssue[];
119
+ };
120
+ }
121
+ export interface UnitSummary {
122
+ id: string;
123
+ document: DocumentId;
124
+ kind: string;
125
+ numbering: {
126
+ official: string;
127
+ sortKey: string;
128
+ };
129
+ title: string;
130
+ hierarchy: CorpusUnit["hierarchy"];
131
+ validity: CorpusUnit["validity"];
132
+ workflowStatus: string;
133
+ openIssueCount: number;
134
+ blockingIssueCount: number;
135
+ chunkPath: string;
136
+ }
137
+ export interface DocumentMetadata {
138
+ shortLabel: string;
139
+ label: string;
140
+ indexPath: string;
141
+ units: number;
142
+ blocks: number;
143
+ sourceId: string;
144
+ sourceUrl: string;
145
+ publicationUrl: string;
146
+ localSourcePath: string;
147
+ sourceSha256: string;
148
+ pages: {
149
+ from: number;
150
+ to: number;
151
+ };
152
+ }
153
+ export interface CorpusManifest {
154
+ formatVersion: 2;
155
+ generatedAt: string;
156
+ structuralCodesVersion: string;
157
+ schemaVersion: string;
158
+ assetSchemaVersion: string;
159
+ status: string;
160
+ disclaimer: string;
161
+ corpusFingerprintSha256: string;
162
+ generatedArtifactFingerprintSha256: string;
163
+ stats: {
164
+ units: number;
165
+ blocks: number;
166
+ explicitRelations: number;
167
+ proposedRelations: number;
168
+ suggestedRelationDiagnostics: number;
169
+ reviewedUnits: number;
170
+ assetUnits: number;
171
+ formulas: number;
172
+ tables: number;
173
+ figures: number;
174
+ chunks: number;
175
+ };
176
+ documents: Record<DocumentId, DocumentMetadata>;
177
+ relationsPath: string;
178
+ relationDiagnosticsPath: string;
179
+ searchIndexPath: string;
180
+ chunks: Array<{
181
+ path: string;
182
+ document: DocumentId;
183
+ key: string;
184
+ units: number;
185
+ bytes: number;
186
+ sha256: string;
187
+ contentFingerprintSha256: string;
188
+ }>;
189
+ }
190
+ export interface DocumentIndex {
191
+ formatVersion: 2;
192
+ structuralCodesVersion: string;
193
+ document: DocumentId;
194
+ units: UnitSummary[];
195
+ }
196
+ export interface CorpusChunk {
197
+ formatVersion: 2;
198
+ structuralCodesVersion: string;
199
+ schemaVersion: string;
200
+ document: DocumentId;
201
+ key: string;
202
+ contentFingerprintSha256: string;
203
+ units: CorpusUnit[];
204
+ assets: AssetBundle;
205
+ }
206
+ export interface RelationEdge extends Relation {
207
+ sourceUnitId: string;
208
+ sourceDocument: DocumentId;
209
+ sourceChunkPath: string;
210
+ targetChunkPath: string | null;
211
+ }
212
+ export interface RelationsIndex {
213
+ formatVersion: 2;
214
+ sourceOfTruth: "explicit-corpus-relations";
215
+ inferredRelationsIncluded: false;
216
+ relations: RelationEdge[];
217
+ }
218
+ export interface SearchIndex {
219
+ formatVersion: 2;
220
+ units: Array<{
221
+ id: string;
222
+ document: DocumentId;
223
+ numbering: string;
224
+ title: string;
225
+ chunkPath: string;
226
+ text: string;
227
+ }>;
228
+ }
229
+ export declare function resolveDataPath(path: string, dataBaseUrl?: string): string;
230
+ export declare function documentForMode(mode: ViewerMode): DocumentId;
231
+ export declare function loadManifest(dataBaseUrl?: string): Promise<CorpusManifest>;
232
+ export declare function loadDocumentIndex(manifest: CorpusManifest, document: DocumentId, dataBaseUrl?: string): Promise<DocumentIndex>;
233
+ export declare function loadChunk(path: string, dataBaseUrl?: string): Promise<CorpusChunk>;
234
+ export declare function loadRelations(manifest: CorpusManifest, dataBaseUrl?: string): Promise<RelationsIndex>;
235
+ export declare function loadSearchIndex(manifest: CorpusManifest, dataBaseUrl?: string): Promise<SearchIndex>;
@@ -0,0 +1,40 @@
1
+ const jsonCache = new Map();
2
+ function basePath(dataBaseUrl) {
3
+ return dataBaseUrl.replace(/\/+$/u, "");
4
+ }
5
+ export function resolveDataPath(path, dataBaseUrl = "/data/codes") {
6
+ if (dataBaseUrl === "/data/codes" || dataBaseUrl === "")
7
+ return path;
8
+ return `${basePath(dataBaseUrl)}/${path.replace(/^\/data\/codes\/?/u, "")}`;
9
+ }
10
+ async function fetchJson(path) {
11
+ let pending = jsonCache.get(path);
12
+ if (!pending) {
13
+ pending = fetch(path).then(async (response) => {
14
+ if (!response.ok)
15
+ throw new Error(`${path}: HTTP ${response.status}`);
16
+ return response.json();
17
+ });
18
+ jsonCache.set(path, pending);
19
+ }
20
+ return pending;
21
+ }
22
+ export function documentForMode(mode) {
23
+ return mode === "circ" ? "circ2019" : "ntc2018";
24
+ }
25
+ export function loadManifest(dataBaseUrl = "/data/codes") {
26
+ return fetchJson(resolveDataPath("/data/codes/manifest.json", dataBaseUrl));
27
+ }
28
+ export function loadDocumentIndex(manifest, document, dataBaseUrl = "/data/codes") {
29
+ return fetchJson(resolveDataPath(manifest.documents[document].indexPath, dataBaseUrl));
30
+ }
31
+ export function loadChunk(path, dataBaseUrl = "/data/codes") {
32
+ return fetchJson(resolveDataPath(path, dataBaseUrl));
33
+ }
34
+ export function loadRelations(manifest, dataBaseUrl = "/data/codes") {
35
+ return fetchJson(resolveDataPath(manifest.relationsPath, dataBaseUrl));
36
+ }
37
+ export function loadSearchIndex(manifest, dataBaseUrl = "/data/codes") {
38
+ return fetchJson(resolveDataPath(manifest.searchIndexPath, dataBaseUrl));
39
+ }
40
+ //# sourceMappingURL=corpusData.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"corpusData.js","sourceRoot":"","sources":["../shared/corpusData.ts"],"names":[],"mappings":"AA2JA,MAAM,SAAS,GAAG,IAAI,GAAG,EAA4B,CAAC;AAEtD,SAAS,QAAQ,CAAC,WAAmB;IACnC,OAAO,WAAW,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,WAAW,GAAG,aAAa;IACvE,IAAI,WAAW,KAAK,aAAa,IAAI,WAAW,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IACrE,OAAO,GAAG,QAAQ,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,oBAAoB,EAAE,EAAE,CAAC,EAAE,CAAC;AAC9E,CAAC;AAED,KAAK,UAAU,SAAS,CAAI,IAAY;IACtC,IAAI,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAClC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE;YAC5C,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,UAAU,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;YACtE,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC;QACzB,CAAC,CAAC,CAAC;QACH,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,OAAqB,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAgB;IAC9C,OAAO,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC;AAClD,CAAC;AACD,MAAM,UAAU,YAAY,CAAC,WAAW,GAAG,aAAa;IACtD,OAAO,SAAS,CAAiB,eAAe,CAAC,2BAA2B,EAAE,WAAW,CAAC,CAAC,CAAC;AAC9F,CAAC;AACD,MAAM,UAAU,iBAAiB,CAAC,QAAwB,EAAE,QAAoB,EAAE,WAAW,GAAG,aAAa;IAC3G,OAAO,SAAS,CAAgB,eAAe,CAAC,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC,CAAC;AACxG,CAAC;AACD,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,WAAW,GAAG,aAAa;IACjE,OAAO,SAAS,CAAc,eAAe,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC;AACpE,CAAC;AACD,MAAM,UAAU,aAAa,CAAC,QAAwB,EAAE,WAAW,GAAG,aAAa;IACjF,OAAO,SAAS,CAAiB,eAAe,CAAC,QAAQ,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC,CAAC;AACzF,CAAC;AACD,MAAM,UAAU,eAAe,CAAC,QAAwB,EAAE,WAAW,GAAG,aAAa;IACnF,OAAO,SAAS,CAAc,eAAe,CAAC,QAAQ,CAAC,eAAe,EAAE,WAAW,CAAC,CAAC,CAAC;AACxF,CAAC"}
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
3
+ import { copyFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
4
+ import { dirname, join, relative, resolve } from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+
7
+ const documentOrder = ["ntc2018", "circ2019"];
8
+ const maxInitialChunkBytes = 1_500_000;
9
+ const documentLabels = {
10
+ ntc2018: { shortLabel: "NTC 2018", label: "Norme Tecniche per le Costruzioni" },
11
+ circ2019: { shortLabel: "Circolare 7/2019", label: "Istruzioni per l’applicazione delle NTC 2018" },
12
+ };
13
+
14
+ function sha256(value) { return createHash("sha256").update(value).digest("hex"); }
15
+ async function json(file) { return JSON.parse(await readFile(file, "utf8")); }
16
+ async function writeJson(file, value) {
17
+ await mkdir(dirname(file), { recursive: true });
18
+ const serialized = `${JSON.stringify(value)}\n`;
19
+ await writeFile(file, serialized, "utf8");
20
+ return { bytes: Buffer.byteLength(serialized), sha256: sha256(serialized) };
21
+ }
22
+ function assertOutputDirectory(outputDirectory, sourceRoot) {
23
+ const output = resolve(outputDirectory);
24
+ if (output === resolve(sourceRoot)) throw new Error(`output generator coincide con il package sorgente: ${output}`);
25
+ if (output === resolve(dirname(sourceRoot))) throw new Error(`output generator troppo ampio: ${output}`);
26
+ }
27
+ async function sourceRootFor(sourcePackage) {
28
+ const packageUrl = await import.meta.resolve(`${sourcePackage}/package.json`);
29
+ return dirname(fileURLToPath(packageUrl));
30
+ }
31
+ async function loadUnits(corpusRoot, document) {
32
+ const directory = join(corpusRoot, document);
33
+ const files = (await readdir(directory)).filter((file) => file.endsWith(".json")).sort((left, right) => left.localeCompare(right, "it", { numeric: true }));
34
+ const units = await Promise.all(files.map((file) => json(join(directory, file))));
35
+ return units.sort((left, right) => left.numbering.sortKey.localeCompare(right.numbering.sortKey, "it", { numeric: true }));
36
+ }
37
+ function baseChunkKey(unit, depth = 2) {
38
+ const components = unit.numbering.official.replace(/^C/iu, "").split(".");
39
+ return components.slice(0, Math.min(depth, components.length)).join(".");
40
+ }
41
+ function assignChunkKeys(units) {
42
+ const initial = new Map();
43
+ for (const unit of units) {
44
+ const key = baseChunkKey(unit);
45
+ const group = initial.get(key) ?? [];
46
+ group.push(unit);
47
+ initial.set(key, group);
48
+ }
49
+ const assigned = new Map();
50
+ for (const [key, group] of initial) {
51
+ if (Buffer.byteLength(JSON.stringify(group)) <= maxInitialChunkBytes) {
52
+ for (const unit of group) assigned.set(unit.id, key);
53
+ continue;
54
+ }
55
+ for (const unit of group) {
56
+ const refined = baseChunkKey(unit, 3);
57
+ assigned.set(unit.id, refined === key ? `${key}.root` : refined);
58
+ }
59
+ }
60
+ return assigned;
61
+ }
62
+ function publicUnit(document, unit) { return { ...unit, document }; }
63
+ function unitSummary(document, unit, chunkPath) {
64
+ const openIssues = unit.workflow.openIssues ?? [];
65
+ return { id: unit.id, document, kind: unit.kind, numbering: unit.numbering, title: unit.title, hierarchy: unit.hierarchy, validity: unit.validity, workflowStatus: unit.workflow.status, openIssueCount: openIssues.length, blockingIssueCount: openIssues.filter(({ severity }) => severity === "blocking").length, chunkPath };
66
+ }
67
+
68
+ export async function generateArtifacts({ sourcePackage = "structural-codes", outputDirectory = join(process.cwd(), "public", "data", "codes"), assetOutputDirectory = join(process.cwd(), "public", "assets") } = {}) {
69
+ const sourceRoot = await sourceRootFor(sourcePackage);
70
+ assertOutputDirectory(outputDirectory, sourceRoot);
71
+ assertOutputDirectory(assetOutputDirectory, sourceRoot);
72
+ const corpusRoot = join(sourceRoot, "corpus", "units");
73
+ const corpusManifestFile = join(sourceRoot, "corpus", "manifest.json");
74
+ const assetManifestDirectory = join(sourceRoot, "corpus", "assets");
75
+ const sourceRegistryFile = join(sourceRoot, "sources", "registry", "sources.v2.json");
76
+ const packageFile = join(sourceRoot, "package.json");
77
+ const legacyOutput = join(dirname(outputDirectory), "corpus.json");
78
+ const figureOutput = assetOutputDirectory;
79
+ const [corpusManifest, sourceRegistry, packageManifest] = await Promise.all([json(corpusManifestFile), json(sourceRegistryFile), json(packageFile)]);
80
+ const assetManifestFiles = (await readdir(assetManifestDirectory, { recursive: true })).filter((file) => file.endsWith(".json")).sort((left, right) => left.localeCompare(right, "it", { numeric: true }));
81
+ const assetManifests = await Promise.all(assetManifestFiles.map((file) => json(join(assetManifestDirectory, file))));
82
+ const assetCollections = { formulas: assetManifests.flatMap(({ formulas }) => formulas), tables: assetManifests.flatMap(({ tables }) => tables), figures: assetManifests.flatMap(({ figures }) => figures) };
83
+ const assetsById = Object.fromEntries(Object.entries(assetCollections).map(([kind, assets]) => [kind, new Map(assets.map((asset) => [asset.id, asset]))]));
84
+ const unitsByDocument = Object.fromEntries(await Promise.all(documentOrder.map(async (document) => [document, await loadUnits(corpusRoot, document)])));
85
+ const allUnits = documentOrder.flatMap((document) => unitsByDocument[document].map((unit) => publicUnit(document, unit)));
86
+ await Promise.all([rm(outputDirectory, { recursive: true, force: true }), rm(legacyOutput, { force: true }), rm(figureOutput, { recursive: true, force: true })]);
87
+ const chunkPathByUnit = new Map();
88
+ const documentIndexes = {};
89
+ const chunkInventory = [];
90
+ for (const document of documentOrder) {
91
+ const units = unitsByDocument[document];
92
+ const keys = assignChunkKeys(units);
93
+ const groups = new Map();
94
+ for (const unit of units) {
95
+ const key = keys.get(unit.id);
96
+ const chunkPath = `/data/codes/${document}/chunks/${key}.json`;
97
+ chunkPathByUnit.set(unit.id, chunkPath);
98
+ const group = groups.get(key) ?? [];
99
+ group.push(unit);
100
+ groups.set(key, group);
101
+ }
102
+ for (const [key, group] of [...groups].sort(([left], [right]) => left.localeCompare(right, "it", { numeric: true }))) {
103
+ const assetIds = new Set(group.flatMap((unit) => [...unit.assets.formulaIds, ...unit.assets.tableIds, ...unit.assets.figureIds, ...unit.blocks.flatMap((block) => block.assetId ? [block.assetId] : [])]));
104
+ const assets = Object.fromEntries(Object.entries(assetsById).map(([kind, lookup]) => [kind, Object.fromEntries([...assetIds].map((id) => lookup.get(id)).filter(Boolean).map((asset) => [asset.id, asset]))]));
105
+ const unitPayload = group.map((unit) => publicUnit(document, unit));
106
+ const contentFingerprintSha256 = sha256(JSON.stringify({ unitPayload, assets }));
107
+ const chunk = { formatVersion: 2, structuralCodesVersion: packageManifest.version, schemaVersion: group[0]?.schemaVersion ?? null, document, key, contentFingerprintSha256, units: unitPayload, assets };
108
+ const chunkFile = join(outputDirectory, document, "chunks", `${key}.json`);
109
+ const written = await writeJson(chunkFile, chunk);
110
+ chunkInventory.push({ path: `/data/codes/${relative(outputDirectory, chunkFile).replaceAll("\\", "/")}`, document, key, units: group.length, bytes: written.bytes, sha256: written.sha256, contentFingerprintSha256 });
111
+ }
112
+ const summaries = units.map((unit) => unitSummary(document, unit, chunkPathByUnit.get(unit.id)));
113
+ const indexPayload = { formatVersion: 2, structuralCodesVersion: packageManifest.version, document, units: summaries };
114
+ const indexFile = join(outputDirectory, document, "index.json");
115
+ const written = await writeJson(indexFile, indexPayload);
116
+ documentIndexes[document] = { ...documentLabels[document], indexPath: `/data/codes/${document}/index.json`, indexFingerprintSha256: written.sha256, units: summaries.length };
117
+ }
118
+ const relations = allUnits.flatMap((unit) => unit.relations.map((relation) => ({ sourceUnitId: unit.id, sourceDocument: unit.document, sourceChunkPath: chunkPathByUnit.get(unit.id), targetUnitId: relation.targetUnitId, targetChunkPath: chunkPathByUnit.get(relation.targetUnitId) ?? null, ...relation })));
119
+ const relationsWritten = await writeJson(join(outputDirectory, "relations.json"), { formatVersion: 2, sourceOfTruth: "explicit-corpus-relations", inferredRelationsIncluded: false, relations });
120
+ const ntcByNumber = new Map(unitsByDocument.ntc2018.map((unit) => [unit.numbering.official, unit]));
121
+ const diagnostics = unitsByDocument.circ2019.flatMap((unit) => { const candidate = ntcByNumber.get(unit.numbering.official.replace(/^C/iu, "")); if (!candidate || unit.relations.some((relation) => relation.targetUnitId === candidate.id)) return []; return [{ kind: "suggested-relation", status: "diagnostic-only-not-canonical", reason: "same-numbering", sourceUnitId: unit.id, targetUnitId: candidate.id, sourceChunkPath: chunkPathByUnit.get(unit.id), targetChunkPath: chunkPathByUnit.get(candidate.id) }]; });
122
+ const diagnosticsWritten = await writeJson(join(outputDirectory, "relation-diagnostics.json"), { formatVersion: 2, warning: "Suggerimenti diagnostici non revisionati: non sono usati dalla vista combinata.", suggestions: diagnostics });
123
+ const searchWritten = await writeJson(join(outputDirectory, "search-index.json"), { formatVersion: 2, normalization: "NFKC lowercase it-IT at query time", units: allUnits.map((unit) => ({ id: unit.id, document: unit.document, numbering: unit.numbering.official, title: unit.title, chunkPath: chunkPathByUnit.get(unit.id), text: unit.blocks.flatMap((block) => block.text?.normalized ? [block.text.normalized] : []).join(" ") })) });
124
+ const manifestationBySourceId = new Map(sourceRegistry.works.flatMap((work) => work.manifestations.map((manifestation) => [manifestation.sourceId, { manifestation, work }])));
125
+ for (const document of documentOrder) {
126
+ const manifestDocument = corpusManifest.documents[document];
127
+ const source = manifestationBySourceId.get(manifestDocument.sourceId);
128
+ if (!source) throw new Error(`sourceId non risolto: ${manifestDocument.sourceId}`);
129
+ Object.assign(documentIndexes[document], { sourceId: manifestDocument.sourceId, sourceUrl: source.manifestation.officialUrl, publicationUrl: source.work.publication.eliUri, localSourcePath: source.manifestation.localFile, sourceSha256: source.manifestation.sha256, pages: manifestDocument.pages, blocks: unitsByDocument[document].reduce((total, unit) => total + unit.blocks.filter((block) => block.text).length, 0) });
130
+ }
131
+ const reviewedStatuses = new Set(["source-checked", "double-reviewed", "published", "superseded"]);
132
+ const corpusFingerprintSha256 = sha256(JSON.stringify({ corpusManifest, unitsByDocument, assetManifests }));
133
+ const artifactFingerprintSha256 = sha256(JSON.stringify({ chunkInventory, indexes: documentIndexes, relations: relationsWritten.sha256, diagnostics: diagnosticsWritten.sha256, search: searchWritten.sha256 }));
134
+ const manifest = { formatVersion: 2, generatedAt: corpusManifest.asOf, structuralCodesVersion: packageManifest.version, schemaVersion: allUnits[0]?.schemaVersion ?? null, assetSchemaVersion: assetManifests[0]?.schemaVersion ?? null, status: corpusManifest.status, disclaimer: corpusManifest.disclaimer, corpusFingerprintSha256, generatedArtifactFingerprintSha256: artifactFingerprintSha256, stats: { units: allUnits.length, blocks: allUnits.reduce((total, unit) => total + unit.blocks.filter((block) => block.text).length, 0), explicitRelations: relations.length, proposedRelations: relations.filter((relation) => relation.review.status === "proposed").length, suggestedRelationDiagnostics: diagnostics.length, reviewedUnits: allUnits.filter((unit) => reviewedStatuses.has(unit.workflow.status)).length, assetUnits: allUnits.filter((unit) => unit.assets.formulaIds.length > 0 || unit.assets.tableIds.length > 0 || unit.assets.figureIds.length > 0).length, formulas: assetCollections.formulas.length, tables: assetCollections.tables.length, figures: assetCollections.figures.length, chunks: chunkInventory.length }, documents: documentIndexes, relationsPath: "/data/codes/relations.json", relationDiagnosticsPath: "/data/codes/relation-diagnostics.json", searchIndexPath: "/data/codes/search-index.json", chunks: chunkInventory };
135
+ const manifestWritten = await writeJson(join(outputDirectory, "manifest.json"), manifest);
136
+ let copiedFigureBytes = 0;
137
+ for (const figure of assetCollections.figures) {
138
+ const source = join(sourceRoot, "corpus", "assets", figure.imagePath);
139
+ const destination = join(figureOutput, figure.imagePath);
140
+ await mkdir(dirname(destination), { recursive: true });
141
+ await copyFile(source, destination);
142
+ copiedFigureBytes += (await stat(source)).size;
143
+ }
144
+ return { manifest, artifactFingerprintSha256, units: allUnits.length, chunks: chunkInventory.length, relations: relations.length, diagnostics: diagnostics.length, manifestBytes: manifestWritten.bytes, figureBytes: copiedFigureBytes };
145
+ }
146
+
147
+ if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
148
+ const args = new Map(process.argv.slice(2).flatMap((argument, index, values) => argument.startsWith("--") && values[index + 1] && !values[index + 1].startsWith("--") ? [[argument.slice(2), values[index + 1]]] : []));
149
+ const result = await generateArtifacts({ sourcePackage: args.get("source") ?? "structural-codes", outputDirectory: args.get("output") ?? join(process.cwd(), "public", "data", "codes"), assetOutputDirectory: args.get("assets") ?? join(process.cwd(), "public", "assets") });
150
+ console.log(`generate-artifacts: ${result.units} unità in ${result.chunks} chunk; ${result.relations} relazioni esplicite; artifact sha256 ${result.artifactFingerprintSha256}`);
151
+ }
@@ -0,0 +1,182 @@
1
+ @import "@fontsource/tinos/latin-400.css";
2
+ @import "@fontsource/tinos/latin-400-italic.css";
3
+ @import "@fontsource/tinos/latin-700.css";
4
+
5
+ .scv-root {
6
+ --scv-primary: #3c52a3;
7
+ --scv-primary-dark: #293b82;
8
+ --scv-primary-soft: #e8ecfb;
9
+ --scv-ink: #20242c;
10
+ --scv-ink-soft: #69717e;
11
+ --scv-paper: #f6f7fb;
12
+ --scv-panel: #ffffff;
13
+ --scv-line: #d9deea;
14
+ --scv-circular: #f0f3fb;
15
+ --scv-amber: #9a671e;
16
+ --scv-amber-soft: #fff3d8;
17
+ display: grid;
18
+ grid-template-columns: minmax(330px, 37fr) minmax(460px, 63fr);
19
+ min-width: 0;
20
+ height: 100vh;
21
+ overflow: hidden;
22
+ color: var(--scv-ink);
23
+ background: var(--scv-paper);
24
+ font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
25
+ }
26
+
27
+ .scv-root.scv-has-auxiliary {
28
+ grid-template-columns: minmax(320px, 27fr) minmax(430px, 35fr) minmax(380px, 38fr);
29
+ }
30
+
31
+ .scv-root *,
32
+ .scv-root *::before,
33
+ .scv-root *::after { box-sizing: border-box; }
34
+
35
+ .scv-root button,
36
+ .scv-root input { font: inherit; }
37
+
38
+ .scv-root button,
39
+ .scv-root a { -webkit-tap-highlight-color: transparent; }
40
+
41
+ .scv-root button:focus-visible,
42
+ .scv-root a:focus-visible,
43
+ .scv-root input:focus-visible,
44
+ .scv-root summary:focus-visible { outline: 2px solid var(--scv-primary); outline-offset: 2px; }
45
+
46
+ .scv-index-pane,
47
+ .scv-text-pane,
48
+ .scv-auxiliary-pane { min-width: 0; height: 100vh; border-right: 1px solid var(--scv-line); }
49
+
50
+ .scv-index-pane { position: relative; display: grid; grid-template-rows: 54px minmax(0, 1fr); overflow: hidden; background: #eef0f6; }
51
+
52
+ .scv-search-toolbar { position: relative; z-index: 5; display: grid; grid-template-columns: minmax(0, 1fr) 48px; gap: 6px; padding: 7px; background: var(--scv-panel); border-bottom: 1px solid var(--scv-line); }
53
+
54
+ .scv-search-form { display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto; align-items: center; gap: 7px; min-width: 0; padding: 0 9px; color: var(--scv-ink-soft); background: #fafbfe; border: 1px solid var(--scv-line); border-radius: 6px; }
55
+
56
+ .scv-search-form input { min-width: 0; padding: 8px 0; color: var(--scv-ink); font-size: 12px; background: transparent; border: 0; outline: 0; }
57
+ .scv-search-form input::placeholder { color: #8a92a1; }
58
+ .scv-search-form kbd { padding: 2px 5px; color: #8991a0; font-size: 9px; border: 1px solid var(--scv-line); border-radius: 3px; }
59
+ .scv-clear-search { padding: 0 3px; color: var(--scv-ink-soft); background: transparent; border: 0; cursor: pointer; }
60
+ .scv-settings-button { display: grid; place-items: center; color: var(--scv-primary-dark); background: var(--scv-primary-soft); border: 1px solid #c8d0ee; border-radius: 6px; cursor: pointer; }
61
+ .scv-settings-button:hover { color: #fff; background: var(--scv-primary); }
62
+
63
+ .scv-search-results { position: absolute; z-index: 10; top: 53px; left: 7px; right: 61px; display: grid; max-height: min(60vh, 510px); overflow: auto; padding: 5px; background: #fff; border: 1px solid var(--scv-line); border-radius: 0 0 7px 7px; box-shadow: 0 12px 28px rgb(34 45 75 / 16%); }
64
+ .scv-search-result { display: grid; gap: 3px; padding: 9px 10px; color: var(--scv-ink); text-align: left; background: transparent; border: 0; border-radius: 5px; cursor: pointer; }
65
+ .scv-search-result:hover, .scv-search-result:focus-visible { background: var(--scv-primary-soft); }
66
+ .scv-search-result span { color: var(--scv-primary); font-size: 9px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }
67
+ .scv-search-result strong { font-family: "Tinos", serif; font-size: 16px; font-weight: 700; }
68
+ .scv-search-result small { overflow: hidden; color: var(--scv-ink-soft); font-family: "Tinos", serif; font-size: 12px; line-height: 1.3; text-overflow: ellipsis; white-space: nowrap; }
69
+ .scv-search-status { margin: 5px; padding: 11px; color: var(--scv-ink-soft); font-size: 11px; }
70
+
71
+ .scv-index-grid { display: grid; min-height: 0; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
72
+ .scv-index-cell { display: grid; min-width: 0; min-height: 0; grid-template-rows: 29px minmax(0, 1fr); border-right: 1px solid var(--scv-line); border-bottom: 1px solid var(--scv-line); }
73
+ .scv-index-cell:nth-child(2) { grid-row: 2; grid-column: 1; }
74
+ .scv-index-cell:nth-child(3) { grid-row: 1; grid-column: 2; }
75
+ .scv-index-cell:nth-child(4) { grid-row: 2; grid-column: 2; }
76
+ .scv-index-cell > header { display: flex; align-items: center; justify-content: space-between; padding: 0 9px; color: #737b8a; font-size: 8px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; background: #e4e7f0; border-bottom: 1px solid var(--scv-line); }
77
+ .scv-index-cell > header b { color: #9aa2b1; font-size: 9px; font-variant-numeric: lining-nums; }
78
+ .scv-index-list { min-height: 0; padding: 3px; overflow: auto; scrollbar-color: #b8c0d2 transparent; scrollbar-width: thin; }
79
+ .scv-index-list button { display: grid; width: 100%; grid-template-columns: max-content minmax(0, 1fr); gap: 6px; padding: 5px 6px; color: #4f5867; text-align: left; background: transparent; border: 0; border-radius: 3px; cursor: pointer; }
80
+ .scv-index-list button:hover { background: rgb(255 255 255 / 70%); }
81
+ .scv-index-list button.active { color: #fff; background: var(--scv-primary); box-shadow: 0 2px 7px rgb(60 82 163 / 24%); }
82
+ .scv-index-list strong { overflow: hidden; max-width: 62px; color: var(--scv-primary-dark); font-family: "Tinos", serif; font-size: 10px; font-variant-numeric: lining-nums; font-weight: 700; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; }
83
+ .scv-index-list button.active strong { color: #fff; }
84
+ .scv-index-list span { display: -webkit-box; overflow: hidden; font-family: "Tinos", serif; font-size: 11px; line-height: 1.2; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
85
+
86
+ .scv-text-pane { position: relative; overflow: auto; background: #fff; scrollbar-color: #b1b8c7 transparent; scrollbar-width: thin; scroll-behavior: auto; }
87
+ .scv-text-flow { width: min(100%, 790px); min-height: 100%; margin: 0 auto; padding: 23px 26px 55vh; }
88
+ .scv-chunk-note { margin: 0 0 14px; padding-bottom: 9px; color: #8991a0; font-size: 8px; border-bottom: 1px solid #e2e5ed; }
89
+ .scv-unit { scroll-margin-top: 14px; }
90
+ .scv-unit h2 { display: block; margin: 5px 0 4px; color: var(--scv-ink); font-family: "Tinos", serif; font-size: 16px; font-weight: 700; line-height: 1.2; font-variant-numeric: lining-nums; }
91
+ .scv-unit h2 span { margin-right: 7px; color: var(--scv-primary); font-family: "Tinos", serif; font-size: inherit; font-variant-numeric: lining-nums; font-weight: 700; white-space: nowrap; }
92
+ .scv-unit-depth-0 h2 { margin-top: 18px; color: var(--scv-primary-dark); font-size: 19px; }
93
+ .scv-unit:first-child h2 { margin-top: 0; }
94
+ .scv-unit-blocks { display: block; }
95
+ .scv-block { display: contents; }
96
+ .scv-block p { display: inline; margin: 0; color: #272c35; font-family: "Tinos", serif; font-size: 13px; line-height: 1.48; }
97
+ .scv-block p::after { content: " "; }
98
+ .scv-block-heading, .scv-block-list-item, .scv-block-formula-ref, .scv-block-table-ref, .scv-block-figure-ref { display: block; }
99
+ .scv-block-heading p { display: block; margin: 6px 0 2px; font-weight: 700; }
100
+ .scv-block-list-item { padding-left: 11px; }
101
+ .scv-block-list-item p::before { margin-right: 5px; color: var(--scv-primary); content: "–"; font-weight: 700; }
102
+ .scv-related-unit { margin: 14px 0 18px; padding: 13px 15px; background: var(--scv-circular); border: 1px solid #cfd7ee; border-left: 3px solid var(--scv-primary); border-radius: 4px; }
103
+ .scv-related-unit > header h3 { display: block; margin: 6px 0; color: var(--scv-primary); font-family: "Tinos", serif; font-size: 16px; font-variant-numeric: lining-nums; font-weight: 700; line-height: 1.2; }
104
+ .scv-related-unit > header h3 span { margin-right: 7px; color: var(--scv-primary); font-family: "Tinos", serif; font-size: inherit; font-variant-numeric: lining-nums; font-weight: 700; white-space: nowrap; }
105
+ .scv-end-note { margin-top: 34px; padding-top: 12px; color: #8991a0; font-family: "Tinos", serif; font-size: 11px; font-style: italic; border-top: 1px solid #e2e5ed; }
106
+
107
+ .scv-root .inline-math { display: inline-block; margin-inline: .035em; color: #202733; font-size: .96em; white-space: nowrap; }
108
+ .scv-root .formula-asset, .scv-root .table-asset, .scv-root .figure-asset { margin: 7px 0 9px; padding: 8px; overflow: hidden; background: #fff; border: 1px solid #dfe3ec; border-radius: 3px; box-shadow: none; }
109
+ .scv-root .formula-row { display: flex; align-items: center; gap: 14px; }
110
+ .scv-root .formula-scroll, .scv-root .table-scroll { max-width: 100%; overflow-x: auto; }
111
+ .scv-root .formula-scroll { flex: 1 1 auto; min-width: 0; padding: 3px; font-size: .8rem; }
112
+ .scv-root .formula-number { flex: 0 0 auto; margin-left: auto; color: var(--scv-primary-dark); font-family: "Tinos", serif; font-size: 12px; font-variant-numeric: lining-nums; white-space: nowrap; }
113
+ .scv-root .formula-asset figcaption, .scv-root .table-asset figcaption, .scv-root .figure-asset figcaption { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin-top: 5px; color: #687284; font-size: 8px; }
114
+ .scv-root .table-asset figcaption { display: block; margin-bottom: 7px; }
115
+ .scv-root .table-asset table { width: 100%; min-width: 520px; border-collapse: collapse; color: #2a3039; font-size: 8px; line-height: 1.42; }
116
+ .scv-root .table-asset th, .scv-root .table-asset td { padding: 4px 5px; vertical-align: middle; border: 1px solid #c7ccda; }
117
+ .scv-root .table-asset th { color: #fff; font-weight: 700; text-align: center; background: var(--scv-primary-dark); }
118
+ .scv-root .table-asset td { background: #fbfcff; }
119
+ .scv-root .table-asset tbody tr:nth-child(even) td { background: #f1f3f9; }
120
+ .scv-root .table-notes { margin: 8px 0 0; padding-left: 17px; color: #6d7583; font-size: 9px; line-height: 1.4; }
121
+ .scv-root .figure-asset img { display: block; width: auto; max-width: 100%; max-height: 430px; margin-inline: auto; object-fit: contain; }
122
+ .scv-root .figure-asset figcaption { padding-top: 7px; border-top: 1px solid #dfe3ec; }
123
+ .scv-root .asset-missing { padding: 12px; color: #8b342a !important; font-family: ui-sans-serif, sans-serif !important; font-size: 11px !important; background: #fff2ef; }
124
+
125
+ .scv-loading, .scv-fatal { display: grid; min-height: 100%; place-content: center; gap: 10px; padding: 28px; color: var(--scv-ink-soft); text-align: center; }
126
+ .scv-loading > span { width: 24px; height: 24px; justify-self: center; border: 2px solid #c6ccda; border-top-color: var(--scv-primary); border-radius: 50%; animation: scv-spin .8s linear infinite; }
127
+ .scv-loading strong, .scv-fatal strong { font-family: "Tinos", serif; font-size: 14px; font-weight: 700; }
128
+ .scv-fatal span { max-width: 300px; font-size: 10px; line-height: 1.5; }
129
+ .scv-loading-rows { display: grid; gap: 9px; padding: 12px; }
130
+ .scv-loading-rows span { height: 28px; background: linear-gradient(90deg, #e3e6ee, #f8f9fc, #e3e6ee); background-size: 200% 100%; border-radius: 4px; animation: scv-shimmer 1.8s infinite linear; }
131
+
132
+ .scv-dialog-backdrop { position: fixed; z-index: 50; inset: 0; display: grid; place-items: center; padding: 20px; background: rgb(26 34 61 / 38%); }
133
+ .scv-dialog { width: min(420px, 100%); padding: 20px; color: var(--scv-ink); background: #fff; border: 1px solid var(--scv-line); border-radius: 10px; box-shadow: 0 24px 70px rgb(22 31 65 / 25%); }
134
+ .scv-dialog header { display: flex; align-items: center; justify-content: space-between; gap: 14px; margin-bottom: 18px; }
135
+ .scv-dialog h2 { margin: 0; color: var(--scv-primary-dark); font-family: "Tinos", serif; font-size: 24px; }
136
+ .scv-dialog header button { padding: 0 4px; color: var(--scv-ink-soft); font-size: 24px; line-height: 1; background: transparent; border: 0; cursor: pointer; }
137
+ .scv-dialog fieldset { display: grid; gap: 10px; margin: 0; padding: 0; border: 0; }
138
+ .scv-dialog legend { margin-bottom: 8px; color: var(--scv-ink-soft); font-size: 10px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; }
139
+ .scv-dialog label { display: flex; align-items: center; gap: 9px; padding: 9px; font-size: 13px; border-radius: 5px; cursor: pointer; }
140
+ .scv-dialog label:has(input:checked) { color: var(--scv-primary-dark); background: var(--scv-primary-soft); }
141
+ .scv-dialog input { accent-color: var(--scv-primary); }
142
+ .scv-auxiliary-toggle { margin-top: 17px; border-top: 1px solid var(--scv-line); }
143
+ .scv-analytical-link { display: inline-block; margin-top: 17px; color: var(--scv-primary); font-size: 11px; font-weight: 700; }
144
+
145
+ .scv-pdf-panel { height: 100%; overflow: auto; color: #edf0f7; background: #30343f; scrollbar-color: #777f91 #282c35; scrollbar-width: thin; }
146
+ .scv-pdf-status { position: sticky; z-index: 4; top: 8px; display: flex; width: max-content; max-width: calc(100% - 28px); align-items: center; gap: 9px; margin: 8px auto -33px; padding: 6px 8px; color: #eef0f7; font-size: 8px; background: rgb(22 26 35 / 90%); border: 1px solid rgb(255 255 255 / 18%); border-radius: 4px; box-shadow: 0 2px 10px rgb(0 0 0 / 28%); backdrop-filter: blur(8px); }
147
+ .scv-pdf-status strong { font-family: "Tinos", serif; font-size: 11px; }
148
+ .scv-pdf-status span { color: #c4cad8; font-variant-numeric: lining-nums; }
149
+ .scv-pdf-status a { color: #b9c5fb; text-decoration: none; }
150
+ .scv-pdf-pages { display: grid; gap: 12px; padding: 46px 14px 55vh; }
151
+ .scv-pdf-page { position: relative; width: 100%; min-height: 240px; aspect-ratio: .707 / 1; overflow: hidden; background: #fff; box-shadow: 0 2px 9px rgb(0 0 0 / 38%); }
152
+ .scv-pdf-page > span { position: absolute; z-index: 2; top: 5px; right: 6px; padding: 2px 4px; color: #727772; font-size: 7px; background: rgb(255 255 255 / 78%); border-radius: 2px; }
153
+ .scv-pdf-page canvas { display: block; width: 100% !important; height: auto !important; background: #fff; }
154
+ .scv-pdf-consent { display: grid; min-height: 100%; place-content: center; gap: 10px; padding: 34px; text-align: center; }
155
+ .scv-pdf-consent strong { font-family: "Tinos", serif; font-size: 19px; }
156
+ .scv-pdf-consent p { max-width: 310px; margin: 0; color: #c6cbd7; font-size: 10px; line-height: 1.55; }
157
+ .scv-pdf-consent button { justify-self: center; padding: 9px 13px; color: #fff; font-size: 10px; font-weight: 800; background: var(--scv-primary); border: 0; border-radius: 4px; cursor: pointer; }
158
+ .scv-pdf-error { display: grid; min-height: 100%; place-content: center; gap: 8px; padding: 34px; text-align: center; }
159
+ .scv-pdf-error strong { font-family: "Tinos", serif; font-size: 15px; }
160
+ .scv-pdf-error span { color: #c6cbd7; font-size: 10px; }
161
+
162
+ @keyframes scv-spin { to { transform: rotate(360deg); } }
163
+ @keyframes scv-shimmer { to { background-position: -200% 0; } }
164
+
165
+ @media (max-width: 1120px) {
166
+ .scv-root, .scv-root.scv-has-auxiliary { position: relative; display: grid; height: auto; min-height: 100vh; grid-template-columns: minmax(260px, 36fr) minmax(0, 64fr); overflow: visible; }
167
+ .scv-index-pane, .scv-text-pane { height: 76vh; }
168
+ .scv-auxiliary-pane { height: 70vh; grid-column: 1 / -1; border-right: 0; }
169
+ }
170
+
171
+ @media (max-width: 700px) {
172
+ .scv-root, .scv-root.scv-has-auxiliary { display: block; }
173
+ .scv-index-pane { height: 48vh; }
174
+ .scv-text-pane { height: auto; min-height: 58vh; }
175
+ .scv-text-flow { padding: 18px 16px 60px; }
176
+ .scv-auxiliary-pane { height: 72vh; }
177
+ .scv-search-form kbd { display: none; }
178
+ }
179
+
180
+ @media (prefers-reduced-motion: reduce) {
181
+ .scv-root *, .scv-root *::before, .scv-root *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
182
+ }