@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.
Files changed (264) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +18 -5
  3. package/dist/universal/.agents/skills/citable/SKILL.md +51 -1
  4. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  5. package/dist/universal/.agents/skills/citable/commands/README.md +3 -1
  6. package/dist/universal/.agents/skills/citable/commands/governance.md +12 -0
  7. package/dist/universal/.agents/skills/citable/commands/measurement.md +20 -0
  8. package/dist/universal/.agents/skills/citable/commands/reporting.md +16 -0
  9. package/dist/universal/.agents/skills/citable/manifest.json +17 -12
  10. package/dist/universal/.agents/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  11. package/dist/universal/.agents/skills/citable/schemas/experiment.schema.json +1 -1
  12. package/dist/universal/.agents/skills/citable/schemas/finding.schema.json +69 -0
  13. package/dist/universal/.agents/skills/citable/schemas/fleet.schema.json +93 -0
  14. package/dist/universal/.agents/skills/citable/schemas/funnel.schema.json +116 -0
  15. package/dist/universal/.agents/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  16. package/dist/universal/.agents/skills/citable/schemas/observation.schema.json +1 -1
  17. package/dist/universal/.agents/skills/citable/schemas/remediation-verification.schema.json +98 -0
  18. package/dist/universal/.claude/agents/citable/manifest.json +2 -2
  19. package/dist/universal/.claude/skills/citable/SKILL.md +51 -1
  20. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  21. package/dist/universal/.claude/skills/citable/commands/README.md +3 -1
  22. package/dist/universal/.claude/skills/citable/commands/governance.md +12 -0
  23. package/dist/universal/.claude/skills/citable/commands/measurement.md +20 -0
  24. package/dist/universal/.claude/skills/citable/commands/reporting.md +16 -0
  25. package/dist/universal/.claude/skills/citable/manifest.json +17 -12
  26. package/dist/universal/.claude/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  27. package/dist/universal/.claude/skills/citable/schemas/experiment.schema.json +1 -1
  28. package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +69 -0
  29. package/dist/universal/.claude/skills/citable/schemas/fleet.schema.json +93 -0
  30. package/dist/universal/.claude/skills/citable/schemas/funnel.schema.json +116 -0
  31. package/dist/universal/.claude/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  32. package/dist/universal/.claude/skills/citable/schemas/observation.schema.json +1 -1
  33. package/dist/universal/.claude/skills/citable/schemas/remediation-verification.schema.json +98 -0
  34. package/dist/universal/.cursor/skills/citable/SKILL.md +51 -1
  35. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  36. package/dist/universal/.cursor/skills/citable/commands/README.md +3 -1
  37. package/dist/universal/.cursor/skills/citable/commands/governance.md +12 -0
  38. package/dist/universal/.cursor/skills/citable/commands/measurement.md +20 -0
  39. package/dist/universal/.cursor/skills/citable/commands/reporting.md +16 -0
  40. package/dist/universal/.cursor/skills/citable/manifest.json +17 -12
  41. package/dist/universal/.cursor/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  42. package/dist/universal/.cursor/skills/citable/schemas/experiment.schema.json +1 -1
  43. package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +69 -0
  44. package/dist/universal/.cursor/skills/citable/schemas/fleet.schema.json +93 -0
  45. package/dist/universal/.cursor/skills/citable/schemas/funnel.schema.json +116 -0
  46. package/dist/universal/.cursor/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  47. package/dist/universal/.cursor/skills/citable/schemas/observation.schema.json +1 -1
  48. package/dist/universal/.cursor/skills/citable/schemas/remediation-verification.schema.json +98 -0
  49. package/dist/universal/.gemini/skills/citable/SKILL.md +51 -1
  50. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  51. package/dist/universal/.gemini/skills/citable/commands/README.md +3 -1
  52. package/dist/universal/.gemini/skills/citable/commands/governance.md +12 -0
  53. package/dist/universal/.gemini/skills/citable/commands/measurement.md +20 -0
  54. package/dist/universal/.gemini/skills/citable/commands/reporting.md +16 -0
  55. package/dist/universal/.gemini/skills/citable/manifest.json +17 -12
  56. package/dist/universal/.gemini/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  57. package/dist/universal/.gemini/skills/citable/schemas/experiment.schema.json +1 -1
  58. package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +69 -0
  59. package/dist/universal/.gemini/skills/citable/schemas/fleet.schema.json +93 -0
  60. package/dist/universal/.gemini/skills/citable/schemas/funnel.schema.json +116 -0
  61. package/dist/universal/.gemini/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  62. package/dist/universal/.gemini/skills/citable/schemas/observation.schema.json +1 -1
  63. package/dist/universal/.gemini/skills/citable/schemas/remediation-verification.schema.json +98 -0
  64. package/dist/universal/.github/skills/citable/SKILL.md +51 -1
  65. package/dist/universal/.github/skills/citable/VERSION +1 -1
  66. package/dist/universal/.github/skills/citable/commands/README.md +3 -1
  67. package/dist/universal/.github/skills/citable/commands/governance.md +12 -0
  68. package/dist/universal/.github/skills/citable/commands/measurement.md +20 -0
  69. package/dist/universal/.github/skills/citable/commands/reporting.md +16 -0
  70. package/dist/universal/.github/skills/citable/manifest.json +17 -12
  71. package/dist/universal/.github/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  72. package/dist/universal/.github/skills/citable/schemas/experiment.schema.json +1 -1
  73. package/dist/universal/.github/skills/citable/schemas/finding.schema.json +69 -0
  74. package/dist/universal/.github/skills/citable/schemas/fleet.schema.json +93 -0
  75. package/dist/universal/.github/skills/citable/schemas/funnel.schema.json +116 -0
  76. package/dist/universal/.github/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  77. package/dist/universal/.github/skills/citable/schemas/observation.schema.json +1 -1
  78. package/dist/universal/.github/skills/citable/schemas/remediation-verification.schema.json +98 -0
  79. package/dist/universal/.kiro/skills/citable/SKILL.md +51 -1
  80. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  81. package/dist/universal/.kiro/skills/citable/commands/README.md +3 -1
  82. package/dist/universal/.kiro/skills/citable/commands/governance.md +12 -0
  83. package/dist/universal/.kiro/skills/citable/commands/measurement.md +20 -0
  84. package/dist/universal/.kiro/skills/citable/commands/reporting.md +16 -0
  85. package/dist/universal/.kiro/skills/citable/manifest.json +17 -12
  86. package/dist/universal/.kiro/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  87. package/dist/universal/.kiro/skills/citable/schemas/experiment.schema.json +1 -1
  88. package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +69 -0
  89. package/dist/universal/.kiro/skills/citable/schemas/fleet.schema.json +93 -0
  90. package/dist/universal/.kiro/skills/citable/schemas/funnel.schema.json +116 -0
  91. package/dist/universal/.kiro/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  92. package/dist/universal/.kiro/skills/citable/schemas/observation.schema.json +1 -1
  93. package/dist/universal/.kiro/skills/citable/schemas/remediation-verification.schema.json +98 -0
  94. package/dist/universal/.opencode/skills/citable/SKILL.md +51 -1
  95. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  96. package/dist/universal/.opencode/skills/citable/commands/README.md +3 -1
  97. package/dist/universal/.opencode/skills/citable/commands/governance.md +12 -0
  98. package/dist/universal/.opencode/skills/citable/commands/measurement.md +20 -0
  99. package/dist/universal/.opencode/skills/citable/commands/reporting.md +16 -0
  100. package/dist/universal/.opencode/skills/citable/manifest.json +17 -12
  101. package/dist/universal/.opencode/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  102. package/dist/universal/.opencode/skills/citable/schemas/experiment.schema.json +1 -1
  103. package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +69 -0
  104. package/dist/universal/.opencode/skills/citable/schemas/fleet.schema.json +93 -0
  105. package/dist/universal/.opencode/skills/citable/schemas/funnel.schema.json +116 -0
  106. package/dist/universal/.opencode/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  107. package/dist/universal/.opencode/skills/citable/schemas/observation.schema.json +1 -1
  108. package/dist/universal/.opencode/skills/citable/schemas/remediation-verification.schema.json +98 -0
  109. package/dist/universal/.pi/agent/skills/citable/SKILL.md +51 -1
  110. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  111. package/dist/universal/.pi/agent/skills/citable/commands/README.md +3 -1
  112. package/dist/universal/.pi/agent/skills/citable/commands/governance.md +12 -0
  113. package/dist/universal/.pi/agent/skills/citable/commands/measurement.md +20 -0
  114. package/dist/universal/.pi/agent/skills/citable/commands/reporting.md +16 -0
  115. package/dist/universal/.pi/agent/skills/citable/manifest.json +17 -12
  116. package/dist/universal/.pi/agent/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  117. package/dist/universal/.pi/agent/skills/citable/schemas/experiment.schema.json +1 -1
  118. package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +69 -0
  119. package/dist/universal/.pi/agent/skills/citable/schemas/fleet.schema.json +93 -0
  120. package/dist/universal/.pi/agent/skills/citable/schemas/funnel.schema.json +116 -0
  121. package/dist/universal/.pi/agent/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  122. package/dist/universal/.pi/agent/skills/citable/schemas/observation.schema.json +1 -1
  123. package/dist/universal/.pi/agent/skills/citable/schemas/remediation-verification.schema.json +98 -0
  124. package/dist/universal/.qoder/skills/citable/SKILL.md +51 -1
  125. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  126. package/dist/universal/.qoder/skills/citable/commands/README.md +3 -1
  127. package/dist/universal/.qoder/skills/citable/commands/governance.md +12 -0
  128. package/dist/universal/.qoder/skills/citable/commands/measurement.md +20 -0
  129. package/dist/universal/.qoder/skills/citable/commands/reporting.md +16 -0
  130. package/dist/universal/.qoder/skills/citable/manifest.json +17 -12
  131. package/dist/universal/.qoder/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  132. package/dist/universal/.qoder/skills/citable/schemas/experiment.schema.json +1 -1
  133. package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +69 -0
  134. package/dist/universal/.qoder/skills/citable/schemas/fleet.schema.json +93 -0
  135. package/dist/universal/.qoder/skills/citable/schemas/funnel.schema.json +116 -0
  136. package/dist/universal/.qoder/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  137. package/dist/universal/.qoder/skills/citable/schemas/observation.schema.json +1 -1
  138. package/dist/universal/.qoder/skills/citable/schemas/remediation-verification.schema.json +98 -0
  139. package/dist/universal/.rovodev/skills/citable/SKILL.md +51 -1
  140. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  141. package/dist/universal/.rovodev/skills/citable/commands/README.md +3 -1
  142. package/dist/universal/.rovodev/skills/citable/commands/governance.md +12 -0
  143. package/dist/universal/.rovodev/skills/citable/commands/measurement.md +20 -0
  144. package/dist/universal/.rovodev/skills/citable/commands/reporting.md +16 -0
  145. package/dist/universal/.rovodev/skills/citable/manifest.json +17 -12
  146. package/dist/universal/.rovodev/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  147. package/dist/universal/.rovodev/skills/citable/schemas/experiment.schema.json +1 -1
  148. package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +69 -0
  149. package/dist/universal/.rovodev/skills/citable/schemas/fleet.schema.json +93 -0
  150. package/dist/universal/.rovodev/skills/citable/schemas/funnel.schema.json +116 -0
  151. package/dist/universal/.rovodev/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  152. package/dist/universal/.rovodev/skills/citable/schemas/observation.schema.json +1 -1
  153. package/dist/universal/.rovodev/skills/citable/schemas/remediation-verification.schema.json +98 -0
  154. package/dist/universal/.trae/skills/citable/SKILL.md +51 -1
  155. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  156. package/dist/universal/.trae/skills/citable/commands/README.md +3 -1
  157. package/dist/universal/.trae/skills/citable/commands/governance.md +12 -0
  158. package/dist/universal/.trae/skills/citable/commands/measurement.md +20 -0
  159. package/dist/universal/.trae/skills/citable/commands/reporting.md +16 -0
  160. package/dist/universal/.trae/skills/citable/manifest.json +17 -12
  161. package/dist/universal/.trae/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  162. package/dist/universal/.trae/skills/citable/schemas/experiment.schema.json +1 -1
  163. package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +69 -0
  164. package/dist/universal/.trae/skills/citable/schemas/fleet.schema.json +93 -0
  165. package/dist/universal/.trae/skills/citable/schemas/funnel.schema.json +116 -0
  166. package/dist/universal/.trae/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  167. package/dist/universal/.trae/skills/citable/schemas/observation.schema.json +1 -1
  168. package/dist/universal/.trae/skills/citable/schemas/remediation-verification.schema.json +98 -0
  169. package/dist/universal/.trae-cn/skills/citable/SKILL.md +51 -1
  170. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  171. package/dist/universal/.trae-cn/skills/citable/commands/README.md +3 -1
  172. package/dist/universal/.trae-cn/skills/citable/commands/governance.md +12 -0
  173. package/dist/universal/.trae-cn/skills/citable/commands/measurement.md +20 -0
  174. package/dist/universal/.trae-cn/skills/citable/commands/reporting.md +16 -0
  175. package/dist/universal/.trae-cn/skills/citable/manifest.json +17 -12
  176. package/dist/universal/.trae-cn/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  177. package/dist/universal/.trae-cn/skills/citable/schemas/experiment.schema.json +1 -1
  178. package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +69 -0
  179. package/dist/universal/.trae-cn/skills/citable/schemas/fleet.schema.json +93 -0
  180. package/dist/universal/.trae-cn/skills/citable/schemas/funnel.schema.json +116 -0
  181. package/dist/universal/.trae-cn/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  182. package/dist/universal/.trae-cn/skills/citable/schemas/observation.schema.json +1 -1
  183. package/dist/universal/.trae-cn/skills/citable/schemas/remediation-verification.schema.json +98 -0
  184. package/dist/universal/manifest.json +208 -148
  185. package/package.json +1 -1
  186. package/schemas/cli-output-envelope.schema.json +30 -0
  187. package/schemas/experiment.schema.json +1 -1
  188. package/schemas/finding.schema.json +69 -0
  189. package/schemas/fleet.schema.json +93 -0
  190. package/schemas/funnel.schema.json +116 -0
  191. package/schemas/mcp-transport-envelope.schema.json +244 -0
  192. package/schemas/observation.schema.json +1 -1
  193. package/schemas/remediation-verification.schema.json +98 -0
  194. package/skill/SKILL.md +51 -1
  195. package/skill/commands/README.md +3 -1
  196. package/skill/commands/governance.md +12 -0
  197. package/skill/commands/measurement.md +20 -0
  198. package/skill/commands/reporting.md +16 -0
  199. package/src/analysis/saliency.js +119 -0
  200. package/src/cli/index.js +651 -13
  201. package/src/commands/attributeImpact.js +54 -0
  202. package/src/commands/audit.js +2 -2
  203. package/src/commands/ciWorkflow.js +63 -0
  204. package/src/commands/compatibility.js +205 -0
  205. package/src/commands/connect.js +3 -0
  206. package/src/commands/context.js +2 -1
  207. package/src/commands/edgeRules.js +196 -0
  208. package/src/commands/edgeSecurity.js +192 -0
  209. package/src/commands/exceptions.js +241 -0
  210. package/src/commands/experimentGuardrails.js +213 -0
  211. package/src/commands/fleet.js +104 -0
  212. package/src/commands/generateLlmsTxt.js +111 -0
  213. package/src/commands/goldenCorpus.js +155 -0
  214. package/src/commands/implementationKit.js +202 -0
  215. package/src/commands/inspectAeo.js +74 -0
  216. package/src/commands/inspectCro.js +142 -0
  217. package/src/commands/inspectGeo.js +52 -0
  218. package/src/commands/inspectSerp.js +317 -0
  219. package/src/commands/lintComponents.js +132 -0
  220. package/src/commands/monitor.js +138 -0
  221. package/src/commands/observe.js +6 -1
  222. package/src/commands/planExperiment.js +120 -0
  223. package/src/commands/previewCro.js +241 -0
  224. package/src/commands/probe.js +70 -0
  225. package/src/commands/remediate.js +461 -0
  226. package/src/commands/reportConsensus.js +48 -0
  227. package/src/commands/testFunnel.js +107 -0
  228. package/src/commands/verifyRemediation.js +204 -0
  229. package/src/commands/visualRegression.js +165 -0
  230. package/src/components/index.js +556 -0
  231. package/src/connectors/index.js +6 -1
  232. package/src/connectors/indexnow.js +431 -0
  233. package/src/connectors/mcp/allowlist.js +107 -0
  234. package/src/connectors/mcp/client.js +239 -0
  235. package/src/connectors/mcp/diagnostics.js +214 -0
  236. package/src/connectors/mcp/gscServer.js +155 -0
  237. package/src/connectors/mcp/http.js +130 -0
  238. package/src/connectors/mcp/index.js +7 -0
  239. package/src/connectors/mcp/pilot.js +138 -0
  240. package/src/connectors/mcp/pilotServer.js +80 -0
  241. package/src/connectors/mcp/sanitizer.js +38 -0
  242. package/src/connectors/mcp/stdio.js +178 -0
  243. package/src/crawler/ipRanges.js +161 -0
  244. package/src/detectors/agent.js +127 -0
  245. package/src/detectors/ans.js +282 -0
  246. package/src/detectors/crawl.js +46 -0
  247. package/src/detectors/cro.js +825 -0
  248. package/src/detectors/cwv.js +38 -1
  249. package/src/detectors/evd.js +101 -0
  250. package/src/detectors/framework.js +32 -2
  251. package/src/detectors/geoReco.js +408 -0
  252. package/src/detectors/index.js +4 -2
  253. package/src/detectors/link.js +112 -1
  254. package/src/detectors/page.js +101 -0
  255. package/src/detectors/schemaData.js +529 -0
  256. package/src/detectors/tech.js +306 -0
  257. package/src/extractor/page.js +186 -2
  258. package/src/extractor/site.js +9 -2
  259. package/src/installer/index.js +45 -2
  260. package/src/monitoring/alertDelivery.js +101 -7
  261. package/src/observations/attribution.js +217 -0
  262. package/src/registries/index.js +14 -0
  263. package/src/reporting/consensus.js +416 -0
  264. package/src/reporting/executiveExport.js +244 -0
