sneakoscope 4.2.0 → 4.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 (126) hide show
  1. package/README.md +35 -8
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/bin/sks.js +1 -1
  6. package/dist/cli/command-registry.js +3 -1
  7. package/dist/cli/ultra-search-command.js +163 -0
  8. package/dist/cli/xai-command.js +28 -168
  9. package/dist/core/agents/agent-codex-cockpit.js +3 -3
  10. package/dist/core/agents/agent-runner-ollama.js +2 -0
  11. package/dist/core/agents/agent-wrongness.js +1 -1
  12. package/dist/core/agents/native-worker-backend-router.js +3 -0
  13. package/dist/core/bench.js +115 -0
  14. package/dist/core/code-structure.js +399 -11
  15. package/dist/core/codex-control/codex-app-server-v2-client.js +86 -2
  16. package/dist/core/codex-control/codex-fake-sdk-adapter.js +67 -9
  17. package/dist/core/codex-control/codex-reliability-shield.js +26 -5
  18. package/dist/core/codex-control/codex-task-runner.js +7 -1
  19. package/dist/core/codex-control/gpt-final-arbiter.js +4 -1
  20. package/dist/core/codex-control/gpt-final-review-schema.js +58 -0
  21. package/dist/core/codex-control/model-call-concurrency.js +1 -1
  22. package/dist/core/codex-native/core-skill-manifest.js +23 -0
  23. package/dist/core/commands/bench-command.js +11 -2
  24. package/dist/core/commands/code-structure-command.js +34 -2
  25. package/dist/core/commands/qa-loop-command.js +23 -7
  26. package/dist/core/commands/run-command.js +92 -2
  27. package/dist/core/commands/seo-command.js +130 -0
  28. package/dist/core/feature-fixtures.js +6 -0
  29. package/dist/core/feature-registry.js +3 -1
  30. package/dist/core/fsx.js +1 -1
  31. package/dist/core/hooks-runtime.js +9 -1
  32. package/dist/core/init.js +8 -6
  33. package/dist/core/lean-engineering-policy.js +159 -0
  34. package/dist/core/pipeline-internals/runtime-core.js +15 -5
  35. package/dist/core/proof/auto-finalize.js +3 -2
  36. package/dist/core/proof/proof-schema.js +2 -1
  37. package/dist/core/proof/proof-writer.js +1 -0
  38. package/dist/core/proof/route-adapter.js +4 -2
  39. package/dist/core/proof/route-finalizer.js +35 -3
  40. package/dist/core/qa-loop/qa-app-server-driver.js +134 -0
  41. package/dist/core/qa-loop/qa-contract-v2.js +231 -0
  42. package/dist/core/qa-loop/qa-gate-v2.js +132 -0
  43. package/dist/core/qa-loop/qa-runtime-artifacts.js +53 -0
  44. package/dist/core/qa-loop/qa-surface-router.js +114 -0
  45. package/dist/core/qa-loop/qa-types.js +18 -0
  46. package/dist/core/qa-loop.js +83 -26
  47. package/dist/core/release/gate-manifest.js +1 -0
  48. package/dist/core/release/sla-scheduler.js +1 -1
  49. package/dist/core/release-parallel-full-coverage.js +1 -1
  50. package/dist/core/routes.js +96 -14
  51. package/dist/core/search-visibility/adapter-registry.js +26 -0
  52. package/dist/core/search-visibility/adapters/next-app.js +6 -0
  53. package/dist/core/search-visibility/adapters/next-pages.js +6 -0
  54. package/dist/core/search-visibility/adapters/static-site.js +6 -0
  55. package/dist/core/search-visibility/analyzers.js +377 -0
  56. package/dist/core/search-visibility/artifacts.js +183 -0
  57. package/dist/core/search-visibility/discovery.js +347 -0
  58. package/dist/core/search-visibility/index.js +199 -0
  59. package/dist/core/search-visibility/mission.js +67 -0
  60. package/dist/core/search-visibility/mutation.js +314 -0
  61. package/dist/core/search-visibility/types.js +2 -0
  62. package/dist/core/search-visibility/verifier.js +60 -0
  63. package/dist/core/source-intelligence/source-intelligence-policy.js +45 -26
  64. package/dist/core/source-intelligence/source-intelligence-proof.js +10 -16
  65. package/dist/core/source-intelligence/source-intelligence-runner.js +56 -42
  66. package/dist/core/triwiki/triwiki-affected-graph.js +3 -2
  67. package/dist/core/trust-kernel/trust-report.js +3 -5
  68. package/dist/core/ultra-search/index.js +3 -0
  69. package/dist/core/ultra-search/runtime.js +502 -0
  70. package/dist/core/ultra-search/types.js +3 -0
  71. package/dist/core/version.js +1 -1
  72. package/dist/scripts/agent-visual-consistency-check.js +1 -1
  73. package/dist/scripts/check-architecture.js +40 -7
  74. package/dist/scripts/check-command-module-budget.js +43 -5
  75. package/dist/scripts/check-pipeline-budget.js +17 -30
  76. package/dist/scripts/check-publish-tag.js +33 -6
  77. package/dist/scripts/check-route-modularity.js +25 -33
  78. package/dist/scripts/check-runtime-schemas.js +22 -0
  79. package/dist/scripts/codex-control-all-pipelines-check.js +1 -0
  80. package/dist/scripts/codex-control-model-capacity-fallback-check.js +53 -0
  81. package/dist/scripts/config-managed-merge-callsite-coverage-check.js +7 -1
  82. package/dist/scripts/core-skill-immutable-sync-check.js +3 -2
  83. package/dist/scripts/core-skill-integrity-blackbox.js +3 -2
  84. package/dist/scripts/core-skill-manifest-check.js +7 -2
  85. package/dist/scripts/geo-claim-evidence-check.js +18 -0
  86. package/dist/scripts/geo-cli-blackbox-check.js +18 -0
  87. package/dist/scripts/geo-crawler-policy-check.js +16 -0
  88. package/dist/scripts/geo-llms-txt-optional-check.js +19 -0
  89. package/dist/scripts/gpt-final-arbiter-check.js +4 -1
  90. package/dist/scripts/loop-directive-check-lib.js +78 -1
  91. package/dist/scripts/qa-loop-app-server-driver-check.js +74 -0
  92. package/dist/scripts/qa-loop-surface-router-check.js +49 -0
  93. package/dist/scripts/release-check-dynamic-execute.js +1 -1
  94. package/dist/scripts/release-metadata-1-19-check.js +2 -2
  95. package/dist/scripts/release-parallel-check.js +17 -2
  96. package/dist/scripts/release-parallel-full-coverage-check.js +1 -1
  97. package/dist/scripts/release-readiness-report.js +6 -6
  98. package/dist/scripts/release-registry-check.js +33 -14
  99. package/dist/scripts/runtime-ts-rust-boundary-check.js +1 -1
  100. package/dist/scripts/search-visibility-gate-lib.js +124 -0
  101. package/dist/scripts/seo-audit-fixture-check.js +16 -0
  102. package/dist/scripts/seo-canonical-locale-check.js +19 -0
  103. package/dist/scripts/seo-cli-blackbox-check.js +18 -0
  104. package/dist/scripts/seo-geo-feature-fixture-quality-check.js +18 -0
  105. package/dist/scripts/seo-geo-geo-disambiguation-check.js +12 -0
  106. package/dist/scripts/seo-geo-no-unsupported-ranking-claims-check.js +18 -0
  107. package/dist/scripts/seo-geo-route-identity-check.js +12 -0
  108. package/dist/scripts/seo-geo-skill-rich-content-check.js +22 -0
  109. package/dist/scripts/seo-mutation-rollback-check.js +23 -0
  110. package/dist/scripts/seo-no-mutation-by-default-check.js +17 -0
  111. package/dist/scripts/seo-structured-data-visible-content-check.js +19 -0
  112. package/dist/scripts/sks-1-18-gate-lib.js +2 -2
  113. package/dist/scripts/sks-3-1-5-directive-check-lib.js +10 -1
  114. package/dist/scripts/source-intelligence-all-modes-check.js +9 -19
  115. package/dist/scripts/source-intelligence-policy-check.js +6 -6
  116. package/dist/scripts/triwiki-affected-graph-check.js +2 -2
  117. package/dist/scripts/ultra-search-provider-interface-check.js +27 -0
  118. package/package.json +26 -5
  119. package/schemas/search-visibility/finding-ledger.schema.json +36 -0
  120. package/schemas/search-visibility/gate.schema.json +22 -0
  121. package/schemas/search-visibility/mutation-plan.schema.json +27 -0
  122. package/schemas/search-visibility/site-inventory.schema.json +21 -0
  123. package/schemas/search-visibility/verification-report.schema.json +23 -0
  124. package/dist/core/mcp/xai-mcp-detector.js +0 -157
  125. package/dist/core/mcp/xai-search-adapter.js +0 -100
  126. package/dist/scripts/xai-mcp-capability-check.js +0 -14
