@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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readJson, sha256File } from './io.js';
|
|
4
|
+
import { validateAgainst } from './schemaValidator.js';
|
|
5
|
+
|
|
6
|
+
export class RunVerificationError extends Error {
|
|
7
|
+
constructor(message, details = {}) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'RunVerificationError';
|
|
10
|
+
this.code = details.code || 'RUN_VERIFICATION_FAILED';
|
|
11
|
+
this.details = details;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Verifies the cryptographic and structural integrity of an audit run evidence package.
|
|
17
|
+
*
|
|
18
|
+
* Checks:
|
|
19
|
+
* 1. Existence of run directory and manifest.json
|
|
20
|
+
* 2. Manifest compliance with run.schema.json
|
|
21
|
+
* 3. Existence and validity of checksums.json
|
|
22
|
+
* 4. Exact sha256 hash match for all sealed artifacts in checksums.json
|
|
23
|
+
* 5. Run completion status (rejecting failed or partial runs in contractual contexts)
|
|
24
|
+
* 6. Findings structural validity
|
|
25
|
+
*/
|
|
26
|
+
export function verifyRunPackage(runDir, options = {}) {
|
|
27
|
+
const {
|
|
28
|
+
requireFindings = true,
|
|
29
|
+
requireCompleted = false,
|
|
30
|
+
requireChecksums = true,
|
|
31
|
+
} = options;
|
|
32
|
+
|
|
33
|
+
if (!runDir || !fs.existsSync(runDir)) {
|
|
34
|
+
throw new RunVerificationError(`Run directory not found: ${runDir}`, { code: 'RUN_NOT_FOUND', runDir });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const manifestPath = path.join(runDir, 'manifest.json');
|
|
38
|
+
if (!fs.existsSync(manifestPath)) {
|
|
39
|
+
throw new RunVerificationError(`Run package is missing manifest.json at ${manifestPath}`, {
|
|
40
|
+
code: 'MANIFEST_MISSING',
|
|
41
|
+
runDir,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let manifest;
|
|
46
|
+
try {
|
|
47
|
+
manifest = readJson(manifestPath);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
throw new RunVerificationError(`Run manifest.json is invalid JSON: ${err.message}`, {
|
|
50
|
+
code: 'MANIFEST_INVALID',
|
|
51
|
+
runDir,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const schemaValidation = validateAgainst('run.schema.json', manifest);
|
|
56
|
+
if (!schemaValidation.valid) {
|
|
57
|
+
throw new RunVerificationError(`Run manifest violates run.schema.json: ${schemaValidation.errors.join('; ')}`, {
|
|
58
|
+
code: 'MANIFEST_SCHEMA_INVALID',
|
|
59
|
+
errors: schemaValidation.errors,
|
|
60
|
+
runDir,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (requireCompleted && manifest.status !== 'completed' && manifest.status !== 'success') {
|
|
65
|
+
throw new RunVerificationError(`Run status is "${manifest.status}"; expected completed/success`, {
|
|
66
|
+
code: 'RUN_INCOMPLETE',
|
|
67
|
+
status: manifest.status,
|
|
68
|
+
runDir,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Verify checksums.json
|
|
73
|
+
const checksumsPath = path.join(runDir, 'checksums.json');
|
|
74
|
+
let checksumsVerified = false;
|
|
75
|
+
if (fs.existsSync(checksumsPath)) {
|
|
76
|
+
let checksums;
|
|
77
|
+
try {
|
|
78
|
+
checksums = readJson(checksumsPath);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
throw new RunVerificationError(`checksums.json is invalid JSON: ${err.message}`, {
|
|
81
|
+
code: 'CHECKSUMS_INVALID',
|
|
82
|
+
runDir,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const tamperedFiles = [];
|
|
87
|
+
const missingFiles = [];
|
|
88
|
+
|
|
89
|
+
for (const [relPath, expectedHash] of Object.entries(checksums)) {
|
|
90
|
+
const artifactPath = path.join(runDir, relPath);
|
|
91
|
+
if (!fs.existsSync(artifactPath)) {
|
|
92
|
+
missingFiles.push(relPath);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const actualHash = sha256File(artifactPath);
|
|
96
|
+
if (actualHash !== expectedHash) {
|
|
97
|
+
tamperedFiles.push({ file: relPath, expected: expectedHash, actual: actualHash });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (tamperedFiles.length > 0 || missingFiles.length > 0) {
|
|
102
|
+
throw new RunVerificationError(
|
|
103
|
+
`Cryptographic verification failed for run package: ${tamperedFiles.length} tampered files, ${missingFiles.length} missing files`,
|
|
104
|
+
{
|
|
105
|
+
code: 'CHECKSUM_MISMATCH',
|
|
106
|
+
tamperedFiles,
|
|
107
|
+
missingFiles,
|
|
108
|
+
runDir,
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
checksumsVerified = true;
|
|
113
|
+
} else if (requireChecksums) {
|
|
114
|
+
throw new RunVerificationError(`Run package is missing checksums.json at ${checksumsPath}`, {
|
|
115
|
+
code: 'CHECKSUMS_MISSING',
|
|
116
|
+
runDir,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Findings verification if present or required
|
|
121
|
+
const findingsPath = path.join(runDir, 'findings.json');
|
|
122
|
+
let findings = null;
|
|
123
|
+
if (fs.existsSync(findingsPath)) {
|
|
124
|
+
try {
|
|
125
|
+
findings = readJson(findingsPath);
|
|
126
|
+
if (!Array.isArray(findings)) {
|
|
127
|
+
throw new Error('findings.json must contain an array of findings');
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
throw new RunVerificationError(`findings.json is malformed: ${err.message}`, {
|
|
131
|
+
code: 'FINDINGS_MALFORMED',
|
|
132
|
+
runDir,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
} else if (requireFindings) {
|
|
136
|
+
throw new RunVerificationError(`Run package is missing findings.json at ${findingsPath}`, {
|
|
137
|
+
code: 'FINDINGS_MISSING',
|
|
138
|
+
runDir,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
valid: true,
|
|
144
|
+
runId: manifest.run_id,
|
|
145
|
+
timestamp: manifest.timestamp,
|
|
146
|
+
status: manifest.status,
|
|
147
|
+
manifest,
|
|
148
|
+
checksumsVerified,
|
|
149
|
+
findingsPath: fs.existsSync(findingsPath) ? findingsPath : null,
|
|
150
|
+
findingsCount: Array.isArray(findings) ? findings.length : 0,
|
|
151
|
+
findings,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { safePath } from '../detectors/framework.js';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Enterprise Scope Admissibility Gate
|
|
5
3
|
*
|
|
@@ -7,32 +5,139 @@ import { safePath } from '../detectors/framework.js';
|
|
|
7
5
|
* work packages, explicitly refusing findings that are exploratory, unverified,
|
|
8
6
|
* economically unjustified, out of organizational boundary, or lack testable acceptance.
|
|
9
7
|
*/
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_ROLE_MAPPINGS = {
|
|
10
|
+
TECH: 'Supplier Engineering Lead',
|
|
11
|
+
TECHNICAL: 'Supplier Engineering Lead',
|
|
12
|
+
CRO: 'Supplier CRO Architect',
|
|
13
|
+
AEO: 'Supplier Discovery Specialist',
|
|
14
|
+
ANS: 'Supplier Discovery Specialist',
|
|
15
|
+
GEO: 'Supplier Discovery Specialist',
|
|
16
|
+
SCHEMA: 'Supplier Data Engineer',
|
|
17
|
+
ENTITY: 'Supplier Data Engineer',
|
|
18
|
+
SEO: 'Supplier Search Strategist',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check whether a target subject falls within declared in-scope properties.
|
|
23
|
+
*
|
|
24
|
+
* Supported scope modes:
|
|
25
|
+
* - 'HOST_AND_SUBDOMAINS' (default): matches host and any subdomains
|
|
26
|
+
* - 'HOST': matches exact host only
|
|
27
|
+
* - 'EXACT_ORIGIN': matches exact scheme + host + port
|
|
28
|
+
* - 'PATH_PREFIX': matches host/subdomain and path prefix
|
|
29
|
+
* - 'EXACT_RESOURCE': matches exact host and path
|
|
30
|
+
*/
|
|
31
|
+
export function checkScopeBoundary(subject, inScopeProperties = [], scopeMode = 'HOST_AND_SUBDOMAINS') {
|
|
32
|
+
if (!inScopeProperties || inScopeProperties.length === 0) return true;
|
|
33
|
+
if (!subject) return false;
|
|
34
|
+
|
|
35
|
+
const subjectStr = typeof subject === 'string' ? subject : (subject.url || subject.identifier || '');
|
|
36
|
+
if (!subjectStr) return false;
|
|
37
|
+
|
|
38
|
+
const isRelative = !/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/|^\/\//.test(subjectStr);
|
|
39
|
+
|
|
40
|
+
for (const prop of inScopeProperties) {
|
|
41
|
+
let propUrl;
|
|
42
|
+
try {
|
|
43
|
+
propUrl = new URL(prop.includes('://') ? prop : `https://${prop}`);
|
|
44
|
+
} catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const propHost = propUrl.hostname.toLowerCase();
|
|
48
|
+
const propPath = propUrl.pathname.replace(/\/+$/, '');
|
|
49
|
+
|
|
50
|
+
if (isRelative) {
|
|
51
|
+
if (scopeMode === 'PATH_PREFIX') {
|
|
52
|
+
const normSubj = subjectStr.startsWith('/') ? subjectStr : `/${subjectStr}`;
|
|
53
|
+
if (normSubj.startsWith(propPath || '/')) return true;
|
|
54
|
+
} else if (scopeMode === 'EXACT_RESOURCE') {
|
|
55
|
+
const normSubj = subjectStr.startsWith('/') ? subjectStr : `/${subjectStr}`;
|
|
56
|
+
if (normSubj === (propPath || '/')) return true;
|
|
57
|
+
} else {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let subjUrl;
|
|
64
|
+
try {
|
|
65
|
+
subjUrl = new URL(subjectStr);
|
|
66
|
+
} catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const subjHost = subjUrl.hostname.toLowerCase();
|
|
71
|
+
const subjPath = subjUrl.pathname.replace(/\/+$/, '');
|
|
72
|
+
|
|
73
|
+
switch (scopeMode) {
|
|
74
|
+
case 'EXACT_ORIGIN':
|
|
75
|
+
if (subjUrl.origin.toLowerCase() === propUrl.origin.toLowerCase()) return true;
|
|
76
|
+
break;
|
|
77
|
+
case 'HOST':
|
|
78
|
+
if (subjHost === propHost) return true;
|
|
79
|
+
break;
|
|
80
|
+
case 'HOST_AND_SUBDOMAINS':
|
|
81
|
+
if (subjHost === propHost || subjHost.endsWith(`.${propHost}`)) return true;
|
|
82
|
+
break;
|
|
83
|
+
case 'PATH_PREFIX':
|
|
84
|
+
if ((subjHost === propHost || subjHost.endsWith(`.${propHost}`)) &&
|
|
85
|
+
(subjPath === propPath || subjPath.startsWith(`${propPath}/`))) return true;
|
|
86
|
+
break;
|
|
87
|
+
case 'EXACT_RESOURCE':
|
|
88
|
+
if (subjHost === propHost && subjPath === propPath) return true;
|
|
89
|
+
break;
|
|
90
|
+
default:
|
|
91
|
+
if (subjHost === propHost || subjHost.endsWith(`.${propHost}`)) return true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
10
98
|
export function evaluateScopeAdmissibility(findings = [], {
|
|
11
99
|
inScopeProperties = [],
|
|
100
|
+
scopeMode = 'HOST_AND_SUBDOMAINS',
|
|
12
101
|
minIceScore = 8.0,
|
|
13
102
|
allowedDisciplines = ['seo', 'aeo', 'geo', 'technical', 'schema', 'entity', 'cro'],
|
|
14
103
|
allowExperimental = false,
|
|
15
104
|
excludedDetectors = [],
|
|
105
|
+
roleMapping = null,
|
|
106
|
+
ownerMappingVersion = '1.0',
|
|
107
|
+
rejectTemplateDefaultOwner = false,
|
|
16
108
|
} = {}) {
|
|
17
109
|
const admitted = [];
|
|
18
110
|
const refused = [];
|
|
19
111
|
|
|
20
112
|
let reqIndex = 1;
|
|
21
113
|
let testIndex = 1;
|
|
114
|
+
const seenFindingIds = new Map();
|
|
22
115
|
|
|
23
116
|
for (let i = 0; i < findings.length; i++) {
|
|
24
117
|
const f = findings[i];
|
|
25
|
-
const
|
|
26
|
-
const
|
|
118
|
+
const subject = f.subject?.identifier || f.subject?.url || (typeof f.subject === 'string' ? f.subject : '/');
|
|
119
|
+
const detectorId = f.detector_id || (f.finding_id ? f.finding_id.replace(/^F-/, '') : `FINDING-${i + 1}`);
|
|
120
|
+
|
|
121
|
+
const rawFindingId = f.finding_id || f.id || f.detector_id || `FINDING-${i + 1}`;
|
|
122
|
+
let findingId = rawFindingId;
|
|
123
|
+
if (seenFindingIds.has(rawFindingId)) {
|
|
124
|
+
const count = seenFindingIds.get(rawFindingId) + 1;
|
|
125
|
+
seenFindingIds.set(rawFindingId, count);
|
|
126
|
+
findingId = `${rawFindingId}:${subject || count}`;
|
|
127
|
+
} else {
|
|
128
|
+
seenFindingIds.set(rawFindingId, 1);
|
|
129
|
+
}
|
|
130
|
+
|
|
27
131
|
const severity = f.classification?.severity || 'medium';
|
|
28
132
|
const confidence = f.classification?.confidence || 'observed';
|
|
29
133
|
const evidenceList = f.observation?.evidence || [];
|
|
30
|
-
const remediation = f.remediation?.preferred || '';
|
|
134
|
+
const remediation = f.remediation?.preferred || (typeof f.remediation === 'string' ? f.remediation : '');
|
|
31
135
|
|
|
32
136
|
// Gate 1: Excluded detectors / explicitly blacklisted checks
|
|
33
|
-
if (excludedDetectors.includes(detectorId)) {
|
|
137
|
+
if (excludedDetectors.includes(detectorId) || excludedDetectors.includes(findingId)) {
|
|
34
138
|
refused.push({
|
|
35
|
-
finding_id:
|
|
139
|
+
finding_id: findingId,
|
|
140
|
+
detector_id: detectorId,
|
|
36
141
|
subject,
|
|
37
142
|
gate_failed: 'excluded_detector',
|
|
38
143
|
refusal_code: 'REFUSE-EXCLUDED',
|
|
@@ -45,7 +150,8 @@ export function evaluateScopeAdmissibility(findings = [], {
|
|
|
45
150
|
// Gate 2: Evidence Maturity Gate
|
|
46
151
|
if (!allowExperimental && (confidence === 'experimental' || f.classification?.finding_type === 'experimental')) {
|
|
47
152
|
refused.push({
|
|
48
|
-
finding_id:
|
|
153
|
+
finding_id: findingId,
|
|
154
|
+
detector_id: detectorId,
|
|
49
155
|
subject,
|
|
50
156
|
gate_failed: 'evidence_maturity',
|
|
51
157
|
refusal_code: 'REFUSE-EXPERIMENTAL',
|
|
@@ -55,9 +161,10 @@ export function evaluateScopeAdmissibility(findings = [], {
|
|
|
55
161
|
continue;
|
|
56
162
|
}
|
|
57
163
|
|
|
58
|
-
if (evidenceList.length === 0) {
|
|
164
|
+
if (!Array.isArray(evidenceList) || evidenceList.length === 0) {
|
|
59
165
|
refused.push({
|
|
60
|
-
finding_id:
|
|
166
|
+
finding_id: findingId,
|
|
167
|
+
detector_id: detectorId,
|
|
61
168
|
subject,
|
|
62
169
|
gate_failed: 'evidence_maturity',
|
|
63
170
|
refusal_code: 'REFUSE-NO-EVIDENCE',
|
|
@@ -67,17 +174,13 @@ export function evaluateScopeAdmissibility(findings = [], {
|
|
|
67
174
|
continue;
|
|
68
175
|
}
|
|
69
176
|
|
|
70
|
-
// Gate
|
|
177
|
+
// Gate 4: Scope Boundary Gate
|
|
71
178
|
if (inScopeProperties.length > 0) {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
const sStem = subject.replace(/^https?:\/\//, '').replace(/\/+$/, '').toLowerCase();
|
|
75
|
-
return sStem.includes(pStem) || pStem.includes(sStem);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
if (!isInScope) {
|
|
179
|
+
const inScope = checkScopeBoundary(f.subject || subject, inScopeProperties, scopeMode);
|
|
180
|
+
if (!inScope) {
|
|
79
181
|
refused.push({
|
|
80
|
-
finding_id:
|
|
182
|
+
finding_id: findingId,
|
|
183
|
+
detector_id: detectorId,
|
|
81
184
|
subject,
|
|
82
185
|
gate_failed: 'scope_boundary',
|
|
83
186
|
refusal_code: 'REFUSE-OUT-OF-SCOPE',
|
|
@@ -88,28 +191,30 @@ export function evaluateScopeAdmissibility(findings = [], {
|
|
|
88
191
|
}
|
|
89
192
|
}
|
|
90
193
|
|
|
91
|
-
// Gate
|
|
92
|
-
|
|
194
|
+
// Gate 5: Technical Feasibility & Remediation Safety
|
|
195
|
+
const remLower = remediation.toLowerCase();
|
|
196
|
+
if (!remediation || remLower.includes('consult external legal') || remLower.includes('out of scope') || remLower.includes('unfeasible')) {
|
|
93
197
|
refused.push({
|
|
94
|
-
finding_id:
|
|
198
|
+
finding_id: findingId,
|
|
199
|
+
detector_id: detectorId,
|
|
95
200
|
subject,
|
|
96
201
|
gate_failed: 'technical_feasibility',
|
|
97
202
|
refusal_code: 'REFUSE-UNFEASIBLE-REMEDIATION',
|
|
98
|
-
refusal_rationale: 'Finding requires external legal advice
|
|
99
|
-
suggested_handling: 'Refer to customer legal counsel; exclude from engineering delivery.',
|
|
203
|
+
refusal_rationale: 'Finding requires external legal advice, lacks defined engineering remediation method, or is declared technically unfeasible.',
|
|
204
|
+
suggested_handling: 'Refer to customer legal counsel or appropriate third party; exclude from engineering delivery.',
|
|
100
205
|
});
|
|
101
206
|
continue;
|
|
102
207
|
}
|
|
103
208
|
|
|
104
|
-
// Gate
|
|
105
|
-
|
|
106
|
-
const isCoreSeverity = ['critical', 'high'].includes(severity);
|
|
209
|
+
// Gate 6: Commercial Materiality & ICE-BV Gate
|
|
210
|
+
const isCoreSeverity = ['critical', 'high'].includes(severity.toLowerCase());
|
|
107
211
|
const isVitalsOrFunnel = ['TECH-001', 'TECH-002', 'CRO-001', 'CRO-002', 'CRO-005', 'CRO-006', 'CRO-007', 'CRO-013', 'CRO-021', 'SCHEMA-001'].includes(detectorId);
|
|
108
|
-
const iceScore = f.ice_score
|
|
212
|
+
const iceScore = typeof f.ice_score === 'number' ? f.ice_score : (isCoreSeverity ? 12 : 6);
|
|
109
213
|
|
|
110
214
|
if (!isCoreSeverity && !isVitalsOrFunnel && iceScore < minIceScore) {
|
|
111
215
|
refused.push({
|
|
112
|
-
finding_id:
|
|
216
|
+
finding_id: findingId,
|
|
217
|
+
detector_id: detectorId,
|
|
113
218
|
subject,
|
|
114
219
|
gate_failed: 'commercial_materiality',
|
|
115
220
|
refusal_code: 'REFUSE-LOW-MATERIALITY',
|
|
@@ -119,11 +224,17 @@ export function evaluateScopeAdmissibility(findings = [], {
|
|
|
119
224
|
continue;
|
|
120
225
|
}
|
|
121
226
|
|
|
122
|
-
// Gate
|
|
123
|
-
const
|
|
124
|
-
|
|
227
|
+
// Gate 7: Objectively Testable Acceptance Gate
|
|
228
|
+
const hasVerification = Boolean(
|
|
229
|
+
f.verification?.detector_to_rerun ||
|
|
230
|
+
f.verification?.method ||
|
|
231
|
+
f.verification?.acceptance_test
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
if (!hasVerification) {
|
|
125
235
|
refused.push({
|
|
126
|
-
finding_id:
|
|
236
|
+
finding_id: findingId,
|
|
237
|
+
detector_id: detectorId,
|
|
127
238
|
subject,
|
|
128
239
|
gate_failed: 'measurable_acceptance',
|
|
129
240
|
refusal_code: 'REFUSE-UNVERIFIABLE',
|
|
@@ -133,36 +244,109 @@ export function evaluateScopeAdmissibility(findings = [], {
|
|
|
133
244
|
continue;
|
|
134
245
|
}
|
|
135
246
|
|
|
136
|
-
//
|
|
137
|
-
const
|
|
247
|
+
// Gate 8: Ownership Clarity Gate
|
|
248
|
+
const effectiveRoles = { ...DEFAULT_ROLE_MAPPINGS, ...(roleMapping || {}) };
|
|
249
|
+
const disciplineKey = (f.discipline?.[0] || detectorId.split('-')[0] || 'TECH').toUpperCase();
|
|
250
|
+
|
|
251
|
+
let resolvedOwner = null;
|
|
252
|
+
let ownerSource = null;
|
|
253
|
+
|
|
254
|
+
if (f.delivery_owner && f.delivery_owner !== 'UNRESOLVED') {
|
|
255
|
+
resolvedOwner = f.delivery_owner;
|
|
256
|
+
ownerSource = 'finding.delivery_owner';
|
|
257
|
+
} else if (f.remediation?.owner && f.remediation.owner !== 'UNRESOLVED') {
|
|
258
|
+
resolvedOwner = f.remediation.owner;
|
|
259
|
+
ownerSource = 'finding.remediation.owner';
|
|
260
|
+
} else if (f.owner && f.owner !== 'UNRESOLVED') {
|
|
261
|
+
resolvedOwner = f.owner;
|
|
262
|
+
ownerSource = 'finding.owner';
|
|
263
|
+
} else if (roleMapping && (roleMapping[disciplineKey] || roleMapping[detectorId])) {
|
|
264
|
+
resolvedOwner = roleMapping[disciplineKey] || roleMapping[detectorId];
|
|
265
|
+
ownerSource = 'engagement_role_mapping';
|
|
266
|
+
} else if (effectiveRoles[disciplineKey] && effectiveRoles[disciplineKey] !== 'UNRESOLVED') {
|
|
267
|
+
resolvedOwner = effectiveRoles[disciplineKey];
|
|
268
|
+
ownerSource = 'built_in_template_default';
|
|
269
|
+
} else if (effectiveRoles[detectorId] && effectiveRoles[detectorId] !== 'UNRESOLVED') {
|
|
270
|
+
resolvedOwner = effectiveRoles[detectorId];
|
|
271
|
+
ownerSource = 'built_in_template_default';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!resolvedOwner || resolvedOwner === 'UNRESOLVED' || resolvedOwner.toLowerCase() === 'unassigned') {
|
|
275
|
+
refused.push({
|
|
276
|
+
finding_id: findingId,
|
|
277
|
+
detector_id: detectorId,
|
|
278
|
+
subject,
|
|
279
|
+
gate_failed: 'ownership_clarity',
|
|
280
|
+
refusal_code: 'REFUSE-OWNER-UNRESOLVED',
|
|
281
|
+
refusal_rationale: `Responsible delivery owner could not be resolved for finding ${findingId} (discipline: ${disciplineKey}).`,
|
|
282
|
+
suggested_handling: 'Specify delivery_owner on finding or map discipline to accountable delivery role in SOW configuration.',
|
|
283
|
+
});
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (rejectTemplateDefaultOwner && ownerSource === 'built_in_template_default') {
|
|
288
|
+
refused.push({
|
|
289
|
+
finding_id: findingId,
|
|
290
|
+
detector_id: detectorId,
|
|
291
|
+
subject,
|
|
292
|
+
gate_failed: 'ownership_clarity',
|
|
293
|
+
refusal_code: 'REFUSE-TEMPLATE-DEFAULT-OWNER',
|
|
294
|
+
refusal_rationale: `Responsible delivery owner for finding ${findingId} relies on built-in template default. Contractual SOW requires an explicit finding owner or engagement role mapping.`,
|
|
295
|
+
suggested_handling: 'Specify delivery_owner on finding or map discipline in engagement role mapping.',
|
|
296
|
+
});
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Gate 8b: Discipline Authorization Gate
|
|
301
|
+
const rawDisciplines = Array.isArray(f.discipline)
|
|
302
|
+
? f.discipline
|
|
303
|
+
: (f.discipline ? [f.discipline] : [detectorId.split('-')[0]]);
|
|
304
|
+
const findingDisciplines = rawDisciplines.map((d) => String(d).toLowerCase());
|
|
305
|
+
|
|
306
|
+
if (allowedDisciplines && allowedDisciplines.length > 0) {
|
|
307
|
+
const normAllowed = allowedDisciplines.map((d) => String(d).toLowerCase());
|
|
308
|
+
const isAuthorized = findingDisciplines.some((d) => normAllowed.includes(d));
|
|
309
|
+
if (!isAuthorized) {
|
|
310
|
+
refused.push({
|
|
311
|
+
finding_id: findingId,
|
|
312
|
+
detector_id: detectorId,
|
|
313
|
+
subject,
|
|
314
|
+
gate_failed: 'discipline_authorization',
|
|
315
|
+
refusal_code: 'REFUSE-DISCIPLINE-NOT-AUTHORIZED',
|
|
316
|
+
refusal_rationale: `Discipline (${findingDisciplines.join(', ')}) is not authorized under contracted delivery disciplines (${allowedDisciplines.join(', ')}).`,
|
|
317
|
+
suggested_handling: 'Expand authorized disciplines in SOW engagement terms or contract under separate statement of work.',
|
|
318
|
+
});
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Passed all gates -> Admitted to Contractual SOW Scope
|
|
138
324
|
let wpId = 'WP-TECH-01';
|
|
139
325
|
let wpName = 'Technical Search & Performance Infrastructure';
|
|
140
326
|
let deliverableId = 'DELIV-01';
|
|
141
|
-
let owner = 'Supplier Engineering Lead';
|
|
142
327
|
|
|
143
|
-
if (
|
|
328
|
+
if (disciplineKey === 'CRO' || detectorId.startsWith('CRO-')) {
|
|
144
329
|
wpId = 'WP-CRO-02';
|
|
145
330
|
wpName = 'Conversion Funnel & Offer Architecture Optimization';
|
|
146
331
|
deliverableId = 'DELIV-02';
|
|
147
|
-
|
|
148
|
-
} else if (discipline === 'AEO' || discipline === 'GEO' || discipline === 'ANS') {
|
|
332
|
+
} else if (disciplineKey === 'AEO' || disciplineKey === 'GEO' || disciplineKey === 'ANS') {
|
|
149
333
|
wpId = 'WP-AEO-03';
|
|
150
334
|
wpName = 'Answer-Engine (AEO) & Generative Search Extraction';
|
|
151
335
|
deliverableId = 'DELIV-03';
|
|
152
|
-
|
|
153
|
-
} else if (discipline === 'SCHEMA' || discipline === 'ENTITY') {
|
|
336
|
+
} else if (disciplineKey === 'SCHEMA' || disciplineKey === 'ENTITY') {
|
|
154
337
|
wpId = 'WP-SCHEMA-04';
|
|
155
338
|
wpName = 'Structured Data & Entity Knowledge Graph Architecture';
|
|
156
339
|
deliverableId = 'DELIV-04';
|
|
157
|
-
owner = 'Supplier Data Engineer';
|
|
158
340
|
}
|
|
159
341
|
|
|
160
342
|
const sowReqId = `REQ-SOW-${String(reqIndex++).padStart(3, '0')}`;
|
|
161
343
|
const accTestId = `ACC-TEST-${String(testIndex++).padStart(3, '0')}`;
|
|
162
|
-
const evidenceId = evidenceList[0]
|
|
344
|
+
const evidenceId = evidenceList[0];
|
|
345
|
+
const detectorToRerun = f.verification?.detector_to_rerun || detectorId;
|
|
163
346
|
|
|
164
347
|
admitted.push({
|
|
165
|
-
finding_id:
|
|
348
|
+
finding_id: findingId,
|
|
349
|
+
detector_id: detectorId,
|
|
166
350
|
sow_requirement_id: sowReqId,
|
|
167
351
|
work_package_id: wpId,
|
|
168
352
|
work_package_name: wpName,
|
|
@@ -172,9 +356,13 @@ export function evaluateScopeAdmissibility(findings = [], {
|
|
|
172
356
|
severity,
|
|
173
357
|
confidence,
|
|
174
358
|
recommendation: remediation,
|
|
175
|
-
acceptance_test: `Execute closed-loop verification: citable verify remediation --finding ${
|
|
176
|
-
|
|
359
|
+
acceptance_test: f.verification?.acceptance_test || `Execute closed-loop verification: citable verify remediation --finding ${detectorToRerun} --target ${subject} (Expect status: RESOLVED, zero regressions)`,
|
|
360
|
+
acceptance_basis: f.verification?.method ? 'empirical_verification_method' : (f.verification?.detector_to_rerun ? 'automated_detector_rerun' : 'deterministic_remediation_test'),
|
|
361
|
+
owner: resolvedOwner,
|
|
362
|
+
owner_source: ownerSource,
|
|
363
|
+
owner_mapping_version: ownerMappingVersion,
|
|
177
364
|
evidence_id: evidenceId,
|
|
365
|
+
evidence_ids: [...evidenceList],
|
|
178
366
|
commercial_priority: isCoreSeverity ? 'P1_CRITICAL' : 'P2_HIGH_VALUE',
|
|
179
367
|
ice_bv_score: iceScore,
|
|
180
368
|
});
|