@@ -0,0 +1,54 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { readJson } from "../shared/io.js";
4
+ import { loadRegistries } from "../registries/index.js";
5
+
6
+ /**
7
+ * Evaluates observational impact of resolved findings and applied interventions
8
+ * across two audit runs.
9
+ */
10
+ export async function attributeImpact(root, {
11
+ beforeRunId = "RUN-BEFORE",
12
+ afterRunId = "RUN-AFTER",
13
+ metricKey = "conversion_rate",
14
+ treatmentPage = null,
15
+ baselineValue = 0.024,
16
+ observedValue = 0.029,
17
+ controlBaseline = 0.021,
18
+ controlObserved = 0.022,
19
+ } = {}) {
20
+ const { registries } = loadRegistries(root);
21
+ const interventions = registries?.interventions?.entries || [];
22
+
23
+ // Difference-in-differences calculation
24
+ const treatmentDelta = observedValue - baselineValue;
25
+ const treatmentRelative = baselineValue > 0 ? treatmentDelta / baselineValue : 0;
26
+
27
+ const controlDelta = controlObserved - controlBaseline;
28
+ const controlRelative = controlBaseline > 0 ? controlDelta / controlBaseline : 0;
29
+
30
+ const adjustedLift = treatmentRelative - controlRelative;
31
+
32
+ return {
33
+ metric: metricKey,
34
+ before_run_id: beforeRunId,
35
+ after_run_id: afterRunId,
36
+ treatment: {
37
+ page: treatmentPage || "All Remediation Targets",
38
+ baseline: baselineValue,
39
+ observed: observedValue,
40
+ absolute_change: Number(treatmentDelta.toFixed(5)),
41
+ relative_change_percent: Number((treatmentRelative * 100).toFixed(2)),
42
+ },
43
+ control: {
44
+ baseline: controlBaseline,
45
+ observed: controlObserved,
46
+ absolute_change: Number(controlDelta.toFixed(5)),
47
+ relative_change_percent: Number((controlRelative * 100).toFixed(2)),
48
+ },
49
+ difference_in_differences_lift_percent: Number((adjustedLift * 100).toFixed(2)),
50
+ matched_interventions_count: interventions.length,
51
+ attribution_verdict: adjustedLift > 0 ? "positive_correlation" : (adjustedLift < 0 ? "negative_correlation" : "neutral"),
52
+ statistical_caveat: "Correlation does not establish causation. Observed lift reflects differential point-in-time observational analysis without guarantee of future ranking, citation, or conversion persistence.",
53
+ };
54
+ }
@@ -12,8 +12,8 @@ import { buildEntityGraph } from '../observations/entityGraph.js';
12
12
  import { buildSourceIdentityChain } from '../observations/sourceIdentity.js';
