@nebulacomponents/citable 1.18.0 → 1.18.2

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 (190) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/universal/.agents/skills/citable/SKILL.md +22 -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/artifact-interchange.md +5 -0
  8. package/dist/universal/.agents/skills/citable/commands/remediation.md +55 -0
  9. package/dist/universal/.agents/skills/citable/commands/reporting.md +28 -0
  10. package/dist/universal/.agents/skills/citable/commands/sow.md +95 -0
  11. package/dist/universal/.agents/skills/citable/manifest.json +15 -9
  12. package/dist/universal/.agents/skills/citable/schemas/cro-report.schema.json +112 -0
  13. package/dist/universal/.agents/skills/citable/schemas/search-report.schema.json +99 -0
  14. package/dist/universal/.agents/skills/citable/schemas/sow.schema.json +344 -26
  15. package/dist/universal/.claude/agents/citable/citable-remediator.md +43 -0
  16. package/dist/universal/.claude/agents/citable/citable-sow-architect.md +43 -0
  17. package/dist/universal/.claude/agents/citable/manifest.json +6 -4
  18. package/dist/universal/.claude/skills/citable/SKILL.md +22 -1
  19. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  20. package/dist/universal/.claude/skills/citable/agents/citable-remediator.md +43 -0
  21. package/dist/universal/.claude/skills/citable/agents/citable-sow-architect.md +43 -0
  22. package/dist/universal/.claude/skills/citable/commands/README.md +13 -0
  23. package/dist/universal/.claude/skills/citable/commands/artifact-interchange.md +5 -0
  24. package/dist/universal/.claude/skills/citable/commands/remediation.md +55 -0
  25. package/dist/universal/.claude/skills/citable/commands/reporting.md +28 -0
  26. package/dist/universal/.claude/skills/citable/commands/sow.md +95 -0
  27. package/dist/universal/.claude/skills/citable/manifest.json +19 -11
  28. package/dist/universal/.claude/skills/citable/schemas/cro-report.schema.json +112 -0
  29. package/dist/universal/.claude/skills/citable/schemas/search-report.schema.json +99 -0
  30. package/dist/universal/.claude/skills/citable/schemas/sow.schema.json +344 -26
  31. package/dist/universal/.cursor/skills/citable/SKILL.md +22 -1
  32. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  33. package/dist/universal/.cursor/skills/citable/agents/citable-remediator.md +43 -0
  34. package/dist/universal/.cursor/skills/citable/agents/citable-sow-architect.md +43 -0
  35. package/dist/universal/.cursor/skills/citable/commands/README.md +13 -0
  36. package/dist/universal/.cursor/skills/citable/commands/artifact-interchange.md +5 -0
  37. package/dist/universal/.cursor/skills/citable/commands/remediation.md +55 -0
  38. package/dist/universal/.cursor/skills/citable/commands/reporting.md +28 -0
  39. package/dist/universal/.cursor/skills/citable/commands/sow.md +95 -0
  40. package/dist/universal/.cursor/skills/citable/manifest.json +15 -9
  41. package/dist/universal/.cursor/skills/citable/schemas/cro-report.schema.json +112 -0
  42. package/dist/universal/.cursor/skills/citable/schemas/search-report.schema.json +99 -0
  43. package/dist/universal/.cursor/skills/citable/schemas/sow.schema.json +344 -26
  44. package/dist/universal/.gemini/skills/citable/SKILL.md +22 -1
  45. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  46. package/dist/universal/.gemini/skills/citable/agents/citable-remediator.md +43 -0
  47. package/dist/universal/.gemini/skills/citable/agents/citable-sow-architect.md +43 -0
  48. package/dist/universal/.gemini/skills/citable/commands/README.md +13 -0
  49. package/dist/universal/.gemini/skills/citable/commands/artifact-interchange.md +5 -0
  50. package/dist/universal/.gemini/skills/citable/commands/remediation.md +55 -0
  51. package/dist/universal/.gemini/skills/citable/commands/reporting.md +28 -0
  52. package/dist/universal/.gemini/skills/citable/commands/sow.md +95 -0
  53. package/dist/universal/.gemini/skills/citable/manifest.json +15 -9
  54. package/dist/universal/.gemini/skills/citable/schemas/cro-report.schema.json +112 -0
  55. package/dist/universal/.gemini/skills/citable/schemas/search-report.schema.json +99 -0
  56. package/dist/universal/.gemini/skills/citable/schemas/sow.schema.json +344 -26
  57. package/dist/universal/.github/skills/citable/SKILL.md +22 -1
  58. package/dist/universal/.github/skills/citable/VERSION +1 -1
  59. package/dist/universal/.github/skills/citable/agents/citable-remediator.md +43 -0
  60. package/dist/universal/.github/skills/citable/agents/citable-sow-architect.md +43 -0
  61. package/dist/universal/.github/skills/citable/commands/README.md +13 -0
  62. package/dist/universal/.github/skills/citable/commands/artifact-interchange.md +5 -0
  63. package/dist/universal/.github/skills/citable/commands/remediation.md +55 -0
  64. package/dist/universal/.github/skills/citable/commands/reporting.md +28 -0
  65. package/dist/universal/.github/skills/citable/commands/sow.md +95 -0
  66. package/dist/universal/.github/skills/citable/manifest.json +15 -9
  67. package/dist/universal/.github/skills/citable/schemas/cro-report.schema.json +112 -0
  68. package/dist/universal/.github/skills/citable/schemas/search-report.schema.json +99 -0
  69. package/dist/universal/.github/skills/citable/schemas/sow.schema.json +344 -26
  70. package/dist/universal/.kiro/skills/citable/SKILL.md +22 -1
  71. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  72. package/dist/universal/.kiro/skills/citable/agents/citable-remediator.md +43 -0
  73. package/dist/universal/.kiro/skills/citable/agents/citable-sow-architect.md +43 -0
  74. package/dist/universal/.kiro/skills/citable/commands/README.md +13 -0
  75. package/dist/universal/.kiro/skills/citable/commands/artifact-interchange.md +5 -0
  76. package/dist/universal/.kiro/skills/citable/commands/remediation.md +55 -0
  77. package/dist/universal/.kiro/skills/citable/commands/reporting.md +28 -0
  78. package/dist/universal/.kiro/skills/citable/commands/sow.md +95 -0
  79. package/dist/universal/.kiro/skills/citable/manifest.json +15 -9
  80. package/dist/universal/.kiro/skills/citable/schemas/cro-report.schema.json +112 -0
  81. package/dist/universal/.kiro/skills/citable/schemas/search-report.schema.json +99 -0
  82. package/dist/universal/.kiro/skills/citable/schemas/sow.schema.json +344 -26
  83. package/dist/universal/.opencode/skills/citable/SKILL.md +22 -1
  84. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  85. package/dist/universal/.opencode/skills/citable/agents/citable-remediator.md +43 -0
  86. package/dist/universal/.opencode/skills/citable/agents/citable-sow-architect.md +43 -0
  87. package/dist/universal/.opencode/skills/citable/commands/README.md +13 -0
  88. package/dist/universal/.opencode/skills/citable/commands/artifact-interchange.md +5 -0
  89. package/dist/universal/.opencode/skills/citable/commands/remediation.md +55 -0
  90. package/dist/universal/.opencode/skills/citable/commands/reporting.md +28 -0
  91. package/dist/universal/.opencode/skills/citable/commands/sow.md +95 -0
  92. package/dist/universal/.opencode/skills/citable/manifest.json +15 -9
  93. package/dist/universal/.opencode/skills/citable/schemas/cro-report.schema.json +112 -0
  94. package/dist/universal/.opencode/skills/citable/schemas/search-report.schema.json +99 -0
  95. package/dist/universal/.opencode/skills/citable/schemas/sow.schema.json +344 -26
  96. package/dist/universal/.pi/agent/skills/citable/SKILL.md +22 -1
  97. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  98. package/dist/universal/.pi/agent/skills/citable/agents/citable-remediator.md +43 -0
  99. package/dist/universal/.pi/agent/skills/citable/agents/citable-sow-architect.md +43 -0
  100. package/dist/universal/.pi/agent/skills/citable/commands/README.md +13 -0
  101. package/dist/universal/.pi/agent/skills/citable/commands/artifact-interchange.md +5 -0
  102. package/dist/universal/.pi/agent/skills/citable/commands/remediation.md +55 -0
  103. package/dist/universal/.pi/agent/skills/citable/commands/reporting.md +28 -0
  104. package/dist/universal/.pi/agent/skills/citable/commands/sow.md +95 -0
  105. package/dist/universal/.pi/agent/skills/citable/manifest.json +15 -9
  106. package/dist/universal/.pi/agent/skills/citable/schemas/cro-report.schema.json +112 -0
  107. package/dist/universal/.pi/agent/skills/citable/schemas/search-report.schema.json +99 -0
  108. package/dist/universal/.pi/agent/skills/citable/schemas/sow.schema.json +344 -26
  109. package/dist/universal/.qoder/skills/citable/SKILL.md +22 -1
  110. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  111. package/dist/universal/.qoder/skills/citable/agents/citable-remediator.md +43 -0
  112. package/dist/universal/.qoder/skills/citable/agents/citable-sow-architect.md +43 -0
  113. package/dist/universal/.qoder/skills/citable/commands/README.md +13 -0
  114. package/dist/universal/.qoder/skills/citable/commands/artifact-interchange.md +5 -0
  115. package/dist/universal/.qoder/skills/citable/commands/remediation.md +55 -0
  116. package/dist/universal/.qoder/skills/citable/commands/reporting.md +28 -0
  117. package/dist/universal/.qoder/skills/citable/commands/sow.md +95 -0
  118. package/dist/universal/.qoder/skills/citable/manifest.json +15 -9
  119. package/dist/universal/.qoder/skills/citable/schemas/cro-report.schema.json +112 -0
  120. package/dist/universal/.qoder/skills/citable/schemas/search-report.schema.json +99 -0
  121. package/dist/universal/.qoder/skills/citable/schemas/sow.schema.json +344 -26
  122. package/dist/universal/.rovodev/skills/citable/SKILL.md +22 -1
  123. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  124. package/dist/universal/.rovodev/skills/citable/agents/citable-remediator.md +43 -0
  125. package/dist/universal/.rovodev/skills/citable/agents/citable-sow-architect.md +43 -0
  126. package/dist/universal/.rovodev/skills/citable/commands/README.md +13 -0
  127. package/dist/universal/.rovodev/skills/citable/commands/artifact-interchange.md +5 -0
  128. package/dist/universal/.rovodev/skills/citable/commands/remediation.md +55 -0
  129. package/dist/universal/.rovodev/skills/citable/commands/reporting.md +28 -0
  130. package/dist/universal/.rovodev/skills/citable/commands/sow.md +95 -0
  131. package/dist/universal/.rovodev/skills/citable/manifest.json +15 -9
  132. package/dist/universal/.rovodev/skills/citable/schemas/cro-report.schema.json +112 -0
  133. package/dist/universal/.rovodev/skills/citable/schemas/search-report.schema.json +99 -0
  134. package/dist/universal/.rovodev/skills/citable/schemas/sow.schema.json +344 -26
  135. package/dist/universal/.trae/skills/citable/SKILL.md +22 -1
  136. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  137. package/dist/universal/.trae/skills/citable/agents/citable-remediator.md +43 -0
  138. package/dist/universal/.trae/skills/citable/agents/citable-sow-architect.md +43 -0
  139. package/dist/universal/.trae/skills/citable/commands/README.md +13 -0
  140. package/dist/universal/.trae/skills/citable/commands/artifact-interchange.md +5 -0
  141. package/dist/universal/.trae/skills/citable/commands/remediation.md +55 -0
  142. package/dist/universal/.trae/skills/citable/commands/reporting.md +28 -0
  143. package/dist/universal/.trae/skills/citable/commands/sow.md +95 -0
  144. package/dist/universal/.trae/skills/citable/manifest.json +15 -9
  145. package/dist/universal/.trae/skills/citable/schemas/cro-report.schema.json +112 -0
  146. package/dist/universal/.trae/skills/citable/schemas/search-report.schema.json +99 -0
  147. package/dist/universal/.trae/skills/citable/schemas/sow.schema.json +344 -26
  148. package/dist/universal/.trae-cn/skills/citable/SKILL.md +22 -1
  149. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  150. package/dist/universal/.trae-cn/skills/citable/agents/citable-remediator.md +43 -0
  151. package/dist/universal/.trae-cn/skills/citable/agents/citable-sow-architect.md +43 -0
  152. package/dist/universal/.trae-cn/skills/citable/commands/README.md +13 -0
  153. package/dist/universal/.trae-cn/skills/citable/commands/artifact-interchange.md +5 -0
  154. package/dist/universal/.trae-cn/skills/citable/commands/remediation.md +55 -0
  155. package/dist/universal/.trae-cn/skills/citable/commands/reporting.md +28 -0
  156. package/dist/universal/.trae-cn/skills/citable/commands/sow.md +95 -0
  157. package/dist/universal/.trae-cn/skills/citable/manifest.json +15 -9
  158. package/dist/universal/.trae-cn/skills/citable/schemas/cro-report.schema.json +112 -0
  159. package/dist/universal/.trae-cn/skills/citable/schemas/search-report.schema.json +99 -0
  160. package/dist/universal/.trae-cn/skills/citable/schemas/sow.schema.json +344 -26
  161. package/dist/universal/manifest.json +186 -112
  162. package/package.json +2 -1
  163. package/schemas/cro-report.schema.json +112 -0
  164. package/schemas/search-report.schema.json +99 -0
  165. package/schemas/sow.schema.json +344 -26
  166. package/skill/SKILL.md +22 -1
  167. package/skill/agents/citable-remediator.md +43 -0
  168. package/skill/agents/citable-sow-architect.md +43 -0
  169. package/skill/commands/README.md +13 -0
  170. package/skill/commands/artifact-interchange.md +5 -0
  171. package/skill/commands/remediation.md +55 -0
  172. package/skill/commands/reporting.md +28 -0
  173. package/skill/commands/sow.md +95 -0
  174. package/src/analysis/behavioral.js +5 -4
  175. package/src/analysis/eeat.js +10 -9
  176. package/src/analysis/offpage.js +19 -36
  177. package/src/analysis/readiness.js +8 -2
  178. package/src/artifacts/verifyCustomerArtifact.js +283 -0
  179. package/src/cli/index.js +8 -1
  180. package/src/commands/sowCmd.js +17 -0
  181. package/src/reporting/executiveCroReport.js +393 -229
  182. package/src/reporting/executiveExport.js +6 -2
  183. package/src/reporting/executiveSearchReport.js +519 -271
  184. package/src/shared/domainUtils.js +140 -0
  185. package/src/shared/epistemicStatus.js +111 -0
  186. package/src/shared/evidenceSourceResolver.js +286 -0
  187. package/src/shared/htmlEscape.js +65 -0
  188. package/src/shared/runPackageVerifier.js +153 -0
  189. package/src/sow/admissibilityGate.js +234 -46
  190. package/src/sow/generateSow.js +524 -142
