@nebulacomponents/citable 1.18.2 → 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 +64 -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 +19 -13
- 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/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.agents/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.agents/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.agents/skills/citable/schemas/browser-state-observations.schema.json +97 -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 +19 -13
- 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/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.claude/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.claude/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.claude/skills/citable/schemas/browser-state-observations.schema.json +97 -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 +19 -13
- 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/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.cursor/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.cursor/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.cursor/skills/citable/schemas/browser-state-observations.schema.json +97 -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 +19 -13
- 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/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.gemini/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.gemini/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.gemini/skills/citable/schemas/browser-state-observations.schema.json +97 -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 +19 -13
- 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/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.github/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.github/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.github/skills/citable/schemas/browser-state-observations.schema.json +97 -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 +19 -13
- 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/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.kiro/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.kiro/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.kiro/skills/citable/schemas/browser-state-observations.schema.json +97 -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 +19 -13
- 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/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.opencode/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.opencode/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.opencode/skills/citable/schemas/browser-state-observations.schema.json +97 -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 +19 -13
- 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/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.pi/agent/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/browser-state-observations.schema.json +97 -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 +19 -13
- 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/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.qoder/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.qoder/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.qoder/skills/citable/schemas/browser-state-observations.schema.json +97 -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 +19 -13
- 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/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.rovodev/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.rovodev/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.rovodev/skills/citable/schemas/browser-state-observations.schema.json +97 -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 +19 -13
- 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/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.trae/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.trae/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.trae/skills/citable/schemas/browser-state-observations.schema.json +97 -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 +19 -13
- 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/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.trae-cn/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/browser-state-observations.schema.json +97 -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 +232 -160
- package/package.json +1 -1
- package/schemas/action-plan.schema.json +38 -1
- package/schemas/audit-coverage.schema.json +118 -0
- package/schemas/browser-evidence-plan.schema.json +186 -3
- package/schemas/browser-network-events.schema.json +142 -0
- package/schemas/browser-runtime-events.schema.json +112 -0
- package/schemas/browser-state-observations.schema.json +97 -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/observations/browser/correlation.js +83 -0
- package/src/observations/browser/networkCollector.js +413 -0
- package/src/observations/browser/runtimeEventCollector.js +337 -0
- package/src/observations/browser/sanitizer.js +322 -0
- package/src/observations/browser/stateCollector.js +386 -0
- package/src/observations/browserJourney.js +131 -3
- package/src/reporting/executiveCroReport.js +37 -14
- package/src/reporting/executiveExport.js +45 -16
- package/src/reporting/executiveSearchReport.js +57 -23
- 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/sow/generateSow.js +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { loadVerifiedRun } from '../shared/verifiedRunLoader.js';
|
|
3
4
|
import crypto from 'node:crypto';
|
|
4
5
|
import { readJson, writeJson, nowIso } from '../shared/io.js';
|
|
5
6
|
import { validateAgainst } from '../shared/schemaValidator.js';
|
|
@@ -7,6 +8,7 @@ import { buildContext } from './context.js';
|
|
|
7
8
|
import { runDetectors } from '../detectors/framework.js';
|
|
8
9
|
import { selectDetectors } from '../detectors/index.js';
|
|
9
10
|
import { remediateCommand } from './remediate.js';
|
|
11
|
+
import { assessReobservation, resourceIdentityFor, sameResourceIdentity } from './compareSnapshots.js';
|
|
10
12
|
|
|
11
13
|
const PKG_VERSION = JSON.parse(
|
|
12
14
|
fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf8')
|
|
@@ -20,14 +22,49 @@ function sha256(content) {
|
|
|
20
22
|
return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
+
function findingIdentityKey(finding) {
|
|
26
|
+
const identity = resourceIdentityFor(finding);
|
|
27
|
+
const id = identity.resourceIds.size ? [...identity.resourceIds].sort().join(',')
|
|
28
|
+
: identity.effective || identity.requested || (identity.fallbackUrls.size === 1 ? [...identity.fallbackUrls][0] : JSON.stringify(finding?.subject || {}));
|
|
29
|
+
return `${finding?.detector_id || ''}|${id}`;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
function newVerificationId() {
|
|
28
33
|
return `VR-${nowIso().replace(/\D/g, '')}-${Math.random().toString(36).slice(2, 6)}`;
|
|
29
34
|
}
|
|
30
35
|
|
|
36
|
+
function canonicalJson(value) {
|
|
37
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
|
|
38
|
+
if (value && typeof value === 'object') {
|
|
39
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
|
|
40
|
+
}
|
|
41
|
+
return JSON.stringify(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function recheckComparability(manifest, sourceFinding, afterFinding, ctx, detector) {
|
|
45
|
+
const sourceKind = manifest?.target?.kind || null;
|
|
46
|
+
const recheckKind = ctx.site?.mode || null;
|
|
47
|
+
const sourceDetectorVersion = sourceFinding?.provenance?.detector_version ?? 1;
|
|
48
|
+
const recheckDetectorVersion = afterFinding?.provenance?.detector_version ?? detector?.version ?? 1;
|
|
49
|
+
const sourceViewport = sourceFinding?.provenance?.viewport ?? null;
|
|
50
|
+
const recheckViewport = ctx.viewport ?? null;
|
|
51
|
+
const sourceConfig = manifest?.configuration_hash || null;
|
|
52
|
+
const recheckConfig = sha256(JSON.stringify(ctx.config));
|
|
53
|
+
const dimensions = {
|
|
54
|
+
tool_changed: (manifest?.tool_version || null) !== PKG_VERSION,
|
|
55
|
+
evaluator_changed: sourceDetectorVersion !== recheckDetectorVersion,
|
|
56
|
+
observation_method_changed: sourceKind !== recheckKind
|
|
57
|
+
|| canonicalJson(sourceViewport) !== canonicalJson(recheckViewport),
|
|
58
|
+
configuration_changed: sourceConfig !== recheckConfig,
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
comparable: !Object.values(dimensions).some(Boolean),
|
|
62
|
+
dimensions,
|
|
63
|
+
source: { tool_version: manifest?.tool_version || null, evaluator_version: sourceDetectorVersion, target_kind: sourceKind, configuration_hash: sourceConfig },
|
|
64
|
+
recheck: { tool_version: PKG_VERSION, evaluator_version: recheckDetectorVersion, target_kind: recheckKind, configuration_hash: recheckConfig },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
31
68
|
/**
|
|
32
69
|
* `citable verify remediation` — closed loop: source-run finding -> patch ->
|
|
33
70
|
* detector re-run -> before/after evidence bundle.
|
|
@@ -37,7 +74,7 @@ function newVerificationId() {
|
|
|
37
74
|
* never a silent `verified`.
|
|
38
75
|
*/
|
|
39
76
|
export async function verifyRemediation(root, options = {}) {
|
|
40
|
-
const { run: runId, finding, target, apply = false, subject = null } = options;
|
|
77
|
+
const { run: runId, finding, target, apply = false, subject = null, viewport = null } = options;
|
|
41
78
|
const runDir = path.join(root, '.citable', 'runs', runId || '');
|
|
42
79
|
const limitations = [
|
|
43
80
|
RESOLUTION_DEFINITION,
|
|
@@ -55,7 +92,7 @@ export async function verifyRemediation(root, options = {}) {
|
|
|
55
92
|
recheck_run_id: null,
|
|
56
93
|
detector_id: finding ? String(finding).toUpperCase() : null,
|
|
57
94
|
subject: null,
|
|
58
|
-
verdict: { resolved: false, before_finding_ids: [], after_finding_ids: [], new_regressions: [], resolution_definition: RESOLUTION_DEFINITION },
|
|
95
|
+
verdict: { resolved: false, comparison_state: 'indeterminate', before_finding_ids: [], after_finding_ids: [], new_regressions: [], resolution_definition: RESOLUTION_DEFINITION },
|
|
59
96
|
patch: null,
|
|
60
97
|
provenance: { command: 'verify remediation', recheck_target: target || null, detectors_run: [], detectors_skipped: [], recheck_errors: [], recheck_warnings: [] },
|
|
61
98
|
limitations,
|
|
@@ -73,23 +110,23 @@ export async function verifyRemediation(root, options = {}) {
|
|
|
73
110
|
return finalizeResult(result);
|
|
74
111
|
}
|
|
75
112
|
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
: null;
|
|
113
|
+
const source = loadVerifiedRun(runDir, { requireCompletedExecution: false, allowLegacy: true, requireCoverage: false });
|
|
114
|
+
const beforeFindings = source.findings;
|
|
115
|
+
const manifest = source.manifest;
|
|
80
116
|
result.tool_version = manifest?.tool_version || PKG_VERSION;
|
|
81
117
|
result.repository_commit = manifest?.repository_commit ?? null;
|
|
82
118
|
|
|
119
|
+
const requestedSubject = subject ? { subject: { identifier: subject } } : null;
|
|
83
120
|
const matches = beforeFindings.filter((f) =>
|
|
84
121
|
(f.detector_id || '').toUpperCase() === result.detector_id
|
|
85
|
-
&& (!
|
|
122
|
+
&& (!requestedSubject || sameResourceIdentity(f, requestedSubject)));
|
|
86
123
|
if (matches.length === 0) {
|
|
87
124
|
result.provenance.recheck_errors.push(`finding ${result.detector_id} not present in source run ${runId}`);
|
|
88
125
|
result.status = 'blocked';
|
|
89
126
|
return finalizeResult(result);
|
|
90
127
|
}
|
|
91
128
|
result.verdict.before_finding_ids = matches.map((f) => f.finding_id);
|
|
92
|
-
result.subject = { type: matches[0].subject?.type || 'unknown', identifier: subject ||
|
|
129
|
+
result.subject = { type: matches[0].subject?.type || 'unknown', identifier: subject || matches[0].subject?.identifier || matches[0].subject?.url || 'unknown' };
|
|
93
130
|
|
|
94
131
|
// Optional gated patch application (production-safe pipeline from remediate.js)
|
|
95
132
|
if (target && apply) {
|
|
@@ -133,7 +170,7 @@ export async function verifyRemediation(root, options = {}) {
|
|
|
133
170
|
const namespace = result.detector_id.split('-')[0];
|
|
134
171
|
let ctx;
|
|
135
172
|
try {
|
|
136
|
-
ctx = await buildContext(root, { target: recheckTarget, baseUrl: options.baseUrl, refDate: options.refDate });
|
|
173
|
+
ctx = await buildContext(root, { target: recheckTarget, baseUrl: options.baseUrl, refDate: options.refDate, viewport });
|
|
137
174
|
} catch (err) {
|
|
138
175
|
result.provenance.recheck_errors.push(`re-check context failed: ${err.message}`);
|
|
139
176
|
result.limitations.push('required_input: a readable target directory or URL for the detector re-run.');
|
|
@@ -155,24 +192,62 @@ export async function verifyRemediation(root, options = {}) {
|
|
|
155
192
|
|
|
156
193
|
const afterMatches = afterFindings.filter((f) =>
|
|
157
194
|
(f.detector_id || '').toUpperCase() === result.detector_id
|
|
158
|
-
&&
|
|
195
|
+
&& sameResourceIdentity(f, matches[0]));
|
|
159
196
|
result.verdict.after_finding_ids = afterMatches.map((f) => f.finding_id);
|
|
160
197
|
|
|
161
|
-
const beforeKeys = new Set(beforeFindings.map((f) => `${
|
|
162
|
-
const afterKeys = new Set(afterFindings.map((f) => `${
|
|
198
|
+
const beforeKeys = new Set(beforeFindings.map((f) => `${findingIdentityKey(f)}|${f.observation?.summary}`));
|
|
199
|
+
const afterKeys = new Set(afterFindings.map((f) => `${findingIdentityKey(f)}|${f.observation?.summary}`));
|
|
163
200
|
result.verdict.new_regressions = afterFindings
|
|
164
201
|
.filter((f) => ['critical', 'high'].includes(f.classification?.severity))
|
|
165
|
-
.filter((f) => !beforeKeys.has(`${
|
|
202
|
+
.filter((f) => !beforeKeys.has(`${findingIdentityKey(f)}|${f.observation?.summary}`))
|
|
166
203
|
.map((f) => ({ finding_id: f.finding_id, detector_id: f.detector_id, severity: f.classification.severity, summary: f.observation?.summary }));
|
|
167
204
|
|
|
205
|
+
const detectorDefinition = selectDetectors({ namespaces: [namespace] })
|
|
206
|
+
.find((candidate) => candidate.id === result.detector_id);
|
|
207
|
+
const comparability = recheckComparability(manifest, matches[0], afterMatches[0], ctx, detectorDefinition);
|
|
208
|
+
|
|
209
|
+
// Finalize the same collection envelope used by audit so detector absence
|
|
210
|
+
// is only resolution when the responsible resource was actually observed.
|
|
211
|
+
let recheckCoverage = null;
|
|
212
|
+
if (ctx.site?.finalizeCoverage) {
|
|
213
|
+
try { recheckCoverage = ctx.site.finalizeCoverage({ evaluated: true }); }
|
|
214
|
+
catch (error) { result.provenance.recheck_errors.push(`coverage finalization failed: ${error.message}`); }
|
|
215
|
+
}
|
|
216
|
+
let reobservation = assessReobservation(matches[0], recheckCoverage);
|
|
217
|
+
// Directory re-checks predate the coverage ledger and intentionally do not
|
|
218
|
+
// manufacture a package artifact. Their concrete page model is still a
|
|
219
|
+
// sufficient page-local observation when the subject is present.
|
|
220
|
+
if (!recheckCoverage && ctx.site?.pages?.some((page) => sameResourceIdentity(matches[0], page))) {
|
|
221
|
+
reobservation = { state: 'resolved', reason: 'page_present_in_recheck_model', resource: null };
|
|
222
|
+
}
|
|
223
|
+
result.comparison = {
|
|
224
|
+
state: comparability.comparable ? reobservation.state : 'not_comparable',
|
|
225
|
+
reason: reobservation.reason,
|
|
226
|
+
coverage_status: recheckCoverage?.coverage_status || 'not_available',
|
|
227
|
+
resource: reobservation.resource?.normalized_url || null,
|
|
228
|
+
comparability,
|
|
229
|
+
};
|
|
230
|
+
result.verdict.comparison_state = comparability.comparable ? reobservation.state : 'not_comparable';
|
|
231
|
+
|
|
168
232
|
const errorsDuringRecheck = result.provenance.recheck_errors.length > 0;
|
|
169
233
|
if (errorsDuringRecheck) {
|
|
170
|
-
result.status = '
|
|
234
|
+
result.status = 'indeterminate';
|
|
235
|
+
result.verdict.resolved = false;
|
|
236
|
+
result.verdict.comparison_state = 'indeterminate';
|
|
237
|
+
result.limitations.push('Detector or collection errors occurred during the re-check; resolution cannot be established from a failed observation.');
|
|
238
|
+
} else if (!comparability.comparable) {
|
|
239
|
+
result.status = 'not_comparable';
|
|
171
240
|
result.verdict.resolved = false;
|
|
172
|
-
result.limitations.push('
|
|
241
|
+
result.limitations.push('The source and re-check evaluator/tool/method/configuration envelopes are not comparable; resolution is not established.');
|
|
173
242
|
} else if (afterMatches.length === 0) {
|
|
174
|
-
|
|
175
|
-
|
|
243
|
+
if (reobservation.state === 'resolved') {
|
|
244
|
+
result.status = 'verified';
|
|
245
|
+
result.verdict.resolved = true;
|
|
246
|
+
} else {
|
|
247
|
+
result.status = reobservation.state === 'indeterminate' ? 'indeterminate' : 'not_reobserved';
|
|
248
|
+
result.verdict.resolved = false;
|
|
249
|
+
result.limitations.push(`The source subject was not sufficiently reobserved: ${reobservation.reason}.`);
|
|
250
|
+
}
|
|
176
251
|
} else {
|
|
177
252
|
result.status = 'not_resolved';
|
|
178
253
|
result.verdict.resolved = false;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { validateAgainst } from '../shared/schemaValidator.js';
|
|
2
|
+
|
|
3
|
+
export const COLLECTION_SCHEMA_VERSION = 1;
|
|
4
|
+
export const COLLECTION_COVERAGE_STATUSES = new Set([
|
|
5
|
+
'complete',
|
|
6
|
+
'truncated',
|
|
7
|
+
'indeterminate',
|
|
8
|
+
'provider_bounded',
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
function nonNegativeInteger(value, name) {
|
|
12
|
+
if (!Number.isInteger(value) || value < 0) throw new TypeError(`${name} must be a non-negative integer`);
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function stringList(value, name) {
|
|
17
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) throw new TypeError(`${name} must be an array of strings`);
|
|
18
|
+
return [...new Set(value)];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build the provider-neutral collection envelope. `items` are the items
|
|
23
|
+
* actually observed, never a provider-reported total or an inferred census.
|
|
24
|
+
*/
|
|
25
|
+
export function collectionResult({
|
|
26
|
+
items = [],
|
|
27
|
+
paginationState = {},
|
|
28
|
+
providerReportedTotal = null,
|
|
29
|
+
continuationState = null,
|
|
30
|
+
limitations = [],
|
|
31
|
+
errors = [],
|
|
32
|
+
providerCompleteness = 'declared',
|
|
33
|
+
} = {}) {
|
|
34
|
+
if (!Array.isArray(items)) throw new TypeError('items must be an array');
|
|
35
|
+
if (!paginationState || typeof paginationState !== 'object' || Array.isArray(paginationState)) throw new TypeError('paginationState must be an object');
|
|
36
|
+
if (providerReportedTotal !== null) nonNegativeInteger(providerReportedTotal, 'providerReportedTotal');
|
|
37
|
+
stringList(limitations, 'limitations');
|
|
38
|
+
stringList(errors, 'errors');
|
|
39
|
+
if (continuationState !== null && (typeof continuationState !== 'object' || Array.isArray(continuationState))) throw new TypeError('continuationState must be an object or null');
|
|
40
|
+
if (!['declared', 'unknown'].includes(providerCompleteness)) throw new TypeError('providerCompleteness must be declared or unknown');
|
|
41
|
+
|
|
42
|
+
const totalErrors = providerReportedTotal !== null && providerReportedTotal < items.length
|
|
43
|
+
? [`provider reported total ${providerReportedTotal}, below retrieved item count ${items.length}`]
|
|
44
|
+
: [];
|
|
45
|
+
const cleanErrors = [...new Set([...errors, ...totalErrors])];
|
|
46
|
+
const contradictoryTotal = providerReportedTotal !== null && providerReportedTotal < items.length;
|
|
47
|
+
const unresolvedTotal = providerReportedTotal !== null && providerReportedTotal > items.length && !continuationState;
|
|
48
|
+
const status = (cleanErrors.length || contradictoryTotal)
|
|
49
|
+
? 'indeterminate'
|
|
50
|
+
: continuationState
|
|
51
|
+
? 'truncated'
|
|
52
|
+
: (unresolvedTotal || providerCompleteness === 'unknown')
|
|
53
|
+
? 'provider_bounded'
|
|
54
|
+
: 'complete';
|
|
55
|
+
const result = {
|
|
56
|
+
schema_version: COLLECTION_SCHEMA_VERSION,
|
|
57
|
+
items,
|
|
58
|
+
pagination_state: paginationState,
|
|
59
|
+
provider_reported_total: providerReportedTotal,
|
|
60
|
+
retrieved_total: items.length,
|
|
61
|
+
coverage_status: status,
|
|
62
|
+
continuation_state: continuationState,
|
|
63
|
+
limitations: [...new Set(limitations)],
|
|
64
|
+
errors: cleanErrors,
|
|
65
|
+
};
|
|
66
|
+
const check = validateAgainst('collection-result.schema.json', result);
|
|
67
|
+
if (!check.valid) throw new TypeError(`invalid collection result: ${check.errors.join('; ')}`);
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function paginationBoundary(context = {}) {
|
|
72
|
+
const value = context.collectionMaxPages ?? context.maxPages ?? 1000;
|
|
73
|
+
if (!Number.isInteger(value) || value < 1 || value > 10000) throw new RangeError('collection max pages must be an integer from 1 to 10000');
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function errorMessage(error) {
|
|
78
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
79
|
+
return message.replace(/Bearer\s+\S+/gi, 'Bearer [redacted]').slice(0, 300);
|
|
80
|
+
}
|
package/src/connectors/ga4.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { providerRequest } from './http.js';
|
|
2
|
+
import { collectionResult, errorMessage, paginationBoundary } from './collectionResult.js';
|
|
2
3
|
|
|
3
4
|
const DATA = 'https://analyticsdata.googleapis.com/v1beta';
|
|
4
5
|
const ADMIN = 'https://analyticsadmin.googleapis.com/v1beta';
|
|
@@ -36,24 +37,58 @@ export const ga4Connector = {
|
|
|
36
37
|
if (!dimensions.includes('landingPagePlusQueryString')) dimensions.push('landingPagePlusQueryString');
|
|
37
38
|
const rows = [];
|
|
38
39
|
const limitations = [];
|
|
40
|
+
const errors = [];
|
|
41
|
+
const items = [];
|
|
42
|
+
const maxPages = paginationBoundary(context);
|
|
43
|
+
let pagesRequested = 0;
|
|
44
|
+
let pagesRetrieved = 0;
|
|
45
|
+
let continuationState = null;
|
|
46
|
+
let providerTotal = null;
|
|
47
|
+
let connectorError = null;
|
|
39
48
|
let offset = 0;
|
|
40
49
|
do {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
pagesRequested += 1;
|
|
51
|
+
let result;
|
|
52
|
+
try {
|
|
53
|
+
result = await providerRequest(`${DATA}/properties/${encodeURIComponent(connection.property_id)}:runReport`, {
|
|
54
|
+
...context, method: 'POST', body: {
|
|
55
|
+
dateRanges: [{ startDate, endDate }], dimensions: dimensions.map((name) => ({ name })),
|
|
56
|
+
metrics: metrics.map((metric) => ({ name: metric.external_name })), limit: '100000', offset: String(offset),
|
|
57
|
+
dimensionFilter: { filter: { fieldName: 'sessionDefaultChannelGroup', stringFilter: { matchType: 'EXACT', value: 'Organic Search' } } },
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
} catch (error) {
|
|
61
|
+
connectorError ||= error;
|
|
62
|
+
errors.push(`offset ${offset}: ${errorMessage(error)}`);
|
|
63
|
+
continuationState = { offset };
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
pagesRetrieved += 1;
|
|
67
|
+
const pageRows = result.rows || [];
|
|
68
|
+
items.push(...pageRows);
|
|
69
|
+
if (Number.isInteger(result.rowCount) && result.rowCount >= 0) providerTotal = result.rowCount;
|
|
70
|
+
for (const row of pageRows) {
|
|
49
71
|
const values = Object.fromEntries(dimensions.map((name, index) => [name === 'landingPagePlusQueryString' ? 'url' : name, row.dimensionValues[index]?.value]));
|
|
50
72
|
const date = values.date?.replace(/^(\d{4})(\d{2})(\d{2})$/, '$1-$2-$3');
|
|
51
73
|
rows.push(...metrics.map((metric, index) => ({ metric, value: Number(row.metricValues[index]?.value), dimensions: values, observed_at: `${date}T00:00:00.000Z` })));
|
|
52
74
|
}
|
|
53
75
|
if (result.metadata?.dataLossFromOtherRow) limitations.push('Provider reports data loss from the (other) row.');
|
|
54
|
-
offset +=
|
|
55
|
-
if (
|
|
76
|
+
offset += pageRows.length;
|
|
77
|
+
if ((result.rowCount === undefined || result.rowCount === null)
|
|
78
|
+
? pageRows.length < 100000 || !pageRows.length
|
|
79
|
+
: offset >= result.rowCount || !pageRows.length) break;
|
|
80
|
+
if (pagesRequested >= maxPages) { continuationState = { offset }; break; }
|
|
56
81
|
} while (true);
|
|
57
|
-
|
|
82
|
+
const providerCompleteness = limitations.some((item) => /data loss/i.test(item)) || providerTotal === null ? 'unknown' : 'declared';
|
|
83
|
+
const collection = collectionResult({
|
|
84
|
+
items,
|
|
85
|
+
paginationState: { pages_requested: pagesRequested, pages_retrieved: pagesRetrieved, boundary: { max_pages: maxPages } },
|
|
86
|
+
providerReportedTotal: providerTotal,
|
|
87
|
+
continuationState,
|
|
88
|
+
providerCompleteness,
|
|
89
|
+
limitations: [...new Set([...limitations, 'GA4 reporting identity, thresholding, attribution, and consent configuration affect results.', 'Sync is filtered to Organic Search sessions.'])],
|
|
90
|
+
errors,
|
|
91
|
+
});
|
|
92
|
+
return { rows, cursor: endDate, limitations: collection.limitations, collection, ...(connectorError ? { connectorError } : {}) };
|
|
58
93
|
},
|
|
59
94
|
};
|
package/src/connectors/gsc.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { providerRequest } from './http.js';
|
|
2
|
+
import { collectionResult, errorMessage, paginationBoundary } from './collectionResult.js';
|
|
2
3
|
|
|
3
4
|
const BASE = 'https://www.googleapis.com/webmasters/v3';
|
|
4
5
|
const METRICS = {
|
|
@@ -25,17 +26,51 @@ export const gscConnector = {
|
|
|
25
26
|
if (!dimensions.includes('date')) dimensions.unshift('date');
|
|
26
27
|
const rows = [];
|
|
27
28
|
let startRow = 0;
|
|
29
|
+
const items = [];
|
|
30
|
+
const errors = [];
|
|
31
|
+
const maxPages = paginationBoundary(context);
|
|
32
|
+
let pagesRequested = 0;
|
|
33
|
+
let pagesRetrieved = 0;
|
|
34
|
+
let continuationState = null;
|
|
35
|
+
let providerTotal = null;
|
|
36
|
+
let connectorError = null;
|
|
28
37
|
do {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
pagesRequested += 1;
|
|
39
|
+
let result;
|
|
40
|
+
try {
|
|
41
|
+
result = await providerRequest(`${BASE}/sites/${encodeURIComponent(connection.property_id)}/searchAnalytics/query`, {
|
|
42
|
+
...context, method: 'POST', body: { startDate, endDate, dimensions, rowLimit: 25000, startRow, dataState: 'final' },
|
|
43
|
+
});
|
|
44
|
+
} catch (error) {
|
|
45
|
+
connectorError ||= error;
|
|
46
|
+
errors.push(`start row ${startRow}: ${errorMessage(error)}`);
|
|
47
|
+
continuationState = { start_row: startRow };
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
pagesRetrieved += 1;
|
|
51
|
+
const pageRows = result.rows || [];
|
|
52
|
+
items.push(...pageRows);
|
|
53
|
+
if (Number.isInteger(result.rowCount) && result.rowCount >= 0) providerTotal = result.rowCount;
|
|
54
|
+
for (const row of pageRows) {
|
|
33
55
|
const values = Object.fromEntries(dimensions.map((name, index) => [name === 'page' ? 'url' : name, row.keys[index]]));
|
|
34
56
|
for (const metric of metrics) rows.push({ metric, value: row[metric.external_name], dimensions: values, observed_at: `${values.date}T00:00:00.000Z` });
|
|
35
57
|
}
|
|
36
|
-
if (
|
|
58
|
+
if (pageRows.length < 25000) break;
|
|
37
59
|
startRow += 25000;
|
|
60
|
+
if (pagesRequested >= maxPages) {
|
|
61
|
+
continuationState = { start_row: startRow };
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
38
64
|
} while (true);
|
|
39
|
-
|
|
65
|
+
const collection = collectionResult({
|
|
66
|
+
items,
|
|
67
|
+
paginationState: { pages_requested: pagesRequested, pages_retrieved: pagesRetrieved, boundary: { max_pages: maxPages } },
|
|
68
|
+
providerReportedTotal: providerTotal,
|
|
69
|
+
continuationState,
|
|
70
|
+
providerCompleteness: 'unknown',
|
|
71
|
+
limitations: ['Search Console privacy filtering and aggregation apply.', 'Search Analytics does not guarantee every data row; the API can return top rows only.', 'Final data can still be revised by the provider.'],
|
|
72
|
+
errors,
|
|
73
|
+
});
|
|
74
|
+
return { rows, cursor: endDate, limitations: collection.limitations, collection, ...(connectorError ? { connectorError } : {}) };
|
|
40
75
|
},
|
|
41
76
|
};
|
package/src/connectors/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { wordpressConnector } from './wordpress.js';
|
|
|
4
4
|
import { webflowConnector } from './webflow.js';
|
|
5
5
|
import { indexnowConnector } from './indexnow.js';
|
|
6
6
|
import { mcpConnector } from './mcp/pilot.js';
|
|
7
|
+
export { collectionResult, paginationBoundary, errorMessage } from './collectionResult.js';
|
|
7
8
|
|
|
8
9
|
const CONNECTORS = new Map([gscConnector, ga4Connector, wordpressConnector, webflowConnector, indexnowConnector, mcpConnector].map((connector) => [connector.provider, connector]));
|
|
9
10
|
|
|
@@ -18,4 +19,3 @@ export function listConnectors() {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export { McpClient, McpStdioTransport, McpHttpTransport, ALLOWED_MCP_SERVERS, isServerAllowlisted, isToolAllowlisted, assertToolAllowed, collectMcpEvidence, mcpConnector, diagnoseMcpTransports } from './mcp/index.js';
|
|
21
|
-
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { providerRequest } from './http.js';
|
|
2
2
|
import { sha256, nowIso } from '../shared/io.js';
|
|
3
|
+
import { collectionResult, errorMessage, paginationBoundary } from './collectionResult.js';
|
|
3
4
|
|
|
4
5
|
const BASE = 'https://api.webflow.com/v2';
|
|
6
|
+
const API_ORIGIN = new URL(BASE).origin;
|
|
5
7
|
|
|
6
8
|
const METRICS = {
|
|
7
9
|
pages: { unit: 'count', value_type: 'integer' },
|
|
@@ -24,6 +26,96 @@ function computeWebflowPayloadHash(payload) {
|
|
|
24
26
|
return sha256(JSON.stringify(canonical));
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
function safeContinuation(url, { preserveOrigin = true } = {}) {
|
|
30
|
+
try {
|
|
31
|
+
const parsed = new URL(url);
|
|
32
|
+
return { ...(preserveOrigin ? { url: `${parsed.origin}${parsed.pathname}` } : { path: parsed.pathname }), query_redacted: parsed.search.length > 0 };
|
|
33
|
+
} catch {
|
|
34
|
+
return { url: '[invalid continuation]', query_redacted: true };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function collectWebflowList(url, field, context) {
|
|
39
|
+
const maxPages = paginationBoundary(context);
|
|
40
|
+
const items = [];
|
|
41
|
+
const errors = [];
|
|
42
|
+
const limitations = [];
|
|
43
|
+
let providerTotal = null;
|
|
44
|
+
let pagesRequested = 0;
|
|
45
|
+
let pagesRetrieved = 0;
|
|
46
|
+
let offset = 0;
|
|
47
|
+
let cursor = null;
|
|
48
|
+
let nextUrl = null;
|
|
49
|
+
let continuationState = null;
|
|
50
|
+
let providerCompleteness = 'declared';
|
|
51
|
+
let connectorError = null;
|
|
52
|
+
while (pagesRequested < maxPages) {
|
|
53
|
+
pagesRequested += 1;
|
|
54
|
+
const query = cursor
|
|
55
|
+
? `?limit=100&cursor=${encodeURIComponent(cursor)}`
|
|
56
|
+
: `?limit=100${offset ? `&offset=${offset}` : ''}`;
|
|
57
|
+
let result;
|
|
58
|
+
try {
|
|
59
|
+
if (nextUrl) {
|
|
60
|
+
let continuation;
|
|
61
|
+
try { continuation = new URL(nextUrl); } catch {
|
|
62
|
+
errors.push(`${field} page ${pagesRequested}: malformed continuation URL`);
|
|
63
|
+
continuationState = { next: safeContinuation(nextUrl), reason: 'invalid_url' };
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
if (continuation.origin !== API_ORIGIN || continuation.protocol !== 'https:') {
|
|
67
|
+
errors.push(`${field} page ${pagesRequested}: continuation origin is outside the Webflow API`);
|
|
68
|
+
continuationState = { next: safeContinuation(nextUrl, { preserveOrigin: false }), reason: 'origin_mismatch' };
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
result = await providerRequest(nextUrl || `${url}${query}`, context);
|
|
73
|
+
const pageItems = Array.isArray(result?.[field]) ? result[field] : [];
|
|
74
|
+
pagesRetrieved += 1;
|
|
75
|
+
items.push(...pageItems);
|
|
76
|
+
const pagination = result.pagination || {};
|
|
77
|
+
const reported = result.total ?? pagination.total ?? pagination.totalCount ?? null;
|
|
78
|
+
if (Number.isInteger(reported) && reported >= 0) providerTotal = reported;
|
|
79
|
+
const next = result.nextCursor ?? pagination.nextCursor ?? result.next?.cursor ?? null;
|
|
80
|
+
const providerNextUrl = typeof result.next === 'string' ? result.next : (typeof pagination.next === 'string' ? pagination.next : null);
|
|
81
|
+
const nextOffset = result.nextOffset ?? pagination.nextOffset ?? null;
|
|
82
|
+
nextUrl = null;
|
|
83
|
+
if (next !== null && (typeof next !== 'string' || !next.trim())) {
|
|
84
|
+
errors.push(`${field} page ${pagesRequested}: malformed continuation cursor`);
|
|
85
|
+
continuationState = { cursor: next };
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
if (providerNextUrl !== null && (!/^https:\/\//i.test(providerNextUrl))) {
|
|
89
|
+
errors.push(`${field} page ${pagesRequested}: malformed continuation URL`);
|
|
90
|
+
continuationState = { next: safeContinuation(providerNextUrl, { preserveOrigin: false }) };
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
if (next) cursor = String(next);
|
|
94
|
+
else if (providerNextUrl) { nextUrl = providerNextUrl; cursor = null; }
|
|
95
|
+
else if (nextOffset !== null && (!Number.isInteger(nextOffset) || nextOffset <= offset)) {
|
|
96
|
+
errors.push(`${field} page ${pagesRequested}: malformed continuation offset`);
|
|
97
|
+
continuationState = { offset: nextOffset };
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
else if (providerTotal !== null && items.length >= providerTotal) break;
|
|
101
|
+
else if (Number.isInteger(nextOffset) && nextOffset >= 0) { offset = nextOffset; cursor = null; }
|
|
102
|
+
else if (pageItems.length >= 100 && (providerTotal === null || items.length < providerTotal)) offset += pageItems.length;
|
|
103
|
+
else { providerCompleteness = 'unknown'; break; }
|
|
104
|
+
if (providerTotal !== null && items.length >= providerTotal) break;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
connectorError ||= error;
|
|
107
|
+
errors.push(`${field} page ${pagesRequested}: ${errorMessage(error)}`);
|
|
108
|
+
continuationState = cursor ? { cursor } : nextUrl ? { next: safeContinuation(nextUrl) } : { offset };
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!continuationState && pagesRequested >= maxPages && (providerTotal === null || items.length < providerTotal)) {
|
|
113
|
+
continuationState = cursor ? { cursor } : nextUrl ? { next: safeContinuation(nextUrl) } : { offset: offset + 100 };
|
|
114
|
+
limitations.push(`Webflow ${field} collection reached the ${maxPages}-page boundary.`);
|
|
115
|
+
}
|
|
116
|
+
return { items, providerTotal, providerCompleteness, connectorError, pagesRequested, pagesRetrieved, continuationState, limitations, errors };
|
|
117
|
+
}
|
|
118
|
+
|
|
27
119
|
export const webflowConnector = {
|
|
28
120
|
provider: 'webflow',
|
|
29
121
|
defaultCredentialEnv: 'WEBFLOW_API_TOKEN',
|
|
@@ -62,10 +154,26 @@ export const webflowConnector = {
|
|
|
62
154
|
const siteId = connection.property_id;
|
|
63
155
|
const rows = [];
|
|
64
156
|
const metricNames = new Set(metrics.map((m) => m.external_name));
|
|
157
|
+
const allItems = [];
|
|
158
|
+
const errors = [];
|
|
159
|
+
const limitations = [];
|
|
160
|
+
let pagesRequested = 0;
|
|
161
|
+
let pagesRetrieved = 0;
|
|
162
|
+
let continuationState = null;
|
|
163
|
+
let providerTotal = 0;
|
|
164
|
+
let providerTotalKnown = true;
|
|
165
|
+
let connectorError = null;
|
|
65
166
|
|
|
66
167
|
if (metricNames.has('pages')) {
|
|
67
|
-
const pageResult = await
|
|
68
|
-
const pages = pageResult.
|
|
168
|
+
const pageResult = await collectWebflowList(`${BASE}/sites/${encodeURIComponent(siteId)}/pages`, 'pages', context);
|
|
169
|
+
const pages = pageResult.items;
|
|
170
|
+
allItems.push(...pages.map((item) => ({ ...item, _collection_type: 'pages' })));
|
|
171
|
+
errors.push(...pageResult.errors); limitations.push(...pageResult.limitations);
|
|
172
|
+
pagesRequested += pageResult.pagesRequested; pagesRetrieved += pageResult.pagesRetrieved;
|
|
173
|
+
continuationState ||= pageResult.continuationState;
|
|
174
|
+
connectorError ||= pageResult.connectorError;
|
|
175
|
+
if (pageResult.providerCompleteness === 'unknown') providerTotalKnown = false;
|
|
176
|
+
if (pageResult.providerTotal === null) providerTotalKnown = false; else providerTotal += pageResult.providerTotal;
|
|
69
177
|
const pageMetric = metrics.find((m) => m.external_name === 'pages');
|
|
70
178
|
rows.push({
|
|
71
179
|
metric: pageMetric,
|
|
@@ -76,8 +184,15 @@ export const webflowConnector = {
|
|
|
76
184
|
}
|
|
77
185
|
|
|
78
186
|
if (metricNames.has('collections') || metricNames.has('items')) {
|
|
79
|
-
const collectionsResult = await
|
|
80
|
-
const collections = collectionsResult.
|
|
187
|
+
const collectionsResult = await collectWebflowList(`${BASE}/sites/${encodeURIComponent(siteId)}/collections`, 'collections', context);
|
|
188
|
+
const collections = collectionsResult.items;
|
|
189
|
+
allItems.push(...collections.map((item) => ({ ...item, _collection_type: 'collections' })));
|
|
190
|
+
errors.push(...collectionsResult.errors); limitations.push(...collectionsResult.limitations);
|
|
191
|
+
pagesRequested += collectionsResult.pagesRequested; pagesRetrieved += collectionsResult.pagesRetrieved;
|
|
192
|
+
continuationState ||= collectionsResult.continuationState;
|
|
193
|
+
connectorError ||= collectionsResult.connectorError;
|
|
194
|
+
if (collectionsResult.providerCompleteness === 'unknown') providerTotalKnown = false;
|
|
195
|
+
if (collectionsResult.providerTotal === null) providerTotalKnown = false; else providerTotal += collectionsResult.providerTotal;
|
|
81
196
|
|
|
82
197
|
if (metricNames.has('collections')) {
|
|
83
198
|
const colMetric = metrics.find((m) => m.external_name === 'collections');
|
|
@@ -92,8 +207,15 @@ export const webflowConnector = {
|
|
|
92
207
|
if (metricNames.has('items')) {
|
|
93
208
|
let totalItems = 0;
|
|
94
209
|
for (const col of collections) {
|
|
95
|
-
const itemsResult = await
|
|
96
|
-
totalItems +=
|
|
210
|
+
const itemsResult = await collectWebflowList(`${BASE}/collections/${encodeURIComponent(col.id)}/items`, 'items', context);
|
|
211
|
+
totalItems += itemsResult.items.length;
|
|
212
|
+
allItems.push(...itemsResult.items.map((item) => ({ ...item, _collection_type: 'items', _collection_id: col.id })));
|
|
213
|
+
errors.push(...itemsResult.errors); limitations.push(...itemsResult.limitations);
|
|
214
|
+
pagesRequested += itemsResult.pagesRequested; pagesRetrieved += itemsResult.pagesRetrieved;
|
|
215
|
+
continuationState ||= itemsResult.continuationState;
|
|
216
|
+
connectorError ||= itemsResult.connectorError;
|
|
217
|
+
if (itemsResult.providerCompleteness === 'unknown') providerTotalKnown = false;
|
|
218
|
+
if (itemsResult.providerTotal === null) providerTotalKnown = false; else providerTotal += itemsResult.providerTotal;
|
|
97
219
|
}
|
|
98
220
|
const itemMetric = metrics.find((m) => m.external_name === 'items');
|
|
99
221
|
rows.push({
|
|
@@ -105,13 +227,21 @@ export const webflowConnector = {
|
|
|
105
227
|
}
|
|
106
228
|
}
|
|
107
229
|
|
|
230
|
+
const collection = collectionResult({
|
|
231
|
+
items: allItems,
|
|
232
|
+
paginationState: { pages_requested: pagesRequested, pages_retrieved: pagesRetrieved, boundary: { max_pages: paginationBoundary(context) } },
|
|
233
|
+
providerReportedTotal: providerTotalKnown ? providerTotal : null,
|
|
234
|
+
continuationState,
|
|
235
|
+
providerCompleteness: providerTotalKnown ? 'declared' : 'unknown',
|
|
236
|
+
limitations: [...new Set([...limitations, 'Webflow API enforces rate limits of 60 requests per minute.', 'Staged CMS changes require site publish to appear on live domains.'])],
|
|
237
|
+
errors,
|
|
238
|
+
});
|
|
108
239
|
return {
|
|
109
240
|
rows,
|
|
110
241
|
cursor: endDate,
|
|
111
|
-
limitations:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
],
|
|
242
|
+
limitations: collection.limitations,
|
|
243
|
+
collection,
|
|
244
|
+
...(connectorError ? { connectorError } : {}),
|
|
115
245
|
};
|
|
116
246
|
},
|
|
117
247
|
|