@nebulacomponents/citable 1.15.1 → 1.17.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 +50 -0
- package/README.md +18 -5
- package/dist/universal/.agents/skills/citable/SKILL.md +51 -1
- package/dist/universal/.agents/skills/citable/VERSION +1 -1
- package/dist/universal/.agents/skills/citable/commands/README.md +3 -1
- package/dist/universal/.agents/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.agents/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.agents/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.agents/skills/citable/manifest.json +17 -12
- package/dist/universal/.agents/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.agents/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.agents/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.agents/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.agents/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.agents/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.agents/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.agents/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.claude/agents/citable/manifest.json +2 -2
- package/dist/universal/.claude/skills/citable/SKILL.md +51 -1
- package/dist/universal/.claude/skills/citable/VERSION +1 -1
- package/dist/universal/.claude/skills/citable/commands/README.md +3 -1
- package/dist/universal/.claude/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.claude/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.claude/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.claude/skills/citable/manifest.json +17 -12
- package/dist/universal/.claude/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.claude/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.claude/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.claude/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.claude/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.claude/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.claude/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.cursor/skills/citable/SKILL.md +51 -1
- package/dist/universal/.cursor/skills/citable/VERSION +1 -1
- package/dist/universal/.cursor/skills/citable/commands/README.md +3 -1
- package/dist/universal/.cursor/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.cursor/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.cursor/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.cursor/skills/citable/manifest.json +17 -12
- package/dist/universal/.cursor/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.cursor/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.cursor/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.cursor/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.cursor/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.cursor/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.cursor/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.gemini/skills/citable/SKILL.md +51 -1
- package/dist/universal/.gemini/skills/citable/VERSION +1 -1
- package/dist/universal/.gemini/skills/citable/commands/README.md +3 -1
- package/dist/universal/.gemini/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.gemini/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.gemini/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.gemini/skills/citable/manifest.json +17 -12
- package/dist/universal/.gemini/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.gemini/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.gemini/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.gemini/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.gemini/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.gemini/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.gemini/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.github/skills/citable/SKILL.md +51 -1
- package/dist/universal/.github/skills/citable/VERSION +1 -1
- package/dist/universal/.github/skills/citable/commands/README.md +3 -1
- package/dist/universal/.github/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.github/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.github/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.github/skills/citable/manifest.json +17 -12
- package/dist/universal/.github/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.github/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.github/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.github/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.github/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.github/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.github/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.github/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.kiro/skills/citable/SKILL.md +51 -1
- package/dist/universal/.kiro/skills/citable/VERSION +1 -1
- package/dist/universal/.kiro/skills/citable/commands/README.md +3 -1
- package/dist/universal/.kiro/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.kiro/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.kiro/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.kiro/skills/citable/manifest.json +17 -12
- package/dist/universal/.kiro/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.kiro/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.kiro/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.kiro/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.kiro/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.kiro/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.kiro/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.opencode/skills/citable/SKILL.md +51 -1
- package/dist/universal/.opencode/skills/citable/VERSION +1 -1
- package/dist/universal/.opencode/skills/citable/commands/README.md +3 -1
- package/dist/universal/.opencode/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.opencode/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.opencode/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.opencode/skills/citable/manifest.json +17 -12
- package/dist/universal/.opencode/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.opencode/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.opencode/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.opencode/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.opencode/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.opencode/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.opencode/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.pi/agent/skills/citable/SKILL.md +51 -1
- package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
- package/dist/universal/.pi/agent/skills/citable/commands/README.md +3 -1
- package/dist/universal/.pi/agent/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.pi/agent/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.pi/agent/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.pi/agent/skills/citable/manifest.json +17 -12
- package/dist/universal/.pi/agent/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.qoder/skills/citable/SKILL.md +51 -1
- package/dist/universal/.qoder/skills/citable/VERSION +1 -1
- package/dist/universal/.qoder/skills/citable/commands/README.md +3 -1
- package/dist/universal/.qoder/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.qoder/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.qoder/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.qoder/skills/citable/manifest.json +17 -12
- package/dist/universal/.qoder/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.qoder/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.qoder/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.qoder/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.qoder/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.qoder/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.qoder/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.rovodev/skills/citable/SKILL.md +51 -1
- package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
- package/dist/universal/.rovodev/skills/citable/commands/README.md +3 -1
- package/dist/universal/.rovodev/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.rovodev/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.rovodev/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.rovodev/skills/citable/manifest.json +17 -12
- package/dist/universal/.rovodev/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.rovodev/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.rovodev/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.rovodev/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.rovodev/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.rovodev/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.rovodev/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.trae/skills/citable/SKILL.md +51 -1
- package/dist/universal/.trae/skills/citable/VERSION +1 -1
- package/dist/universal/.trae/skills/citable/commands/README.md +3 -1
- package/dist/universal/.trae/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.trae/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.trae/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.trae/skills/citable/manifest.json +17 -12
- package/dist/universal/.trae/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.trae/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.trae/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.trae/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.trae/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.trae/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.trae/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.trae-cn/skills/citable/SKILL.md +51 -1
- package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
- package/dist/universal/.trae-cn/skills/citable/commands/README.md +3 -1
- package/dist/universal/.trae-cn/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.trae-cn/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.trae-cn/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.trae-cn/skills/citable/manifest.json +17 -12
- package/dist/universal/.trae-cn/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/manifest.json +208 -148
- package/package.json +1 -1
- package/schemas/cli-output-envelope.schema.json +30 -0
- package/schemas/experiment.schema.json +1 -1
- package/schemas/finding.schema.json +69 -0
- package/schemas/fleet.schema.json +93 -0
- package/schemas/funnel.schema.json +116 -0
- package/schemas/mcp-transport-envelope.schema.json +244 -0
- package/schemas/observation.schema.json +1 -1
- package/schemas/remediation-verification.schema.json +98 -0
- package/skill/SKILL.md +51 -1
- package/skill/commands/README.md +3 -1
- package/skill/commands/governance.md +12 -0
- package/skill/commands/measurement.md +20 -0
- package/skill/commands/reporting.md +16 -0
- package/src/analysis/saliency.js +119 -0
- package/src/cli/index.js +651 -13
- package/src/commands/attributeImpact.js +54 -0
- package/src/commands/audit.js +2 -2
- package/src/commands/ciWorkflow.js +63 -0
- package/src/commands/compatibility.js +205 -0
- package/src/commands/connect.js +3 -0
- package/src/commands/context.js +2 -1
- package/src/commands/edgeRules.js +196 -0
- package/src/commands/edgeSecurity.js +192 -0
- package/src/commands/exceptions.js +241 -0
- package/src/commands/experimentGuardrails.js +213 -0
- package/src/commands/fleet.js +104 -0
- package/src/commands/generateLlmsTxt.js +111 -0
- package/src/commands/goldenCorpus.js +155 -0
- package/src/commands/implementationKit.js +202 -0
- package/src/commands/inspectAeo.js +74 -0
- package/src/commands/inspectCro.js +142 -0
- package/src/commands/inspectGeo.js +52 -0
- package/src/commands/inspectSerp.js +317 -0
- package/src/commands/lintComponents.js +132 -0
- package/src/commands/monitor.js +138 -0
- package/src/commands/observe.js +6 -1
- package/src/commands/planExperiment.js +120 -0
- package/src/commands/previewCro.js +241 -0
- package/src/commands/probe.js +70 -0
- package/src/commands/remediate.js +461 -0
- package/src/commands/reportConsensus.js +48 -0
- package/src/commands/testFunnel.js +107 -0
- package/src/commands/verifyRemediation.js +204 -0
- package/src/commands/visualRegression.js +165 -0
- package/src/components/index.js +556 -0
- package/src/connectors/index.js +6 -1
- package/src/connectors/indexnow.js +431 -0
- package/src/connectors/mcp/allowlist.js +107 -0
- package/src/connectors/mcp/client.js +239 -0
- package/src/connectors/mcp/diagnostics.js +214 -0
- package/src/connectors/mcp/gscServer.js +155 -0
- package/src/connectors/mcp/http.js +130 -0
- package/src/connectors/mcp/index.js +7 -0
- package/src/connectors/mcp/pilot.js +138 -0
- package/src/connectors/mcp/pilotServer.js +80 -0
- package/src/connectors/mcp/sanitizer.js +38 -0
- package/src/connectors/mcp/stdio.js +178 -0
- package/src/crawler/ipRanges.js +161 -0
- package/src/detectors/agent.js +127 -0
- package/src/detectors/ans.js +282 -0
- package/src/detectors/crawl.js +46 -0
- package/src/detectors/cro.js +825 -0
- package/src/detectors/cwv.js +38 -1
- package/src/detectors/evd.js +101 -0
- package/src/detectors/framework.js +32 -2
- package/src/detectors/geoReco.js +408 -0
- package/src/detectors/index.js +4 -2
- package/src/detectors/link.js +112 -1
- package/src/detectors/page.js +101 -0
- package/src/detectors/schemaData.js +529 -0
- package/src/detectors/tech.js +306 -0
- package/src/extractor/page.js +186 -2
- package/src/extractor/site.js +9 -2
- package/src/installer/index.js +45 -2
- package/src/monitoring/alertDelivery.js +101 -7
- package/src/observations/attribution.js +217 -0
- package/src/registries/index.js +14 -0
- package/src/reporting/consensus.js +416 -0
- package/src/reporting/executiveExport.js +244 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { readJson, writeJson, nowIso } from '../shared/io.js';
|
|
5
|
+
import { validateAgainst } from '../shared/schemaValidator.js';
|
|
6
|
+
import { buildContext } from './context.js';
|
|
7
|
+
import { runDetectors } from '../detectors/framework.js';
|
|
8
|
+
import { selectDetectors } from '../detectors/index.js';
|
|
9
|
+
import { remediateCommand } from './remediate.js';
|
|
10
|
+
|
|
11
|
+
const PKG_VERSION = JSON.parse(
|
|
12
|
+
fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf8')
|
|
13
|
+
).version;
|
|
14
|
+
|
|
15
|
+
const RESOLUTION_DEFINITION =
|
|
16
|
+
'Resolved means the named detector no longer reports a finding for the same subject in the re-run. '
|
|
17
|
+
+ 'Detector absence is not a guarantee of conversion, ranking, or outcome; it is a mechanical re-check only.';
|
|
18
|
+
|
|
19
|
+
function sha256(content) {
|
|
20
|
+
return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function subjectKey(subject) {
|
|
24
|
+
return subject?.identifier || subject?.url || JSON.stringify(subject) || 'unknown';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function newVerificationId() {
|
|
28
|
+
return `VR-${nowIso().replace(/\D/g, '')}-${Math.random().toString(36).slice(2, 6)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `citable verify remediation` — closed loop: source-run finding -> patch ->
|
|
33
|
+
* detector re-run -> before/after evidence bundle.
|
|
34
|
+
*
|
|
35
|
+
* Fail-closed: a missing source run, an unloadable re-check target, a refused
|
|
36
|
+
* patch, or a failed detector re-run yields status `blocked`/`patch_refused`,
|
|
37
|
+
* never a silent `verified`.
|
|
38
|
+
*/
|
|
39
|
+
export async function verifyRemediation(root, options = {}) {
|
|
40
|
+
const { run: runId, finding, target, apply = false, subject = null } = options;
|
|
41
|
+
const runDir = path.join(root, '.citable', 'runs', runId || '');
|
|
42
|
+
const limitations = [
|
|
43
|
+
RESOLUTION_DEFINITION,
|
|
44
|
+
'The re-check observes the target as it exists now; unrelated concurrent changes are not attributable to the patch.',
|
|
45
|
+
'Rendering-dependent conditions on URL targets are re-observed only if the original collection method is available.',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const result = {
|
|
49
|
+
verification_id: newVerificationId(),
|
|
50
|
+
timestamp: nowIso(),
|
|
51
|
+
status: 'blocked',
|
|
52
|
+
tool_version: PKG_VERSION,
|
|
53
|
+
repository_commit: null,
|
|
54
|
+
source_run_id: runId || null,
|
|
55
|
+
recheck_run_id: null,
|
|
56
|
+
detector_id: finding ? String(finding).toUpperCase() : null,
|
|
57
|
+
subject: null,
|
|
58
|
+
verdict: { resolved: false, before_finding_ids: [], after_finding_ids: [], new_regressions: [], resolution_definition: RESOLUTION_DEFINITION },
|
|
59
|
+
patch: null,
|
|
60
|
+
provenance: { command: 'verify remediation', recheck_target: target || null, detectors_run: [], detectors_skipped: [], recheck_errors: [], recheck_warnings: [] },
|
|
61
|
+
limitations,
|
|
62
|
+
bundle_dir: null,
|
|
63
|
+
bundle_checksums: {},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (!runId || !fs.existsSync(path.join(runDir, 'findings.json'))) {
|
|
67
|
+
result.provenance.recheck_errors.push(`source run not found: .citable/runs/${runId || '(none)'}`);
|
|
68
|
+
result.limitations.push('required_input: run-id of the immutable source audit containing the finding.');
|
|
69
|
+
return finalizeResult(result);
|
|
70
|
+
}
|
|
71
|
+
if (!finding) {
|
|
72
|
+
result.provenance.recheck_errors.push('missing required input: --finding <detector-id>');
|
|
73
|
+
return finalizeResult(result);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const beforeFindings = readJson(path.join(runDir, 'findings.json'));
|
|
77
|
+
const manifest = fs.existsSync(path.join(runDir, 'manifest.json'))
|
|
78
|
+
? readJson(path.join(runDir, 'manifest.json'))
|
|
79
|
+
: null;
|
|
80
|
+
result.tool_version = manifest?.tool_version || PKG_VERSION;
|
|
81
|
+
result.repository_commit = manifest?.repository_commit ?? null;
|
|
82
|
+
|
|
83
|
+
const matches = beforeFindings.filter((f) =>
|
|
84
|
+
(f.detector_id || '').toUpperCase() === result.detector_id
|
|
85
|
+
&& (!subject || subjectKey(f.subject) === subject));
|
|
86
|
+
if (matches.length === 0) {
|
|
87
|
+
result.provenance.recheck_errors.push(`finding ${result.detector_id} not present in source run ${runId}`);
|
|
88
|
+
result.status = 'blocked';
|
|
89
|
+
return finalizeResult(result);
|
|
90
|
+
}
|
|
91
|
+
result.verdict.before_finding_ids = matches.map((f) => f.finding_id);
|
|
92
|
+
result.subject = { type: matches[0].subject?.type || 'unknown', identifier: subject || subjectKey(matches[0].subject) };
|
|
93
|
+
|
|
94
|
+
// Optional gated patch application (production-safe pipeline from remediate.js)
|
|
95
|
+
if (target && apply) {
|
|
96
|
+
const targetPath = path.resolve(root, target);
|
|
97
|
+
const beforeSha = fs.existsSync(targetPath) ? sha256(fs.readFileSync(targetPath, 'utf8')) : null;
|
|
98
|
+
const patchRes = await remediateCommand(root, { finding: result.detector_id, target, write: true });
|
|
99
|
+
result.patch = {
|
|
100
|
+
target_file: target,
|
|
101
|
+
framework: patchRes.framework?.framework ?? null,
|
|
102
|
+
patcher_class: patchRes.patcher_class ?? null,
|
|
103
|
+
confidence: patchRes.confidence?.score ?? null,
|
|
104
|
+
validation_passed: Boolean(patchRes.validation?.ok),
|
|
105
|
+
applied: Boolean(patchRes.written),
|
|
106
|
+
refusal_reason: patchRes.refusal_reason ?? null,
|
|
107
|
+
rollback_snapshot: patchRes.rollback?.snapshot_file ?? null,
|
|
108
|
+
before_sha256: beforeSha,
|
|
109
|
+
after_sha256: patchRes.written ? sha256(fs.readFileSync(targetPath, 'utf8')) : null,
|
|
110
|
+
};
|
|
111
|
+
if (!patchRes.written) {
|
|
112
|
+
result.status = 'patch_refused';
|
|
113
|
+
result.limitations.push('Patch was refused by the safety gate; no re-check verdict is possible until the patch is applied or refused intentionally.');
|
|
114
|
+
return finalizeResult(result);
|
|
115
|
+
}
|
|
116
|
+
} else if (target) {
|
|
117
|
+
const p = path.resolve(root, target);
|
|
118
|
+
if (fs.existsSync(p)) {
|
|
119
|
+
result.patch = { target_file: target, applied: true, before_sha256: null, after_sha256: sha256(fs.readFileSync(p, 'utf8')) };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Re-run the detector namespace against the original audit target (site dir
|
|
124
|
+
// or URL) — deliberately independent from the patch file target.
|
|
125
|
+
const recheckTarget = options.recheckTarget || manifest?.target?.location || target || null;
|
|
126
|
+
if (!recheckTarget) {
|
|
127
|
+
result.provenance.recheck_errors.push('no re-check target available (original run had no location and no --target supplied)');
|
|
128
|
+
result.limitations.push('required_input: --target <dir|url> matching the source run target.');
|
|
129
|
+
return finalizeResult(result);
|
|
130
|
+
}
|
|
131
|
+
result.provenance.recheck_target = recheckTarget;
|
|
132
|
+
|
|
133
|
+
const namespace = result.detector_id.split('-')[0];
|
|
134
|
+
let ctx;
|
|
135
|
+
try {
|
|
136
|
+
ctx = await buildContext(root, { target: recheckTarget, baseUrl: options.baseUrl, refDate: options.refDate });
|
|
137
|
+
} catch (err) {
|
|
138
|
+
result.provenance.recheck_errors.push(`re-check context failed: ${err.message}`);
|
|
139
|
+
result.limitations.push('required_input: a readable target directory or URL for the detector re-run.');
|
|
140
|
+
return finalizeResult(result);
|
|
141
|
+
}
|
|
142
|
+
if (!ctx.site) {
|
|
143
|
+
result.provenance.recheck_errors.push('re-check target produced no site context; detector re-run impossible (fail closed)');
|
|
144
|
+
return finalizeResult(result);
|
|
145
|
+
}
|
|
146
|
+
result.provenance.recheck_warnings.push(...(ctx.warnings || []));
|
|
147
|
+
|
|
148
|
+
const { findings: afterFindings, detectorsRun, detectorsSkipped, errors } = runDetectors(
|
|
149
|
+
selectDetectors({ namespaces: [namespace] }),
|
|
150
|
+
ctx
|
|
151
|
+
);
|
|
152
|
+
result.provenance.detectors_run = detectorsRun;
|
|
153
|
+
result.provenance.detectors_skipped = detectorsSkipped;
|
|
154
|
+
result.provenance.recheck_errors.push(...errors);
|
|
155
|
+
|
|
156
|
+
const afterMatches = afterFindings.filter((f) =>
|
|
157
|
+
(f.detector_id || '').toUpperCase() === result.detector_id
|
|
158
|
+
&& subjectKey(f.subject) === subjectKey(matches[0].subject));
|
|
159
|
+
result.verdict.after_finding_ids = afterMatches.map((f) => f.finding_id);
|
|
160
|
+
|
|
161
|
+
const beforeKeys = new Set(beforeFindings.map((f) => `${f.detector_id}|${subjectKey(f.subject)}|${f.observation?.summary}`));
|
|
162
|
+
const afterKeys = new Set(afterFindings.map((f) => `${f.detector_id}|${subjectKey(f.subject)}|${f.observation?.summary}`));
|
|
163
|
+
result.verdict.new_regressions = afterFindings
|
|
164
|
+
.filter((f) => ['critical', 'high'].includes(f.classification?.severity))
|
|
165
|
+
.filter((f) => !beforeKeys.has(`${f.detector_id}|${subjectKey(f.subject)}|${f.observation?.summary}`))
|
|
166
|
+
.map((f) => ({ finding_id: f.finding_id, detector_id: f.detector_id, severity: f.classification.severity, summary: f.observation?.summary }));
|
|
167
|
+
|
|
168
|
+
const errorsDuringRecheck = result.provenance.recheck_errors.length > 0;
|
|
169
|
+
if (errorsDuringRecheck) {
|
|
170
|
+
result.status = 'blocked';
|
|
171
|
+
result.verdict.resolved = false;
|
|
172
|
+
result.limitations.push('Detector errors occurred during the re-check; resolution cannot be established from a failed observation.');
|
|
173
|
+
} else if (afterMatches.length === 0) {
|
|
174
|
+
result.status = 'verified';
|
|
175
|
+
result.verdict.resolved = true;
|
|
176
|
+
} else {
|
|
177
|
+
result.status = 'not_resolved';
|
|
178
|
+
result.verdict.resolved = false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return finalizeResult(result, root);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function finalizeResult(result, root = null) {
|
|
185
|
+
const { valid, errors } = validateAgainst('remediation-verification.schema.json', result);
|
|
186
|
+
if (!valid) throw new Error(`verification record invalid: ${errors.join('; ')}`);
|
|
187
|
+
|
|
188
|
+
if (root) {
|
|
189
|
+
const bundleDir = path.join(root, '.citable', 'verification', result.verification_id);
|
|
190
|
+
fs.mkdirSync(bundleDir, { recursive: true });
|
|
191
|
+
writeJson(path.join(bundleDir, 'verification.json'), result);
|
|
192
|
+
writeJson(path.join(bundleDir, 'before-findings.json'), { source_run_id: result.source_run_id, findings: result.verdict.before_finding_ids });
|
|
193
|
+
const checksums = {};
|
|
194
|
+
for (const name of fs.readdirSync(bundleDir)) {
|
|
195
|
+
const p = path.join(bundleDir, name);
|
|
196
|
+
if (fs.statSync(p).isFile()) checksums[name] = sha256(fs.readFileSync(p, 'utf8'));
|
|
197
|
+
}
|
|
198
|
+
result.bundle_dir = path.relative(root, bundleDir);
|
|
199
|
+
result.bundle_checksums = checksums;
|
|
200
|
+
writeJson(path.join(bundleDir, 'verification.json'), result);
|
|
201
|
+
writeJson(path.join(bundleDir, 'checksums.json'), checksums);
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { nowIso } from '../shared/io.js';
|
|
5
|
+
|
|
6
|
+
const PKG_VERSION = JSON.parse(
|
|
7
|
+
fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf8')
|
|
8
|
+
).version;
|
|
9
|
+
|
|
10
|
+
export const VIEWPORT_CLASSES = [
|
|
11
|
+
{ id: 'mobile', width: 375, height: 667 },
|
|
12
|
+
{ id: 'tablet', width: 768, height: 1024 },
|
|
13
|
+
{ id: 'desktop', width: 1280, height: 800 },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export const VARIANT_CLASSES = [
|
|
17
|
+
{ id: 'default' },
|
|
18
|
+
{ id: 'keyboard-focus', note: 'tab to first interactive element; visible focus indicator required' },
|
|
19
|
+
{ id: 'reduced-motion', note: 'emulate prefers-reduced-motion: reduce; no animation may run' },
|
|
20
|
+
{ id: 'text-zoom-200', note: 'browser text zoom 200%; layout must wrap, not clip' },
|
|
21
|
+
{ id: 'long-translation', note: 'substitute longest German strings; layout must wrap, not overflow' },
|
|
22
|
+
{ id: 'narrow-mobile', viewport: 'mobile', width: 320, note: '320px width; split view must collapse to one column' },
|
|
23
|
+
{ id: 'dark', note: 'emulate prefers-color-scheme: dark' },
|
|
24
|
+
{ id: 'light', note: 'emulate prefers-color-scheme: light' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Deterministic layout-contract checks over the preview HTML (or any exported
|
|
29
|
+
* preview artifact). These always run — no browser required — and lock in the
|
|
30
|
+
* properties the screenshot matrix would otherwise only observe.
|
|
31
|
+
*/
|
|
32
|
+
export function visualContractChecks(html) {
|
|
33
|
+
const checks = [];
|
|
34
|
+
const push = (check_id, passed, detail) => checks.push({ check_id, passed, detail });
|
|
35
|
+
|
|
36
|
+
push('viewport-meta', /<meta[^>]*name="viewport"[^>]*>/i.test(html), 'mobile viewport meta present');
|
|
37
|
+
push(
|
|
38
|
+
'three-viewport-classes',
|
|
39
|
+
/375px/.test(html) && /768px/.test(html) && /Desktop/i.test(html),
|
|
40
|
+
'Mobile (375px), Tablet (768px), and Desktop toggles declared'
|
|
41
|
+
);
|
|
42
|
+
push('keyboard-focus-visible', /:focus-visible/i.test(html), 'visible :focus-visible outline defined for interactive elements');
|
|
43
|
+
push('reduced-motion', /prefers-reduced-motion\s*:\s*reduce/i.test(html), 'prefers-reduced-motion: reduce disables transitions and animations');
|
|
44
|
+
push('narrow-mobile-collapse', /max-width:\s*480px[^}]*\{[^}]*grid-template-columns:\s*1fr/i.test(html) || /max-width:\s*480px[\s\S]{0,200}1fr/.test(html), 'split view collapses to one column at narrow mobile widths');
|
|
45
|
+
push('text-zoom-wrap', /overflow-wrap\s*:\s*break-word/.test(html), 'long words wrap instead of clipping (text zoom / translations)');
|
|
46
|
+
push('touch-targets', /min-height:\s*48px/.test(html), 'remediated side declares >= 48px touch targets');
|
|
47
|
+
push('dark-light-preference', /prefers-color-scheme/i.test(html), 'preview chrome adapts to prefers-color-scheme');
|
|
48
|
+
push('viewport-buttons-labelled', /aria-pressed/.test(html), 'viewport toggle state exposed via aria-pressed');
|
|
49
|
+
push('font-size-legible', !/font-size:\s*(?:[0-9]|1[01])px\s*;?\s*[^}]*background:\s*#16a34a/.test(html) && /font-size:\s*16px/.test(html), 'remediated CTAs use 16px baseline');
|
|
50
|
+
|
|
51
|
+
return { ok: checks.every((c) => c.passed), checks };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function playwrightImportable() {
|
|
55
|
+
try {
|
|
56
|
+
execFileSync(process.execPath, ['--input-type=module', '-e', "await import('playwright')"], {
|
|
57
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
58
|
+
timeout: 20000,
|
|
59
|
+
env: { ...process.env, NODE_OPTIONS: '' },
|
|
60
|
+
});
|
|
61
|
+
return true;
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Visual regression runner over the preview artifact.
|
|
69
|
+
*
|
|
70
|
+
* The deterministic contract checks always run. Screenshots run only when
|
|
71
|
+
* playwright is importable AND a browser launches; otherwise the run is
|
|
72
|
+
* recorded as `skipped` with the exact required input — never silently
|
|
73
|
+
* skipped. Screenshots are recorded (hash + bytes); cross-platform pixel
|
|
74
|
+
* comparison is intentionally NOT claimed because rendering is not
|
|
75
|
+
* byte-deterministic across machines.
|
|
76
|
+
*/
|
|
77
|
+
export async function runVisualRegression(root, { previewHtml = null, output = null } = {}) {
|
|
78
|
+
let html = previewHtml;
|
|
79
|
+
if (!html) {
|
|
80
|
+
const { generateCroPreviewHtml } = await import('./previewCro.js');
|
|
81
|
+
html = generateCroPreviewHtml({});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const contract = visualContractChecks(html);
|
|
85
|
+
const cells = [];
|
|
86
|
+
for (const vp of VIEWPORT_CLASSES) {
|
|
87
|
+
for (const variant of VARIANT_CLASSES) {
|
|
88
|
+
if (variant.viewport && variant.viewport !== vp.id) continue;
|
|
89
|
+
cells.push({ viewport: vp.id, width: vp.width, height: vp.height, variant: variant.id, note: variant.note || null });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const result = {
|
|
94
|
+
timestamp: nowIso(),
|
|
95
|
+
tool_version: PKG_VERSION,
|
|
96
|
+
contract,
|
|
97
|
+
matrix_cells: cells,
|
|
98
|
+
screenshots: { status: 'skipped', captured: [], required_input: null },
|
|
99
|
+
limitations: [
|
|
100
|
+
'Layout-contract checks are deterministic string checks; they do not observe actual rendering.',
|
|
101
|
+
'Screenshots are recorded per cell with hashes; no cross-machine pixel comparison is claimed.',
|
|
102
|
+
'A skipped screenshot run is an explicit, recorded state — it never implies the matrix passed.',
|
|
103
|
+
],
|
|
104
|
+
output_dir: null,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (!playwrightImportable()) {
|
|
108
|
+
result.screenshots.required_input = 'optional peer dependency playwright >= 1.50 with a Chromium install';
|
|
109
|
+
if (output) writeReport(root, output, result);
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const outDir = output ? path.resolve(root, output) : path.join(root, '.citable', 'visual', nowIso().replace(/\D/g, '').slice(0, 14));
|
|
114
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
115
|
+
try {
|
|
116
|
+
const pw = await import('playwright');
|
|
117
|
+
const browser = await pw.chromium.launch({ headless: true });
|
|
118
|
+
try {
|
|
119
|
+
const tmp = path.join(outDir, 'preview.html');
|
|
120
|
+
fs.writeFileSync(tmp, html, 'utf8');
|
|
121
|
+
for (const cell of cells) {
|
|
122
|
+
const context = await browser.newContext({
|
|
123
|
+
viewport: { width: cell.width, height: cell.height },
|
|
124
|
+
reducedMotion: cell.variant === 'reduced-motion' ? 'reduce' : 'no-preference',
|
|
125
|
+
colorScheme: cell.variant === 'dark' ? 'dark' : cell.variant === 'light' ? 'light' : 'no-preference',
|
|
126
|
+
});
|
|
127
|
+
const page = await context.newPage();
|
|
128
|
+
await page.goto(`file://${tmp}`);
|
|
129
|
+
if (cell.variant === 'keyboard-focus') await page.keyboard.press('Tab');
|
|
130
|
+
if (cell.variant === 'text-zoom-200') {
|
|
131
|
+
await page.addStyleTag({ content: 'html { font-size: 200%; }' });
|
|
132
|
+
}
|
|
133
|
+
if (cell.variant === 'long-translation') {
|
|
134
|
+
await page.evaluate(() => {
|
|
135
|
+
const apply = (el, t) => { if (el) el.textContent = t; };
|
|
136
|
+
apply(document.querySelector('h2'), 'Skalieren Sie Ihre Unternehmensarchitektur mit unprecedented Geschwindigkeit');
|
|
137
|
+
const btn = document.querySelector('.btn-mock.btn-nebula');
|
|
138
|
+
if (btn) btn.textContent = 'Jetzt kostenlos testen und sofort durchstarten';
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (cell.variant === 'narrow-mobile') await page.setViewportSize({ width: 320, height: 667 });
|
|
142
|
+
const file = path.join(outDir, `${cell.viewport}-${cell.variant}.png`);
|
|
143
|
+
await page.screenshot({ path: file, fullPage: true });
|
|
144
|
+
const { sha256File } = await import('../shared/io.js');
|
|
145
|
+
result.screenshots.captured.push({ cell: `${cell.viewport}/${cell.variant}`, file: path.relative(root, file), sha256: sha256File(file), bytes: fs.statSync(file).size });
|
|
146
|
+
await context.close();
|
|
147
|
+
}
|
|
148
|
+
result.screenshots.status = 'captured';
|
|
149
|
+
} finally {
|
|
150
|
+
await browser.close();
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
result.screenshots.status = 'skipped';
|
|
154
|
+
result.screenshots.required_input = `browser launch failed: ${err.message}`;
|
|
155
|
+
}
|
|
156
|
+
if (output || result.screenshots.status === 'captured') writeReport(root, output || path.relative(root, outDir), result);
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function writeReport(root, output, result) {
|
|
161
|
+
const outFull = path.resolve(root, output, 'visual-regression.json');
|
|
162
|
+
fs.mkdirSync(path.dirname(outFull), { recursive: true });
|
|
163
|
+
fs.writeFileSync(outFull, JSON.stringify(result, null, 2) + '\n', 'utf8');
|
|
164
|
+
result.output_dir = path.dirname(outFull);
|
|
165
|
+
}
|