@nebulacomponents/citable 1.1.0 → 1.3.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 (248) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +14 -2
  3. package/dist/universal/.agents/skills/citable/SKILL.md +49 -1
  4. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  5. package/dist/universal/.agents/skills/citable/commands/README.md +1 -0
  6. package/dist/universal/.agents/skills/citable/commands/action-findings.md +37 -0
  7. package/dist/universal/.agents/skills/citable/manifest.json +20 -16
  8. package/dist/universal/.agents/skills/citable/references/aeo-geo-validation.md +50 -0
  9. package/dist/universal/.agents/skills/citable/references/capability-boundaries.md +34 -0
  10. package/dist/universal/.agents/skills/citable/schemas/action-plan.schema.json +55 -0
  11. package/dist/universal/.agents/skills/citable/schemas/claim.schema.json +2 -0
  12. package/dist/universal/.agents/skills/citable/schemas/common.defs.json +2 -0
  13. package/dist/universal/.agents/skills/citable/schemas/competitor.schema.json +1 -0
  14. package/dist/universal/.agents/skills/citable/schemas/crawler.schema.json +1 -0
  15. package/dist/universal/.agents/skills/citable/schemas/entity.schema.json +2 -1
  16. package/dist/universal/.agents/skills/citable/schemas/evidence.schema.json +1 -0
  17. package/dist/universal/.agents/skills/citable/schemas/experiment.schema.json +1 -0
  18. package/dist/universal/.agents/skills/citable/schemas/page.schema.json +1 -0
  19. package/dist/universal/.agents/skills/citable/schemas/prompt.schema.json +1 -0
  20. package/dist/universal/.agents/skills/citable/schemas/query.schema.json +1 -0
  21. package/dist/universal/.claude/skills/citable/SKILL.md +49 -1
  22. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  23. package/dist/universal/.claude/skills/citable/commands/README.md +1 -0
  24. package/dist/universal/.claude/skills/citable/commands/action-findings.md +37 -0
  25. package/dist/universal/.claude/skills/citable/manifest.json +20 -16
  26. package/dist/universal/.claude/skills/citable/references/aeo-geo-validation.md +50 -0
  27. package/dist/universal/.claude/skills/citable/references/capability-boundaries.md +34 -0
  28. package/dist/universal/.claude/skills/citable/schemas/action-plan.schema.json +55 -0
  29. package/dist/universal/.claude/skills/citable/schemas/claim.schema.json +2 -0
  30. package/dist/universal/.claude/skills/citable/schemas/common.defs.json +2 -0
  31. package/dist/universal/.claude/skills/citable/schemas/competitor.schema.json +1 -0
  32. package/dist/universal/.claude/skills/citable/schemas/crawler.schema.json +1 -0
  33. package/dist/universal/.claude/skills/citable/schemas/entity.schema.json +2 -1
  34. package/dist/universal/.claude/skills/citable/schemas/evidence.schema.json +1 -0
  35. package/dist/universal/.claude/skills/citable/schemas/experiment.schema.json +1 -0
  36. package/dist/universal/.claude/skills/citable/schemas/page.schema.json +1 -0
  37. package/dist/universal/.claude/skills/citable/schemas/prompt.schema.json +1 -0
  38. package/dist/universal/.claude/skills/citable/schemas/query.schema.json +1 -0
  39. package/dist/universal/.cursor/skills/citable/SKILL.md +49 -1
  40. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  41. package/dist/universal/.cursor/skills/citable/commands/README.md +1 -0
  42. package/dist/universal/.cursor/skills/citable/commands/action-findings.md +37 -0
  43. package/dist/universal/.cursor/skills/citable/manifest.json +20 -16
  44. package/dist/universal/.cursor/skills/citable/references/aeo-geo-validation.md +50 -0
  45. package/dist/universal/.cursor/skills/citable/references/capability-boundaries.md +34 -0
  46. package/dist/universal/.cursor/skills/citable/schemas/action-plan.schema.json +55 -0
  47. package/dist/universal/.cursor/skills/citable/schemas/claim.schema.json +2 -0
  48. package/dist/universal/.cursor/skills/citable/schemas/common.defs.json +2 -0
  49. package/dist/universal/.cursor/skills/citable/schemas/competitor.schema.json +1 -0
  50. package/dist/universal/.cursor/skills/citable/schemas/crawler.schema.json +1 -0
  51. package/dist/universal/.cursor/skills/citable/schemas/entity.schema.json +2 -1
  52. package/dist/universal/.cursor/skills/citable/schemas/evidence.schema.json +1 -0
  53. package/dist/universal/.cursor/skills/citable/schemas/experiment.schema.json +1 -0
  54. package/dist/universal/.cursor/skills/citable/schemas/page.schema.json +1 -0
  55. package/dist/universal/.cursor/skills/citable/schemas/prompt.schema.json +1 -0
  56. package/dist/universal/.cursor/skills/citable/schemas/query.schema.json +1 -0
  57. package/dist/universal/.gemini/skills/citable/SKILL.md +49 -1
  58. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  59. package/dist/universal/.gemini/skills/citable/commands/README.md +1 -0
  60. package/dist/universal/.gemini/skills/citable/commands/action-findings.md +37 -0
  61. package/dist/universal/.gemini/skills/citable/manifest.json +20 -16
  62. package/dist/universal/.gemini/skills/citable/references/aeo-geo-validation.md +50 -0
  63. package/dist/universal/.gemini/skills/citable/references/capability-boundaries.md +34 -0
  64. package/dist/universal/.gemini/skills/citable/schemas/action-plan.schema.json +55 -0
  65. package/dist/universal/.gemini/skills/citable/schemas/claim.schema.json +2 -0
  66. package/dist/universal/.gemini/skills/citable/schemas/common.defs.json +2 -0
  67. package/dist/universal/.gemini/skills/citable/schemas/competitor.schema.json +1 -0
  68. package/dist/universal/.gemini/skills/citable/schemas/crawler.schema.json +1 -0
  69. package/dist/universal/.gemini/skills/citable/schemas/entity.schema.json +2 -1
  70. package/dist/universal/.gemini/skills/citable/schemas/evidence.schema.json +1 -0
  71. package/dist/universal/.gemini/skills/citable/schemas/experiment.schema.json +1 -0
  72. package/dist/universal/.gemini/skills/citable/schemas/page.schema.json +1 -0
  73. package/dist/universal/.gemini/skills/citable/schemas/prompt.schema.json +1 -0
  74. package/dist/universal/.gemini/skills/citable/schemas/query.schema.json +1 -0
  75. package/dist/universal/.github/skills/citable/SKILL.md +49 -1
  76. package/dist/universal/.github/skills/citable/VERSION +1 -1
  77. package/dist/universal/.github/skills/citable/commands/README.md +1 -0
  78. package/dist/universal/.github/skills/citable/commands/action-findings.md +37 -0
  79. package/dist/universal/.github/skills/citable/manifest.json +20 -16
  80. package/dist/universal/.github/skills/citable/references/aeo-geo-validation.md +50 -0
  81. package/dist/universal/.github/skills/citable/references/capability-boundaries.md +34 -0
  82. package/dist/universal/.github/skills/citable/schemas/action-plan.schema.json +55 -0
  83. package/dist/universal/.github/skills/citable/schemas/claim.schema.json +2 -0
  84. package/dist/universal/.github/skills/citable/schemas/common.defs.json +2 -0
  85. package/dist/universal/.github/skills/citable/schemas/competitor.schema.json +1 -0
  86. package/dist/universal/.github/skills/citable/schemas/crawler.schema.json +1 -0
  87. package/dist/universal/.github/skills/citable/schemas/entity.schema.json +2 -1
  88. package/dist/universal/.github/skills/citable/schemas/evidence.schema.json +1 -0
  89. package/dist/universal/.github/skills/citable/schemas/experiment.schema.json +1 -0
  90. package/dist/universal/.github/skills/citable/schemas/page.schema.json +1 -0
  91. package/dist/universal/.github/skills/citable/schemas/prompt.schema.json +1 -0
  92. package/dist/universal/.github/skills/citable/schemas/query.schema.json +1 -0
  93. package/dist/universal/.kiro/skills/citable/SKILL.md +49 -1
  94. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  95. package/dist/universal/.kiro/skills/citable/commands/README.md +1 -0
  96. package/dist/universal/.kiro/skills/citable/commands/action-findings.md +37 -0
  97. package/dist/universal/.kiro/skills/citable/manifest.json +20 -16
  98. package/dist/universal/.kiro/skills/citable/references/aeo-geo-validation.md +50 -0
  99. package/dist/universal/.kiro/skills/citable/references/capability-boundaries.md +34 -0
  100. package/dist/universal/.kiro/skills/citable/schemas/action-plan.schema.json +55 -0
  101. package/dist/universal/.kiro/skills/citable/schemas/claim.schema.json +2 -0
  102. package/dist/universal/.kiro/skills/citable/schemas/common.defs.json +2 -0
  103. package/dist/universal/.kiro/skills/citable/schemas/competitor.schema.json +1 -0
  104. package/dist/universal/.kiro/skills/citable/schemas/crawler.schema.json +1 -0
  105. package/dist/universal/.kiro/skills/citable/schemas/entity.schema.json +2 -1
  106. package/dist/universal/.kiro/skills/citable/schemas/evidence.schema.json +1 -0
  107. package/dist/universal/.kiro/skills/citable/schemas/experiment.schema.json +1 -0
  108. package/dist/universal/.kiro/skills/citable/schemas/page.schema.json +1 -0
  109. package/dist/universal/.kiro/skills/citable/schemas/prompt.schema.json +1 -0
  110. package/dist/universal/.kiro/skills/citable/schemas/query.schema.json +1 -0
  111. package/dist/universal/.opencode/skills/citable/SKILL.md +49 -1
  112. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  113. package/dist/universal/.opencode/skills/citable/commands/README.md +1 -0
  114. package/dist/universal/.opencode/skills/citable/commands/action-findings.md +37 -0
  115. package/dist/universal/.opencode/skills/citable/manifest.json +20 -16
  116. package/dist/universal/.opencode/skills/citable/references/aeo-geo-validation.md +50 -0
  117. package/dist/universal/.opencode/skills/citable/references/capability-boundaries.md +34 -0
  118. package/dist/universal/.opencode/skills/citable/schemas/action-plan.schema.json +55 -0
  119. package/dist/universal/.opencode/skills/citable/schemas/claim.schema.json +2 -0
  120. package/dist/universal/.opencode/skills/citable/schemas/common.defs.json +2 -0
  121. package/dist/universal/.opencode/skills/citable/schemas/competitor.schema.json +1 -0
  122. package/dist/universal/.opencode/skills/citable/schemas/crawler.schema.json +1 -0
  123. package/dist/universal/.opencode/skills/citable/schemas/entity.schema.json +2 -1
  124. package/dist/universal/.opencode/skills/citable/schemas/evidence.schema.json +1 -0
  125. package/dist/universal/.opencode/skills/citable/schemas/experiment.schema.json +1 -0
  126. package/dist/universal/.opencode/skills/citable/schemas/page.schema.json +1 -0
  127. package/dist/universal/.opencode/skills/citable/schemas/prompt.schema.json +1 -0
  128. package/dist/universal/.opencode/skills/citable/schemas/query.schema.json +1 -0
  129. package/dist/universal/.pi/agent/skills/citable/SKILL.md +49 -1
  130. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  131. package/dist/universal/.pi/agent/skills/citable/commands/README.md +1 -0
  132. package/dist/universal/.pi/agent/skills/citable/commands/action-findings.md +37 -0
  133. package/dist/universal/.pi/agent/skills/citable/manifest.json +20 -16
  134. package/dist/universal/.pi/agent/skills/citable/references/aeo-geo-validation.md +50 -0
  135. package/dist/universal/.pi/agent/skills/citable/references/capability-boundaries.md +34 -0
  136. package/dist/universal/.pi/agent/skills/citable/schemas/action-plan.schema.json +55 -0
  137. package/dist/universal/.pi/agent/skills/citable/schemas/claim.schema.json +2 -0
  138. package/dist/universal/.pi/agent/skills/citable/schemas/common.defs.json +2 -0
  139. package/dist/universal/.pi/agent/skills/citable/schemas/competitor.schema.json +1 -0
  140. package/dist/universal/.pi/agent/skills/citable/schemas/crawler.schema.json +1 -0
  141. package/dist/universal/.pi/agent/skills/citable/schemas/entity.schema.json +2 -1
  142. package/dist/universal/.pi/agent/skills/citable/schemas/evidence.schema.json +1 -0
  143. package/dist/universal/.pi/agent/skills/citable/schemas/experiment.schema.json +1 -0
  144. package/dist/universal/.pi/agent/skills/citable/schemas/page.schema.json +1 -0
  145. package/dist/universal/.pi/agent/skills/citable/schemas/prompt.schema.json +1 -0
  146. package/dist/universal/.pi/agent/skills/citable/schemas/query.schema.json +1 -0
  147. package/dist/universal/.qoder/skills/citable/SKILL.md +49 -1
  148. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  149. package/dist/universal/.qoder/skills/citable/commands/README.md +1 -0
  150. package/dist/universal/.qoder/skills/citable/commands/action-findings.md +37 -0
  151. package/dist/universal/.qoder/skills/citable/manifest.json +20 -16
  152. package/dist/universal/.qoder/skills/citable/references/aeo-geo-validation.md +50 -0
  153. package/dist/universal/.qoder/skills/citable/references/capability-boundaries.md +34 -0
  154. package/dist/universal/.qoder/skills/citable/schemas/action-plan.schema.json +55 -0
  155. package/dist/universal/.qoder/skills/citable/schemas/claim.schema.json +2 -0
  156. package/dist/universal/.qoder/skills/citable/schemas/common.defs.json +2 -0
  157. package/dist/universal/.qoder/skills/citable/schemas/competitor.schema.json +1 -0
  158. package/dist/universal/.qoder/skills/citable/schemas/crawler.schema.json +1 -0
  159. package/dist/universal/.qoder/skills/citable/schemas/entity.schema.json +2 -1
  160. package/dist/universal/.qoder/skills/citable/schemas/evidence.schema.json +1 -0
  161. package/dist/universal/.qoder/skills/citable/schemas/experiment.schema.json +1 -0
  162. package/dist/universal/.qoder/skills/citable/schemas/page.schema.json +1 -0
  163. package/dist/universal/.qoder/skills/citable/schemas/prompt.schema.json +1 -0
  164. package/dist/universal/.qoder/skills/citable/schemas/query.schema.json +1 -0
  165. package/dist/universal/.rovodev/skills/citable/SKILL.md +49 -1
  166. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  167. package/dist/universal/.rovodev/skills/citable/commands/README.md +1 -0
  168. package/dist/universal/.rovodev/skills/citable/commands/action-findings.md +37 -0
  169. package/dist/universal/.rovodev/skills/citable/manifest.json +20 -16
  170. package/dist/universal/.rovodev/skills/citable/references/aeo-geo-validation.md +50 -0
  171. package/dist/universal/.rovodev/skills/citable/references/capability-boundaries.md +34 -0
  172. package/dist/universal/.rovodev/skills/citable/schemas/action-plan.schema.json +55 -0
  173. package/dist/universal/.rovodev/skills/citable/schemas/claim.schema.json +2 -0
  174. package/dist/universal/.rovodev/skills/citable/schemas/common.defs.json +2 -0
  175. package/dist/universal/.rovodev/skills/citable/schemas/competitor.schema.json +1 -0
  176. package/dist/universal/.rovodev/skills/citable/schemas/crawler.schema.json +1 -0
  177. package/dist/universal/.rovodev/skills/citable/schemas/entity.schema.json +2 -1
  178. package/dist/universal/.rovodev/skills/citable/schemas/evidence.schema.json +1 -0
  179. package/dist/universal/.rovodev/skills/citable/schemas/experiment.schema.json +1 -0
  180. package/dist/universal/.rovodev/skills/citable/schemas/page.schema.json +1 -0
  181. package/dist/universal/.rovodev/skills/citable/schemas/prompt.schema.json +1 -0
  182. package/dist/universal/.rovodev/skills/citable/schemas/query.schema.json +1 -0
  183. package/dist/universal/.trae/skills/citable/SKILL.md +49 -1
  184. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  185. package/dist/universal/.trae/skills/citable/commands/README.md +1 -0
  186. package/dist/universal/.trae/skills/citable/commands/action-findings.md +37 -0
  187. package/dist/universal/.trae/skills/citable/manifest.json +20 -16
  188. package/dist/universal/.trae/skills/citable/references/aeo-geo-validation.md +50 -0
  189. package/dist/universal/.trae/skills/citable/references/capability-boundaries.md +34 -0
  190. package/dist/universal/.trae/skills/citable/schemas/action-plan.schema.json +55 -0
  191. package/dist/universal/.trae/skills/citable/schemas/claim.schema.json +2 -0
  192. package/dist/universal/.trae/skills/citable/schemas/common.defs.json +2 -0
  193. package/dist/universal/.trae/skills/citable/schemas/competitor.schema.json +1 -0
  194. package/dist/universal/.trae/skills/citable/schemas/crawler.schema.json +1 -0
  195. package/dist/universal/.trae/skills/citable/schemas/entity.schema.json +2 -1
  196. package/dist/universal/.trae/skills/citable/schemas/evidence.schema.json +1 -0
  197. package/dist/universal/.trae/skills/citable/schemas/experiment.schema.json +1 -0
  198. package/dist/universal/.trae/skills/citable/schemas/page.schema.json +1 -0
  199. package/dist/universal/.trae/skills/citable/schemas/prompt.schema.json +1 -0
  200. package/dist/universal/.trae/skills/citable/schemas/query.schema.json +1 -0
  201. package/dist/universal/.trae-cn/skills/citable/SKILL.md +49 -1
  202. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  203. package/dist/universal/.trae-cn/skills/citable/commands/README.md +1 -0
  204. package/dist/universal/.trae-cn/skills/citable/commands/action-findings.md +37 -0
  205. package/dist/universal/.trae-cn/skills/citable/manifest.json +20 -16
  206. package/dist/universal/.trae-cn/skills/citable/references/aeo-geo-validation.md +50 -0
  207. package/dist/universal/.trae-cn/skills/citable/references/capability-boundaries.md +34 -0
  208. package/dist/universal/.trae-cn/skills/citable/schemas/action-plan.schema.json +55 -0
  209. package/dist/universal/.trae-cn/skills/citable/schemas/claim.schema.json +2 -0
  210. package/dist/universal/.trae-cn/skills/citable/schemas/common.defs.json +2 -0
  211. package/dist/universal/.trae-cn/skills/citable/schemas/competitor.schema.json +1 -0
  212. package/dist/universal/.trae-cn/skills/citable/schemas/crawler.schema.json +1 -0
  213. package/dist/universal/.trae-cn/skills/citable/schemas/entity.schema.json +2 -1
  214. package/dist/universal/.trae-cn/skills/citable/schemas/evidence.schema.json +1 -0
  215. package/dist/universal/.trae-cn/skills/citable/schemas/experiment.schema.json +1 -0
  216. package/dist/universal/.trae-cn/skills/citable/schemas/page.schema.json +1 -0
  217. package/dist/universal/.trae-cn/skills/citable/schemas/prompt.schema.json +1 -0
  218. package/dist/universal/.trae-cn/skills/citable/schemas/query.schema.json +1 -0
  219. package/dist/universal/manifest.json +243 -195
  220. package/package.json +1 -1
  221. package/schemas/action-plan.schema.json +55 -0
  222. package/schemas/claim.schema.json +2 -0
  223. package/schemas/common.defs.json +2 -0
  224. package/schemas/competitor.schema.json +1 -0
  225. package/schemas/crawler.schema.json +1 -0
  226. package/schemas/entity.schema.json +2 -1
  227. package/schemas/evidence.schema.json +1 -0
  228. package/schemas/experiment.schema.json +1 -0
  229. package/schemas/page.schema.json +1 -0
  230. package/schemas/prompt.schema.json +1 -0
  231. package/schemas/query.schema.json +1 -0
  232. package/skill/SKILL.md +49 -1
  233. package/skill/commands/README.md +1 -0
  234. package/skill/commands/action-findings.md +37 -0
  235. package/skill/references/aeo-geo-validation.md +50 -0
  236. package/skill/references/capability-boundaries.md +34 -0
  237. package/src/cli/index.js +19 -0
  238. package/src/commands/actionPlan.js +120 -0
  239. package/src/commands/audit.js +6 -1
  240. package/src/commands/context.js +2 -2
  241. package/src/commands/selfUpgrade.js +155 -0
  242. package/src/commands/substantiate.js +2 -2
  243. package/src/crawler/fetch.js +98 -41
  244. package/src/detectors/ans.js +43 -0
  245. package/src/detectors/geoReco.js +48 -0
  246. package/src/extractor/site.js +12 -4
  247. package/src/reporting/report.js +58 -1
  248. package/src/shared/io.js +12 -0
