@nebulacomponents/citable 1.17.0 → 1.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/universal/.agents/skills/citable/SKILL.md +41 -1
  3. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  4. package/dist/universal/.agents/skills/citable/agents/citable-remediator.md +43 -0
  5. package/dist/universal/.agents/skills/citable/agents/citable-sow-architect.md +43 -0
  6. package/dist/universal/.agents/skills/citable/commands/README.md +13 -0
  7. package/dist/universal/.agents/skills/citable/commands/remediation.md +55 -0
  8. package/dist/universal/.agents/skills/citable/commands/reporting.md +28 -0
  9. package/dist/universal/.agents/skills/citable/commands/sow.md +95 -0
  10. package/dist/universal/.agents/skills/citable/manifest.json +12 -7
  11. package/dist/universal/.agents/skills/citable/schemas/sow.schema.json +409 -0
  12. package/dist/universal/.claude/agents/citable/citable-remediator.md +43 -0
  13. package/dist/universal/.claude/agents/citable/citable-sow-architect.md +43 -0
  14. package/dist/universal/.claude/agents/citable/manifest.json +6 -4
  15. package/dist/universal/.claude/skills/citable/SKILL.md +41 -1
  16. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  17. package/dist/universal/.claude/skills/citable/agents/citable-remediator.md +43 -0
  18. package/dist/universal/.claude/skills/citable/agents/citable-sow-architect.md +43 -0
  19. package/dist/universal/.claude/skills/citable/commands/README.md +13 -0
  20. package/dist/universal/.claude/skills/citable/commands/remediation.md +55 -0
  21. package/dist/universal/.claude/skills/citable/commands/reporting.md +28 -0
  22. package/dist/universal/.claude/skills/citable/commands/sow.md +95 -0
  23. package/dist/universal/.claude/skills/citable/manifest.json +16 -9
  24. package/dist/universal/.claude/skills/citable/schemas/sow.schema.json +409 -0
  25. package/dist/universal/.cursor/skills/citable/SKILL.md +41 -1
  26. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  27. package/dist/universal/.cursor/skills/citable/agents/citable-remediator.md +43 -0
  28. package/dist/universal/.cursor/skills/citable/agents/citable-sow-architect.md +43 -0
  29. package/dist/universal/.cursor/skills/citable/commands/README.md +13 -0
  30. package/dist/universal/.cursor/skills/citable/commands/remediation.md +55 -0
  31. package/dist/universal/.cursor/skills/citable/commands/reporting.md +28 -0
  32. package/dist/universal/.cursor/skills/citable/commands/sow.md +95 -0
  33. package/dist/universal/.cursor/skills/citable/manifest.json +12 -7
  34. package/dist/universal/.cursor/skills/citable/schemas/sow.schema.json +409 -0
  35. package/dist/universal/.gemini/skills/citable/SKILL.md +41 -1
  36. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  37. package/dist/universal/.gemini/skills/citable/agents/citable-remediator.md +43 -0
  38. package/dist/universal/.gemini/skills/citable/agents/citable-sow-architect.md +43 -0
  39. package/dist/universal/.gemini/skills/citable/commands/README.md +13 -0
  40. package/dist/universal/.gemini/skills/citable/commands/remediation.md +55 -0
  41. package/dist/universal/.gemini/skills/citable/commands/reporting.md +28 -0
  42. package/dist/universal/.gemini/skills/citable/commands/sow.md +95 -0
  43. package/dist/universal/.gemini/skills/citable/manifest.json +12 -7
  44. package/dist/universal/.gemini/skills/citable/schemas/sow.schema.json +409 -0
  45. package/dist/universal/.github/skills/citable/SKILL.md +41 -1
  46. package/dist/universal/.github/skills/citable/VERSION +1 -1
  47. package/dist/universal/.github/skills/citable/agents/citable-remediator.md +43 -0
  48. package/dist/universal/.github/skills/citable/agents/citable-sow-architect.md +43 -0
  49. package/dist/universal/.github/skills/citable/commands/README.md +13 -0
  50. package/dist/universal/.github/skills/citable/commands/remediation.md +55 -0
  51. package/dist/universal/.github/skills/citable/commands/reporting.md +28 -0
  52. package/dist/universal/.github/skills/citable/commands/sow.md +95 -0
  53. package/dist/universal/.github/skills/citable/manifest.json +12 -7
  54. package/dist/universal/.github/skills/citable/schemas/sow.schema.json +409 -0
  55. package/dist/universal/.kiro/skills/citable/SKILL.md +41 -1
  56. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  57. package/dist/universal/.kiro/skills/citable/agents/citable-remediator.md +43 -0
  58. package/dist/universal/.kiro/skills/citable/agents/citable-sow-architect.md +43 -0
  59. package/dist/universal/.kiro/skills/citable/commands/README.md +13 -0
  60. package/dist/universal/.kiro/skills/citable/commands/remediation.md +55 -0
  61. package/dist/universal/.kiro/skills/citable/commands/reporting.md +28 -0
  62. package/dist/universal/.kiro/skills/citable/commands/sow.md +95 -0
  63. package/dist/universal/.kiro/skills/citable/manifest.json +12 -7
  64. package/dist/universal/.kiro/skills/citable/schemas/sow.schema.json +409 -0
  65. package/dist/universal/.opencode/skills/citable/SKILL.md +41 -1
  66. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  67. package/dist/universal/.opencode/skills/citable/agents/citable-remediator.md +43 -0
  68. package/dist/universal/.opencode/skills/citable/agents/citable-sow-architect.md +43 -0
  69. package/dist/universal/.opencode/skills/citable/commands/README.md +13 -0
  70. package/dist/universal/.opencode/skills/citable/commands/remediation.md +55 -0
  71. package/dist/universal/.opencode/skills/citable/commands/reporting.md +28 -0
  72. package/dist/universal/.opencode/skills/citable/commands/sow.md +95 -0
  73. package/dist/universal/.opencode/skills/citable/manifest.json +12 -7
  74. package/dist/universal/.opencode/skills/citable/schemas/sow.schema.json +409 -0
  75. package/dist/universal/.pi/agent/skills/citable/SKILL.md +41 -1
  76. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  77. package/dist/universal/.pi/agent/skills/citable/agents/citable-remediator.md +43 -0
  78. package/dist/universal/.pi/agent/skills/citable/agents/citable-sow-architect.md +43 -0
  79. package/dist/universal/.pi/agent/skills/citable/commands/README.md +13 -0
  80. package/dist/universal/.pi/agent/skills/citable/commands/remediation.md +55 -0
  81. package/dist/universal/.pi/agent/skills/citable/commands/reporting.md +28 -0
  82. package/dist/universal/.pi/agent/skills/citable/commands/sow.md +95 -0
  83. package/dist/universal/.pi/agent/skills/citable/manifest.json +12 -7
  84. package/dist/universal/.pi/agent/skills/citable/schemas/sow.schema.json +409 -0
  85. package/dist/universal/.qoder/skills/citable/SKILL.md +41 -1
  86. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  87. package/dist/universal/.qoder/skills/citable/agents/citable-remediator.md +43 -0
  88. package/dist/universal/.qoder/skills/citable/agents/citable-sow-architect.md +43 -0
  89. package/dist/universal/.qoder/skills/citable/commands/README.md +13 -0
  90. package/dist/universal/.qoder/skills/citable/commands/remediation.md +55 -0
  91. package/dist/universal/.qoder/skills/citable/commands/reporting.md +28 -0
  92. package/dist/universal/.qoder/skills/citable/commands/sow.md +95 -0
  93. package/dist/universal/.qoder/skills/citable/manifest.json +12 -7
  94. package/dist/universal/.qoder/skills/citable/schemas/sow.schema.json +409 -0
  95. package/dist/universal/.rovodev/skills/citable/SKILL.md +41 -1
  96. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  97. package/dist/universal/.rovodev/skills/citable/agents/citable-remediator.md +43 -0
  98. package/dist/universal/.rovodev/skills/citable/agents/citable-sow-architect.md +43 -0
  99. package/dist/universal/.rovodev/skills/citable/commands/README.md +13 -0
  100. package/dist/universal/.rovodev/skills/citable/commands/remediation.md +55 -0
  101. package/dist/universal/.rovodev/skills/citable/commands/reporting.md +28 -0
  102. package/dist/universal/.rovodev/skills/citable/commands/sow.md +95 -0
  103. package/dist/universal/.rovodev/skills/citable/manifest.json +12 -7
  104. package/dist/universal/.rovodev/skills/citable/schemas/sow.schema.json +409 -0
  105. package/dist/universal/.trae/skills/citable/SKILL.md +41 -1
  106. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  107. package/dist/universal/.trae/skills/citable/agents/citable-remediator.md +43 -0
  108. package/dist/universal/.trae/skills/citable/agents/citable-sow-architect.md +43 -0
  109. package/dist/universal/.trae/skills/citable/commands/README.md +13 -0
  110. package/dist/universal/.trae/skills/citable/commands/remediation.md +55 -0
  111. package/dist/universal/.trae/skills/citable/commands/reporting.md +28 -0
  112. package/dist/universal/.trae/skills/citable/commands/sow.md +95 -0
  113. package/dist/universal/.trae/skills/citable/manifest.json +12 -7
  114. package/dist/universal/.trae/skills/citable/schemas/sow.schema.json +409 -0
  115. package/dist/universal/.trae-cn/skills/citable/SKILL.md +41 -1
  116. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  117. package/dist/universal/.trae-cn/skills/citable/agents/citable-remediator.md +43 -0
  118. package/dist/universal/.trae-cn/skills/citable/agents/citable-sow-architect.md +43 -0
  119. package/dist/universal/.trae-cn/skills/citable/commands/README.md +13 -0
  120. package/dist/universal/.trae-cn/skills/citable/commands/remediation.md +55 -0
  121. package/dist/universal/.trae-cn/skills/citable/commands/reporting.md +28 -0
  122. package/dist/universal/.trae-cn/skills/citable/commands/sow.md +95 -0
  123. package/dist/universal/.trae-cn/skills/citable/manifest.json +12 -7
  124. package/dist/universal/.trae-cn/skills/citable/schemas/sow.schema.json +409 -0
  125. package/dist/universal/manifest.json +150 -88
  126. package/package.json +1 -1
  127. package/schemas/sow.schema.json +409 -0
  128. package/skill/SKILL.md +41 -1
  129. package/skill/agents/citable-remediator.md +43 -0
  130. package/skill/agents/citable-sow-architect.md +43 -0
  131. package/skill/commands/README.md +13 -0
  132. package/skill/commands/remediation.md +55 -0
  133. package/skill/commands/reporting.md +28 -0
  134. package/skill/commands/sow.md +95 -0
  135. package/src/analysis/behavioral.js +126 -0
  136. package/src/analysis/croAudit.js +302 -0
  137. package/src/analysis/croRoadmap.js +121 -0
  138. package/src/analysis/eeat.js +300 -0
  139. package/src/analysis/funnelAnalysis.js +171 -0
  140. package/src/analysis/iceMatrix.js +205 -0
  141. package/src/analysis/offpage.js +228 -0
  142. package/src/analysis/readiness.js +253 -0
  143. package/src/analysis/schemaRecommendations.js +173 -0
  144. package/src/analysis/strategicRoadmap.js +148 -0
  145. package/src/cli/index.js +149 -3
  146. package/src/commands/answerEngineReadiness.js +48 -0
  147. package/src/commands/auditBacklinks.js +79 -0
  148. package/src/commands/croBacklog.js +182 -0
  149. package/src/commands/croSuite.js +156 -0
  150. package/src/commands/executive.js +6 -0
  151. package/src/commands/executiveCro.js +30 -0
  152. package/src/commands/executiveSearch.js +28 -0
  153. package/src/commands/inspectCro.js +7 -0
  154. package/src/commands/inspectEeat.js +53 -0
  155. package/src/commands/prioritize.js +42 -6
  156. package/src/commands/roadmapCmd.js +69 -0
  157. package/src/commands/schemaCmd.js +10 -2
  158. package/src/commands/sowCmd.js +84 -0
  159. package/src/commands/sweep.js +357 -0
  160. package/src/reporting/executiveCroReport.js +731 -0
  161. package/src/reporting/executiveExport.js +62 -0
  162. package/src/reporting/executiveSearchReport.js +704 -0
  163. package/src/sow/admissibilityGate.js +364 -0
  164. package/src/sow/generateSow.js +1178 -0
