@pactosigna/records 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/cli.d.ts +2 -0
  2. package/dist/cli.d.ts.map +1 -0
  3. package/dist/cli.js +5347 -0
  4. package/dist/config.d.ts +42 -0
  5. package/dist/config.d.ts.map +1 -0
  6. package/dist/generate.d.ts +7 -0
  7. package/dist/generate.d.ts.map +1 -0
  8. package/dist/generators/prs-generator.d.ts +6 -0
  9. package/dist/generators/prs-generator.d.ts.map +1 -0
  10. package/dist/generators/pta-generator.d.ts +20 -0
  11. package/dist/generators/pta-generator.d.ts.map +1 -0
  12. package/dist/generators/rmr-generator.d.ts +17 -0
  13. package/dist/generators/rmr-generator.d.ts.map +1 -0
  14. package/dist/generators/srs-generator.d.ts +6 -0
  15. package/dist/generators/srs-generator.d.ts.map +1 -0
  16. package/dist/generators/urs-generator.d.ts +12 -0
  17. package/dist/generators/urs-generator.d.ts.map +1 -0
  18. package/dist/github/changelog.d.ts +3 -0
  19. package/dist/github/changelog.d.ts.map +1 -0
  20. package/dist/github/draft-release.d.ts +2 -0
  21. package/dist/github/draft-release.d.ts.map +1 -0
  22. package/dist/github/publish.d.ts +2 -0
  23. package/dist/github/publish.d.ts.map +1 -0
  24. package/dist/index.d.ts +11 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +1167 -0
  27. package/dist/pdf/layout.d.ts +19 -0
  28. package/dist/pdf/layout.d.ts.map +1 -0
  29. package/dist/pdf/markdown-to-pdfmake.d.ts +9 -0
  30. package/dist/pdf/markdown-to-pdfmake.d.ts.map +1 -0
  31. package/dist/pdf/risk-matrix.d.ts +4 -0
  32. package/dist/pdf/risk-matrix.d.ts.map +1 -0
  33. package/dist/reader/document-resolver.d.ts +9 -0
  34. package/dist/reader/document-resolver.d.ts.map +1 -0
  35. package/dist/reader/markdown-reader.d.ts +11 -0
  36. package/dist/reader/markdown-reader.d.ts.map +1 -0
  37. package/package.json +68 -0
