@nebulacomponents/citable 1.19.0 → 1.20.0
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 +18 -0
- package/README.md +12 -1
- package/dist/universal/.agents/skills/citable/SKILL.md +11 -2
- package/dist/universal/.agents/skills/citable/VERSION +1 -1
- package/dist/universal/.agents/skills/citable/manifest.json +15 -12
- package/dist/universal/.agents/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.agents/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.agents/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.agents/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.agents/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.agents/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.agents/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.agents/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.agents/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.agents/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.claude/agents/citable/manifest.json +2 -2
- package/dist/universal/.claude/skills/citable/SKILL.md +11 -2
- package/dist/universal/.claude/skills/citable/VERSION +1 -1
- package/dist/universal/.claude/skills/citable/manifest.json +15 -12
- package/dist/universal/.claude/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.claude/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.claude/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.claude/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.claude/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.claude/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.claude/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.claude/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.claude/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.cursor/skills/citable/SKILL.md +11 -2
- package/dist/universal/.cursor/skills/citable/VERSION +1 -1
- package/dist/universal/.cursor/skills/citable/manifest.json +15 -12
- package/dist/universal/.cursor/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.cursor/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.cursor/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.cursor/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.cursor/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.cursor/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.cursor/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.cursor/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.cursor/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.gemini/skills/citable/SKILL.md +11 -2
- package/dist/universal/.gemini/skills/citable/VERSION +1 -1
- package/dist/universal/.gemini/skills/citable/manifest.json +15 -12
- package/dist/universal/.gemini/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.gemini/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.gemini/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.gemini/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.gemini/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.gemini/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.gemini/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.gemini/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.gemini/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.github/skills/citable/SKILL.md +11 -2
- package/dist/universal/.github/skills/citable/VERSION +1 -1
- package/dist/universal/.github/skills/citable/manifest.json +15 -12
- package/dist/universal/.github/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.github/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.github/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.github/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.github/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.github/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.github/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.github/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.github/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.github/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.kiro/skills/citable/SKILL.md +11 -2
- package/dist/universal/.kiro/skills/citable/VERSION +1 -1
- package/dist/universal/.kiro/skills/citable/manifest.json +15 -12
- package/dist/universal/.kiro/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.kiro/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.kiro/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.kiro/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.kiro/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.kiro/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.kiro/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.kiro/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.kiro/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.opencode/skills/citable/SKILL.md +11 -2
- package/dist/universal/.opencode/skills/citable/VERSION +1 -1
- package/dist/universal/.opencode/skills/citable/manifest.json +15 -12
- package/dist/universal/.opencode/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.opencode/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.opencode/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.opencode/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.opencode/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.opencode/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.opencode/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.opencode/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.opencode/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.pi/agent/skills/citable/SKILL.md +11 -2
- package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
- package/dist/universal/.pi/agent/skills/citable/manifest.json +15 -12
- package/dist/universal/.pi/agent/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.pi/agent/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.pi/agent/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.qoder/skills/citable/SKILL.md +11 -2
- package/dist/universal/.qoder/skills/citable/VERSION +1 -1
- package/dist/universal/.qoder/skills/citable/manifest.json +15 -12
- package/dist/universal/.qoder/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.qoder/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.qoder/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.qoder/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.qoder/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.qoder/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.qoder/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.qoder/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.qoder/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.rovodev/skills/citable/SKILL.md +11 -2
- package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
- package/dist/universal/.rovodev/skills/citable/manifest.json +15 -12
- package/dist/universal/.rovodev/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.rovodev/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.rovodev/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.rovodev/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.rovodev/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.rovodev/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.rovodev/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.rovodev/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.rovodev/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.trae/skills/citable/SKILL.md +11 -2
- package/dist/universal/.trae/skills/citable/VERSION +1 -1
- package/dist/universal/.trae/skills/citable/manifest.json +15 -12
- package/dist/universal/.trae/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.trae/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.trae/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.trae/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.trae/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.trae/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.trae/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.trae/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.trae/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.trae-cn/skills/citable/SKILL.md +11 -2
- package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
- package/dist/universal/.trae-cn/skills/citable/manifest.json +15 -12
- package/dist/universal/.trae-cn/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.trae-cn/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.trae-cn/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/manifest.json +184 -148
- package/package.json +1 -1
- package/schemas/action-plan.schema.json +38 -1
- package/schemas/audit-coverage.schema.json +118 -0
- package/schemas/collection-result.schema.json +35 -0
- package/schemas/config.schema.json +2 -0
- package/schemas/cro-report.schema.json +8 -1
- package/schemas/finding.schema.json +105 -2
- package/schemas/remediation-verification.schema.json +12 -1
- package/schemas/run.schema.json +15 -2
- package/schemas/search-report.schema.json +8 -1
- package/schemas/summary.schema.json +41 -0
- package/skill/SKILL.md +11 -2
- package/src/acceptance/reproducibility.js +5 -25
- package/src/analysis/croRoadmap.js +3 -3
- package/src/analysis/strategicRoadmap.js +3 -3
- package/src/artifacts/interchange.js +2 -6
- package/src/cli/index.js +15 -3
- package/src/commands/actionPlan.js +46 -5
- package/src/commands/applyRemediation.js +5 -5
- package/src/commands/audit.js +75 -8
- package/src/commands/ciWorkflow.js +7 -2
- package/src/commands/compareSnapshots.js +155 -47
- package/src/commands/connect.js +24 -4
- package/src/commands/context.js +41 -2
- package/src/commands/croSuite.js +51 -13
- package/src/commands/delivery.js +7 -7
- package/src/commands/governance.js +9 -10
- package/src/commands/implementationKit.js +2 -1
- package/src/commands/init.js +8 -1
- package/src/commands/previewCro.js +1 -1
- package/src/commands/prioritize.js +3 -2
- package/src/commands/reviews.js +31 -5
- package/src/commands/roadmapCmd.js +7 -13
- package/src/commands/sweep.js +15 -2
- package/src/commands/verifyRemediation.js +94 -19
- package/src/connectors/collectionResult.js +80 -0
- package/src/connectors/ga4.js +46 -11
- package/src/connectors/gsc.js +41 -6
- package/src/connectors/index.js +1 -1
- package/src/connectors/webflow.js +140 -10
- package/src/connectors/wordpress.js +92 -8
- package/src/crawler/fetch.js +220 -24
- package/src/crawler/resourceValidity.js +119 -0
- package/src/crawler/sitemap.js +435 -16
- package/src/crawler/sitemapCollector.js +364 -0
- package/src/crawler/urlIdentity.js +97 -0
- package/src/detectors/agent.js +11 -0
- package/src/detectors/ans.js +16 -1
- package/src/detectors/arch.js +6 -0
- package/src/detectors/claim.js +9 -0
- package/src/detectors/crawl.js +7 -0
- package/src/detectors/cro.js +21 -0
- package/src/detectors/cwv.js +4 -0
- package/src/detectors/entity.js +7 -0
- package/src/detectors/evd.js +9 -0
- package/src/detectors/framework.js +28 -2
- package/src/detectors/geoReco.js +16 -0
- package/src/detectors/hreflang.js +3 -0
- package/src/detectors/lifeMeas.js +9 -0
- package/src/detectors/link.js +6 -1
- package/src/detectors/page.js +11 -0
- package/src/detectors/schemaData.js +16 -1
- package/src/detectors/tech.js +34 -3
- package/src/evidence/coverage.js +448 -0
- package/src/evidence/determination.js +157 -0
- package/src/evidence/downstream.js +110 -0
- package/src/evidence/hashes.js +109 -0
- package/src/evidence/run.js +68 -8
- package/src/extractor/page.js +29 -2
- package/src/extractor/site.js +298 -50
- package/src/reporting/executiveCroReport.js +36 -13
- package/src/reporting/executiveExport.js +45 -16
- package/src/reporting/executiveSearchReport.js +56 -22
- package/src/reporting/report.js +56 -9
- package/src/shared/epistemicLanguage.js +86 -0
- package/src/shared/evidenceSourceResolver.js +48 -17
- package/src/shared/htmlEscape.js +8 -4
- package/src/shared/runPackageVerifier.js +99 -8
- package/src/shared/verifiedRunLoader.js +129 -0
package/src/extractor/site.js
CHANGED
|
@@ -3,7 +3,106 @@ import path from 'node:path';
|
|
|
3
3
|
import { extractPage } from './page.js';
|
|
4
4
|
import { parseRobots } from '../crawler/robots.js';
|
|
5
5
|
import { parseSitemap } from '../crawler/sitemap.js';
|
|
6
|
+
import { collectSitemapTopology } from '../crawler/sitemapCollector.js';
|
|
6
7
|
import { fetchUrl } from '../crawler/fetch.js';
|
|
8
|
+
import { classifyResource } from '../crawler/resourceValidity.js';
|
|
9
|
+
import { createUrlIdentity } from '../crawler/urlIdentity.js';
|
|
10
|
+
import { createCoverageLedger } from '../evidence/coverage.js';
|
|
11
|
+
import { hashPageArtifact } from '../evidence/hashes.js';
|
|
12
|
+
|
|
13
|
+
const MAX_PAGE_FETCH_ATTEMPTS = 100;
|
|
14
|
+
const SAFE_FETCH_ERROR_CODES = new Set([
|
|
15
|
+
'FETCH_ABORTED', 'FETCH_BODY_LIMIT', 'FETCH_ERROR', 'FETCH_REDIRECT_INVALID',
|
|
16
|
+
'FETCH_REDIRECT_LIMIT', 'FETCH_REDIRECT_ORIGIN', 'FETCH_TIMEOUT',
|
|
17
|
+
'EAI_AGAIN', 'ECONNREFUSED', 'ECONNRESET', 'EHOSTUNREACH', 'ENETUNREACH',
|
|
18
|
+
'ENOTFOUND', 'EPIPE', 'ETIMEDOUT', 'UND_ERR_CONNECT_TIMEOUT',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
function sanitizeEvidenceUrl(rawUrl) {
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(String(rawUrl));
|
|
24
|
+
url.hash = '';
|
|
25
|
+
if (url.search) url.search = '?[REDACTED]';
|
|
26
|
+
return url.href;
|
|
27
|
+
} catch {
|
|
28
|
+
return '[invalid-url]';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function safeErrorCode(value, fallback = 'FETCH_ERROR') {
|
|
33
|
+
return typeof value === 'string' && SAFE_FETCH_ERROR_CODES.has(value) ? value : fallback;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function finiteNonNegative(value) {
|
|
37
|
+
return Number.isFinite(value) && value >= 0 ? value : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sanitizeFetchAttempts(attempts) {
|
|
41
|
+
if (!Array.isArray(attempts)) return { attempts: [], truncated: false };
|
|
42
|
+
const bounded = attempts.slice(0, MAX_PAGE_FETCH_ATTEMPTS).map((rawAttempt, index) => {
|
|
43
|
+
const attempt = rawAttempt && typeof rawAttempt === 'object' ? rawAttempt : {};
|
|
44
|
+
const outcome = attempt.outcome === 'response' ? 'response' : 'error';
|
|
45
|
+
return {
|
|
46
|
+
attempt: Number.isInteger(attempt.attempt) && attempt.attempt > 0 ? attempt.attempt : index + 1,
|
|
47
|
+
url: sanitizeEvidenceUrl(attempt.url),
|
|
48
|
+
startedAtMs: finiteNonNegative(attempt.startedAtMs),
|
|
49
|
+
endedAtMs: finiteNonNegative(attempt.endedAtMs),
|
|
50
|
+
elapsedMs: finiteNonNegative(attempt.elapsedMs),
|
|
51
|
+
outcome,
|
|
52
|
+
status: Number.isInteger(attempt.status) && attempt.status >= 100 && attempt.status <= 599
|
|
53
|
+
? attempt.status
|
|
54
|
+
: null,
|
|
55
|
+
errorCode: outcome === 'error' ? safeErrorCode(attempt.errorCode) : null,
|
|
56
|
+
retryDecision: ['retry', 'stop', 'redirect', 'complete'].includes(attempt.retryDecision)
|
|
57
|
+
? attempt.retryDecision
|
|
58
|
+
: 'stop',
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
return { attempts: bounded, truncated: attempts.length > MAX_PAGE_FETCH_ATTEMPTS };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function sanitizedFetchError(rawUrl, error) {
|
|
65
|
+
const attempts = sanitizeFetchAttempts(error?.attempts).attempts;
|
|
66
|
+
const reason = safeErrorCode(error?.code, attempts.at(-1)?.errorCode ?? 'FETCH_ERROR');
|
|
67
|
+
return `${sanitizeEvidenceUrl(rawUrl)}: ${reason}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sanitizedSitemapError(error) {
|
|
71
|
+
const reason = typeof error?.reason === 'string' && /^[a-z0-9_]{1,80}$/.test(error.reason)
|
|
72
|
+
? error.reason
|
|
73
|
+
: 'sitemap_error';
|
|
74
|
+
return `${sanitizeEvidenceUrl(error?.url)}: ${reason}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function attachResourceMetadata(page, {
|
|
78
|
+
requestedUrl,
|
|
79
|
+
bodyComplete = true,
|
|
80
|
+
fetchAttempts = [],
|
|
81
|
+
} = {}) {
|
|
82
|
+
const effectiveUrl = page.url;
|
|
83
|
+
const declaredCanonicalUrl = page.canonicals[0] ?? null;
|
|
84
|
+
page.requestedUrl = requestedUrl ?? effectiveUrl;
|
|
85
|
+
page.bodyComplete = bodyComplete === true;
|
|
86
|
+
const sanitizedAttempts = sanitizeFetchAttempts(fetchAttempts);
|
|
87
|
+
page.fetchAttempts = sanitizedAttempts.attempts;
|
|
88
|
+
page.fetchAttemptsTruncated = sanitizedAttempts.truncated;
|
|
89
|
+
page.resourceValidity = classifyResource({
|
|
90
|
+
status: page.status,
|
|
91
|
+
headers: page.headers,
|
|
92
|
+
body: page.rawHtml,
|
|
93
|
+
bodyComplete: page.bodyComplete,
|
|
94
|
+
requestedUrl: page.requestedUrl,
|
|
95
|
+
effectiveUrl,
|
|
96
|
+
});
|
|
97
|
+
page.urlIdentity = createUrlIdentity({
|
|
98
|
+
requestedUrl: page.requestedUrl,
|
|
99
|
+
effectiveUrl,
|
|
100
|
+
redirectChain: page.redirectChain,
|
|
101
|
+
declaredCanonicalUrl,
|
|
102
|
+
});
|
|
103
|
+
page.artifact_hash = hashPageArtifact(page);
|
|
104
|
+
return page;
|
|
105
|
+
}
|
|
7
106
|
|
|
8
107
|
/**
|
|
9
108
|
* Build a SiteModel from a directory of built/static HTML output.
|
|
@@ -27,16 +126,17 @@ export function buildSiteFromDir(dir, { baseUrl = 'https://example.test' } = {})
|
|
|
27
126
|
let urlPath = '/' + rel;
|
|
28
127
|
if (urlPath.endsWith('/index.html')) urlPath = urlPath.slice(0, -'index.html'.length);
|
|
29
128
|
const t = transport['/' + rel] || transport[urlPath] || {};
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
);
|
|
129
|
+
const responseBody = fs.readFileSync(p);
|
|
130
|
+
const page = extractPage({
|
|
131
|
+
url: new URL(urlPath, baseUrl).href,
|
|
132
|
+
html: responseBody.toString('utf8'),
|
|
133
|
+
responseBody,
|
|
134
|
+
status: t.status ?? 200,
|
|
135
|
+
headers: { 'content-type': 'text/html', ...(t.headers || {}) },
|
|
136
|
+
sourceFile: p,
|
|
137
|
+
redirectChain: [],
|
|
138
|
+
});
|
|
139
|
+
pages.push(attachResourceMetadata(page, { requestedUrl: page.url }));
|
|
40
140
|
}
|
|
41
141
|
}
|
|
42
142
|
};
|
|
@@ -62,68 +162,216 @@ export function buildSiteFromDir(dir, { baseUrl = 'https://example.test' } = {})
|
|
|
62
162
|
|
|
63
163
|
/** Build a SiteModel by fetching a deployed URL set (target URL + same-origin discovery, bounded). */
|
|
64
164
|
export async function buildSiteFromUrl(startUrl, {
|
|
65
|
-
maxPages =
|
|
165
|
+
maxPages = 500, timeBudgetSeconds = 1800, userAgent, pageMaxBytes = 5 * 1024 * 1024,
|
|
166
|
+
// Collection is intentionally sequential. Reject unsupported values rather
|
|
167
|
+
// than silently changing reproducibility or budget semantics.
|
|
168
|
+
concurrency = 1,
|
|
66
169
|
robotsMaxBytes = 512 * 1024, sitemapMaxBytes = 5 * 1024 * 1024,
|
|
170
|
+
sitemapMaxDepth = 4, sitemapMaxDocuments = 1000,
|
|
171
|
+
sitemapMaxUncompressedBytes = 20 * 1024 * 1024,
|
|
172
|
+
sitemapMaxTotalUncompressedBytes = 100 * 1024 * 1024,
|
|
173
|
+
sitemapMaxDiscoveredUrls = 50_000, sitemapMaxQueuedDocuments = 1000,
|
|
174
|
+
sitemapMaxRawEntries = 100_000, sitemapMaxXmlTokens = 500_000,
|
|
67
175
|
fetcher = fetchUrl,
|
|
176
|
+
now = () => performance.now(),
|
|
68
177
|
} = {}) {
|
|
178
|
+
if (concurrency !== 1) {
|
|
179
|
+
throw new RangeError(`URL collection concurrency is fixed at 1; received ${String(concurrency)}`);
|
|
180
|
+
}
|
|
69
181
|
const origin = new URL(startUrl).origin;
|
|
70
|
-
const
|
|
71
|
-
const
|
|
182
|
+
const coverageLedger = createCoverageLedger({ startUrl, maxPages, timeBudgetSeconds });
|
|
183
|
+
const startedAt = now();
|
|
184
|
+
const timeExpired = () => now() - startedAt >= timeBudgetSeconds * 1000;
|
|
72
185
|
const pages = [];
|
|
73
186
|
const errors = [];
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
|
|
187
|
+
let stopReason = 'frontier_exhausted';
|
|
188
|
+
const timeBudgetStopped = () => {
|
|
189
|
+
if (!timeExpired()) return false;
|
|
190
|
+
if (stopReason === 'frontier_exhausted') stopReason = 'time_budget_exhausted';
|
|
191
|
+
return true;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Robots and sitemap topology are collected first so sitemap-only pages share
|
|
195
|
+
// the same bounded, deduplicated frontier as pages found through HTML links.
|
|
196
|
+
let robotsText = null;
|
|
197
|
+
if (!timeBudgetStopped()) {
|
|
79
198
|
try {
|
|
80
|
-
const
|
|
199
|
+
const robotsUrl = new URL('/robots.txt', origin).href;
|
|
200
|
+
const response = await fetcher(robotsUrl, { userAgent, maxBodyBytes: robotsMaxBytes });
|
|
201
|
+
if (response.status === 200) robotsText = response.body;
|
|
202
|
+
} catch {
|
|
203
|
+
// Missing robots remains represented by robotsText=null for existing detectors.
|
|
204
|
+
}
|
|
205
|
+
timeBudgetStopped();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const declaredSitemaps = robotsText != null ? parseRobots(robotsText).sitemaps : [];
|
|
209
|
+
const sitemapEntries = declaredSitemaps.length
|
|
210
|
+
? declaredSitemaps
|
|
211
|
+
: [new URL('/sitemap.xml', origin).href];
|
|
212
|
+
const sitemapTopology = await collectSitemapTopology(sitemapEntries, {
|
|
213
|
+
fetcher,
|
|
214
|
+
maxDepth: sitemapMaxDepth,
|
|
215
|
+
maxDocuments: sitemapMaxDocuments,
|
|
216
|
+
maxUncompressedBytes: sitemapMaxUncompressedBytes,
|
|
217
|
+
maxTotalUncompressedBytes: sitemapMaxTotalUncompressedBytes,
|
|
218
|
+
maxDiscoveredUrls: sitemapMaxDiscoveredUrls,
|
|
219
|
+
maxQueuedDocuments: sitemapMaxQueuedDocuments,
|
|
220
|
+
maxRawEntries: sitemapMaxRawEntries,
|
|
221
|
+
maxXmlTokens: sitemapMaxXmlTokens,
|
|
222
|
+
sitemapMaxBytes,
|
|
223
|
+
origin,
|
|
224
|
+
userAgent,
|
|
225
|
+
shouldStop: timeExpired,
|
|
226
|
+
});
|
|
227
|
+
if (sitemapTopology.stop_reasons.includes('time_budget_exhausted')) timeBudgetStopped();
|
|
228
|
+
for (const error of sitemapTopology.errors) {
|
|
229
|
+
errors.push(sanitizedSitemapError(error));
|
|
230
|
+
}
|
|
231
|
+
const sitemaps = sitemapTopology.documents
|
|
232
|
+
.filter((document) => document.http_status === 200 && document.parsed?.rootValid)
|
|
233
|
+
.map((document) => ({
|
|
234
|
+
source: document.effective_url,
|
|
235
|
+
parsed: document.parsed,
|
|
236
|
+
status: document.status,
|
|
237
|
+
failure_reason: document.failure_reason,
|
|
238
|
+
}));
|
|
239
|
+
|
|
240
|
+
const seen = new Set();
|
|
241
|
+
const queued = new Set();
|
|
242
|
+
const queue = [];
|
|
243
|
+
const enqueuePage = (rawUrl, source, base = origin) => {
|
|
244
|
+
let url;
|
|
245
|
+
let requestedUrl;
|
|
246
|
+
try {
|
|
247
|
+
const raw = String(rawUrl).trim();
|
|
248
|
+
url = new URL(raw, base);
|
|
249
|
+
url.hash = '';
|
|
250
|
+
const fragmentAt = raw.indexOf('#');
|
|
251
|
+
const withoutFragment = fragmentAt === -1 ? raw : raw.slice(0, fragmentAt);
|
|
252
|
+
if (/^[A-Za-z][A-Za-z\d+.-]*:\/\//.test(withoutFragment)) requestedUrl = withoutFragment;
|
|
253
|
+
else if (withoutFragment.startsWith('//')) requestedUrl = `${new URL(base).protocol}${withoutFragment}`;
|
|
254
|
+
else requestedUrl = url.href;
|
|
255
|
+
} catch {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (url.origin !== origin || isProviderUtilityPath(url.pathname) || seen.has(url.href) || queued.has(url.href)) return;
|
|
259
|
+
coverageLedger.discover(url.href, source, { requested_url: requestedUrl });
|
|
260
|
+
queued.add(url.href);
|
|
261
|
+
queue.push({ url: url.href, requestedUrl, source });
|
|
262
|
+
};
|
|
263
|
+
enqueuePage(startUrl, 'start');
|
|
264
|
+
for (const entry of sitemapTopology.urls) enqueuePage(entry.url, 'sitemap');
|
|
265
|
+
|
|
266
|
+
while (queue.length && stopReason !== 'time_budget_exhausted') {
|
|
267
|
+
if (timeBudgetStopped()) break;
|
|
268
|
+
if (seen.size >= maxPages) {
|
|
269
|
+
stopReason = 'page_budget_exhausted';
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
const next = queue.shift();
|
|
273
|
+
queued.delete(next.url);
|
|
274
|
+
seen.add(next.url);
|
|
275
|
+
coverageLedger.attempt(next.url, { attempt: 1, requested_url: next.requestedUrl });
|
|
276
|
+
try {
|
|
277
|
+
const response = await fetcher(next.url, { userAgent, maxBodyBytes: pageMaxBytes });
|
|
278
|
+
coverageLedger.retrieve(next.url, {
|
|
279
|
+
status: response.status,
|
|
280
|
+
effective_url: response.url,
|
|
281
|
+
body_complete: response.bodyComplete ?? true,
|
|
282
|
+
});
|
|
81
283
|
const page = extractPage({
|
|
82
|
-
url:
|
|
284
|
+
url: response.url, html: response.body, responseBody: response.bodyBytes ?? response.body, status: response.status,
|
|
285
|
+
headers: response.headers, redirectChain: response.redirectChain,
|
|
83
286
|
});
|
|
84
|
-
page
|
|
287
|
+
attachResourceMetadata(page, {
|
|
288
|
+
requestedUrl: next.requestedUrl,
|
|
289
|
+
bodyComplete: response.bodyComplete ?? true,
|
|
290
|
+
fetchAttempts: response.attempts ?? [],
|
|
291
|
+
});
|
|
292
|
+
page.crawlUrl = next.url;
|
|
293
|
+
if (page.resourceValidity?.state === 'valid_resource') {
|
|
294
|
+
coverageLedger.classify(next.url, {
|
|
295
|
+
state: 'valid_resource',
|
|
296
|
+
resource_id: page.urlIdentity?.resource_id,
|
|
297
|
+
requested_url: page.urlIdentity?.requested?.url ?? page.requestedUrl ?? null,
|
|
298
|
+
effective_url: page.urlIdentity?.effective?.url ?? page.url ?? null,
|
|
299
|
+
canonical_url: page.urlIdentity?.canonical?.url ?? null,
|
|
300
|
+
url_identity: page.urlIdentity,
|
|
301
|
+
response_body_hash: page.response_body_hash,
|
|
302
|
+
extracted_text_hash: page.extracted_text_hash,
|
|
303
|
+
structured_data_hash: page.structured_data_hash,
|
|
304
|
+
evidence_hash: page.evidence_hash,
|
|
305
|
+
artifact_hash: page.artifact_hash,
|
|
306
|
+
});
|
|
307
|
+
} else {
|
|
308
|
+
coverageLedger.markIndeterminate(next.url, {
|
|
309
|
+
reason_codes: page.resourceValidity?.reason_codes || ['resource_invalid'],
|
|
310
|
+
resource_validity: page.resourceValidity?.state || 'indeterminate',
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
page.discoverySource = next.source;
|
|
85
314
|
pages.push(page);
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (u.origin === origin && !isProviderUtilityPath(u.pathname) && !seen.has(u.href)) queue.push(u.href);
|
|
92
|
-
} catch { /* unresolvable href — surfaced by LINK detectors */ }
|
|
315
|
+
if (timeBudgetStopped()) break;
|
|
316
|
+
if (String(response.headers['content-type'] || '').includes('text/html')) {
|
|
317
|
+
for (const link of page.links) {
|
|
318
|
+
if (timeBudgetStopped()) break;
|
|
319
|
+
enqueuePage(link.href, 'link', response.url);
|
|
93
320
|
}
|
|
94
321
|
}
|
|
95
|
-
|
|
96
|
-
|
|
322
|
+
if (stopReason === 'time_budget_exhausted') break;
|
|
323
|
+
} catch (error) {
|
|
324
|
+
coverageLedger.fail(next.url, { reason: error?.code || 'FETCH_ERROR' });
|
|
325
|
+
errors.push(sanitizedFetchError(next.url, error));
|
|
97
326
|
}
|
|
98
327
|
}
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
try {
|
|
102
|
-
const r = await fetcher(new URL('/robots.txt', origin).href, { userAgent, maxBodyBytes: robotsMaxBytes });
|
|
103
|
-
if (r.status === 200) robotsText = r.body;
|
|
104
|
-
} catch { /* recorded as missing robots */ }
|
|
105
|
-
const smUrls = robotsText ? parseRobots(robotsText).sitemaps : [new URL('/sitemap.xml', origin).href];
|
|
106
|
-
for (const sm of smUrls) {
|
|
107
|
-
try {
|
|
108
|
-
const sitemapUrl = new URL(sm, origin);
|
|
109
|
-
if (sitemapUrl.origin !== origin) {
|
|
110
|
-
errors.push(`${sitemapUrl.href}: sitemap URL leaves audited origin`);
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
const r = await fetcher(sitemapUrl.href, { userAgent, maxBodyBytes: sitemapMaxBytes });
|
|
114
|
-
if (r.status === 200) sitemaps.push({ source: sm, parsed: parseSitemap(r.body) });
|
|
115
|
-
} catch { /* absence handled by TECH detectors */ }
|
|
116
|
-
}
|
|
117
|
-
const pendingUrls = [...new Set(queue.map((url) => url.replace(/#.*$/, '')).filter((url) => !seen.has(url)))];
|
|
328
|
+
if (stopReason === 'frontier_exhausted') timeBudgetStopped();
|
|
329
|
+
const pendingUrls = queue.map((entry) => entry.url).filter((url) => !seen.has(url));
|
|
118
330
|
const crawl = {
|
|
119
331
|
maxPages,
|
|
332
|
+
timeBudgetSeconds,
|
|
333
|
+
stopReason,
|
|
120
334
|
pagesFetched: pages.length,
|
|
121
|
-
truncated:
|
|
335
|
+
truncated: stopReason === 'page_budget_exhausted' || stopReason === 'time_budget_exhausted',
|
|
122
336
|
pendingUrlCount: pendingUrls.length,
|
|
123
337
|
pendingUrls,
|
|
338
|
+
sitemapStopReasons: [...sitemapTopology.stop_reasons],
|
|
339
|
+
sitemapTopology: {
|
|
340
|
+
status: sitemapTopology.status,
|
|
341
|
+
stopReasons: [...sitemapTopology.stop_reasons],
|
|
342
|
+
limitations: [...sitemapTopology.limitations],
|
|
343
|
+
},
|
|
124
344
|
};
|
|
125
345
|
const site = assembleSite({ baseUrl: origin, pages, robotsText, sitemaps, transport: {}, mode: 'url', location: startUrl, crawl });
|
|
346
|
+
site.coverageLedger = coverageLedger;
|
|
347
|
+
site.coverageOptions = {
|
|
348
|
+
discoveryStatus: sitemapTopology.status,
|
|
349
|
+
discoveryMethods: ['start', 'link', 'sitemap'],
|
|
350
|
+
discoveryLimitations: [...sitemapTopology.limitations],
|
|
351
|
+
limitations: [...sitemapTopology.limitations],
|
|
352
|
+
};
|
|
353
|
+
site.previewCoverage = () => coverageLedger.preview(stopReason, site.coverageOptions);
|
|
354
|
+
site.finalizeCoverage = ({ evaluated = true } = {}) => {
|
|
355
|
+
if (evaluated) {
|
|
356
|
+
for (const page of pages) {
|
|
357
|
+
if (page.resourceValidity?.state === 'valid_resource') {
|
|
358
|
+
coverageLedger.evaluate(page.crawlUrl || page.url, {
|
|
359
|
+
evaluator: 'audit-detectors',
|
|
360
|
+
detector_scope: 'selected',
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
for (const page of pages) {
|
|
366
|
+
if (page.resourceValidity?.state === 'valid_resource') {
|
|
367
|
+
coverageLedger.markValidButUnevaluated(page.crawlUrl || page.url, { reason: 'not_evaluated' });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return coverageLedger.finalize(stopReason, site.coverageOptions);
|
|
372
|
+
};
|
|
126
373
|
site.fetchErrors = errors;
|
|
374
|
+
site.sitemapTopology = sitemapTopology;
|
|
127
375
|
return site;
|
|
128
376
|
}
|
|
129
377
|
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
} from '../shared/htmlEscape.js';
|
|
30
30
|
import { extractHostname } from '../shared/domainUtils.js';
|
|
31
31
|
import { validateAgainst } from '../shared/schemaValidator.js';
|
|
32
|
+
import { assertEpistemicLanguage, hasVerifiedEvidenceScope } from '../shared/epistemicLanguage.js';
|
|
32
33
|
|
|
33
34
|
function formatVal(v, suffix = '') {
|
|
34
35
|
if (v === null || v === undefined) return 'NOT OBSERVED';
|
|
@@ -529,6 +530,13 @@ export async function buildExecutiveCroReport(root, options = {}) {
|
|
|
529
530
|
source_findings_count: findings.length,
|
|
530
531
|
integrity_hash: resolved.integrity_hash,
|
|
531
532
|
synthetic_evidence: isSample,
|
|
533
|
+
package_verified: resolved.package_verified === true,
|
|
534
|
+
integrity_mode: resolved.integrity_mode || 'unknown',
|
|
535
|
+
legacy: resolved.legacy === true,
|
|
536
|
+
coverage_status: resolved.coverage_status || 'indeterminate',
|
|
537
|
+
determination_status: resolved.determination_status || 'indeterminate',
|
|
538
|
+
evaluated: resolved.coverage?.populations?.evaluated ?? null,
|
|
539
|
+
eligible: resolved.coverage?.populations?.eligible ?? null,
|
|
532
540
|
};
|
|
533
541
|
|
|
534
542
|
const report = {
|
|
@@ -542,7 +550,7 @@ export async function buildExecutiveCroReport(root, options = {}) {
|
|
|
542
550
|
epistemological_framework: {
|
|
543
551
|
observation: 'Raw empirical facts measured directly from DOM geometry, HTTP status, or analytics telemetry',
|
|
544
552
|
hypothesis: 'Testable causal explanations predicting why the observed friction occurs',
|
|
545
|
-
causation: '
|
|
553
|
+
causation: 'No current causal outcome is established; controlled A/B experiment evidence is required',
|
|
546
554
|
unknowns: 'Gaps in telemetry or attribution that require further instrumentation before forming hypotheses',
|
|
547
555
|
},
|
|
548
556
|
pillars: {
|
|
@@ -588,7 +596,11 @@ export async function buildExecutiveCroReport(root, options = {}) {
|
|
|
588
596
|
/**
|
|
589
597
|
* Render CRO Executive Report as GitHub-flavored Markdown
|
|
590
598
|
*/
|
|
591
|
-
export function renderCroReportMarkdown(report) {
|
|
599
|
+
export function renderCroReportMarkdown(report, evidenceContext = null) {
|
|
600
|
+
const context = evidenceContext || report.generation_provenance || {};
|
|
601
|
+
const verifiedScope = hasVerifiedEvidenceScope(context);
|
|
602
|
+
const causalEvidenceVerified = verifiedScope && context.causal_evidence_verified === true;
|
|
603
|
+
const evidenceRegisterTitle = `${verifiedScope ? 'Verified Conversion Evidence Register' : 'Conversion Evidence Register (scope-limited; verified status not established)'}`;
|
|
592
604
|
const p = report.pillars;
|
|
593
605
|
const ds = report.decision_summary;
|
|
594
606
|
const lines = [
|
|
@@ -597,7 +609,7 @@ export function renderCroReportMarkdown(report) {
|
|
|
597
609
|
`- **Client / Property**: \`${sanitizeForMarkdown(report.client_name)}\` (\`${sanitizeForMarkdown(report.target_domain)}\`)`,
|
|
598
610
|
`- **Generated At**: \`${report.generated_at}\``,
|
|
599
611
|
`- **Conversion Readiness Index**: **${report.overall_conversion_readiness} / 100**`,
|
|
600
|
-
`- **Evidence Register Traceability**: ${report.evidence_register.length}
|
|
612
|
+
`- **Evidence Register Traceability**: ${report.evidence_register.length} evidence references (authority and scope recorded below)`,
|
|
601
613
|
`- **Generation Mode**: \`${report.generation_provenance.generation_mode}\`${report.generation_provenance.synthetic_evidence ? ' *(Synthetic Demo Evidence)*' : ''}`,
|
|
602
614
|
``,
|
|
603
615
|
`> **Scientific Epistemology & Governance Notice**: Citable enforces a strict separation between **Observable Facts**, **Predictive Hypotheses**, and **Causal Evidence**. In adherence to empirical governance, **no conversion rates, ARR lift, or metric improvements are guaranteed**.`,
|
|
@@ -613,8 +625,8 @@ export function renderCroReportMarkdown(report) {
|
|
|
613
625
|
`*(Testable causal predictions requiring validation under controlled experimentation)*`,
|
|
614
626
|
...ds.evidence_supported_hypotheses.map((h) => `- [ ] **[HYPOTHESIS]** ${sanitizeForMarkdown(h)}`),
|
|
615
627
|
``,
|
|
616
|
-
`### 3. Causal Findings (Verified Under Controlled Experiments)`,
|
|
617
|
-
`*(Demonstrated metric shifts under A/B testing with Sample Ratio Mismatch guardrails)*`,
|
|
628
|
+
`### 3. ${causalEvidenceVerified ? 'Causal Findings (Verified Under Controlled Experiments)' : 'Causal Findings (Causal Evidence Not Established)'}`,
|
|
629
|
+
`*(${causalEvidenceVerified ? 'Demonstrated metric shifts under A/B testing with Sample Ratio Mismatch guardrails' : 'No current causal outcome is established from this report; controlled experiment evidence is required'})*`,
|
|
618
630
|
...ds.causal_findings.map((c) => `- [=] **[CAUSAL]** ${sanitizeForMarkdown(c)}`),
|
|
619
631
|
``,
|
|
620
632
|
`### 4. Critical Unresolved Unknowns (Measurement Gaps)`,
|
|
@@ -694,7 +706,7 @@ export function renderCroReportMarkdown(report) {
|
|
|
694
706
|
`- **Evidence Link**: \`${p[15].data.evidence_ref}\``,
|
|
695
707
|
...(p[15].data.top_customer_objections || []).map((o) => `- "${sanitizeForMarkdown(o)}"`),
|
|
696
708
|
``,
|
|
697
|
-
`## 15.
|
|
709
|
+
`## 15. ${evidenceRegisterTitle}`,
|
|
698
710
|
`| Evidence ID | Source Channel | Methodology | Confidence | Observation Date |`,
|
|
699
711
|
`| :--- | :--- | :--- | :--- | :--- |`,
|
|
700
712
|
...report.evidence_register.map((e) => `| **${e.evidence_id}** | \`${escapeMarkdownTableCell(e.source)}\` | ${escapeMarkdownTableCell((e.methodology || '').slice(0, 50))}... | \`${e.confidence_level}\` | ${e.observation_date} |`),
|
|
@@ -720,16 +732,24 @@ export function renderCroReportMarkdown(report) {
|
|
|
720
732
|
``,
|
|
721
733
|
];
|
|
722
734
|
|
|
723
|
-
|
|
735
|
+
const rendered = lines.join('\n');
|
|
736
|
+
assertEpistemicLanguage(rendered, { ...context, package_verified: context.package_verified === true });
|
|
737
|
+
return rendered;
|
|
724
738
|
}
|
|
725
739
|
|
|
726
740
|
/**
|
|
727
741
|
* Render CRO Executive Report as Standalone Enterprise HTML
|
|
728
742
|
*/
|
|
729
|
-
export function renderCroReportHtml(report) {
|
|
743
|
+
export function renderCroReportHtml(report, evidenceContext = null) {
|
|
730
744
|
const p = report.pillars;
|
|
731
745
|
const ds = report.decision_summary;
|
|
732
|
-
|
|
746
|
+
const context = evidenceContext || report.generation_provenance || {};
|
|
747
|
+
const verifiedScope = hasVerifiedEvidenceScope(context);
|
|
748
|
+
const causalEvidenceVerified = verifiedScope && context.causal_evidence_verified === true;
|
|
749
|
+
const evidenceRegisterTitle = verifiedScope
|
|
750
|
+
? 'Verified Conversion Evidence Register'
|
|
751
|
+
: 'Conversion Evidence Register (scope-limited; verified status not established)';
|
|
752
|
+
const rendered = `<!DOCTYPE html>
|
|
733
753
|
<html lang="en">
|
|
734
754
|
<head>
|
|
735
755
|
<meta charset="UTF-8">
|
|
@@ -817,13 +837,13 @@ export function renderCroReportHtml(report) {
|
|
|
817
837
|
|
|
818
838
|
<div class="epistemic-box" style="border-left: 4px solid var(--accent-light);">
|
|
819
839
|
<span class="epistemic-tag badge-causal">Causal Findings</span>
|
|
820
|
-
<p style="font-size:12px; color:var(--muted); margin: 0 0 8px 0;"
|
|
840
|
+
<p style="font-size:12px; color:var(--muted); margin: 0 0 8px 0;">${causalEvidenceVerified ? 'Statistically verified outcomes under controlled A/B experiments with SRM checks.' : 'Causal evidence is not established; controlled A/B experiment evidence is required.'}</p>
|
|
821
841
|
<ul>
|
|
822
842
|
${ds.causal_findings.map((c) => `<li>${escapeHtml(c)}</li>`).join('')}
|
|
823
843
|
</ul>
|
|
824
844
|
</div>
|
|
825
845
|
|
|
826
|
-
<h2 class="section-title"
|
|
846
|
+
<h2 class="section-title">${evidenceRegisterTitle} (${report.evidence_register.length} Observations)</h2>
|
|
827
847
|
<table>
|
|
828
848
|
<thead>
|
|
829
849
|
<tr>
|
|
@@ -858,6 +878,8 @@ export function renderCroReportHtml(report) {
|
|
|
858
878
|
</div>
|
|
859
879
|
</body>
|
|
860
880
|
</html>`;
|
|
881
|
+
assertEpistemicLanguage(rendered, { ...context, package_verified: context.package_verified === true });
|
|
882
|
+
return rendered;
|
|
861
883
|
}
|
|
862
884
|
|
|
863
885
|
/**
|
|
@@ -865,15 +887,16 @@ export function renderCroReportHtml(report) {
|
|
|
865
887
|
*/
|
|
866
888
|
export async function exportExecutiveCroReport(root, options = {}) {
|
|
867
889
|
const report = await buildExecutiveCroReport(root, options);
|
|
890
|
+
const evidenceContext = report.generation_provenance;
|
|
868
891
|
const format = options.format || 'markdown';
|
|
869
892
|
let content = '';
|
|
870
893
|
|
|
871
894
|
if (format === 'html' || format === 'html-brief') {
|
|
872
|
-
content = renderCroReportHtml(report);
|
|
895
|
+
content = renderCroReportHtml(report, evidenceContext);
|
|
873
896
|
} else if (format === 'json') {
|
|
874
897
|
content = JSON.stringify(report, null, 2);
|
|
875
898
|
} else {
|
|
876
|
-
content = renderCroReportMarkdown(report);
|
|
899
|
+
content = renderCroReportMarkdown(report, evidenceContext);
|
|
877
900
|
}
|
|
878
901
|
|
|
879
902
|
let outputPath = null;
|
|
@@ -3,6 +3,9 @@ import path from "node:path";
|
|
|
3
3
|
import { readJson } from "../shared/io.js";
|
|
4
4
|
import { exportExecutiveSearchReport, buildExecutiveSearchReport, renderSearchReportMarkdown, renderSearchReportHtml } from "./executiveSearchReport.js";
|
|
5
5
|
import { exportExecutiveCroReport, buildExecutiveCroReport, renderCroReportMarkdown, renderCroReportHtml } from "./executiveCroReport.js";
|
|
6
|
+
import { downstreamEnvelope } from '../evidence/downstream.js';
|
|
7
|
+
import { loadVerifiedRun } from '../shared/verifiedRunLoader.js';
|
|
8
|
+
import { assertEpistemicLanguage, evidenceScopeStatement } from '../shared/epistemicLanguage.js';
|
|
6
9
|
|
|
7
10
|
export {
|
|
8
11
|
exportExecutiveSearchReport,
|
|
@@ -78,7 +81,7 @@ export async function exportExecutiveReport(root, runId, {
|
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
const runsDir = path.join(root, ".citable", "runs");
|
|
81
|
-
let targetRun = runId;
|
|
84
|
+
let targetRun = runId && fs.existsSync(path.join(root, '.citable', 'runs', runId)) ? runId : null;
|
|
82
85
|
|
|
83
86
|
if (!targetRun && fs.existsSync(runsDir)) {
|
|
84
87
|
const runs = fs.readdirSync(runsDir).filter((d) => !d.startsWith("."));
|
|
@@ -86,23 +89,41 @@ export async function exportExecutiveReport(root, runId, {
|
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
let summary = {
|
|
89
|
-
counts: { critical: 0, high:
|
|
92
|
+
counts: { critical: 0, high: 0, medium: 0, low: 0 },
|
|
90
93
|
posture: { retrieval: "eligible", support: "suitable", citation: "observed" },
|
|
94
|
+
coverage_status: 'indeterminate',
|
|
95
|
+
determination_status: 'indeterminate',
|
|
91
96
|
};
|
|
92
97
|
let findings = [];
|
|
98
|
+
let coverage = null;
|
|
99
|
+
let packageVerified = false;
|
|
93
100
|
|
|
94
101
|
if (targetRun) {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
102
|
+
const loaded = loadVerifiedRun(path.join(runsDir, targetRun), { requireCompletedExecution: false, allowLegacy: true, requireCoverage: false });
|
|
103
|
+
summary = loaded.summary || summary;
|
|
104
|
+
findings = loaded.findings;
|
|
105
|
+
coverage = loaded.coverage;
|
|
106
|
+
packageVerified = loaded.verified === true && loaded.integrity_mode === 'sealed';
|
|
107
|
+
summary.coverage_status = loaded.coverage_status;
|
|
108
|
+
summary.determination_status = loaded.determination_status;
|
|
103
109
|
}
|
|
104
110
|
|
|
111
|
+
const evidence = downstreamEnvelope(coverage);
|
|
112
|
+
summary.coverage_status = evidence.coverage_status;
|
|
113
|
+
summary.determination_status = evidence.determination_status;
|
|
114
|
+
summary.coverage = {
|
|
115
|
+
...evidence.evidence_scope,
|
|
116
|
+
limitations: evidence.limitations,
|
|
117
|
+
};
|
|
118
|
+
|
|
105
119
|
let content = "";
|
|
120
|
+
const evidenceNotice = `Evidence determination: ${summary.determination_status} (coverage: ${summary.coverage_status}; evaluated ${summary.coverage.evaluated ?? 0} of ${summary.coverage.eligible ?? 'unknown'} eligible). ${summary.coverage.limitations.join('; ') || evidenceScopeStatement({
|
|
121
|
+
coverage_status: summary.coverage_status,
|
|
122
|
+
determination_status: summary.determination_status,
|
|
123
|
+
evaluated: summary.coverage.evaluated,
|
|
124
|
+
eligible: summary.coverage.eligible,
|
|
125
|
+
package_verified: packageVerified,
|
|
126
|
+
})}`;
|
|
106
127
|
|
|
107
128
|
if (format === "html-brief") {
|
|
108
129
|
content = `<!DOCTYPE html>
|
|
@@ -136,6 +157,7 @@ export async function exportExecutiveReport(root, runId, {
|
|
|
136
157
|
<div class="disclosure">
|
|
137
158
|
<strong>Regulatory & Governance Notice:</strong> Citable measures observable retrieval suitability, source extraction fidelity, and external AI citation outcomes. Citable does not guarantee search crawling, indexing, ranking, citation, recommendation, or conversion.
|
|
138
159
|
</div>
|
|
160
|
+
<p class="disclosure"><strong>${evidenceNotice}</strong></p>
|
|
139
161
|
|
|
140
162
|
<div class="kpi-grid">
|
|
141
163
|
<div class="kpi-card"><div>Critical Risks</div><div class="kpi-val ${(summary.counts?.critical || 0) > 0 ? "crit" : "healthy"}">${summary.counts?.critical || 0}</div></div>
|
|
@@ -150,10 +172,7 @@ export async function exportExecutiveReport(root, runId, {
|
|
|
150
172
|
<tr><th>Detector</th><th>Severity</th><th>Summary</th><th>Required Remediation</th></tr>
|
|
151
173
|
</thead>
|
|
152
174
|
<tbody>
|
|
153
|
-
${(findings.length > 0 ? findings : [
|
|
154
|
-
{ detector_id: "CRO-015", severity: "low", summary: "Mobile touch target below 44px on primary CTA", remediation: "Increase button min-height to 48px" },
|
|
155
|
-
{ detector_id: "GEO-008", severity: "high", summary: "Attribution distortion in AI search citation", remediation: "Corroborate claims directly on landing page" },
|
|
156
|
-
]).map((f) => `
|
|
175
|
+
${(findings.length > 0 ? findings : [{ detector_id: "—", severity: "informational", summary: evidenceNotice, remediation: "Collect sufficient evidence before drawing a site-wide determination" }]).map((f) => `
|
|
157
176
|
<tr>
|
|
158
177
|
<td><code>${f.detector_id}</code></td>
|
|
159
178
|
<td><span class="badge badge-${f.severity}">${f.severity.toUpperCase()}</span></td>
|
|
@@ -174,11 +193,13 @@ export async function exportExecutiveReport(root, runId, {
|
|
|
174
193
|
- **High Severity Findings**: ${summary.counts?.high || 0}
|
|
175
194
|
- **Medium Severity Findings**: ${summary.counts?.medium || 0}
|
|
176
195
|
|
|
196
|
+
> **${evidenceNotice}** Findings remain limited to the evaluated evidence scope.
|
|
197
|
+
|
|
177
198
|
> Citable does not guarantee crawling, indexing, ranking, citation, or conversion.
|
|
178
199
|
|
|
179
200
|
<!-- slide -->
|
|
180
201
|
## Prioritized Action Items
|
|
181
|
-
${findings.slice(0, 5).map((f) => `- **[${f.detector_id}]** (${f.severity}): ${f.summary}`).join("\n") ||
|
|
202
|
+
${findings.slice(0, 5).map((f) => `- **[${f.detector_id}]** (${f.severity}): ${f.summary}`).join("\n") || `- No determination established from the available evidence (${summary.determination_status || 'indeterminate'}).`}
|
|
182
203
|
`;
|
|
183
204
|
} else if (format === "slides") {
|
|
184
205
|
const fsa = findings.reduce((acc, f) => {
|
|
@@ -263,7 +284,7 @@ ${findings.slice(0, 5).map((f) => `- **[${f.detector_id}]** (${f.severity}): ${f
|
|
|
263
284
|
<h2>Prioritized Remediations</h2>
|
|
264
285
|
<p>Every finding maps directly to a production-ready, accessible Nebula Component:</p>
|
|
265
286
|
<div style="background:#1e293b; border-radius:0.75rem; padding:1.5rem; border:1px solid #334155;">
|
|
266
|
-
${findings.slice(0, 4).map((f) => `<div style="padding:0.5rem 0; border-bottom:1px solid #334155; font-size:1.1rem;"><strong>[${f.detector_id || "CRO"}]</strong>: ${f.summary || "Conversion action friction"}</div>`).join("") ||
|
|
287
|
+
${findings.slice(0, 4).map((f) => `<div style="padding:0.5rem 0; border-bottom:1px solid #334155; font-size:1.1rem;"><strong>[${f.detector_id || "CRO"}]</strong>: ${f.summary || "Conversion action friction"}</div>`).join("") || `<p>No determination established from the available evidence (${summary.determination_status || 'indeterminate'}).</p>`}
|
|
267
288
|
</div>
|
|
268
289
|
<div style="margin-top:1.5rem; font-family:monospace; background:#0f172a; padding:1rem; border-radius:0.5rem; border:1px solid #334155; color:#38bdf8;">
|
|
269
290
|
$ npx @nebulacomponents/citable remediate --finding CRO-007 --write
|
|
@@ -300,6 +321,14 @@ ${findings.slice(0, 5).map((f) => `- **[${f.detector_id}]** (${f.severity}): ${f
|
|
|
300
321
|
fs.writeFileSync(path.resolve(root, output), content, "utf8");
|
|
301
322
|
}
|
|
302
323
|
|
|
324
|
+
const languageContext = {
|
|
325
|
+
coverage_status: summary.coverage_status,
|
|
326
|
+
determination_status: summary.determination_status,
|
|
327
|
+
evaluated: summary.coverage.evaluated,
|
|
328
|
+
eligible: summary.coverage.eligible,
|
|
329
|
+
package_verified: packageVerified,
|
|
330
|
+
};
|
|
331
|
+
assertEpistemicLanguage(content, languageContext);
|
|
303
332
|
return {
|
|
304
333
|
format,
|
|
305
334
|
client_name: clientName,
|