@@ -0,0 +1,1178 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { buildContext } from '../commands/context.js';
4
+ import { selectDetectors } from '../detectors/index.js';
5
+ import { runDetectors, indexTargets } from '../detectors/framework.js';
6
+ import { evaluateScopeAdmissibility } from './admissibilityGate.js';
7
+ import { readJson, nowIso, sha256 } from '../shared/io.js';
8
+ import { validateAgainst } from '../shared/schemaValidator.js';
9
+
10
+ /**
11
+ * Typed SOW Domain Errors
12
+ */
13
+ export class SowError extends Error {
14
+ constructor(message, code = 'SOW_ERROR') {
15
+ super(message);
16
+ this.name = this.constructor.name;
17
+ this.code = code;
18
+ }
19
+ }
20
+
21
+ export class RunNotFoundError extends SowError {
22
+ constructor(runId) {
23
+ super(`Specified run not found: ${runId}`, 'RUN_NOT_FOUND');
24
+ this.runId = runId;
25
+ }
26
+ }
27
+
28
+ export class FindingsMissingError extends SowError {
29
+ constructor(runId, filePath) {
30
+ super(`Run ${runId} is missing findings.json at ${filePath}`, 'FINDINGS_MISSING');
31
+ this.runId = runId;
32
+ this.filePath = filePath;
33
+ }
34
+ }
35
+
36
+ export class FindingsInvalidError extends SowError {
37
+ constructor(runId, message) {
38
+ super(`Run ${runId} contains malformed or invalid findings: ${message}`, 'FINDINGS_INVALID');
39
+ this.runId = runId;
40
+ }
41
+ }
42
+
43
+ export class LiveInspectionFailedError extends SowError {
44
+ constructor(target, reason) {
45
+ super(`Live target inspection failed for ${target}: ${reason}`, 'LIVE_INSPECTION_FAILED');
46
+ this.target = target;
47
+ }
48
+ }
49
+
50
+ export class NoFindingsError extends SowError {
51
+ constructor(source) {
52
+ super(`No audit findings available from ${source}. Production SOW generation requires authoritative findings. (Use --sample to generate non-contractual demonstration SOW)`, 'NO_FINDINGS');
53
+ }
54
+ }
55
+
56
+ export class NoAdmissibleRequirementsError extends SowError {
57
+ constructor(totalEvaluated, refusalCount) {
58
+ super(`Scope admissibility gate refused all ${totalEvaluated} findings (${refusalCount} refused). Zero contractable requirements remain.`, 'NO_ADMISSIBLE_REQUIREMENTS');
59
+ }
60
+ }
61
+
62
+ export class BudgetCalculationError extends SowError {
63
+ constructor(message) {
64
+ super(message, 'INVALID_COMMERCIAL_TERMS');
65
+ }
66
+ }
67
+
68
+ export class SowInvariantError extends SowError {
69
+ constructor(message) {
70
+ super(`SOW invariant failure: ${message}`, 'INVARIANT_VIOLATION');
71
+ }
72
+ }
73
+
74
+ export const SOW_CURRENCY = 'USD';
75
+ export const SOW_CURRENCY_MINOR_EXPONENT = 2;
76
+
77
+ /**
78
+ * Parse commercial budget into integer minor units (cents) and major units (USD).
79
+ * Ensures money units are unambiguous and rejects invalid, negative, or sub-cent fractional budgets.
80
+ */
81
+ export function parseCommercialBudget({ budget = 45000, budgetMinor = null } = {}) {
82
+ if (budgetMinor !== null && budgetMinor !== undefined) {
83
+ if (!Number.isInteger(budgetMinor) || budgetMinor < 0) {
84
+ throw new BudgetCalculationError(`budgetMinor must be a non-negative integer (cents), got: ${budgetMinor}`);
85
+ }
86
+ const feeUsd = Number((budgetMinor / 100).toFixed(2));
87
+ return {
88
+ feeMinor: budgetMinor,
89
+ feeUsd,
90
+ currency: SOW_CURRENCY,
91
+ minorUnitExponent: SOW_CURRENCY_MINOR_EXPONENT,
92
+ };
93
+ }
94
+
95
+ const num = Number(budget);
96
+ if (Number.isNaN(num) || !Number.isFinite(num) || num < 0) {
97
+ throw new BudgetCalculationError(`Commercial budget must be a non-negative number, got: ${budget}`);
98
+ }
99
+
100
+ const minorUnits = Math.round(num * 100);
101
+ if (Math.abs(num * 100 - minorUnits) > 1e-6) {
102
+ throw new BudgetCalculationError(`Commercial budget cannot contain fractional minor units (sub-cents): ${budget}`);
103
+ }
104
+
105
+ const feeUsd = Number((minorUnits / 100).toFixed(2));
106
+ return {
107
+ feeMinor: minorUnits,
108
+ feeUsd,
109
+ currency: SOW_CURRENCY,
110
+ minorUnitExponent: SOW_CURRENCY_MINOR_EXPONENT,
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Exact integer milestone fee allocation distributing remainder deterministically.
116
+ * Allocates integer currency units (such as minor units / cents) across work packages without remainder leaks.
117
+ */
118
+ export function allocateMilestoneFees(totalUnits, count) {
119
+ if (!Number.isInteger(totalUnits) || totalUnits < 0) {
120
+ throw new BudgetCalculationError(`Commercial budget units must be a non-negative integer, got: ${totalUnits}`);
121
+ }
122
+ if (!Number.isInteger(count) || count <= 0) {
123
+ throw new BudgetCalculationError(`Cannot allocate milestone fees across ${count} work packages`);
124
+ }
125
+ const base = Math.floor(totalUnits / count);
126
+ const remainder = totalUnits - (base * count);
127
+ const fees = [];
128
+ for (let i = 0; i < count; i++) {
129
+ fees.push(base + (i < remainder ? 1 : 0));
130
+ }
131
+ const sum = fees.reduce((acc, f) => acc + f, 0);
132
+ if (sum !== totalUnits) {
133
+ throw new BudgetCalculationError(`Milestone fee sum (${sum}) does not equal total units (${totalUnits})`);
134
+ }
135
+ return fees;
136
+ }
137
+
138
+ /**
139
+ * Extract canonical timestamp (epoch ms) from run package
140
+ * Hierarchy:
141
+ * 1. manifest.json `timestamp` or `created_at` ISO string
142
+ * 2. Run ID timestamp prefix (YYYYMMDDTHHmmss)
143
+ * 3. findings.json mtime
144
+ * 4. run directory mtime
145
+ */
146
+ export function getCanonicalRunTimestamp(runDir, runId = '') {
147
+ // 1. manifest.json
148
+ const manifestPath = path.join(runDir, 'manifest.json');
149
+ if (fs.existsSync(manifestPath)) {
150
+ try {
151
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
152
+ if (manifest.timestamp) {
153
+ const ms = Date.parse(manifest.timestamp);
154
+ if (!Number.isNaN(ms)) return ms;
155
+ }
156
+ if (manifest.created_at) {
157
+ const ms = Date.parse(manifest.created_at);
158
+ if (!Number.isNaN(ms)) return ms;
159
+ }
160
+ } catch {}
161
+ }
162
+
163
+ // 2. Run ID timestamp prefix (YYYYMMDDTHHmmss)
164
+ const m = String(runId).match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/);
165
+ if (m) {
166
+ const iso = `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}Z`;
167
+ const ms = Date.parse(iso);
168
+ if (!Number.isNaN(ms)) return ms;
169
+ }
170
+
171
+ // 3. findings.json mtime
172
+ const findPath = path.join(runDir, 'findings.json');
173
+ if (fs.existsSync(findPath)) {
174
+ try {
175
+ return fs.statSync(findPath).mtimeMs;
176
+ } catch {}
177
+ }
178
+
179
+ // 4. Directory mtime
180
+ try {
181
+ return fs.statSync(runDir).mtimeMs;
182
+ } catch {
183
+ return 0;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Sort run candidate directory names by chronological timestamp descending,
189
+ * with deterministic lexicographical tie-breaking.
190
+ * Invariant:
191
+ * latest run = max(canonical run timestamp)
192
+ * tie = deterministic secondary key
193
+ */
194
+ export function sortRunCandidatesChronologically(runsDir, candidateRunIds = []) {
195
+ return [...candidateRunIds].sort((a, b) => {
196
+ const timeA = getCanonicalRunTimestamp(path.join(runsDir, a), a);
197
+ const timeB = getCanonicalRunTimestamp(path.join(runsDir, b), b);
198
+ if (timeB !== timeA) {
199
+ return timeB - timeA; // Descending: latest timestamp first
200
+ }
201
+ return b.localeCompare(a); // Deterministic tie-breaker
202
+ });
203
+ }
204
+
205
+ /**
206
+ * Verify cross-object runtime invariants before SOW export
207
+ */
208
+ export function validateSowInvariants(sow) {
209
+ if (!sow || typeof sow !== 'object') {
210
+ throw new SowInvariantError('SOW root must be an object');
211
+ }
212
+ if (!['CONTRACTUAL', 'DRAFT', 'NON_CONTRACTUAL_SAMPLE'].includes(sow.generation_mode)) {
213
+ throw new SowInvariantError(`Invalid generation_mode: ${sow.generation_mode}`);
214
+ }
215
+ if (sow.generation_mode === 'CONTRACTUAL' && sow.synthetic_evidence) {
216
+ throw new SowInvariantError('Contractual SOW cannot contain synthetic evidence');
217
+ }
218
+ if (sow.currency !== 'USD') {
219
+ throw new SowInvariantError(`Currency must be 'USD', got: ${sow.currency}`);
220
+ }
221
+ if (sow.currency_minor_unit_exponent !== 2) {
222
+ throw new SowInvariantError(`Minor unit exponent must be 2, got: ${sow.currency_minor_unit_exponent}`);
223
+ }
224
+ if (typeof sow.commercial_total_fee_minor !== 'number' || !Number.isInteger(sow.commercial_total_fee_minor)) {
225
+ throw new SowInvariantError(`commercial_total_fee_minor must be an integer, got: ${sow.commercial_total_fee_minor}`);
226
+ }
227
+ if (typeof sow.commercial_total_fee_usd !== 'number' || sow.commercial_total_fee_usd < 0) {
228
+ throw new SowInvariantError(`commercial_total_fee_usd must be a non-negative number, got: ${sow.commercial_total_fee_usd}`);
229
+ }
230
+ if (sow.commercial_total_fee_minor !== Math.round(sow.commercial_total_fee_usd * 100)) {
231
+ throw new SowInvariantError(`commercial_total_fee_minor (${sow.commercial_total_fee_minor}) does not match commercial_total_fee_usd (${sow.commercial_total_fee_usd})`);
232
+ }
233
+ if (!Array.isArray(sow.work_packages) || sow.work_packages.length === 0) {
234
+ throw new SowInvariantError('SOW must contain at least one work package');
235
+ }
236
+ if (!Array.isArray(sow.deliverables) || sow.deliverables.length === 0) {
237
+ throw new SowInvariantError('SOW must contain at least one deliverable');
238
+ }
239
+ if (sow.work_packages.length !== sow.deliverables.length) {
240
+ throw new SowInvariantError(`Work packages count (${sow.work_packages.length}) must match deliverables count (${sow.deliverables.length})`);
241
+ }
242
+ if (!Array.isArray(sow.traceability_matrix) || sow.traceability_matrix.length === 0) {
243
+ throw new SowInvariantError('SOW must contain at least one traceability matrix requirement');
244
+ }
245
+ const wpIds = new Set(sow.work_packages.map((wp) => wp.work_package_id));
246
+ const delivIds = new Set(sow.deliverables.map((d) => d.deliverable_id));
247
+ for (const row of sow.traceability_matrix) {
248
+ if (!wpIds.has(row.work_package_id)) {
249
+ throw new SowInvariantError(`Traceability matrix references unknown work package: ${row.work_package_id}`);
250
+ }
251
+ if (!delivIds.has(row.deliverable_id)) {
252
+ throw new SowInvariantError(`Traceability matrix references unknown deliverable: ${row.deliverable_id}`);
253
+ }
254
+ if (!Array.isArray(row.evidence_ids) || row.evidence_ids.length === 0) {
255
+ throw new SowInvariantError(`Traceability matrix row ${row.sow_requirement_id} lacks supporting evidence_ids`);
256
+ }
257
+ }
258
+
259
+ const milestones = sow.delivery_schedule?.milestones || [];
260
+ const milestoneMinorSum = milestones.reduce((sum, m) => sum + m.fee_minor, 0);
261
+ if (milestoneMinorSum !== sow.commercial_total_fee_minor) {
262
+ throw new SowInvariantError(`Milestone minor fees sum (${milestoneMinorSum}) does not match total minor fee (${sow.commercial_total_fee_minor})`);
263
+ }
264
+ const milestoneUsdSum = Number(milestones.reduce((sum, m) => sum + m.fee_usd, 0).toFixed(2));
265
+ if (milestoneUsdSum !== sow.commercial_total_fee_usd) {
266
+ throw new SowInvariantError(`Milestone USD fees sum (${milestoneUsdSum}) does not match total fee USD (${sow.commercial_total_fee_usd})`);
267
+ }
268
+ for (const m of milestones) {
269
+ if (m.fee_minor !== Math.round(m.fee_usd * 100)) {
270
+ throw new SowInvariantError(`Milestone ${m.milestone_id} fee_minor (${m.fee_minor}) does not match fee_usd (${m.fee_usd})`);
271
+ }
272
+ }
273
+
274
+ const gate = sow.admissibility_gate;
275
+ if (gate.total_findings_evaluated !== gate.admitted_count + gate.refused_count) {
276
+ throw new SowInvariantError(`Admissibility gate numbers do not balance: ${gate.total_findings_evaluated} != ${gate.admitted_count} + ${gate.refused_count}`);
277
+ }
278
+ return true;
279
+ }
280
+
281
+ /**
282
+ * Standard synthetic baseline findings used strictly for sample mode (--sample / --demo)
283
+ */
284
+ export function getSampleBaselineFindings() {
285
+ return [
286
+ {
287
+ finding_id: 'F-TECH-CWV-001',
288
+ detector_id: 'TECH-001',
289
+ discipline: ['technical'],
290
+ classification: { severity: 'critical', confidence: 'deterministic' },
291
+ subject: { identifier: 'https://example.test/', url: 'https://example.test/' },
292
+ observation: { summary: 'Render-blocking JavaScript bundle degrades Mobile LCP to 4.2s', evidence: ['EVD-CWV-001'] },
293
+ remediation: { preferred: 'Implement asynchronous resource loading and preconnect headers' },
294
+ verification: { detector_to_rerun: 'TECH-001', method: 'Static CWV inspection and HTTP header probe' },
295
+ ice_score: 14.5,
296
+ },
297
+ {
298
+ finding_id: 'F-CRO-AUTO-007',
299
+ detector_id: 'CRO-007',
300
+ discipline: ['cro'],
301
+ classification: { severity: 'high', confidence: 'deterministic' },
302
+ subject: { identifier: 'https://example.test/checkout', url: 'https://example.test/checkout' },
303
+ observation: { summary: 'Checkout form inputs lack HTML5 autocomplete attributes, increasing manual mobile entry by 68%', evidence: ['EVD-CRO-007'] },
304
+ remediation: { preferred: 'Inject standard autocomplete attributes (autocomplete="email", autocomplete="name", autocomplete="tel")' },
305
+ verification: { detector_to_rerun: 'CRO-007', method: 'DOM inspection and AST patch verification' },
306
+ ice_score: 18.0,
307
+ },
308
+ {
309
+ finding_id: 'F-CRO-DIST-013',
310
+ detector_id: 'CRO-013',
311
+ discipline: ['cro'],
312
+ classification: { severity: 'high', confidence: 'deterministic' },
313
+ subject: { identifier: 'https://example.test/checkout', url: 'https://example.test/checkout' },
314
+ observation: { summary: 'Dedicated checkout funnel contains 14 external header navigation links creating distraction leaks', evidence: ['EVD-CRO-013'] },
315
+ remediation: { preferred: 'Deploy enclosed distraction-free checkout layout stripping non-essential navigation' },
316
+ verification: { detector_to_rerun: 'CRO-013', method: 'Navigation link count verification' },
317
+ ice_score: 12.0,
318
+ },
319
+ {
320
+ finding_id: 'F-ANS-PRIC-001',
321
+ detector_id: 'ANS-001',
322
+ discipline: ['aeo'],
323
+ classification: { severity: 'high', confidence: 'deterministic' },
324
+ subject: { identifier: 'https://example.test/saas/pricing.html', url: 'https://example.test/saas/pricing.html' },
325
+ observation: { summary: 'Commercial pricing questions lack direct-extract definition passages under 75 words', evidence: ['EVD-ANS-001'] },
326
+ remediation: { preferred: 'Restructure FAQ headings with immediate concise copular answer passages' },
327
+ verification: { detector_to_rerun: 'ANS-001', method: 'Passage length and question-answer extraction test' },
328
+ ice_score: 11.0,
329
+ },
330
+ {
331
+ finding_id: 'F-SCHEMA-FAQ-001',
332
+ detector_id: 'SCHEMA-001',
333
+ discipline: ['schema'],
334
+ classification: { severity: 'medium', confidence: 'deterministic' },
335
+ subject: { identifier: 'https://example.test/pricing', url: 'https://example.test/pricing' },
336
+ observation: { summary: 'Commercial FAQ content lacks FAQPage JSON-LD schema markup', evidence: ['EVD-SCH-001'] },
337
+ remediation: { preferred: 'Deploy validated Schema.org FAQPage structured data' },
338
+ verification: { detector_to_rerun: 'SCHEMA-001', method: 'JSON-LD schema validation gate' },
339
+ ice_score: 9.5,
340
+ },
341
+ {
342
+ finding_id: 'F-EXP-GEO-999',
343
+ detector_id: 'EXP-GEO-999',
344
+ discipline: ['geo'],
345
+ classification: { severity: 'low', confidence: 'experimental', finding_type: 'experimental' },
346
+ subject: { identifier: 'https://unrelated-blog.test/post-1', url: 'https://unrelated-blog.test/post-1' },
347
+ observation: { summary: 'Speculative model hallucination on third-party forum', evidence: [] },
348
+ remediation: { preferred: 'Consult external legal counsel regarding public forum sentiment' },
349
+ ice_score: 2.0,
350
+ },
351
+ ];
352
+ }
353
+
354
+ /**
355
+ * Generate an Enterprise-Grade Statement of Work (SOW) from SEO, AEO, GEO, SERP, and CRO findings.
356
+ * Forces strict downward traceability: Evidence -> Obligation -> Acceptance Test.
357
+ */
358
+ export async function generateSow(root, {
359
+ target,
360
+ baseUrl,
361
+ refDate,
362
+ runId,
363
+ live = false,
364
+ sample = false,
365
+ demo = false,
366
+ draft = false,
367
+ findings: inputFindings = null,
368
+ client = 'Acme Corporation',
369
+ clientContact = 'client-procurement@acme.test',
370
+ supplier = 'Nebula Components & Citable Practice',
371
+ supplierContact = 'advisory@nebulacomponents.test',
372
+ budget = 45000,
373
+ budgetMinor = null,
374
+ termDays = 90,
375
+ inScopeProperties = [],
376
+ scopeMode = 'HOST_AND_SUBDOMAINS',
377
+ minIceScore = 8.0,
378
+ allowedDisciplines = ['seo', 'aeo', 'geo', 'technical', 'schema', 'entity', 'cro'],
379
+ allowExperimental = false,
380
+ excludedDetectors = [],
381
+ roleMapping = null,
382
+ sowId = null,
383
+ } = {}) {
384
+ const generatedAt = nowIso();
385
+ const sowIdentifier = sowId || `SOW-${Date.now().toString(36).toUpperCase()}`;
386
+ const isSample = Boolean(sample || demo);
387
+ const isDraft = Boolean(draft);
388
+ const generationMode = isSample ? 'NON_CONTRACTUAL_SAMPLE' : (isDraft ? 'DRAFT' : 'CONTRACTUAL');
389
+
390
+ let findings = [];
391
+ let sourceProvenance = {
392
+ source_type: null,
393
+ source_identifier: null,
394
+ findings_count: 0,
395
+ integrity_hash: null,
396
+ };
397
+
398
+ // 1. Precedence: Direct findings supplied programmatically
399
+ if (Array.isArray(inputFindings)) {
400
+ findings = inputFindings;
401
+ sourceProvenance.source_type = 'DIRECT_FINDINGS';
402
+ sourceProvenance.source_identifier = 'in_memory';
403
+ }
404
+ // 2. Precedence: Explicit run requested via --run <id>
405
+ else if (runId) {
406
+ const runsDir = path.join(root, '.citable', 'runs');
407
+ const runPath = path.join(runsDir, runId);
408
+ if (!fs.existsSync(runPath)) {
409
+ throw new RunNotFoundError(runId);
410
+ }
411
+ const findPath = path.join(runPath, 'findings.json');
412
+ if (!fs.existsSync(findPath)) {
413
+ throw new FindingsMissingError(runId, findPath);
414
+ }
415
+ try {
416
+ findings = readJson(findPath);
417
+ if (!Array.isArray(findings)) throw new Error('findings.json root must be an array');
418
+ sourceProvenance.source_type = 'HISTORICAL_RUN';
419
+ sourceProvenance.source_identifier = runId;
420
+ sourceProvenance.integrity_hash = sha256(fs.readFileSync(findPath));
421
+ } catch (err) {
422
+ throw new FindingsInvalidError(runId, err.message);
423
+ }
424
+ }
425
+ // 3. Precedence: Live target inspection requested via --live or target
426
+ else if (live || target) {
427
+ if (!target) {
428
+ throw new SowError('Target is required for live inspection (e.g. --target <url|dir>)', 'TARGET_REQUIRED');
429
+ }
430
+ try {
431
+ const ctx = await buildContext(root, { target, baseUrl, refDate });
432
+ if (!ctx?.site) {
433
+ throw new Error(`Target ${target} did not produce a valid site context`);
434
+ }
435
+ indexTargets(ctx);
436
+ const detectors = selectDetectors({ scopes: ['technical', 'seo', 'aeo', 'geo', 'schema', 'entity', 'cro'] });
437
+ const res = runDetectors(detectors, ctx);
438
+ findings = res.findings || [];
439
+ sourceProvenance.source_type = 'LIVE_INSPECTION';
440
+ sourceProvenance.source_identifier = target;
441
+ } catch (err) {
442
+ throw new LiveInspectionFailedError(target, err.message);
443
+ }
444
+ }
445
+ // 4. Precedence: Automatically load latest recorded run from .citable/runs if present
446
+ // Invariant: latest run = max(canonical run timestamp), tie = deterministic secondary key
447
+ else {
448
+ const runsDir = path.join(root, '.citable', 'runs');
449
+ if (fs.existsSync(runsDir)) {
450
+ const candidates = fs.readdirSync(runsDir)
451
+ .filter((d) => !d.startsWith('.') && fs.existsSync(path.join(runsDir, d, 'findings.json')));
452
+ const sortedCandidates = sortRunCandidatesChronologically(runsDir, candidates);
453
+
454
+ if (sortedCandidates.length > 0) {
455
+ const latestRunId = sortedCandidates[0];
456
+ const findPath = path.join(runsDir, latestRunId, 'findings.json');
457
+ try {
458
+ findings = readJson(findPath);
459
+ if (Array.isArray(findings)) {
460
+ sourceProvenance.source_type = 'LATEST_RECORDED_RUN';
461
+ sourceProvenance.source_identifier = latestRunId;
462
+ sourceProvenance.integrity_hash = sha256(fs.readFileSync(findPath));
463
+ }
464
+ } catch {}
465
+ }
466
+ }
467
+ }
468
+
469
+ // 5. If no findings were discovered:
470
+ if (!Array.isArray(findings) || findings.length === 0) {
471
+ if (isSample) {
472
+ findings = getSampleBaselineFindings();
473
+ sourceProvenance.source_type = 'SAMPLE_BASELINE';
474
+ sourceProvenance.source_identifier = 'citable://samples/enterprise-baseline';
475
+ } else {
476
+ const srcDesc = target ? `live target "${target}"` : (runId ? `run "${runId}"` : 'local workspace');
477
+ throw new NoFindingsError(srcDesc);
478
+ }
479
+ }
480
+
481
+ sourceProvenance.findings_count = findings.length;
482
+
483
+ const effectiveScopeProps = inScopeProperties.length > 0
484
+ ? inScopeProperties
485
+ : (baseUrl ? [baseUrl] : (isSample ? ['https://example.test'] : []));
486
+
487
+ // -------------------------------------------------------------
488
+ // THE ADMISSIBILITY GATE (Filter findings into contractual scope)
489
+ // -------------------------------------------------------------
490
+ const gateResult = evaluateScopeAdmissibility(findings, {
491
+ inScopeProperties: effectiveScopeProps,
492
+ scopeMode,
493
+ minIceScore,
494
+ allowedDisciplines,
495
+ allowExperimental,
496
+ excludedDetectors,
497
+ roleMapping,
498
+ });
499
+
500
+ const admitted = gateResult.admitted_requirements;
501
+ if (admitted.length === 0) {
502
+ throw new NoAdmissibleRequirementsError(findings.length, gateResult.refusal_log.length);
503
+ }
504
+
505
+ // Group admitted requirements into Work Packages
506
+ const wpMap = new Map();
507
+ for (const item of admitted) {
508
+ if (!wpMap.has(item.work_package_id)) {
509
+ wpMap.set(item.work_package_id, {
510
+ work_package_id: item.work_package_id,
511
+ name: item.work_package_name,
512
+ deliverable_id: item.deliverable_id,
513
+ owner: item.owner,
514
+ requirements: [],
515
+ });
516
+ }
517
+ wpMap.get(item.work_package_id).requirements.push(item);
518
+ }
519
+ const workPackages = Array.from(wpMap.values());
520
+
521
+ // 25. THE FINAL TRACEABILITY MATRIX
522
+ // Mapping Finding -> Recommendation -> SOW Requirement -> Deliverable -> Acceptance Test -> Owner -> Evidence
523
+ const traceabilityMatrix = admitted.map((item) => ({
524
+ finding_id: item.finding_id,
525
+ detector_id: item.detector_id,
526
+ recommendation: item.recommendation,
527
+ sow_requirement_id: item.sow_requirement_id,
528
+ work_package_id: item.work_package_id,
529
+ deliverable_id: item.deliverable_id,
530
+ acceptance_test_id: item.acceptance_test_id,
531
+ owner: item.owner,
532
+ owner_source: item.owner_source,
533
+ owner_mapping_version: item.owner_mapping_version,
534
+ evidence_id: item.evidence_id,
535
+ evidence_ids: item.evidence_ids,
536
+ severity: item.severity,
537
+ target_subject: item.subject,
538
+ acceptance_criteria: item.acceptance_test,
539
+ }));
540
+
541
+ // Deliverables definitions
542
+ const deliverables = workPackages.map((wp) => ({
543
+ deliverable_id: wp.deliverable_id,
544
+ work_package_id: wp.work_package_id,
545
+ title: `${wp.name} Implementation & Verification Package`,
546
+ artifact_type: 'Code Patches, AST Snapshots, and Evidence Package',
547
+ format: 'Git Pull Request + JSON Verification Receipt',
548
+ owner: wp.owner,
549
+ delivery_criteria: `All ${wp.requirements.length} requirements must pass automated verification (exit code 0) with zero regressions on Core Web Vitals.`,
550
+ }));
551
+
552
+ // Acceptance criteria definitions
553
+ const acceptanceCriteria = admitted.map((item) => ({
554
+ acceptance_test_id: item.acceptance_test_id,
555
+ sow_requirement_id: item.sow_requirement_id,
556
+ test_description: item.acceptance_test,
557
+ validation_method: 'citable verify remediation closed-loop command',
558
+ pass_threshold: '100% detector defect resolution on audited surface',
559
+ evidence_required: `Deterministic before-and-after observation JSON matching ${item.evidence_id}`,
560
+ }));
561
+
562
+ // Commercial structure with deterministic integer minor-unit arithmetic
563
+ const { feeMinor, feeUsd, currency, minorUnitExponent } = parseCommercialBudget({ budget, budgetMinor });
564
+ const milestoneFeesMinor = allocateMilestoneFees(feeMinor, workPackages.length);
565
+ const milestoneFeesUsd = milestoneFeesMinor.map((m) => Number((m / 100).toFixed(2)));
566
+ const commercialMilestones = workPackages.map((wp, idx) => ({
567
+ milestone_id: `MILESTONE-0${idx + 1}`,
568
+ work_package_id: wp.work_package_id,
569
+ name: wp.name,
570
+ fee_minor: milestoneFeesMinor[idx],
571
+ fee_usd: milestoneFeesUsd[idx],
572
+ billing_trigger: `Successful customer acceptance sign-off of Deliverable ${wp.deliverable_id}`,
573
+ target_delivery_week: (idx + 1) * 3,
574
+ }));
575
+
576
+ const sowTitle = isSample
577
+ ? 'Statement of Work: Enterprise Search & Conversion Intelligence Engineering [DEMONSTRATION SAMPLE]'
578
+ : (isDraft
579
+ ? 'Statement of Work: Enterprise Search & Conversion Intelligence Engineering [DRAFT]'
580
+ : 'Statement of Work: Enterprise Search & Conversion Intelligence Engineering');
581
+
582
+ const sow = {
583
+ $schema: 'citable://schemas/sow.schema.json',
584
+ sow_id: sowIdentifier,
585
+ version: '1.0.0',
586
+ generation_mode: generationMode,
587
+ title: sowTitle,
588
+ synthetic_evidence: isSample,
589
+ client: { name: client, contact: clientContact },
590
+ supplier: { name: supplier, contact: supplierContact },
591
+ effective_date: generatedAt.split('T')[0],
592
+ term_days: termDays,
593
+ currency,
594
+ currency_minor_unit_exponent: minorUnitExponent,
595
+ commercial_total_fee_minor: feeMinor,
596
+ commercial_total_fee_usd: feeUsd,
597
+
598
+ // Pillar 1: Executive Scope Statement
599
+ executive_scope: {
600
+ business_objective: 'Eliminate deterministic technical search and conversion friction, establish AEO/GEO answer extraction architecture, and deploy governed CRO design remediations with verifiable evidence.',
601
+ in_scope_properties: effectiveScopeProps,
602
+ in_scope_systems: ['Web Front-End Codebase', 'CMS Rendering Layer', 'Search & Conversion Analytics (GSC, GA4, PostHog)', 'Edge Middleware / Reverse Proxy'],
603
+ channels: ['Organic Search (Google, Bing)', 'AI Answer Engines (Perplexity, ChatGPT, Copilot)', 'Direct Conversion Funnels'],
604
+ geographies: ['Global', 'North America (US/CA)', 'European Union (GDPR-compliant surfaces)'],
605
+ organizational_boundaries: 'Applies strictly to customer-owned digital production surfaces; excludes third-party partner portals and non-contracted subdomains.',
606
+ },
607
+
608
+ // Pillar 2: Evidence-to-Work Traceability & Admissibility Gate
609
+ admissibility_gate: {
610
+ total_findings_evaluated: gateResult.summary.total_evaluated,
611
+ admitted_count: gateResult.summary.admitted_count,
612
+ refused_count: gateResult.summary.refused_count,
613
+ admissibility_rate_pct: gateResult.summary.admissibility_rate_pct,
614
+ refusal_log: gateResult.refusal_log,
615
+ methodology: 'Admissibility Gate evaluates findings against 6 strict tests: Evidence Maturity, Scope Boundary, Technical Feasibility, Commercial Materiality, Measurable Acceptance, and Ownership Clarity.',
616
+ },
617
+
618
+ // Pillar 3: Explicit Assumptions, Dependencies, Exclusions & Constraints
619
+ assumptions_and_constraints: {
620
+ assumptions: [
621
+ 'Customer maintains active version control via GitHub or GitLab with standard pull-request workflow.',
622
+ 'Staging environment provides parity with production infrastructure for pre-release verification testing.',
623
+ 'Audited search and conversion telemetry represents normal operating seasonality.',
624
+ ],
625
+ dependencies: [
626
+ 'Customer delivers read-only credentials to Google Search Console and GA4 within 5 business days of effective date.',
627
+ 'Customer engineering performs technical reviews and merges approved pull requests within 48 hours of verification receipt.',
628
+ ],
629
+ exclusions: [
630
+ 'Paid media ad buy management or advertising budget spend.',
631
+ 'Complete brand identity overhaul or custom creative video production.',
632
+ 'Legal, privacy, or regulatory compliance counsel (medical, financial, GDPR legal defense).',
633
+ 'Refactoring core backend transactional databases or proprietary billing engines.',
634
+ ],
635
+ constraints: [
636
+ 'Zero deployment downtime allowed during production release windows.',
637
+ 'Zero regression permitted on Core Web Vitals (Mobile LCP <= 2.5s, CLS <= 0.10, INP <= 200ms).',
638
+ ],
639
+ unresolved_unknowns: [
640
+ 'In-memory citation query volume from ChatGPT search (requires server-side log ingestion phase).',
641
+ 'Post-cookie-consent degradation on regional EU mobile conversions.',
642
+ ],
643
+ },
644
+
645
+ // Pillar 4: Prioritized Work Packages
646
+ work_packages: workPackages,
647
+
648
+ // Pillar 5: Detailed Deliverables
649
+ deliverables,
650
+
651
+ // Pillar 6: Technical Implementation Requirements
652
+ technical_requirements: {
653
+ supported_frameworks: ['React', 'Next.js', 'Vue', 'Nuxt', 'HTML5/Tailwind', 'Astro'],
654
+ git_branching_model: 'citable/remediation-<finding-id>',
655
+ rollback_mechanism: 'Automated atomic snapshot generated in .citable/remediation/snapshots/ prior to file modification',
656
+ access_requirements: ['Read-only git repository access', 'Read-only GSC/GA4 analytics access', 'Staging deploy preview access'],
657
+ },
658
+
659
+ // Pillar 7: Acceptance Criteria
660
+ acceptance_criteria: acceptanceCriteria,
661
+
662
+ // Pillar 8: Baseline & Target-State Metrics
663
+ baseline_and_target_metrics: {
664
+ disclaimer: 'Metrics represent technical readiness, friction eradication, and modeled index targets. In adherence to Citable governance, this SOW never warrants guaranteed ranking or revenue volumes.',
665
+ metrics: [
666
+ { metric: 'Core Web Vitals Render-Blockers', baseline: '1 blocker', target: '0 blockers', validation: 'Lighthouse / static CWV sweep' },
667
+ { metric: 'HTML5 Autocomplete Form Coverage', baseline: '32%', target: '100%', validation: 'AST form field inspection' },
668
+ { metric: 'AEO Direct Answer Extraction Score', baseline: '58 / 100', target: '>= 80 / 100', validation: 'citable inspect readiness' },
669
+ { metric: 'Checkout Distraction Leaks', baseline: '14 header links', target: '0 links (Enclosed)', validation: 'DOM link count' },
670
+ ],
671
+ },
672
+
673
+ // Pillar 9: Experiment Specifications
674
+ experiment_specifications: {
675
+ standard: 'Two-tailed hypothesis testing (alpha = 0.05, 80% statistical power)',
676
+ stopping_criteria: 'Minimum 14 calendar days lock; mandatory daily Sample Ratio Mismatch (SRM) Chi-Square check (p < 0.001 terminates test)',
677
+ guardrails: [
678
+ 'SEO Retention: Variant must preserve all canonical tags, JSON-LD structured data, and meta robots tags',
679
+ 'Performance Guardrail: Mobile LCP must not degrade by > 250ms; CLS must remain <= 0.10',
680
+ ],
681
+ },
682
+
683
+ // Pillar 10: Roles & RACI Matrix
684
+ raci_matrix: [
685
+ { role: 'Customer Executive Sponsor', r: 'Approve SOW, sign off commercial milestones, resolve executive escalations', raci: 'Accountable (A)' },
686
+ { role: 'Customer Lead Engineer', r: 'Review code pull requests, grant repository access, execute production deploy', raci: 'Responsible (R)' },
687
+ { role: 'Supplier Principal Architect', r: 'Design AST patches, author verification receipts, direct technical delivery', raci: 'Responsible (R)' },
688
+ { role: 'Supplier QA & Governance Lead', r: 'Execute admissibility gate, validate acceptance criteria, maintain evidence packages', raci: 'Consulted (C)' },
689
+ ],
690
+
691
+ // Pillar 11: Customer Obligations
692
+ customer_obligations: [
693
+ 'Provide authorized read-only API access to required search and analytics tools within 5 business days.',
694
+ 'Review and respond to delivered Pull Requests and verification packages within 48 business hours.',
695
+ 'Maintain staging environment availability for automated closed-loop verification probes.',
696
+ ],
697
+
698
+ // Pillar 12: Delivery Sequencing & Milestone Schedule
699
+ delivery_schedule: {
700
+ total_duration_weeks: Math.ceil(termDays / 7),
701
+ milestones: commercialMilestones,
702
+ },
703
+
704
+ // Pillar 13: Scope Change-Control Process
705
+ change_control_process: {
706
+ procedure: 'Any expansion of in-scope systems, addition of new domains, or changes to accepted requirements requires a written Scope Change Request (SCR).',
707
+ authorization: 'SCR must be signed by Customer Executive Sponsor and Supplier Practice Lead before work begins.',
708
+ superceding_rules: 'Superseded requirements are archived in .citable/sow/history/ with reason code and date.',
709
+ },
710
+
711
+ // Pillar 14: Risk Register
712
+ risk_register: [
713
+ { id: 'RISK-01', category: 'deployment', description: 'Customer deployment freeze during holiday quarter delays PR merges', mitigation: 'Schedule staging verification ahead of freeze; stage patches in feature flags' },
714
+ { id: 'RISK-02', category: 'platform', description: 'Search engine or AI answer engine algorithmic updates shift third-party citation UI', mitigation: 'Focus on deterministic schema and direct entity corroboration rather than transient UI exploits' },
715
+ { id: 'RISK-03', category: 'srm_contamination', description: 'Paid ad campaign sudden burst contaminates running A/B test cohort', mitigation: 'Enforce UTM parameter isolation and SRM daily monitoring' },
716
+ ],
717
+
718
+ // Pillar 15: Data Governance Requirements
719
+ data_governance: {
720
+ authorized_sources: ['Audited customer DOM', 'Search Console telemetry', 'Public AI search observation endpoints'],
721
+ retention_period: '90 days post-completion, followed by immutable archive',
722
+ processing_purpose: 'Exclusively for fulfilling contracted SOW engineering and verification obligations',
723
+ deletion_upon_termination: 'Supplier will delete all customer source code clones within 14 calendar days of contract completion',
724
+ },
725
+
726
+ // Pillar 16: Security & Privacy Requirements
727
+ security_requirements: [
728
+ 'Zero plaintext credentials stored; all authentication managed via environment variable tokens (credential_env).',
729
+ 'Least privilege: Supplier requires only read-only repository and analytics access.',
730
+ 'No customer PII (personally identifiable customer data) will be stored, processed, or logged.',
731
+ 'Incident notification: Any suspected security anomaly reported to customer security within 24 hours.',
732
+ ],
733
+
734
+ // Pillar 17: Platform & Third-Party Dependency Statement
735
+ platform_dependencies: [
736
+ 'Customer acknowledges that third-party search engines (Google, Bing) and AI platforms (OpenAI, Anthropic, Perplexity) operate autonomously. Supplier cannot control, and does not warrant, unilateral external algorithm modifications.',
737
+ ],
738
+
739
+ // Pillar 18: Quality Assurance & Validation Plan
740
+ qa_plan: {
741
+ methodology: 'Three-tier verification: (1) Static AST patch linting, (2) Closed-loop detector rerun, (3) Visual screenshot layout regression check.',
742
+ evidence_package: 'Every accepted deliverable includes a cryptographic SHA-256 hash-locked verification envelope.',
743
+ },
744
+
745
+ // Pillar 19: Reporting Cadence & Governance Model
746
+ governance_model: {
747
+ weekly_sync: '30-minute sprint progress and blocker resolution call',
748
+ written_reports: 'Bi-weekly status dashboard and updated traceability matrix',
749
+ decision_logging: 'All material technical decisions logged to .citable/decisions/',
750
+ },
751
+
752
+ // Pillar 20: Commercial Structure
753
+ commercial_terms: {
754
+ total_fixed_fee_minor: feeMinor,
755
+ total_fixed_fee_usd: feeUsd,
756
+ currency,
757
+ payment_terms: 'Net 30 upon verified deliverable acceptance',
758
+ milestones: commercialMilestones,
759
+ },
760
+
761
+ // Pillar 21: Out-of-Scope Section
762
+ out_of_scope: [
763
+ 'Writing bespoke corporate PR press releases or off-site guest blogging.',
764
+ 'Modifying legacy subdomains not explicitly enumerated in Executive Scope.',
765
+ 'Purchasing media ad credits, paid backlinks, or advertising inventory.',
766
+ 'Providing legal counsel regarding privacy regulations or intellectual property.',
767
+ ],
768
+
769
+ // Pillar 22: Warranty & Remediation Terms
770
+ warranty_terms: {
771
+ warranty_window_days: 30,
772
+ defect_definition: 'A defect is strictly defined as a failure of an accepted deliverable to satisfy its explicit Acceptance Criteria test upon rerun under unchanged baseline conditions.',
773
+ exclusion: 'Defects caused by subsequent customer code modifications, third-party library updates, or external platform outages are excluded.',
774
+ },
775
+
776
+ // Pillar 23: Completion & Operational Handoff
777
+ completion_and_handoff: {
778
+ handoff_assets: [
779
+ 'Merged and verified Git Pull Requests',
780
+ 'Signed Acceptance Certificates for each Work Package',
781
+ 'Immutable Evidence Package and Cryptographic Verification Envelope',
782
+ 'Developer Documentation & Runbook for ongoing automated maintenance',
783
+ ],
784
+ operational_signoff: 'Final operational transfer completed upon customer tech lead sign-off.',
785
+ },
786
+
787
+ // Pillar 24: Exit & Termination Provisions
788
+ termination_provisions: {
789
+ convenience: 'Either party may terminate upon 14 calendar days written notice.',
790
+ compensation: 'Customer shall compensate Supplier for all completed and accepted deliverables plus pro-rata work in progress up to notice date.',
791
+ asset_transfer: 'Supplier delivers all completed patches and evidence packages generated up to the termination effective date.',
792
+ },
793
+
794
+ // Pillar 25: The Final Traceability Matrix
795
+ traceability_matrix: traceabilityMatrix,
796
+
797
+ // Machine-readable Provenance Envelope
798
+ generation_provenance: {
799
+ generated_at: generatedAt,
800
+ generator_version: '1.18.1',
801
+ generation_mode: generationMode,
802
+ source_type: sourceProvenance.source_type || 'UNKNOWN',
803
+ source_identifier: sourceProvenance.source_identifier || null,
804
+ source_findings_count: sourceProvenance.findings_count,
805
+ findings_integrity_hash: sourceProvenance.integrity_hash || null,
806
+ synthetic_evidence: isSample,
807
+ },
808
+ };
809
+
810
+ validateSowInvariants(sow);
811
+
812
+ return sow;
813
+ }
814
+
815
+ /**
816
+ * Render Statement of Work as GitHub-flavored Markdown
817
+ */
818
+ export function renderSowMarkdown(sow) {
819
+ const modeBanner = sow.generation_mode === 'NON_CONTRACTUAL_SAMPLE'
820
+ ? '> ⚠️ **NON-CONTRACTUAL DEMONSTRATION SAMPLE**: This document was generated with synthetic audit baseline findings for evaluation and demonstration purposes. It does NOT represent a binding contractual obligation or live audit evidence.'
821
+ : (sow.generation_mode === 'DRAFT'
822
+ ? '> 📝 **DRAFT STATEMENT OF WORK**: This preliminary draft contains unfinalized engagement terms and requirements. Final executive approval required prior to signature.'
823
+ : '> **Contractual Principle**: This Statement of Work forces strict downward traceability from documented audit evidence to contractual obligation, and from obligation to verifiable acceptance. Findings are admitted strictly through a formal Admissibility Gate. In adherence to Citable governance principles, **no search rankings, AI citations, or conversion revenues are guaranteed**; fees are tied exclusively to objective deliverable acceptance.');
824
+
825
+ const lines = [
826
+ `# ${sow.title}`,
827
+ `====================================================================`,
828
+ `- **SOW ID**: \`${sow.sow_id}\` | **Version**: \`${sow.version}\` | **Mode**: \`${sow.generation_mode}\``,
829
+ `- **Client**: **${sow.client.name}** (${sow.client.contact})`,
830
+ `- **Supplier**: **${sow.supplier.name}** (${sow.supplier.contact})`,
831
+ `- **Effective Date**: \`${sow.effective_date}\` | **Term**: \`${sow.term_days} calendar days\``,
832
+ `- **Commercial Total**: **$${sow.commercial_total_fee_usd.toLocaleString('en-US', { minimumFractionDigits: (sow.commercial_total_fee_minor % 100 === 0) ? 0 : 2, maximumFractionDigits: 2 })} USD**`,
833
+ `- **Audit Source**: \`${sow.generation_provenance.source_type}\` (${sow.generation_provenance.source_identifier || 'N/A'}) | **Generator**: \`v${sow.generation_provenance.generator_version}\``,
834
+ ``,
835
+ modeBanner,
836
+ ``,
837
+ `---`,
838
+ `## 1. Executive Scope Statement`,
839
+ `- **Core Business Objective**: ${sow.executive_scope.business_objective}`,
840
+ `- **In-Scope Digital Properties**: ${sow.executive_scope.in_scope_properties.join(', ')}`,
841
+ `- **In-Scope Systems**: ${sow.executive_scope.in_scope_systems.join('; ')}`,
842
+ `- **Contracted Channels**: ${sow.executive_scope.channels.join(', ')}`,
843
+ `- **Geographies & Jurisdictions**: ${sow.executive_scope.geographies.join(', ')}`,
844
+ `- **Organizational Boundary**: ${sow.executive_scope.organizational_boundaries}`,
845
+ ``,
846
+ `## 2. Scope Admissibility Gate (Evidence-to-Work Filter)`,
847
+ `- **Total Audit Findings Evaluated**: ${sow.admissibility_gate.total_findings_evaluated}`,
848
+ `- **Admitted Contractual Requirements**: **${sow.admissibility_gate.admitted_count}** (${sow.admissibility_gate.admissibility_rate_pct}% admission rate)`,
849
+ `- **Refused / Excluded Findings**: **${sow.admissibility_gate.refused_count}** (preventing unverified or exploratory creep)`,
850
+ ``,
851
+ `### Refused & Deferred Findings Log:`,
852
+ `| Finding ID | Subject | Gate Failed | Refusal Code | Contractual Rationale |`,
853
+ `| :--- | :--- | :--- | :--- | :--- |`,
854
+ ...(sow.admissibility_gate.refusal_log.length > 0
855
+ ? sow.admissibility_gate.refusal_log.map((r) => `| \`${r.finding_id}\` | \`${r.subject}\` | \`${r.gate_failed}\` | \`${r.refusal_code}\` | ${r.refusal_rationale.slice(0, 75)}... |`)
856
+ : ['| None | N/A | N/A | N/A | All evaluated findings met strict admissibility criteria |']),
857
+ ``,
858
+ `## 3. Assumptions, Dependencies, Exclusions & Constraints`,
859
+ `### Explicit Assumptions:`,
860
+ ...sow.assumptions_and_constraints.assumptions.map((a) => `- ${a}`),
861
+ `### Key Dependencies:`,
862
+ ...sow.assumptions_and_constraints.dependencies.map((d) => `- ${d}`),
863
+ `### Known Constraints:`,
864
+ ...sow.assumptions_and_constraints.constraints.map((c) => `- ${c}`),
865
+ `### Unresolved Unknowns:`,
866
+ ...sow.assumptions_and_constraints.unresolved_unknowns.map((u) => `- [?] ${u}`),
867
+ ``,
868
+ `## 4. Prioritized Work Packages (Derived from ICE-BV)`,
869
+ ...sow.work_packages.map((wp) => `### ${wp.work_package_id}: ${wp.name}\n- **Owner**: \`${wp.owner}\`\n- **Deliverable Binding**: \`${wp.deliverable_id}\`\n- **Requirements Count**: ${wp.requirements.length} requirement(s)`),
870
+ ``,
871
+ `## 5. Detailed Deliverables & Delivery Criteria`,
872
+ `| Deliverable ID | Title | Artifact Type | Format | Delivery & Acceptance Criteria |`,
873
+ `| :--- | :--- | :--- | :--- | :--- |`,
874
+ ...sow.deliverables.map((d) => `| **${d.deliverable_id}** | ${d.title} | \`${d.artifact_type}\` | ${d.format} | ${d.delivery_criteria} |`),
875
+ ``,
876
+ `## 6. Technical Implementation Requirements`,
877
+ `- **Supported Frameworks**: ${sow.technical_requirements.supported_frameworks.join(', ')}`,
878
+ `- **Branching Model**: \`${sow.technical_requirements.git_branching_model}\``,
879
+ `- **Rollback Safeguard**: ${sow.technical_requirements.rollback_mechanism}`,
880
+ ``,
881
+ `## 7. Acceptance Criteria & Objective Verification Methods`,
882
+ `| Test ID | SOW Requirement | Validation Method | Pass / Fail Threshold | Required Evidence |`,
883
+ `| :--- | :--- | :--- | :--- | :--- |`,
884
+ ...sow.acceptance_criteria.map((a) => `| **${a.acceptance_test_id}** | \`${a.sow_requirement_id}\` | \`${a.validation_method}\` | ${a.pass_threshold} | ${a.evidence_required} |`),
885
+ ``,
886
+ `## 8. Baseline & Target-State Metrics (Non-Guarantee Disclosure)`,
887
+ `> *${sow.baseline_and_target_metrics.disclaimer}*`,
888
+ ``,
889
+ `| Dimension | Baseline State | Contracted Target State | Objective Verification |`,
890
+ `| :--- | :--- | :--- | :--- |`,
891
+ ...sow.baseline_and_target_metrics.metrics.map((m) => `| ${m.metric} | **${m.baseline}** | **${m.target}** | \`${m.validation}\` |`),
892
+ ``,
893
+ `## 9. Governed Experiment Specifications`,
894
+ `- **Statistical Standard**: ${sow.experiment_specifications.standard}`,
895
+ `- **Stopping & Guardrails**: ${sow.experiment_specifications.stopping_criteria}`,
896
+ ...sow.experiment_specifications.guardrails.map((g) => `- ${g}`),
897
+ ``,
898
+ `## 10. Roles & RACI Matrix`,
899
+ `| Role | Responsibility Description | RACI Designation |`,
900
+ `| :--- | :--- | :--- |`,
901
+ ...sow.raci_matrix.map((r) => `| **${r.role}** | ${r.r} | **${r.raci}** |`),
902
+ ``,
903
+ `## 11. Customer Obligations & SLAs`,
904
+ ...sow.customer_obligations.map((o) => `- ${o}`),
905
+ ``,
906
+ `## 12. Delivery Sequencing & Milestone Schedule`,
907
+ `| Milestone | Work Package | Target Week | Fee (USD) | Acceptance Trigger |`,
908
+ `| :--- | :--- | :--- | :--- | :--- |`,
909
+ ...sow.delivery_schedule.milestones.map((m) => `| **${m.milestone_id}** | \`${m.work_package_id}\` | Week ${m.target_delivery_week} | **$${m.fee_usd.toLocaleString('en-US', { minimumFractionDigits: (m.fee_minor % 100 === 0) ? 0 : 2, maximumFractionDigits: 2 })}** | ${m.billing_trigger} |`),
910
+ ``,
911
+ `## 13. Scope Change-Control Process`,
912
+ `- **Procedure**: ${sow.change_control_process.procedure}`,
913
+ `- **Authorization**: ${sow.change_control_process.authorization}`,
914
+ `- **Superseded Controls**: ${sow.change_control_process.superceding_rules}`,
915
+ ``,
916
+ `## 14. Risk & Dependency Register`,
917
+ `| Risk ID | Category | Risk Description | Planned Mitigation |`,
918
+ `| :--- | :--- | :--- | :--- |`,
919
+ ...sow.risk_register.map((r) => `| **${r.id}** | \`${r.category}\` | ${r.description} | ${r.mitigation} |`),
920
+ ``,
921
+ `## 15. Data Governance & Information Handling`,
922
+ `- **Authorized Sources**: ${sow.data_governance.authorized_sources.join(', ')}`,
923
+ `- **Retention & Purging**: ${sow.data_governance.retention_period}; ${sow.data_governance.deletion_upon_termination}`,
924
+ ``,
925
+ `## 16. Security & Privacy Safeguards`,
926
+ ...sow.security_requirements.map((s) => `- ${s}`),
927
+ ``,
928
+ `## 17. Platform & Third-Party Dependency Disclosures`,
929
+ ...sow.platform_dependencies.map((p) => `> ${p}`),
930
+ ``,
931
+ `## 18. Quality Assurance & Validation Plan`,
932
+ `- **QA Methodology**: ${sow.qa_plan.methodology}`,
933
+ `- **Evidence Integrity**: ${sow.qa_plan.evidence_package}`,
934
+ ``,
935
+ `## 19. Governance Cadence & Reporting Model`,
936
+ `- **Weekly Sync**: ${sow.governance_model.weekly_sync}`,
937
+ `- **Status Reports**: ${sow.governance_model.written_reports}`,
938
+ `- **Decision Records**: ${sow.governance_model.decision_logging}`,
939
+ ``,
940
+ `## 20. Commercial Terms & Payment Schedule`,
941
+ `- **Total Contract Value**: **$${sow.commercial_terms.total_fixed_fee_usd.toLocaleString('en-US', { minimumFractionDigits: (sow.commercial_terms.total_fixed_fee_minor % 100 === 0) ? 0 : 2, maximumFractionDigits: 2 })} USD**`,
942
+ `- **Terms**: ${sow.commercial_terms.payment_terms}`,
943
+ ``,
944
+ `## 21. Explicit Out-of-Scope Declarations`,
945
+ ...sow.out_of_scope.map((item) => `- [x] **OUT OF SCOPE**: ${item}`),
946
+ ``,
947
+ `## 22. Warranty & Remediation Terms`,
948
+ `- **Warranty Period**: **${sow.warranty_terms.warranty_window_days} calendar days** following written deliverable acceptance.`,
949
+ `- **Defect Standard**: ${sow.warranty_terms.defect_definition}`,
950
+ `- **Warranty Exclusions**: ${sow.warranty_terms.exclusion}`,
951
+ ``,
952
+ `## 23. Completion & Operational Handoff`,
953
+ `### Required Deliverable Handoff Assets:`,
954
+ ...sow.completion_and_handoff.handoff_assets.map((h) => `- [x] ${h}`),
955
+ ``,
956
+ `## 24. Exit, Transition & Termination Provisions`,
957
+ `- **Termination for Convenience**: ${sow.termination_provisions.convenience}`,
958
+ `- **Accrued Compensation**: ${sow.termination_provisions.compensation}`,
959
+ `- **Asset Return**: ${sow.termination_provisions.asset_transfer}`,
960
+ ``,
961
+ `---`,
962
+ `## 25. THE FINAL TRACEABILITY MATRIX`,
963
+ `### Finding → Recommendation → SOW Requirement → Deliverable → Acceptance Test → Owner → Evidence`,
964
+ ``,
965
+ `| Finding ID | Recommendation | SOW Req ID | Deliverable ID | Acceptance Test ID | Responsible Owner | Source Evidence |`,
966
+ `| :--- | :--- | :--- | :--- | :--- | :--- | :--- |`,
967
+ ...sow.traceability_matrix.map((t) => `| **\`${t.finding_id}\`** | ${t.recommendation.slice(0, 45)}... | **\`${t.sow_requirement_id}\`** | \`${t.deliverable_id}\` | \`${t.acceptance_test_id}\` | ${t.owner} | \`${t.evidence_ids?.join(', ') || t.evidence_id}\` |`),
968
+ ``,
969
+ `---`,
970
+ `### Contract Execution & Authorization`,
971
+ ``,
972
+ `| On Behalf of Customer: ${sow.client.name} | On Behalf of Supplier: ${sow.supplier.name} |`,
973
+ `| :--- | :--- |`,
974
+ `| Signature: __________________________________ | Signature: __________________________________ |`,
975
+ `| Name: _____________________________________ | Name: _____________________________________ |`,
976
+ `| Title: ______________________________________ | Title: ______________________________________ |`,
977
+ `| Date: _______________________________________ | Date: _______________________________________ |`,
978
+ ];
979
+
980
+ return lines.join('\n');
981
+ }
982
+
983
+ /**
984
+ * Render Statement of Work as Standalone Enterprise HTML
985
+ */
986
+ export function renderSowHtml(sow) {
987
+ const isSample = sow.generation_mode === 'NON_CONTRACTUAL_SAMPLE';
988
+ const isDraft = sow.generation_mode === 'DRAFT';
989
+ const badgeColor = isSample ? 'var(--warning)' : (isDraft ? 'var(--accent)' : 'var(--success)');
990
+ const modeNotice = isSample
991
+ ? '<div class="disclosure" style="border-left-color:var(--warning); background:rgba(210,153,34,0.15);"><strong style="color:var(--warning);">DEMONSTRATION SAMPLE NOTICE:</strong> This Statement of Work contains synthetic audit baseline findings for evaluation and demonstration purposes. It does NOT represent a binding contractual obligation or live audit evidence.</div>'
992
+ : (isDraft
993
+ ? '<div class="disclosure"><strong style="color:var(--accent);">DRAFT ENGAGEMENT NOTICE:</strong> This document represents a preliminary draft with unfinalized commercial parameters. Final executive sign-off required prior to contract binding.</div>'
994
+ : '<div class="disclosure"><strong>Enterprise Statement of Work Governance Notice:</strong> This agreement binds supplier fees exclusively to verified deliverable acceptance and closed-loop test execution. In compliance with Citable governance standards, <strong>no search engine ranking, AI citation presence, or commercial conversion revenue outcomes are guaranteed</strong>.</div>');
995
+
996
+ return `<!DOCTYPE html>
997
+ <html lang="en">
998
+ <head>
999
+ <meta charset="UTF-8">
1000
+ <title>${sow.title} - ${sow.sow_id}</title>
1001
+ <style>
1002
+ :root { --bg: #0d1117; --card: #161b22; --border: #30363d; --text: #c9d1d9; --accent: #58a6ff; --danger: #f85149; --warning: #d29922; --success: #3fb950; }
1003
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; margin: 0; padding: 40px 20px; }
1004
+ .container { max-width: 1180px; margin: 0 auto; }
1005
+ header { border-bottom: 2px solid var(--border); padding-bottom: 24px; margin-bottom: 32px; }
1006
+ h1 { font-size: 28px; margin: 0 0 10px 0; color: #fff; }
1007
+ .sow-badge { background: ${badgeColor}; color: #000; padding: 4px 10px; border-radius: 4px; font-size: 13px; font-weight: bold; }
1008
+ .meta-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin: 24px 0; }
1009
+ .card { background: var(--card); border: 1px solid var(--border); border-radius: 6px; padding: 18px; }
1010
+ .card-val { font-size: 26px; font-weight: bold; color: #fff; margin-top: 4px; }
1011
+ .disclosure { background: rgba(56, 139, 253, 0.1); border-left: 4px solid var(--accent); padding: 14px 18px; border-radius: 4px; font-size: 13px; margin: 24px 0; }
1012
+ .section-title { font-size: 20px; border-bottom: 1px solid var(--border); padding-bottom: 8px; margin: 36px 0 16px 0; color: #fff; }
1013
+ table { width: 100%; border-collapse: collapse; margin: 20px 0; background: var(--card); border-radius: 6px; overflow: hidden; border: 1px solid var(--border); }
1014
+ th, td { padding: 12px 14px; text-align: left; border-bottom: 1px solid var(--border); font-size: 13px; }
1015
+ th { background: #21262d; color: #8b949e; font-weight: 600; }
1016
+ .badge { display: inline-block; padding: 2px 6px; border-radius: 4px; font-size: 11px; font-weight: bold; }
1017
+ .badge-admit { background: rgba(63, 185, 80, 0.2); color: var(--success); }
1018
+ .badge-refuse { background: rgba(248, 81, 73, 0.2); color: var(--danger); }
1019
+ .code-ref { font-family: monospace; color: var(--accent); background: rgba(88, 166, 255, 0.15); padding: 2px 5px; border-radius: 3px; }
1020
+ .sig-table { margin-top: 40px; }
1021
+ .sig-table td { height: 60px; vertical-align: top; }
1022
+ </style>
1023
+ </head>
1024
+ <body>
1025
+ <div class="container">
1026
+ <header>
1027
+ <div style="display:flex; justify-content:space-between; align-items:center;">
1028
+ <h1>${sow.title}</h1>
1029
+ <div>
1030
+ <span class="sow-badge">${sow.generation_mode}</span>
1031
+ <span class="sow-badge" style="background:#1f6feb; color:#fff; margin-left:8px;">${sow.sow_id}</span>
1032
+ </div>
1033
+ </div>
1034
+ <div style="color:#8b949e; font-size:14px; margin-top:8px;">
1035
+ Client: <strong>${sow.client.name}</strong> &nbsp;|&nbsp; Supplier: <strong>${sow.supplier.name}</strong> &nbsp;|&nbsp; Term: <strong>${sow.term_days} Days</strong> &nbsp;|&nbsp; Effective: <strong>${sow.effective_date}</strong> &nbsp;|&nbsp; Source: <strong>${sow.generation_provenance.source_type}</strong>
1036
+ </div>
1037
+ </header>
1038
+
1039
+ ${modeNotice}
1040
+
1041
+ <div class="meta-grid">
1042
+ <div class="card"><div>Total Contract Fee</div><div class="card-val">$${sow.commercial_total_fee_usd.toLocaleString('en-US', { minimumFractionDigits: (sow.commercial_total_fee_minor % 100 === 0) ? 0 : 2, maximumFractionDigits: 2 })}</div></div>
1043
+ <div class="card"><div>Admitted SOW Requirements</div><div class="card-val">${sow.admissibility_gate.admitted_count}</div></div>
1044
+ <div class="card"><div>Refused / Excluded Scope</div><div class="card-val" style="color:var(--danger);">${sow.admissibility_gate.refused_count}</div></div>
1045
+ <div class="card"><div>Contracted Work Packages</div><div class="card-val">${sow.work_packages.length}</div></div>
1046
+ </div>
1047
+
1048
+ <h2 class="section-title">1. Executive Scope Statement</h2>
1049
+ <div class="card">
1050
+ <p><strong>Objective:</strong> ${sow.executive_scope.business_objective}</p>
1051
+ <p><strong>In-Scope Properties:</strong> <code>${sow.executive_scope.in_scope_properties.join(', ')}</code></p>
1052
+ <p><strong>In-Scope Systems:</strong> ${sow.executive_scope.in_scope_systems.join('; ')}</p>
1053
+ <p><strong>Boundaries:</strong> ${sow.executive_scope.organizational_boundaries}</p>
1054
+ </div>
1055
+
1056
+ <h2 class="section-title">2. Scope Admissibility Gate & Excluded Findings Log</h2>
1057
+ <table>
1058
+ <thead>
1059
+ <tr><th>Finding ID</th><th>Subject</th><th>Gate Failed</th><th>Refusal Code</th><th>Contractual Refusal Rationale</th></tr>
1060
+ </thead>
1061
+ <tbody>
1062
+ ${sow.admissibility_gate.refusal_log.map((r) => `
1063
+ <tr>
1064
+ <td><span class="code-ref">${r.finding_id}</span></td>
1065
+ <td>${r.subject}</td>
1066
+ <td><code>${r.gate_failed}</code></td>
1067
+ <td><span class="badge badge-refuse">${r.refusal_code}</span></td>
1068
+ <td>${r.refusal_rationale}</td>
1069
+ </tr>
1070
+ `).join('')}
1071
+ </tbody>
1072
+ </table>
1073
+
1074
+ <h2 class="section-title">3. Contracted Deliverables & Milestone Payment Schedule</h2>
1075
+ <table>
1076
+ <thead>
1077
+ <tr><th>Milestone</th><th>Work Package</th><th>Deliverable ID</th><th>Delivery Criteria</th><th>Milestone Fee</th></tr>
1078
+ </thead>
1079
+ <tbody>
1080
+ ${sow.delivery_schedule.milestones.map((m) => `
1081
+ <tr>
1082
+ <td><strong>${m.milestone_id}</strong></td>
1083
+ <td>${m.name}</td>
1084
+ <td><span class="code-ref">${m.work_package_id}</span></td>
1085
+ <td>${m.billing_trigger}</td>
1086
+ <td style="font-weight:bold; color:var(--success);">$${m.fee_usd.toLocaleString('en-US', { minimumFractionDigits: (m.fee_minor % 100 === 0) ? 0 : 2, maximumFractionDigits: 2 })} USD</td>
1087
+ </tr>
1088
+ `).join('')}
1089
+ </tbody>
1090
+ </table>
1091
+
1092
+ <h2 class="section-title">25. The Final Traceability Matrix</h2>
1093
+ <p style="color:#8b949e; font-size:13px;">Proving exactly why work is performed, what must be delivered, who owns it, how it will be validated, and what evidence justifies it.</p>
1094
+ <table>
1095
+ <thead>
1096
+ <tr><th>Finding ID</th><th>Recommendation</th><th>SOW Req ID</th><th>Deliverable</th><th>Acceptance Test ID</th><th>Responsible Owner</th><th>Source Evidence</th></tr>
1097
+ </thead>
1098
+ <tbody>
1099
+ ${sow.traceability_matrix.map((t) => `
1100
+ <tr>
1101
+ <td><span class="code-ref">${t.finding_id}</span></td>
1102
+ <td>${t.recommendation}</td>
1103
+ <td><strong>${t.sow_requirement_id}</strong></td>
1104
+ <td>${t.deliverable_id}</td>
1105
+ <td><code>${t.acceptance_test_id}</code></td>
1106
+ <td>${t.owner}</td>
1107
+ <td><span class="code-ref">${t.evidence_ids?.join(', ') || t.evidence_id}</span></td>
1108
+ </tr>
1109
+ `).join('')}
1110
+ </tbody>
1111
+ </table>
1112
+
1113
+ <h2 class="section-title">Signatures & Contractual Authorization</h2>
1114
+ <table class="sig-table">
1115
+ <thead>
1116
+ <tr><th>For Customer: ${sow.client.name}</th><th>For Supplier: ${sow.supplier.name}</th></tr>
1117
+ </thead>
1118
+ <tbody>
1119
+ <tr>
1120
+ <td>
1121
+ <br>Signature: _______________________________<br><br>
1122
+ Name: __________________________________<br><br>
1123
+ Title: ___________________________________<br><br>
1124
+ Date: ____________________________________
1125
+ </td>
1126
+ <td>
1127
+ <br>Signature: _______________________________<br><br>
1128
+ Name: __________________________________<br><br>
1129
+ Title: ___________________________________<br><br>
1130
+ Date: ____________________________________
1131
+ </td>
1132
+ </tr>
1133
+ </tbody>
1134
+ </table>
1135
+ </div>
1136
+ </body>
1137
+ </html>`;
1138
+ }
1139
+
1140
+ /**
1141
+ * Export SOW deliverable to filesystem or string with strict schema validation
1142
+ */
1143
+ export async function exportSow(root, options = {}) {
1144
+ const sow = await generateSow(root, options);
1145
+ const validation = validateAgainst('sow.schema.json', sow);
1146
+ if (!validation.valid) {
1147
+ throw new SowError(`Generated SOW violates schemas/sow.schema.json: ${validation.errors.join('; ')}`, 'SCHEMA_VALIDATION_FAILED');
1148
+ }
1149
+
1150
+ const format = options.format || 'markdown';
1151
+ let content = '';
1152
+
1153
+ if (format === 'html' || format === 'html-brief') {
1154
+ content = renderSowHtml(sow);
1155
+ } else if (format === 'json') {
1156
+ content = JSON.stringify(sow, null, 2);
1157
+ } else {
1158
+ content = renderSowMarkdown(sow);
1159
+ }
1160
+
1161
+ let outputPath = null;
1162
+ if (options.output) {
1163
+ outputPath = path.resolve(root, options.output);
1164
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
1165
+ fs.writeFileSync(outputPath, content, 'utf8');
1166
+ }
1167
+
1168
+ return {
1169
+ sow_id: sow.sow_id,
1170
+ title: sow.title,
1171
+ client: sow.client,
1172
+ supplier: sow.supplier,
1173
+ format,
1174
+ output_path: outputPath,
1175
+ content,
1176
+ data: sow,
1177
+ };
1178
+ }