package/dist/index.js ADDED
@@ -0,0 +1,1167 @@
1
+ // src/generate.ts
2
+ import { mkdirSync, writeFileSync } from "fs";
3
+ import { resolve as pathResolve } from "path";
4
+ import PdfPrinterModule from "pdfmake/js/Printer.js";
5
+
6
+ // src/config.ts
7
+ import { z } from "zod";
8
+ import { readFileSync, existsSync } from "fs";
9
+ import { resolve } from "path";
10
+ import { parse as parseYaml } from "yaml";
11
+ var BrandingSchema = z.object({
12
+ logo: z.string().optional()
13
+ }).default({});
14
+ var ReleaseConfigSchema = z.object({
15
+ enabled: z.boolean().default(false),
16
+ attach_pdfs: z.boolean().default(true)
17
+ }).default({});
18
+ var RecordsConfigSchema = z.object({
19
+ branding: BrandingSchema.default({}),
20
+ signatures: z.array(z.string()).default(["Author", "Reviewer", "Approver"]),
21
+ release: ReleaseConfigSchema.default({})
22
+ });
23
+ function loadConfig(rootDir) {
24
+ const configPath = resolve(rootDir, "pactosigna.yaml");
25
+ if (!existsSync(configPath)) {
26
+ return RecordsConfigSchema.parse({});
27
+ }
28
+ const raw = readFileSync(configPath, "utf-8");
29
+ const parsed = parseYaml(raw);
30
+ const recordsSection = parsed.records ?? {};
31
+ return RecordsConfigSchema.parse(recordsSection);
32
+ }
33
+
34
+ // src/reader/markdown-reader.ts
35
+ import { readdirSync, readFileSync as readFileSync2 } from "fs";
36
+ import { resolve as resolve2, join } from "path";
37
+ import matter from "gray-matter";
38
+ var INCLUDED_STATUSES = /* @__PURE__ */ new Set(["approved", "effective"]);
39
+ function readDocuments(rootDir, folderPath, statusFilter = INCLUDED_STATUSES) {
40
+ const fullPath = resolve2(rootDir, folderPath);
41
+ let files;
42
+ try {
43
+ files = readdirSync(fullPath).filter((f) => f.endsWith(".md"));
44
+ } catch {
45
+ return [];
46
+ }
47
+ const docs = [];
48
+ const warnings = [];
49
+ for (const file of files) {
50
+ const filePath = join(folderPath, file);
51
+ const raw = readFileSync2(resolve2(rootDir, filePath), "utf-8");
52
+ const { data, content } = matter(raw);
53
+ if (!data.id || !data.title || !data.status) {
54
+ warnings.push(`Skipping ${filePath}: missing required frontmatter (id, title, or status)`);
55
+ continue;
56
+ }
57
+ if (!statusFilter.has(data.status)) {
58
+ continue;
59
+ }
60
+ docs.push({
61
+ frontmatter: data,
62
+ body: content.trim(),
63
+ filePath
64
+ });
65
+ }
66
+ for (const w of warnings) {
67
+ console.warn(w);
68
+ }
69
+ return docs.sort(
70
+ (a, b) => a.frontmatter.id.localeCompare(b.frontmatter.id, void 0, { numeric: true })
71
+ );
72
+ }
73
+
74
+ // src/pdf/markdown-to-pdfmake.ts
75
+ import MarkdownIt from "markdown-it";
76
+ var md = new MarkdownIt();
77
+ function markdownToPdfmake(markdown) {
78
+ if (!markdown.trim()) return [];
79
+ const tokens = md.parse(markdown, {});
80
+ const result = [];
81
+ let i = 0;
82
+ while (i < tokens.length) {
83
+ const token = tokens[i];
84
+ if (token.type === "heading_open") {
85
+ const level = token.tag;
86
+ const inlineToken = tokens[i + 1];
87
+ const fragments = inlineToken ? parseInlineContent(inlineToken) : [];
88
+ result.push({
89
+ text: fragments.length === 1 && typeof fragments[0] === "string" ? fragments[0] : fragments,
90
+ style: level,
91
+ margin: [0, 12, 0, 4]
92
+ });
93
+ i += 3;
94
+ continue;
95
+ }
96
+ if (token.type === "paragraph_open") {
97
+ const inlineToken = tokens[i + 1];
98
+ const fragments = inlineToken ? parseInlineContent(inlineToken) : [];
99
+ result.push({
100
+ text: fragments,
101
+ margin: [0, 0, 0, 6]
102
+ });
103
+ i += 3;
104
+ continue;
105
+ }
106
+ if (token.type === "bullet_list_open") {
107
+ const { items, endIndex } = parseListItems(tokens, i + 1);
108
+ result.push({
109
+ ul: items,
110
+ margin: [0, 0, 0, 6]
111
+ });
112
+ i = endIndex + 1;
113
+ continue;
114
+ }
115
+ if (token.type === "ordered_list_open") {
116
+ const { items, endIndex } = parseListItems(tokens, i + 1);
117
+ result.push({
118
+ ol: items,
119
+ margin: [0, 0, 0, 6]
120
+ });
121
+ i = endIndex + 1;
122
+ continue;
123
+ }
124
+ if (token.type === "fence") {
125
+ result.push({
126
+ text: token.content,
127
+ style: "code",
128
+ margin: [0, 4, 0, 4],
129
+ background: "#F5F5F5"
130
+ });
131
+ i += 1;
132
+ continue;
133
+ }
134
+ if (token.type === "table_open") {
135
+ const { table, endIndex } = parseTable(tokens, i + 1);
136
+ result.push(table);
137
+ i = endIndex + 1;
138
+ continue;
139
+ }
140
+ i += 1;
141
+ }
142
+ return result;
143
+ }
144
+ function parseInlineContent(token) {
145
+ const children = token.children ?? [];
146
+ const fragments = [];
147
+ let bold = false;
148
+ let italics = false;
149
+ for (const child of children) {
150
+ switch (child.type) {
151
+ case "strong_open":
152
+ bold = true;
153
+ break;
154
+ case "strong_close":
155
+ bold = false;
156
+ break;
157
+ case "em_open":
158
+ italics = true;
159
+ break;
160
+ case "em_close":
161
+ italics = false;
162
+ break;
163
+ case "code_inline":
164
+ fragments.push({ text: child.content, font: "Courier", fontSize: 9 });
165
+ break;
166
+ case "softbreak":
167
+ case "hardbreak":
168
+ fragments.push("\n");
169
+ break;
170
+ case "text":
171
+ if (bold || italics) {
172
+ const fragment = { text: child.content };
173
+ if (bold) fragment.bold = true;
174
+ if (italics) fragment.italics = true;
175
+ fragments.push(fragment);
176
+ } else {
177
+ fragments.push(child.content);
178
+ }
179
+ break;
180
+ default:
181
+ if (child.content) {
182
+ fragments.push(child.content);
183
+ }
184
+ break;
185
+ }
186
+ }
187
+ return fragments;
188
+ }
189
+ function parseListItems(tokens, startIndex) {
190
+ const items = [];
191
+ let i = startIndex;
192
+ while (i < tokens.length) {
193
+ const token = tokens[i];
194
+ if (token.type === "bullet_list_close" || token.type === "ordered_list_close") {
195
+ return { items, endIndex: i };
196
+ }
197
+ if (token.type === "list_item_open") {
198
+ const inlineToken = tokens[i + 2];
199
+ if (inlineToken?.type === "inline") {
200
+ const fragments = parseInlineContent(inlineToken);
201
+ items.push({
202
+ text: fragments
203
+ });
204
+ }
205
+ while (i < tokens.length && tokens[i].type !== "list_item_close") {
206
+ i += 1;
207
+ }
208
+ i += 1;
209
+ continue;
210
+ }
211
+ i += 1;
212
+ }
213
+ return { items, endIndex: i - 1 };
214
+ }
215
+ function parseTable(tokens, startIndex) {
216
+ const rows = [];
217
+ let currentRow = [];
218
+ let i = startIndex;
219
+ while (i < tokens.length) {
220
+ const token = tokens[i];
221
+ if (token.type === "table_close") {
222
+ return {
223
+ table: {
224
+ table: {
225
+ headerRows: 1,
226
+ body: rows
227
+ },
228
+ layout: "lightHorizontalLines",
229
+ margin: [0, 4, 0, 8]
230
+ },
231
+ endIndex: i
232
+ };
233
+ }
234
+ if (token.type === "tr_open") {
235
+ currentRow = [];
236
+ i += 1;
237
+ continue;
238
+ }
239
+ if (token.type === "tr_close") {
240
+ if (currentRow.length > 0) {
241
+ rows.push(currentRow);
242
+ }
243
+ i += 1;
244
+ continue;
245
+ }
246
+ if (token.type === "th_open" || token.type === "td_open") {
247
+ const inlineToken = tokens[i + 1];
248
+ if (inlineToken?.type === "inline") {
249
+ const fragments = parseInlineContent(inlineToken);
250
+ const isHeader = token.type === "th_open";
251
+ currentRow.push({
252
+ text: fragments,
253
+ bold: isHeader || void 0
254
+ });
255
+ } else {
256
+ currentRow.push({ text: "" });
257
+ }
258
+ while (i < tokens.length && tokens[i].type !== "th_close" && tokens[i].type !== "td_close") {
259
+ i += 1;
260
+ }
261
+ i += 1;
262
+ continue;
263
+ }
264
+ i += 1;
265
+ }
266
+ return {
267
+ table: {
268
+ table: { headerRows: 1, body: rows },
269
+ layout: "lightHorizontalLines",
270
+ margin: [0, 4, 0, 8]
271
+ },
272
+ endIndex: i - 1
273
+ };
274
+ }
275
+
276
+ // src/pdf/layout.ts
277
+ var HEADER_GRAY = "#F0F0F0";
278
+ var HEADING_COLOR = "#2C3E50";
279
+ var SIGNATURE_ROW_HEIGHT = 30;
280
+ function buildCoverPage(options) {
281
+ const { config, revisionHistory } = options;
282
+ const signatureHeaders = [
283
+ { text: "Role", bold: true, fillColor: HEADER_GRAY },
284
+ { text: "Name", bold: true, fillColor: HEADER_GRAY },
285
+ { text: "Signature", bold: true, fillColor: HEADER_GRAY },
286
+ { text: "Date", bold: true, fillColor: HEADER_GRAY }
287
+ ];
288
+ const signatureRows = config.signatures.map((role) => [
289
+ { text: role, margin: [0, 8, 0, 8] },
290
+ { text: "", margin: [0, 8, 0, 8] },
291
+ {
292
+ text: "",
293
+ margin: [0, 8, 0, 8],
294
+ minHeight: SIGNATURE_ROW_HEIGHT
295
+ },
296
+ { text: "", margin: [0, 8, 0, 8] }
297
+ ]);
298
+ const revisionHeaders = [
299
+ { text: "Revision", bold: true, fillColor: HEADER_GRAY },
300
+ { text: "Date", bold: true, fillColor: HEADER_GRAY },
301
+ { text: "Description", bold: true, fillColor: HEADER_GRAY }
302
+ ];
303
+ const revisionRows = revisionHistory.map((entry) => [
304
+ entry.revision,
305
+ entry.date,
306
+ entry.description
307
+ ]);
308
+ const cover = [
309
+ { text: "Signatures", bold: true, fontSize: 14, margin: [0, 0, 0, 8] },
310
+ {
311
+ table: {
312
+ headerRows: 1,
313
+ widths: ["auto", "*", "*", "auto"],
314
+ body: [signatureHeaders, ...signatureRows]
315
+ },
316
+ layout: "lightHorizontalLines"
317
+ },
318
+ { text: "Revision History", bold: true, fontSize: 14, margin: [0, 20, 0, 8] },
319
+ {
320
+ table: {
321
+ headerRows: 1,
322
+ widths: ["auto", "auto", "*"],
323
+ body: revisionRows.length > 0 ? [revisionHeaders, ...revisionRows] : [revisionHeaders, ["", "", ""]]
324
+ },
325
+ layout: "lightHorizontalLines"
326
+ },
327
+ { text: "", pageBreak: "after" }
328
+ ];
329
+ return cover;
330
+ }
331
+ function buildDocumentDefinition(options) {
332
+ const { recordTitle, recordId, version, date, content = [] } = options;
333
+ const coverPage = buildCoverPage(options);
334
+ return {
335
+ pageSize: "A4",
336
+ pageMargins: [40, 80, 40, 50],
337
+ defaultStyle: {
338
+ font: "Helvetica",
339
+ fontSize: 10
340
+ },
341
+ styles: {
342
+ h1: { fontSize: 18, bold: true },
343
+ h2: { fontSize: 14, bold: true, color: HEADING_COLOR },
344
+ h3: { fontSize: 12, bold: true },
345
+ h4: { fontSize: 11, bold: true },
346
+ code: { font: "Courier", fontSize: 9, background: "#F5F5F5" }
347
+ },
348
+ header: (_currentPage, _pageCount, pageSize) => {
349
+ const usableWidth = pageSize.width - 80;
350
+ return {
351
+ margin: [40, 15, 40, 0],
352
+ table: {
353
+ widths: [usableWidth * 0.2, "*", usableWidth * 0.25],
354
+ body: [
355
+ [
356
+ { text: "[LOGO]", fontSize: 8, color: "#999999" },
357
+ {
358
+ stack: [
359
+ { text: recordTitle, bold: true, fontSize: 10, alignment: "center" },
360
+ { text: recordId, fontSize: 8, alignment: "center", color: "#666666" }
361
+ ]
362
+ },
363
+ {
364
+ stack: [
365
+ {
366
+ text: `Rev: ${version}`,
367
+ fontSize: 8,
368
+ alignment: "right"
369
+ },
370
+ {
371
+ text: `Date: ${date}`,
372
+ fontSize: 8,
373
+ alignment: "right",
374
+ color: "#666666"
375
+ }
376
+ ]
377
+ }
378
+ ]
379
+ ]
380
+ },
381
+ layout: {
382
+ hLineWidth: (i, node) => i === node.table.body.length ? 1 : 0,
383
+ vLineWidth: () => 0,
384
+ hLineColor: () => "#CCCCCC"
385
+ }
386
+ };
387
+ },
388
+ footer: (currentPage, pageCount) => ({
389
+ margin: [40, 10, 40, 0],
390
+ columns: [
391
+ { text: "", width: "*" },
392
+ {
393
+ text: `${recordId} \xB7 Rev ${version}`,
394
+ alignment: "center",
395
+ fontSize: 8,
396
+ color: "#999999",
397
+ width: "*"
398
+ },
399
+ {
400
+ text: `Page ${currentPage} of ${pageCount}`,
401
+ alignment: "right",
402
+ fontSize: 8,
403
+ color: "#999999",
404
+ width: "*"
405
+ }
406
+ ]
407
+ }),
408
+ content: [...coverPage, ...content]
409
+ };
410
+ }
411
+
412
+ // src/generators/urs-generator.ts
413
+ function generateUrs(input) {
414
+ const docs = readDocuments(input.rootDir, "docs/product/user-needs");
415
+ if (docs.length === 0) {
416
+ console.warn("URS: no approved/effective user need documents found \u2014 skipping");
417
+ return null;
418
+ }
419
+ const content = [];
420
+ content.push({ text: "Table of Contents", style: "h2", margin: [0, 0, 0, 10] });
421
+ content.push({
422
+ ol: docs.map((d) => ({
423
+ text: `${d.frontmatter.id}: ${d.frontmatter.title}`,
424
+ margin: [0, 2, 0, 2]
425
+ }))
426
+ });
427
+ content.push({ text: "", pageBreak: "after" });
428
+ for (const doc of docs) {
429
+ content.push({
430
+ text: `${doc.frontmatter.id}: ${doc.frontmatter.title}`,
431
+ style: "h2",
432
+ margin: [0, 10, 0, 6]
433
+ });
434
+ content.push(...markdownToPdfmake(doc.body));
435
+ content.push({ text: "", margin: [0, 20, 0, 0] });
436
+ }
437
+ return buildDocumentDefinition({
438
+ config: input.config,
439
+ recordTitle: "User Requirements Specification",
440
+ recordId: "URS",
441
+ version: input.version,
442
+ date: input.date,
443
+ revisionHistory: input.revisionHistory,
444
+ content
445
+ });
446
+ }
447
+
448
+ // src/generators/prs-generator.ts
449
+ function generatePrs(input) {
450
+ const docs = readDocuments(input.rootDir, "docs/product/requirements");
451
+ if (docs.length === 0) {
452
+ console.warn("PRS: no approved/effective product requirement documents found \u2014 skipping");
453
+ return null;
454
+ }
455
+ const content = [];
456
+ content.push({ text: "Table of Contents", style: "h2", margin: [0, 0, 0, 10] });
457
+ content.push({
458
+ ol: docs.map((d) => ({
459
+ text: `${d.frontmatter.id}: ${d.frontmatter.title}`,
460
+ margin: [0, 2, 0, 2]
461
+ }))
462
+ });
463
+ content.push({ text: "", pageBreak: "after" });
464
+ for (const doc of docs) {
465
+ content.push({
466
+ text: `${doc.frontmatter.id}: ${doc.frontmatter.title}`,
467
+ style: "h2",
468
+ margin: [0, 10, 0, 6]
469
+ });
470
+ content.push(...markdownToPdfmake(doc.body));
471
+ content.push({ text: "", margin: [0, 20, 0, 0] });
472
+ }
473
+ return buildDocumentDefinition({
474
+ config: input.config,
475
+ recordTitle: "Product Requirements Specification",
476
+ recordId: "PRS",
477
+ version: input.version,
478
+ date: input.date,
479
+ revisionHistory: input.revisionHistory,
480
+ content
481
+ });
482
+ }
483
+
484
+ // src/generators/srs-generator.ts
485
+ function generateSrs(input) {
486
+ const docs = readDocuments(input.rootDir, "docs/software/requirements");
487
+ if (docs.length === 0) {
488
+ console.warn("SRS: no approved/effective software requirement documents found \u2014 skipping");
489
+ return null;
490
+ }
491
+ const content = [];
492
+ content.push({ text: "Table of Contents", style: "h2", margin: [0, 0, 0, 10] });
493
+ content.push({
494
+ ol: docs.map((d) => ({
495
+ text: `${d.frontmatter.id}: ${d.frontmatter.title}`,
496
+ margin: [0, 2, 0, 2]
497
+ }))
498
+ });
499
+ content.push({ text: "", pageBreak: "after" });
500
+ for (const doc of docs) {
501
+ content.push({
502
+ text: `${doc.frontmatter.id}: ${doc.frontmatter.title}`,
503
+ style: "h2",
504
+ margin: [0, 10, 0, 6]
505
+ });
506
+ content.push(...markdownToPdfmake(doc.body));
507
+ content.push({ text: "", margin: [0, 20, 0, 0] });
508
+ }
509
+ return buildDocumentDefinition({
510
+ config: input.config,
511
+ recordTitle: "Software Requirements Specification",
512
+ recordId: "SRS",
513
+ version: input.version,
514
+ date: input.date,
515
+ revisionHistory: input.revisionHistory,
516
+ content
517
+ });
518
+ }
519
+
520
+ // src/reader/document-resolver.ts
521
+ var DOC_ID_PATTERN = /^[A-Z]+-(?:[A-Z]+-)?[0-9]+$/;
522
+ var DocumentResolver = class {
523
+ index;
524
+ constructor(documents) {
525
+ this.index = new Map(documents.map((d) => [d.frontmatter.id, d]));
526
+ }
527
+ resolve(reference) {
528
+ const doc = this.index.get(reference);
529
+ if (doc) {
530
+ return `${doc.frontmatter.id}: ${doc.frontmatter.title}`;
531
+ }
532
+ if (DOC_ID_PATTERN.test(reference)) {
533
+ return `[UNRESOLVED: ${reference}]`;
534
+ }
535
+ return reference;
536
+ }
537
+ resolveAll(references) {
538
+ return references.map((r) => this.resolve(r));
539
+ }
540
+ getDocument(id) {
541
+ return this.index.get(id);
542
+ }
543
+ };
544
+
545
+ // src/generators/pta-generator.ts
546
+ function getStringArray(value) {
547
+ if (Array.isArray(value)) {
548
+ return value.filter((v) => typeof v === "string");
549
+ }
550
+ return [];
551
+ }
552
+ function buildTraceabilityMatrix(uns, prss, srss) {
553
+ const allDocs = [...uns, ...prss, ...srss];
554
+ const resolver = new DocumentResolver(allDocs);
555
+ const unById = new Map(uns.map((d) => [d.frontmatter.id, d]));
556
+ const prsById = new Map(prss.map((d) => [d.frontmatter.id, d]));
557
+ const unToPrs = /* @__PURE__ */ new Map();
558
+ for (const un of uns) {
559
+ const derives = getStringArray(un.frontmatter.derives);
560
+ for (const prsId of derives) {
561
+ if (!unToPrs.has(un.frontmatter.id)) {
562
+ unToPrs.set(un.frontmatter.id, /* @__PURE__ */ new Set());
563
+ }
564
+ unToPrs.get(un.frontmatter.id).add(prsId);
565
+ }
566
+ }
567
+ for (const prs of prss) {
568
+ const tracesFrom = getStringArray(prs.frontmatter.traces_from);
569
+ for (const unId of tracesFrom) {
570
+ if (unById.has(unId)) {
571
+ if (!unToPrs.has(unId)) {
572
+ unToPrs.set(unId, /* @__PURE__ */ new Set());
573
+ }
574
+ unToPrs.get(unId).add(prs.frontmatter.id);
575
+ }
576
+ }
577
+ }
578
+ const prsToSrs = /* @__PURE__ */ new Map();
579
+ for (const prs of prss) {
580
+ const tracesTo = getStringArray(prs.frontmatter.traces_to);
581
+ for (const srsId of tracesTo) {
582
+ if (!prsToSrs.has(prs.frontmatter.id)) {
583
+ prsToSrs.set(prs.frontmatter.id, /* @__PURE__ */ new Set());
584
+ }
585
+ prsToSrs.get(prs.frontmatter.id).add(srsId);
586
+ }
587
+ }
588
+ for (const srs of srss) {
589
+ const tracesFrom = getStringArray(srs.frontmatter.traces_from);
590
+ for (const prsId of tracesFrom) {
591
+ if (prsById.has(prsId)) {
592
+ if (!prsToSrs.has(prsId)) {
593
+ prsToSrs.set(prsId, /* @__PURE__ */ new Set());
594
+ }
595
+ prsToSrs.get(prsId).add(srs.frontmatter.id);
596
+ }
597
+ }
598
+ }
599
+ const prsToSource = /* @__PURE__ */ new Map();
600
+ for (const [unId, prsIds] of unToPrs) {
601
+ for (const prsId of prsIds) {
602
+ if (!prsToSource.has(prsId)) {
603
+ prsToSource.set(prsId, /* @__PURE__ */ new Set());
604
+ }
605
+ prsToSource.get(prsId).add(unId);
606
+ }
607
+ }
608
+ for (const prs of prss) {
609
+ const tracesFrom = getStringArray(prs.frontmatter.traces_from);
610
+ for (const ref of tracesFrom) {
611
+ if (!unById.has(ref)) {
612
+ if (!prsToSource.has(prs.frontmatter.id)) {
613
+ prsToSource.set(prs.frontmatter.id, /* @__PURE__ */ new Set());
614
+ }
615
+ prsToSource.get(prs.frontmatter.id).add(ref);
616
+ }
617
+ }
618
+ }
619
+ const rows = [];
620
+ for (const prs of prss) {
621
+ const sourceRefs = prsToSource.get(prs.frontmatter.id);
622
+ const sources = sourceRefs ? [...sourceRefs].map((ref) => resolver.resolve(ref)).join(", ") : "";
623
+ const srsRefs = prsToSrs.get(prs.frontmatter.id);
624
+ const srsResolved = srsRefs ? [...srsRefs].map((ref) => resolver.resolve(ref)).join(", ") : "";
625
+ rows.push({
626
+ source: sources,
627
+ prs: `${prs.frontmatter.id}: ${prs.frontmatter.title}`,
628
+ srs: srsResolved
629
+ });
630
+ }
631
+ const gaps = [];
632
+ const unsWithPrs = /* @__PURE__ */ new Set();
633
+ for (const [unId, prsIds] of unToPrs) {
634
+ if (prsIds.size > 0) {
635
+ unsWithPrs.add(unId);
636
+ }
637
+ }
638
+ for (const un of uns) {
639
+ if (!unsWithPrs.has(un.frontmatter.id)) {
640
+ gaps.push({
641
+ id: un.frontmatter.id,
642
+ title: un.frontmatter.title,
643
+ type: "un_no_prs"
644
+ });
645
+ }
646
+ }
647
+ for (const prs of prss) {
648
+ const srsRefs = prsToSrs.get(prs.frontmatter.id);
649
+ if (!srsRefs || srsRefs.size === 0) {
650
+ gaps.push({
651
+ id: prs.frontmatter.id,
652
+ title: prs.frontmatter.title,
653
+ type: "prs_no_srs"
654
+ });
655
+ }
656
+ }
657
+ const srssWithPrs = /* @__PURE__ */ new Set();
658
+ for (const [, srsIds] of prsToSrs) {
659
+ for (const srsId of srsIds) {
660
+ srssWithPrs.add(srsId);
661
+ }
662
+ }
663
+ for (const srs of srss) {
664
+ if (!srssWithPrs.has(srs.frontmatter.id)) {
665
+ gaps.push({
666
+ id: srs.frontmatter.id,
667
+ title: srs.frontmatter.title,
668
+ type: "srs_no_prs"
669
+ });
670
+ }
671
+ }
672
+ return { rows, gaps };
673
+ }
674
+ var HEADER_GRAY2 = "#F0F0F0";
675
+ var GAP_LABELS = {
676
+ un_no_prs: "User Need with no Product Requirement",
677
+ prs_no_srs: "Product Requirement with no Software Requirement",
678
+ srs_no_prs: "Software Requirement with no Product Requirement"
679
+ };
680
+ function generatePta(input) {
681
+ const uns = readDocuments(input.rootDir, "docs/product/user-needs");
682
+ const prss = readDocuments(input.rootDir, "docs/product/requirements");
683
+ const srss = readDocuments(input.rootDir, "docs/software/requirements");
684
+ if (prss.length === 0) {
685
+ console.warn("PTA: no approved/effective product requirement documents found \u2014 skipping");
686
+ return null;
687
+ }
688
+ const matrix = buildTraceabilityMatrix(uns, prss, srss);
689
+ const content = [];
690
+ content.push({
691
+ text: "Requirements Traceability Matrix",
692
+ style: "h2",
693
+ margin: [0, 0, 0, 10]
694
+ });
695
+ const tableHeaders = [
696
+ { text: "Source", bold: true, fillColor: HEADER_GRAY2 },
697
+ { text: "Product Requirement", bold: true, fillColor: HEADER_GRAY2 },
698
+ { text: "Software Requirement(s)", bold: true, fillColor: HEADER_GRAY2 }
699
+ ];
700
+ const tableBody = matrix.rows.map((row) => [
701
+ { text: row.source || "\u2014" },
702
+ { text: row.prs },
703
+ { text: row.srs || "\u2014" }
704
+ ]);
705
+ content.push({
706
+ table: {
707
+ headerRows: 1,
708
+ widths: ["*", "*", "*"],
709
+ body: [tableHeaders, ...tableBody]
710
+ },
711
+ layout: "lightHorizontalLines"
712
+ });
713
+ if (matrix.gaps.length > 0) {
714
+ content.push({
715
+ text: "Coverage Gaps",
716
+ style: "h2",
717
+ margin: [0, 20, 0, 10]
718
+ });
719
+ content.push({
720
+ ul: matrix.gaps.map((gap) => ({
721
+ text: `${gap.id}: ${gap.title} \u2014 ${GAP_LABELS[gap.type]}`,
722
+ margin: [0, 2, 0, 2]
723
+ }))
724
+ });
725
+ }
726
+ return buildDocumentDefinition({
727
+ config: input.config,
728
+ recordTitle: "Product Traceability Analysis",
729
+ recordId: "PTA",
730
+ version: input.version,
731
+ date: input.date,
732
+ revisionHistory: input.revisionHistory,
733
+ content
734
+ });
735
+ }
736
+
737
+ // src/pdf/risk-matrix.ts
738
+ var COLOR_UNACCEPTABLE = "#FF6B6B";
739
+ var COLOR_REVIEW_REQUIRED = "#FFD93D";
740
+ var COLOR_ACCEPTABLE = "#90EE90";
741
+ var COLOR_HEADER = "#F0F0F0";
742
+ function getCellColor(p, s, config) {
743
+ const unacceptable = config.acceptability.unacceptable;
744
+ const reviewRequired = config.acceptability.review_required ?? [];
745
+ if (unacceptable.some(([cp, cs]) => cp === p && cs === s)) {
746
+ return COLOR_UNACCEPTABLE;
747
+ }
748
+ if (reviewRequired.some(([cp, cs]) => cp === p && cs === s)) {
749
+ return COLOR_REVIEW_REQUIRED;
750
+ }
751
+ return COLOR_ACCEPTABLE;
752
+ }
753
+ function buildRiskMatrixTable(config, riskCounts, probabilityAxis) {
754
+ const severityLabels = config.labels.severity;
755
+ const probabilityLabels = probabilityAxis === "exploitability" && config.labels.exploitability ? config.labels.exploitability : config.labels.probability;
756
+ const headerRow = [
757
+ { text: "", fillColor: COLOR_HEADER, bold: true, alignment: "center" },
758
+ ...severityLabels.map((label, i) => ({
759
+ text: `${i + 1}
760
+ ${label}`,
761
+ fillColor: COLOR_HEADER,
762
+ bold: true,
763
+ alignment: "center",
764
+ fontSize: 8
765
+ }))
766
+ ];
767
+ const dataRows = [];
768
+ for (let p = 5; p >= 1; p--) {
769
+ const labelIndex = p - 1;
770
+ const labelCell = {
771
+ text: `${p}
772
+ ${probabilityLabels[labelIndex]}`,
773
+ fillColor: COLOR_HEADER,
774
+ bold: true,
775
+ alignment: "center",
776
+ fontSize: 8
777
+ };
778
+ const cells = [];
779
+ for (let s = 1; s <= 5; s++) {
780
+ const count = riskCounts.get(`${p},${s}`) ?? 0;
781
+ cells.push({
782
+ text: count > 0 ? String(count) : "",
783
+ fillColor: getCellColor(p, s, config),
784
+ bold: count > 0,
785
+ alignment: "center"
786
+ });
787
+ }
788
+ dataRows.push([labelCell, ...cells]);
789
+ }
790
+ return {
791
+ table: {
792
+ headerRows: 1,
793
+ widths: ["auto", "*", "*", "*", "*", "*"],
794
+ body: [headerRow, ...dataRows]
795
+ }
796
+ };
797
+ }
798
+
799
+ // src/generators/rmr-generator.ts
800
+ import { RiskMatrixConfigSchema } from "@pactosigna/schemas";
801
+ import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
802
+ import { resolve as resolve3 } from "path";
803
+ import { parse as parseYaml2 } from "yaml";
804
+ var HEADER_GRAY3 = "#F0F0F0";
805
+ function isHazard(doc) {
806
+ return doc.frontmatter.id.startsWith("HAZ-");
807
+ }
808
+ function isRiskEntry(doc) {
809
+ return doc.frontmatter.id.startsWith("RISK-");
810
+ }
811
+ function loadRiskMatrix(rootDir) {
812
+ const candidates = [
813
+ resolve3(rootDir, "risk-matrix.yaml"),
814
+ resolve3(rootDir, "docs/risk/risk-matrix.yaml")
815
+ ];
816
+ for (const candidate of candidates) {
817
+ if (existsSync2(candidate)) {
818
+ const raw = readFileSync3(candidate, "utf-8");
819
+ const parsed = parseYaml2(raw);
820
+ const result = RiskMatrixConfigSchema.safeParse(parsed);
821
+ if (result.success) {
822
+ return result.data;
823
+ }
824
+ console.warn(
825
+ `RMR: risk-matrix.yaml at ${candidate} failed validation: ${result.error.message}`
826
+ );
827
+ }
828
+ }
829
+ return null;
830
+ }
831
+ function getHarmSeverity(harmId, harmsById, harmOverride) {
832
+ if (harmOverride != null) {
833
+ return harmOverride;
834
+ }
835
+ const harmDoc = harmsById.get(harmId);
836
+ if (harmDoc && typeof harmDoc.frontmatter.severity === "number") {
837
+ return harmDoc.frontmatter.severity;
838
+ }
839
+ return 0;
840
+ }
841
+ function incrementCount(counts, key) {
842
+ counts.set(key, (counts.get(key) ?? 0) + 1);
843
+ }
844
+ function getAssessments(doc) {
845
+ const raw = doc.frontmatter.hazardous_situation_assessments;
846
+ if (!Array.isArray(raw)) return [];
847
+ return raw;
848
+ }
849
+ function isSecurityRisk(doc) {
850
+ return doc.frontmatter.id.startsWith("RISK-SEC-");
851
+ }
852
+ function buildRiskGraph(rootDir) {
853
+ const hazardCategories = readDocuments(rootDir, "docs/risk/hazard-categories");
854
+ const situations = readDocuments(rootDir, "docs/risk/situations");
855
+ const harms = readDocuments(rootDir, "docs/risk/harms");
856
+ const softwareRiskDocs = readDocuments(rootDir, "docs/software/risks");
857
+ const cybersecurityRiskDocs = readDocuments(rootDir, "docs/cybersecurity/risks");
858
+ const usabilityRiskDocs = readDocuments(rootDir, "docs/usability/risks");
859
+ const allSharedDocs = [...softwareRiskDocs, ...cybersecurityRiskDocs, ...usabilityRiskDocs];
860
+ const hazards = allSharedDocs.filter(isHazard);
861
+ const riskEntries = allSharedDocs.filter(isRiskEntry);
862
+ const matrixConfig = loadRiskMatrix(rootDir);
863
+ if (!matrixConfig) {
864
+ throw new Error(
865
+ "RMR: risk-matrix.yaml not found or invalid \u2014 cannot generate Risk Management Report. Place a valid risk-matrix.yaml at the repo root or docs/risk/risk-matrix.yaml."
866
+ );
867
+ }
868
+ const harmsById = new Map(harms.map((h) => [h.frontmatter.id, h]));
869
+ const inherentCounts = /* @__PURE__ */ new Map();
870
+ const residualCounts = /* @__PURE__ */ new Map();
871
+ for (const entry of riskEntries) {
872
+ const assessments = getAssessments(entry);
873
+ const useExploitability = isSecurityRisk(entry);
874
+ for (const assessment of assessments) {
875
+ const harmList = assessment.harms ?? [];
876
+ for (const harmEntry of harmList) {
877
+ const severity = getHarmSeverity(
878
+ harmEntry.harm,
879
+ harmsById,
880
+ harmEntry.harm_severity_override
881
+ );
882
+ if (severity === 0) continue;
883
+ const inherentP = useExploitability ? harmEntry.inherent_exploitability ?? 0 : harmEntry.inherent_probability ?? 0;
884
+ const residualP = useExploitability ? harmEntry.residual_exploitability ?? 0 : harmEntry.residual_probability ?? 0;
885
+ if (inherentP > 0) {
886
+ incrementCount(inherentCounts, `${inherentP},${severity}`);
887
+ }
888
+ if (residualP > 0) {
889
+ incrementCount(residualCounts, `${residualP},${severity}`);
890
+ }
891
+ }
892
+ }
893
+ }
894
+ return {
895
+ hazardCategories,
896
+ hazards,
897
+ situations,
898
+ harms,
899
+ riskEntries,
900
+ matrixConfig,
901
+ inherentCounts,
902
+ residualCounts
903
+ };
904
+ }
905
+ function buildRiskSummaryTable(graph) {
906
+ const totalRisks = graph.riskEntries.length;
907
+ let acceptableCount = 0;
908
+ let unacceptableCount = 0;
909
+ for (const entry of graph.riskEntries) {
910
+ const assessments = getAssessments(entry);
911
+ let entryAcceptable = true;
912
+ for (const assessment of assessments) {
913
+ for (const harm of assessment.harms ?? []) {
914
+ if (harm.risk_acceptable === false) {
915
+ entryAcceptable = false;
916
+ }
917
+ }
918
+ }
919
+ if (entryAcceptable) {
920
+ acceptableCount++;
921
+ } else {
922
+ unacceptableCount++;
923
+ }
924
+ }
925
+ return {
926
+ table: {
927
+ headerRows: 1,
928
+ widths: ["*", "auto"],
929
+ body: [
930
+ [
931
+ { text: "Metric", bold: true, fillColor: HEADER_GRAY3 },
932
+ { text: "Count", bold: true, fillColor: HEADER_GRAY3 }
933
+ ],
934
+ ["Total Risk Entries", String(totalRisks)],
935
+ ["Acceptable", String(acceptableCount)],
936
+ ["Unacceptable", String(unacceptableCount)],
937
+ ["Hazards Analyzed", String(graph.hazards.length)],
938
+ ["Hazardous Situations", String(graph.situations.length)],
939
+ ["Identified Harms", String(graph.harms.length)]
940
+ ]
941
+ },
942
+ layout: "lightHorizontalLines"
943
+ };
944
+ }
945
+ function buildRiskEntrySection(entry, hazardsById) {
946
+ const items = [];
947
+ items.push({
948
+ text: `${entry.frontmatter.id}: ${entry.frontmatter.title}`,
949
+ style: "h3",
950
+ margin: [0, 10, 0, 4]
951
+ });
952
+ const analyzedHazardId = entry.frontmatter.analyzes;
953
+ if (analyzedHazardId) {
954
+ const hazard = hazardsById.get(analyzedHazardId);
955
+ const hazardLabel = hazard ? `${hazard.frontmatter.id}: ${hazard.frontmatter.title}` : analyzedHazardId;
956
+ items.push({
957
+ text: [{ text: "Analyzes: ", bold: true }, hazardLabel],
958
+ margin: [0, 0, 0, 4]
959
+ });
960
+ }
961
+ const assessments = getAssessments(entry);
962
+ for (const assessment of assessments) {
963
+ items.push({
964
+ text: [{ text: "Hazardous Situation: ", bold: true }, assessment.hazardous_situation],
965
+ margin: [0, 4, 0, 2]
966
+ });
967
+ const mitigations = assessment.mitigations;
968
+ if (mitigations && mitigations.length > 0) {
969
+ items.push({
970
+ ul: mitigations.map((m) => ({
971
+ text: `${m.control} (${m.iso_category ?? "unspecified"}, reduces: ${m.reduces ?? "unspecified"})`,
972
+ margin: [0, 1, 0, 1]
973
+ }))
974
+ });
975
+ }
976
+ for (const harm of assessment.harms ?? []) {
977
+ const useExploitability = isSecurityRisk(entry);
978
+ const inherentLabel = useExploitability ? "exploitability" : "probability";
979
+ const inherentVal = useExploitability ? harm.inherent_exploitability ?? "\u2014" : harm.inherent_probability ?? "\u2014";
980
+ const residualVal = useExploitability ? harm.residual_exploitability ?? "\u2014" : harm.residual_probability ?? "\u2014";
981
+ items.push({
982
+ text: [
983
+ { text: ` Harm: ${harm.harm}`, bold: true },
984
+ ` \u2014 inherent ${inherentLabel}: ${inherentVal}, residual ${inherentLabel}: ${residualVal}`
985
+ ],
986
+ margin: [10, 1, 0, 1]
987
+ });
988
+ }
989
+ }
990
+ return items;
991
+ }
992
+ function generateRmr(input) {
993
+ let graph;
994
+ try {
995
+ graph = buildRiskGraph(input.rootDir);
996
+ } catch (error) {
997
+ console.error(error instanceof Error ? error.message : error);
998
+ return null;
999
+ }
1000
+ if (graph.riskEntries.length === 0) {
1001
+ console.warn("RMR: no approved/effective risk entry documents found \u2014 skipping");
1002
+ return null;
1003
+ }
1004
+ const content = [];
1005
+ content.push({
1006
+ text: "Risk Summary",
1007
+ style: "h2",
1008
+ margin: [0, 0, 0, 10]
1009
+ });
1010
+ content.push(buildRiskSummaryTable(graph));
1011
+ content.push({
1012
+ text: "Inherent Risk Matrix",
1013
+ style: "h2",
1014
+ margin: [0, 20, 0, 10]
1015
+ });
1016
+ content.push(buildRiskMatrixTable(graph.matrixConfig, graph.inherentCounts, "probability"));
1017
+ content.push({
1018
+ text: "Residual Risk Matrix",
1019
+ style: "h2",
1020
+ margin: [0, 20, 0, 10]
1021
+ });
1022
+ content.push(buildRiskMatrixTable(graph.matrixConfig, graph.residualCounts, "probability"));
1023
+ content.push({
1024
+ text: "Individual Risk Entries",
1025
+ style: "h2",
1026
+ margin: [0, 20, 0, 10]
1027
+ });
1028
+ const hazardsById = new Map(graph.hazards.map((h) => [h.frontmatter.id, h]));
1029
+ for (const entry of graph.riskEntries) {
1030
+ content.push(...buildRiskEntrySection(entry, hazardsById));
1031
+ }
1032
+ return buildDocumentDefinition({
1033
+ config: input.config,
1034
+ recordTitle: "Risk Management Report",
1035
+ recordId: "RMR",
1036
+ version: input.version,
1037
+ date: input.date,
1038
+ revisionHistory: input.revisionHistory,
1039
+ content
1040
+ });
1041
+ }
1042
+
1043
+ // src/github/changelog.ts
1044
+ import { execFileSync } from "child_process";
1045
+ import { resolve as resolve4 } from "path";
1046
+ import { readFileSync as readFileSync4 } from "fs";
1047
+ import matter2 from "gray-matter";
1048
+ function buildChangelog(rootDir, version, date) {
1049
+ let previousTag = null;
1050
+ try {
1051
+ const output = execFileSync(
1052
+ "gh",
1053
+ ["release", "list", "--json", "tagName,isDraft", "--limit", "20"],
1054
+ { encoding: "utf-8", cwd: rootDir }
1055
+ );
1056
+ const releases = JSON.parse(output);
1057
+ const published = releases.filter((r) => !r.isDraft);
1058
+ if (published.length > 0) {
1059
+ previousTag = published[0].tagName;
1060
+ }
1061
+ } catch {
1062
+ }
1063
+ if (!previousTag) {
1064
+ return [{ revision: version, date, description: "Initial release" }];
1065
+ }
1066
+ let changedFiles = [];
1067
+ try {
1068
+ const diff = execFileSync(
1069
+ "git",
1070
+ ["diff", "--name-only", `${previousTag}..HEAD`, "--", "docs/"],
1071
+ { encoding: "utf-8", cwd: rootDir }
1072
+ );
1073
+ changedFiles = diff.trim().split("\n").filter(Boolean);
1074
+ } catch {
1075
+ return [{ revision: version, date, description: "Changes since " + previousTag }];
1076
+ }
1077
+ const descriptions = [];
1078
+ for (const file of changedFiles.slice(0, 20)) {
1079
+ try {
1080
+ const raw = readFileSync4(resolve4(rootDir, file), "utf-8");
1081
+ const { data } = matter2(raw);
1082
+ if (data.id && data.title) {
1083
+ descriptions.push(`${data.id}: ${data.title}`);
1084
+ }
1085
+ } catch {
1086
+ }
1087
+ }
1088
+ const description = descriptions.length > 0 ? descriptions.join(", ") : `Changes since ${previousTag}`;
1089
+ return [{ revision: version, date, description }];
1090
+ }
1091
+
1092
+ // src/generate.ts
1093
+ var PdfPrinter = "default" in PdfPrinterModule ? PdfPrinterModule.default : PdfPrinterModule;
1094
+ var RECORD_GENERATORS = [
1095
+ { name: "URS-User-Requirements-Specification", fn: generateUrs },
1096
+ { name: "PRS-Product-Requirements-Specification", fn: generatePrs },
1097
+ { name: "SRS-Software-Requirements-Specification", fn: generateSrs },
1098
+ { name: "PTA-Product-Traceability-Analysis", fn: generatePta },
1099
+ { name: "RMR-Risk-Management-Report", fn: generateRmr }
1100
+ ];
1101
+ var noopUrlResolver = {
1102
+ resolve: () => {
1103
+ },
1104
+ resolved: () => Promise.resolve()
1105
+ };
1106
+ async function renderPdf(printer, docDef) {
1107
+ const pdfDoc = await printer.createPdfKitDocument(docDef);
1108
+ return new Promise((done, reject) => {
1109
+ const chunks = [];
1110
+ pdfDoc.on("data", (chunk) => chunks.push(chunk));
1111
+ pdfDoc.on("end", () => done(Buffer.concat(chunks)));
1112
+ pdfDoc.on("error", reject);
1113
+ pdfDoc.end();
1114
+ });
1115
+ }
1116
+ async function generate(options) {
1117
+ const config = loadConfig(options.rootDir);
1118
+ const outDir = options.outputDir ?? pathResolve(options.rootDir, "out");
1119
+ mkdirSync(outDir, { recursive: true });
1120
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1121
+ const revisionHistory = buildChangelog(options.rootDir, options.version, date);
1122
+ const fonts = {
1123
+ Helvetica: {
1124
+ normal: "Helvetica",
1125
+ bold: "Helvetica-Bold",
1126
+ italics: "Helvetica-Oblique",
1127
+ bolditalics: "Helvetica-BoldOblique"
1128
+ },
1129
+ Courier: {
1130
+ normal: "Courier",
1131
+ bold: "Courier-Bold",
1132
+ italics: "Courier-Oblique",
1133
+ bolditalics: "Courier-BoldOblique"
1134
+ }
1135
+ };
1136
+ const printer = new PdfPrinter(fonts, void 0, noopUrlResolver);
1137
+ const input = {
1138
+ rootDir: options.rootDir,
1139
+ version: options.version,
1140
+ date,
1141
+ config,
1142
+ revisionHistory
1143
+ };
1144
+ const generated = [];
1145
+ for (const { name, fn } of RECORD_GENERATORS) {
1146
+ const docDef = fn(input);
1147
+ if (!docDef) continue;
1148
+ const buffer = await renderPdf(printer, docDef);
1149
+ const filePath = pathResolve(outDir, `${name}.pdf`);
1150
+ writeFileSync(filePath, buffer);
1151
+ console.log(`Generated: ${name}.pdf`);
1152
+ generated.push(filePath);
1153
+ }
1154
+ return generated;
1155
+ }
1156
+ export {
1157
+ RecordsConfigSchema,
1158
+ buildRiskGraph,
1159
+ buildTraceabilityMatrix,
1160
+ generate,
1161
+ generatePrs,
1162
+ generatePta,
1163
+ generateRmr,
1164
+ generateSrs,
1165
+ generateUrs,
1166
+ loadConfig
1167
+ };