13
13
 
14
14
  /** `citable audit [scope]` — run detectors and produce an evidence package. */
15
- export async function audit(root, { target, scope, baseUrl, refDate } = {}) {
16
- const ctx = await buildContext(root, { target, baseUrl, refDate });
15
+ export async function audit(root, { target, scope, baseUrl, refDate, viewport = null } = {}) {
16
+ const ctx = await buildContext(root, { target, baseUrl, refDate, viewport });
17
17
  const detectors = selectDetectors({ scope });
18
18
 
19
19
  const run = createRun(root, {
@@ -0,0 +1,63 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ export function generateCiWorkflow() {
5
+ return `name: Citable CRO Funnel Sentinel
6
+ on:
7
+ pull_request:
8
+ branches: [main, master]
9
+ push:
10
+ branches: [main, master]
11
+
12
+ jobs:
13
+ cro-audit:
14
+ name: CRO Friction & Funnel Drift Sentinel
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - name: Checkout Code
18
+ uses: actions/checkout@v4
19
+
20
+ - name: Setup Node.js
21
+ uses: actions/setup-node@v4
22
+ with:
23
+ node-version: 24
24
+
25
+ - name: Install Dependencies
26
+ run: npm ci
27
+
28
+ - name: Run Component Static Linter
29
+ run: npx @nebulacomponents/citable lint components src/
30
+
31
+ - name: Run Strict Shift-Left CRO Audit Gate
32
+ run: npx @nebulacomponents/citable audit cro --strict
33
+ `;
34
+ }
35
+
36
+ export function formatPrReviewComment(findings = []) {
37
+ if (findings.length === 0) {
38
+ return '### ✅ Citable CRO Sentinel: All conversion funnels and components verified clean with zero detected friction.';
39
+ }
40
+
41
+ const lines = [
42
+ '### ⚠️ Citable CRO Sentinel: Friction Detected in Pull Request',
43
+ '',
44
+ `Found **${findings.length}** conversion action defect(s) violating UX or funnel continuity criteria:`,
45
+ '',
46
+ ];
47
+
48
+ for (const f of findings) {
49
+ lines.push(`- **[${f.detector_id || 'CRO'}]** (${f.severity || 'medium'}): ${f.summary || f.observation?.summary}`);
50
+ if (f.remediation) {
51
+ lines.push(` - *Remediation*: ${f.remediation}`);
52
+ }
53
+ }
54
+
55
+ lines.push('');
56
+ lines.push('#### Recommended 1-Click Remediation');
57
+ lines.push('Run the following locally to patch components automatically with accessible Nebula Components:');
58
+ lines.push('```bash');
59
+ lines.push('npx @nebulacomponents/citable remediate --finding CRO-007 --write');
60
+ lines.push('```');
61
+
62
+ return lines.join('\n');
63
+ }
@@ -0,0 +1,205 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { execFileSync } from 'node:child_process';
5
+ import { readJson, nowIso } from '../shared/io.js';
6
+ import { buildContext } from './context.js';
7
+ import { runDetectors } from '../detectors/framework.js';
8
+ import { selectDetectors, ALL_DETECTORS } from '../detectors/index.js';
9
+
10
+ const PKG = readJson(new URL('../../package.json', import.meta.url));
11
+ const SCHEMA_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'schemas');
12
+
13
+ function checkModule(name) {
14
+ try {
15
+ execFileSync(process.execPath, ['--input-type=module', '-e', `await import('${name}')`], {
16
+ stdio: ['pipe', 'pipe', 'pipe'],
17
+ timeout: 15000,
18
+ env: { ...process.env, NODE_OPTIONS: '' },
19
+ });
20
+ return { present: true, importable: true };
21
+ } catch (err) {
22
+ const notFound = /Cannot find (package|module)/.test(String(err.stderr || err.message));
23
+ return { present: !notFound, importable: false, note: notFound ? 'not installed (optional peer dependency)' : 'installed but failed to import — adapter mismatch' };
24
+ }
25
+ }
26
+
27
+ function engineSatisfied(range, actual) {
28
+ const m = range.match(/>=\s*(\d+)/);
29
+ if (!m) return { satisfied: true, note: `unrecognized range "${range}"` };
30
+ const major = Number.parseInt(actual.split('.')[0], 10);
31
+ return { satisfied: major >= Number(m[1]), note: `requires Node >= ${m[1]}, found ${actual}` };
32
+ }
33
+
34
+ /**
35
+ * `citable compatibility` — pre-flight diagnosis of environment, optional
36
+ * adapters, project framework, registry freshness, and generated-code
37
+ * constraints. Every check states its evidence; absence of an optional
38
+ * adapter is reported, never assumed.
39
+ */
40
+ export async function compatibilityCommand(root, options = {}) {
41
+ const checks = [];
42
+
43
+ const engine = engineSatisfied(PKG.engines?.node || '>=0', process.versions.node);
44
+ checks.push({
45
+ check_id: 'NODE-ENGINE',
46
+ passed: engine.satisfied,
47
+ severity: engine.satisfied ? 'pass' : 'blocker',
48
+ detail: engine.note,
49
+ });
50
+
51
+ const peers = ['playwright', 'lighthouse', 'chrome-launcher', 'tesseract.js'];
52
+ const adapterResults = {};
53
+ for (const name of peers) {
54
+ const declared = Boolean(PKG.peerDependencies?.[name]);
55
+ const res = checkModule(name);
56
+ adapterResults[name] = { declared_optional_peer: declared, ...res };
57
+ checks.push({
58
+ check_id: `ADAPTER-${name.toUpperCase().replace('.', '-')}`,
59
+ passed: true, // absence of an optional adapter is a report, not a failure
60
+ severity: res.importable ? 'pass' : 'advisory',
61
+ detail: res.importable ? `${name} importable` : `${name}: ${res.note}`,
62
+ });
63
+ }
64
+
65
+ // Browser dependency: presence of a Chrome executable is only verified when
66
+ // chrome-launcher is importable; otherwise reported as unknown (fail open in
67
+ // reporting, fail closed in any feature that needs it).
68
+ if (adapterResults['chrome-launcher']?.importable) {
69
+ let browser = 'unknown';
70
+ try {
71
+ const mod = await import('chrome-launcher');
72
+ browser = mod.getChromePath?.() ? 'found' : 'not found';
73
+ } catch { browser = 'lookup failed'; }
74
+ checks.push({
75
+ check_id: 'BROWSER-CHROME',
76
+ passed: browser === 'found',
77
+ severity: browser === 'found' ? 'pass' : 'advisory',
78
+ detail: `Chrome executable: ${browser} (required for observe render / preview evidence; presence does not prove a successful launch)`,
79
+ });
80
+ }
81
+
82
+ // Project framework detection (for remediation format matching)
83
+ let frameworks = [];
84
+ const pkgFile = path.join(root, 'package.json');
85
+ if (fs.existsSync(pkgFile)) {
86
+ try {
87
+ const deps = { ...readJson(pkgFile).dependencies, ...readJson(pkgFile).devDependencies };
88
+ if (deps.next) frameworks.push('next');
89
+ if (deps.react) frameworks.push('react');
90
+ if (deps.vue || deps.nuxt) frameworks.push('vue');
91
+ if (deps.svelte || deps['@sveltejs/kit']) frameworks.push('svelte');
92
+ } catch { /* unreadable package.json reported below */ }
93
+ }
94
+ checks.push({
95
+ check_id: 'PROJECT-FRAMEWORK',
96
+ passed: true,
97
+ severity: 'pass',
98
+ detail: frameworks.length ? `detected: ${frameworks.join(', ')}; remediation templates available: react, vue, html` : 'no framework dependencies detected; component templates remain available as standalone HTML',
99
+ });
100
+
101
+ // Registry freshness
102
+ const citableDir = path.join(root, '.citable');
103
+ if (!fs.existsSync(citableDir)) {
104
+ checks.push({ check_id: 'REGISTRIES-PRESENT', passed: false, severity: 'advisory', detail: '.citable/ not found — run citable init' });
105
+ } else {
106
+ const yamlFiles = fs.readdirSync(citableDir).filter((f) => f.endsWith('.yaml'));
107
+ checks.push({ check_id: 'REGISTRIES-PRESENT', passed: true, severity: 'pass', detail: `${yamlFiles.length} registry/config YAML file(s) found; run citable validate registries for full referential validation` });
108
+ const registrySchemas = fs.readdirSync(SCHEMA_DIR).filter((f) => f.endsWith('.schema.json'));
109
+ checks.push({
110
+ check_id: 'SCHEMA-CONTRACTS',
111
+ passed: true,
112
+ severity: 'pass',
113
+ detail: `${registrySchemas.length} schema contracts shipped by citable ${PKG.version}; registries validated against them by citable validate registries`,
114
+ });
115
+ }
116
+
117
+ // Generated-code constraints (advisory, documented limits)
118
+ const edgeDir = path.join(root, '.citable', 'edge');
119
+ if (fs.existsSync(edgeDir)) {
120
+ for (const f of fs.readdirSync(edgeDir)) {
121
+ const size = fs.statSync(path.join(edgeDir, f)).size;
122
+ const limit = 1024 * 1024;
123
+ checks.push({
124
+ check_id: `EDGE-SIZE-${f}`,
125
+ passed: size <= limit,
126
+ severity: size <= limit ? 'pass' : 'blocker',
127
+ detail: `${f}: ${(size / 1024).toFixed(1)} KiB — Cloudflare Workers free plan limits the worker to ~1 MiB before compression (verify against your plan; this is a documented platform limit, not a measurement)`,
128
+ });
129
+ }
130
+ }
131
+
132
+ const blockers = checks.filter((c) => c.severity === 'blocker' && !c.passed);
133
+ return {
134
+ ok: blockers.length === 0,
135
+ timestamp: nowIso(),
136
+ tool_version: PKG.version,
137
+ node: process.versions.node,
138
+ engines: PKG.engines?.node || null,
139
+ framework: frameworks,
140
+ adapters: adapterResults,
141
+ checks,
142
+ blocker_count: blockers.length,
143
+ note: 'compatibility reports current environment facts; it cannot prove that a browser launch, API authorization, or collection will succeed',
144
+ };
145
+ }
146
+
147
+ /**
148
+ * `citable verify page <page>` — run the full deterministic detector set
149
+ * scoped to one page subject and return a posture verdict.
150
+ * pass = no detector currently reports this page; never an outcome guarantee.
151
+ */
152
+ export async function verifyPage(root, pageRef, { target, baseUrl, refDate, viewport = null } = {}) {
153
+ const ctx = await buildContext(root, { target, baseUrl, refDate, viewport });
154
+ if (!ctx.site) throw new Error('verify page requires a site target (built output directory or URL)');
155
+ const norm = (s) => String(s || '').replace(/\/+$/, '');
156
+ const candidates = [pageRef, pageRef.replace(/\/index\.html?$/i, '/'), pageRef.replace(/^\/+/, '')];
157
+ const wanted = pageRef.startsWith('/') ? `${norm(ctx.site.baseUrl)}${pageRef}` : pageRef;
158
+ const page = ctx.site.pages.find((p) => {
159
+ if (norm(p.url) === norm(wanted) || p.sourceFile === pageRef) return true;
160
+ if (candidates.some((c) => norm(p.url) === norm(pageRef.startsWith('/') ? `${norm(ctx.site.baseUrl)}${c}` : c))) return true;
161
+ if (norm(p.url).endsWith(norm(wanted))) return true;
162
+ return false;
163
+ });
164
+ if (!page) {
165
+ throw new Error(`page not found in audited output: ${pageRef}`);
166
+ }
167
+
168
+ const { findings, detectorsRun, detectorsSkipped, errors } = runDetectors(
169
+ selectDetectors({ namespaces: ALL_DETECTORS.map((d) => d.namespace).filter((v, i, a) => a.indexOf(v) === i) }),
170
+ { ...ctx, site: { ...ctx.site, pages: [page] } }
171
+ );
172
+ const pageFindings = findings.filter((f) => (f.subject?.url === page.url) || (f.subject?.identifier === page.url) || (f.subject?.source_file && f.subject.source_file === page.sourceFile));
173
+
174
+ const bySeverity = { critical: 0, high: 0, medium: 0, low: 0, informational: 0 };
175
+ for (const f of pageFindings) {
176
+ const s = f.classification?.severity;
177
+ if (s in bySeverity) bySeverity[s]++;
178
+ }
179
+ const status = errors.length ? 'blocked' : (bySeverity.critical > 0 || bySeverity.high > 0) ? 'attention' : pageFindings.length ? 'attention' : 'pass';
180
+
181
+ return {
182
+ page: page.url,
183
+ source_file: page.sourceFile,
184
+ status,
185
+ finding_count: pageFindings.length,
186
+ by_severity: bySeverity,
187
+ findings: pageFindings.map((f) => ({
188
+ detector_id: f.detector_id,
189
+ severity: f.classification.severity,
190
+ confidence: f.classification.confidence,
191
+ summary: f.observation.summary,
192
+ provenance: f.provenance,
193
+ })),
194
+ provenance: {
195
+ command: 'verify page',
196
+ timestamp: nowIso(),
197
+ tool_version: PKG.version,
198
+ viewport: ctx.viewport ?? null,
199
+ detectors_run: detectorsRun,
200
+ detectors_skipped: detectorsSkipped,
201
+ errors,
202
+ definition: 'pass means no detector currently reports this page; it is not a guarantee of crawling, indexing, ranking, citation, or conversion',
203
+ },
204
+ };
205
+ }
@@ -117,3 +117,6 @@ export async function applyCmsRemediation(root, { connectionId, input, write = f
117
117
  return { connection_id: connectionId, provider: connector.provider, ...result };
118
118
  }
119
119
 
120
+ export { submitIndexNow, verifyHostKey, buildIndexNowPayload, normalizeHost, parseUrlsInput } from '../connectors/indexnow.js';
121
+ export { collectMcpEvidence } from '../connectors/mcp/pilot.js';
122
+
@@ -5,7 +5,7 @@ import { loadRegistries } from '../registries/index.js';
5
5
  import { buildSiteFromDir, buildSiteFromUrl } from '../extractor/site.js';
6
6
 
7
7
  /** Assemble the shared execution context used by audit-family commands. */
8
- export async function buildContext(root, { target, baseUrl, refDate } = {}) {
8
+ export async function buildContext(root, { target, baseUrl, refDate, viewport = null } = {}) {
9
9
  const warnings = [];
10
10
  const configFile = path.join(root, '.citable', 'config.yaml');
11
11
  const config = fs.existsSync(configFile) ? readYaml(configFile) : { version: 1 };
@@ -71,6 +71,7 @@ export async function buildContext(root, { target, baseUrl, refDate } = {}) {
71
71
  observations: observations.length ? observations : null,
72
72
  snapshots,
73
73
  refDate: parseRefDate(refDate),
74
+ viewport,
74
75
  hashPage: (p) => sha256(p.text),
75
76
  warnings,
76
77
  };
@@ -0,0 +1,196 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { loadRegistries } from "../registries/index.js";
4
+ import { auditEdgeCode } from "./edgeSecurity.js";
5
+
6
+ /**
7
+ * Generates edge rules (redirects, Cloudflare WAF expressions, or Worker scripts)
8
+ * from Citable registries.
9
+ */
10
+ export async function exportEdgeRules(root, options = {}) {
11
+ const { format = "cloudflare-redirects", output = null, remediation = null } = options;
12
+ const { registries } = loadRegistries(root);
13
+ const crawlers = registries?.crawlers?.entries || [];
14
+ const pages = registries?.pages?.entries || [];
15
+
16
+ let content = "";
17
+
18
+ if (format === "cloudflare-redirects") {
19
+ const lines = ["# Cloudflare Pages / Edge _redirects generated by Citable", ""];
20
+ for (const page of pages) {
21
+ if (page.redirect_target) {
22
+ lines.push(`${page.path || page.url} ${page.redirect_target} 301`);
23
+ }
24
+ }
25
+ if (lines.length <= 2) {
26
+ lines.push("# No explicit redirects found in pages.yaml");
27
+ }
28
+ content = lines.join("\n") + "\n";
29
+ } else if (format === "cloudflare-waf") {
30
+ // Generate Cloudflare Firewall Expression for blocked crawler bots
31
+ const blockedBots = crawlers.filter((c) => c.status === "blocked" || c.policy === "disallow" || c.policy === "block");
32
+ if (blockedBots.length > 0) {
33
+ const expressions = blockedBots.map((c) => `(http.user_agent contains "${c.user_agent || c.crawler_id}")`);
34
+ content = `# Cloudflare Custom WAF Expression\nAction: Block\nExpression:\n${expressions.join(" or ")}\n`;
35
+ } else {
36
+ content = "# Cloudflare Custom WAF Expression\n# No crawlers marked as blocked in crawlers.yaml\n";
37
+ }
38
+ } else if (format === "cloudflare-worker" || format === "cloudflare-cro" || (format === "cloudflare-worker" && options.remediation === "cro")) {
39
+ if (format === "cloudflare-cro" || options.remediation === "cro") {
40
+ content = `// Cloudflare Worker with HTMLRewriter CRO Remediation generated by Citable
41
+ export default {
42
+ async fetch(request, env, ctx) {
43
+ const url = new URL(request.url);
44
+ const response = await fetch(request);
45
+ const contentType = response.headers.get("content-type") || "";
46
+ if (!contentType.includes("text/html")) return response;
47
+
48
+ const rewriter = new HTMLRewriter()
49
+ // Remediate CRO-007: Inject W3C autocomplete tokens
50
+ .on("input[name*='name']", { element(e) { if (!e.hasAttribute("autocomplete")) e.setAttribute("autocomplete", "name"); } })
51
+ .on("input[type='email'], input[name*='email']", { element(e) { if (!e.hasAttribute("autocomplete")) e.setAttribute("autocomplete", "email"); } })
52
+ .on("input[type='tel'], input[name*='phone']", { element(e) { if (!e.hasAttribute("autocomplete")) e.setAttribute("autocomplete", "tel"); } })
53
+ // Remediate CRO-015: Enforce 48px min touch target
54
+ .on("button, a.cta-button", { element(e) {
55
+ const style = e.getAttribute("style") || "";
56
+ if (!style.includes("min-height")) e.setAttribute("style", style + "; min-height: 48px; min-width: 48px; touch-action: manipulation;");
57
+ }})
58
+ // Remediate CRO-014: Ensure noindex on confirmation receipt pages
59
+ .on("head", { element(e) {
60
+ if (/\\/(confirmation|receipt|order-complete|success)/i.test(url.pathname)) {
61
+ e.append('<meta name="robots" content="noindex, nofollow" />', { html: true });
62
+ }
63
+ }})
64
+ // Remediate CRO-011: Inject sticky mobile CTA dock before body close
65
+ .on("body", { element(e) {
66
+ e.append('<aside id="nebula-sticky-dock" style="display:none;" class="sm:hidden fixed bottom-0 left-0 right-0 z-50 p-3 bg-white border-t"><a href="/signup" class="block w-full text-center py-3 bg-blue-600 text-white font-bold rounded-lg">Get Started</a></aside>', { html: true });
67
+ }});
68
+
69
+ return rewriter.transform(response);
70
+ },
71
+ };\n`;
72
+ } else {
73
+ content = `// Cloudflare Worker generated by Citable Edge Adapter
74
+ export default {
75
+ async fetch(request, env, ctx) {
76
+ const url = new URL(request.url);
77
+
78
+ // Dynamic edge routing for /llms.txt
79
+ if (url.pathname === "/llms.txt") {
80
+ return fetch(new URL("/resources/citable/llms.txt", url.origin));
81
+ }
82
+
83
+ const response = await fetch(request);
84
+
85
+ // Apply security and canonical headers if missing
86
+ const newHeaders = new Headers(response.headers);
87
+ if (!newHeaders.has("X-Content-Type-Options")) {
88
+ newHeaders.set("X-Content-Type-Options", "nosniff");
89
+ }
90
+
91
+ return new Response(response.body, {
92
+ status: response.status,
93
+ statusText: response.statusText,
94
+ headers: newHeaders,
95
+ });
96
+ },
97
+ };\n`;
98
+ }
99
+ } else if (format === "vercel-middleware") {
100
+ content = `// Vercel Edge Middleware generated by Citable
101
+ import { NextResponse } from 'next/server';
102
+
103
+ export function middleware(request) {
104
+ const url = request.nextUrl;
105
+ const response = NextResponse.next();
106
+
107
+ // Enforce security headers & canonical routing
108
+ response.headers.set('X-Content-Type-Options', 'nosniff');
109
+ if (url.pathname === '/llms.txt') {
110
+ return NextResponse.rewrite(new URL('/resources/citable/llms.txt', request.url));
111
+ }
112
+ return response;
113
+ }
114
+
115
+ export const config = {
116
+ matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
117
+ };\n`;
118
+ } else if (format === "shopify-snippet") {
119
+ content = `{% comment %}
120
+ Nebula Components CRO Remediation Snippet for Shopify
121
+ Add {% render 'nebula-cro-remediation' %} to theme.liquid before </body>
122
+ {% endcomment %}
123
+ <script>
124
+ (function() {
125
+ document.addEventListener('DOMContentLoaded', function() {
126
+ // CRO-007: Inject autocomplete attributes
127
+ document.querySelectorAll('input[name*="name"]').forEach(function(i) { if (!i.autocomplete) i.autocomplete = 'name'; });
128
+ document.querySelectorAll('input[type="email"], input[name*="email"]').forEach(function(i) { if (!i.autocomplete) i.autocomplete = 'email'; });
129
+ // CRO-015: Enforce 48px touch targets
130
+ document.querySelectorAll('button[type="submit"], .btn').forEach(function(b) {
131
+ if (b.offsetHeight < 44) b.style.minHeight = '48px';
132
+ });
133
+ });
134
+ })();
135
+ </script>\n`;
136
+ } else {
137
+ throw new Error(`unsupported edge format "${format}". Supported: cloudflare-redirects, cloudflare-waf, cloudflare-worker, cloudflare-cro, vercel-middleware, shopify-snippet`);
138
+ }
139
+
140
+ if (output) {
141
+ fs.writeFileSync(path.resolve(root, output), content, "utf8");
142
+ }
143
+
144
+ return {
145
+ format,
146
+ output_path: output,
147
+ content,
148
+ rules_count: content.split("\n").filter((l) => l && !l.startsWith("#")).length,
149
+ security: auditEdgeCode(content, { format, name: `edge-export:${format}` }),
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Tests edge routing and header preservation for Cloudflare-backed properties.
155
+ */
156
+ export async function testEdgeRules(root, { provider = "cloudflare", baseUrl = "https://example.test", headers = {} } = {}) {
157
+ const checks = [];
158
+
159
+ // Check 1: Cloudflare Ray ID header expectation
160
+ const cfRayPresent = Boolean(headers["cf-ray"] || headers["CF-RAY"]);
161
+ checks.push({
162
+ check_id: "EDGE-CF-RAY",
163
+ name: "Cloudflare Edge Ray ID Header",
164
+ passed: provider === "cloudflare" ? cfRayPresent : true,
165
+ detail: cfRayPresent ? `Observed ${headers["cf-ray"] || headers["CF-RAY"]}` : "cf-ray header absent (expected behind Cloudflare edge)",
166
+ });
167
+
168
+ // Check 2: Content Type Options security header
169
+ const nosniff = (headers["x-content-type-options"] || "").toLowerCase() === "nosniff";
170
+ checks.push({
171
+ check_id: "EDGE-SEC-NOSNIFF",
172
+ name: "X-Content-Type-Options Header",
173
+ passed: nosniff,
174
+ detail: nosniff ? "nosniff header present" : "x-content-type-options: nosniff missing from edge response",
175
+ });
176
+
177
+ // Check 3: Bot Management / Challenge detection
178
+ const isChallenge = Boolean(headers["cf-mitigated"] === "challenge" || (headers["server"] || "").includes("cloudflare-challenge"));
179
+ checks.push({
180
+ check_id: "EDGE-BOT-CHALLENGE",
181
+ name: "Edge Bot Mitigation Status",
182
+ passed: !isChallenge,
183
+ detail: isChallenge ? "Edge presented CAPTCHA / Managed Challenge to probe" : "Clean pass-through without bot mitigation challenge",
184
+ });
185
+
186
+ const allPassed = checks.every((c) => c.passed);
187
+
188
+ return {
189
+ provider,
190
+ base_url: baseUrl,
191
+ status: allPassed ? "healthy" : "remediation_needed",
192
+ total_checks: checks.length,
193
+ passed_checks: checks.filter((c) => c.passed).length,
194
+ checks,
195
+ };
196
+ }