@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.
- package/CHANGELOG.md +70 -0
- package/dist/universal/.agents/skills/citable/SKILL.md +22 -1
- package/dist/universal/.agents/skills/citable/VERSION +1 -1
- package/dist/universal/.agents/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.agents/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.agents/skills/citable/commands/README.md +13 -0
- package/dist/universal/.agents/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.agents/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.agents/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.agents/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.agents/skills/citable/manifest.json +15 -9
- package/dist/universal/.agents/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.agents/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.agents/skills/citable/schemas/sow.schema.json +344 -26
- package/dist/universal/.claude/agents/citable/citable-remediator.md +43 -0
- package/dist/universal/.claude/agents/citable/citable-sow-architect.md +43 -0
- package/dist/universal/.claude/agents/citable/manifest.json +6 -4
- package/dist/universal/.claude/skills/citable/SKILL.md +22 -1
- package/dist/universal/.claude/skills/citable/VERSION +1 -1
- package/dist/universal/.claude/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.claude/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.claude/skills/citable/commands/README.md +13 -0
- package/dist/universal/.claude/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.claude/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.claude/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.claude/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.claude/skills/citable/manifest.json +19 -11
- package/dist/universal/.claude/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.claude/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.claude/skills/citable/schemas/sow.schema.json +344 -26
- package/dist/universal/.cursor/skills/citable/SKILL.md +22 -1
- package/dist/universal/.cursor/skills/citable/VERSION +1 -1
- package/dist/universal/.cursor/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.cursor/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.cursor/skills/citable/commands/README.md +13 -0
- package/dist/universal/.cursor/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.cursor/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.cursor/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.cursor/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.cursor/skills/citable/manifest.json +15 -9
- package/dist/universal/.cursor/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.cursor/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.cursor/skills/citable/schemas/sow.schema.json +344 -26
- package/dist/universal/.gemini/skills/citable/SKILL.md +22 -1
- package/dist/universal/.gemini/skills/citable/VERSION +1 -1
- package/dist/universal/.gemini/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.gemini/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.gemini/skills/citable/commands/README.md +13 -0
- package/dist/universal/.gemini/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.gemini/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.gemini/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.gemini/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.gemini/skills/citable/manifest.json +15 -9
- package/dist/universal/.gemini/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.gemini/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.gemini/skills/citable/schemas/sow.schema.json +344 -26
- package/dist/universal/.github/skills/citable/SKILL.md +22 -1
- package/dist/universal/.github/skills/citable/VERSION +1 -1
- package/dist/universal/.github/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.github/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.github/skills/citable/commands/README.md +13 -0
- package/dist/universal/.github/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.github/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.github/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.github/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.github/skills/citable/manifest.json +15 -9
- package/dist/universal/.github/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.github/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.github/skills/citable/schemas/sow.schema.json +344 -26
- package/dist/universal/.kiro/skills/citable/SKILL.md +22 -1
- package/dist/universal/.kiro/skills/citable/VERSION +1 -1
- package/dist/universal/.kiro/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.kiro/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.kiro/skills/citable/commands/README.md +13 -0
- package/dist/universal/.kiro/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.kiro/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.kiro/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.kiro/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.kiro/skills/citable/manifest.json +15 -9
- package/dist/universal/.kiro/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.kiro/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.kiro/skills/citable/schemas/sow.schema.json +344 -26
- package/dist/universal/.opencode/skills/citable/SKILL.md +22 -1
- package/dist/universal/.opencode/skills/citable/VERSION +1 -1
- package/dist/universal/.opencode/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.opencode/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.opencode/skills/citable/commands/README.md +13 -0
- package/dist/universal/.opencode/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.opencode/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.opencode/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.opencode/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.opencode/skills/citable/manifest.json +15 -9
- package/dist/universal/.opencode/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.opencode/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.opencode/skills/citable/schemas/sow.schema.json +344 -26
- package/dist/universal/.pi/agent/skills/citable/SKILL.md +22 -1
- package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
- package/dist/universal/.pi/agent/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.pi/agent/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.pi/agent/skills/citable/commands/README.md +13 -0
- package/dist/universal/.pi/agent/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.pi/agent/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.pi/agent/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.pi/agent/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.pi/agent/skills/citable/manifest.json +15 -9
- package/dist/universal/.pi/agent/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/sow.schema.json +344 -26
- package/dist/universal/.qoder/skills/citable/SKILL.md +22 -1
- package/dist/universal/.qoder/skills/citable/VERSION +1 -1
- package/dist/universal/.qoder/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.qoder/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.qoder/skills/citable/commands/README.md +13 -0
- package/dist/universal/.qoder/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.qoder/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.qoder/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.qoder/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.qoder/skills/citable/manifest.json +15 -9
- package/dist/universal/.qoder/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.qoder/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.qoder/skills/citable/schemas/sow.schema.json +344 -26
- package/dist/universal/.rovodev/skills/citable/SKILL.md +22 -1
- package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
- package/dist/universal/.rovodev/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.rovodev/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.rovodev/skills/citable/commands/README.md +13 -0
- package/dist/universal/.rovodev/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.rovodev/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.rovodev/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.rovodev/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.rovodev/skills/citable/manifest.json +15 -9
- package/dist/universal/.rovodev/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.rovodev/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.rovodev/skills/citable/schemas/sow.schema.json +344 -26
- package/dist/universal/.trae/skills/citable/SKILL.md +22 -1
- package/dist/universal/.trae/skills/citable/VERSION +1 -1
- package/dist/universal/.trae/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.trae/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.trae/skills/citable/commands/README.md +13 -0
- package/dist/universal/.trae/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.trae/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.trae/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.trae/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.trae/skills/citable/manifest.json +15 -9
- package/dist/universal/.trae/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.trae/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.trae/skills/citable/schemas/sow.schema.json +344 -26
- package/dist/universal/.trae-cn/skills/citable/SKILL.md +22 -1
- package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
- package/dist/universal/.trae-cn/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.trae-cn/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.trae-cn/skills/citable/commands/README.md +13 -0
- package/dist/universal/.trae-cn/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.trae-cn/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.trae-cn/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.trae-cn/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.trae-cn/skills/citable/manifest.json +15 -9
- package/dist/universal/.trae-cn/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/sow.schema.json +344 -26
- package/dist/universal/manifest.json +186 -112
- package/package.json +2 -1
- package/schemas/cro-report.schema.json +112 -0
- package/schemas/search-report.schema.json +99 -0
- package/schemas/sow.schema.json +344 -26
- package/skill/SKILL.md +22 -1
- package/skill/agents/citable-remediator.md +43 -0
- package/skill/agents/citable-sow-architect.md +43 -0
- package/skill/commands/README.md +13 -0
- package/skill/commands/artifact-interchange.md +5 -0
- package/skill/commands/remediation.md +55 -0
- package/skill/commands/reporting.md +28 -0
- package/skill/commands/sow.md +95 -0
- package/src/analysis/behavioral.js +5 -4
- package/src/analysis/eeat.js +10 -9
- package/src/analysis/offpage.js +19 -36
- package/src/analysis/readiness.js +8 -2
- package/src/artifacts/verifyCustomerArtifact.js +283 -0
- package/src/cli/index.js +8 -1
- package/src/commands/sowCmd.js +17 -0
- package/src/reporting/executiveCroReport.js +393 -229
- package/src/reporting/executiveExport.js +6 -2
- package/src/reporting/executiveSearchReport.js +519 -271
- package/src/shared/domainUtils.js +140 -0
- package/src/shared/epistemicStatus.js +111 -0
- package/src/shared/evidenceSourceResolver.js +286 -0
- package/src/shared/htmlEscape.js +65 -0
- package/src/shared/runPackageVerifier.js +153 -0
- package/src/sow/admissibilityGate.js +234 -46
- package/src/sow/generateSow.js +524 -142
package/src/sow/generateSow.js
CHANGED
|
@@ -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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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 (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
<
|
|
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> | Supplier: <strong>${sow.supplier.name}</strong> | Term: <strong>${sow.term_days} Days</strong> | Effective: <strong>${sow.effective_date}</strong>
|
|
1001
|
+
Client: <strong>${escapeHtml(sow.client.name)}</strong> | Supplier: <strong>${escapeHtml(sow.supplier.name)}</strong> | Term: <strong>${escapeHtml(String(sow.term_days))} Days</strong> | Effective: <strong>${escapeHtml(sow.effective_date)}</strong> | Source: <strong>${escapeHtml(sow.generation_provenance.source_type)}</strong>
|
|
623
1002
|
</div>
|
|
624
1003
|
</header>
|
|
625
1004
|
|
|
626
|
-
|
|
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
|
|