@nebulacomponents/citable 0.1.0 → 1.1.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 (48) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +2 -2
  3. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  4. package/dist/universal/.agents/skills/citable/manifest.json +5 -5
  5. package/dist/universal/.agents/skills/citable/schemas/finding.schema.json +253 -50
  6. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  7. package/dist/universal/.claude/skills/citable/manifest.json +5 -5
  8. package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +253 -50
  9. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  10. package/dist/universal/.cursor/skills/citable/manifest.json +5 -5
  11. package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +253 -50
  12. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  13. package/dist/universal/.gemini/skills/citable/manifest.json +5 -5
  14. package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +253 -50
  15. package/dist/universal/.github/skills/citable/VERSION +1 -1
  16. package/dist/universal/.github/skills/citable/manifest.json +5 -5
  17. package/dist/universal/.github/skills/citable/schemas/finding.schema.json +253 -50
  18. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  19. package/dist/universal/.kiro/skills/citable/manifest.json +5 -5
  20. package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +253 -50
  21. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  22. package/dist/universal/.opencode/skills/citable/manifest.json +5 -5
  23. package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +253 -50
  24. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  25. package/dist/universal/.pi/agent/skills/citable/manifest.json +5 -5
  26. package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +253 -50
  27. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  28. package/dist/universal/.qoder/skills/citable/manifest.json +5 -5
  29. package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +253 -50
  30. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  31. package/dist/universal/.rovodev/skills/citable/manifest.json +5 -5
  32. package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +253 -50
  33. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  34. package/dist/universal/.trae/skills/citable/manifest.json +5 -5
  35. package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +253 -50
  36. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  37. package/dist/universal/.trae-cn/skills/citable/manifest.json +5 -5
  38. package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +253 -50
  39. package/dist/universal/manifest.json +51 -51
  40. package/package.json +4 -1
  41. package/schemas/finding.schema.json +253 -50
  42. package/src/crawler/fetch.js +42 -9
  43. package/src/detectors/agent.js +495 -0
  44. package/src/detectors/cwv.js +171 -0
  45. package/src/detectors/framework.js +1 -1
  46. package/src/detectors/hreflang.js +164 -0
  47. package/src/detectors/index.js +4 -1
  48. package/src/installer/index.js +7 -2
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Hreflang detectors for international SEO
3
+ *
4
+ * Checks hreflang annotations for correctness, completeness, and reciprocity.
5
+ */
6
+
7
+ import { defineDetector } from './framework.js';
8
+
9
+ /**
10
+ * HREFLANG-001: hreflang link present and valid
11
+ */
12
+ export const HREFLANG_001 = defineDetector({
13
+ id: 'HREFLANG-001',
14
+ name: 'hreflang link valid',
15
+ namespace: 'HREFLANG',
16
+ discipline: ['seo'],
17
+ severity: 'high',
18
+ deterministic: true,
19
+ description: 'Page has valid hreflang link elements',
20
+ remediation: 'Use valid ISO 639-1 language codes and absolute URLs for hreflang targets',
21
+ verification: 'Check each hreflang link element for valid language code and absolute URL',
22
+ check: (ctx) => {
23
+ const hits = [];
24
+ const page = ctx.page;
25
+ if (!page || !page.head) return hits;
26
+
27
+ const hreflangLinks = page.head.querySelectorAll('link[rel="alternate"][hreflang]');
28
+
29
+ if (hreflangLinks.length === 0) {
30
+ // Not a hit - many pages don't need hreflang
31
+ return hits;
32
+ }
33
+
34
+ for (const link of hreflangLinks) {
35
+ const hreflang = link.getAttribute('hreflang');
36
+ const href = link.getAttribute('href');
37
+
38
+ // Check for invalid hreflang codes
39
+ if (!hreflang || hreflang === '') {
40
+ hits.push({
41
+ subject: ctx.url,
42
+ evidence: ['Empty hreflang attribute found'],
43
+ remediation: 'Provide a valid hreflang code (e.g., "en", "en-US", "x-default")',
44
+ });
45
+ continue;
46
+ }
47
+
48
+ // Check for x-default
49
+ if (hreflang === 'x-default') {
50
+ // x-default is valid and recommended
51
+ continue;
52
+ }
53
+
54
+ // Validate hreflang format (ISO 639-1 or 639-1-3166-1)
55
+ const validPattern = /^[a-z]{2}(-[A-Z]{2})?$/;
56
+ if (!validPattern.test(hreflang)) {
57
+ hits.push({
58
+ subject: ctx.url,
59
+ evidence: ['Invalid hreflang code: "' + hreflang + '"'],
60
+ remediation: 'Use valid ISO 639-1 language code (e.g., "en") or language-region format (e.g., "en-US")',
61
+ });
62
+ }
63
+
64
+ // Check href is absolute URL
65
+ if (href && !href.startsWith('http')) {
66
+ hits.push({
67
+ subject: ctx.url,
68
+ evidence: ['hreflang "' + hreflang + '" uses relative URL: "' + href + '"'],
69
+ remediation: 'Use absolute URLs for hreflang targets',
70
+ });
71
+ }
72
+ }
73
+
74
+ return hits;
75
+ },
76
+ });
77
+
78
+ /**
79
+ * HREFLANG-002: Self-referencing hreflang exists
80
+ */
81
+ export const HREFLANG_002 = defineDetector({
82
+ id: 'HREFLANG-002',
83
+ name: 'hreflang self-reference',
84
+ namespace: 'HREFLANG',
85
+ discipline: ['seo'],
86
+ severity: 'medium',
87
+ deterministic: true,
88
+ description: 'Page references itself in hreflang',
89
+ remediation: 'Add a self-referencing hreflang link pointing to the current URL',
90
+ verification: 'Check if any hreflang link href matches the current page URL',
91
+ check: (ctx) => {
92
+ const page = ctx.page;
93
+ if (!page || !page.head) return [];
94
+
95
+ const hreflangLinks = page.head.querySelectorAll('link[rel="alternate"][hreflang]');
96
+ if (hreflangLinks.length === 0) return [];
97
+
98
+ // Find self-reference
99
+ let hasSelfRef = false;
100
+ for (const link of hreflangLinks) {
101
+ const href = link.getAttribute('href');
102
+ if (href === ctx.url) {
103
+ hasSelfRef = true;
104
+ break;
105
+ }
106
+ }
107
+
108
+ if (!hasSelfRef && hreflangLinks.length > 0) {
109
+ return [{
110
+ subject: ctx.url,
111
+ evidence: ['Page has hreflang links but does not reference itself'],
112
+ remediation: 'Add a self-referencing hreflang link (e.g., if page is English, include <link rel="alternate" hreflang="en" href="CURRENT_URL" />)',
113
+ }];
114
+ }
115
+
116
+ return [];
117
+ },
118
+ });
119
+
120
+ /**
121
+ * HREFLANG-003: x-default present for international pages
122
+ */
123
+ export const HREFLANG_003 = defineDetector({
124
+ id: 'HREFLANG-003',
125
+ name: 'hreflang x-default',
126
+ namespace: 'HREFLANG',
127
+ discipline: ['seo'],
128
+ severity: 'medium',
129
+ deterministic: true,
130
+ determinismNote: 'False positive possible if x-default is intentionally omitted',
131
+ description: 'Pages with multiple hreflang include x-default',
132
+ remediation: 'Add x-default hreflang for users outside targeted locales',
133
+ verification: 'Check for presence of hreflang="x-default" when multiple hreflang exist',
134
+ check: (ctx) => {
135
+ const page = ctx.page;
136
+ if (!page || !page.head) return [];
137
+
138
+ const hreflangLinks = page.head.querySelectorAll('link[rel="alternate"][hreflang]');
139
+ if (hreflangLinks.length < 2) {
140
+ // Only check x-default for pages that have multiple hreflang
141
+ return [];
142
+ }
143
+
144
+ const hasDefault = Array.from(hreflangLinks).some(
145
+ link => link.getAttribute('hreflang') === 'x-default'
146
+ );
147
+
148
+ if (!hasDefault) {
149
+ return [{
150
+ subject: ctx.url,
151
+ evidence: ['Page has ' + hreflangLinks.length + ' hreflang links but no x-default'],
152
+ remediation: 'Add <link rel="alternate" hreflang="x-default" href="FALLBACK_URL" /> for users outside targeted locales',
153
+ }];
154
+ }
155
+
156
+ return [];
157
+ },
158
+ });
159
+
160
+ export const hreflangDetectors = [
161
+ HREFLANG_001,
162
+ HREFLANG_002,
163
+ HREFLANG_003,
164
+ ];
@@ -10,8 +10,11 @@ import schemaData from './schemaData.js';
10
10
  import link from './link.js';
