@transitrix/cli 1.0.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/LICENSE +21 -0
- package/README.md +69 -0
- package/dist/cli.js +3184 -0
- package/dist/export-compliance.js +1076 -0
- package/dist/repo-validate.js +207 -0
- package/package.json +48 -0
- package/schemas/bpmn-dsl.schema.json +136 -0
- package/schemas/cervinrc.schema.json +23 -0
- package/schemas/transitrixrc.schema.json +23 -0
|
@@ -0,0 +1,1076 @@
|
|
|
1
|
+
import { createRequire as __createRequire__ } from 'node:module'; const require = __createRequire__(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// ../../src/export-compliance.ts
|
|
4
|
+
import { writeFileSync, readFileSync, readdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import yaml from "js-yaml";
|
|
9
|
+
|
|
10
|
+
// ../diagrams/src/compliance/reverse-index.ts
|
|
11
|
+
function push(map, key, value) {
|
|
12
|
+
const list = map.get(key);
|
|
13
|
+
if (list) list.push(value);
|
|
14
|
+
else map.set(key, [value]);
|
|
15
|
+
}
|
|
16
|
+
function buildComplianceIndex(input) {
|
|
17
|
+
const requirementById = /* @__PURE__ */ new Map();
|
|
18
|
+
const requirementsByLaw = /* @__PURE__ */ new Map();
|
|
19
|
+
const assertionsByRequirement = /* @__PURE__ */ new Map();
|
|
20
|
+
const assertionsBySubject = /* @__PURE__ */ new Map();
|
|
21
|
+
for (const r of input.requirements) {
|
|
22
|
+
requirementById.set(r.id, r);
|
|
23
|
+
for (const law of r.derived_from ?? []) push(requirementsByLaw, law, r);
|
|
24
|
+
}
|
|
25
|
+
for (const a of input.assertions) {
|
|
26
|
+
push(assertionsByRequirement, a.about, a);
|
|
27
|
+
push(assertionsBySubject, a.subject, a);
|
|
28
|
+
}
|
|
29
|
+
return { requirementById, requirementsByLaw, assertionsByRequirement, assertionsBySubject };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ../diagrams/src/compliance/views.ts
|
|
33
|
+
function byId(a, b) {
|
|
34
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
35
|
+
}
|
|
36
|
+
function buildLawTree(lawId, index) {
|
|
37
|
+
const requirements = [...index.requirementsByLaw.get(lawId) ?? []].sort(byId);
|
|
38
|
+
return {
|
|
39
|
+
lawId,
|
|
40
|
+
requirements: requirements.map((requirement) => ({
|
|
41
|
+
requirement,
|
|
42
|
+
assertions: [...index.assertionsByRequirement.get(requirement.id) ?? []].sort(byId)
|
|
43
|
+
}))
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function buildProductView(productId, index) {
|
|
47
|
+
const assertions = [...index.assertionsBySubject.get(productId) ?? []];
|
|
48
|
+
const requirements = assertions.map((assertion) => {
|
|
49
|
+
const requirement = index.requirementById.get(assertion.about) ?? { id: assertion.about, name: assertion.about };
|
|
50
|
+
return { requirement, assertion };
|
|
51
|
+
}).sort((a, b) => byId(a.requirement, b.requirement));
|
|
52
|
+
return { productId, requirements };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ../diagrams/src/compliance/impact.ts
|
|
56
|
+
var COMPLIANCE_IMPACT_DEFAULTS = {
|
|
57
|
+
/** Accept all four active statuses + n_a in cell aggregation. */
|
|
58
|
+
status_display: {
|
|
59
|
+
show: ["compliant", "partial", "non_compliant", "under_review", "n_a"]
|
|
60
|
+
},
|
|
61
|
+
/** Order rows by canonical REQUIREMENT ID (lexicographic). */
|
|
62
|
+
order_rows_by: "id",
|
|
63
|
+
/** §5.3 empty-cell labels. */
|
|
64
|
+
empty_cells: {
|
|
65
|
+
no_obligation_label: "No mapped obligation (current model)",
|
|
66
|
+
no_obligation_applies_label: "No obligation applies"
|
|
67
|
+
},
|
|
68
|
+
/** Obligation scope: all known REQUIREMENTs in the canon (no filter). */
|
|
69
|
+
obligations: { include: void 0, filter: void 0 },
|
|
70
|
+
/** Subjects: empty — must be supplied explicitly in the view config. */
|
|
71
|
+
subjects: { products: [], processes: [] },
|
|
72
|
+
/** Column grain: one column per business object (no stage decomposition). */
|
|
73
|
+
grouping: { columns: "object" }
|
|
74
|
+
};
|
|
75
|
+
function parseImpactViewConfig(raw) {
|
|
76
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
77
|
+
return { ok: false, errors: ["view config: expected an object at the document root"] };
|
|
78
|
+
}
|
|
79
|
+
const top = raw;
|
|
80
|
+
const v = "view" in top && top.view && typeof top.view === "object" && !Array.isArray(top.view) ? top.view : top;
|
|
81
|
+
const errors = [];
|
|
82
|
+
if (!v.id || typeof v.id !== "string") errors.push("view.id: required string");
|
|
83
|
+
if (!v.name || typeof v.name !== "string") errors.push("view.name: required string");
|
|
84
|
+
if (v.subjects !== void 0 && (typeof v.subjects !== "object" || Array.isArray(v.subjects))) {
|
|
85
|
+
errors.push("view.subjects: expected an object");
|
|
86
|
+
}
|
|
87
|
+
if (errors.length) return { ok: false, errors };
|
|
88
|
+
const subjects = v.subjects && typeof v.subjects === "object" && !Array.isArray(v.subjects) ? v.subjects : {};
|
|
89
|
+
const obligations = v.obligations && typeof v.obligations === "object" && !Array.isArray(v.obligations) ? v.obligations : {};
|
|
90
|
+
const obFilter = obligations.filter && typeof obligations.filter === "object" && !Array.isArray(obligations.filter) ? obligations.filter : null;
|
|
91
|
+
const statusDisplay = v.status_display && typeof v.status_display === "object" && !Array.isArray(v.status_display) ? v.status_display : {};
|
|
92
|
+
const emptyCells = v.empty_cells && typeof v.empty_cells === "object" && !Array.isArray(v.empty_cells) ? v.empty_cells : {};
|
|
93
|
+
const config = {
|
|
94
|
+
id: v.id,
|
|
95
|
+
name: v.name,
|
|
96
|
+
description: typeof v.description === "string" ? v.description : void 0,
|
|
97
|
+
snapshot_at: typeof v.snapshot_at === "string" ? v.snapshot_at : void 0,
|
|
98
|
+
subjects: {
|
|
99
|
+
products: Array.isArray(subjects.products) ? subjects.products.filter((x) => typeof x === "string") : [...COMPLIANCE_IMPACT_DEFAULTS.subjects.products],
|
|
100
|
+
processes: Array.isArray(subjects.processes) ? subjects.processes.filter((x) => typeof x === "string") : [...COMPLIANCE_IMPACT_DEFAULTS.subjects.processes]
|
|
101
|
+
},
|
|
102
|
+
obligations: {
|
|
103
|
+
include: Array.isArray(obligations.include) ? obligations.include.filter((x) => typeof x === "string") : void 0,
|
|
104
|
+
filter: obFilter ? {
|
|
105
|
+
derived_from_codex: Array.isArray(obFilter.derived_from_codex) ? obFilter.derived_from_codex.filter((x) => typeof x === "string") : void 0
|
|
106
|
+
} : void 0
|
|
107
|
+
},
|
|
108
|
+
status_display: {
|
|
109
|
+
show: Array.isArray(statusDisplay.show) ? statusDisplay.show.filter((x) => typeof x === "string") : [...COMPLIANCE_IMPACT_DEFAULTS.status_display.show]
|
|
110
|
+
},
|
|
111
|
+
empty_cells: {
|
|
112
|
+
no_obligation_label: typeof emptyCells.no_obligation_label === "string" ? emptyCells.no_obligation_label : COMPLIANCE_IMPACT_DEFAULTS.empty_cells.no_obligation_label,
|
|
113
|
+
no_obligation_applies_label: typeof emptyCells.no_obligation_applies_label === "string" ? emptyCells.no_obligation_applies_label : COMPLIANCE_IMPACT_DEFAULTS.empty_cells.no_obligation_applies_label
|
|
114
|
+
},
|
|
115
|
+
order_rows_by: v.order_rows_by === "name" ? "name" : COMPLIANCE_IMPACT_DEFAULTS.order_rows_by
|
|
116
|
+
};
|
|
117
|
+
return { ok: true, config };
|
|
118
|
+
}
|
|
119
|
+
var DEFAULT_NO_OBLIGATION_LABEL = "No mapped obligation (current model)";
|
|
120
|
+
var DEFAULT_NO_OBLIGATION_APPLIES_LABEL = "No obligation applies";
|
|
121
|
+
var IN_FORCE_HORIZON_DAYS = 30;
|
|
122
|
+
function computeDeadlineStatus(deadline, today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) {
|
|
123
|
+
if (!deadline) return "none";
|
|
124
|
+
if (deadline < today) return "past_due";
|
|
125
|
+
const daysAway = Math.round(
|
|
126
|
+
(new Date(deadline).getTime() - new Date(today).getTime()) / (1e3 * 60 * 60 * 24)
|
|
127
|
+
);
|
|
128
|
+
return daysAway <= IN_FORCE_HORIZON_DAYS ? "in_force" : "upcoming";
|
|
129
|
+
}
|
|
130
|
+
function byId2(a, b) {
|
|
131
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
132
|
+
}
|
|
133
|
+
function resolveObligations(config, index, allRequirements) {
|
|
134
|
+
if (config.obligations.include?.length) {
|
|
135
|
+
const out2 = [];
|
|
136
|
+
for (const id of config.obligations.include) {
|
|
137
|
+
const r = index.requirementById.get(id);
|
|
138
|
+
if (r) out2.push(r);
|
|
139
|
+
}
|
|
140
|
+
return out2;
|
|
141
|
+
}
|
|
142
|
+
const codices = config.obligations.filter?.derived_from_codex ?? [];
|
|
143
|
+
if (codices.length === 0) return [...allRequirements].sort(byId2);
|
|
144
|
+
const seen = /* @__PURE__ */ new Set();
|
|
145
|
+
const out = [];
|
|
146
|
+
for (const codex of codices) {
|
|
147
|
+
for (const r of index.requirementsByLaw.get(codex) ?? []) {
|
|
148
|
+
if (!seen.has(r.id)) {
|
|
149
|
+
seen.add(r.id);
|
|
150
|
+
out.push(r);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
function orderRows(rows, key) {
|
|
157
|
+
const sorted = [...rows];
|
|
158
|
+
if (key === "name") {
|
|
159
|
+
sorted.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
160
|
+
} else {
|
|
161
|
+
sorted.sort(byId2);
|
|
162
|
+
}
|
|
163
|
+
return sorted;
|
|
164
|
+
}
|
|
165
|
+
function aggregateStatus(assertions) {
|
|
166
|
+
let sawPartial = false;
|
|
167
|
+
let sawUnderReview = false;
|
|
168
|
+
let sawCompliant = false;
|
|
169
|
+
for (const a of assertions) {
|
|
170
|
+
if (a.status === "non_compliant") return "non_compliant";
|
|
171
|
+
if (a.status === "partial") sawPartial = true;
|
|
172
|
+
else if (a.status === "under_review") sawUnderReview = true;
|
|
173
|
+
else if (a.status === "compliant") sawCompliant = true;
|
|
174
|
+
}
|
|
175
|
+
if (sawPartial) return "partial";
|
|
176
|
+
if (sawUnderReview) return "under_review";
|
|
177
|
+
if (sawCompliant) return "compliant";
|
|
178
|
+
return "n_a";
|
|
179
|
+
}
|
|
180
|
+
function buildProductColumns(config) {
|
|
181
|
+
const subjects = [...config.subjects.products ?? [], ...config.subjects.processes ?? []];
|
|
182
|
+
return subjects.map((id) => ({ subjectId: id, label: id }));
|
|
183
|
+
}
|
|
184
|
+
function buildObjectDetailColumns(config, objectDetails) {
|
|
185
|
+
const detailMap = new Map(objectDetails.map((d) => [d.objectId, d.details]));
|
|
186
|
+
const subjects = [...config.subjects.products ?? [], ...config.subjects.processes ?? []];
|
|
187
|
+
const cols = [];
|
|
188
|
+
for (const subjectId of subjects) {
|
|
189
|
+
const details = detailMap.get(subjectId);
|
|
190
|
+
if (!details || details.length === 0) {
|
|
191
|
+
cols.push({ subjectId, label: subjectId });
|
|
192
|
+
} else {
|
|
193
|
+
for (const detail of details) {
|
|
194
|
+
cols.push({ subjectId, stageId: detail.id, label: `${subjectId}:${detail.id}` });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return cols;
|
|
199
|
+
}
|
|
200
|
+
function assertionMatchesColumn(a, col) {
|
|
201
|
+
if (a.subject !== col.subjectId) return false;
|
|
202
|
+
if (!col.stageId) return true;
|
|
203
|
+
if (!a.realised_via || a.realised_via.length === 0) return true;
|
|
204
|
+
return a.realised_via.includes(col.stageId);
|
|
205
|
+
}
|
|
206
|
+
function buildImpactMatrix(canon, config, objectDetails) {
|
|
207
|
+
const index = buildComplianceIndex({
|
|
208
|
+
requirements: canon.requirements,
|
|
209
|
+
assertions: canon.assertions
|
|
210
|
+
});
|
|
211
|
+
const obligations = orderRows(resolveObligations(config, index, canon.requirements), config.order_rows_by);
|
|
212
|
+
const useStageGrain = config.grouping?.columns === "object-details" && objectDetails && objectDetails.length > 0;
|
|
213
|
+
const columns = useStageGrain ? buildObjectDetailColumns(config, objectDetails) : buildProductColumns(config);
|
|
214
|
+
const allowedStatuses = new Set(
|
|
215
|
+
config.status_display?.show ?? ["compliant", "partial", "non_compliant", "under_review", "n_a"]
|
|
216
|
+
);
|
|
217
|
+
const rowIndex = new Set(obligations.map((r) => r.id));
|
|
218
|
+
const cells = obligations.map(() => columns.map(() => emptyCell()));
|
|
219
|
+
for (let c = 0; c < columns.length; c++) {
|
|
220
|
+
const col = columns[c];
|
|
221
|
+
const subjectAssertions = index.assertionsBySubject.get(col.subjectId) ?? [];
|
|
222
|
+
for (const a of subjectAssertions) {
|
|
223
|
+
if (!rowIndex.has(a.about)) continue;
|
|
224
|
+
if (!allowedStatuses.has(a.status)) continue;
|
|
225
|
+
if (!assertionMatchesColumn(a, col)) continue;
|
|
226
|
+
const rowIdx = obligations.findIndex((r) => r.id === a.about);
|
|
227
|
+
if (rowIdx < 0) continue;
|
|
228
|
+
cells[rowIdx][c].assertions.push(a);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
for (let r = 0; r < cells.length; r++) {
|
|
232
|
+
for (let c = 0; c < cells[r].length; c++) {
|
|
233
|
+
const cell2 = cells[r][c];
|
|
234
|
+
cell2.assertions.sort(byId2);
|
|
235
|
+
if (cell2.assertions.length === 0) {
|
|
236
|
+
cell2.kind = "gap";
|
|
237
|
+
cell2.status = null;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (cell2.assertions.every((a) => a.status === "n_a")) {
|
|
241
|
+
cell2.kind = "n_a_only";
|
|
242
|
+
cell2.status = "n_a";
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
cell2.kind = "bound";
|
|
246
|
+
cell2.status = aggregateStatus(cell2.assertions);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
250
|
+
for (let r = 0; r < cells.length; r++) {
|
|
251
|
+
const req = obligations[r];
|
|
252
|
+
const deadlineStatus = computeDeadlineStatus(req.deadline, today);
|
|
253
|
+
const isNew = !!config.snapshot_at && !!req.admitted_at && req.admitted_at > config.snapshot_at;
|
|
254
|
+
for (let c = 0; c < cells[r].length; c++) {
|
|
255
|
+
const cell2 = cells[r][c];
|
|
256
|
+
const isUrgent = cell2.kind === "gap" && (deadlineStatus === "past_due" || deadlineStatus === "in_force");
|
|
257
|
+
cell2.decoration = { isNew, isUrgent, deadlineStatus };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const obligationsLane = columns.map((_, c) => {
|
|
261
|
+
const codexIds = /* @__PURE__ */ new Set();
|
|
262
|
+
for (let r = 0; r < cells.length; r++) {
|
|
263
|
+
if (cells[r][c].kind !== "gap") {
|
|
264
|
+
for (const src of obligations[r].derived_from ?? []) {
|
|
265
|
+
codexIds.add(src);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return [...codexIds].sort();
|
|
270
|
+
});
|
|
271
|
+
return {
|
|
272
|
+
viewId: config.id,
|
|
273
|
+
viewName: config.name,
|
|
274
|
+
description: config.description,
|
|
275
|
+
snapshotAt: config.snapshot_at,
|
|
276
|
+
rows: obligations,
|
|
277
|
+
columns,
|
|
278
|
+
cells,
|
|
279
|
+
emptyLabels: {
|
|
280
|
+
no_obligation_label: config.empty_cells?.no_obligation_label ?? DEFAULT_NO_OBLIGATION_LABEL,
|
|
281
|
+
no_obligation_applies_label: config.empty_cells?.no_obligation_applies_label ?? DEFAULT_NO_OBLIGATION_APPLIES_LABEL
|
|
282
|
+
},
|
|
283
|
+
obligationsLane
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function emptyCell() {
|
|
287
|
+
return {
|
|
288
|
+
status: null,
|
|
289
|
+
kind: "gap",
|
|
290
|
+
assertions: [],
|
|
291
|
+
decoration: { isNew: false, isUrgent: false, deadlineStatus: "none" }
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
var STATUS_GLYPH = {
|
|
295
|
+
compliant: "OK",
|
|
296
|
+
partial: "PARTIAL",
|
|
297
|
+
non_compliant: "FAIL",
|
|
298
|
+
under_review: "REVIEW",
|
|
299
|
+
n_a: "N/A"
|
|
300
|
+
};
|
|
301
|
+
function escMd(s) {
|
|
302
|
+
return s.replace(/\|/g, "\\|");
|
|
303
|
+
}
|
|
304
|
+
function renderImpactMarkdown(matrix) {
|
|
305
|
+
const lines = [];
|
|
306
|
+
lines.push(`# ${matrix.viewName}`);
|
|
307
|
+
lines.push("");
|
|
308
|
+
lines.push(`View ID: \`${matrix.viewId}\``);
|
|
309
|
+
if (matrix.snapshotAt) lines.push(`Report snapshot: ${matrix.snapshotAt}`);
|
|
310
|
+
if (matrix.description) {
|
|
311
|
+
lines.push("");
|
|
312
|
+
lines.push(matrix.description);
|
|
313
|
+
}
|
|
314
|
+
lines.push("");
|
|
315
|
+
if (matrix.rows.length === 0) {
|
|
316
|
+
lines.push("_No obligations in scope \u2014 the configured filter / include selected zero REQUIREMENTs._");
|
|
317
|
+
return lines.join("\n") + "\n";
|
|
318
|
+
}
|
|
319
|
+
if (matrix.columns.length === 0) {
|
|
320
|
+
lines.push("_No subjects in scope \u2014 `view.subjects` is empty._");
|
|
321
|
+
return lines.join("\n") + "\n";
|
|
322
|
+
}
|
|
323
|
+
const header = ["Obligation", ...matrix.columns.map((col) => escMd(col.label))];
|
|
324
|
+
lines.push("| " + header.join(" | ") + " |");
|
|
325
|
+
lines.push("|" + header.map(() => "---").join("|") + "|");
|
|
326
|
+
for (let r = 0; r < matrix.rows.length; r++) {
|
|
327
|
+
const row = matrix.rows[r];
|
|
328
|
+
const rowLabel = `${row.id}${row.name && row.name !== row.id ? ` \u2014 ${row.name}` : ""}`;
|
|
329
|
+
const cells = matrix.cells[r].map((cell2) => renderCell(cell2, matrix.emptyLabels));
|
|
330
|
+
lines.push("| " + [escMd(rowLabel), ...cells].join(" | ") + " |");
|
|
331
|
+
}
|
|
332
|
+
lines.push("");
|
|
333
|
+
lines.push("## Legend");
|
|
334
|
+
lines.push("");
|
|
335
|
+
lines.push("- **OK / PARTIAL / FAIL / REVIEW / N/A** \u2014 aggregated `ASSERTION.status` per \xA75.2.");
|
|
336
|
+
lines.push(`- **${escMd(matrix.emptyLabels.no_obligation_label)}** \u2014 modelling gap; no admitted ASSERTION binds this (obligation, subject) pair.`);
|
|
337
|
+
lines.push(`- **${escMd(matrix.emptyLabels.no_obligation_applies_label)}** \u2014 modelled fact; an admitted ASSERTION with status \`n_a\` excludes this pair.`);
|
|
338
|
+
return lines.join("\n") + "\n";
|
|
339
|
+
}
|
|
340
|
+
function renderCell(cell2, labels) {
|
|
341
|
+
if (cell2.kind === "gap") return escMd(labels.no_obligation_label);
|
|
342
|
+
if (cell2.kind === "n_a_only") return escMd(labels.no_obligation_applies_label);
|
|
343
|
+
return STATUS_GLYPH[cell2.status];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ../diagrams/src/compliance/gap-report.ts
|
|
347
|
+
var SEVERITY_RANK = { high: 0, medium: 1, low: 2 };
|
|
348
|
+
function severityRank(s) {
|
|
349
|
+
return s !== void 0 && s in SEVERITY_RANK ? SEVERITY_RANK[s] : 3;
|
|
350
|
+
}
|
|
351
|
+
function allAssertions(index) {
|
|
352
|
+
const out = [];
|
|
353
|
+
for (const list of index.assertionsByRequirement.values()) out.push(...list);
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
356
|
+
function buildGapReport(index, options = {}) {
|
|
357
|
+
const { today } = options;
|
|
358
|
+
const requirementsWithoutAssertions = [...index.requirementById.values()].filter((r) => (index.assertionsByRequirement.get(r.id) ?? []).length === 0).sort((a, b) => {
|
|
359
|
+
const d = severityRank(a.severity) - severityRank(b.severity);
|
|
360
|
+
return d !== 0 ? d : a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
361
|
+
});
|
|
362
|
+
const assertions = allAssertions(index);
|
|
363
|
+
const assertionsWithoutEvidence = assertions.filter((a) => (a.status === "compliant" || a.status === "partial") && (a.evidenceCount ?? 0) === 0).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
364
|
+
const staleAssertions = (today ? assertions.filter((a) => typeof a.next_review_at === "string" && a.next_review_at < today) : []).sort((a, b) => {
|
|
365
|
+
const ra = a.next_review_at ?? "";
|
|
366
|
+
const rb = b.next_review_at ?? "";
|
|
367
|
+
return ra < rb ? -1 : ra > rb ? 1 : a.id < b.id ? -1 : 1;
|
|
368
|
+
});
|
|
369
|
+
const pastDeadlineRequirements = today ? [...index.requirementById.values()].filter((r) => {
|
|
370
|
+
if (computeDeadlineStatus(r.deadline, today) !== "past_due") return false;
|
|
371
|
+
const assertions2 = index.assertionsByRequirement.get(r.id) ?? [];
|
|
372
|
+
const hasCompliant = assertions2.some((a) => a.status === "compliant");
|
|
373
|
+
return !hasCompliant;
|
|
374
|
+
}).sort((a, b) => {
|
|
375
|
+
const da = a.deadline ?? "";
|
|
376
|
+
const db = b.deadline ?? "";
|
|
377
|
+
return da < db ? -1 : da > db ? 1 : a.id < b.id ? -1 : 1;
|
|
378
|
+
}) : [];
|
|
379
|
+
return { requirementsWithoutAssertions, assertionsWithoutEvidence, staleAssertions, pastDeadlineRequirements };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ../diagrams/src/compliance/classify.ts
|
|
383
|
+
function emptyCanon() {
|
|
384
|
+
return { products: [], requirements: [], assertions: [], codex: [] };
|
|
385
|
+
}
|
|
386
|
+
var str = (v) => typeof v === "string" ? v : void 0;
|
|
387
|
+
var strArray = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : void 0;
|
|
388
|
+
function ingestComplianceDoc(canon, doc) {
|
|
389
|
+
if (doc === null || typeof doc !== "object" || Array.isArray(doc)) return null;
|
|
390
|
+
const d = doc;
|
|
391
|
+
const id = str(d.id);
|
|
392
|
+
if (!id) return null;
|
|
393
|
+
if (d.notation === "product") {
|
|
394
|
+
canon.products.push({ id, name: str(d.name) ?? id });
|
|
395
|
+
return id;
|
|
396
|
+
}
|
|
397
|
+
if (d.notation === "requirement") {
|
|
398
|
+
canon.requirements.push({
|
|
399
|
+
id,
|
|
400
|
+
name: str(d.name) ?? id,
|
|
401
|
+
severity: str(d.severity),
|
|
402
|
+
derived_from: strArray(d.derived_from),
|
|
403
|
+
admitted_at: str(d.admitted_at),
|
|
404
|
+
deadline: str(d.deadline)
|
|
405
|
+
});
|
|
406
|
+
return id;
|
|
407
|
+
}
|
|
408
|
+
if (d.notation === "assertion") {
|
|
409
|
+
const about = str(d.about);
|
|
410
|
+
const subject = str(d.subject);
|
|
411
|
+
const status = str(d.status);
|
|
412
|
+
if (!about || !subject || !status) return null;
|
|
413
|
+
canon.assertions.push({
|
|
414
|
+
id,
|
|
415
|
+
about,
|
|
416
|
+
subject,
|
|
417
|
+
status,
|
|
418
|
+
assessed_at: str(d.assessed_at),
|
|
419
|
+
next_review_at: str(d.next_review_at),
|
|
420
|
+
evidenceCount: Array.isArray(d.evidence) ? d.evidence.length : 0,
|
|
421
|
+
admitted_at: str(d.admitted_at),
|
|
422
|
+
realised_via: strArray(d.realised_via)
|
|
423
|
+
});
|
|
424
|
+
return id;
|
|
425
|
+
}
|
|
426
|
+
if (d.zone === "codex") {
|
|
427
|
+
canon.codex.push({ id, name: str(d.name) ?? id, type: str(d.type), jurisdiction: str(d.jurisdiction) });
|
|
428
|
+
return id;
|
|
429
|
+
}
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ../diagrams/src/typed-id.ts
|
|
434
|
+
var TYPE_PREFIX_RE = /^([A-Z][A-Z0-9_]*)-/;
|
|
435
|
+
function typeOfId(id) {
|
|
436
|
+
if (typeof id !== "string") return null;
|
|
437
|
+
const m = TYPE_PREFIX_RE.exec(id);
|
|
438
|
+
return m ? m[1] : null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ../diagrams/src/compliance-matrix/build.ts
|
|
442
|
+
function pairKey(subject, about) {
|
|
443
|
+
return `${subject}\0${about}`;
|
|
444
|
+
}
|
|
445
|
+
function byId3(a, b) {
|
|
446
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
447
|
+
}
|
|
448
|
+
function buildComplianceMatrix(input) {
|
|
449
|
+
const productMap = /* @__PURE__ */ new Map();
|
|
450
|
+
for (const p of input.products) productMap.set(p.id, { id: p.id, name: p.name });
|
|
451
|
+
const byPair = /* @__PURE__ */ new Map();
|
|
452
|
+
let productAssertions = 0;
|
|
453
|
+
for (const a of input.assertions) {
|
|
454
|
+
if (typeOfId(a.subject) !== "PRODUCT") continue;
|
|
455
|
+
productAssertions++;
|
|
456
|
+
if (!productMap.has(a.subject)) {
|
|
457
|
+
productMap.set(a.subject, { id: a.subject, name: a.subject, unresolved: true });
|
|
458
|
+
}
|
|
459
|
+
byPair.set(pairKey(a.subject, a.about), a);
|
|
460
|
+
}
|
|
461
|
+
const products = [...productMap.values()].sort(byId3);
|
|
462
|
+
const jurisdictionByCodex = /* @__PURE__ */ new Map();
|
|
463
|
+
for (const c of input.codex ?? []) {
|
|
464
|
+
if (c.jurisdiction) jurisdictionByCodex.set(c.id, c.jurisdiction);
|
|
465
|
+
}
|
|
466
|
+
const requirements = [...input.requirements].sort(byId3).map((r) => {
|
|
467
|
+
const jset = /* @__PURE__ */ new Set();
|
|
468
|
+
for (const src of r.derived_from ?? []) {
|
|
469
|
+
const j = jurisdictionByCodex.get(src);
|
|
470
|
+
if (j) jset.add(j);
|
|
471
|
+
}
|
|
472
|
+
const jurisdictions = jset.size ? [...jset].sort() : void 0;
|
|
473
|
+
return { ...r, jurisdictions };
|
|
474
|
+
});
|
|
475
|
+
let gaps = 0;
|
|
476
|
+
const cells = products.map(
|
|
477
|
+
(p) => requirements.map((r) => {
|
|
478
|
+
const a = byPair.get(pairKey(p.id, r.id));
|
|
479
|
+
if (!a) {
|
|
480
|
+
gaps++;
|
|
481
|
+
return { productId: p.id, requirementId: r.id };
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
productId: p.id,
|
|
485
|
+
requirementId: r.id,
|
|
486
|
+
assertionId: a.id,
|
|
487
|
+
status: a.status,
|
|
488
|
+
assessed_at: a.assessed_at,
|
|
489
|
+
next_review_at: a.next_review_at
|
|
490
|
+
};
|
|
491
|
+
})
|
|
492
|
+
);
|
|
493
|
+
return {
|
|
494
|
+
products,
|
|
495
|
+
requirements,
|
|
496
|
+
cells,
|
|
497
|
+
summary: {
|
|
498
|
+
products: products.length,
|
|
499
|
+
requirements: requirements.length,
|
|
500
|
+
assertions: productAssertions,
|
|
501
|
+
gaps
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ../diagrams/src/compliance/markdown.ts
|
|
507
|
+
var STATUS_LABELS = {
|
|
508
|
+
compliant: "Compliant",
|
|
509
|
+
partial: "Partial",
|
|
510
|
+
non_compliant: "Non-compliant",
|
|
511
|
+
under_review: "Under review",
|
|
512
|
+
n_a: "N/A"
|
|
513
|
+
};
|
|
514
|
+
function cell(s) {
|
|
515
|
+
return s.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
516
|
+
}
|
|
517
|
+
function renderComplianceMarkdown(canon, scope, options = {}) {
|
|
518
|
+
switch (scope.mode) {
|
|
519
|
+
case "matrix":
|
|
520
|
+
return matrixMarkdown(canon);
|
|
521
|
+
case "law":
|
|
522
|
+
return lawMarkdown(canon, scope.id);
|
|
523
|
+
case "product":
|
|
524
|
+
return productMarkdown(canon, scope.id);
|
|
525
|
+
case "gap":
|
|
526
|
+
return gapMarkdown(canon, options.today);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function matrixMarkdown(canon) {
|
|
530
|
+
const m = buildComplianceMatrix({ products: canon.products, requirements: canon.requirements, assertions: canon.assertions });
|
|
531
|
+
const out = ["# Compliance Matrix", ""];
|
|
532
|
+
out.push(`_${m.summary.products} products \xD7 ${m.summary.requirements} requirements \xB7 ${m.summary.gaps} gaps \xB7 ${m.summary.assertions} assertions_`, "");
|
|
533
|
+
if (m.products.length === 0 || m.requirements.length === 0) {
|
|
534
|
+
out.push("_No products or requirements found in the scanned canon._", "");
|
|
535
|
+
return out.join("\n");
|
|
536
|
+
}
|
|
537
|
+
out.push(`| Product \\ Requirement | ${m.requirements.map((r) => cell(r.name)).join(" | ")} |`);
|
|
538
|
+
out.push(`|---|${m.requirements.map(() => "---").join("|")}|`);
|
|
539
|
+
m.products.forEach((p, ri) => {
|
|
540
|
+
const cells = m.requirements.map((_r, ci) => {
|
|
541
|
+
const c = m.cells[ri][ci];
|
|
542
|
+
return c.status ? STATUS_LABELS[c.status] : "\u2014";
|
|
543
|
+
});
|
|
544
|
+
const name = cell(p.name) + (p.unresolved ? " \u26A0" : "");
|
|
545
|
+
out.push(`| ${name} | ${cells.join(" | ")} |`);
|
|
546
|
+
});
|
|
547
|
+
out.push("");
|
|
548
|
+
return out.join("\n");
|
|
549
|
+
}
|
|
550
|
+
function lawMarkdown(canon, lawId) {
|
|
551
|
+
const index = buildComplianceIndex({ requirements: canon.requirements, assertions: canon.assertions });
|
|
552
|
+
const tree = buildLawTree(lawId, index);
|
|
553
|
+
const law = canon.codex.find((c) => c.id === lawId);
|
|
554
|
+
const out = [`# Compliance \u2014 ${law ? law.name : lawId}`, ""];
|
|
555
|
+
out.push(`\`${lawId}\` \xB7 ${tree.requirements.length} requirement(s)`, "");
|
|
556
|
+
if (tree.requirements.length === 0) {
|
|
557
|
+
out.push(`_No requirements derive from \`${lawId}\`._`, "");
|
|
558
|
+
return out.join("\n");
|
|
559
|
+
}
|
|
560
|
+
for (const node of tree.requirements) {
|
|
561
|
+
const sev = node.requirement.severity ? ` \u2014 severity: ${node.requirement.severity}` : "";
|
|
562
|
+
out.push(`## ${node.requirement.name} (\`${node.requirement.id}\`)${sev}`, "");
|
|
563
|
+
if (node.assertions.length === 0) {
|
|
564
|
+
out.push("_No assertion \u2014 compliance gap._", "");
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
for (const a of node.assertions) {
|
|
568
|
+
const meta = [a.assessed_at ? `assessed ${a.assessed_at}` : null, a.next_review_at ? `review by ${a.next_review_at}` : null].filter(Boolean).join(", ");
|
|
569
|
+
out.push(`- **${STATUS_LABELS[a.status]}** \u2014 \`${a.id}\` (subject \`${a.subject}\`${meta ? `; ${meta}` : ""})`);
|
|
570
|
+
}
|
|
571
|
+
out.push("");
|
|
572
|
+
}
|
|
573
|
+
return out.join("\n");
|
|
574
|
+
}
|
|
575
|
+
function productMarkdown(canon, productId) {
|
|
576
|
+
const index = buildComplianceIndex({ requirements: canon.requirements, assertions: canon.assertions });
|
|
577
|
+
const view = buildProductView(productId, index);
|
|
578
|
+
const product = canon.products.find((p) => p.id === productId);
|
|
579
|
+
const out = [`# Compliance \u2014 ${product ? product.name : productId}`, ""];
|
|
580
|
+
out.push(`\`${productId}\` \xB7 ${view.requirements.length} requirement(s) asserted`, "");
|
|
581
|
+
if (view.requirements.length === 0) {
|
|
582
|
+
out.push("_No assertion names this product as its subject._", "");
|
|
583
|
+
return out.join("\n");
|
|
584
|
+
}
|
|
585
|
+
out.push("| Requirement | Status | Assertion | Next review |", "|---|---|---|---|");
|
|
586
|
+
for (const { requirement, assertion } of view.requirements) {
|
|
587
|
+
out.push(`| ${cell(requirement.name)} (\`${requirement.id}\`) | ${STATUS_LABELS[assertion.status]} | \`${assertion.id}\` | ${assertion.next_review_at ?? "\u2014"} |`);
|
|
588
|
+
}
|
|
589
|
+
out.push("");
|
|
590
|
+
return out.join("\n");
|
|
591
|
+
}
|
|
592
|
+
function gapMarkdown(canon, today) {
|
|
593
|
+
const index = buildComplianceIndex({ requirements: canon.requirements, assertions: canon.assertions });
|
|
594
|
+
const report = buildGapReport(index, { today });
|
|
595
|
+
const total = report.requirementsWithoutAssertions.length + report.assertionsWithoutEvidence.length + report.staleAssertions.length + report.pastDeadlineRequirements.length;
|
|
596
|
+
const out = ["# Compliance Gap Dashboard", "", `_${total} gap(s) across 4 checks_`, ""];
|
|
597
|
+
out.push(`## Requirements without assertions (${report.requirementsWithoutAssertions.length})`, "");
|
|
598
|
+
if (report.requirementsWithoutAssertions.length === 0) out.push("_\u2713 none_", "");
|
|
599
|
+
else {
|
|
600
|
+
for (const r of report.requirementsWithoutAssertions) out.push(`- [ ] \`${r.id}\` ${r.name}${r.severity ? ` \u2014 severity: ${r.severity}` : ""}${r.deadline ? ` \u2014 deadline: ${r.deadline}` : ""}`);
|
|
601
|
+
out.push("");
|
|
602
|
+
}
|
|
603
|
+
out.push(`## Assertions without evidence \u2014 ASSERT-007 (${report.assertionsWithoutEvidence.length})`, "");
|
|
604
|
+
if (report.assertionsWithoutEvidence.length === 0) out.push("_\u2713 none_", "");
|
|
605
|
+
else {
|
|
606
|
+
for (const a of report.assertionsWithoutEvidence) out.push(`- [ ] \`${a.id}\` \u2014 about \`${a.about}\`, subject \`${a.subject}\`, status ${a.status}`);
|
|
607
|
+
out.push("");
|
|
608
|
+
}
|
|
609
|
+
out.push(`## Stale assertions \u2014 ASSERT-008 (${report.staleAssertions.length})`, "");
|
|
610
|
+
if (report.staleAssertions.length === 0) out.push("_\u2713 none_", "");
|
|
611
|
+
else {
|
|
612
|
+
for (const a of report.staleAssertions) out.push(`- [ ] \`${a.id}\` \u2014 review due ${a.next_review_at ?? "\u2014"}, subject \`${a.subject}\``);
|
|
613
|
+
out.push("");
|
|
614
|
+
}
|
|
615
|
+
out.push(`## Past-deadline requirements \u2014 CV-5 (${report.pastDeadlineRequirements.length})`, "");
|
|
616
|
+
if (report.pastDeadlineRequirements.length === 0) out.push("_\u2713 none_", "");
|
|
617
|
+
else {
|
|
618
|
+
for (const r of report.pastDeadlineRequirements) out.push(`- [ ] \`${r.id}\` ${r.name} \u2014 deadline: ${r.deadline ?? "\u2014"}${r.severity ? `, severity: ${r.severity}` : ""}`);
|
|
619
|
+
out.push("");
|
|
620
|
+
}
|
|
621
|
+
return out.join("\n");
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ../diagrams/src/compliance/html.ts
|
|
625
|
+
var STATUS_LABELS2 = {
|
|
626
|
+
compliant: "Compliant",
|
|
627
|
+
partial: "Partial",
|
|
628
|
+
non_compliant: "Non-compliant",
|
|
629
|
+
under_review: "Under review",
|
|
630
|
+
n_a: "N/A"
|
|
631
|
+
};
|
|
632
|
+
function escHtml(s) {
|
|
633
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
634
|
+
}
|
|
635
|
+
function badge(status) {
|
|
636
|
+
return `<span class="cmp-badge cmp-${status}">${escHtml(STATUS_LABELS2[status])}</span>`;
|
|
637
|
+
}
|
|
638
|
+
var CSS = `
|
|
639
|
+
@page {
|
|
640
|
+
size: A4 portrait;
|
|
641
|
+
margin: 18mm 16mm 22mm;
|
|
642
|
+
@bottom-left { content: "Transitrix \u2014 compliance report"; font: 9pt "Helvetica", "Arial", sans-serif; color: #475569; }
|
|
643
|
+
@bottom-right { content: "Page " counter(page) " / " counter(pages); font: 9pt "Helvetica", "Arial", sans-serif; color: #475569; }
|
|
644
|
+
}
|
|
645
|
+
html, body { margin: 0; padding: 0; }
|
|
646
|
+
body { font-family: "Helvetica", "Arial", sans-serif; font-size: 10pt; color: #0f172a; line-height: 1.4; }
|
|
647
|
+
.cmp-header { border-bottom: 2px solid #004d67; padding-bottom: 6pt; margin-bottom: 14pt; }
|
|
648
|
+
.cmp-header .cmp-eyebrow { font-size: 8.5pt; letter-spacing: 0.08em; text-transform: uppercase; color: #004d67; font-weight: 700; }
|
|
649
|
+
.cmp-header h1 { font-size: 17pt; margin: 2pt 0 4pt; color: #0f172a; }
|
|
650
|
+
.cmp-header .cmp-stamp { font-size: 9pt; color: #475569; }
|
|
651
|
+
h2 { font-size: 12pt; margin: 16pt 0 6pt; color: #0f172a; page-break-after: avoid; }
|
|
652
|
+
h2 .cmp-count { color: #64748b; font-weight: 400; font-size: 10pt; }
|
|
653
|
+
p.cmp-empty { color: #64748b; font-style: italic; }
|
|
654
|
+
table { border-collapse: collapse; width: 100%; font-size: 9.5pt; margin-bottom: 10pt; }
|
|
655
|
+
th, td { border: 0.5pt solid #cbd5e1; padding: 4pt 6pt; text-align: left; vertical-align: top; }
|
|
656
|
+
th { background: #f1f5f9; font-weight: 700; color: #0f172a; }
|
|
657
|
+
table.cmp-matrix th:first-child, table.cmp-matrix td:first-child { background: #f8fafc; font-weight: 600; }
|
|
658
|
+
code, .cmp-id { font-family: "Menlo", "Consolas", monospace; font-size: 9pt; color: #475569; }
|
|
659
|
+
.cmp-badge { display: inline-block; padding: 1pt 6pt; border-radius: 8pt; font-size: 8.5pt; font-weight: 700; }
|
|
660
|
+
.cmp-compliant { background: #d1fae5; color: #065f46; }
|
|
661
|
+
.cmp-partial { background: #fef9c3; color: #854d0e; }
|
|
662
|
+
.cmp-non_compliant { background: #fee2e2; color: #991b1b; }
|
|
663
|
+
.cmp-under_review { background: #e0f2fe; color: #0c4a6e; }
|
|
664
|
+
.cmp-n_a { background: #f1f5f9; color: #64748b; }
|
|
665
|
+
.cmp-cell-gap { color: #94a3b8; text-align: center; }
|
|
666
|
+
.cmp-req { border: 0.5pt solid #cbd5e1; border-radius: 3pt; margin-bottom: 8pt; page-break-inside: avoid; }
|
|
667
|
+
.cmp-req-head { background: #f1f5f9; padding: 5pt 8pt; }
|
|
668
|
+
.cmp-req-head .cmp-req-name { font-weight: 700; color: #0f172a; }
|
|
669
|
+
.cmp-req-head .cmp-req-id { margin-left: 6pt; color: #475569; font-size: 9pt; font-family: "Menlo", "Consolas", monospace; }
|
|
670
|
+
.cmp-sev { font-size: 8pt; text-transform: uppercase; letter-spacing: 0.06em; margin-left: 8pt; }
|
|
671
|
+
.cmp-sev-high { color: #b91c1c; }
|
|
672
|
+
.cmp-sev-medium { color: #b45309; }
|
|
673
|
+
.cmp-sev-low { color: #2563eb; }
|
|
674
|
+
.cmp-assertions { list-style: none; margin: 0; padding: 0; }
|
|
675
|
+
.cmp-assertions li { padding: 4pt 8pt 4pt 16pt; border-top: 0.5pt solid #e2e8f0; }
|
|
676
|
+
.cmp-assertions li .cmp-meta { color: #475569; font-size: 8.5pt; margin-left: 6pt; }
|
|
677
|
+
.cmp-section { margin-bottom: 12pt; }
|
|
678
|
+
.cmp-rows { list-style: none; margin: 0; padding: 0; }
|
|
679
|
+
.cmp-rows li { padding: 3pt 0 3pt 14pt; border-bottom: 0.5pt solid #e2e8f0; text-indent: -10pt; }
|
|
680
|
+
.cmp-rows li::before { content: "\u2610"; color: #94a3b8; margin-right: 6pt; }
|
|
681
|
+
.cmp-ok { color: #065f46; font-weight: 600; }
|
|
682
|
+
.cmp-summary { font-size: 9pt; color: #475569; }
|
|
683
|
+
`;
|
|
684
|
+
function htmlDoc(title, today, body) {
|
|
685
|
+
const stamp = today ? `Generated ${escHtml(today)}` : "";
|
|
686
|
+
return [
|
|
687
|
+
"<!doctype html>",
|
|
688
|
+
'<html lang="en">',
|
|
689
|
+
"<head>",
|
|
690
|
+
'<meta charset="utf-8"/>',
|
|
691
|
+
`<title>${escHtml(title)}</title>`,
|
|
692
|
+
`<style>${CSS}</style>`,
|
|
693
|
+
"</head>",
|
|
694
|
+
"<body>",
|
|
695
|
+
'<header class="cmp-header">',
|
|
696
|
+
'<div class="cmp-eyebrow">Transitrix \xB7 Compliance</div>',
|
|
697
|
+
`<h1>${escHtml(title)}</h1>`,
|
|
698
|
+
stamp ? `<div class="cmp-stamp">${stamp}</div>` : "",
|
|
699
|
+
"</header>",
|
|
700
|
+
body,
|
|
701
|
+
"</body>",
|
|
702
|
+
"</html>",
|
|
703
|
+
""
|
|
704
|
+
].join("\n");
|
|
705
|
+
}
|
|
706
|
+
function renderComplianceHtml(canon, scope, options = {}) {
|
|
707
|
+
switch (scope.mode) {
|
|
708
|
+
case "matrix": {
|
|
709
|
+
const m = buildComplianceMatrix({ products: canon.products, requirements: canon.requirements, assertions: canon.assertions });
|
|
710
|
+
const title = options.title ?? "Compliance Matrix";
|
|
711
|
+
const summary = `<p class="cmp-summary">${m.summary.products} products \xD7 ${m.summary.requirements} requirements \xB7 ${m.summary.gaps} gaps \xB7 ${m.summary.assertions} assertions</p>`;
|
|
712
|
+
if (m.products.length === 0 || m.requirements.length === 0) {
|
|
713
|
+
return htmlDoc(title, options.today, `${summary}<p class="cmp-empty">No products or requirements found in the scanned canon.</p>`);
|
|
714
|
+
}
|
|
715
|
+
const head = `<tr><th>Product \\ Requirement</th>${m.requirements.map((r) => `<th>${escHtml(r.name)}</th>`).join("")}</tr>`;
|
|
716
|
+
const rows = m.products.map((p, ri) => {
|
|
717
|
+
const cells = m.requirements.map((_r, ci) => {
|
|
718
|
+
const c = m.cells[ri][ci];
|
|
719
|
+
return c.status ? `<td>${badge(c.status)}</td>` : '<td class="cmp-cell-gap">\u2014</td>';
|
|
720
|
+
}).join("");
|
|
721
|
+
const name = escHtml(p.name) + (p.unresolved ? ' <span class="cmp-sev cmp-sev-high">unresolved</span>' : "");
|
|
722
|
+
return `<tr><td>${name}</td>${cells}</tr>`;
|
|
723
|
+
}).join("");
|
|
724
|
+
return htmlDoc(title, options.today, `${summary}<table class="cmp-matrix"><thead>${head}</thead><tbody>${rows}</tbody></table>`);
|
|
725
|
+
}
|
|
726
|
+
case "law": {
|
|
727
|
+
const index = buildComplianceIndex({ requirements: canon.requirements, assertions: canon.assertions });
|
|
728
|
+
const tree = buildLawTree(scope.id, index);
|
|
729
|
+
const law = canon.codex.find((c) => c.id === scope.id);
|
|
730
|
+
const title = options.title ?? `Compliance \u2014 ${law ? law.name : scope.id}`;
|
|
731
|
+
const summary = `<p class="cmp-summary"><code>${escHtml(scope.id)}</code> \xB7 ${tree.requirements.length} requirement(s)</p>`;
|
|
732
|
+
if (tree.requirements.length === 0) {
|
|
733
|
+
return htmlDoc(title, options.today, `${summary}<p class="cmp-empty">No requirements derive from <code>${escHtml(scope.id)}</code>.</p>`);
|
|
734
|
+
}
|
|
735
|
+
const blocks = tree.requirements.map((node) => {
|
|
736
|
+
const sev = node.requirement.severity ? ` <span class="cmp-sev cmp-sev-${escHtml(node.requirement.severity)}">${escHtml(node.requirement.severity)}</span>` : "";
|
|
737
|
+
const head = `<div class="cmp-req-head"><span class="cmp-req-name">${escHtml(node.requirement.name)}</span><span class="cmp-req-id">${escHtml(node.requirement.id)}</span>${sev}</div>`;
|
|
738
|
+
if (node.assertions.length === 0) {
|
|
739
|
+
return `<section class="cmp-req">${head}<ul class="cmp-assertions"><li><em>No assertion \u2014 compliance gap.</em></li></ul></section>`;
|
|
740
|
+
}
|
|
741
|
+
const items = node.assertions.map((a) => {
|
|
742
|
+
const metaParts = [];
|
|
743
|
+
if (a.assessed_at) metaParts.push(`assessed ${escHtml(a.assessed_at)}`);
|
|
744
|
+
if (a.next_review_at) metaParts.push(`review by ${escHtml(a.next_review_at)}`);
|
|
745
|
+
const meta = metaParts.length > 0 ? `<span class="cmp-meta">${metaParts.join(" \xB7 ")}</span>` : "";
|
|
746
|
+
return `<li>${badge(a.status)} <code>${escHtml(a.id)}</code> <span class="cmp-meta">subject <code>${escHtml(a.subject)}</code></span> ${meta}</li>`;
|
|
747
|
+
}).join("");
|
|
748
|
+
return `<section class="cmp-req">${head}<ul class="cmp-assertions">${items}</ul></section>`;
|
|
749
|
+
}).join("");
|
|
750
|
+
return htmlDoc(title, options.today, `${summary}${blocks}`);
|
|
751
|
+
}
|
|
752
|
+
case "product": {
|
|
753
|
+
const index = buildComplianceIndex({ requirements: canon.requirements, assertions: canon.assertions });
|
|
754
|
+
const view = buildProductView(scope.id, index);
|
|
755
|
+
const product = canon.products.find((p) => p.id === scope.id);
|
|
756
|
+
const title = options.title ?? `Compliance \u2014 ${product ? product.name : scope.id}`;
|
|
757
|
+
const summary = `<p class="cmp-summary"><code>${escHtml(scope.id)}</code> \xB7 ${view.requirements.length} requirement(s) asserted</p>`;
|
|
758
|
+
if (view.requirements.length === 0) {
|
|
759
|
+
return htmlDoc(title, options.today, `${summary}<p class="cmp-empty">No assertion names this product as its subject.</p>`);
|
|
760
|
+
}
|
|
761
|
+
const rows = view.requirements.map(({ requirement, assertion }) => `<tr><td>${escHtml(requirement.name)} <code>${escHtml(requirement.id)}</code></td><td>${badge(assertion.status)}</td><td><code>${escHtml(assertion.id)}</code></td><td>${assertion.next_review_at ? escHtml(assertion.next_review_at) : "\u2014"}</td></tr>`).join("");
|
|
762
|
+
const head = "<tr><th>Requirement</th><th>Status</th><th>Assertion</th><th>Next review</th></tr>";
|
|
763
|
+
return htmlDoc(title, options.today, `${summary}<table><thead>${head}</thead><tbody>${rows}</tbody></table>`);
|
|
764
|
+
}
|
|
765
|
+
case "gap": {
|
|
766
|
+
const index = buildComplianceIndex({ requirements: canon.requirements, assertions: canon.assertions });
|
|
767
|
+
const report = buildGapReport(index, { today: options.today });
|
|
768
|
+
const title = options.title ?? "Compliance Gap Dashboard";
|
|
769
|
+
const total = report.requirementsWithoutAssertions.length + report.assertionsWithoutEvidence.length + report.staleAssertions.length;
|
|
770
|
+
const summary = `<p class="cmp-summary">${total} gap(s)</p>`;
|
|
771
|
+
const section = (heading, count, items) => {
|
|
772
|
+
const body2 = items.length === 0 ? '<p class="cmp-ok">\u2713 none</p>' : `<ul class="cmp-rows">${items.join("")}</ul>`;
|
|
773
|
+
return `<section class="cmp-section"><h2>${escHtml(heading)} <span class="cmp-count">(${count})</span></h2>${body2}</section>`;
|
|
774
|
+
};
|
|
775
|
+
const reqs = report.requirementsWithoutAssertions.map((r) => `<li><code>${escHtml(r.id)}</code> ${escHtml(r.name)}${r.severity ? ` <span class="cmp-meta">severity ${escHtml(r.severity)}</span>` : ""}${r.deadline ? ` <span class="cmp-meta">deadline ${escHtml(r.deadline)}</span>` : ""}</li>`);
|
|
776
|
+
const noEvidence = report.assertionsWithoutEvidence.map((a) => `<li><code>${escHtml(a.id)}</code> <span class="cmp-meta">about <code>${escHtml(a.about)}</code>, subject <code>${escHtml(a.subject)}</code>, status ${escHtml(a.status)}</span></li>`);
|
|
777
|
+
const stale = report.staleAssertions.map((a) => `<li><code>${escHtml(a.id)}</code> <span class="cmp-meta">review due ${a.next_review_at ? escHtml(a.next_review_at) : "\u2014"}, subject <code>${escHtml(a.subject)}</code></span></li>`);
|
|
778
|
+
const pastDl = report.pastDeadlineRequirements.map((r) => `<li><code>${escHtml(r.id)}</code> ${escHtml(r.name)} <span class="cmp-meta">deadline ${r.deadline ? escHtml(r.deadline) : "\u2014"}${r.severity ? `, severity ${escHtml(r.severity)}` : ""}</span></li>`);
|
|
779
|
+
const total4 = report.requirementsWithoutAssertions.length + report.assertionsWithoutEvidence.length + report.staleAssertions.length + report.pastDeadlineRequirements.length;
|
|
780
|
+
const body = [
|
|
781
|
+
`<p class="cmp-summary">${total4} gap(s) across 4 checks</p>`,
|
|
782
|
+
section("Requirements without assertions", report.requirementsWithoutAssertions.length, reqs),
|
|
783
|
+
section("Assertions without evidence \u2014 ASSERT-007", report.assertionsWithoutEvidence.length, noEvidence),
|
|
784
|
+
section("Stale assertions \u2014 ASSERT-008", report.staleAssertions.length, stale),
|
|
785
|
+
section("Past-deadline requirements (CV-5)", report.pastDeadlineRequirements.length, pastDl)
|
|
786
|
+
].join("");
|
|
787
|
+
return htmlDoc(title, options.today, body);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function renderImpactMatrixHtml(matrix, options = {}) {
|
|
792
|
+
const STATUS_GLYPH2 = {
|
|
793
|
+
compliant: "OK",
|
|
794
|
+
partial: "PARTIAL",
|
|
795
|
+
non_compliant: "FAIL",
|
|
796
|
+
under_review: "REVIEW",
|
|
797
|
+
n_a: "N/A"
|
|
798
|
+
};
|
|
799
|
+
const statusClass = (s) => s ?? "gap";
|
|
800
|
+
const colHeaders = matrix.columns.map((c) => `<th>${escHtml(c.label)}</th>`).join("");
|
|
801
|
+
const head = `<tr><th>Obligation</th>${colHeaders}</tr>`;
|
|
802
|
+
const rows = matrix.rows.map((req, ri) => {
|
|
803
|
+
const rowLabel = `${escHtml(req.id)}${req.name && req.name !== req.id ? ` \u2014 ${escHtml(req.name)}` : ""}`;
|
|
804
|
+
const cells = matrix.cells[ri].map((cell2) => {
|
|
805
|
+
if (cell2.kind === "gap") return `<td class="cmp-gap">${escHtml(matrix.emptyLabels.no_obligation_label)}</td>`;
|
|
806
|
+
if (cell2.kind === "n_a_only") return `<td class="cmp-na">${escHtml(matrix.emptyLabels.no_obligation_applies_label)}</td>`;
|
|
807
|
+
const glyph = STATUS_GLYPH2[cell2.status ?? ""] ?? String(cell2.status);
|
|
808
|
+
return `<td class="cmp-${statusClass(cell2.status)}">${escHtml(glyph)}</td>`;
|
|
809
|
+
}).join("");
|
|
810
|
+
const dlInfo = req.deadline ? ` <span class="cmp-dl">\u2691 ${escHtml(req.deadline)}</span>` : "";
|
|
811
|
+
return `<tr><td class="cmp-row-label">${rowLabel}${dlInfo}</td>${cells}</tr>`;
|
|
812
|
+
}).join("");
|
|
813
|
+
const snapshotLine = matrix.snapshotAt ? `<span>Snapshot: ${escHtml(matrix.snapshotAt)}</span>` : "";
|
|
814
|
+
const desc = matrix.description ? `<p>${escHtml(matrix.description)}</p>` : "";
|
|
815
|
+
const extraCss = `
|
|
816
|
+
table { border-collapse: collapse; width: 100%; font-size: 9pt; }
|
|
817
|
+
th, td { border: 1px solid #cbd5e1; padding: 3pt 5pt; text-align: center; vertical-align: middle; }
|
|
818
|
+
td.cmp-row-label { text-align: left; font-size: 8.5pt; max-width: 180pt; white-space: normal; }
|
|
819
|
+
.cmp-compliant { background: #d1fae5; color: #065f46; font-weight: 700; }
|
|
820
|
+
.cmp-partial { background: #fef9c3; color: #854d0e; font-weight: 700; }
|
|
821
|
+
.cmp-non_compliant { background: #fee2e2; color: #991b1b; font-weight: 700; }
|
|
822
|
+
.cmp-under_review { background: #e0f2fe; color: #0c4a6e; }
|
|
823
|
+
.cmp-gap { background: #f8fafc; color: #94a3b8; font-size: 8pt; }
|
|
824
|
+
.cmp-na { background: #f1f5f9; color: #94a3b8; font-size: 8pt; }
|
|
825
|
+
.cmp-dl { color: #b45309; font-size: 8pt; }`;
|
|
826
|
+
return `<!DOCTYPE html>
|
|
827
|
+
<html lang="en">
|
|
828
|
+
<head>
|
|
829
|
+
<meta charset="UTF-8">
|
|
830
|
+
<style>
|
|
831
|
+
${CSS}
|
|
832
|
+
${extraCss}
|
|
833
|
+
</style>
|
|
834
|
+
</head>
|
|
835
|
+
<body>
|
|
836
|
+
<div class="cmp-header">
|
|
837
|
+
<div class="cmp-eyebrow">Compliance Impact Matrix</div>
|
|
838
|
+
<h1>${escHtml(matrix.viewName)}</h1>
|
|
839
|
+
<div class="cmp-stamp">View: <code>${escHtml(matrix.viewId)}</code> ${snapshotLine}${options.today ? ` \xB7 Generated: ${escHtml(options.today)}` : ""}</div>
|
|
840
|
+
</div>
|
|
841
|
+
${desc}
|
|
842
|
+
<table><thead>${head}</thead><tbody>${rows}</tbody></table>
|
|
843
|
+
</body>
|
|
844
|
+
</html>`;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// ../diagrams/src/confidence/score.ts
|
|
848
|
+
var SOURCE_TRUST_WEIGHTS = {
|
|
849
|
+
authoritative: 1,
|
|
850
|
+
corroborated: 0.8,
|
|
851
|
+
single_source: 0.5,
|
|
852
|
+
unverified: 0.25
|
|
853
|
+
};
|
|
854
|
+
var UNVERIFIED_WEIGHT = SOURCE_TRUST_WEIGHTS.unverified;
|
|
855
|
+
|
|
856
|
+
// ../../src/export-compliance.ts
|
|
857
|
+
function flagValue(argv, name) {
|
|
858
|
+
const i = argv.indexOf(name);
|
|
859
|
+
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : void 0;
|
|
860
|
+
}
|
|
861
|
+
function loadViewConfigRaw(registry, reportId, root) {
|
|
862
|
+
const candidates = [];
|
|
863
|
+
if (registry) {
|
|
864
|
+
candidates.push(
|
|
865
|
+
path.join(registry, `${reportId}.compliance-impact.view.yaml`),
|
|
866
|
+
path.join(registry, `${reportId}.yaml`)
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
candidates.push(
|
|
870
|
+
path.join(root, `${reportId}.compliance-impact.view.yaml`),
|
|
871
|
+
path.join(root, `${reportId}.yaml`)
|
|
872
|
+
);
|
|
873
|
+
for (const candidate of candidates) {
|
|
874
|
+
try {
|
|
875
|
+
const raw = yaml.load(readFileSync(candidate, "utf-8"));
|
|
876
|
+
return raw;
|
|
877
|
+
} catch {
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
try {
|
|
881
|
+
const entries = readdirSync(root, { recursive: true });
|
|
882
|
+
for (const rel of entries) {
|
|
883
|
+
if (typeof rel !== "string") continue;
|
|
884
|
+
if (!/\.ya?ml$/i.test(rel)) continue;
|
|
885
|
+
if (rel.split(/[\\/]/).includes("node_modules")) continue;
|
|
886
|
+
try {
|
|
887
|
+
const raw = yaml.load(readFileSync(path.join(root, rel), "utf-8"));
|
|
888
|
+
if (raw && typeof raw === "object") {
|
|
889
|
+
const r = raw;
|
|
890
|
+
const inner = r.view && typeof r.view === "object" ? r.view : r;
|
|
891
|
+
if (inner.id === reportId) return raw;
|
|
892
|
+
}
|
|
893
|
+
} catch {
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
} catch {
|
|
897
|
+
}
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
function scanCanonFs(root) {
|
|
901
|
+
const canon = emptyCanon();
|
|
902
|
+
let entries = [];
|
|
903
|
+
try {
|
|
904
|
+
entries = readdirSync(root, { recursive: true });
|
|
905
|
+
} catch {
|
|
906
|
+
return canon;
|
|
907
|
+
}
|
|
908
|
+
for (const rel of entries) {
|
|
909
|
+
if (typeof rel !== "string" || !/\.ya?ml$/i.test(rel)) continue;
|
|
910
|
+
if (rel.split(/[\\/]/).includes("node_modules")) continue;
|
|
911
|
+
let parsed;
|
|
912
|
+
try {
|
|
913
|
+
parsed = yaml.load(readFileSync(path.join(root, rel), "utf-8"));
|
|
914
|
+
} catch {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
ingestComplianceDoc(canon, parsed);
|
|
918
|
+
}
|
|
919
|
+
return canon;
|
|
920
|
+
}
|
|
921
|
+
function parseScope(scopeArg) {
|
|
922
|
+
if (!scopeArg || scopeArg === "matrix") return { mode: "matrix" };
|
|
923
|
+
if (scopeArg === "gap") return { mode: "gap" };
|
|
924
|
+
if (scopeArg.startsWith("law:")) return { mode: "law", id: scopeArg.slice("law:".length) };
|
|
925
|
+
if (scopeArg.startsWith("product:")) return { mode: "product", id: scopeArg.slice("product:".length) };
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
var WEASYPRINT_TIMEOUT_MS = 12e4;
|
|
929
|
+
function runWeasyPrint(htmlPath, pdfPath, opts) {
|
|
930
|
+
const timeoutMs = opts?.timeoutMs ?? WEASYPRINT_TIMEOUT_MS;
|
|
931
|
+
const candidates = process.platform === "win32" ? ["weasyprint.exe", "weasyprint"] : ["weasyprint"];
|
|
932
|
+
let lastErr = null;
|
|
933
|
+
for (const cmd of candidates) {
|
|
934
|
+
const res = spawnSync(cmd, [htmlPath, pdfPath], { encoding: "utf-8", timeout: timeoutMs });
|
|
935
|
+
if (res.error) {
|
|
936
|
+
const err = res.error;
|
|
937
|
+
if (err.code === "ENOENT") {
|
|
938
|
+
lastErr = err;
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
if (err.code === "ETIMEDOUT") {
|
|
942
|
+
return {
|
|
943
|
+
ok: false,
|
|
944
|
+
message: `weasyprint timed out after ${Math.round(timeoutMs / 1e3)}s \u2014 the report may be too large or WeasyPrint may be hung.`
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
return { ok: false, message: `weasyprint failed to launch: ${err.message}` };
|
|
948
|
+
}
|
|
949
|
+
if (res.status !== 0) {
|
|
950
|
+
const stderr = (res.stderr || "").trim();
|
|
951
|
+
return {
|
|
952
|
+
ok: false,
|
|
953
|
+
message: `weasyprint exited with code ${res.status}${stderr ? `:
|
|
954
|
+
${stderr}` : ""}`
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
return { ok: true };
|
|
958
|
+
}
|
|
959
|
+
return {
|
|
960
|
+
ok: false,
|
|
961
|
+
message: "weasyprint executable not found on PATH. PDF export requires WeasyPrint (https://weasyprint.org/) \u2014 install it (e.g. `pipx install weasyprint`) and re-run." + (lastErr ? ` Last error: ${lastErr.message}` : "")
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
function defaultPdfFilename(scope) {
|
|
965
|
+
switch (scope.mode) {
|
|
966
|
+
case "matrix":
|
|
967
|
+
return "compliance-matrix.pdf";
|
|
968
|
+
case "gap":
|
|
969
|
+
return "compliance-gap.pdf";
|
|
970
|
+
case "law":
|
|
971
|
+
return `compliance-${scope.id}.pdf`;
|
|
972
|
+
case "product":
|
|
973
|
+
return `compliance-${scope.id}.pdf`;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
async function handleExportComplianceCommand(argv) {
|
|
977
|
+
const format = (flagValue(argv, "--format") ?? "md").toLowerCase();
|
|
978
|
+
const output = flagValue(argv, "--output");
|
|
979
|
+
const root = flagValue(argv, "--root") ?? process.cwd();
|
|
980
|
+
const reportId = flagValue(argv, "--report");
|
|
981
|
+
const registry = flagValue(argv, "--registry");
|
|
982
|
+
if (format !== "md" && format !== "pdf") {
|
|
983
|
+
console.error(`export-compliance: unknown --format '${format}' (expected md or pdf).`);
|
|
984
|
+
process.exit(1);
|
|
985
|
+
}
|
|
986
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
987
|
+
if (reportId) {
|
|
988
|
+
const rawCfg = loadViewConfigRaw(registry, reportId, root);
|
|
989
|
+
if (!rawCfg) {
|
|
990
|
+
console.error(`export-compliance: view-config '${reportId}' not found. Searched${registry ? ` registry '${registry}'` : ""} and root '${root}'.`);
|
|
991
|
+
process.exit(1);
|
|
992
|
+
}
|
|
993
|
+
const parseResult = parseImpactViewConfig(rawCfg);
|
|
994
|
+
if (!parseResult.ok) {
|
|
995
|
+
console.error(`export-compliance: view-config '${reportId}' is invalid:
|
|
996
|
+
${parseResult.errors.map((e) => ` - ${e}`).join("\n")}`);
|
|
997
|
+
process.exit(1);
|
|
998
|
+
}
|
|
999
|
+
const viewConfig = parseResult.config;
|
|
1000
|
+
console.error(`[export-compliance] report: ${viewConfig.id} | format: ${format}`);
|
|
1001
|
+
const canon2 = scanCanonFs(root);
|
|
1002
|
+
const effectiveProducts = viewConfig.subjects?.products?.length ? viewConfig.subjects.products : canon2.products.map((p) => p.id).sort();
|
|
1003
|
+
const matrix = buildImpactMatrix(
|
|
1004
|
+
{ products: canon2.products, requirements: canon2.requirements, assertions: canon2.assertions, codex: canon2.codex },
|
|
1005
|
+
{ ...viewConfig, subjects: { ...viewConfig.subjects, products: effectiveProducts } }
|
|
1006
|
+
);
|
|
1007
|
+
if (format === "md") {
|
|
1008
|
+
const markdown = renderImpactMarkdown(matrix);
|
|
1009
|
+
if (output) {
|
|
1010
|
+
writeFileSync(output, markdown, "utf-8");
|
|
1011
|
+
console.error(`Wrote ${output}`);
|
|
1012
|
+
} else {
|
|
1013
|
+
process.stdout.write(markdown);
|
|
1014
|
+
}
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
const html2 = renderImpactMatrixHtml(matrix, { today });
|
|
1018
|
+
const pdfPath2 = output ?? `compliance-impact-${viewConfig.id}.pdf`;
|
|
1019
|
+
const tmpDir2 = mkdtempSync(path.join(os.tmpdir(), "cervin-export-"));
|
|
1020
|
+
const htmlPath2 = path.join(tmpDir2, "report.html");
|
|
1021
|
+
try {
|
|
1022
|
+
writeFileSync(htmlPath2, html2, "utf-8");
|
|
1023
|
+
const result = runWeasyPrint(htmlPath2, pdfPath2);
|
|
1024
|
+
if (!result.ok) {
|
|
1025
|
+
console.error(`export-compliance: ${result.message}`);
|
|
1026
|
+
process.exit(1);
|
|
1027
|
+
}
|
|
1028
|
+
console.error(`Wrote ${pdfPath2}`);
|
|
1029
|
+
} finally {
|
|
1030
|
+
try {
|
|
1031
|
+
rmSync(tmpDir2, { recursive: true, force: true });
|
|
1032
|
+
} catch {
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const scope = parseScope(flagValue(argv, "--scope"));
|
|
1038
|
+
if (scope === null) {
|
|
1039
|
+
console.error("export-compliance: unknown --scope (expected law:<LAW-ID>, product:<PRODUCT-ID>, gap, or omit for the full matrix). Use --report <id> for named view-config.");
|
|
1040
|
+
process.exit(1);
|
|
1041
|
+
}
|
|
1042
|
+
const canon = scanCanonFs(root);
|
|
1043
|
+
if (format === "md") {
|
|
1044
|
+
const markdown = renderComplianceMarkdown(canon, scope, { today });
|
|
1045
|
+
if (output) {
|
|
1046
|
+
writeFileSync(output, markdown, "utf-8");
|
|
1047
|
+
console.error(`Wrote ${output}`);
|
|
1048
|
+
} else {
|
|
1049
|
+
process.stdout.write(markdown);
|
|
1050
|
+
}
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
const html = renderComplianceHtml(canon, scope, { today });
|
|
1054
|
+
const pdfPath = output ?? defaultPdfFilename(scope);
|
|
1055
|
+
const tmpDir = mkdtempSync(path.join(os.tmpdir(), "cervin-export-"));
|
|
1056
|
+
const htmlPath = path.join(tmpDir, "report.html");
|
|
1057
|
+
try {
|
|
1058
|
+
writeFileSync(htmlPath, html, "utf-8");
|
|
1059
|
+
const result = runWeasyPrint(htmlPath, pdfPath);
|
|
1060
|
+
if (!result.ok) {
|
|
1061
|
+
console.error(`export-compliance: ${result.message}`);
|
|
1062
|
+
process.exit(1);
|
|
1063
|
+
}
|
|
1064
|
+
console.error(`Wrote ${pdfPath}`);
|
|
1065
|
+
} finally {
|
|
1066
|
+
try {
|
|
1067
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
1068
|
+
} catch {
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
export {
|
|
1073
|
+
WEASYPRINT_TIMEOUT_MS,
|
|
1074
|
+
handleExportComplianceCommand,
|
|
1075
|
+
runWeasyPrint
|
|
1076
|
+
};
|