@@ -0,0 +1,347 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { exists, readJson, readText, sha256 } from '../fsx.js';
4
+ const MAX_DISCOVERY_FILES = 2500;
5
+ const DEFAULT_CAPABILITIES = {
6
+ sourceAudit: true,
7
+ builtHtmlAudit: false,
8
+ liveHttpAudit: false,
9
+ renderedBrowserAudit: false,
10
+ metadataMutation: false,
11
+ sitemapMutation: false,
12
+ robotsMutation: false,
13
+ structuredDataMutation: false,
14
+ localeMutation: false,
15
+ };
16
+ export async function detectProject(ctx) {
17
+ const files = await walkFiles(ctx.root, MAX_DISCOVERY_FILES);
18
+ const rels = new Set(files);
19
+ const packageJson = await readJson(path.join(ctx.root, 'package.json'), {});
20
+ const deps = {
21
+ ...(isRecord(packageJson.dependencies) ? packageJson.dependencies : {}),
22
+ ...(isRecord(packageJson.devDependencies) ? packageJson.devDependencies : {}),
23
+ };
24
+ const hasNext = Object.hasOwn(deps, 'next') || rels.has('next.config.js') || rels.has('next.config.mjs') || rels.has('next.config.ts');
25
+ const appEvidence = files.find((file) => /(?:^|\/)app\/(?:layout|page)\.(?:tsx|ts|jsx|js|mdx)$/.test(file));
26
+ const pagesEvidence = files.find((file) => /(?:^|\/)pages\/(?:index|_app|_document|.+)\.(?:tsx|ts|jsx|js|mdx)$/.test(file));
27
+ const staticEvidence = files.find((file) => /(?:^|\/)(?:public\/)?index\.html$/.test(file));
28
+ const forced = ctx.framework !== 'auto' ? ctx.framework : null;
29
+ if (forced && forced !== 'unsupported')
30
+ return forcedDetection(forced, files);
31
+ if (hasNext && appEvidence)
32
+ return detection('next-app', 0.95, [{ path: appEvidence, reason: 'Next.js App Router route source found' }], mutationCapabilities('next-app'));
33
+ if (hasNext && pagesEvidence)
34
+ return detection('next-pages', 0.9, [{ path: pagesEvidence, reason: 'Next.js Pages Router source found' }], mutationCapabilities('next-pages'));
35
+ if (staticEvidence)
36
+ return detection('static-site', 0.82, [{ path: staticEvidence, reason: 'Static HTML entry point found' }], mutationCapabilities('static'));
37
+ if (await exists(path.join(ctx.root, 'package.json')))
38
+ return detection('package', 0.75, [{ path: 'package.json', reason: 'Package metadata found' }], mutationCapabilities('package'));
39
+ return detection('unsupported', 0.25, [], DEFAULT_CAPABILITIES, ['No package, Next.js, or static HTML evidence found']);
40
+ }
41
+ export async function discoverSiteInventory(ctx, detected) {
42
+ const files = await walkFiles(ctx.root, MAX_DISCOVERY_FILES);
43
+ const packageJson = await readJson(path.join(ctx.root, 'package.json'), {});
44
+ const readmePath = await firstExisting(ctx.root, ['README.md', 'readme.md']);
45
+ const readme = readmePath ? await readText(path.join(ctx.root, readmePath), '') : '';
46
+ const htmlFiles = await Promise.all(files.filter((file) => file.endsWith('.html')).slice(0, 200).map((file) => summarizeHtml(ctx.root, file)));
47
+ const routes = discoverRoutes(files, htmlFiles);
48
+ const target = resolveTarget(ctx.target, detected, files);
49
+ const inventory = {
50
+ schema: 'sks.search-visibility.site-inventory.v1',
51
+ root: ctx.root,
52
+ origin: ctx.origin,
53
+ target,
54
+ detected_adapter: detected,
55
+ package: {
56
+ path: await exists(path.join(ctx.root, 'package.json')) ? 'package.json' : null,
57
+ name: stringOrNull(packageJson.name),
58
+ version: stringOrNull(packageJson.version),
59
+ description: stringOrNull(packageJson.description),
60
+ keywords: Array.isArray(packageJson.keywords) ? packageJson.keywords.map(String) : [],
61
+ repository: repositoryUrl(packageJson.repository),
62
+ homepage: stringOrNull(packageJson.homepage),
63
+ bugs: repositoryUrl(packageJson.bugs),
64
+ bin: isRecord(packageJson.bin) ? Object.keys(packageJson.bin) : (typeof packageJson.bin === 'string' ? [String(packageJson.name || '')].filter(Boolean) : []),
65
+ scripts: isRecord(packageJson.scripts) ? stringifyRecord(packageJson.scripts) : {},
66
+ framework_versions: frameworkVersions(packageJson),
67
+ },
68
+ readme: {
69
+ path: readmePath,
70
+ h1: firstMarkdownHeading(readme, 1),
71
+ headings: markdownHeadings(readme),
72
+ command_mentions: commandMentions(readme),
73
+ links: markdownLinks(readme),
74
+ },
75
+ routes,
76
+ html_files: htmlFiles,
77
+ policy_files: await policyFiles(ctx.root),
78
+ locale_candidates: discoverLocales(files, htmlFiles),
79
+ metadata_helpers: files.filter((file) => /(seo|metadata|schema|json-ld|structured-data|canonical|sitemap|robots)/i.test(file)).slice(0, 100),
80
+ structured_data_sources: files.filter((file) => /(schema|json-ld|structured-data|ld-json)/i.test(file)).slice(0, 100),
81
+ live_url_checked: Boolean(ctx.origin && !ctx.offline),
82
+ browser_checked: false,
83
+ generated_at: new Date().toISOString(),
84
+ };
85
+ return inventory;
86
+ }
87
+ export function sourceEvidence(pathValue, summary, hash = null, line = null) {
88
+ return {
89
+ type: 'source',
90
+ path: pathValue,
91
+ line,
92
+ selector: null,
93
+ hash,
94
+ url: null,
95
+ observed_at: new Date().toISOString(),
96
+ summary,
97
+ };
98
+ }
99
+ export function officialEvidence(url, summary) {
100
+ return {
101
+ type: 'official_source',
102
+ path: null,
103
+ line: null,
104
+ selector: null,
105
+ hash: null,
106
+ url,
107
+ observed_at: new Date().toISOString(),
108
+ summary,
109
+ };
110
+ }
111
+ export async function walkFiles(root, maxFiles = MAX_DISCOVERY_FILES) {
112
+ const out = [];
113
+ const ignored = new Set(['.git', 'node_modules', 'dist', '.next', '.sneakoscope', '.codex', '.agents', 'coverage', 'archive']);
114
+ async function walk(dir) {
115
+ if (out.length >= maxFiles)
116
+ return;
117
+ let entries;
118
+ try {
119
+ entries = await fs.readdir(dir, { withFileTypes: true });
120
+ }
121
+ catch {
122
+ return;
123
+ }
124
+ for (const entry of entries) {
125
+ if (out.length >= maxFiles)
126
+ return;
127
+ if (ignored.has(entry.name))
128
+ continue;
129
+ const full = path.join(dir, entry.name);
130
+ const rel = path.relative(root, full).split(path.sep).join('/');
131
+ if (entry.isDirectory())
132
+ await walk(full);
133
+ else if (entry.isFile())
134
+ out.push(rel);
135
+ }
136
+ }
137
+ await walk(root);
138
+ return out.sort();
139
+ }
140
+ function forcedDetection(framework, files) {
141
+ const adapterId = framework === 'static' ? 'static-site' : framework;
142
+ return detection(adapterId, 0.8, [{ path: files[0] || '.', reason: `Framework forced by --framework ${framework}` }], mutationCapabilities(framework));
143
+ }
144
+ function detection(adapterId, confidence, evidence, capabilities, blockers = []) {
145
+ return { adapterId, confidence, evidence, capabilities, blockers };
146
+ }
147
+ function mutationCapabilities(framework) {
148
+ const base = { ...DEFAULT_CAPABILITIES };
149
+ if (framework === 'next-app' || framework === 'next-pages') {
150
+ return { ...base, metadataMutation: true, sitemapMutation: true, robotsMutation: true, structuredDataMutation: true, localeMutation: true };
151
+ }
152
+ if (framework === 'static' || framework === 'static-site') {
153
+ return { ...base, builtHtmlAudit: true, metadataMutation: true, sitemapMutation: true, robotsMutation: true, structuredDataMutation: true };
154
+ }
155
+ if (framework === 'package')
156
+ return { ...base };
157
+ return base;
158
+ }
159
+ function resolveTarget(target, detected, files) {
160
+ if (target !== 'auto')
161
+ return target;
162
+ if (detected.adapterId === 'package')
163
+ return 'package';
164
+ if (files.some((file) => /(?:^|\/)(docs|documentation)\//i.test(file)))
165
+ return 'docs';
166
+ return detected.adapterId === 'unsupported' ? 'package' : 'website';
167
+ }
168
+ async function summarizeHtml(root, rel) {
169
+ const text = await readText(path.join(root, rel), '');
170
+ const jsonLdBlocks = Array.from(text.matchAll(/<script[^>]+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi));
171
+ const jsonLdParseErrors = [];
172
+ for (const match of jsonLdBlocks) {
173
+ const raw = match[1] || '';
174
+ try {
175
+ JSON.parse(raw.trim());
176
+ }
177
+ catch (err) {
178
+ jsonLdParseErrors.push(err instanceof Error ? err.message : String(err));
179
+ }
180
+ }
181
+ return {
182
+ path: rel,
183
+ title: tagText(text, 'title'),
184
+ description: metaContent(text, 'description'),
185
+ canonical: linkHref(text, 'canonical'),
186
+ robots: metaContent(text, 'robots'),
187
+ lang: htmlLang(text),
188
+ links: hrefs(text).slice(0, 500),
189
+ jsonLdCount: jsonLdBlocks.length,
190
+ jsonLdParseErrors,
191
+ visibleTextSample: stripTags(text).replace(/\s+/g, ' ').trim().slice(0, 500),
192
+ };
193
+ }
194
+ function discoverRoutes(files, htmlFiles) {
195
+ const routes = [];
196
+ for (const html of htmlFiles)
197
+ routes.push({ path: routePathFromHtml(html.path), source: html.path, kind: 'static', locale: localeFromPath(html.path) });
198
+ for (const file of files) {
199
+ const app = file.match(/(?:^|\/)app\/(.+)\/page\.(?:tsx|ts|jsx|js|mdx)$/);
200
+ if (app?.[1])
201
+ routes.push({ path: `/${normalizeRouteSegments(app[1])}`, source: file, kind: routeKind(app[1]), locale: localeFromPath(app[1]) });
202
+ const appRoot = file.match(/(?:^|\/)app\/page\.(?:tsx|ts|jsx|js|mdx)$/);
203
+ if (appRoot)
204
+ routes.push({ path: '/', source: file, kind: 'static', locale: null });
205
+ const pages = file.match(/(?:^|\/)pages\/(.+)\.(?:tsx|ts|jsx|js|mdx)$/);
206
+ if (pages?.[1] && !pages[1].startsWith('_'))
207
+ routes.push({ path: `/${normalizeRouteSegments(pages[1].replace(/\/index$/, ''))}`, source: file, kind: routeKind(pages[1]), locale: localeFromPath(pages[1]) });
208
+ }
209
+ const byKey = new Map();
210
+ for (const route of routes)
211
+ byKey.set(`${route.path}:${route.source}`, route);
212
+ return Array.from(byKey.values()).sort((a, b) => a.path.localeCompare(b.path));
213
+ }
214
+ async function policyFiles(root) {
215
+ const specs = [
216
+ ['robots.txt', 'robots'],
217
+ ['public/robots.txt', 'robots'],
218
+ ['sitemap.xml', 'sitemap'],
219
+ ['public/sitemap.xml', 'sitemap'],
220
+ ['llms.txt', 'llms'],
221
+ ['public/llms.txt', 'llms'],
222
+ ['site.webmanifest', 'manifest'],
223
+ ['public/site.webmanifest', 'manifest'],
224
+ ];
225
+ const out = [];
226
+ for (const [rel, kind] of specs) {
227
+ const full = path.join(root, rel);
228
+ const present = await exists(full);
229
+ const text = present ? await readText(full, '') : '';
230
+ out.push({
231
+ path: rel,
232
+ kind,
233
+ exists: present,
234
+ managed: /sks-search-visibility|BEGIN SKS SEARCH VISIBILITY/i.test(text),
235
+ hash: present ? sha256(text) : null,
236
+ });
237
+ }
238
+ return out;
239
+ }
240
+ function discoverLocales(files, htmlFiles) {
241
+ const candidates = new Map();
242
+ for (const html of htmlFiles) {
243
+ if (html.lang)
244
+ candidates.set(html.lang, { code: html.lang, source: `${html.path}#html-lang`, confidence: 0.9 });
245
+ }
246
+ for (const file of files) {
247
+ const locale = localeFromPath(file);
248
+ if (locale && !candidates.has(locale))
249
+ candidates.set(locale, { code: locale, source: file, confidence: 0.7 });
250
+ }
251
+ return Array.from(candidates.values()).sort((a, b) => a.code.localeCompare(b.code));
252
+ }
253
+ function routePathFromHtml(rel) {
254
+ const normalized = rel.replace(/^public\//, '').replace(/index\.html$/, '').replace(/\.html$/, '');
255
+ const route = `/${normalized}`.replace(/\/+/g, '/');
256
+ return route === '/' ? '/' : route.replace(/\/$/, '');
257
+ }
258
+ function normalizeRouteSegments(value) {
259
+ return value
260
+ .replace(/\/index$/, '')
261
+ .replace(/\[(\.\.\.)?([^\]]+)\]/g, ':$2')
262
+ .replace(/^\(([^)]+)\)\//, '')
263
+ .replace(/\/+/g, '/')
264
+ .replace(/^\/|\/$/g, '');
265
+ }
266
+ function routeKind(value) {
267
+ if (/\[.+\]|:\w+/.test(value))
268
+ return 'parameterized';
269
+ return 'static';
270
+ }
271
+ function localeFromPath(value) {
272
+ const first = value.split('/').find(Boolean) || '';
273
+ return /^[a-z]{2}(?:-[A-Z]{2})?$/.test(first) ? first : null;
274
+ }
275
+ async function firstExisting(root, rels) {
276
+ for (const rel of rels) {
277
+ if (await exists(path.join(root, rel)))
278
+ return rel;
279
+ }
280
+ return null;
281
+ }
282
+ function isRecord(value) {
283
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
284
+ }
285
+ function stringifyRecord(value) {
286
+ return Object.fromEntries(Object.entries(value).map(([key, val]) => [key, String(val)]));
287
+ }
288
+ function stringOrNull(value) {
289
+ return typeof value === 'string' && value.trim() ? value : null;
290
+ }
291
+ function repositoryUrl(value) {
292
+ if (typeof value === 'string')
293
+ return value;
294
+ if (isRecord(value) && typeof value.url === 'string')
295
+ return value.url;
296
+ return null;
297
+ }
298
+ function frameworkVersions(packageJson) {
299
+ const deps = {
300
+ ...(isRecord(packageJson.dependencies) ? packageJson.dependencies : {}),
301
+ ...(isRecord(packageJson.devDependencies) ? packageJson.devDependencies : {}),
302
+ };
303
+ const names = ['next', 'react', 'astro', 'nuxt', 'svelte', 'vite'];
304
+ return Object.fromEntries(names.filter((name) => typeof deps[name] === 'string').map((name) => [name, String(deps[name])]));
305
+ }
306
+ function firstMarkdownHeading(text, level) {
307
+ const prefix = '#'.repeat(level);
308
+ const line = text.split(/\r?\n/).find((row) => row.startsWith(`${prefix} `));
309
+ return line ? line.replace(/^#+\s*/, '').trim() : null;
310
+ }
311
+ function markdownHeadings(text) {
312
+ return text.split(/\r?\n/).filter((line) => /^#{1,6}\s+/.test(line)).map((line) => line.replace(/^#+\s*/, '').trim()).slice(0, 200);
313
+ }
314
+ function commandMentions(text) {
315
+ return Array.from(new Set(Array.from(text.matchAll(/\b(?:sks|npx|npm|node)\s+[^\n`]{1,80}/g)).map((m) => (m[0] || '').trim()).filter(Boolean))).slice(0, 100);
316
+ }
317
+ function markdownLinks(text) {
318
+ return Array.from(new Set(Array.from(text.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)).map((m) => m[1] || '').filter(Boolean))).slice(0, 500);
319
+ }
320
+ function tagText(text, tag) {
321
+ const re = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, 'i');
322
+ const match = text.match(re);
323
+ return match?.[1] ? stripTags(match[1]).trim() || null : null;
324
+ }
325
+ function metaContent(text, name) {
326
+ const re = new RegExp(`<meta[^>]+(?:name|property)=["']${escapeRegex(name)}["'][^>]*content=["']([^"']+)["'][^>]*>`, 'i');
327
+ const alt = new RegExp(`<meta[^>]+content=["']([^"']+)["'][^>]*(?:name|property)=["']${escapeRegex(name)}["'][^>]*>`, 'i');
328
+ return text.match(re)?.[1] || text.match(alt)?.[1] || null;
329
+ }
330
+ function linkHref(text, rel) {
331
+ const re = new RegExp(`<link[^>]+rel=["']${escapeRegex(rel)}["'][^>]*href=["']([^"']+)["'][^>]*>`, 'i');
332
+ const alt = new RegExp(`<link[^>]+href=["']([^"']+)["'][^>]*rel=["']${escapeRegex(rel)}["'][^>]*>`, 'i');
333
+ return text.match(re)?.[1] || text.match(alt)?.[1] || null;
334
+ }
335
+ function htmlLang(text) {
336
+ return text.match(/<html[^>]+lang=["']([^"']+)["']/i)?.[1] || null;
337
+ }
338
+ function hrefs(text) {
339
+ return Array.from(new Set(Array.from(text.matchAll(/<a[^>]+href=["']([^"']+)["']/gi)).map((m) => m[1] || '').filter(Boolean)));
340
+ }
341
+ function stripTags(text) {
342
+ return text.replace(/<script[\s\S]*?<\/script>/gi, ' ').replace(/<style[\s\S]*?<\/style>/gi, ' ').replace(/<[^>]+>/g, ' ');
343
+ }
344
+ function escapeRegex(value) {
345
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
346
+ }
347
+ //# sourceMappingURL=discovery.js.map
@@ -0,0 +1,199 @@
1
+ import path from 'node:path';
2
+ import { adapterForDetection } from './adapter-registry.js';
3
+ import { auditGeo, auditSeo } from './analyzers.js';
4
+ import { detectProject, discoverSiteInventory } from './discovery.js';
5
+ import { applyMutationPlan, buildMutationPlan, rollbackMutationPlan } from './mutation.js';
6
+ import { createSearchVisibilityMission, resolveSearchVisibilityMission, routeForMode } from './mission.js';
7
+ import { finalizeSearchVisibility, statusForMission, writeAuditArtifacts, writeGate } from './artifacts.js';
8
+ import { verifySearchVisibility } from './verifier.js';
9
+ import { projectRoot, readJson, writeJsonAtomic } from '../fsx.js';
10
+ export * from './types.js';
11
+ export { createSearchVisibilityMission, resolveSearchVisibilityMission } from './mission.js';
12
+ export async function runSearchVisibilityAudit(mode, options) {
13
+ const root = await projectRoot(options.root);
14
+ const ctx = context(mode, root, options);
15
+ const mission = await createSearchVisibilityMission(mode, `${mode} audit`, options);
16
+ const detected = await detectProject(ctx);
17
+ const adapter = adapterForDetection(detected);
18
+ const inventory = await adapter.discover(ctx, detected);
19
+ if (mode === 'geo') {
20
+ const geo = await auditGeo(root, inventory);
21
+ const result = await writeAuditArtifacts(ctx, mission, inventory, geo.findings, {
22
+ entityFacts: geo.entityFacts,
23
+ claims: geo.claims,
24
+ crawlers: geo.crawlers,
25
+ answerability: geo.answerability,
26
+ });
27
+ return {
28
+ schema: 'sks.search-visibility.audit-command.v1',
29
+ ok: result.gate.ok,
30
+ mission_id: mission.id,
31
+ route: routeForMode(mode),
32
+ status: result.gate.status,
33
+ artifacts_dir: `.sneakoscope/missions/${mission.id}/search-visibility`,
34
+ findings: geo.findings.length,
35
+ gate: result.gate,
36
+ proof: `.sneakoscope/missions/${mission.id}/completion-proof.json`,
37
+ };
38
+ }
39
+ const findings = await auditSeo(root, inventory);
40
+ const result = await writeAuditArtifacts(ctx, mission, inventory, findings, null);
41
+ return {
42
+ schema: 'sks.search-visibility.audit-command.v1',
43
+ ok: result.gate.ok,
44
+ mission_id: mission.id,
45
+ route: routeForMode(mode),
46
+ status: result.gate.status,
47
+ artifacts_dir: `.sneakoscope/missions/${mission.id}/search-visibility`,
48
+ findings: findings.length,
49
+ gate: result.gate,
50
+ proof: `.sneakoscope/missions/${mission.id}/completion-proof.json`,
51
+ };
52
+ }
53
+ export async function runSearchVisibilityPlan(mode, missionRef, options) {
54
+ const mission = await resolveOrAudit(mode, missionRef, options);
55
+ const ctx = context(mode, mission.root, options);
56
+ const inventory = await readJson(path.join(mission.artifactDir, 'site-inventory.json'));
57
+ const findingsArtifact = await readJson(path.join(mission.artifactDir, mode === 'seo' ? 'seo-findings.json' : 'geo-findings.json'), {});
58
+ const findings = Array.isArray(findingsArtifact.findings) ? findingsArtifact.findings : [];
59
+ const entityFacts = mode === 'geo'
60
+ ? await readJson(path.join(mission.artifactDir, 'entity-facts.json'), null)
61
+ : null;
62
+ const plan = await buildMutationPlan(mode, mission.id, mission.artifactDir, inventory, findings, options, entityFacts);
63
+ return {
64
+ schema: 'sks.search-visibility.plan-command.v1',
65
+ ok: plan.status !== 'blocked',
66
+ mission_id: mission.id,
67
+ route: routeForMode(mode),
68
+ status: plan.status,
69
+ operations: plan.operations.length,
70
+ blockers: plan.blockers,
71
+ mutation_plan: `search-visibility/mutation-plan.json`,
72
+ };
73
+ }
74
+ export async function runSearchVisibilityApply(mode, missionRef, options) {
75
+ const mission = await resolveOrAudit(mode, missionRef, options);
76
+ const planPath = path.join(mission.artifactDir, 'mutation-plan.json');
77
+ let plan = await readJson(planPath, null);
78
+ if (!plan) {
79
+ const planned = await runSearchVisibilityPlan(mode, mission.id, options);
80
+ plan = await readJson(planPath, null);
81
+ if (!planned.ok && !plan)
82
+ return { schema: 'sks.search-visibility.apply-command.v1', ok: false, mission_id: mission.id, route: routeForMode(mode), status: 'blocked', blockers: ['mutation_plan_missing'] };
83
+ }
84
+ const applied = await applyMutationPlan(mission.root, mission.id, mission.artifactDir, plan, options);
85
+ const verify = await runSearchVisibilityVerify(mode, mission.id, options);
86
+ return {
87
+ schema: 'sks.search-visibility.apply-command.v1',
88
+ ok: applied.ok && verify.ok,
89
+ mission_id: mission.id,
90
+ route: routeForMode(mode),
91
+ status: applied.status,
92
+ applied: applied.applied,
93
+ blockers: [...applied.blockers, ...(verify.blockers || [])],
94
+ rollback_manifest: 'search-visibility/rollback-manifest.json',
95
+ verification: verify,
96
+ };
97
+ }
98
+ export async function runSearchVisibilityVerify(mode, missionRef, options) {
99
+ const mission = await resolveOrAudit(mode, missionRef, options);
100
+ const root = mission.root;
101
+ const ctx = context(mode, root, options);
102
+ const inventory = await readJson(path.join(mission.artifactDir, 'site-inventory.json'));
103
+ const verification = await verifySearchVisibility(ctx, inventory, mission);
104
+ await writeJsonAtomic(path.join(mission.artifactDir, 'verification-report.json'), verification);
105
+ const findingsArtifact = await readJson(path.join(mission.artifactDir, mode === 'seo' ? 'seo-findings.json' : 'geo-findings.json'), {});
106
+ const findings = Array.isArray(findingsArtifact.findings) ? findingsArtifact.findings : [];
107
+ const gate = await writeGate(ctx, mission, findings, verification, mode === 'geo');
108
+ await finalizeSearchVisibility(ctx, mission, gate, `sks ${mode} verify ${mission.id} --json`);
109
+ return {
110
+ schema: 'sks.search-visibility.verify-command.v1',
111
+ ok: gate.ok,
112
+ mission_id: mission.id,
113
+ route: routeForMode(mode),
114
+ status: gate.status,
115
+ blockers: gate.blockers,
116
+ unverified: gate.unverified,
117
+ gate,
118
+ };
119
+ }
120
+ export async function runSearchVisibilityStatus(mode, missionRef, options) {
121
+ const mission = await resolveSearchVisibilityMission(options.root, missionRef);
122
+ if (!mission)
123
+ return { schema: 'sks.search-visibility.status-command.v1', ok: false, status: 'missing_mission', route: routeForMode(mode) };
124
+ return statusForMission(mission);
125
+ }
126
+ export async function runSearchVisibilityRollback(mode, missionRef, options) {
127
+ const mission = await resolveSearchVisibilityMission(options.root, missionRef);
128
+ if (!mission)
129
+ return { schema: 'sks.search-visibility.rollback-command.v1', ok: false, status: 'missing_mission', route: routeForMode(mode) };
130
+ const result = await rollbackMutationPlan(mission.root, mission.artifactDir, options.apply);
131
+ const verify = result.status === 'rolled_back' ? await runSearchVisibilityVerify(mode, mission.id, options) : null;
132
+ return {
133
+ schema: 'sks.search-visibility.rollback-command.v1',
134
+ ok: result.ok,
135
+ mission_id: mission.id,
136
+ route: routeForMode(mode),
137
+ status: result.status,
138
+ rolled_back: result.rolled_back,
139
+ blockers: result.blockers,
140
+ verification: verify,
141
+ };
142
+ }
143
+ export async function runSearchVisibilityDoctor(mode, options) {
144
+ const root = await projectRoot(options.root);
145
+ const ctx = context(mode, root, options);
146
+ const detected = await detectProject(ctx);
147
+ return {
148
+ schema: 'sks.search-visibility.doctor-command.v1',
149
+ ok: detected.blockers.length === 0,
150
+ route: routeForMode(mode),
151
+ root,
152
+ adapter: detected.adapterId,
153
+ confidence: detected.confidence,
154
+ capabilities: detected.capabilities,
155
+ blockers: detected.blockers,
156
+ status: detected.blockers.length ? 'blocked' : 'verified_partial',
157
+ };
158
+ }
159
+ export async function runSearchVisibilityFixture(mode, options) {
160
+ const root = await projectRoot(options.root);
161
+ const result = await runSearchVisibilityAudit(mode, {
162
+ ...options,
163
+ root,
164
+ target: mode === 'seo' ? 'package' : 'package',
165
+ offline: true,
166
+ strict: true,
167
+ });
168
+ const planned = await runSearchVisibilityPlan(mode, result.mission_id, options);
169
+ return {
170
+ schema: 'sks.search-visibility.fixture-command.v1',
171
+ ok: Boolean(result.ok && planned.ok),
172
+ mission_id: result.mission_id,
173
+ route: routeForMode(mode),
174
+ audit: result,
175
+ plan: planned,
176
+ };
177
+ }
178
+ function context(mode, root, options) {
179
+ return {
180
+ root,
181
+ mode,
182
+ target: options.target,
183
+ framework: options.framework,
184
+ origin: options.url,
185
+ offline: options.offline,
186
+ strict: options.strict,
187
+ };
188
+ }
189
+ async function resolveOrAudit(mode, missionRef, options) {
190
+ const mission = await resolveSearchVisibilityMission(options.root, missionRef);
191
+ if (mission)
192
+ return mission;
193
+ const audit = await runSearchVisibilityAudit(mode, options);
194
+ const created = await resolveSearchVisibilityMission(options.root, audit.mission_id);
195
+ if (!created)
196
+ throw new Error('Search visibility audit did not create a mission');
197
+ return created;
198
+ }
199
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,67 @@
1
+ import path from 'node:path';
2
+ import { createMission, findLatestMission, missionDir, setCurrent } from '../mission.js';
3
+ import { ensureDir, projectRoot, readJson, writeJsonAtomic } from '../fsx.js';
4
+ export const SEARCH_VISIBILITY_DIR = 'search-visibility';
5
+ export async function createSearchVisibilityMission(mode, prompt, options) {
6
+ const root = await projectRoot(options.root);
7
+ const { id, dir } = await createMission(root, { mode, prompt });
8
+ const artifactDir = path.join(dir, SEARCH_VISIBILITY_DIR);
9
+ await ensureDir(artifactDir);
10
+ const route = routeForMode(mode);
11
+ await setCurrent(root, {
12
+ mission_id: id,
13
+ mode: 'SEO_GEO_OPTIMIZER',
14
+ route: 'SEO_GEO_OPTIMIZER',
15
+ route_command: route,
16
+ search_visibility_mode: mode,
17
+ phase: `${mode.toUpperCase()}_PREPARED`,
18
+ implementation_allowed: false,
19
+ });
20
+ await writeJsonAtomic(path.join(artifactDir, 'intake.json'), {
21
+ schema: 'sks.search-visibility.intake.v1',
22
+ generated_at: new Date().toISOString(),
23
+ mission_id: id,
24
+ route,
25
+ root,
26
+ target: options.target,
27
+ url: options.url,
28
+ framework: options.framework,
29
+ authorization: {
30
+ apply: options.apply,
31
+ include_llms_txt: options.includeLlmsTxt,
32
+ allow_dirty_touched: options.allowDirtyTouched,
33
+ scope: options.scope,
34
+ },
35
+ network_used: false,
36
+ browser_used: false,
37
+ status: 'prepared',
38
+ blockers: [],
39
+ unverified: ['external production, browser, Search Console, and analytics outcomes are not verified by default'],
40
+ });
41
+ return { id, root, dir, artifactDir };
42
+ }
43
+ export async function resolveSearchVisibilityMission(rootInput, missionRef) {
44
+ const root = await projectRoot(rootInput);
45
+ const id = !missionRef || missionRef === 'latest' ? await findLatestMission(root) : missionRef;
46
+ if (!id)
47
+ return null;
48
+ const dir = missionDir(root, id);
49
+ const artifactDir = path.join(dir, SEARCH_VISIBILITY_DIR);
50
+ return { id, root, dir, artifactDir };
51
+ }
52
+ export async function readSearchVisibilityState(mission) {
53
+ return readJson(path.join(mission.artifactDir, 'intake.json'), {});
54
+ }
55
+ export function routeForMode(_mode) {
56
+ return '$SEO-GEO-OPTIMIZER';
57
+ }
58
+ export function gateFileForMode(mode) {
59
+ return mode === 'seo' ? 'seo-gate.json' : 'geo-gate.json';
60
+ }
61
+ export function findingsFileForMode(mode) {
62
+ return mode === 'seo' ? 'seo-findings.json' : 'geo-findings.json';
63
+ }
64
+ export function missionRel(missionId, artifact) {
65
+ return `.sneakoscope/missions/${missionId}/${SEARCH_VISIBILITY_DIR}/${artifact}`;
66
+ }
67
+ //# sourceMappingURL=mission.js.map