@@ -4,143 +4,468 @@ import { buildContext } from '../commands/context.js';
4
4
  import { selectDetectors } from '../detectors/index.js';
5
5
  import { runDetectors, indexTargets } from '../detectors/framework.js';
6
6
  import { evaluateScopeAdmissibility } from './admissibilityGate.js';
7
- import { readJson, nowIso } from '../shared/io.js';
7
+ import { readJson, nowIso, sha256 } from '../shared/io.js';
8
+ import { validateAgainst } from '../shared/schemaValidator.js';
9
+ import {
10
+ resolveEvidenceSource,
11
+ getCanonicalRunTimestamp,
12
+ sortRunCandidatesChronologically,
13
+ } from '../shared/evidenceSourceResolver.js';
14
+ import {
15
+ escapeHtml,
16
+ escapeHtmlAttr,
17
+ sanitizeUrl,
18
+ escapeMarkdownTableCell,
19
+ sanitizeForMarkdown,
20
+ } from '../shared/htmlEscape.js';
21
+
22
+ /**
23
+ * Typed SOW Domain Errors
24
+ */
25
+ export class SowError extends Error {
26
+ constructor(message, code = 'SOW_ERROR') {
27
+ super(message);
28
+ this.name = this.constructor.name;
29
+ this.code = code;
30
+ }
31
+ }
32
+
33
+ export class RunNotFoundError extends SowError {
34
+ constructor(runId) {
35
+ super(`Specified run not found: ${runId}`, 'RUN_NOT_FOUND');
36
+ this.runId = runId;
37
+ }
38
+ }
39
+
40
+ export class FindingsMissingError extends SowError {
41
+ constructor(runId, filePath) {
42
+ super(`Run ${runId} is missing findings.json at ${filePath}`, 'FINDINGS_MISSING');
43
+ this.runId = runId;
44
+ this.filePath = filePath;
45
+ }
46
+ }
47
+
48
+ export class FindingsInvalidError extends SowError {
49
+ constructor(runId, message) {
50
+ super(`Run ${runId} contains malformed or invalid findings: ${message}`, 'FINDINGS_INVALID');
51
+ this.runId = runId;
52
+ }
53
+ }
54
+
55
+ export class LiveInspectionFailedError extends SowError {
56
+ constructor(target, reason) {
57
+ super(`Live target inspection failed for ${target}: ${reason}`, 'LIVE_INSPECTION_FAILED');
58
+ this.target = target;
59
+ }
60
+ }
61
+
62
+ export class NoFindingsError extends SowError {
63
+ constructor(source) {
64
+ super(`No audit findings available from ${source}. Production SOW generation requires authoritative findings. (Use --sample to generate non-contractual demonstration SOW)`, 'NO_FINDINGS');
65
+ }
66
+ }
67
+
68
+ export class NoAdmissibleRequirementsError extends SowError {
69
+ constructor(totalEvaluated, refusalCount) {
70
+ super(`Scope admissibility gate refused all ${totalEvaluated} findings (${refusalCount} refused). Zero contractable requirements remain.`, 'NO_ADMISSIBLE_REQUIREMENTS');
71
+ }
72
+ }
73
+
74
+ export class BudgetCalculationError extends SowError {
75
+ constructor(message) {
76
+ super(message, 'INVALID_COMMERCIAL_TERMS');
77
+ }
78
+ }
79
+
80
+ export class SowInvariantError extends SowError {
81
+ constructor(message) {
82
+ super(`SOW invariant failure: ${message}`, 'INVARIANT_VIOLATION');
83
+ }
84
+ }
85
+
86
+ export const SOW_CURRENCY = 'USD';
87
+ export const SOW_CURRENCY_MINOR_EXPONENT = 2;
88
+
89
+ /**
90
+ * Parse commercial budget into integer minor units (cents) and major units (USD).
91
+ * Ensures money units are unambiguous and rejects invalid, negative, or sub-cent fractional budgets.
92
+ */
93
+ export function parseCommercialBudget({ budget = 45000, budgetMinor = null } = {}) {
94
+ if (budgetMinor !== null && budgetMinor !== undefined) {
95
+ if (!Number.isInteger(budgetMinor) || budgetMinor < 0) {
96
+ throw new BudgetCalculationError(`budgetMinor must be a non-negative integer (cents), got: ${budgetMinor}`);
97
+ }
98
+ const feeUsd = Number((budgetMinor / 100).toFixed(2));
99
+ return {
100
+ feeMinor: budgetMinor,
101
+ feeUsd,
102
+ currency: SOW_CURRENCY,
103
+ minorUnitExponent: SOW_CURRENCY_MINOR_EXPONENT,
104
+ };
105
+ }
106
+
107
+ const num = Number(budget);
108
+ if (Number.isNaN(num) || !Number.isFinite(num) || num < 0) {
109
+ throw new BudgetCalculationError(`Commercial budget must be a non-negative number, got: ${budget}`);
110
+ }
111
+
112
+ const minorUnits = Math.round(num * 100);
113
+ if (Math.abs(num * 100 - minorUnits) > 1e-6) {
114
+ throw new BudgetCalculationError(`Commercial budget cannot contain fractional minor units (sub-cents): ${budget}`);
115
+ }
116
+
117
+ const feeUsd = Number((minorUnits / 100).toFixed(2));
118
+ return {
119
+ feeMinor: minorUnits,
120
+ feeUsd,
121
+ currency: SOW_CURRENCY,
122
+ minorUnitExponent: SOW_CURRENCY_MINOR_EXPONENT,
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Exact integer milestone fee allocation distributing remainder deterministically.
128
+ * Allocates integer currency units (such as minor units / cents) across work packages without remainder leaks.
129
+ */
130
+ export function allocateMilestoneFees(totalUnits, count) {
131
+ if (!Number.isInteger(totalUnits) || totalUnits < 0) {
132
+ throw new BudgetCalculationError(`Commercial budget units must be a non-negative integer, got: ${totalUnits}`);
133
+ }
134
+ if (!Number.isInteger(count) || count <= 0) {
135
+ throw new BudgetCalculationError(`Cannot allocate milestone fees across ${count} work packages`);
136
+ }
137
+ const base = Math.floor(totalUnits / count);
138
+ const remainder = totalUnits - (base * count);
139
+ const fees = [];
140
+ for (let i = 0; i < count; i++) {
141
+ fees.push(base + (i < remainder ? 1 : 0));
142
+ }
143
+ const sum = fees.reduce((acc, f) => acc + f, 0);
144
+ if (sum !== totalUnits) {
145
+ throw new BudgetCalculationError(`Milestone fee sum (${sum}) does not equal total units (${totalUnits})`);
146
+ }
147
+ return fees;
148
+ }
149
+
150
+ export { getCanonicalRunTimestamp, sortRunCandidatesChronologically };
151
+
152
+ /**
153
+ * Verify cross-object runtime invariants before SOW export
154
+ */
155
+ export function validateSowInvariants(sow) {
156
+ if (!sow || typeof sow !== 'object') {
157
+ throw new SowInvariantError('SOW root must be an object');
158
+ }
159
+ if (!['CONTRACTUAL', 'DRAFT', 'NON_CONTRACTUAL_SAMPLE'].includes(sow.generation_mode)) {
160
+ throw new SowInvariantError(`Invalid generation_mode: ${sow.generation_mode}`);
161
+ }
162
+ if (sow.generation_mode === 'CONTRACTUAL' && sow.synthetic_evidence) {
163
+ throw new SowInvariantError('Contractual SOW cannot contain synthetic evidence');
164
+ }
165
+ if (sow.currency !== 'USD') {
166
+ throw new SowInvariantError(`Currency must be 'USD', got: ${sow.currency}`);
167
+ }
168
+ if (sow.currency_minor_unit_exponent !== 2) {
169
+ throw new SowInvariantError(`Minor unit exponent must be 2, got: ${sow.currency_minor_unit_exponent}`);
170
+ }
171
+ if (typeof sow.commercial_total_fee_minor !== 'number' || !Number.isInteger(sow.commercial_total_fee_minor)) {
172
+ throw new SowInvariantError(`commercial_total_fee_minor must be an integer, got: ${sow.commercial_total_fee_minor}`);
173
+ }
174
+ if (typeof sow.commercial_total_fee_usd !== 'number' || sow.commercial_total_fee_usd < 0) {
175
+ throw new SowInvariantError(`commercial_total_fee_usd must be a non-negative number, got: ${sow.commercial_total_fee_usd}`);
176
+ }
177
+ if (sow.commercial_total_fee_minor !== Math.round(sow.commercial_total_fee_usd * 100)) {
178
+ throw new SowInvariantError(`commercial_total_fee_minor (${sow.commercial_total_fee_minor}) does not match commercial_total_fee_usd (${sow.commercial_total_fee_usd})`);
179
+ }
180
+ if (!Array.isArray(sow.work_packages) || sow.work_packages.length === 0) {
181
+ throw new SowInvariantError('SOW must contain at least one work package');
182
+ }
183
+ if (!Array.isArray(sow.deliverables) || sow.deliverables.length === 0) {
184
+ throw new SowInvariantError('SOW must contain at least one deliverable');
185
+ }
186
+ if (sow.work_packages.length !== sow.deliverables.length) {
187
+ throw new SowInvariantError(`Work packages count (${sow.work_packages.length}) must match deliverables count (${sow.deliverables.length})`);
188
+ }
189
+ if (!Array.isArray(sow.traceability_matrix) || sow.traceability_matrix.length === 0) {
190
+ throw new SowInvariantError('SOW must contain at least one traceability matrix requirement');
191
+ }
192
+ const wpIds = new Set(sow.work_packages.map((wp) => wp.work_package_id));
193
+ const delivIds = new Set(sow.deliverables.map((d) => d.deliverable_id));
194
+ const seenReqIds = new Set();
195
+ const seenFindingKeys = new Set();
196
+ for (const row of sow.traceability_matrix) {
197
+ if (!wpIds.has(row.work_package_id)) {
198
+ throw new SowInvariantError(`Traceability matrix references unknown work package: ${row.work_package_id}`);
199
+ }
200
+ if (!delivIds.has(row.deliverable_id)) {
201
+ throw new SowInvariantError(`Traceability matrix references unknown deliverable: ${row.deliverable_id}`);
202
+ }
203
+ if (!Array.isArray(row.evidence_ids) || row.evidence_ids.length === 0) {
204
+ throw new SowInvariantError(`Traceability matrix row ${row.sow_requirement_id} lacks supporting evidence_ids`);
205
+ }
206
+ if (seenReqIds.has(row.sow_requirement_id)) {
207
+ throw new SowInvariantError(`Duplicate sow_requirement_id in traceability matrix: ${row.sow_requirement_id}`);
208
+ }
209
+ seenReqIds.add(row.sow_requirement_id);
210
+ const findingKey = `${row.finding_id}:${row.target_subject || ''}`;
211
+ if (seenFindingKeys.has(findingKey)) {
212
+ throw new SowInvariantError(`Duplicate finding_id and subject in traceability matrix: ${findingKey}`);
213
+ }
214
+ seenFindingKeys.add(findingKey);
215
+ }
216
+
217
+ const milestones = sow.delivery_schedule?.milestones || [];
218
+ const milestoneMinorSum = milestones.reduce((sum, m) => sum + m.fee_minor, 0);
219
+ if (milestoneMinorSum !== sow.commercial_total_fee_minor) {
220
+ throw new SowInvariantError(`Milestone minor fees sum (${milestoneMinorSum}) does not match total minor fee (${sow.commercial_total_fee_minor})`);
221
+ }
222
+ const milestoneUsdSum = Number(milestones.reduce((sum, m) => sum + m.fee_usd, 0).toFixed(2));
223
+ if (milestoneUsdSum !== sow.commercial_total_fee_usd) {
224
+ throw new SowInvariantError(`Milestone USD fees sum (${milestoneUsdSum}) does not match total fee USD (${sow.commercial_total_fee_usd})`);
225
+ }
226
+ for (const m of milestones) {
227
+ if (m.fee_minor !== Math.round(m.fee_usd * 100)) {
228
+ throw new SowInvariantError(`Milestone ${m.milestone_id} fee_minor (${m.fee_minor}) does not match fee_usd (${m.fee_usd})`);
229
+ }
230
+ }
231
+
232
+ const gate = sow.admissibility_gate;
233
+ if (gate.total_findings_evaluated !== gate.admitted_count + gate.refused_count) {
234
+ throw new SowInvariantError(`Admissibility gate numbers do not balance: ${gate.total_findings_evaluated} != ${gate.admitted_count} + ${gate.refused_count}`);
235
+ }
236
+ return true;
237
+ }
238
+
239
+ /**
240
+ * Standard synthetic baseline findings used strictly for sample mode (--sample / --demo)
241
+ */
242
+ export function getSampleBaselineFindings() {
243
+ return [
244
+ {
245
+ finding_id: 'F-TECH-CWV-001',
246
+ detector_id: 'TECH-001',
247
+ discipline: ['technical'],
248
+ classification: { severity: 'critical', confidence: 'deterministic' },
249
+ subject: { identifier: 'https://example.test/', url: 'https://example.test/' },
250
+ observation: { summary: 'Render-blocking JavaScript bundle degrades Mobile LCP to 4.2s', evidence: ['EVD-CWV-001'] },
251
+ remediation: { preferred: 'Implement asynchronous resource loading and preconnect headers' },
252
+ verification: { detector_to_rerun: 'TECH-001', method: 'Static CWV inspection and HTTP header probe' },
253
+ ice_score: 14.5,
254
+ },
255
+ {
256
+ finding_id: 'F-CRO-AUTO-007',
257
+ detector_id: 'CRO-007',
258
+ discipline: ['cro'],
259
+ classification: { severity: 'high', confidence: 'deterministic' },
260
+ subject: { identifier: 'https://example.test/checkout', url: 'https://example.test/checkout' },
261
+ observation: { summary: 'Checkout form inputs lack HTML5 autocomplete attributes, increasing manual mobile entry by 68%', evidence: ['EVD-CRO-007'] },
262
+ remediation: { preferred: 'Inject standard autocomplete attributes (autocomplete="email", autocomplete="name", autocomplete="tel")' },
263
+ verification: { detector_to_rerun: 'CRO-007', method: 'DOM inspection and AST patch verification' },
264
+ ice_score: 18.0,
265
+ },
266
+ {
267
+ finding_id: 'F-CRO-DIST-013',
268
+ detector_id: 'CRO-013',
269
+ discipline: ['cro'],
270
+ classification: { severity: 'high', confidence: 'deterministic' },
271
+ subject: { identifier: 'https://example.test/checkout', url: 'https://example.test/checkout' },
272
+ observation: { summary: 'Dedicated checkout funnel contains 14 external header navigation links creating distraction leaks', evidence: ['EVD-CRO-013'] },
273
+ remediation: { preferred: 'Deploy enclosed distraction-free checkout layout stripping non-essential navigation' },
274
+ verification: { detector_to_rerun: 'CRO-013', method: 'Navigation link count verification' },
275
+ ice_score: 12.0,
276
+ },
277
+ {
278
+ finding_id: 'F-ANS-PRIC-001',
279
+ detector_id: 'ANS-001',
280
+ discipline: ['aeo'],
281
+ classification: { severity: 'high', confidence: 'deterministic' },
282
+ subject: { identifier: 'https://example.test/saas/pricing.html', url: 'https://example.test/saas/pricing.html' },
283
+ observation: { summary: 'Commercial pricing questions lack direct-extract definition passages under 75 words', evidence: ['EVD-ANS-001'] },
284
+ remediation: { preferred: 'Restructure FAQ headings with immediate concise copular answer passages' },
285
+ verification: { detector_to_rerun: 'ANS-001', method: 'Passage length and question-answer extraction test' },
286
+ ice_score: 11.0,
287
+ },
288
+ {
289
+ finding_id: 'F-SCHEMA-FAQ-001',
290
+ detector_id: 'SCHEMA-001',
291
+ discipline: ['schema'],
292
+ classification: { severity: 'medium', confidence: 'deterministic' },
293
+ subject: { identifier: 'https://example.test/pricing', url: 'https://example.test/pricing' },
294
+ observation: { summary: 'Commercial FAQ content lacks FAQPage JSON-LD schema markup', evidence: ['EVD-SCH-001'] },
295
+ remediation: { preferred: 'Deploy validated Schema.org FAQPage structured data' },
296
+ verification: { detector_to_rerun: 'SCHEMA-001', method: 'JSON-LD schema validation gate' },
297
+ ice_score: 9.5,
298
+ },
299
+ {
300
+ finding_id: 'F-EXP-GEO-999',
301
+ detector_id: 'EXP-GEO-999',
302
+ discipline: ['geo'],
303
+ classification: { severity: 'low', confidence: 'experimental', finding_type: 'experimental' },
304
+ subject: { identifier: 'https://unrelated-blog.test/post-1', url: 'https://unrelated-blog.test/post-1' },
305
+ observation: { summary: 'Speculative model hallucination on third-party forum', evidence: [] },
306
+ remediation: { preferred: 'Consult external legal counsel regarding public forum sentiment' },
307
+ ice_score: 2.0,
308
+ },
309
+ ];
310
+ }
8
311
 
9
312
  /**
10
313
  * Generate an Enterprise-Grade Statement of Work (SOW) from SEO, AEO, GEO, SERP, and CRO findings.
11
- * Forces strict traceability: Evidence -> Obligation -> Acceptance Test.
314
+ * Forces strict downward traceability: Evidence -> Obligation -> Acceptance Test.
12
315
  */
13
316
  export async function generateSow(root, {
14
317
  target,
15
318
  baseUrl,
16
319
  refDate,
17
320
  runId,
321
+ live = false,
322
+ sample = false,
323
+ demo = false,
324
+ draft = false,
325
+ findings: inputFindings = null,
18
326
  client = 'Acme Corporation',
19
327
  clientContact = 'client-procurement@acme.test',
20
328
  supplier = 'Nebula Components & Citable Practice',
21
329
  supplierContact = 'advisory@nebulacomponents.test',
22
330
  budget = 45000,
331
+ budgetMinor = null,
23
332
  termDays = 90,
24
333
  inScopeProperties = [],
334
+ scopeMode = 'HOST_AND_SUBDOMAINS',
25
335
  minIceScore = 8.0,
336
+ allowedDisciplines = ['seo', 'aeo', 'geo', 'technical', 'schema', 'entity', 'cro'],
337
+ allowExperimental = false,
338
+ excludedDetectors = [],
339
+ roleMapping = null,
26
340
  sowId = null,
27
341
  } = {}) {
28
342
  const generatedAt = nowIso();
29
343
  const sowIdentifier = sowId || `SOW-${Date.now().toString(36).toUpperCase()}`;
344
+ const isSample = Boolean(sample || demo);
345
+ const isDraft = Boolean(draft);
346
+ const generationMode = isSample ? 'NON_CONTRACTUAL_SAMPLE' : (isDraft ? 'DRAFT' : 'CONTRACTUAL');
30
347
 
31
348
  let findings = [];
32
- let sitePages = [];
33
-
34
- // Attempt building live or target context if target provided
35
- if (target) {
36
- try {
37
- const ctx = await buildContext(root, { target, baseUrl, refDate });
38
- if (ctx?.site) {
39
- sitePages = indexTargets(ctx);
40
- const detectors = selectDetectors({ scopes: ['technical', 'seo', 'aeo', 'geo', 'schema', 'entity'] });
41
- const res = runDetectors(detectors, ctx);
42
- findings = res.findings;
43
- }
44
- } catch (e) {
45
- // Non-blocking fallback
349
+ let sourceProvenance = {
350
+ source_type: null,
351
+ source_identifier: null,
352
+ findings_count: 0,
353
+ integrity_hash: null,
354
+ };
355
+
356
+ let resolved;
357
+ try {
358
+ resolved = await resolveEvidenceSource(root, {
359
+ findings: inputFindings,
360
+ runId,
361
+ target,
362
+ live,
363
+ baseUrl,
364
+ refDate,
365
+ sample,
366
+ demo,
367
+ draft,
368
+ scopes: ['technical', 'seo', 'aeo', 'geo', 'schema', 'entity', 'cro'],
369
+ });
370
+ } catch (err) {
371
+ if (err.code === 'RUN_NOT_FOUND') {
372
+ throw new RunNotFoundError(runId);
373
+ }
374
+ if (err.code === 'FINDINGS_MISSING') {
375
+ throw new FindingsMissingError(runId, err.details?.filePath);
376
+ }
377
+ if (err.code === 'FINDINGS_INVALID') {
378
+ throw new FindingsInvalidError(runId, err.message);
379
+ }
380
+ if (err.code === 'LIVE_INSPECTION_FAILED') {
381
+ throw new LiveInspectionFailedError(target, err.message);
46
382
  }
383
+ if (err.code === 'NO_FINDINGS') {
384
+ const srcDesc = target ? `live target "${target}"` : (runId ? `run "${runId}"` : 'local workspace');
385
+ throw new NoFindingsError(srcDesc);
386
+ }
387
+ throw err;
47
388
  }
48
389
 
49
- // If runId provided or prior runs exist in .citable/runs, load recorded evidence
50
- const runsDir = path.join(root, '.citable', 'runs');
51
- let loadedRunId = runId;
52
- if (!loadedRunId && fs.existsSync(runsDir)) {
53
- const runs = fs.readdirSync(runsDir).filter((d) => !d.startsWith('.'));
54
- if (runs.length > 0) loadedRunId = runs[runs.length - 1];
390
+ findings = resolved.findings;
391
+ sourceProvenance = {
392
+ source_type: resolved.source_type,
393
+ source_identifier: resolved.source_identifier,
394
+ findings_count: resolved.findings_count,
395
+ integrity_hash: resolved.integrity_hash,
396
+ };
397
+
398
+ // If no findings were discovered:
399
+ if (!Array.isArray(findings) || findings.length === 0) {
400
+ if (isSample) {
401
+ findings = getSampleBaselineFindings();
402
+ sourceProvenance.source_type = 'SAMPLE_BASELINE';
403
+ sourceProvenance.source_identifier = 'citable://samples/enterprise-baseline';
404
+ sourceProvenance.findings_count = findings.length;
405
+ } else {
406
+ const srcDesc = target ? `live target "${target}"` : (runId ? `run "${runId}"` : 'local workspace');
407
+ throw new NoFindingsError(srcDesc);
408
+ }
55
409
  }
56
410
 
57
- if (loadedRunId) {
58
- const runPath = path.join(runsDir, loadedRunId);
59
- const findPath = path.join(runPath, 'findings.json');
60
- if (fs.existsSync(findPath) && findings.length === 0) {
61
- try { findings = readJson(findPath); } catch {}
411
+ if (generationMode === 'CONTRACTUAL') {
412
+ if (!client || typeof client !== 'string' || client.trim() === '') {
413
+ throw new SowError('Contractual SOW requires explicit client name', 'INVALID_COMMERCIAL_TERMS');
414
+ }
415
+ if (budget === null && budgetMinor === null) {
416
+ throw new SowError('Contractual SOW requires explicit budget or budgetMinor', 'INVALID_COMMERCIAL_TERMS');
417
+ }
418
+ if (!termDays || termDays <= 0 || !Number.isFinite(termDays)) {
419
+ throw new SowError('Contractual SOW requires positive finite termDays', 'INVALID_COMMERCIAL_TERMS');
62
420
  }
63
421
  }
64
422
 
65
- // If no findings exist from live run, supply standard baseline audit findings for SOW generation
66
- if (findings.length === 0) {
67
- findings = [
68
- {
69
- detector_id: 'TECH-001',
70
- discipline: ['technical'],
71
- classification: { severity: 'critical', confidence: 'deterministic' },
72
- subject: { identifier: 'https://example.test/', url: 'https://example.test/' },
73
- observation: { summary: 'Render-blocking JavaScript bundle degrades Mobile LCP to 4.2s', evidence: ['EVD-CWV-001'] },
74
- remediation: { preferred: 'Implement asynchronous resource loading and preconnect headers' },
75
- verification: { detector_to_rerun: 'TECH-001', method: 'Static CWV inspection and HTTP header probe' },
76
- ice_score: 14.5,
77
- },
78
- {
79
- detector_id: 'CRO-007',
80
- discipline: ['cro'],
81
- classification: { severity: 'high', confidence: 'deterministic' },
82
- subject: { identifier: 'https://example.test/checkout', url: 'https://example.test/checkout' },
83
- observation: { summary: 'Checkout form inputs lack HTML5 autocomplete attributes, increasing manual mobile entry by 68%', evidence: ['EVD-CRO-007'] },
84
- remediation: { preferred: 'Inject standard autocomplete attributes (autocomplete="email", autocomplete="name", autocomplete="tel")' },
85
- verification: { detector_to_rerun: 'CRO-007', method: 'DOM inspection and AST patch verification' },
86
- ice_score: 18.0,
87
- },
88
- {
89
- detector_id: 'CRO-013',
90
- discipline: ['cro'],
91
- classification: { severity: 'high', confidence: 'deterministic' },
92
- subject: { identifier: 'https://example.test/checkout', url: 'https://example.test/checkout' },
93
- observation: { summary: 'Dedicated checkout funnel contains 14 external header navigation links creating distraction leaks', evidence: ['EVD-CRO-013'] },
94
- remediation: { preferred: 'Deploy enclosed distraction-free checkout layout stripping non-essential navigation' },
95
- verification: { detector_to_rerun: 'CRO-013', method: 'Navigation link count verification' },
96
- ice_score: 12.0,
97
- },
98
- {
99
- detector_id: 'ANS-001',
100
- discipline: ['aeo'],
101
- classification: { severity: 'high', confidence: 'deterministic' },
102
- subject: { identifier: 'https://example.test/saas/pricing.html', url: 'https://example.test/saas/pricing.html' },
103
- observation: { summary: 'Commercial pricing questions lack direct-extract definition passages under 75 words', evidence: ['EVD-ANS-001'] },
104
- remediation: { preferred: 'Restructure FAQ headings with immediate concise copular answer passages' },
105
- verification: { detector_to_rerun: 'ANS-001', method: 'Passage length and question-answer extraction test' },
106
- ice_score: 11.0,
107
- },
108
- {
109
- detector_id: 'SCHEMA-001',
110
- discipline: ['schema'],
111
- classification: { severity: 'medium', confidence: 'deterministic' },
112
- subject: { identifier: 'https://example.test/pricing', url: 'https://example.test/pricing' },
113
- observation: { summary: 'Commercial FAQ content lacks FAQPage JSON-LD schema markup', evidence: ['EVD-SCH-001'] },
114
- remediation: { preferred: 'Deploy validated Schema.org FAQPage structured data' },
115
- verification: { detector_to_rerun: 'SCHEMA-001', method: 'JSON-LD schema validation gate' },
116
- ice_score: 9.5,
117
- },
118
- // Inadmissible exploratory finding (for demonstrating the gate)
119
- {
120
- detector_id: 'EXP-GEO-999',
121
- discipline: ['geo'],
122
- classification: { severity: 'low', confidence: 'experimental', finding_type: 'experimental' },
123
- subject: { identifier: 'https://unrelated-blog.test/post-1', url: 'https://unrelated-blog.test/post-1' },
124
- observation: { summary: 'Speculative model hallucination on third-party forum', evidence: [] },
125
- remediation: { preferred: 'Consult external legal counsel regarding public forum sentiment' },
126
- ice_score: 2.0,
127
- },
128
- ];
129
- }
130
-
131
- const effectiveScopeProps = inScopeProperties.length > 0
132
- ? inScopeProperties
133
- : [baseUrl || 'https://example.test'];
423
+ let effectiveScopeProps = inScopeProperties.length > 0 ? [...inScopeProperties] : [];
424
+ if (effectiveScopeProps.length === 0) {
425
+ if (baseUrl) {
426
+ effectiveScopeProps.push(baseUrl);
427
+ } else if (target && typeof target === 'string' && target.startsWith('http')) {
428
+ effectiveScopeProps.push(target);
429
+ } else if (isSample) {
430
+ effectiveScopeProps.push('https://example.test');
431
+ } else {
432
+ // Derive from finding subjects
433
+ for (const f of findings) {
434
+ const s = f.subject?.identifier || f.subject?.url || (typeof f.subject === 'string' ? f.subject : null);
435
+ if (s) {
436
+ try {
437
+ const u = new URL(s.includes('://') ? s : `https://${s}`);
438
+ if (!effectiveScopeProps.includes(u.origin)) {
439
+ effectiveScopeProps.push(u.origin);
440
+ }
441
+ } catch {}
442
+ }
443
+ }
444
+ }
445
+ }
446
+
447
+ if (generationMode === 'CONTRACTUAL' && effectiveScopeProps.length === 0) {
448
+ throw new SowError('Contractual SOW requires explicit inScopeProperties or derivable target properties', 'SCOPE_REQUIRED');
449
+ }
134
450
 
135
451
  // -------------------------------------------------------------
136
- // THE ADMISSIBILITY GATE (Filter findings to contractual scope)
452
+ // THE ADMISSIBILITY GATE (Filter findings into contractual scope)
137
453
  // -------------------------------------------------------------
138
454
  const gateResult = evaluateScopeAdmissibility(findings, {
139
455
  inScopeProperties: effectiveScopeProps,
456
+ scopeMode,
140
457
  minIceScore,
458
+ allowedDisciplines,
459
+ allowExperimental,
460
+ excludedDetectors,
461
+ roleMapping,
462
+ rejectTemplateDefaultOwner: generationMode === 'CONTRACTUAL',
141
463
  });
142
464
 
143
465
  const admitted = gateResult.admitted_requirements;
466
+ if (admitted.length === 0) {
467
+ throw new NoAdmissibleRequirementsError(findings.length, gateResult.refusal_log.length);
468
+ }
144
469
 
145
470
  // Group admitted requirements into Work Packages
146
471
  const wpMap = new Map();
@@ -162,20 +487,25 @@ export async function generateSow(root, {
162
487
  // Mapping Finding -> Recommendation -> SOW Requirement -> Deliverable -> Acceptance Test -> Owner -> Evidence
163
488
  const traceabilityMatrix = admitted.map((item) => ({
164
489
  finding_id: item.finding_id,
490
+ detector_id: item.detector_id,
165
491
  recommendation: item.recommendation,
166
492
  sow_requirement_id: item.sow_requirement_id,
167
493
  work_package_id: item.work_package_id,
168
494
  deliverable_id: item.deliverable_id,
169
495
  acceptance_test_id: item.acceptance_test_id,
170
496
  owner: item.owner,
497
+ owner_source: item.owner_source,
498
+ owner_mapping_version: item.owner_mapping_version,
171
499
  evidence_id: item.evidence_id,
500
+ evidence_ids: item.evidence_ids,
172
501
  severity: item.severity,
173
502
  target_subject: item.subject,
174
503
  acceptance_criteria: item.acceptance_test,
504
+ acceptance_basis: item.acceptance_basis || 'automated_detector_rerun',
175
505
  }));
176
506
 
177
507
  // Deliverables definitions
178
- const deliverables = workPackages.map((wp, idx) => ({
508
+ const deliverables = workPackages.map((wp) => ({
179
509
  deliverable_id: wp.deliverable_id,
180
510
  work_package_id: wp.work_package_id,
181
511
  title: `${wp.name} Implementation & Verification Package`,
@@ -195,27 +525,41 @@ export async function generateSow(root, {
195
525
  evidence_required: `Deterministic before-and-after observation JSON matching ${item.evidence_id}`,
196
526
  }));
197
527
 
198
- // Commercial structure
199
- const feePerWp = Math.round(budget / (workPackages.length || 1));
528
+ // Commercial structure with deterministic integer minor-unit arithmetic
529
+ const { feeMinor, feeUsd, currency, minorUnitExponent } = parseCommercialBudget({ budget, budgetMinor });
530
+ const milestoneFeesMinor = allocateMilestoneFees(feeMinor, workPackages.length);
531
+ const milestoneFeesUsd = milestoneFeesMinor.map((m) => Number((m / 100).toFixed(2)));
200
532
  const commercialMilestones = workPackages.map((wp, idx) => ({
201
533
  milestone_id: `MILESTONE-0${idx + 1}`,
202
534
  work_package_id: wp.work_package_id,
203
535
  name: wp.name,
204
- fee_usd: feePerWp,
536
+ fee_minor: milestoneFeesMinor[idx],
537
+ fee_usd: milestoneFeesUsd[idx],
205
538
  billing_trigger: `Successful customer acceptance sign-off of Deliverable ${wp.deliverable_id}`,
206
539
  target_delivery_week: (idx + 1) * 3,
207
540
  }));
208
541
 
542
+ const sowTitle = isSample
543
+ ? 'Statement of Work: Enterprise Search & Conversion Intelligence Engineering [DEMONSTRATION SAMPLE]'
544
+ : (isDraft
545
+ ? 'Statement of Work: Enterprise Search & Conversion Intelligence Engineering [DRAFT]'
546
+ : 'Statement of Work: Enterprise Search & Conversion Intelligence Engineering');
547
+
209
548
  const sow = {
210
549
  $schema: 'citable://schemas/sow.schema.json',
211
550
  sow_id: sowIdentifier,
212
551
  version: '1.0.0',
213
- title: `Statement of Work: Enterprise Search & Conversion Intelligence Engineering`,
552
+ generation_mode: generationMode,
553
+ title: sowTitle,
554
+ synthetic_evidence: isSample,
214
555
  client: { name: client, contact: clientContact },
215
556
  supplier: { name: supplier, contact: supplierContact },
216
557
  effective_date: generatedAt.split('T')[0],
217
558
  term_days: termDays,
218
- commercial_total_fee_usd: budget,
559
+ currency,
560
+ currency_minor_unit_exponent: minorUnitExponent,
561
+ commercial_total_fee_minor: feeMinor,
562
+ commercial_total_fee_usd: feeUsd,
219
563
 
220
564
  // Pillar 1: Executive Scope Statement
221
565
  executive_scope: {
@@ -373,7 +717,9 @@ export async function generateSow(root, {
373
717
 
374
718
  // Pillar 20: Commercial Structure
375
719
  commercial_terms: {
376
- total_fixed_fee_usd: budget,
720
+ total_fixed_fee_minor: feeMinor,
721
+ total_fixed_fee_usd: feeUsd,
722
+ currency,
377
723
  payment_terms: 'Net 30 upon verified deliverable acceptance',
378
724
  milestones: commercialMilestones,
379
725
  },
@@ -413,8 +759,22 @@ export async function generateSow(root, {
413
759
 
414
760
  // Pillar 25: The Final Traceability Matrix
415
761
  traceability_matrix: traceabilityMatrix,
762
+
763
+ // Machine-readable Provenance Envelope
764
+ generation_provenance: {
765
+ generated_at: generatedAt,
766
+ generator_version: '1.18.2',
767
+ generation_mode: generationMode,
768
+ source_type: sourceProvenance.source_type || 'UNKNOWN',
769
+ source_identifier: sourceProvenance.source_identifier || null,
770
+ source_findings_count: sourceProvenance.findings_count,
771
+ findings_integrity_hash: sourceProvenance.integrity_hash || null,
772
+ synthetic_evidence: isSample,
773
+ },
416
774
  };
417
775
 
776
+ validateSowInvariants(sow);
777
+
418
778
  return sow;
419
779
  }
420
780
 
@@ -422,16 +782,23 @@ export async function generateSow(root, {
422
782
  * Render Statement of Work as GitHub-flavored Markdown
423
783
  */
424
784
  export function renderSowMarkdown(sow) {
785
+ const modeBanner = sow.generation_mode === 'NON_CONTRACTUAL_SAMPLE'
786
+ ? '> ⚠️ **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.'
787
+ : (sow.generation_mode === 'DRAFT'
788
+ ? '> 📝 **DRAFT STATEMENT OF WORK**: This preliminary draft contains unfinalized engagement terms and requirements. Final executive approval required prior to signature.'
789
+ : '> **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.');
790
+
425
791
  const lines = [
426
792
  `# ${sow.title}`,
427
793
  `====================================================================`,
428
- `- **SOW ID**: \`${sow.sow_id}\` | **Version**: \`${sow.version}\``,
794
+ `- **SOW ID**: \`${sow.sow_id}\` | **Version**: \`${sow.version}\` | **Mode**: \`${sow.generation_mode}\``,
429
795
  `- **Client**: **${sow.client.name}** (${sow.client.contact})`,
430
796
  `- **Supplier**: **${sow.supplier.name}** (${sow.supplier.contact})`,
431
797
  `- **Effective Date**: \`${sow.effective_date}\` | **Term**: \`${sow.term_days} calendar days\``,
432
- `- **Commercial Total**: **$${sow.commercial_total_fee_usd.toLocaleString()} USD**`,
798
+ `- **Commercial Total**: **$${sow.commercial_total_fee_usd.toLocaleString('en-US', { minimumFractionDigits: (sow.commercial_total_fee_minor % 100 === 0) ? 0 : 2, maximumFractionDigits: 2 })} USD**`,
799
+ `- **Audit Source**: \`${sow.generation_provenance.source_type}\` (${sow.generation_provenance.source_identifier || 'N/A'}) | **Generator**: \`v${sow.generation_provenance.generator_version}\``,
433
800
  ``,
434
- `> **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.`,
801
+ modeBanner,
435
802
  ``,
436
803
  `---`,
437
804
  `## 1. Executive Scope Statement`,
@@ -505,7 +872,7 @@ export function renderSowMarkdown(sow) {
505
872
  `## 12. Delivery Sequencing & Milestone Schedule`,
506
873
  `| Milestone | Work Package | Target Week | Fee (USD) | Acceptance Trigger |`,
507
874
  `| :--- | :--- | :--- | :--- | :--- |`,
508
- ...sow.delivery_schedule.milestones.map((m) => `| **${m.milestone_id}** | \`${m.work_package_id}\` | Week ${m.target_delivery_week} | **$${m.fee_usd.toLocaleString()}** | ${m.billing_trigger} |`),
875
+ ...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} |`),
509
876
  ``,
510
877
  `## 13. Scope Change-Control Process`,
511
878
  `- **Procedure**: ${sow.change_control_process.procedure}`,
@@ -537,7 +904,7 @@ export function renderSowMarkdown(sow) {
537
904
  `- **Decision Records**: ${sow.governance_model.decision_logging}`,
538
905
  ``,
539
906
  `## 20. Commercial Terms & Payment Schedule`,
540
- `- **Total Contract Value**: **$${sow.commercial_terms.total_fixed_fee_usd.toLocaleString()} USD**`,
907
+ `- **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**`,
541
908
  `- **Terms**: ${sow.commercial_terms.payment_terms}`,
542
909
  ``,
543
910
  `## 21. Explicit Out-of-Scope Declarations`,
@@ -563,7 +930,7 @@ export function renderSowMarkdown(sow) {
563
930
  ``,
564
931
  `| Finding ID | Recommendation | SOW Req ID | Deliverable ID | Acceptance Test ID | Responsible Owner | Source Evidence |`,
565
932
  `| :--- | :--- | :--- | :--- | :--- | :--- | :--- |`,
566
- ...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_id}\` |`),
933
+ ...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}\` |`),
567
934
  ``,
568
935
  `---`,
569
936
  `### Contract Execution & Authorization`,
@@ -583,18 +950,27 @@ export function renderSowMarkdown(sow) {
583
950
  * Render Statement of Work as Standalone Enterprise HTML
584
951
  */
585
952
  export function renderSowHtml(sow) {
953
+ const isSample = sow.generation_mode === 'NON_CONTRACTUAL_SAMPLE';
954
+ const isDraft = sow.generation_mode === 'DRAFT';
955
+ const badgeColor = isSample ? 'var(--warning)' : (isDraft ? 'var(--accent)' : 'var(--success)');
956
+ const modeNotice = isSample
957
+ ? '<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>'
958
+ : (isDraft
959
+ ? '<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>'
960
+ : '<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>');
961
+
586
962
  return `<!DOCTYPE html>
587
963
  <html lang="en">
588
964
  <head>
589
965
  <meta charset="UTF-8">
590
- <title>${sow.title} - ${sow.sow_id}</title>
966
+ <title>${escapeHtml(sow.title)} - ${escapeHtml(sow.sow_id)}</title>
591
967
  <style>
592
968
  :root { --bg: #0d1117; --card: #161b22; --border: #30363d; --text: #c9d1d9; --accent: #58a6ff; --danger: #f85149; --warning: #d29922; --success: #3fb950; }
593
969
  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; }
594
970
  .container { max-width: 1180px; margin: 0 auto; }
595
971
  header { border-bottom: 2px solid var(--border); padding-bottom: 24px; margin-bottom: 32px; }
596
972
  h1 { font-size: 28px; margin: 0 0 10px 0; color: #fff; }
597
- .sow-badge { background: #1f6feb; color: #fff; padding: 4px 10px; border-radius: 4px; font-size: 13px; font-weight: bold; }
973
+ .sow-badge { background: ${badgeColor}; color: #000; padding: 4px 10px; border-radius: 4px; font-size: 13px; font-weight: bold; }
598
974
  .meta-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin: 24px 0; }
599
975
  .card { background: var(--card); border: 1px solid var(--border); border-radius: 6px; padding: 18px; }
600
976
  .card-val { font-size: 26px; font-weight: bold; color: #fff; margin-top: 4px; }
@@ -615,20 +991,21 @@ export function renderSowHtml(sow) {
615
991
  <div class="container">
616
992
  <header>
617
993
  <div style="display:flex; justify-content:space-between; align-items:center;">
618
- <h1>${sow.title}</h1>
619
- <span class="sow-badge">${sow.sow_id}</span>
994
+ <h1>${escapeHtml(sow.title)}</h1>
995
+ <div>
996
+ <span class="sow-badge">${escapeHtml(sow.generation_mode)}</span>
997
+ <span class="sow-badge" style="background:#1f6feb; color:#fff; margin-left:8px;">${escapeHtml(sow.sow_id)}</span>
998
+ </div>
620
999
  </div>
621
1000
  <div style="color:#8b949e; font-size:14px; margin-top:8px;">
622
- 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>
1001
+ Client: <strong>${escapeHtml(sow.client.name)}</strong> &nbsp;|&nbsp; Supplier: <strong>${escapeHtml(sow.supplier.name)}</strong> &nbsp;|&nbsp; Term: <strong>${escapeHtml(String(sow.term_days))} Days</strong> &nbsp;|&nbsp; Effective: <strong>${escapeHtml(sow.effective_date)}</strong> &nbsp;|&nbsp; Source: <strong>${escapeHtml(sow.generation_provenance.source_type)}</strong>
623
1002
  </div>
624
1003
  </header>
625
1004
 
626
- <div class="disclosure">
627
- <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>.
628
- </div>
1005
+ ${modeNotice}
629
1006
 
630
1007
  <div class="meta-grid">
631
- <div class="card"><div>Total Contract Fee</div><div class="card-val">$${sow.commercial_total_fee_usd.toLocaleString()}</div></div>
1008
+ <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>
632
1009
  <div class="card"><div>Admitted SOW Requirements</div><div class="card-val">${sow.admissibility_gate.admitted_count}</div></div>
633
1010
  <div class="card"><div>Refused / Excluded Scope</div><div class="card-val" style="color:var(--danger);">${sow.admissibility_gate.refused_count}</div></div>
634
1011
  <div class="card"><div>Contracted Work Packages</div><div class="card-val">${sow.work_packages.length}</div></div>
@@ -636,10 +1013,10 @@ export function renderSowHtml(sow) {
636
1013
 
637
1014
  <h2 class="section-title">1. Executive Scope Statement</h2>
638
1015
  <div class="card">
639
- <p><strong>Objective:</strong> ${sow.executive_scope.business_objective}</p>
640
- <p><strong>In-Scope Properties:</strong> <code>${sow.executive_scope.in_scope_properties.join(', ')}</code></p>
641
- <p><strong>In-Scope Systems:</strong> ${sow.executive_scope.in_scope_systems.join('; ')}</p>
642
- <p><strong>Boundaries:</strong> ${sow.executive_scope.organizational_boundaries}</p>
1016
+ <p><strong>Objective:</strong> ${escapeHtml(sow.executive_scope.business_objective)}</p>
1017
+ <p><strong>In-Scope Properties:</strong> <code>${escapeHtml(sow.executive_scope.in_scope_properties.join(', '))}</code></p>
1018
+ <p><strong>In-Scope Systems:</strong> ${escapeHtml(sow.executive_scope.in_scope_systems.join('; '))}</p>
1019
+ <p><strong>Boundaries:</strong> ${escapeHtml(sow.executive_scope.organizational_boundaries)}</p>
643
1020
  </div>
644
1021
 
645
1022
  <h2 class="section-title">2. Scope Admissibility Gate & Excluded Findings Log</h2>
@@ -650,11 +1027,11 @@ export function renderSowHtml(sow) {
650
1027
  <tbody>
651
1028
  ${sow.admissibility_gate.refusal_log.map((r) => `
652
1029
  <tr>
653
- <td><span class="code-ref">${r.finding_id}</span></td>
654
- <td>${r.subject}</td>
655
- <td><code>${r.gate_failed}</code></td>
656
- <td><span class="badge badge-refuse">${r.refusal_code}</span></td>
657
- <td>${r.refusal_rationale}</td>
1030
+ <td><span class="code-ref">${escapeHtml(r.finding_id)}</span></td>
1031
+ <td>${escapeHtml(r.subject)}</td>
1032
+ <td><code>${escapeHtml(r.gate_failed)}</code></td>
1033
+ <td><span class="badge badge-refuse">${escapeHtml(r.refusal_code)}</span></td>
1034
+ <td>${escapeHtml(r.refusal_rationale)}</td>
658
1035
  </tr>
659
1036
  `).join('')}
660
1037
  </tbody>
@@ -668,11 +1045,11 @@ export function renderSowHtml(sow) {
668
1045
  <tbody>
669
1046
  ${sow.delivery_schedule.milestones.map((m) => `
670
1047
  <tr>
671
- <td><strong>${m.milestone_id}</strong></td>
672
- <td>${m.name}</td>
673
- <td><span class="code-ref">${m.work_package_id}</span></td>
674
- <td>${m.billing_trigger}</td>
675
- <td style="font-weight:bold; color:var(--success);">$${m.fee_usd.toLocaleString()} USD</td>
1048
+ <td><strong>${escapeHtml(m.milestone_id)}</strong></td>
1049
+ <td>${escapeHtml(m.name)}</td>
1050
+ <td><span class="code-ref">${escapeHtml(m.work_package_id)}</span></td>
1051
+ <td>${escapeHtml(m.billing_trigger)}</td>
1052
+ <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>
676
1053
  </tr>
677
1054
  `).join('')}
678
1055
  </tbody>
@@ -687,13 +1064,13 @@ export function renderSowHtml(sow) {
687
1064
  <tbody>
688
1065
  ${sow.traceability_matrix.map((t) => `
689
1066
  <tr>
690
- <td><span class="code-ref">${t.finding_id}</span></td>
691
- <td>${t.recommendation}</td>
692
- <td><strong>${t.sow_requirement_id}</strong></td>
693
- <td>${t.deliverable_id}</td>
694
- <td><code>${t.acceptance_test_id}</code></td>
695
- <td>${t.owner}</td>
696
- <td><span class="code-ref">${t.evidence_id}</span></td>
1067
+ <td><span class="code-ref">${escapeHtml(t.finding_id)}</span></td>
1068
+ <td>${escapeHtml(t.recommendation)}</td>
1069
+ <td><strong>${escapeHtml(t.sow_requirement_id)}</strong></td>
1070
+ <td>${escapeHtml(t.deliverable_id)}</td>
1071
+ <td><code>${escapeHtml(t.acceptance_test_id)}</code></td>
1072
+ <td>${escapeHtml(t.owner)}</td>
1073
+ <td><span class="code-ref">${escapeHtml(t.evidence_ids?.join(', ') || t.evidence_id)}</span></td>
697
1074
  </tr>
698
1075
  `).join('')}
699
1076
  </tbody>
@@ -702,7 +1079,7 @@ export function renderSowHtml(sow) {
702
1079
  <h2 class="section-title">Signatures & Contractual Authorization</h2>
703
1080
  <table class="sig-table">
704
1081
  <thead>
705
- <tr><th>For Customer: ${sow.client.name}</th><th>For Supplier: ${sow.supplier.name}</th></tr>
1082
+ <tr><th>For Customer: ${escapeHtml(sow.client.name)}</th><th>For Supplier: ${escapeHtml(sow.supplier.name)}</th></tr>
706
1083
  </thead>
707
1084
  <tbody>
708
1085
  <tr>
@@ -727,10 +1104,15 @@ export function renderSowHtml(sow) {
727
1104
  }
728
1105
 
729
1106
  /**
730
- * Export SOW deliverable to filesystem or string
1107
+ * Export SOW deliverable to filesystem or string with strict schema validation
731
1108
  */
732
1109
  export async function exportSow(root, options = {}) {
733
1110
  const sow = await generateSow(root, options);
1111
+ const validation = validateAgainst('sow.schema.json', sow);
1112
+ if (!validation.valid) {
1113
+ throw new SowError(`Generated SOW violates schemas/sow.schema.json: ${validation.errors.join('; ')}`, 'SCHEMA_VALIDATION_FAILED');
1114
+ }
1115
+
734
1116
  const format = options.format || 'markdown';
735
1117
  let content = '';
736
1118