@nebulacomponents/citable 1.18.1 → 1.19.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 (193) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/dist/universal/.agents/skills/citable/SKILL.md +1 -1
  3. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  4. package/dist/universal/.agents/skills/citable/commands/artifact-interchange.md +5 -0
  5. package/dist/universal/.agents/skills/citable/commands/remediation.md +1 -1
  6. package/dist/universal/.agents/skills/citable/commands/sow.md +2 -2
  7. package/dist/universal/.agents/skills/citable/manifest.json +15 -10
  8. package/dist/universal/.agents/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  9. package/dist/universal/.agents/skills/citable/schemas/browser-network-events.schema.json +142 -0
  10. package/dist/universal/.agents/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  11. package/dist/universal/.agents/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  12. package/dist/universal/.agents/skills/citable/schemas/cro-report.schema.json +112 -0
  13. package/dist/universal/.agents/skills/citable/schemas/search-report.schema.json +99 -0
  14. package/dist/universal/.agents/skills/citable/schemas/sow.schema.json +3 -2
  15. package/dist/universal/.claude/agents/citable/manifest.json +2 -2
  16. package/dist/universal/.claude/skills/citable/SKILL.md +1 -1
  17. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  18. package/dist/universal/.claude/skills/citable/commands/artifact-interchange.md +5 -0
  19. package/dist/universal/.claude/skills/citable/commands/remediation.md +1 -1
  20. package/dist/universal/.claude/skills/citable/commands/sow.md +2 -2
  21. package/dist/universal/.claude/skills/citable/manifest.json +15 -10
  22. package/dist/universal/.claude/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  23. package/dist/universal/.claude/skills/citable/schemas/browser-network-events.schema.json +142 -0
  24. package/dist/universal/.claude/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  25. package/dist/universal/.claude/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  26. package/dist/universal/.claude/skills/citable/schemas/cro-report.schema.json +112 -0
  27. package/dist/universal/.claude/skills/citable/schemas/search-report.schema.json +99 -0
  28. package/dist/universal/.claude/skills/citable/schemas/sow.schema.json +3 -2
  29. package/dist/universal/.cursor/skills/citable/SKILL.md +1 -1
  30. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  31. package/dist/universal/.cursor/skills/citable/commands/artifact-interchange.md +5 -0
  32. package/dist/universal/.cursor/skills/citable/commands/remediation.md +1 -1
  33. package/dist/universal/.cursor/skills/citable/commands/sow.md +2 -2
  34. package/dist/universal/.cursor/skills/citable/manifest.json +15 -10
  35. package/dist/universal/.cursor/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  36. package/dist/universal/.cursor/skills/citable/schemas/browser-network-events.schema.json +142 -0
  37. package/dist/universal/.cursor/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  38. package/dist/universal/.cursor/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  39. package/dist/universal/.cursor/skills/citable/schemas/cro-report.schema.json +112 -0
  40. package/dist/universal/.cursor/skills/citable/schemas/search-report.schema.json +99 -0
  41. package/dist/universal/.cursor/skills/citable/schemas/sow.schema.json +3 -2
  42. package/dist/universal/.gemini/skills/citable/SKILL.md +1 -1
  43. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  44. package/dist/universal/.gemini/skills/citable/commands/artifact-interchange.md +5 -0
  45. package/dist/universal/.gemini/skills/citable/commands/remediation.md +1 -1
  46. package/dist/universal/.gemini/skills/citable/commands/sow.md +2 -2
  47. package/dist/universal/.gemini/skills/citable/manifest.json +15 -10
  48. package/dist/universal/.gemini/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  49. package/dist/universal/.gemini/skills/citable/schemas/browser-network-events.schema.json +142 -0
  50. package/dist/universal/.gemini/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  51. package/dist/universal/.gemini/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  52. package/dist/universal/.gemini/skills/citable/schemas/cro-report.schema.json +112 -0
  53. package/dist/universal/.gemini/skills/citable/schemas/search-report.schema.json +99 -0
  54. package/dist/universal/.gemini/skills/citable/schemas/sow.schema.json +3 -2
  55. package/dist/universal/.github/skills/citable/SKILL.md +1 -1
  56. package/dist/universal/.github/skills/citable/VERSION +1 -1
  57. package/dist/universal/.github/skills/citable/commands/artifact-interchange.md +5 -0
  58. package/dist/universal/.github/skills/citable/commands/remediation.md +1 -1
  59. package/dist/universal/.github/skills/citable/commands/sow.md +2 -2
  60. package/dist/universal/.github/skills/citable/manifest.json +15 -10
  61. package/dist/universal/.github/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  62. package/dist/universal/.github/skills/citable/schemas/browser-network-events.schema.json +142 -0
  63. package/dist/universal/.github/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  64. package/dist/universal/.github/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  65. package/dist/universal/.github/skills/citable/schemas/cro-report.schema.json +112 -0
  66. package/dist/universal/.github/skills/citable/schemas/search-report.schema.json +99 -0
  67. package/dist/universal/.github/skills/citable/schemas/sow.schema.json +3 -2
  68. package/dist/universal/.kiro/skills/citable/SKILL.md +1 -1
  69. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  70. package/dist/universal/.kiro/skills/citable/commands/artifact-interchange.md +5 -0
  71. package/dist/universal/.kiro/skills/citable/commands/remediation.md +1 -1
  72. package/dist/universal/.kiro/skills/citable/commands/sow.md +2 -2
  73. package/dist/universal/.kiro/skills/citable/manifest.json +15 -10
  74. package/dist/universal/.kiro/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  75. package/dist/universal/.kiro/skills/citable/schemas/browser-network-events.schema.json +142 -0
  76. package/dist/universal/.kiro/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  77. package/dist/universal/.kiro/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  78. package/dist/universal/.kiro/skills/citable/schemas/cro-report.schema.json +112 -0
  79. package/dist/universal/.kiro/skills/citable/schemas/search-report.schema.json +99 -0
  80. package/dist/universal/.kiro/skills/citable/schemas/sow.schema.json +3 -2
  81. package/dist/universal/.opencode/skills/citable/SKILL.md +1 -1
  82. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  83. package/dist/universal/.opencode/skills/citable/commands/artifact-interchange.md +5 -0
  84. package/dist/universal/.opencode/skills/citable/commands/remediation.md +1 -1
  85. package/dist/universal/.opencode/skills/citable/commands/sow.md +2 -2
  86. package/dist/universal/.opencode/skills/citable/manifest.json +15 -10
  87. package/dist/universal/.opencode/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  88. package/dist/universal/.opencode/skills/citable/schemas/browser-network-events.schema.json +142 -0
  89. package/dist/universal/.opencode/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  90. package/dist/universal/.opencode/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  91. package/dist/universal/.opencode/skills/citable/schemas/cro-report.schema.json +112 -0
  92. package/dist/universal/.opencode/skills/citable/schemas/search-report.schema.json +99 -0
  93. package/dist/universal/.opencode/skills/citable/schemas/sow.schema.json +3 -2
  94. package/dist/universal/.pi/agent/skills/citable/SKILL.md +1 -1
  95. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  96. package/dist/universal/.pi/agent/skills/citable/commands/artifact-interchange.md +5 -0
  97. package/dist/universal/.pi/agent/skills/citable/commands/remediation.md +1 -1
  98. package/dist/universal/.pi/agent/skills/citable/commands/sow.md +2 -2
  99. package/dist/universal/.pi/agent/skills/citable/manifest.json +15 -10
  100. package/dist/universal/.pi/agent/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  101. package/dist/universal/.pi/agent/skills/citable/schemas/browser-network-events.schema.json +142 -0
  102. package/dist/universal/.pi/agent/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  103. package/dist/universal/.pi/agent/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  104. package/dist/universal/.pi/agent/skills/citable/schemas/cro-report.schema.json +112 -0
  105. package/dist/universal/.pi/agent/skills/citable/schemas/search-report.schema.json +99 -0
  106. package/dist/universal/.pi/agent/skills/citable/schemas/sow.schema.json +3 -2
  107. package/dist/universal/.qoder/skills/citable/SKILL.md +1 -1
  108. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  109. package/dist/universal/.qoder/skills/citable/commands/artifact-interchange.md +5 -0
  110. package/dist/universal/.qoder/skills/citable/commands/remediation.md +1 -1
  111. package/dist/universal/.qoder/skills/citable/commands/sow.md +2 -2
  112. package/dist/universal/.qoder/skills/citable/manifest.json +15 -10
  113. package/dist/universal/.qoder/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  114. package/dist/universal/.qoder/skills/citable/schemas/browser-network-events.schema.json +142 -0
  115. package/dist/universal/.qoder/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  116. package/dist/universal/.qoder/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  117. package/dist/universal/.qoder/skills/citable/schemas/cro-report.schema.json +112 -0
  118. package/dist/universal/.qoder/skills/citable/schemas/search-report.schema.json +99 -0
  119. package/dist/universal/.qoder/skills/citable/schemas/sow.schema.json +3 -2
  120. package/dist/universal/.rovodev/skills/citable/SKILL.md +1 -1
  121. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  122. package/dist/universal/.rovodev/skills/citable/commands/artifact-interchange.md +5 -0
  123. package/dist/universal/.rovodev/skills/citable/commands/remediation.md +1 -1
  124. package/dist/universal/.rovodev/skills/citable/commands/sow.md +2 -2
  125. package/dist/universal/.rovodev/skills/citable/manifest.json +15 -10
  126. package/dist/universal/.rovodev/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  127. package/dist/universal/.rovodev/skills/citable/schemas/browser-network-events.schema.json +142 -0
  128. package/dist/universal/.rovodev/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  129. package/dist/universal/.rovodev/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  130. package/dist/universal/.rovodev/skills/citable/schemas/cro-report.schema.json +112 -0
  131. package/dist/universal/.rovodev/skills/citable/schemas/search-report.schema.json +99 -0
  132. package/dist/universal/.rovodev/skills/citable/schemas/sow.schema.json +3 -2
  133. package/dist/universal/.trae/skills/citable/SKILL.md +1 -1
  134. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  135. package/dist/universal/.trae/skills/citable/commands/artifact-interchange.md +5 -0
  136. package/dist/universal/.trae/skills/citable/commands/remediation.md +1 -1
  137. package/dist/universal/.trae/skills/citable/commands/sow.md +2 -2
  138. package/dist/universal/.trae/skills/citable/manifest.json +15 -10
  139. package/dist/universal/.trae/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  140. package/dist/universal/.trae/skills/citable/schemas/browser-network-events.schema.json +142 -0
  141. package/dist/universal/.trae/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  142. package/dist/universal/.trae/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  143. package/dist/universal/.trae/skills/citable/schemas/cro-report.schema.json +112 -0
  144. package/dist/universal/.trae/skills/citable/schemas/search-report.schema.json +99 -0
  145. package/dist/universal/.trae/skills/citable/schemas/sow.schema.json +3 -2
  146. package/dist/universal/.trae-cn/skills/citable/SKILL.md +1 -1
  147. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  148. package/dist/universal/.trae-cn/skills/citable/commands/artifact-interchange.md +5 -0
  149. package/dist/universal/.trae-cn/skills/citable/commands/remediation.md +1 -1
  150. package/dist/universal/.trae-cn/skills/citable/commands/sow.md +2 -2
  151. package/dist/universal/.trae-cn/skills/citable/manifest.json +15 -10
  152. package/dist/universal/.trae-cn/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  153. package/dist/universal/.trae-cn/skills/citable/schemas/browser-network-events.schema.json +142 -0
  154. package/dist/universal/.trae-cn/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  155. package/dist/universal/.trae-cn/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  156. package/dist/universal/.trae-cn/skills/citable/schemas/cro-report.schema.json +112 -0
  157. package/dist/universal/.trae-cn/skills/citable/schemas/search-report.schema.json +99 -0
  158. package/dist/universal/.trae-cn/skills/citable/schemas/sow.schema.json +3 -2
  159. package/dist/universal/manifest.json +184 -124
  160. package/package.json +2 -1
  161. package/schemas/browser-evidence-plan.schema.json +186 -3
  162. package/schemas/browser-network-events.schema.json +142 -0
  163. package/schemas/browser-runtime-events.schema.json +112 -0
  164. package/schemas/browser-state-observations.schema.json +97 -0
  165. package/schemas/cro-report.schema.json +112 -0
  166. package/schemas/search-report.schema.json +99 -0
  167. package/schemas/sow.schema.json +3 -2
  168. package/skill/SKILL.md +1 -1
  169. package/skill/commands/artifact-interchange.md +5 -0
  170. package/skill/commands/remediation.md +1 -1
  171. package/skill/commands/sow.md +2 -2
  172. package/src/analysis/behavioral.js +5 -4
  173. package/src/analysis/eeat.js +10 -9
  174. package/src/analysis/offpage.js +19 -36
  175. package/src/analysis/readiness.js +8 -2
  176. package/src/artifacts/verifyCustomerArtifact.js +283 -0
  177. package/src/cli/index.js +8 -1
  178. package/src/observations/browser/correlation.js +83 -0
  179. package/src/observations/browser/networkCollector.js +413 -0
  180. package/src/observations/browser/runtimeEventCollector.js +337 -0
  181. package/src/observations/browser/sanitizer.js +322 -0
  182. package/src/observations/browser/stateCollector.js +386 -0
  183. package/src/observations/browserJourney.js +131 -3
  184. package/src/reporting/executiveCroReport.js +393 -229
  185. package/src/reporting/executiveExport.js +6 -2
  186. package/src/reporting/executiveSearchReport.js +519 -271
  187. package/src/shared/domainUtils.js +140 -0
  188. package/src/shared/epistemicStatus.js +111 -0
  189. package/src/shared/evidenceSourceResolver.js +286 -0
  190. package/src/shared/htmlEscape.js +65 -0
  191. package/src/shared/runPackageVerifier.js +153 -0
  192. package/src/sow/admissibilityGate.js +20 -2
  193. package/src/sow/generateSow.js +125 -159
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Canonical Domain & Public Suffix Extraction Utility
3
+ *
4
+ * Implements bounded registrable domain extraction and authority domain validation
5
+ * without naive substring matching.
6
+ */
7
+
8
+ // Common multi-part public suffixes across gTLDs and ccTLDs
9
+ const MULTI_PART_SUFFIXES = new Set([
10
+ // UK
11
+ 'co.uk', 'org.uk', 'gov.uk', 'ac.uk', 'ltd.uk', 'plc.uk', 'me.uk', 'net.uk', 'sch.uk',
12
+ // Australia
13
+ 'com.au', 'net.au', 'org.au', 'edu.au', 'gov.au', 'asn.au', 'id.au',
14
+ // New Zealand
15
+ 'co.nz', 'org.nz', 'net.nz', 'govt.nz', 'ac.nz', 'school.nz', 'geek.nz',
16
+ // Japan
17
+ 'co.jp', 'ne.jp', 'or.jp', 'ac.jp', 'go.jp', 'ed.jp', 'gr.jp',
18
+ // Brazil
19
+ 'com.br', 'org.br', 'gov.br', 'edu.br', 'net.br',
20
+ // Mexico
21
+ 'com.mx', 'org.mx', 'gob.mx', 'edu.mx', 'net.mx',
22
+ // India
23
+ 'co.in', 'net.in', 'org.in', 'gen.in', 'firm.in', 'ind.in', 'ac.in', 'edu.in', 'res.in', 'gov.in',
24
+ // Singapore
25
+ 'com.sg', 'org.sg', 'edu.sg', 'gov.sg', 'net.sg',
26
+ // Hong Kong
27
+ 'com.hk', 'org.hk', 'edu.hk', 'gov.hk', 'net.hk',
28
+ // South Africa
29
+ 'co.za', 'org.za', 'gov.za', 'ac.za', 'net.za',
30
+ // Turkey
31
+ 'com.tr', 'org.tr', 'gov.tr', 'edu.tr', 'net.tr',
32
+ // South Korea
33
+ 'co.kr', 'ne.kr', 're.kr', 'or.kr', 'go.kr', 'ac.kr',
34
+ // Canada
35
+ 'gc.ca', 'qc.ca', 'on.ca', 'bc.ca', 'ab.ca',
36
+ // France / Germany / Israel / etc.
37
+ 'tm.fr', 'asso.fr', 'co.il', 'org.il', 'gov.il', 'muni.il', 'ac.il',
38
+ // Commercial second-level
39
+ 'us.com', 'eu.com', 'uk.com', 'de.com',
40
+ // Developer platforms (treated as public suffixes for site identification)
41
+ 'github.io', 'gitlab.io', 'pages.dev', 'vercel.app', 'webflow.io',
42
+ ]);
43
+
44
+ const AUTHORITATIVE_EXACT_DOMAINS = new Set([
45
+ 'w3.org',
46
+ 'rfc-editor.org',
47
+ 'iana.org',
48
+ 'ietf.org',
49
+ 'iso.org',
50
+ 'nist.gov',
51
+ 'arxiv.org',
52
+ 'developer.mozilla.org',
53
+ 'web.dev',
54
+ 'github.com',
55
+ ]);
56
+
57
+ export function extractHostname(urlString) {
58
+ if (!urlString || typeof urlString !== 'string') return null;
59
+ const trimmed = urlString.trim();
60
+ try {
61
+ const url = new URL(trimmed.includes('://') ? trimmed : `https://${trimmed}`);
62
+ return url.hostname.toLowerCase();
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ export function extractRegistrableDomain(hostnameOrUrl) {
69
+ const host = extractHostname(hostnameOrUrl);
70
+ if (!host) return null;
71
+
72
+ // Split into dot-separated parts
73
+ const parts = host.split('.');
74
+ if (parts.length <= 1) return host;
75
+
76
+ // Check 2-part suffix (e.g. co.uk, com.au)
77
+ if (parts.length >= 3) {
78
+ const twoPartSuffix = parts.slice(-2).join('.');
79
+ if (MULTI_PART_SUFFIXES.has(twoPartSuffix)) {
80
+ return parts.slice(-3).join('.');
81
+ }
82
+ }
83
+
84
+ // Check 3-part suffix if any exist
85
+ if (parts.length >= 4) {
86
+ const threePartSuffix = parts.slice(-3).join('.');
87
+ if (MULTI_PART_SUFFIXES.has(threePartSuffix)) {
88
+ return parts.slice(-4).join('.');
89
+ }
90
+ }
91
+
92
+ // Default: last 2 parts (e.g. example.com)
93
+ return parts.slice(-2).join('.');
94
+ }
95
+
96
+ export function extractPublicSuffix(hostnameOrUrl) {
97
+ const host = extractHostname(hostnameOrUrl);
98
+ if (!host) return null;
99
+
100
+ const parts = host.split('.');
101
+ if (parts.length <= 1) return null;
102
+
103
+ if (parts.length >= 3) {
104
+ const twoPart = parts.slice(-2).join('.');
105
+ if (MULTI_PART_SUFFIXES.has(twoPart)) return twoPart;
106
+ }
107
+
108
+ return parts.at(-1) || null;
109
+ }
110
+
111
+ export function isAuthoritativeDomain(urlOrHost) {
112
+ const host = extractHostname(urlOrHost);
113
+ if (!host) return false;
114
+
115
+ const regDomain = extractRegistrableDomain(host);
116
+
117
+ // Exact match to known authority domain or its subdomains
118
+ if (AUTHORITATIVE_EXACT_DOMAINS.has(host) || (regDomain && AUTHORITATIVE_EXACT_DOMAINS.has(regDomain))) {
119
+ return true;
120
+ }
121
+
122
+ // Check government TLD / suffix: .gov, .gov.uk, .gov.au, etc.
123
+ if (host.endsWith('.gov') || host.endsWith('.mil') || (regDomain && (regDomain.endsWith('.gov') || regDomain.endsWith('.mil')))) {
124
+ return true;
125
+ }
126
+ const suffix = extractPublicSuffix(host);
127
+ if (suffix && (suffix.startsWith('gov.') || suffix.endsWith('.gov') || suffix.startsWith('gob.'))) {
128
+ return true;
129
+ }
130
+
131
+ // Check academic / educational TLD: .edu, .edu.au, .ac.uk, etc.
132
+ if (host.endsWith('.edu') || (regDomain && regDomain.endsWith('.edu'))) {
133
+ return true;
134
+ }
135
+ if (suffix && (suffix.startsWith('edu.') || suffix.startsWith('ac.') || suffix.endsWith('.edu'))) {
136
+ return true;
137
+ }
138
+
139
+ return false;
140
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Epistemic Status & Provenance Envelope
3
+ *
4
+ * Core Operating Doctrine:
5
+ * OBSERVED stays OBSERVED
6
+ * DERIVED stays DERIVED
7
+ * MODELED stays MODELED
8
+ * HUMAN_REVIEWED stays HUMAN_REVIEWED
9
+ * NOT_OBSERVED stays NOT_OBSERVED
10
+ * NOT_APPLICABLE stays NOT_APPLICABLE
11
+ * UNRESOLVED stays UNRESOLVED
12
+ * SYNTHETIC stays SAMPLE-ONLY
13
+ */
14
+
15
+ export const EPISTEMIC_STATUS = Object.freeze({
16
+ OBSERVED: 'OBSERVED',
17
+ DERIVED: 'DERIVED',
18
+ MODELED: 'MODELED',
19
+ HUMAN_REVIEWED: 'HUMAN_REVIEWED',
20
+ NOT_OBSERVED: 'NOT_OBSERVED',
21
+ NOT_APPLICABLE: 'NOT_APPLICABLE',
22
+ UNRESOLVED: 'UNRESOLVED',
23
+ SYNTHETIC_SAMPLE: 'SYNTHETIC_SAMPLE',
24
+ });
25
+
26
+ export const DECISION_CATEGORY = Object.freeze({
27
+ CONFIRMED_OBSERVATION: 'CONFIRMED_OBSERVATION',
28
+ DERIVED_FINDING: 'DERIVED_FINDING',
29
+ MODELED_HYPOTHESIS: 'MODELED_HYPOTHESIS',
30
+ CONTROLLED_CAUSAL_EVIDENCE: 'CONTROLLED_CAUSAL_EVIDENCE',
31
+ UNRESOLVED: 'UNRESOLVED',
32
+ REQUIRES_HUMAN_REVIEW: 'REQUIRES_HUMAN_REVIEW',
33
+ REQUIRES_EXTERNAL_DATA: 'REQUIRES_EXTERNAL_DATA',
34
+ });
35
+
36
+ export function wrapValue(value, status, options = {}) {
37
+ if (!Object.values(EPISTEMIC_STATUS).includes(status)) {
38
+ throw new Error(`Invalid epistemic status: ${status}`);
39
+ }
40
+ return {
41
+ value,
42
+ status,
43
+ evidence_refs: options.evidence_refs ? [].concat(options.evidence_refs) : [],
44
+ method: options.method || null,
45
+ confidence: options.confidence || null,
46
+ derivation_id: options.derivation_id || null,
47
+ model_id: options.model_id || null,
48
+ required_input: options.required_input || null,
49
+ note: options.note || null,
50
+ };
51
+ }
52
+
53
+ export function observed(value, evidenceRefs = [], method = null, confidence = 'empirical_observation') {
54
+ return wrapValue(value, EPISTEMIC_STATUS.OBSERVED, {
55
+ evidence_refs: evidenceRefs,
56
+ method,
57
+ confidence,
58
+ });
59
+ }
60
+
61
+ export function derived(value, evidenceRefs = [], method = null, derivationId = null) {
62
+ return wrapValue(value, EPISTEMIC_STATUS.DERIVED, {
63
+ evidence_refs: evidenceRefs,
64
+ method,
65
+ derivation_id: derivationId,
66
+ confidence: 'reproducible_derivation',
67
+ });
68
+ }
69
+
70
+ export function modeled(value, method, modelId = null, confidence = 'modeled_heuristic') {
71
+ return wrapValue(value, EPISTEMIC_STATUS.MODELED, {
72
+ method,
73
+ model_id: modelId,
74
+ confidence,
75
+ });
76
+ }
77
+
78
+ export function humanReviewed(value, reviewerId, reviewTimestamp, note = null) {
79
+ return wrapValue(value, EPISTEMIC_STATUS.HUMAN_REVIEWED, {
80
+ method: `human_review:${reviewerId}`,
81
+ note,
82
+ confidence: 'attested_human_review',
83
+ });
84
+ }
85
+
86
+ export function notObserved(requiredInput, note = null) {
87
+ return wrapValue(null, EPISTEMIC_STATUS.NOT_OBSERVED, {
88
+ required_input: requiredInput,
89
+ note: note || `Metric unavailable: required input "${requiredInput}" was not provided or not observed.`,
90
+ });
91
+ }
92
+
93
+ export function notApplicable(reason) {
94
+ return wrapValue(null, EPISTEMIC_STATUS.NOT_APPLICABLE, {
95
+ note: reason,
96
+ });
97
+ }
98
+
99
+ export function unresolved(reason, requiredInput = null) {
100
+ return wrapValue(null, EPISTEMIC_STATUS.UNRESOLVED, {
101
+ required_input: requiredInput,
102
+ note: reason,
103
+ });
104
+ }
105
+
106
+ export function syntheticSample(value, rationale = 'Non-contractual synthetic demonstration sample') {
107
+ return wrapValue(value, EPISTEMIC_STATUS.SYNTHETIC_SAMPLE, {
108
+ note: rationale,
109
+ confidence: 'synthetic_sample_only',
110
+ });
111
+ }
@@ -0,0 +1,286 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { readJson, sha256, sha256File } from './io.js';
4
+ import { verifyRunPackage, RunVerificationError } from './runPackageVerifier.js';
5
+ import { buildContext } from '../commands/context.js';
6
+ import { selectDetectors } from '../detectors/index.js';
7
+ import { runDetectors, indexTargets } from '../detectors/framework.js';
8
+
9
+ export class SourceResolutionError extends Error {
10
+ constructor(message, code = 'SOURCE_RESOLUTION_ERROR', details = {}) {
11
+ super(message);
12
+ this.name = 'SourceResolutionError';
13
+ this.code = code;
14
+ this.details = details;
15
+ }
16
+ }
17
+
18
+ export class RunNotFoundError extends SourceResolutionError {
19
+ constructor(runId) {
20
+ super(`Specified run not found: ${runId}`, 'RUN_NOT_FOUND', { runId });
21
+ this.runId = runId;
22
+ }
23
+ }
24
+
25
+ export class FindingsMissingError extends SourceResolutionError {
26
+ constructor(runId, filePath) {
27
+ super(`Run ${runId} is missing findings.json at ${filePath}`, 'FINDINGS_MISSING', { runId, filePath });
28
+ this.runId = runId;
29
+ this.filePath = filePath;
30
+ }
31
+ }
32
+
33
+ export class FindingsInvalidError extends SourceResolutionError {
34
+ constructor(runId, message) {
35
+ super(`Run ${runId} contains malformed or invalid findings: ${message}`, 'FINDINGS_INVALID', { runId, message });
36
+ this.runId = runId;
37
+ }
38
+ }
39
+
40
+ export class LiveInspectionFailedError extends SourceResolutionError {
41
+ constructor(target, reason) {
42
+ super(`Live target inspection failed for ${target}: ${reason}`, 'LIVE_INSPECTION_FAILED', { target, reason });
43
+ this.target = target;
44
+ }
45
+ }
46
+
47
+ export class NoFindingsAvailableError extends SourceResolutionError {
48
+ constructor(source, mode = 'CONTRACTUAL') {
49
+ super(
50
+ `No verified audit findings available from ${source} for ${mode} generation. Production generation requires authoritative findings. (Use --sample or --demo to generate non-contractual sample)`,
51
+ 'NO_FINDINGS',
52
+ { source, mode }
53
+ );
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Extract canonical timestamp (epoch ms) from run package
59
+ * Hierarchy:
60
+ * 1. manifest.json `timestamp` or `created_at` ISO string
61
+ * 2. Run ID timestamp prefix (YYYYMMDDTHHmmss)
62
+ * 3. findings.json mtime
63
+ * 4. run directory mtime
64
+ */
65
+ export function getCanonicalRunTimestamp(runDir, runId = '') {
66
+ // 1. manifest.json
67
+ const manifestPath = path.join(runDir, 'manifest.json');
68
+ if (fs.existsSync(manifestPath)) {
69
+ try {
70
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
71
+ if (manifest.timestamp) {
72
+ const ms = Date.parse(manifest.timestamp);
73
+ if (!Number.isNaN(ms)) return ms;
74
+ }
75
+ if (manifest.created_at) {
76
+ const ms = Date.parse(manifest.created_at);
77
+ if (!Number.isNaN(ms)) return ms;
78
+ }
79
+ } catch {}
80
+ }
81
+
82
+ // 2. Run ID timestamp prefix (YYYYMMDDTHHmmss)
83
+ const m = String(runId).match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/);
84
+ if (m) {
85
+ const iso = `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}Z`;
86
+ const ms = Date.parse(iso);
87
+ if (!Number.isNaN(ms)) return ms;
88
+ }
89
+
90
+ // 3. findings.json mtime
91
+ const findPath = path.join(runDir, 'findings.json');
92
+ if (fs.existsSync(findPath)) {
93
+ try {
94
+ return fs.statSync(findPath).mtimeMs;
95
+ } catch {}
96
+ }
97
+
98
+ // 4. Directory mtime
99
+ try {
100
+ return fs.statSync(runDir).mtimeMs;
101
+ } catch {
102
+ return 0;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Sort run candidate directory names by chronological timestamp descending,
108
+ * with deterministic lexicographical tie-breaking.
109
+ * Invariant:
110
+ * latest run = max(canonical run timestamp)
111
+ * tie = deterministic secondary key
112
+ */
113
+ export function sortRunCandidatesChronologically(runsDir, candidateRunIds = []) {
114
+ return [...candidateRunIds].sort((a, b) => {
115
+ const timeA = getCanonicalRunTimestamp(path.join(runsDir, a), a);
116
+ const timeB = getCanonicalRunTimestamp(path.join(runsDir, b), b);
117
+ if (timeB !== timeA) {
118
+ return timeB - timeA; // Descending: latest timestamp first
119
+ }
120
+ return b.localeCompare(a); // Deterministic tie-breaker
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Unified Canonical Evidence Source Resolver
126
+ *
127
+ * Enforces strict precedence and fail-closed integrity across SOW and Executive Reports:
128
+ * 1. DIRECT_FINDINGS (in-memory findings array)
129
+ * 2. EXPLICIT_RUN (--run <id>): verified via runPackageVerifier
130
+ * 3. LIVE_INSPECTION (--live or target): executed on demand
131
+ * 4. LATEST_RECORDED_RUN: chronologically latest run in .citable/runs verified via runPackageVerifier
132
+ * 5. SAMPLE: synthetic demonstration sample
133
+ */
134
+ export async function resolveEvidenceSource(root, options = {}) {
135
+ const {
136
+ findings: inputFindings = null,
137
+ runId = null,
138
+ target = null,
139
+ live = false,
140
+ baseUrl = null,
141
+ refDate = null,
142
+ sample = false,
143
+ demo = false,
144
+ draft = false,
145
+ contractual = false,
146
+ scopes = ['technical', 'seo', 'aeo', 'geo', 'schema', 'entity', 'cro'],
147
+ } = options;
148
+
149
+ const isSample = Boolean(sample || demo);
150
+ const isDraft = Boolean(draft);
151
+ const generationMode = isSample ? 'NON_CONTRACTUAL_SAMPLE' : (isDraft ? 'DRAFT' : 'CONTRACTUAL');
152
+
153
+ // 1. Direct findings in memory
154
+ if (Array.isArray(inputFindings)) {
155
+ const findingsJson = JSON.stringify(inputFindings);
156
+ return {
157
+ source_type: 'DIRECT_FINDINGS',
158
+ source_identifier: 'in_memory',
159
+ findings: inputFindings,
160
+ findings_count: inputFindings.length,
161
+ integrity_hash: sha256(findingsJson),
162
+ generation_mode: generationMode,
163
+ run_metadata: null,
164
+ };
165
+ }
166
+
167
+ // 2. Explicit run requested via runId
168
+ if (runId) {
169
+ const runsDir = path.join(root, '.citable', 'runs');
170
+ const runPath = path.join(runsDir, runId);
171
+ if (!fs.existsSync(runPath)) {
172
+ throw new RunNotFoundError(runId);
173
+ }
174
+ const findPath = path.join(runPath, 'findings.json');
175
+ if (!fs.existsSync(findPath)) {
176
+ throw new FindingsMissingError(runId, findPath);
177
+ }
178
+
179
+ try {
180
+ const verification = verifyRunPackage(runPath, {
181
+ requireFindings: true,
182
+ requireCompleted: generationMode === 'CONTRACTUAL',
183
+ requireChecksums: true,
184
+ });
185
+
186
+ return {
187
+ source_type: 'HISTORICAL_RUN',
188
+ source_identifier: runId,
189
+ findings: verification.findings,
190
+ findings_count: verification.findingsCount,
191
+ integrity_hash: sha256File(findPath),
192
+ generation_mode: generationMode,
193
+ run_metadata: verification.manifest,
194
+ checksums_verified: verification.checksumsVerified,
195
+ };
196
+ } catch (err) {
197
+ if (err instanceof RunVerificationError) {
198
+ throw new FindingsInvalidError(runId, err.message);
199
+ }
200
+ throw err;
201
+ }
202
+ }
203
+
204
+ // 3. Live target inspection
205
+ if (live || target) {
206
+ if (!target) {
207
+ throw new SourceResolutionError('Target is required for live inspection (e.g. --target <url|dir>)', 'TARGET_REQUIRED');
208
+ }
209
+ try {
210
+ const ctx = await buildContext(root, { target, baseUrl, refDate });
211
+ if (!ctx?.site) {
212
+ throw new Error(`Target ${target} did not produce a valid site context`);
213
+ }
214
+ indexTargets(ctx);
215
+ const detectors = selectDetectors({ scopes });
216
+ const res = runDetectors(detectors, ctx);
217
+ const liveFindings = res.findings || [];
218
+
219
+ return {
220
+ source_type: 'LIVE_INSPECTION',
221
+ source_identifier: target,
222
+ findings: liveFindings,
223
+ findings_count: liveFindings.length,
224
+ integrity_hash: sha256(JSON.stringify(liveFindings)),
225
+ generation_mode: generationMode,
226
+ run_metadata: null,
227
+ context: ctx,
228
+ };
229
+ } catch (err) {
230
+ throw new LiveInspectionFailedError(target, err.message);
231
+ }
232
+ }
233
+
234
+ // 4. Latest recorded run in .citable/runs
235
+ const runsDir = path.join(root, '.citable', 'runs');
236
+ if (fs.existsSync(runsDir)) {
237
+ const candidateDirs = fs.readdirSync(runsDir).filter((name) => {
238
+ const p = path.join(runsDir, name);
239
+ return fs.statSync(p).isDirectory() && fs.existsSync(path.join(p, 'findings.json'));
240
+ });
241
+
242
+ if (candidateDirs.length > 0) {
243
+ const sorted = sortRunCandidatesChronologically(runsDir, candidateDirs);
244
+ for (const candidateId of sorted) {
245
+ const runPath = path.join(runsDir, candidateId);
246
+ const findPath = path.join(runPath, 'findings.json');
247
+ try {
248
+ const verification = verifyRunPackage(runPath, {
249
+ requireFindings: true,
250
+ requireCompleted: false, // allow non-completed in discovery if valid
251
+ requireChecksums: false,
252
+ });
253
+
254
+ return {
255
+ source_type: 'LATEST_RECORDED_RUN',
256
+ source_identifier: candidateId,
257
+ findings: verification.findings,
258
+ findings_count: verification.findingsCount,
259
+ integrity_hash: sha256File(findPath),
260
+ generation_mode: generationMode,
261
+ run_metadata: verification.manifest,
262
+ };
263
+ } catch {
264
+ // Try next candidate
265
+ continue;
266
+ }
267
+ }
268
+ }
269
+ }
270
+
271
+ // 5. Sample / Demo mode
272
+ if (isSample) {
273
+ return {
274
+ source_type: 'SAMPLE',
275
+ source_identifier: 'synthetic_sample_template',
276
+ findings: [],
277
+ findings_count: 0,
278
+ integrity_hash: null,
279
+ generation_mode: 'NON_CONTRACTUAL_SAMPLE',
280
+ run_metadata: null,
281
+ };
282
+ }
283
+
284
+ // If we reach here in contractual mode, fail closed
285
+ throw new NoFindingsAvailableError('.citable/runs or input target', generationMode);
286
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Canonical HTML and Markdown Escaping Utility
3
+ *
4
+ * Prevents HTML/XSS injection vulnerabilities in generated reports, SOWs,
5
+ * dashboards, and markdown/HTML interchange representations.
6
+ * Treats all finding summaries, client names, URLs, titles, and detector
7
+ * outputs as potentially hostile user input.
8
+ */
9
+
10
+ const HTML_CHARS = {
11
+ '&': '&amp;',
12
+ '<': '&lt;',
13
+ '>': '&gt;',
14
+ '"': '&quot;',
15
+ "'": '&#39;',
16
+ };
17
+
18
+ const HTML_RX = /[&<>"']/g;
19
+
20
+ export function escapeHtml(str) {
21
+ if (str === null || str === undefined) return '';
22
+ return String(str).replace(HTML_RX, (char) => HTML_CHARS[char]);
23
+ }
24
+
25
+ export function escapeHtmlAttr(str) {
26
+ return escapeHtml(str);
27
+ }
28
+
29
+ const SAFE_PROTOCOLS = new Set(['http:', 'https:', 'mailto:']);
30
+
31
+ export function sanitizeUrl(urlStr, fallback = '#') {
32
+ if (!urlStr || typeof urlStr !== 'string') return fallback;
33
+ const trimmed = urlStr.trim();
34
+ if (trimmed.startsWith('/') || trimmed.startsWith('#')) {
35
+ // Relative path or anchor is safe
36
+ return escapeHtmlAttr(trimmed);
37
+ }
38
+ try {
39
+ const parsed = new URL(trimmed);
40
+ if (SAFE_PROTOCOLS.has(parsed.protocol)) {
41
+ return escapeHtmlAttr(trimmed);
42
+ }
43
+ return fallback;
44
+ } catch {
45
+ return fallback;
46
+ }
47
+ }
48
+
49
+ export function escapeMarkdownTableCell(str) {
50
+ if (str === null || str === undefined) return '';
51
+ return String(str)
52
+ .replace(/\r?\n/g, ' ')
53
+ .replace(/\\/g, '\\\\')
54
+ .replace(/\|/g, '\\|')
55
+ .replace(/</g, '&lt;')
56
+ .replace(/>/g, '&gt;');
57
+ }
58
+
59
+ export function sanitizeForMarkdown(str) {
60
+ if (str === null || str === undefined) return '';
61
+ // Prevent raw script or malicious HTML tag injections in markdown by encoding brackets
62
+ return String(str)
63
+ .replace(/</g, '&lt;')
64
+ .replace(/>/g, '&gt;');
65
+ }