@nebulacomponents/citable 1.18.0 → 1.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/universal/.agents/skills/citable/SKILL.md +22 -1
  3. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  4. package/dist/universal/.agents/skills/citable/agents/citable-remediator.md +43 -0
  5. package/dist/universal/.agents/skills/citable/agents/citable-sow-architect.md +43 -0
  6. package/dist/universal/.agents/skills/citable/commands/README.md +13 -0
  7. package/dist/universal/.agents/skills/citable/commands/artifact-interchange.md +5 -0
  8. package/dist/universal/.agents/skills/citable/commands/remediation.md +55 -0
  9. package/dist/universal/.agents/skills/citable/commands/reporting.md +28 -0
  10. package/dist/universal/.agents/skills/citable/commands/sow.md +95 -0
  11. package/dist/universal/.agents/skills/citable/manifest.json +15 -9
  12. package/dist/universal/.agents/skills/citable/schemas/cro-report.schema.json +112 -0
  13. package/dist/universal/.agents/skills/citable/schemas/search-report.schema.json +99 -0
  14. package/dist/universal/.agents/skills/citable/schemas/sow.schema.json +344 -26
  15. package/dist/universal/.claude/agents/citable/citable-remediator.md +43 -0
  16. package/dist/universal/.claude/agents/citable/citable-sow-architect.md +43 -0
  17. package/dist/universal/.claude/agents/citable/manifest.json +6 -4
  18. package/dist/universal/.claude/skills/citable/SKILL.md +22 -1
  19. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  20. package/dist/universal/.claude/skills/citable/agents/citable-remediator.md +43 -0
  21. package/dist/universal/.claude/skills/citable/agents/citable-sow-architect.md +43 -0
  22. package/dist/universal/.claude/skills/citable/commands/README.md +13 -0
  23. package/dist/universal/.claude/skills/citable/commands/artifact-interchange.md +5 -0
  24. package/dist/universal/.claude/skills/citable/commands/remediation.md +55 -0
  25. package/dist/universal/.claude/skills/citable/commands/reporting.md +28 -0
  26. package/dist/universal/.claude/skills/citable/commands/sow.md +95 -0
  27. package/dist/universal/.claude/skills/citable/manifest.json +19 -11
  28. package/dist/universal/.claude/skills/citable/schemas/cro-report.schema.json +112 -0
  29. package/dist/universal/.claude/skills/citable/schemas/search-report.schema.json +99 -0
  30. package/dist/universal/.claude/skills/citable/schemas/sow.schema.json +344 -26
  31. package/dist/universal/.cursor/skills/citable/SKILL.md +22 -1
  32. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  33. package/dist/universal/.cursor/skills/citable/agents/citable-remediator.md +43 -0
  34. package/dist/universal/.cursor/skills/citable/agents/citable-sow-architect.md +43 -0
  35. package/dist/universal/.cursor/skills/citable/commands/README.md +13 -0
  36. package/dist/universal/.cursor/skills/citable/commands/artifact-interchange.md +5 -0
  37. package/dist/universal/.cursor/skills/citable/commands/remediation.md +55 -0
  38. package/dist/universal/.cursor/skills/citable/commands/reporting.md +28 -0
  39. package/dist/universal/.cursor/skills/citable/commands/sow.md +95 -0
  40. package/dist/universal/.cursor/skills/citable/manifest.json +15 -9
  41. package/dist/universal/.cursor/skills/citable/schemas/cro-report.schema.json +112 -0
  42. package/dist/universal/.cursor/skills/citable/schemas/search-report.schema.json +99 -0
  43. package/dist/universal/.cursor/skills/citable/schemas/sow.schema.json +344 -26
  44. package/dist/universal/.gemini/skills/citable/SKILL.md +22 -1
  45. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  46. package/dist/universal/.gemini/skills/citable/agents/citable-remediator.md +43 -0
  47. package/dist/universal/.gemini/skills/citable/agents/citable-sow-architect.md +43 -0
  48. package/dist/universal/.gemini/skills/citable/commands/README.md +13 -0
  49. package/dist/universal/.gemini/skills/citable/commands/artifact-interchange.md +5 -0
  50. package/dist/universal/.gemini/skills/citable/commands/remediation.md +55 -0
  51. package/dist/universal/.gemini/skills/citable/commands/reporting.md +28 -0
  52. package/dist/universal/.gemini/skills/citable/commands/sow.md +95 -0
  53. package/dist/universal/.gemini/skills/citable/manifest.json +15 -9
  54. package/dist/universal/.gemini/skills/citable/schemas/cro-report.schema.json +112 -0
  55. package/dist/universal/.gemini/skills/citable/schemas/search-report.schema.json +99 -0
  56. package/dist/universal/.gemini/skills/citable/schemas/sow.schema.json +344 -26
  57. package/dist/universal/.github/skills/citable/SKILL.md +22 -1
  58. package/dist/universal/.github/skills/citable/VERSION +1 -1
  59. package/dist/universal/.github/skills/citable/agents/citable-remediator.md +43 -0
  60. package/dist/universal/.github/skills/citable/agents/citable-sow-architect.md +43 -0
  61. package/dist/universal/.github/skills/citable/commands/README.md +13 -0
  62. package/dist/universal/.github/skills/citable/commands/artifact-interchange.md +5 -0
  63. package/dist/universal/.github/skills/citable/commands/remediation.md +55 -0
  64. package/dist/universal/.github/skills/citable/commands/reporting.md +28 -0
  65. package/dist/universal/.github/skills/citable/commands/sow.md +95 -0
  66. package/dist/universal/.github/skills/citable/manifest.json +15 -9
  67. package/dist/universal/.github/skills/citable/schemas/cro-report.schema.json +112 -0
  68. package/dist/universal/.github/skills/citable/schemas/search-report.schema.json +99 -0
  69. package/dist/universal/.github/skills/citable/schemas/sow.schema.json +344 -26
  70. package/dist/universal/.kiro/skills/citable/SKILL.md +22 -1
  71. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  72. package/dist/universal/.kiro/skills/citable/agents/citable-remediator.md +43 -0
  73. package/dist/universal/.kiro/skills/citable/agents/citable-sow-architect.md +43 -0
  74. package/dist/universal/.kiro/skills/citable/commands/README.md +13 -0
  75. package/dist/universal/.kiro/skills/citable/commands/artifact-interchange.md +5 -0
  76. package/dist/universal/.kiro/skills/citable/commands/remediation.md +55 -0
  77. package/dist/universal/.kiro/skills/citable/commands/reporting.md +28 -0
  78. package/dist/universal/.kiro/skills/citable/commands/sow.md +95 -0
  79. package/dist/universal/.kiro/skills/citable/manifest.json +15 -9
  80. package/dist/universal/.kiro/skills/citable/schemas/cro-report.schema.json +112 -0
  81. package/dist/universal/.kiro/skills/citable/schemas/search-report.schema.json +99 -0
  82. package/dist/universal/.kiro/skills/citable/schemas/sow.schema.json +344 -26
  83. package/dist/universal/.opencode/skills/citable/SKILL.md +22 -1
  84. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  85. package/dist/universal/.opencode/skills/citable/agents/citable-remediator.md +43 -0
  86. package/dist/universal/.opencode/skills/citable/agents/citable-sow-architect.md +43 -0
  87. package/dist/universal/.opencode/skills/citable/commands/README.md +13 -0
  88. package/dist/universal/.opencode/skills/citable/commands/artifact-interchange.md +5 -0
  89. package/dist/universal/.opencode/skills/citable/commands/remediation.md +55 -0
  90. package/dist/universal/.opencode/skills/citable/commands/reporting.md +28 -0
  91. package/dist/universal/.opencode/skills/citable/commands/sow.md +95 -0
  92. package/dist/universal/.opencode/skills/citable/manifest.json +15 -9
  93. package/dist/universal/.opencode/skills/citable/schemas/cro-report.schema.json +112 -0
  94. package/dist/universal/.opencode/skills/citable/schemas/search-report.schema.json +99 -0
  95. package/dist/universal/.opencode/skills/citable/schemas/sow.schema.json +344 -26
  96. package/dist/universal/.pi/agent/skills/citable/SKILL.md +22 -1
  97. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  98. package/dist/universal/.pi/agent/skills/citable/agents/citable-remediator.md +43 -0
  99. package/dist/universal/.pi/agent/skills/citable/agents/citable-sow-architect.md +43 -0
  100. package/dist/universal/.pi/agent/skills/citable/commands/README.md +13 -0
  101. package/dist/universal/.pi/agent/skills/citable/commands/artifact-interchange.md +5 -0
  102. package/dist/universal/.pi/agent/skills/citable/commands/remediation.md +55 -0
  103. package/dist/universal/.pi/agent/skills/citable/commands/reporting.md +28 -0
  104. package/dist/universal/.pi/agent/skills/citable/commands/sow.md +95 -0
  105. package/dist/universal/.pi/agent/skills/citable/manifest.json +15 -9
  106. package/dist/universal/.pi/agent/skills/citable/schemas/cro-report.schema.json +112 -0
  107. package/dist/universal/.pi/agent/skills/citable/schemas/search-report.schema.json +99 -0
  108. package/dist/universal/.pi/agent/skills/citable/schemas/sow.schema.json +344 -26
  109. package/dist/universal/.qoder/skills/citable/SKILL.md +22 -1
  110. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  111. package/dist/universal/.qoder/skills/citable/agents/citable-remediator.md +43 -0
  112. package/dist/universal/.qoder/skills/citable/agents/citable-sow-architect.md +43 -0
  113. package/dist/universal/.qoder/skills/citable/commands/README.md +13 -0
  114. package/dist/universal/.qoder/skills/citable/commands/artifact-interchange.md +5 -0
  115. package/dist/universal/.qoder/skills/citable/commands/remediation.md +55 -0
  116. package/dist/universal/.qoder/skills/citable/commands/reporting.md +28 -0
  117. package/dist/universal/.qoder/skills/citable/commands/sow.md +95 -0
  118. package/dist/universal/.qoder/skills/citable/manifest.json +15 -9
  119. package/dist/universal/.qoder/skills/citable/schemas/cro-report.schema.json +112 -0
  120. package/dist/universal/.qoder/skills/citable/schemas/search-report.schema.json +99 -0
  121. package/dist/universal/.qoder/skills/citable/schemas/sow.schema.json +344 -26
  122. package/dist/universal/.rovodev/skills/citable/SKILL.md +22 -1
  123. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  124. package/dist/universal/.rovodev/skills/citable/agents/citable-remediator.md +43 -0
  125. package/dist/universal/.rovodev/skills/citable/agents/citable-sow-architect.md +43 -0
  126. package/dist/universal/.rovodev/skills/citable/commands/README.md +13 -0
  127. package/dist/universal/.rovodev/skills/citable/commands/artifact-interchange.md +5 -0
  128. package/dist/universal/.rovodev/skills/citable/commands/remediation.md +55 -0
  129. package/dist/universal/.rovodev/skills/citable/commands/reporting.md +28 -0
  130. package/dist/universal/.rovodev/skills/citable/commands/sow.md +95 -0
  131. package/dist/universal/.rovodev/skills/citable/manifest.json +15 -9
  132. package/dist/universal/.rovodev/skills/citable/schemas/cro-report.schema.json +112 -0
  133. package/dist/universal/.rovodev/skills/citable/schemas/search-report.schema.json +99 -0
  134. package/dist/universal/.rovodev/skills/citable/schemas/sow.schema.json +344 -26
  135. package/dist/universal/.trae/skills/citable/SKILL.md +22 -1
  136. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  137. package/dist/universal/.trae/skills/citable/agents/citable-remediator.md +43 -0
  138. package/dist/universal/.trae/skills/citable/agents/citable-sow-architect.md +43 -0
  139. package/dist/universal/.trae/skills/citable/commands/README.md +13 -0
  140. package/dist/universal/.trae/skills/citable/commands/artifact-interchange.md +5 -0
  141. package/dist/universal/.trae/skills/citable/commands/remediation.md +55 -0
  142. package/dist/universal/.trae/skills/citable/commands/reporting.md +28 -0
  143. package/dist/universal/.trae/skills/citable/commands/sow.md +95 -0
  144. package/dist/universal/.trae/skills/citable/manifest.json +15 -9
  145. package/dist/universal/.trae/skills/citable/schemas/cro-report.schema.json +112 -0
  146. package/dist/universal/.trae/skills/citable/schemas/search-report.schema.json +99 -0
  147. package/dist/universal/.trae/skills/citable/schemas/sow.schema.json +344 -26
  148. package/dist/universal/.trae-cn/skills/citable/SKILL.md +22 -1
  149. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  150. package/dist/universal/.trae-cn/skills/citable/agents/citable-remediator.md +43 -0
  151. package/dist/universal/.trae-cn/skills/citable/agents/citable-sow-architect.md +43 -0
  152. package/dist/universal/.trae-cn/skills/citable/commands/README.md +13 -0
  153. package/dist/universal/.trae-cn/skills/citable/commands/artifact-interchange.md +5 -0
  154. package/dist/universal/.trae-cn/skills/citable/commands/remediation.md +55 -0
  155. package/dist/universal/.trae-cn/skills/citable/commands/reporting.md +28 -0
  156. package/dist/universal/.trae-cn/skills/citable/commands/sow.md +95 -0
  157. package/dist/universal/.trae-cn/skills/citable/manifest.json +15 -9
  158. package/dist/universal/.trae-cn/skills/citable/schemas/cro-report.schema.json +112 -0
  159. package/dist/universal/.trae-cn/skills/citable/schemas/search-report.schema.json +99 -0
  160. package/dist/universal/.trae-cn/skills/citable/schemas/sow.schema.json +344 -26
  161. package/dist/universal/manifest.json +186 -112
  162. package/package.json +2 -1
  163. package/schemas/cro-report.schema.json +112 -0
  164. package/schemas/search-report.schema.json +99 -0
  165. package/schemas/sow.schema.json +344 -26
  166. package/skill/SKILL.md +22 -1
  167. package/skill/agents/citable-remediator.md +43 -0
  168. package/skill/agents/citable-sow-architect.md +43 -0
  169. package/skill/commands/README.md +13 -0
  170. package/skill/commands/artifact-interchange.md +5 -0
  171. package/skill/commands/remediation.md +55 -0
  172. package/skill/commands/reporting.md +28 -0
  173. package/skill/commands/sow.md +95 -0
  174. package/src/analysis/behavioral.js +5 -4
  175. package/src/analysis/eeat.js +10 -9
  176. package/src/analysis/offpage.js +19 -36
  177. package/src/analysis/readiness.js +8 -2
  178. package/src/artifacts/verifyCustomerArtifact.js +283 -0
  179. package/src/cli/index.js +8 -1
  180. package/src/commands/sowCmd.js +17 -0
  181. package/src/reporting/executiveCroReport.js +393 -229
  182. package/src/reporting/executiveExport.js +6 -2
  183. package/src/reporting/executiveSearchReport.js +519 -271
  184. package/src/shared/domainUtils.js +140 -0
  185. package/src/shared/epistemicStatus.js +111 -0
  186. package/src/shared/evidenceSourceResolver.js +286 -0
  187. package/src/shared/htmlEscape.js +65 -0
  188. package/src/shared/runPackageVerifier.js +153 -0
  189. package/src/sow/admissibilityGate.js +234 -46
  190. package/src/sow/generateSow.js +524 -142
