@aexol/spectral 0.9.126 → 0.9.128

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 (89) hide show
  1. package/dist/agent/index.d.ts.map +1 -1
  2. package/dist/agent/index.js +5 -0
  3. package/dist/extensions/seo/analyzers/content.d.ts +29 -6
  4. package/dist/extensions/seo/analyzers/content.d.ts.map +1 -1
  5. package/dist/extensions/seo/analyzers/content.js +243 -8
  6. package/dist/extensions/seo/analyzers/crawler.d.ts.map +1 -1
  7. package/dist/extensions/seo/analyzers/crawler.js +133 -7
  8. package/dist/extensions/seo/analyzers/drift.d.ts.map +1 -1
  9. package/dist/extensions/seo/analyzers/drift.js +86 -29
  10. package/dist/extensions/seo/analyzers/images.d.ts +10 -4
  11. package/dist/extensions/seo/analyzers/images.d.ts.map +1 -1
  12. package/dist/extensions/seo/analyzers/images.js +153 -5
  13. package/dist/extensions/seo/analyzers/performance.d.ts +3 -3
  14. package/dist/extensions/seo/analyzers/performance.d.ts.map +1 -1
  15. package/dist/extensions/seo/analyzers/performance.js +77 -6
  16. package/dist/extensions/seo/analyzers/readability.d.ts.map +1 -1
  17. package/dist/extensions/seo/analyzers/readability.js +17 -9
  18. package/dist/extensions/seo/analyzers/schema.d.ts.map +1 -1
  19. package/dist/extensions/seo/analyzers/schema.js +99 -10
  20. package/dist/extensions/seo/analyzers/sitemap.d.ts.map +1 -1
  21. package/dist/extensions/seo/analyzers/sitemap.js +57 -16
  22. package/dist/extensions/seo/analyzers/technical.d.ts +0 -7
  23. package/dist/extensions/seo/analyzers/technical.d.ts.map +1 -1
  24. package/dist/extensions/seo/analyzers/technical.js +408 -37
  25. package/dist/extensions/seo/config.d.ts +2 -0
  26. package/dist/extensions/seo/config.d.ts.map +1 -1
  27. package/dist/extensions/seo/config.js +2 -0
  28. package/dist/extensions/seo/tools/batch.js +1 -1
  29. package/dist/extensions/seo/tools/content.d.ts.map +1 -1
  30. package/dist/extensions/seo/tools/content.js +5 -3
  31. package/dist/extensions/seo/tools/full-audit.d.ts +1 -1
  32. package/dist/extensions/seo/tools/full-audit.d.ts.map +1 -1
  33. package/dist/extensions/seo/tools/full-audit.js +391 -66
  34. package/dist/extensions/seo/tools/helpers.d.ts +15 -4
  35. package/dist/extensions/seo/tools/helpers.d.ts.map +1 -1
  36. package/dist/extensions/seo/tools/helpers.js +57 -6
  37. package/dist/extensions/seo/tools/performance.d.ts.map +1 -1
  38. package/dist/extensions/seo/tools/performance.js +17 -11
  39. package/dist/extensions/seo/tools/report.d.ts.map +1 -1
  40. package/dist/extensions/seo/tools/report.js +513 -47
  41. package/dist/extensions/seo/types.d.ts +295 -3
  42. package/dist/extensions/seo/types.d.ts.map +1 -1
  43. package/dist/extensions/seo/utils/fetcher.d.ts +4 -0
  44. package/dist/extensions/seo/utils/fetcher.d.ts.map +1 -1
  45. package/dist/extensions/seo/utils/fetcher.js +77 -11
  46. package/dist/extensions/seo/utils/parser.d.ts +3 -1
  47. package/dist/extensions/seo/utils/parser.d.ts.map +1 -1
  48. package/dist/extensions/seo/utils/parser.js +105 -25
  49. package/dist/extensions/seo/utils/renderer.d.ts +6 -1
  50. package/dist/extensions/seo/utils/renderer.d.ts.map +1 -1
  51. package/dist/extensions/seo/utils/renderer.js +207 -1
  52. package/dist/extensions/seo/utils/url-safety.d.ts.map +1 -1
  53. package/dist/extensions/seo/utils/url-safety.js +19 -3
  54. package/dist/extensions/web/tools/fetch.js +2 -2
  55. package/dist/memory/tools/compact-context.d.ts.map +1 -1
  56. package/dist/memory/tools/compact-context.js +10 -3
  57. package/dist/sdk/agent-core/agent-loop.d.ts.map +1 -1
  58. package/dist/sdk/agent-core/agent-loop.js +87 -4
  59. package/dist/sdk/agent-core/types.d.ts +1 -0
  60. package/dist/sdk/agent-core/types.d.ts.map +1 -1
  61. package/dist/sdk/ai/utils/validation.d.ts +0 -7
  62. package/dist/sdk/ai/utils/validation.d.ts.map +1 -1
  63. package/dist/sdk/ai/utils/validation.js +21 -1
  64. package/dist/sdk/coding-agent/core/extensions/runner.d.ts.map +1 -1
  65. package/dist/sdk/coding-agent/core/extensions/runner.js +15 -1
  66. package/dist/sdk/coding-agent/core/tools/apply-patch.d.ts +7 -2
  67. package/dist/sdk/coding-agent/core/tools/apply-patch.d.ts.map +1 -1
  68. package/dist/sdk/coding-agent/core/tools/apply-patch.js +219 -41
  69. package/dist/sdk/coding-agent/core/tools/atomic-write.d.ts +30 -0
  70. package/dist/sdk/coding-agent/core/tools/atomic-write.d.ts.map +1 -0
  71. package/dist/sdk/coding-agent/core/tools/atomic-write.js +42 -0
  72. package/dist/sdk/coding-agent/core/tools/edit-diff.d.ts +10 -4
  73. package/dist/sdk/coding-agent/core/tools/edit-diff.d.ts.map +1 -1
  74. package/dist/sdk/coding-agent/core/tools/edit-diff.js +44 -24
  75. package/dist/sdk/coding-agent/core/tools/edit.d.ts +6 -0
  76. package/dist/sdk/coding-agent/core/tools/edit.d.ts.map +1 -1
  77. package/dist/sdk/coding-agent/core/tools/edit.js +31 -5
  78. package/dist/sdk/coding-agent/core/tools/file-mutation-queue.d.ts +9 -3
  79. package/dist/sdk/coding-agent/core/tools/file-mutation-queue.d.ts.map +1 -1
  80. package/dist/sdk/coding-agent/core/tools/file-mutation-queue.js +42 -3
  81. package/dist/sdk/coding-agent/core/tools/read.d.ts.map +1 -1
  82. package/dist/sdk/coding-agent/core/tools/read.js +79 -47
  83. package/dist/sdk/coding-agent/core/tools/truncate.d.ts +4 -2
  84. package/dist/sdk/coding-agent/core/tools/truncate.d.ts.map +1 -1
  85. package/dist/sdk/coding-agent/core/tools/truncate.js +43 -23
  86. package/dist/sdk/coding-agent/core/tools/write.d.ts +5 -8
  87. package/dist/sdk/coding-agent/core/tools/write.d.ts.map +1 -1
  88. package/dist/sdk/coding-agent/core/tools/write.js +29 -11
  89. package/package.json +1 -1