11
11
  import geoReco from './geoReco.js';
12
12
  import lifeMeas from './lifeMeas.js';
13
+ import { hreflangDetectors } from './hreflang.js';
14
+ import { cwvDetectors } from './cwv.js';
15
+ import { AGENT_DETECTORS } from './agent.js';
13
16
 
14
- export const ALL_DETECTORS = [...tech, ...crawl, ...arch, ...page, ...ans, ...entity, ...claim, ...evd, ...schemaData, ...link, ...geoReco, ...lifeMeas];
17
+ export const ALL_DETECTORS = [...tech, ...crawl, ...arch, ...page, ...ans, ...entity, ...claim, ...evd, ...schemaData, ...link, ...geoReco, ...lifeMeas, ...hreflangDetectors, ...cwvDetectors, ...AGENT_DETECTORS];
15
18
 
16
19
  const ids = new Set();
17
20
  for (const d of ALL_DETECTORS) {
@@ -698,8 +698,13 @@ function installOrUpdateTarget(target, args, options = {}, command = 'install')
698
698
  }
699
699
 
700
700
  function ensureTargetInsideScope(targetPath, scopeRoot) {
701
- const resolvedTarget = path.resolve(targetPath);
702
- const resolvedScope = path.resolve(scopeRoot);
701
+ // Use realpathSync when possible to resolve symlinks (e.g. /tmp → /private/tmp on macOS)
702
+ // so that cross-symlink paths compare correctly.
703
+ function realOrResolve(p) {
704
+ try { return fs.realpathSync(p); } catch { return path.resolve(p); }
705
+ }
706
+ const resolvedTarget = realOrResolve(targetPath);
707
+ const resolvedScope = realOrResolve(scopeRoot);
703
708
  const rel = path.relative(resolvedScope, resolvedTarget);
704
709
  if (rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))) return;
705
710
  throw new CitableInstallerError(`refusing to write outside selected scope: ${resolvedTarget}`, EXIT_CODES.integrityFailure);