@@ -0,0 +1,155 @@
1
+ /**
2
+ * self-upgrade command
3
+ *
4
+ * Checks npm for the latest @nebulacomponents/citable version and upgrades
5
+ * the running npx cache if a newer version is available.
6
+ *
7
+ * Usage:
8
+ * npx @nebulacomponents/citable self-upgrade
9
+ * npx @nebulacomponents/citable self-upgrade --check (check only, no upgrade)
10
+ * npx @nebulacomponents/citable self-upgrade --json (machine-readable output)
11
+ */
12
+
13
+ import { execFileSync, spawnSync } from 'node:child_process';
14
+ import { createRequire } from 'node:module';
15
+ import { fileURLToPath } from 'node:url';
16
+ import path from 'node:path';
17
+
18
+ const PACKAGE_NAME = '@nebulacomponents/citable';
19
+ const REGISTRY = 'https://registry.npmjs.org';
20
+ const FETCH_TIMEOUT_MS = 10_000;
21
+
22
+ /**
23
+ * Fetch the latest published version from the npm registry.
24
+ * Uses Node's built-in fetch (Node 22+); no extra dependencies.
25
+ *
26
+ * @returns {Promise<string>} latest semver string
27
+ */
28
+ async function fetchLatestVersion() {
29
+ const url = `${REGISTRY}/${encodeURIComponent(PACKAGE_NAME)}/latest`;
30
+ const controller = new AbortController();
31
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
32
+ try {
33
+ const res = await fetch(url, {
34
+ headers: { Accept: 'application/json' },
35
+ signal: controller.signal,
36
+ });
37
+ if (!res.ok) throw new Error(`registry returned ${res.status}`);
38
+ const data = await res.json();
39
+ if (!data.version) throw new Error('no version field in registry response');
40
+ return data.version;
41
+ } finally {
42
+ clearTimeout(timer);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Compare two semver strings. Returns:
48
+ * 1 if a > b
49
+ * 0 if a === b
50
+ * -1 if a < b
51
+ */
52
+ function compareSemver(a, b) {
53
+ const parse = (v) => v.replace(/^v/, '').split('.').map(Number);
54
+ const [aMaj, aMin, aPatch] = parse(a);
55
+ const [bMaj, bMin, bPatch] = parse(b);
56
+ if (aMaj !== bMaj) return aMaj > bMaj ? 1 : -1;
57
+ if (aMin !== bMin) return aMin > bMin ? 1 : -1;
58
+ if (aPatch !== bPatch) return aPatch > bPatch ? 1 : -1;
59
+ return 0;
60
+ }
61
+
62
+ /**
63
+ * Re-execute the current command via npx at the new version.
64
+ * Used to trigger the npx cache refresh as a side-effect.
65
+ */
66
+ function triggerNpxRefresh(latestVersion) {
67
+ // npx --yes ensures no prompt; @version pins to latest
68
+ const result = spawnSync(
69
+ 'npx',
70
+ ['--yes', `${PACKAGE_NAME}@${latestVersion}`, '--version'],
71
+ { stdio: 'pipe', timeout: 60_000 },
72
+ );
73
+ return result.status === 0;
74
+ }
75
+
76
+ export async function selfUpgradeCommand(args) {
77
+ const checkOnly = args.includes('--check');
78
+ const jsonOutput = args.includes('--json');
79
+
80
+ // Resolve current version from our own package.json
81
+ let currentVersion;
82
+ try {
83
+ const req = createRequire(import.meta.url);
84
+ const pkgPath = path.resolve(fileURLToPath(import.meta.url), '../../../package.json');
85
+ currentVersion = req(pkgPath).version;
86
+ } catch {
87
+ const err = { ok: false, error: 'could not read local package.json version' };
88
+ if (jsonOutput) return JSON.stringify(err, null, 2);
89
+ return `error: ${err.error}`;
90
+ }
91
+
92
+ // Fetch latest from registry
93
+ let latestVersion;
94
+ try {
95
+ latestVersion = await fetchLatestVersion();
96
+ } catch (err) {
97
+ const result = { ok: false, current: currentVersion, error: `registry fetch failed: ${err.message}` };
98
+ if (jsonOutput) return JSON.stringify(result, null, 2);
99
+ return `error: ${result.error}\nCurrent version: ${currentVersion}`;
100
+ }
101
+
102
+ const cmp = compareSemver(latestVersion, currentVersion);
103
+
104
+ if (cmp <= 0) {
105
+ // Already current or ahead (local dev)
106
+ const result = { ok: true, current: currentVersion, latest: latestVersion, status: 'current' };
107
+ if (jsonOutput) return JSON.stringify(result, null, 2);
108
+ return `citable ${currentVersion} is already up to date (latest: ${latestVersion})`;
109
+ }
110
+
111
+ // Update available
112
+ if (checkOnly) {
113
+ const result = { ok: true, current: currentVersion, latest: latestVersion, status: 'update-available' };
114
+ if (jsonOutput) return JSON.stringify(result, null, 2);
115
+ return [
116
+ `Update available: ${currentVersion} → ${latestVersion}`,
117
+ `Run: npx ${PACKAGE_NAME}@latest self-upgrade`,
118
+ ].join('\n');
119
+ }
120
+
121
+ // Perform upgrade: warm the npx cache at the new version
122
+ const refreshed = triggerNpxRefresh(latestVersion);
123
+
124
+ const result = {
125
+ ok: refreshed,
126
+ current: currentVersion,
127
+ latest: latestVersion,
128
+ status: refreshed ? 'upgraded' : 'upgrade-failed',
129
+ };
130
+
131
+ if (jsonOutput) return JSON.stringify(result, null, 2);
132
+
133
+ if (refreshed) {
134
+ return [
135
+ `citable upgraded: ${currentVersion} → ${latestVersion}`,
136
+ `Run your next command with: npx ${PACKAGE_NAME}@latest <command>`,
137
+ `Or pin globally: npm install -g ${PACKAGE_NAME}@latest`,
138
+ ].join('\n');
139
+ }
140
+
141
+ return [
142
+ `Upgrade attempt failed (npx returned non-zero).`,
143
+ `Current: ${currentVersion} Latest: ${latestVersion}`,
144
+ `Manual upgrade: npx ${PACKAGE_NAME}@${latestVersion} <command>`,
145
+ ].join('\n');
146
+ }
147
+
148
+ export function selfUpgradeExitCode(output) {
149
+ try {
150
+ const parsed = JSON.parse(output);
151
+ return parsed.ok === false ? 1 : 0;
152
+ } catch {
153
+ return /^(error:|Upgrade attempt failed)/.test(output) ? 1 : 0;
154
+ }
155
+ }
@@ -1,5 +1,5 @@
1
1
  import { loadRegistries, saveRegistry, diffRegistries } from '../registries/index.js';
2
- import { isPastDate, nowIso } from '../shared/io.js';
2
+ import { isPastDate, nowIso, parseRefDate } from '../shared/io.js';
3
3
 
4
4
  const REGULATED = new Set(['legal_regulatory', 'security', 'commercial']);
5
5
 
@@ -17,7 +17,7 @@ const REGULATED = new Set(['legal_regulatory', 'security', 'commercial']);
17
17
  */
18
18
  export function substantiate(root, { write = false, refDate } = {}) {
19
19
  const { registries, problems } = loadRegistries(root);
20
- const ref = refDate ? new Date(refDate) : new Date();
20
+ const ref = parseRefDate(refDate);
21
21
  const evidence = new Map(registries.evidence.entries.map((e) => [e.evidence_id, e]));
22
22
  const before = JSON.parse(JSON.stringify(registries.claims));
23
23
  const assessments = [];
@@ -1,63 +1,120 @@
1
- /**
2
- * Fetch a URL capturing status, headers, body, and redirect chain.
3
- * Used only when the operator supplies a deployed target and fetching is permitted.
4
- */
5
- export async function fetchUrl(url, { userAgent = 'CitableAudit/0.1', maxRedirects = 10, timeoutMs = 20000, maxRetries = 3, retryDelayMs = 1000 } = {}) {
1
+ import dns from 'node:dns';
2
+ import net from 'node:net';
3
+
4
+ const blockedAddresses = new net.BlockList();
5
+ for (const [network, prefix] of [
6
+ ['0.0.0.0', 8], ['10.0.0.0', 8], ['100.64.0.0', 10], ['127.0.0.0', 8],
7
+ ['169.254.0.0', 16], ['172.16.0.0', 12], ['192.168.0.0', 16], ['224.0.0.0', 4],
8
+ ]) blockedAddresses.addSubnet(network, prefix, 'ipv4');
9
+ for (const [network, prefix] of [['::', 128], ['::1', 128], ['fc00::', 7], ['fe80::', 10], ['ff00::', 8]]) {
10
+ blockedAddresses.addSubnet(network, prefix, 'ipv6');
11
+ }
12
+
13
+ function isBlockedAddress(address) {
14
+ const family = net.isIP(address);
15
+ if (!family) return true;
16
+ if (family === 6 && address.toLowerCase().startsWith('::ffff:')) {
17
+ return blockedAddresses.check(address.slice(7), 'ipv4');
18
+ }
19
+ return blockedAddresses.check(address, family === 4 ? 'ipv4' : 'ipv6');
20
+ }
21
+
22
+ async function validateDestination(url, lookup) {
23
+ const parsed = new URL(url);
24
+ if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error(`unsupported URL protocol: ${parsed.protocol}`);
25
+ if (parsed.username || parsed.password) throw new Error('URL credentials are not permitted');
26
+ const hostname = parsed.hostname.replace(/^\[|\]$/g, '');
27
+ const literalFamily = net.isIP(hostname);
28
+ const addresses = literalFamily
29
+ ? [{ address: hostname, family: literalFamily }]
30
+ : await lookup(hostname, { all: true, verbatim: true });
31
+ if (!addresses.length || addresses.some(({ address }) => isBlockedAddress(address))) {
32
+ throw new Error(`refusing private, loopback, or non-public destination: ${hostname}`);
33
+ }
34
+ return parsed;
35
+ }
36
+
37
+ async function readBodyLimited(res, maxBodyBytes) {
38
+ const declared = Number(res.headers.get('content-length'));
39
+ if (Number.isFinite(declared) && declared > maxBodyBytes) {
40
+ throw new Error(`response body exceeds ${maxBodyBytes} bytes`);
41
+ }
42
+ if (!res.body) return '';
43
+ const reader = res.body.getReader();
44
+ const chunks = [];
45
+ let total = 0;
46
+ try {
47
+ while (true) {
48
+ const { done, value } = await reader.read();
49
+ if (done) break;
50
+ total += value.byteLength;
51
+ if (total > maxBodyBytes) throw new Error(`response body exceeds ${maxBodyBytes} bytes`);
52
+ chunks.push(value);
53
+ }
54
+ } finally {
55
+ reader.releaseLock();
56
+ }
57
+ const bytes = new Uint8Array(total);
58
+ let offset = 0;
59
+ for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
60
+ return new TextDecoder().decode(bytes);
61
+ }
62
+
63
+ /** Fetch a public URL with bounded retries, redirects, time, and response size. */
64
+ export async function fetchUrl(url, {
65
+ userAgent = 'CitableAudit/0.1', maxRedirects = 10, timeoutMs = 20000,
66
+ maxRetries = 3, retryDelayMs = 1000, maxBodyBytes = 5 * 1024 * 1024,
67
+ fetchImpl = globalThis.fetch, lookup = dns.promises.lookup,
68
+ } = {}) {
69
+ const requested = await validateDestination(url, lookup);
70
+ const allowedOrigin = requested.origin;
6
71
  const chain = [];
7
- let current = url;
8
-
9
- for (let i = 0; i <= maxRedirects; i++) {
10
- const controller = new AbortController();
11
- const timer = setTimeout(() => controller.abort(), timeoutMs);
72
+ let current = requested.href;
73
+
74
+ for (let redirects = 0; redirects <= maxRedirects; redirects += 1) {
75
+ await validateDestination(current, lookup);
12
76
  let res;
13
77
  let lastError;
14
-
15
- // Retry loop for transient failures
16
- for (let attempt = 0; attempt < maxRetries; attempt++) {
78
+ for (let attempt = 0; attempt < Math.max(1, maxRetries); attempt += 1) {
79
+ const controller = new AbortController();
80
+ const timer = setTimeout(() => controller.abort(new Error(`request timeout after ${timeoutMs}ms`)), timeoutMs);
17
81
  try {
18
- res = await fetch(current, {
82
+ res = await fetchImpl(current, {
19
83
  redirect: 'manual',
20
84
  headers: { 'user-agent': userAgent },
21
85
  signal: controller.signal,
22
86
  });
23
- lastError = null;
24
- break; // Success, break retry loop
87
+ if (res.status < 500 || attempt === Math.max(1, maxRetries) - 1) {
88
+ lastError = null;
89
+ break;
90
+ }
91
+ await res.body?.cancel();
92
+ lastError = new Error(`server returned ${res.status}`);
25
93
  } catch (err) {
26
94
  lastError = err;
27
- // Only retry on network errors or 5xx, not on 4xx
28
- if (err.name === 'AbortError') {
29
- throw err; // Timeout, don't retry
30
- }
31
- // Exponential backoff with jitter
32
- const delay = retryDelayMs * Math.pow(2, attempt) + Math.random() * retryDelayMs;
33
- if (attempt < maxRetries - 1) {
34
- await new Promise(resolve => setTimeout(resolve, delay));
35
- }
95
+ if (controller.signal.aborted) throw controller.signal.reason ?? err;
36
96
  } finally {
37
97
  clearTimeout(timer);
38
98
  }
99
+ if (attempt < Math.max(1, maxRetries) - 1) {
100
+ const delay = retryDelayMs * (2 ** attempt) + Math.random() * retryDelayMs;
101
+ await new Promise((resolve) => setTimeout(resolve, delay));
102
+ }
39
103
  }
40
-
41
- if (lastError) {
42
- throw lastError;
43
- }
44
-
104
+ if (lastError) throw lastError;
105
+
45
106
  const headers = Object.fromEntries(res.headers.entries());
46
107
  if (res.status >= 300 && res.status < 400 && headers.location) {
108
+ if (redirects === maxRedirects) break;
109
+ const next = new URL(headers.location, current);
110
+ if (next.origin !== allowedOrigin) throw new Error(`redirect leaves audited origin: ${next.href}`);
111
+ await validateDestination(next.href, lookup);
47
112
  chain.push({ url: current, status: res.status, location: headers.location });
48
- current = new URL(headers.location, current).href;
49
- continue;
50
- }
51
-
52
- // Retry on 5xx with one less attempt (already used retry loop above)
53
- if (res.status >= 500 && i < maxRedirects) {
54
- // Treat 5xx as transient, add to chain and retry same URL
55
- chain.push({ url: current, status: res.status, location: null, error: 'server error' });
56
- await new Promise(resolve => setTimeout(resolve, retryDelayMs));
113
+ current = next.href;
114
+ await res.body?.cancel();
57
115
  continue;
58
116
  }
59
-
60
- const body = await res.text();
117
+ const body = await readBodyLimited(res, maxBodyBytes);
61
118
  return { url: current, requestedUrl: url, status: res.status, headers, body, redirectChain: chain };
62
119
  }
63
120
  throw new Error(`redirect chain exceeded ${maxRedirects} hops for ${url}`);
@@ -216,4 +216,47 @@ D.push(defineDetector({
216
216
  },
217
217
  }));
218
218
 
219
+ D.push(defineDetector({
220
+ id: 'ANS-009', name: 'Answer-target page has no prompt coverage', namespace: 'ANS',
221
+ description: 'An answer-bearing page is intended for discovery but is not mapped to any prompt in the governed prompt corpus.',
222
+ discipline: ['aeo', 'geo'], severity: 'medium', deterministic: true, requires: ['site', 'registries'],
223
+ impact: { citation: 'high', representation: 'medium', maintainability: 'medium' },
224
+ applicable_requirement: 'AEO §1 governed question corpus and §13 page acceptance; GEO §1 prompt registry and §19 page acceptance',
225
+ remediation: 'Map the page to at least one reviewed prompt via target_prompts, then verify that the page answers the prompt without broadening registered claims.',
226
+ verification: 'Page registry target_prompts is non-empty and every referenced prompt exists.',
227
+ check(ctx) {
228
+ const answerTypes = new Set(['definition', 'problem', 'solution', 'comparison', 'recommendation', 'implementation', 'evidence', 'faq', 'glossary', 'documentation', 'article', 'product']);
229
+ return indexTargets(ctx).flatMap((p) => {
230
+ const reg = registryPageFor(ctx, p);
231
+ if (!reg || !answerTypes.has(reg.page_type) || (reg.target_prompts || []).length > 0) return [];
232
+ return [{
233
+ subject: pageSubject(p),
234
+ summary: `${reg.page_type} page ${reg.page_id} has no governed target prompt`,
235
+ evidence: ['target_prompts is empty or absent'],
236
+ }];
237
+ });
238
+ },
239
+ }));
240
+
241
+ D.push(defineDetector({
242
+ id: 'ANS-010', name: 'Published claims lack page evidence mapping', namespace: 'ANS',
243
+ description: 'A page publishes governed claims but records no page-level evidence references, preventing evidence-adjacency review.',
244
+ discipline: ['aeo', 'geo'], severity: 'high', deterministic: true, requires: ['site', 'registries'],
245
+ impact: { citation: 'high', representation: 'high', legal: 'medium', reputational: 'medium' },
246
+ applicable_requirement: 'AEO §4 evidence adjacency and atomic claims; GEO §4 claim registry and §5 claim-evidence proximity',
247
+ remediation: 'Map supporting evidence in evidence_references and place the relevant source or methodology adjacent to each material claim.',
248
+ verification: 'Page registry contains evidence_references and claim/evidence referential integrity passes.',
249
+ check(ctx) {
250
+ return indexTargets(ctx).flatMap((p) => {
251
+ const reg = registryPageFor(ctx, p);
252
+ if (!reg || !(reg.published_claims || []).length || (reg.evidence_references || []).length) return [];
253
+ return [{
254
+ subject: pageSubject(p),
255
+ summary: `Page ${reg.page_id} publishes claims without page-level evidence references`,
256
+ evidence: [`published_claims: ${reg.published_claims.join(', ')}`, 'evidence_references is empty or absent'],
257
+ }];
258
+ });
259
+ },
260
+ }));
261
+
219
262
  export default D;
@@ -175,6 +175,54 @@ D.push(defineDetector({
175
175
  },
176
176
  }));
177
177
 
178
+ D.push(defineDetector({
179
+ id: 'GEO-005', name: 'Generative target lacks primary entity mapping', namespace: 'GEO',
180
+ description: 'A page intended to define, compare, categorize, or recommend entities does not declare its primary entities.',
181
+ discipline: ['geo', 'aeo'], severity: 'high', deterministic: true, requires: ['site', 'registries'],
182
+ impact: { representation: 'high', citation: 'medium', maintainability: 'medium' },
183
+ applicable_requirement: 'GEO §3 canonical entity registry and §19 page acceptance; AEO §3 entity architecture',
184
+ remediation: 'Map the page to canonical entity IDs in primary_entities and ensure visible names and structured-data @ids agree.',
185
+ verification: 'Page primary_entities is non-empty, referential integrity passes, and ENTITY detectors report no conflict.',
186
+ check(ctx) {
187
+ const entityTypes = new Set(['definition', 'product', 'service', 'comparison', 'recommendation', 'category', 'about']);
188
+ return indexTargets(ctx).flatMap((p) => {
189
+ const reg = registryPageFor(ctx, p);
190
+ if (!reg || !entityTypes.has(reg.page_type) || (reg.primary_entities || []).length) return [];
191
+ return [{
192
+ subject: pageSubject(p),
193
+ summary: `${reg.page_type} page ${reg.page_id} has no primary entity mapping`,
194
+ evidence: ['primary_entities is empty or absent'],
195
+ }];
196
+ });
197
+ },
198
+ }));
199
+
200
+ D.push(defineDetector({
201
+ id: 'GEO-006', name: 'Active prompt lacks evaluation brief', namespace: 'GEO',
202
+ description: 'An active prompt lacks the expected answer components, desired outcome, risk classification, or accountable owner needed for reproducible GEO evaluation.',
203
+ discipline: ['geo', 'aeo'], severity: 'medium', deterministic: true, requires: ['registries'],
204
+ impact: { representation: 'high', citation: 'medium', maintainability: 'high' },
205
+ applicable_requirement: 'GEO §1 prompt registry, §12 repeatable measurement, and §15 governance; AEO §1 question corpus',
206
+ remediation: 'Complete expected_answer_components, desired_outcome, risk_classification, and owner before using the prompt as an optimization target.',
207
+ verification: 'Every active prompt has a complete evaluation brief and passes registry validation.',
208
+ check(ctx) {
209
+ return (ctx.registries.prompts?.entries || [])
210
+ .filter((p) => !['retired', 'deprecated'].includes(p.status))
211
+ .flatMap((p) => {
212
+ const missing = [];
213
+ if (!(p.expected_answer_components || []).length) missing.push('expected_answer_components');
214
+ if (!p.desired_outcome) missing.push('desired_outcome');
215
+ if (!p.risk_classification) missing.push('risk_classification');
216
+ if (!p.owner) missing.push('owner');
217
+ return missing.length ? [{
218
+ subject: entrySubject('prompts', p.prompt_id),
219
+ summary: `Prompt ${p.prompt_id} lacks a complete evaluation brief`,
220
+ evidence: [`missing: ${missing.join(', ')}`],
221
+ }] : [];
222
+ });
223
+ },
224
+ }));
225
+
178
226
  /* ---------------- RECO: recommendation eligibility ---------------- */
179
227
 
180
228
  const RECO_CHECKS = [
@@ -56,7 +56,10 @@ export function buildSiteFromDir(dir, { baseUrl = 'https://example.test' } = {})
56
56
  }
57
57
 
58
58
  /** Build a SiteModel by fetching a deployed URL set (target URL + same-origin discovery, bounded). */
59
- export async function buildSiteFromUrl(startUrl, { maxPages = 50, userAgent } = {}) {
59
+ export async function buildSiteFromUrl(startUrl, {
60
+ maxPages = 50, userAgent, pageMaxBytes = 5 * 1024 * 1024,
61
+ robotsMaxBytes = 512 * 1024, sitemapMaxBytes = 5 * 1024 * 1024,
62
+ } = {}) {
60
63
  const origin = new URL(startUrl).origin;
61
64
  const seen = new Set();
62
65
  const queue = [startUrl];
@@ -68,7 +71,7 @@ export async function buildSiteFromUrl(startUrl, { maxPages = 50, userAgent } =
68
71
  if (seen.has(key)) continue;
69
72
  seen.add(key);
70
73
  try {
71
- const res = await fetchUrl(url, { userAgent });
74
+ const res = await fetchUrl(url, { userAgent, maxBodyBytes: pageMaxBytes });
72
75
  const page = extractPage({
73
76
  url: res.url, html: res.body, status: res.status, headers: res.headers, redirectChain: res.redirectChain,
74
77
  });
@@ -89,13 +92,18 @@ export async function buildSiteFromUrl(startUrl, { maxPages = 50, userAgent } =
89
92
  let robotsText = null;
90
93
  const sitemaps = [];
91
94
  try {
92
- const r = await fetchUrl(new URL('/robots.txt', origin).href, { userAgent });
95
+ const r = await fetchUrl(new URL('/robots.txt', origin).href, { userAgent, maxBodyBytes: robotsMaxBytes });
93
96
  if (r.status === 200) robotsText = r.body;
94
97
  } catch { /* recorded as missing robots */ }
95
98
  const smUrls = robotsText ? parseRobots(robotsText).sitemaps : [new URL('/sitemap.xml', origin).href];
96
99
  for (const sm of smUrls) {
97
100
  try {
98
- const r = await fetchUrl(sm, { userAgent });
101
+ const sitemapUrl = new URL(sm, origin);
102
+ if (sitemapUrl.origin !== origin) {
103
+ errors.push(`${sitemapUrl.href}: sitemap URL leaves audited origin`);
104
+ continue;
105
+ }
106
+ const r = await fetchUrl(sitemapUrl.href, { userAgent, maxBodyBytes: sitemapMaxBytes });
99
107
  if (r.status === 200) sitemaps.push({ source: sm, parsed: parseSitemap(r.body) });
100
108
  } catch { /* absence handled by TECH detectors */ }
101
109
  }
@@ -1,6 +1,45 @@
1
1
  const SEV_ORDER = { critical: 0, high: 1, medium: 2, low: 3, informational: 4, experimental: 5 };
2
2
 
3
- export function summarize(findings) {
3
+ const RETRIEVAL_NAMESPACES = new Set(['TECH', 'CRAWL', 'LINK', 'HREFLANG']);
4
+ const SOURCE_NAMESPACES = new Set(['PAGE', 'ANS', 'ENTITY', 'CLAIM', 'EVD', 'SCHEMA', 'ARCH', 'GEO', 'RECO', 'LIFE', 'EXT']);
5
+
6
+ function namespace(detectorId) {
7
+ return detectorId.split('-')[0];
8
+ }
9
+
10
+ function readinessPosture(findings, detectorsRun, detectorsSkipped, namespaces) {
11
+ const relevant = findings.filter((finding) => namespaces.has(namespace(finding.detector_id)));
12
+ const skipped = detectorsSkipped.filter((check) => namespaces.has(namespace(check.detector_id)));
13
+ const ran = detectorsRun.some((detectorId) => namespaces.has(namespace(detectorId)));
14
+ let result;
15
+ if (!ran || skipped.length) result = 'not_established';
16
+ else if (relevant.some((finding) => ['critical', 'high'].includes(finding.classification.severity))) result = 'fail';
17
+ else if (relevant.length) result = 'partial';
18
+ else result = 'pass';
19
+ return {
20
+ result,
21
+ finding_count: relevant.length,
22
+ skipped_checks: skipped.map((check) => check.detector_id),
23
+ };
24
+ }
25
+
26
+ function citationPosture(promptResults, targetOrigin) {
27
+ if (!promptResults.length) {
28
+ return { result: 'not_evidenced', finding_count: 0, observations: 0, property_citations: 0, citation_presence_rate: null };
29
+ }
30
+ const cited = promptResults.filter((observation) => (observation.citations || []).some((citation) => {
31
+ try { return targetOrigin && new URL(citation).origin === targetOrigin; } catch { return false; }
32
+ })).length;
33
+ return {
34
+ result: 'observed',
35
+ finding_count: 0,
36
+ observations: promptResults.length,
37
+ property_citations: cited,
38
+ citation_presence_rate: cited / promptResults.length,
39
+ };
40
+ }
41
+
42
+ export function summarize(findings, { detectorsRun = [], detectorsSkipped = [], promptResults = [], targetOrigin = null } = {}) {
4
43
  const bySeverity = {};
5
44
  const byNamespace = {};
6
45
  const byDiscipline = { seo: 0, aeo: 0, geo: 0 };
@@ -19,6 +58,11 @@ export function summarize(findings) {
19
58
  by_severity: bySeverity,
20
59
  by_namespace: byNamespace,
21
60
  by_discipline: byDiscipline,
61
+ posture: {
62
+ retrieval_eligibility: readinessPosture(findings, detectorsRun, detectorsSkipped, RETRIEVAL_NAMESPACES),
63
+ source_extraction_and_support: readinessPosture(findings, detectorsRun, detectorsSkipped, SOURCE_NAMESPACES),
64
+ observed_citation_behavior: citationPosture(promptResults, targetOrigin),
65
+ },
22
66
  };
23
67
  }
24
68
 
@@ -45,6 +89,19 @@ export function renderMarkdownReport({ findings, manifest, summary, detectorsSki
45
89
  lines.push(`| ${sev} | ${n} |`);
46
90
  }
47
91
  lines.push('');
92
+ lines.push('## Separate eligibility and observation states');
93
+ lines.push('');
94
+ lines.push('| Dimension | Result | Evidence |');
95
+ lines.push('| --- | --- | --- |');
96
+ const retrieval = summary.posture.retrieval_eligibility;
97
+ const source = summary.posture.source_extraction_and_support;
98
+ const citation = summary.posture.observed_citation_behavior;
99
+ lines.push(`| Retrieval eligibility | ${retrieval.result} | ${retrieval.finding_count} finding(s); ${retrieval.skipped_checks.length} skipped check(s) |`);
100
+ lines.push(`| Source extraction and support | ${source.result} | ${source.finding_count} finding(s); ${source.skipped_checks.length} skipped check(s) |`);
101
+ lines.push(`| Observed citation behavior | ${citation.result} | ${citation.observations} controlled observation(s); property citation rate: ${citation.citation_presence_rate == null ? 'not evidenced' : `${(citation.citation_presence_rate * 100).toFixed(1)}%`} |`);
102
+ lines.push('');
103
+ lines.push('These states are not combined into an AI visibility score. Readiness does not establish retrieval selection or citation behavior.');
104
+ lines.push('');
48
105
  if (detectorsSkipped.length) {
49
106
  lines.push(`## Skipped checks`);
50
107
  lines.push('');
package/src/shared/io.js CHANGED
@@ -37,6 +37,18 @@ export function nowIso() {
37
37
  return new Date().toISOString();
38
38
  }
39
39
 
40
+ export function parseRefDate(value) {
41
+ if (value == null) return new Date();
42
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
43
+ throw new Error('ref-date must be a valid date in YYYY-MM-DD format');
44
+ }
45
+ const date = new Date(`${value}T00:00:00Z`);
46
+ if (Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== value) {
47
+ throw new Error('ref-date must be a valid date in YYYY-MM-DD format');
48
+ }
49
+ return date;
50
+ }
51
+
40
52
  export function isPastDate(isoDate, ref = new Date()) {
41
53
  if (!isoDate) return false;
42
54
  const d = new Date(String(isoDate).slice(0, 10) + 'T23:59:59Z');