@@ -35,20 +35,86 @@ const REQUIRED_PROPERTIES = {
35
35
  VideoObject: ["name", "uploadDate"],
36
36
  JobPosting: ["title", "description", "hiringOrganization"],
37
37
  Course: ["name", "provider"],
38
+ HowTo: ["name", "step"],
39
+ ItemList: ["itemListElement"],
40
+ Recipe: ["name", "recipeIngredient", "recipeInstructions"],
41
+ FAQPage: ["mainEntity"],
42
+ SoftwareApplication: ["name", "operatingSystem"],
43
+ WebApplication: ["name", "operatingSystem"],
44
+ Person: ["name"],
45
+ WebSite: ["name", "url"],
38
46
  };
47
+ /** Normalize a `@type` value (string or array) into a single type name,
48
+ * stripping a leading `https://schema.org/` prefix when present. */
49
+ function normalizeType(rawType) {
50
+ if (typeof rawType === "string") {
51
+ return rawType.replace("https://schema.org/", "");
52
+ }
53
+ if (Array.isArray(rawType) && rawType.length > 0) {
54
+ const first = rawType[0];
55
+ return typeof first === "string" ? first.replace("https://schema.org/", "") : "Unknown";
56
+ }
57
+ return "Unknown";
58
+ }
59
+ /** Build a map of `@id` -> entity from a list of JSON-LD items. */
60
+ function buildEntityMap(items) {
61
+ const map = new Map();
62
+ for (const item of items) {
63
+ if (!item || typeof item !== "object")
64
+ continue;
65
+ const obj = item;
66
+ const id = obj["@id"];
67
+ if (typeof id === "string" && id.length > 0 && !map.has(id)) {
68
+ map.set(id, item);
69
+ }
70
+ }
71
+ return map;
72
+ }
73
+ /** Resolve `@id` references recursively. A bare reference (an object containing
74
+ * only `@id`, or `@id` plus one other key) is replaced by the referenced
75
+ * entity. A `visited` Set prevents infinite loops on cyclic references. */
76
+ function resolveReferences(value, entityMap, visited) {
77
+ if (!value || typeof value !== "object")
78
+ return value;
79
+ if (Array.isArray(value)) {
80
+ return value.map((v) => resolveReferences(v, entityMap, visited));
81
+ }
82
+ const obj = value;
83
+ const id = obj["@id"];
84
+ if (typeof id === "string") {
85
+ const keys = Object.keys(obj);
86
+ const isBareRef = keys.length === 1 || keys.length === 2;
87
+ if (isBareRef && entityMap.has(id) && !visited.has(id)) {
88
+ visited.add(id);
89
+ const resolved = resolveReferences(entityMap.get(id), entityMap, visited);
90
+ visited.delete(id);
91
+ return resolved;
92
+ }
93
+ }
94
+ // Recursively resolve nested references on a shallow copy.
95
+ const out = {};
96
+ for (const [k, v] of Object.entries(obj)) {
97
+ out[k] = resolveReferences(v, entityMap, visited);
98
+ }
99
+ return out;
100
+ }
39
101
  /** Validate a parsed JSON-LD object and return issues. */