@@ -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 detectorId = f.detector_id || `FINDING-${i + 1}`;
26
- const subject = f.subject?.identifier || f.subject?.url || '/';
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: detectorId,
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: detectorId,
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: detectorId,
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 3: Scope Boundary Gate
177
+ // Gate 4: Scope Boundary Gate
71
178
  if (inScopeProperties.length > 0) {
72
- const isInScope = inScopeProperties.some((prop) => {
73
- const pStem = prop.replace(/^https?:\/\//, '').replace(/\/+$/, '').toLowerCase();
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: detectorId,
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 4: Technical Feasibility & Remediation Safety
92
- if (!remediation || remediation.toLowerCase().includes('consult external legal') || remediation.toLowerCase().includes('out of scope')) {
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: detectorId,
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 or lacks defined engineering remediation method.',
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 5: Commercial Materiality & ICE-BV Gate
105
- // High & critical severity are automatically material; medium/low must meet ICE threshold or be key conversion/vitals defects
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 || (isCoreSeverity ? 12 : 6);
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: detectorId,
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 6: Objectively Testable Acceptance Gate
123
- const rerunDetector = f.verification?.detector_to_rerun || detectorId;
124
- if (!rerunDetector && !f.verification?.method) {
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: detectorId,
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
- // Passed All 6 Gates -> Admitted to Contractual SOW Scope
137
- const discipline = (f.discipline?.[0] || detectorId.split('-')[0] || 'TECH').toUpperCase();
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 (discipline === 'CRO' || detectorId.startsWith('CRO-')) {
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
- owner = 'Supplier CRO Architect';
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
- owner = 'Supplier Discovery Specialist';
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] || `EVD-OBS-${detectorId}`;
344
+ const evidenceId = evidenceList[0];
345
+ const detectorToRerun = f.verification?.detector_to_rerun || detectorId;
163
346
 
164
347
  admitted.push({
165
- finding_id: detectorId,
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 ${detectorId} --target ${subject} (Expect status: RESOLVED, zero regressions)`,
176
- owner,
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
  });