@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,104 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { loadRegistries, contextDir } from "../registries/index.js";
4
+ import { audit } from "./audit.js";
5
+
6
+ /**
7
+ * Returns summary of all properties defined in the fleet registry.
8
+ */
9
+ export async function fleetSummary(root) {
10
+ const { registries } = loadRegistries(root);
11
+ const fleet = registries?.fleet?.entries || [];
12
+
13
+ const byEnv = {};
14
+ const byOwner = {};
15
+ for (const p of fleet) {
16
+ byEnv[p.environment] = (byEnv[p.environment] || 0) + 1;
17
+ if (p.owner) byOwner[p.owner] = (byOwner[p.owner] || 0) + 1;
18
+ }
19
+
20
+ return {
21
+ total_properties: fleet.length,
22
+ active_properties: fleet.filter((p) => p.status === "active").length,
23
+ by_environment: byEnv,
24
+ by_owner: byOwner,
25
+ properties: fleet.map((p) => ({
26
+ property_id: p.property_id,
27
+ name: p.name,
28
+ environment: p.environment,
29
+ primary_domain: p.primary_domain,
30
+ status: p.status,
31
+ owner: p.owner,
32
+ })),
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Runs fleet-wide audit across registered fleet properties.
38
+ */
39
+ export async function fleetAudit(root, { propertyId = null, environment = null, scope = null } = {}) {
40
+ const { registries } = loadRegistries(root);
41
+ const fleet = registries?.fleet?.entries || [];
42
+
43
+ if (fleet.length === 0) {
44
+ throw new Error("no fleet properties declared in registries (expected .citable/fleet.yaml)");
45
+ }
46
+
47
+ let targets = fleet.filter((p) => p.status === "active");
48
+ if (propertyId) {
49
+ targets = targets.filter((p) => p.property_id === propertyId || p.primary_domain === propertyId);
50
+ if (targets.length === 0) throw new Error(`property "${propertyId}" not found in active fleet`);
51
+ }
52
+ if (environment) {
53
+ targets = targets.filter((p) => p.environment === environment);
54
+ }
55
+
56
+ const results = [];
57
+ for (const prop of targets) {
58
+ const propTarget = prop.built_output_dir && fs.existsSync(path.resolve(root, prop.built_output_dir))
59
+ ? path.resolve(root, prop.built_output_dir)
60
+ : null;
61
+
62
+ let auditRes = null;
63
+ let error = null;
64
+
65
+ try {
66
+ if (propTarget || prop.base_url) {
67
+ auditRes = await audit(root, {
68
+ target: propTarget || prop.base_url,
69
+ baseUrl: prop.base_url,
70
+ scope,
71
+ });
72
+ }
73
+ } catch (err) {
74
+ error = err.message;
75
+ }
76
+
77
+ results.push({
78
+ property_id: prop.property_id,
79
+ name: prop.name,
80
+ primary_domain: prop.primary_domain,
81
+ environment: prop.environment,
82
+ status: auditRes ? "audited" : (error ? "error" : "skipped"),
83
+ findings_count: auditRes?.findings?.length ?? 0,
84
+ critical_count: auditRes?.findings?.filter((f) => f.severity === "critical").length ?? 0,
85
+ high_count: auditRes?.findings?.filter((f) => f.severity === "high").length ?? 0,
86
+ medium_count: auditRes?.findings?.filter((f) => f.severity === "medium").length ?? 0,
87
+ low_count: auditRes?.findings?.filter((f) => f.severity === "low").length ?? 0,
88
+ error,
89
+ });
90
+ }
91
+
92
+ const totalFindings = results.reduce((acc, r) => acc + r.findings_count, 0);
93
+ const totalCritical = results.reduce((acc, r) => acc + r.critical_count, 0);
94
+ const totalHigh = results.reduce((acc, r) => acc + r.high_count, 0);
95
+
96
+ return {
97
+ fleet_size: targets.length,
98
+ total_findings: totalFindings,
99
+ total_critical: totalCritical,
100
+ total_high: totalHigh,
101
+ fleet_health: totalCritical === 0 && totalHigh === 0 ? "healthy" : (totalCritical > 0 ? "critical" : "needs_attention"),
102
+ properties: results,
103
+ };
104
+ }
@@ -0,0 +1,111 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { loadRegistries } from '../registries/index.js';
4
+
5
+ /**
6
+ * Build spec-compliant /llms.txt and /llms-full.txt from Citable registries.
7
+ * Adheres to https://llmstxt.org/ standard:
8
+ * - H1 title with brand/project name
9
+ * - Blockquote summary
10
+ * - Markdown links with descriptions
11
+ * - Optional links section pointing to /llms-full.txt
12
+ */
13
+ export function buildLlmsTxt(root, options = {}) {
14
+ const { registries } = loadRegistries(root);
15
+ const siteConfig = registries.config?.site || {};
16
+ const siteName = siteConfig.name || registries.entities?.entries?.[0]?.canonical_name || 'Project Documentation';
17
+ const siteUrl = options.siteUrl || siteConfig.base_url || 'https://example.test';
18
+ const description = siteConfig.description || registries.entities?.entries?.[0]?.description || 'Evidence-backed documentation and verified specifications.';
19
+
20
+ const pages = (registries.pages?.entries || []).filter((p) => !p.noindex);
21
+ const claims = (registries.claims?.entries || []).filter((c) => ['verified', 'verified_narrowed', 'stated'].includes(c.status));
22
+
23
+ // Build llms.txt
24
+ const lines = [];
25
+ lines.push(`# ${siteName}`);
26
+ lines.push('');
27
+ lines.push(`> ${description}`);
28
+ lines.push('');
29
+
30
+ if (pages.length > 0) {
31
+ lines.push('## Documentation & Key Pages');
32
+ lines.push('');
33
+ for (const p of pages) {
34
+ const url = p.url ? p.url : new URL(p.path || '/', siteUrl).href;
35
+ const title = p.title || p.page_id || 'Untitled page';
36
+ const desc = p.description ? `: ${p.description}` : '';
37
+ lines.push(`- [${title}](${url})${desc}`);
38
+ }
39
+ lines.push('');
40
+ }
41
+
42
+ if (claims.length > 0) {
43
+ lines.push('## Verified Capabilities & Claims');
44
+ lines.push('');
45
+ for (const c of claims) {
46
+ const target = c.publication_surfaces?.[0] || siteUrl;
47
+ const fullUrl = target.startsWith('http') ? target : new URL(target, siteUrl).href;
48
+ lines.push(`- [${c.claim}](${fullUrl}): Verified claim (${c.claim_type})`);
49
+ }
50
+ lines.push('');
51
+ }
52
+
53
+ lines.push('## Optional');
54
+ lines.push('');
55
+ const fullUrl = new URL('/llms-full.txt', siteUrl).href;
56
+ lines.push(`- [Full Content Directory](${fullUrl}): Complete uncompressed documentation representation`);
57
+ lines.push('');
58
+
59
+ const llmsTxt = lines.join('\n');
60
+
61
+ // Build llms-full.txt
62
+ const fullLines = [];
63
+ fullLines.push(`# ${siteName} (Full Text Specification)`);
64
+ fullLines.push('');
65
+ fullLines.push(`> Complete textual representation of ${siteName} for generative engines and autonomous agents.`);
66
+ fullLines.push('');
67
+ for (const p of pages) {
68
+ const url = p.url ? p.url : new URL(p.path || '/', siteUrl).href;
69
+ fullLines.push(`### ${p.title || p.page_id}`);
70
+ fullLines.push(`- URL: ${url}`);
71
+ if (p.description) fullLines.push(`- Description: ${p.description}`);
72
+ if (p.published_claims?.length) fullLines.push(`- Published Claims: ${p.published_claims.join(', ')}`);
73
+ fullLines.push('');
74
+ }
75
+ const llmsFullTxt = fullLines.join('\n');
76
+
77
+ return {
78
+ siteName,
79
+ siteUrl,
80
+ llmsTxt,
81
+ llmsFullTxt,
82
+ pageCount: pages.length,
83
+ claimCount: claims.length,
84
+ };
85
+ }
86
+
87
+ export function generateLlmsTxt(root, options = {}) {
88
+ const built = buildLlmsTxt(root, options);
89
+ const outDir = options.output || root;
90
+
91
+ let written = false;
92
+ let llmsTxtPath = null;
93
+ let llmsFullTxtPath = null;
94
+
95
+ if (options.write) {
96
+ fs.mkdirSync(outDir, { recursive: true });
97
+ llmsTxtPath = path.join(outDir, 'llms.txt');
98
+ llmsFullTxtPath = path.join(outDir, 'llms-full.txt');
99
+ fs.writeFileSync(llmsTxtPath, built.llmsTxt, 'utf8');
100
+ fs.writeFileSync(llmsFullTxtPath, built.llmsFullTxt, 'utf8');
101
+ written = true;
102
+ }
103
+
104
+ return {
105
+ ...built,
106
+ written,
107
+ outputDir: outDir,
108
+ llmsTxtPath,
109
+ llmsFullTxtPath,
110
+ };
111
+ }
@@ -0,0 +1,155 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { readYaml, nowIso } from '../shared/io.js';
5
+ import { buildSiteFromDir } from '../extractor/site.js';
6
+ import { runDetectors } from '../detectors/framework.js';
7
+ import { selectDetectors } from '../detectors/index.js';
8
+ import { calculateVisualSaliency } from '../analysis/saliency.js';
9
+
10
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
11
+
12
+ /**
13
+ * `citable corpus benchmark` — golden-benchmark evaluation of deterministic
14
+ * CRO detectors over a versioned, anonymized, labeled page corpus.
15
+ *
16
+ * Labels are page-level: expect_findings (must fire), expect_clean (must not
17
+ * fire), saliency bounds for the modeled conspicuity index. Output carries
18
+ * per-detector true/false positive/negative counts, precision, recall, and
19
+ * F1, plus an explicit pass/fail gate against the manifest thresholds.
20
+ *
21
+ * The benchmark evaluates detectors against synthetic fixtures; it is not
22
+ * evidence about real-world accuracy, and it never becomes a visibility or
23
+ * conversion claim.
24
+ */
25
+ export async function runGoldenBenchmark(root, { corpusDir = null, writeReport = null } = {}) {
26
+ const resolvedCorpus = corpusDir
27
+ ? path.resolve(root, corpusDir)
28
+ : path.join(MODULE_DIR, '..', '..', 'tests', 'fixtures', 'golden-corpus');
29
+ const manifestPath = path.join(resolvedCorpus, 'MANIFEST.yaml');
30
+ if (!fs.existsSync(manifestPath)) {
31
+ throw new Error(`golden corpus manifest not found: ${manifestPath}`);
32
+ }
33
+ const manifest = readYaml(manifestPath);
34
+ const namespaces = manifest.namespaces || ['CRO'];
35
+ const detectors = selectDetectors({ namespaces });
36
+ const thresholds = manifest.thresholds || { recall_min: 1.0, precision_min: 1.0 };
37
+
38
+ // per-detector confusion counters
39
+ const tally = new Map();
40
+ for (const d of detectors) {
41
+ tally.set(d.id, { detector_id: d.id, deterministic: d.deterministic, true_positives: 0, false_positives: 0, false_negatives: 0, true_negatives: 0 });
42
+ }
43
+ const pageResults = [];
44
+ const saliencyResults = [];
45
+
46
+ for (const [siteName, site] of Object.entries(manifest.sites || {})) {
47
+ const siteDir = path.join(resolvedCorpus, siteName);
48
+ if (!fs.existsSync(siteDir)) throw new Error(`corpus site directory missing: ${siteDir}`);
49
+ const built = buildSiteFromDir(siteDir, { baseUrl: `https://${siteName}.corpus.test` });
50
+ for (const page of built.pages) {
51
+ const relPage = path.relative(siteDir, page.sourceFile).split(path.sep).join('/');
52
+ const labels = (site.pages || {})[relPage] || {};
53
+ const expected = new Set(labels.expect_findings || []);
54
+ const expectClean = new Set(labels.expect_clean || []);
55
+
56
+ const scoped = { site: { ...built, pages: [page] }, registries: null, observations: null };
57
+ const { findings } = runDetectors(detectors, scoped);
58
+ const fired = new Set(findings.map((f) => f.detector_id));
59
+
60
+ for (const d of detectors) {
61
+ const t = tally.get(d.id);
62
+ const isExpected = expected.has(d.id);
63
+ const didFire = fired.has(d.id);
64
+ if (isExpected && didFire) t.true_positives++;
65
+ else if (isExpected && !didFire) t.false_negatives++;
66
+ else if (!isExpected && didFire) t.false_positives++;
67
+ else t.true_negatives++;
68
+ }
69
+
70
+ const violations = [];
71
+ for (const id of expected) {
72
+ if (!fired.has(id)) violations.push({ kind: 'false_negative', detector_id: id });
73
+ }
74
+ for (const id of fired) {
75
+ if (!expected.has(id)) {
76
+ violations.push({ kind: expectClean.has(id) ? 'labeled_clean_violation' : 'unexpected_finding', detector_id: id, summary: findings.find((f) => f.detector_id === id)?.observation?.summary });
77
+ }
78
+ }
79
+
80
+ pageResults.push({
81
+ site: siteName,
82
+ kind: site.kind,
83
+ page: relPage,
84
+ url: page.url,
85
+ expected: [...expected],
86
+ fired: [...fired],
87
+ violations,
88
+ });
89
+
90
+ if (labels.saliency) {
91
+ const sal = calculateVisualSaliency({
92
+ ctas: page.ctas.map((c) => ({ ...c })),
93
+ headings: (page.h1s || []).map((h) => ({ level: 1, text: h.text })),
94
+ forms: page.forms.map((f) => ({ fieldCount: f.fieldCount })),
95
+ });
96
+ const pciMin = labels.saliency.pci_min;
97
+ const pass = pciMin === undefined ? true : sal.primary_cta_conspicuity_index >= pciMin;
98
+ saliencyResults.push({
99
+ site: siteName,
100
+ page: relPage,
101
+ primary_cta_conspicuity_index: sal.primary_cta_conspicuity_index,
102
+ pci_min_label: pciMin ?? null,
103
+ passed: pass,
104
+ methodology: 'PCI is a modeled heuristic index over DOM geometry labels, not observed user attention',
105
+ });
106
+ }
107
+ }
108
+ }
109
+
110
+ const perDetector = [...tally.values()].map((t) => {
111
+ const precision = (t.true_positives + t.false_positives) > 0 ? t.true_positives / (t.true_positives + t.false_positives) : null;
112
+ const recall = (t.true_positives + t.false_negatives) > 0 ? t.true_positives / (t.true_positives + t.false_negatives) : null;
113
+ const f1 = precision !== null && recall !== null && (precision + recall) > 0 ? (2 * precision * recall) / (precision + recall) : null;
114
+ return { ...t, precision, recall, f1 };
115
+ });
116
+
117
+ const exercised = perDetector.filter((d) => d.true_positives + d.false_negatives > 0);
118
+ const violations = pageResults.flatMap((p) => p.violations.map((v) => ({ site: p.site, page: p.page, ...v })));
119
+ const recallFailures = exercised.filter((d) => d.recall < thresholds.recall_min);
120
+ const precisionFailures = perDetector.filter((d) => d.precision !== null && d.precision < thresholds.precision_min);
121
+
122
+ const result = {
123
+ benchmark: 'golden-corpus',
124
+ corpus_version: manifest.corpus_version,
125
+ corpus_dir: path.relative(root, resolvedCorpus) || resolvedCorpus,
126
+ anonymized: Boolean(manifest.anonymized),
127
+ timestamp: nowIso(),
128
+ tool_version: JSON.parse(fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version,
129
+ thresholds,
130
+ sites_evaluated: Object.keys(manifest.sites || {}).length,
131
+ pages_evaluated: pageResults.length,
132
+ per_detector: perDetector,
133
+ saliency: saliencyResults,
134
+ violations,
135
+ gate: {
136
+ ok: recallFailures.length === 0 && precisionFailures.length === 0 && violations.length === 0,
137
+ recall_failures: recallFailures.map((d) => d.detector_id),
138
+ precision_failures: precisionFailures.map((d) => d.detector_id),
139
+ violation_count: violations.length,
140
+ },
141
+ limitations: [
142
+ 'Synthetic fixture benchmark: measures consistency with labels, not real-world accuracy.',
143
+ 'Registry-dependent detectors (funnels, experiments) are not exercised by this corpus.',
144
+ 'Saliency bounds evaluate a modeled heuristic index, not observed user attention.',
145
+ ],
146
+ };
147
+
148
+ if (writeReport) {
149
+ const outFull = path.resolve(root, writeReport);
150
+ fs.mkdirSync(path.dirname(outFull), { recursive: true });
151
+ fs.writeFileSync(outFull, JSON.stringify(result, null, 2) + '\n', 'utf8');
152
+ result.report_path = outFull;
153
+ }
154
+ return result;
155
+ }
@@ -0,0 +1,202 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { readJson, nowIso } from '../shared/io.js';
4
+ import { remediateCommand } from './remediate.js';
5
+
6
+ const PKG_VERSION = JSON.parse(
7
+ fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf8')
8
+ ).version;
9
+
10
+ const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.svg']);
11
+
12
+ function findRenderEvidence(runDir) {
13
+ const shots = [];
14
+ const walk = (d) => {
15
+ for (const name of fs.readdirSync(d)) {
16
+ const p = path.join(d, name);
17
+ if (fs.statSync(p).isDirectory()) walk(p);
18
+ else if (IMAGE_EXTS.has(path.extname(name).toLowerCase())) shots.push(path.relative(runDir, p));
19
+ }
20
+ };
21
+ if (fs.existsSync(runDir)) walk(runDir);
22
+ return shots;
23
+ }
24
+
25
+ function acceptanceTestsFor(finding, rem) {
26
+ const lines = [
27
+ '## Acceptance tests',
28
+ '',
29
+ '### 1. Deterministic detector re-check (authoritative)',
30
+ '```',
31
+ `citable verify remediation --run <source-run-id> --finding ${finding.detector_id} --target <file> --apply`,
32
+ '```',
33
+ 'Pass criterion: status `verified` — the detector no longer reports this finding for the same subject. Detector absence is not an outcome guarantee.',
34
+ '',
35
+ ];
36
+ if (rem?.scaffold_command) {
37
+ lines.push(`### 2. Component integration`, '```', rem.scaffold_command, '```', 'After scaffolding, confirm the component renders without console errors in the consuming page.');
38
+ lines.push('');
39
+ }
40
+ lines.push(
41
+ '### 3. Manual review gates',
42
+ '- Confirm the change preserves factual accuracy, human comprehension, accessibility, and conversion function.',
43
+ '- Run the repository build/test suite. A failed build makes "validated" an unavailable conclusion.',
44
+ '- Capture after-change rendering evidence (screenshot) for the customer record.'
45
+ );
46
+ return lines.join('\n');
47
+ }
48
+
49
+ function deploymentFor(rem) {
50
+ const fw = rem?.framework?.framework || 'unknown';
51
+ const lines = [
52
+ '## Deployment instructions',
53
+ '',
54
+ `Detected framework: **${fw}**${rem?.framework ? ` (detection confidence ${rem.framework.confidence}; signals: ${rem.framework.signals.join(', ') || 'none'})` : ''}`,
55
+ '',
56
+ '1. Review `patch.diff`. Apply with `citable remediate --finding <id> --target <file> --write` or by hand from the diff.',
57
+ '2. Run your project build and test commands. The Citable static check does not replace your compiler.',
58
+ '3. Deploy through your normal pipeline; commit the rollback snapshot path noted in the verification record.',
59
+ '4. Frozen-CMS alternative: regenerate the edge adapter with `citable export edge --format cloudflare-cro` (Cloudflare Workers) and verify with `citable test edge`. Edge changes ship without a code deploy but remain subject to the same detector verification.',
60
+ '',
61
+ 'Rollback: restore the snapshot recorded in `.citable/remediation/snapshots/` (see verification record `rollback_snapshot`).',
62
+ ];
63
+ return lines.join('\n');
64
+ }
65
+
66
+ /**
67
+ * `citable kit export` — customer-ready implementation kit connecting audit
68
+ * findings to Nebula's fulfillment artifact: explanation, exact changes,
69
+ * rendering evidence, acceptance tests, deployment steps, re-audit command,
70
+ * and explicit limitations. Screenshots are included only when real rendering
71
+ * evidence exists in the source run; Citable never fabricates imagery.
72
+ */
73
+ export async function exportImplementationKit(root, options = {}) {
74
+ const { run: runId, finding, subject = null, target = null, output = null } = options;
75
+ const runDir = path.join(root, '.citable', 'runs', runId || '');
76
+ if (!runId || !fs.existsSync(path.join(runDir, 'findings.json'))) {
77
+ throw new Error(`source run not found: .citable/runs/${runId || '(none)'}`);
78
+ }
79
+ const findings = readJson(path.join(runDir, 'findings.json'));
80
+ const matches = findings.filter((f) =>
81
+ (f.detector_id || '').toUpperCase() === String(finding || '').toUpperCase()
82
+ && (!subject || (f.subject?.identifier || f.subject?.url) === subject));
83
+ if (matches.length === 0) {
84
+ throw new Error(`finding ${finding} not present in run ${runId}`);
85
+ }
86
+
87
+ const f = matches[0];
88
+ const patch = target
89
+ ? await remediateCommand(root, { finding: f.detector_id, target, format: options.format || 'react' })
90
+ : await remediateCommand(root, { finding: f.detector_id, format: options.format || 'react' });
91
+
92
+ const slug = `${f.detector_id}-${String(subject || (f.subject?.identifier || f.subject?.url || 'subject'))
93
+ .replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 48)}`;
94
+ const kitDir = output ? path.resolve(root, output) : path.join(root, '.citable', 'kits', `${nowIso().replace(/\D/g, '').slice(0, 14)}-${slug}`);
95
+ fs.mkdirSync(path.join(kitDir, 'evidence'), { recursive: true });
96
+
97
+ // finding explanation (verbatim from the immutable run)
98
+ fs.writeFileSync(path.join(kitDir, 'finding.json'), JSON.stringify(matches, null, 2) + '\n');
99
+
100
+ // exact change: unified diff when a target file is known, else component code
101
+ if (target && patch.diff) {
102
+ fs.writeFileSync(path.join(kitDir, 'patch.diff'), patch.diff);
103
+ fs.writeFileSync(path.join(kitDir, 'patch-metadata.json'), JSON.stringify({
104
+ target_file: target,
105
+ framework: patch.framework,
106
+ patcher_class: patch.patcher_class,
107
+ confidence: patch.confidence?.score ?? null,
108
+ validation: patch.validation,
109
+ write_policy: patch.write_policy,
110
+ refusal: patch.write_refused ? patch.refusal_reason : null,
111
+ }, null, 2) + '\n');
112
+ }
113
+ if (patch.code) {
114
+ fs.writeFileSync(path.join(kitDir, `component-${patch.format || 'react'}.${patch.format === 'vue' ? 'vue' : patch.format === 'html' ? 'html' : 'jsx'}`), patch.code + '\n');
115
+ }
116
+
117
+ // before/after screenshots: only real rendering evidence from the run
118
+ const shots = findRenderEvidence(runDir);
119
+ for (const rel of shots) {
120
+ fs.copyFileSync(path.join(runDir, rel), path.join(kitDir, 'evidence', path.basename(rel)));
121
+ }
122
+ fs.writeFileSync(path.join(kitDir, 'evidence', 'MANIFEST.md'), shots.length
123
+ ? `Rendering evidence files copied from run ${runId}:\n${shots.map((s) => `- ${s}`).join('\n')}\nThese are before-state observations unless a paired post-remediation run is referenced. Citable does not fabricate imagery.\n`
124
+ : `No rendering evidence (screenshots) was present in run ${runId}. Collect after-state evidence with \`citable observe render --target <url>\` before and after the change. No placeholder imagery is provided because Citable does not fabricate evidence.\n`);
125
+
126
+ fs.writeFileSync(path.join(kitDir, 'acceptance-tests.md'), acceptanceTestsFor(f, patch) + '\n');
127
+ fs.writeFileSync(path.join(kitDir, 'deployment.md'), deploymentFor(patch) + '\n');
128
+
129
+ const ns = f.detector_id.split('-')[0].toLowerCase();
130
+ fs.writeFileSync(path.join(kitDir, 're-audit.sh'), `#!/bin/sh
131
+ # Re-audit the affected scope after deployment and compare against the source run.
132
+ set -eu
133
+ citable audit --scope ${ns} --target <deployed-target> --base-url <base-url>
134
+ citable verify remediation --run ${runId} --finding ${f.detector_id} --target <file> --apply
135
+ citable compare-snapshots ${runId} <new-run-id>
136
+ ` + '\n');
137
+ fs.chmodSync(path.join(kitDir, 're-audit.sh'), 0o755);
138
+
139
+ const limitations = [
140
+ ...(f.reasoning?.limitations || []),
141
+ 'This kit contains no modeled revenue or conversion-lift estimates; impact projections are excluded by policy.',
142
+ 'Detection was performed against a snapshot; the deployed property may differ by the time this kit is applied.',
143
+ 'A verified patch resolves the detector condition only; it is not a guarantee of conversion, ranking, or citation outcomes.',
144
+ ];
145
+ const assumptions = f.reasoning?.assumptions || [];
146
+ fs.writeFileSync(path.join(kitDir, 'limitations.md'), [
147
+ '## Limitations', '', ...limitations.map((l) => `- ${l}`), '',
148
+ '## Assumptions', '',
149
+ ...(assumptions.length ? assumptions.map((a) => `- ${a}`) : ['- No additional assumptions recorded by the detector.']),
150
+ ].join('\n') + '\n');
151
+
152
+ fs.writeFileSync(path.join(kitDir, 'README.md'), [
153
+ `# Implementation Kit — ${f.detector_id}`,
154
+ '',
155
+ `**Finding:** ${f.observation?.summary}`,
156
+ `**Subject:** ${f.subject?.identifier || f.subject?.url || 'unknown'}`,
157
+ `**Severity:** ${f.classification?.severity} | **Confidence:** ${f.classification?.confidence}`,
158
+ `**Source run:** ${runId} | **Kit generated:** ${nowIso()} by Citable ${PKG_VERSION}`,
159
+ '',
160
+ '## Contents',
161
+ '',
162
+ '- `finding.json` — verbatim finding record from the immutable source run',
163
+ target && patch.diff ? '- `patch.diff` — unified diff of the exact proposed change (dry-run; nothing was written)' : '- (no patch.diff: no target file supplied; component template below)',
164
+ patch.code ? '- `component-*.jsx|vue|html` — recommended Nebula Component template' : null,
165
+ '- `patch-metadata.json` — framework detection, confidence, validation checks (when target supplied)',
166
+ '- `evidence/` — rendering evidence from the source run (see evidence/MANIFEST.md)',
167
+ '- `acceptance-tests.md` — detector re-check plus manual review gates',
168
+ '- `deployment.md` — deployment and rollback instructions',
169
+ '- `re-audit.sh` — re-audit and verification commands',
170
+ '- `limitations.md` — limitations and assumptions (fact vs inference separation)',
171
+ '',
172
+ '## Order of operations',
173
+ '',
174
+ '1. Review finding and diff; assign an accountable owner.',
175
+ '2. Apply the smallest change (patch or component).',
176
+ '3. Run acceptance tests, then deployment.',
177
+ '4. Run `re-audit.sh`; keep the verification bundle with the customer record.',
178
+ '',
179
+ 'Citable does not guarantee crawling, indexing, ranking, citation, recommendation, or conversion.',
180
+ ].filter(Boolean).join('\n') + '\n');
181
+
182
+ const files = [];
183
+ const walk = (d) => {
184
+ for (const name of fs.readdirSync(d)) {
185
+ const p = path.join(d, name);
186
+ if (fs.statSync(p).isDirectory()) walk(p);
187
+ else files.push(path.relative(kitDir, p));
188
+ }
189
+ };
190
+ walk(kitDir);
191
+
192
+ return {
193
+ ok: true,
194
+ finding_id: f.finding_id,
195
+ detector_id: f.detector_id,
196
+ source_run_id: runId,
197
+ kit_dir: path.relative(root, kitDir),
198
+ files: files.sort(),
199
+ rendering_evidence_count: shots.length,
200
+ has_patch_diff: Boolean(target && patch.diff),
201
+ };
202
+ }
@@ -0,0 +1,74 @@
1
+ import { buildContext } from './context.js';
2
+ import { registryPageFor, safePath } from '../detectors/framework.js';
3
+ import { selectDetectors } from '../detectors/index.js';
4
+ import { runDetectors } from '../detectors/framework.js';
5
+
6
+ /**
7
+ * `citable inspect aeo <page>` — evaluate page answer extractability, question density,
8
+ * direct lead conciseness, grounded quantitative metrics, and synthesis readiness.
9
+ */
10
+ export async function inspectAeo(root, pageRef, { target, baseUrl, refDate } = {}) {
11
+ const ctx = await buildContext(root, { target, baseUrl, refDate });
12
+ if (!ctx.site) throw new Error('inspect aeo requires a site target (built output directory or URL)');
13
+ const wanted = safePath(pageRef, ctx.site.baseUrl);
14
+ const page = ctx.site.pages.find((p) => safePath(p.url) === wanted || p.sourceFile === pageRef);
15
+ if (!page) throw new Error(`page not found in audited output: ${pageRef}`);
16
+
17
+ const reg = registryPageFor(ctx, page);
18
+
19
+ // Evaluate findings for this single page across ANS namespace
20
+ const single = { ...ctx, site: { ...ctx.site, pages: [page] } };
21
+ const { findings } = runDetectors(selectDetectors({ namespaces: ['ANS'] }), single);
22
+ const aeoFindings = findings.filter((f) => f.subject.identifier === page.url || f.subject.url === page.url);
23
+
24
+ // Analyze question headings and answer leads
25
+ const QUESTION_RX = /^(?:how|what|why|when|where|who|can|is|does|which|should)\b|\?$/i;
26
+ const questionHeadings = (page.headings || []).filter((h) => QUESTION_RX.test(h.text.trim()));
27
+
28
+ // Definitional copula check
29
+ const COPULA_RX = /\b(?:is|are|refers\s+to|denotes|represents|means)\b/i;
30
+ const firstParagraph = (page.paragraphs && page.paragraphs[0]) || '';
31
+ const hasCopularDefinition = COPULA_RX.test(firstParagraph.slice(0, 140));
32
+
33
+ // Structured elements
34
+ const tablesCount = (page.tables || []).length;
35
+ const listsCount = (page.orderedLists || []).length;
36
+
37
+ // Executive summary
38
+ const SUMMARY_RX = /\b(?:executive\s+summary|key\s+takeaways?|summary|overview|at\s+a\s+glance|tl;?dr|quick\s+summary|highlights?)\b/i;
39
+ const hasExecutiveSummary = (page.headings || []).some((h) => SUMMARY_RX.test(h.text));
40
+
41
+ // Determine synthesis posture
42
+ let extractabilityStatus = 'optimal';
43
+ if (aeoFindings.some((f) => f.classification.severity === 'high')) {
44
+ extractabilityStatus = 'obstructed';
45
+ } else if (aeoFindings.length >= 2) {
46
+ extractabilityStatus = 'suboptimal';
47
+ } else if (aeoFindings.length === 1) {
48
+ extractabilityStatus = 'minor_improvements';
49
+ }
50
+
51
+ return {
52
+ url: page.url,
53
+ sourceFile: page.sourceFile,
54
+ status: page.status,
55
+ page_type: reg?.page_type ?? null,
56
+ word_count: page.rawVisibleWordCount || page.wordCount || 0,
57
+ extractability_status: extractabilityStatus,
58
+ headings: {
59
+ total: (page.headings || []).length,
60
+ questions: questionHeadings.map((h) => ({ level: h.level, text: h.text })),
61
+ },
62
+ answer_structure: {
63
+ has_copular_definition: hasCopularDefinition,
64
+ has_executive_summary: hasExecutiveSummary,
65
+ tables_count: tablesCount,
66
+ ordered_lists_count: listsCount,
67
+ },
68
+ findings: aeoFindings.map((f) => ({
69
+ detector_id: f.detector_id,
70
+ severity: f.classification.severity,
71
+ summary: f.observation.summary,
72
+ })),
73
+ };
74
+ }