40
- function validateSchemaItem(item) {
102
+ function validateSchemaItem(item, entityMap) {
41
103
  if (!item || typeof item !== "object") {
42
104
  return { type: "Unknown", issues: ["Invalid schema object"] };
43
105
  }
44
- const obj = item;
106
+ let obj;
107
+ if (entityMap) {
108
+ const resolved = resolveReferences(item, entityMap, new Set());
109
+ obj = (resolved && typeof resolved === "object"
110
+ ? resolved
111
+ : item);
112
+ }
113
+ else {
114
+ obj = item;
115
+ }
45
116
  const rawType = obj["@type"];
46
- // Handle @type arrays (e.g. ["Article", "NewsArticle"]) and full URLs.
47
- const type = typeof rawType === "string"
48
- ? rawType.replace("https://schema.org/", "")
49
- : Array.isArray(rawType) && rawType.length > 0
50
- ? rawType[0].replace("https://schema.org/", "")
51
- : "Unknown";
117
+ const type = normalizeType(rawType);
52
118
  const issues = [];
53
119
  if (!obj["@context"]) {
54
120
  issues.push("Missing @context");
@@ -64,7 +130,7 @@ function validateSchemaItem(item) {
64
130
  }
65
131
  // Check for relative URLs.
66
132
  const jsonStr = JSON.stringify(obj);
67
- if (jsonStr.includes('"url":"/') || jsonStr.includes('"url": "/')) {
133
+ if (/("url"\s*:\s*"\/)[^/]/.test(jsonStr)) {
68
134
  issues.push("Contains relative URL — should be absolute");
69
135
  }
70
136
  // Check date formats.
@@ -85,11 +151,25 @@ export function auditSchema(html, url) {
85
151
  const deprecatedTypes = [];
86
152
  const relativeUrls = [];
87
153
  const invalidDates = [];
154
+ const duplicateEntities = [];
88
155
  let missingContext = false;
89
156
  // Re-validate with deeper checks using parsed JSON-LD.
90
157
  const jsonLdItems = extractJsonLd(html);
158
+ const entityMap = buildEntityMap(jsonLdItems);
159
+ const seenIds = new Set();
91
160
  for (const item of jsonLdItems) {
92
- const { type, issues } = validateSchemaItem(item);
161
+ if (item && typeof item === "object") {
162
+ const id = item["@id"];
163
+ if (typeof id === "string" && id.length > 0) {
164
+ if (seenIds.has(id)) {
165
+ duplicateEntities.push(id);
166
+ }
167
+ else {
168
+ seenIds.add(id);
169
+ }
170
+ }
171
+ }
172
+ const { type, issues } = validateSchemaItem(item, entityMap);
93
173
  if (DEPRECATED_TYPES.has(type)) {
94
174
  deprecatedTypes.push(type);
95
175
  }
@@ -115,6 +195,9 @@ export function auditSchema(html, url) {
115
195
  if (detectedSchemas.length === 0) {
116
196
  recommendations.push("No structured data found. Add JSON-LD markup for rich result eligibility.");
117
197
  }
198
+ if (duplicateEntities.length > 0) {
199
+ recommendations.push(`Duplicate @id entities detected: ${duplicateEntities.length}. Deduplicate entities referenced by @id.`);
200
+ }
118
201
  // Recommend types based on page content.
119
202
  const hasArticle = pageData.wordCount > 300 && (pageData.title ?? "").length > 0;
120
203
  if (hasArticle && !detectedSchemas.some((s) => ["Article", "BlogPosting", "NewsArticle"].includes(s.type))) {
@@ -123,8 +206,14 @@ export function auditSchema(html, url) {
123
206
  if (detectedSchemas.length > 0 && !detectedSchemas.some((s) => s.type === "BreadcrumbList")) {
124
207
  recommendations.push("Add BreadcrumbList schema to improve navigation rich results.");
125
208
  }
209
+ const schemaIssues = deprecatedTypes.length + relativeUrls.length + invalidDates.length +
210
+ (missingContext ? 1 : 0) + duplicateEntities.length;
211
+ const score = detectedSchemas.length === 0
212
+ ? 0
213
+ : Math.max(0, Math.round(100 - (schemaIssues / detectedSchemas.length) * 25));
126
214
  return {
127
215
  url,
216
+ score,
128
217
  detectedSchemas,
129
218
  deprecatedTypes,
130
219
  missingContext,
@@ -1 +1 @@
1
- {"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/sitemap.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AA8C9C,wCAAwC;AACxC,wBAAsB,cAAc,CAClC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,SAAS,GAChB,OAAO,CAAC,qBAAqB,CAAC,CA4DhC;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,mDAAmD;AACnD,wBAAgB,eAAe,CAAC,OAAO,EAAE,oBAAoB,EAAE,GAAG,MAAM,CAuBvE"}
1
+ {"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/sitemap.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AA8C9C,wCAAwC;AACxC,wBAAsB,cAAc,CAClC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,SAAS,GAChB,OAAO,CAAC,qBAAqB,CAAC,CAsGhC;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,mDAAmD;AACnD,wBAAgB,eAAe,CAAC,OAAO,EAAE,oBAAoB,EAAE,GAAG,MAAM,CAuBvE"}
@@ -52,39 +52,80 @@ export async function analyzeSitemap(sitemapUrl, config) {
52
52
  // Validate URLs.
53
53
  let invalidEntries = 0;
54
54
  const validUrls = [];
55
- for (const u of parsed.urls) {
56
- try {
57
- new URL(u.loc);
58
- validUrls.push(u.loc);
55
+ let childSitemapUrls = parsed.childSitemaps;
56
+ // Recursively resolve sitemap indexes up to 10 children.
57
+ if (parsed.isIndex && childSitemapUrls.length > 0) {
58
+ const childLimit = Math.min(childSitemapUrls.length, 10);
59
+ const childResults = await Promise.allSettled(childSitemapUrls.slice(0, childLimit).map((childUrl) => fetchPage(childUrl, config)));
60
+ for (let i = 0; i < childResults.length; i++) {
61
+ const child = childResults[i];
62
+ if (child.status === "fulfilled" && child.value.statusCode === 200) {
63
+ const childParsed = parseSitemapXml(child.value.content);
64
+ if (!childParsed.isIndex) {
65
+ for (const u of childParsed.urls) {
66
+ try {
67
+ new URL(u.loc);
68
+ validUrls.push(u.loc);
69
+ }
70
+ catch {
71
+ invalidEntries++;
72
+ }
73
+ }
74
+ }
75
+ else {
76
+ warnings.push(`Nested sitemap index at ${childSitemapUrls[i]} — only 2 levels supported.`);
77
+ }
78
+ }
79
+ else {
80
+ const err = child.status === "rejected" ? child.reason : child.value.statusCode;
81
+ warnings.push(`Failed to fetch child sitemap ${childSitemapUrls[i]}: ${err}`);
82
+ }
59
83
  }
60
- catch {
61
- invalidEntries++;
84
+ if (childSitemapUrls.length > 10) {
85
+ warnings.push(`Sitemap index has ${childSitemapUrls.length} children, only first 10 were fetched.`);
62
86
  }
63
87
  }
88
+ else {
89
+ for (const u of parsed.urls) {
90
+ try {
91
+ new URL(u.loc);
92
+ validUrls.push(u.loc);
93
+ }
94
+ catch {
95
+ invalidEntries++;
96
+ }
97
+ }
98
+ }
99
+ const totalUrls = validUrls.length;
64
100
  // Calculate lastmod coverage.
65
- const withLastmod = parsed.urls.filter((u) => u.lastmod).length;
66
- const lastmodCoverage = parsed.urls.length > 0
67
- ? Math.round((withLastmod / parsed.urls.length) * 100)
68
- : 0;
101
+ const withLastmod = parsed.isIndex ? 0 : parsed.urls.filter((u) => u.lastmod).length;
102
+ const lastmodCoverage = parsed.isIndex
103
+ ? 0
104
+ : parsed.urls.length > 0
105
+ ? Math.round((withLastmod / parsed.urls.length) * 100)
106
+ : 0;
107
+ if (parsed.isIndex) {
108
+ warnings.push(`Resolved ${validUrls.length} URLs from ${Math.min(childSitemapUrls.length, 10)} child sitemaps.`);
109
+ }
69
110
  // Size warnings.
70
- if (result.contentLengthBytes > 50_000_000) {
111
+ if (!parsed.isIndex && result.contentLengthBytes > 50_000_000) {
71
112
  warnings.push("Sitemap exceeds 50MB uncompressed limit. Split into multiple sitemaps.");
72
113
  }
73
- if (parsed.urls.length > 50_000) {
74
- warnings.push("Sitemap contains more than 50,000 URLs. Split into multiple sitemaps.");
114
+ if (!parsed.isIndex && parsed.urls.length > 50_000) {
115
+ warnings.push(`Sitemap contains ${parsed.urls.length} URLs, exceeding the 50,000 limit. Split into multiple sitemaps.`);
75
116
  }
76
- if (lastmodCoverage < 50 && parsed.urls.length > 0) {
117
+ if (!parsed.isIndex && lastmodCoverage < 50 && parsed.urls.length > 0) {
77
118
  warnings.push(`Only ${lastmodCoverage}% of URLs have lastmod dates. Adding lastmod improves crawl efficiency.`);
78
119
  }
79
120
  return {
80
121
  url: result.finalUrl,
81
- urlCount: parsed.urls.length,
122
+ urlCount: totalUrls,
82
123
  urls: validUrls,
83
124
  lastmodCoverage,
84
125
  invalidEntries,
85
126
  sizeBytes: result.contentLengthBytes,
86
127
  isSitemapIndex: parsed.isIndex,
87
- childSitemaps: parsed.childSitemaps,
128
+ childSitemaps: childSitemapUrls,
88
129
  warnings,
89
130
  };
90
131
  }
@@ -1,10 +1,3 @@
1
- /**
2
- * Technical SEO audit analyzer.
3
- *
4
- * Ported from the Python `seo-technical/SKILL.md` directives in claude-seo.
5
- * Audits crawlability, indexability, security, URL structure, mobile,
6
- * and AI crawler management — all using local HTTP fetches only.
7
- */
8
1
  import type { SeoConfig } from "../config.js";
9
2
  import type { FetchResult, PageData, TechnicalAuditResult } from "../types.js";
10
3
  export declare function auditTechnical(url: string, config: SeoConfig, pageData?: PageData, fetchResult?: FetchResult): Promise<TechnicalAuditResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"technical.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/technical.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAc,WAAW,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AA2D3F,wBAAsB,cAAc,CAClC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,SAAS,EACjB,QAAQ,CAAC,EAAE,QAAQ,EACnB,WAAW,CAAC,EAAE,WAAW,GACxB,OAAO,CAAC,oBAAoB,CAAC,CA+N/B"}
1
+ {"version":3,"file":"technical.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/technical.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAc,WAAW,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAiE3F,wBAAsB,cAAc,CAClC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,SAAS,EACjB,QAAQ,CAAC,EAAE,QAAQ,EACnB,WAAW,CAAC,EAAE,WAAW,GACxB,OAAO,CAAC,oBAAoB,CAAC,CAqoB/B"}