sneakoscope 4.4.0 → 4.6.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 (80) hide show
  1. package/README.md +28 -2
  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 +1 -0
  7. package/dist/core/agents/agent-runner-ollama.js +2 -0
  8. package/dist/core/agents/native-worker-backend-router.js +3 -0
  9. package/dist/core/bench.js +115 -0
  10. package/dist/core/code-structure.js +399 -11
  11. package/dist/core/codex-control/codex-fake-sdk-adapter.js +67 -9
  12. package/dist/core/codex-control/gpt-final-arbiter.js +4 -1
  13. package/dist/core/codex-control/gpt-final-review-schema.js +58 -0
  14. package/dist/core/codex-native/core-skill-manifest.js +23 -0
  15. package/dist/core/commands/bench-command.js +11 -2
  16. package/dist/core/commands/code-structure-command.js +34 -2
  17. package/dist/core/commands/run-command.js +92 -2
  18. package/dist/core/commands/seo-command.js +130 -0
  19. package/dist/core/feature-fixtures.js +6 -0
  20. package/dist/core/feature-registry.js +3 -1
  21. package/dist/core/fsx.js +1 -1
  22. package/dist/core/hooks-runtime.js +8 -0
  23. package/dist/core/init.js +8 -6
  24. package/dist/core/lean-engineering-policy.js +159 -0
  25. package/dist/core/pipeline-internals/runtime-core.js +15 -5
  26. package/dist/core/proof/auto-finalize.js +3 -2
  27. package/dist/core/proof/proof-schema.js +2 -1
  28. package/dist/core/proof/proof-writer.js +1 -0
  29. package/dist/core/proof/route-adapter.js +4 -2
  30. package/dist/core/proof/route-finalizer.js +35 -3
  31. package/dist/core/routes.js +75 -9
  32. package/dist/core/search-visibility/adapter-registry.js +26 -0
  33. package/dist/core/search-visibility/adapters/next-app.js +6 -0
  34. package/dist/core/search-visibility/adapters/next-pages.js +6 -0
  35. package/dist/core/search-visibility/adapters/static-site.js +6 -0
  36. package/dist/core/search-visibility/analyzers.js +377 -0
  37. package/dist/core/search-visibility/artifacts.js +183 -0
  38. package/dist/core/search-visibility/discovery.js +347 -0
  39. package/dist/core/search-visibility/index.js +199 -0
  40. package/dist/core/search-visibility/mission.js +67 -0
  41. package/dist/core/search-visibility/mutation.js +314 -0
  42. package/dist/core/search-visibility/types.js +2 -0
  43. package/dist/core/search-visibility/verifier.js +60 -0
  44. package/dist/core/version.js +1 -1
  45. package/dist/scripts/check-architecture.js +40 -7
  46. package/dist/scripts/check-command-module-budget.js +43 -5
  47. package/dist/scripts/check-pipeline-budget.js +17 -30
  48. package/dist/scripts/check-publish-tag.js +33 -6
  49. package/dist/scripts/check-route-modularity.js +25 -33
  50. package/dist/scripts/check-runtime-schemas.js +22 -0
  51. package/dist/scripts/config-managed-merge-callsite-coverage-check.js +2 -2
  52. package/dist/scripts/core-skill-immutable-sync-check.js +3 -2
  53. package/dist/scripts/core-skill-integrity-blackbox.js +3 -2
  54. package/dist/scripts/core-skill-manifest-check.js +7 -2
  55. package/dist/scripts/geo-claim-evidence-check.js +18 -0
  56. package/dist/scripts/geo-cli-blackbox-check.js +18 -0
  57. package/dist/scripts/geo-crawler-policy-check.js +16 -0
  58. package/dist/scripts/geo-llms-txt-optional-check.js +19 -0
  59. package/dist/scripts/gpt-final-arbiter-check.js +4 -1
  60. package/dist/scripts/release-parallel-check.js +15 -0
  61. package/dist/scripts/release-registry-check.js +33 -14
  62. package/dist/scripts/search-visibility-gate-lib.js +124 -0
  63. package/dist/scripts/seo-audit-fixture-check.js +16 -0
  64. package/dist/scripts/seo-canonical-locale-check.js +19 -0
  65. package/dist/scripts/seo-cli-blackbox-check.js +18 -0
  66. package/dist/scripts/seo-geo-feature-fixture-quality-check.js +18 -0
  67. package/dist/scripts/seo-geo-geo-disambiguation-check.js +12 -0
  68. package/dist/scripts/seo-geo-no-unsupported-ranking-claims-check.js +18 -0
  69. package/dist/scripts/seo-geo-route-identity-check.js +12 -0
  70. package/dist/scripts/seo-geo-skill-rich-content-check.js +22 -0
  71. package/dist/scripts/seo-mutation-rollback-check.js +23 -0
  72. package/dist/scripts/seo-no-mutation-by-default-check.js +17 -0
  73. package/dist/scripts/seo-structured-data-visible-content-check.js +19 -0
  74. package/dist/scripts/sks-3-1-5-directive-check-lib.js +10 -1
  75. package/package.json +19 -1
  76. package/schemas/search-visibility/finding-ledger.schema.json +36 -0
  77. package/schemas/search-visibility/gate.schema.json +22 -0
  78. package/schemas/search-visibility/mutation-plan.schema.json +27 -0
  79. package/schemas/search-visibility/site-inventory.schema.json +21 -0
  80. package/schemas/search-visibility/verification-report.schema.json +23 -0
@@ -0,0 +1,183 @@
1
+ import path from 'node:path';
2
+ import { maybeFinalizeRoute } from '../proof/auto-finalize.js';
3
+ import { PACKAGE_VERSION, exists, readJson, writeJsonAtomic } from '../fsx.js';
4
+ import { buildAiCrawlerPolicy, buildCanonicalMap, buildInternalLinkGraph, buildLlmsTxtPlan, buildLocaleGraph, buildRobotsPolicy, buildRouteGraph, buildSitemapAudit, buildStructuredDataLedger, } from './analyzers.js';
5
+ import { findingsFileForMode, gateFileForMode, missionRel, routeForMode } from './mission.js';
6
+ import { verifySearchVisibility } from './verifier.js';
7
+ const COMMON_ARTIFACTS = [
8
+ 'intake.json',
9
+ 'adapter-detection.json',
10
+ 'site-inventory.json',
11
+ 'route-graph.json',
12
+ 'robots-policy.json',
13
+ 'structured-data-ledger.json',
14
+ 'verification-report.json',
15
+ ];
16
+ export async function writeAuditArtifacts(ctx, mission, inventory, findings, geo) {
17
+ const route = routeForMode(ctx.mode);
18
+ await writeJsonAtomic(path.join(mission.artifactDir, 'adapter-detection.json'), withMeta(ctx, mission, {
19
+ schema: 'sks.search-visibility.adapter-detection.v1',
20
+ ...inventory.detected_adapter,
21
+ }));
22
+ await writeJsonAtomic(path.join(mission.artifactDir, 'site-inventory.json'), withMeta(ctx, mission, inventory));
23
+ await writeJsonAtomic(path.join(mission.artifactDir, 'route-graph.json'), withMeta(ctx, mission, buildRouteGraph(inventory)));
24
+ await writeJsonAtomic(path.join(mission.artifactDir, findingsFileForMode(ctx.mode)), withMeta(ctx, mission, {
25
+ schema: `sks.search-visibility.${ctx.mode}-findings.v1`,
26
+ findings,
27
+ counts: countFindings(findings),
28
+ }));
29
+ await writeJsonAtomic(path.join(mission.artifactDir, 'canonical-map.json'), withMeta(ctx, mission, buildCanonicalMap(inventory)));
30
+ await writeJsonAtomic(path.join(mission.artifactDir, 'locale-graph.json'), withMeta(ctx, mission, buildLocaleGraph(inventory)));
31
+ await writeJsonAtomic(path.join(mission.artifactDir, 'sitemap-audit.json'), withMeta(ctx, mission, buildSitemapAudit(inventory)));
32
+ await writeJsonAtomic(path.join(mission.artifactDir, 'robots-policy.json'), withMeta(ctx, mission, buildRobotsPolicy(inventory, geo?.crawlers)));
33
+ await writeJsonAtomic(path.join(mission.artifactDir, 'structured-data-ledger.json'), withMeta(ctx, mission, buildStructuredDataLedger(inventory)));
34
+ await writeJsonAtomic(path.join(mission.artifactDir, 'internal-link-graph.json'), withMeta(ctx, mission, buildInternalLinkGraph(inventory)));
35
+ if (geo) {
36
+ await writeJsonAtomic(path.join(mission.artifactDir, 'entity-facts.json'), withMeta(ctx, mission, geo.entityFacts));
37
+ await writeJsonAtomic(path.join(mission.artifactDir, 'claim-evidence-ledger.json'), withMeta(ctx, mission, {
38
+ schema: 'sks.search-visibility.claim-evidence-ledger.v1',
39
+ claims: geo.claims,
40
+ }));
41
+ await writeJsonAtomic(path.join(mission.artifactDir, 'answerability-report.json'), withMeta(ctx, mission, geo.answerability));
42
+ await writeJsonAtomic(path.join(mission.artifactDir, 'ai-crawler-policy.json'), withMeta(ctx, mission, buildAiCrawlerPolicy()));
43
+ await writeJsonAtomic(path.join(mission.artifactDir, 'llms-txt-plan.json'), withMeta(ctx, mission, buildLlmsTxtPlan(inventory, geo.entityFacts)));
44
+ }
45
+ const verification = await verifySearchVisibility(ctx, inventory, mission);
46
+ await writeJsonAtomic(path.join(mission.artifactDir, 'verification-report.json'), withMeta(ctx, mission, verification));
47
+ const gate = await writeGate(ctx, mission, findings, verification, Boolean(geo));
48
+ const proof = await finalizeSearchVisibility(ctx, mission, gate);
49
+ return { verification, gate, proof };
50
+ }
51
+ export async function writeGate(ctx, mission, findings, verification, includeGeoArtifacts) {
52
+ const route = routeForMode(ctx.mode);
53
+ const gateFile = gateFileForMode(ctx.mode);
54
+ const required = requiredArtifacts(ctx.mode, includeGeoArtifacts);
55
+ const requiredStatus = await Promise.all(required.map(async (artifact) => {
56
+ if (artifact === gateFile)
57
+ return { path: gateFile, present: true };
58
+ if (artifact === 'completion-proof.json')
59
+ return { path: artifact, present: true };
60
+ const file = artifact === gateFile || artifact === 'completion-proof.json'
61
+ ? path.join(mission.dir, artifact)
62
+ : path.join(mission.artifactDir, artifact);
63
+ return { path: artifact === 'completion-proof.json' ? artifact : artifact === gateFile ? gateFile : `search-visibility/${artifact}`, present: await exists(file) };
64
+ }));
65
+ const unsupportedClaims = findings
66
+ .filter((finding) => finding.severity === 'critical' && /guarantee|ranking|traffic|citation|unsupported/i.test(finding.summary))
67
+ .map((finding) => finding.id);
68
+ const artifactBlockers = requiredStatus.filter((item) => !item.present && item.path !== 'completion-proof.json').map((item) => `missing:${item.path}`);
69
+ const findingBlockers = findings.filter((finding) => finding.blocking && !/unsupported ranking|Unsupported ranking/i.test(finding.summary)).map((finding) => finding.id);
70
+ const blockers = [...artifactBlockers, ...verification.blockers, ...findingBlockers];
71
+ const unverified = Array.from(new Set([
72
+ ...verification.unverified,
73
+ 'production URL, browser rendering, Search Console, analytics, ranking, traffic, and AI citation outcomes are not claimed without direct evidence',
74
+ ]));
75
+ const ok = blockers.length === 0 && unsupportedClaims.length === 0;
76
+ const gate = {
77
+ schema: 'sks.search-visibility.gate.v1',
78
+ generated_at: new Date().toISOString(),
79
+ mission_id: mission.id,
80
+ route,
81
+ ok,
82
+ passed: ok,
83
+ status: ok ? 'verified_partial' : 'blocked',
84
+ command_identity: route === '$SEO-GEO-OPTIMIZER',
85
+ required_artifacts: requiredStatus,
86
+ unsupported_claims: unsupportedClaims,
87
+ blockers,
88
+ unverified,
89
+ completion_proof: `.sneakoscope/missions/${mission.id}/completion-proof.json`,
90
+ };
91
+ await writeJsonAtomic(path.join(mission.dir, gateFile), gate);
92
+ return gate;
93
+ }
94
+ export async function finalizeSearchVisibility(ctx, mission, gate, command = `sks ${ctx.mode} audit --json`) {
95
+ const route = routeForMode(ctx.mode);
96
+ const artifacts = [
97
+ ...requiredArtifacts(ctx.mode, ctx.mode === 'geo').map((artifact) => artifact === gateFileForMode(ctx.mode) ? artifact : `search-visibility/${artifact}`),
98
+ gateFileForMode(ctx.mode),
99
+ 'completion-proof.json',
100
+ ];
101
+ return maybeFinalizeRoute(mission.root, {
102
+ missionId: mission.id,
103
+ route,
104
+ gateFile: gateFileForMode(ctx.mode),
105
+ gate,
106
+ artifacts,
107
+ statusHint: gate.ok ? 'verified_partial' : 'blocked',
108
+ blockers: gate.blockers,
109
+ unverified: gate.unverified,
110
+ command: { cmd: command, status: gate.ok ? 0 : 1 },
111
+ agents: false,
112
+ lightweightEvidence: true,
113
+ });
114
+ }
115
+ export function requiredArtifacts(mode, includeGeoArtifacts = mode === 'geo') {
116
+ const seo = [
117
+ ...COMMON_ARTIFACTS,
118
+ 'seo-findings.json',
119
+ 'canonical-map.json',
120
+ 'locale-graph.json',
121
+ 'sitemap-audit.json',
122
+ 'internal-link-graph.json',
123
+ 'seo-gate.json',
124
+ 'completion-proof.json',
125
+ ];
126
+ if (mode === 'seo')
127
+ return seo;
128
+ return [
129
+ ...COMMON_ARTIFACTS,
130
+ 'geo-findings.json',
131
+ 'canonical-map.json',
132
+ 'locale-graph.json',
133
+ 'sitemap-audit.json',
134
+ 'internal-link-graph.json',
135
+ ...(includeGeoArtifacts ? ['entity-facts.json', 'claim-evidence-ledger.json', 'answerability-report.json', 'ai-crawler-policy.json', 'llms-txt-plan.json'] : []),
136
+ 'geo-gate.json',
137
+ 'completion-proof.json',
138
+ ];
139
+ }
140
+ export async function statusForMission(mission) {
141
+ const seoGate = await readJson(path.join(mission.dir, 'seo-gate.json'), null);
142
+ const geoGate = await readJson(path.join(mission.dir, 'geo-gate.json'), null);
143
+ const verification = await readJson(path.join(mission.artifactDir, 'verification-report.json'), null);
144
+ const plan = await readJson(path.join(mission.artifactDir, 'mutation-plan.json'), null);
145
+ const proof = await readJson(path.join(mission.dir, 'completion-proof.json'), null);
146
+ return {
147
+ schema: 'sks.search-visibility.status.v1',
148
+ ok: Boolean(seoGate?.ok || geoGate?.ok || verification),
149
+ mission_id: mission.id,
150
+ artifacts_dir: missionRel(mission.id, ''),
151
+ gate: seoGate || geoGate,
152
+ verification,
153
+ mutation_plan: plan,
154
+ completion_proof: proof ? `.sneakoscope/missions/${mission.id}/completion-proof.json` : null,
155
+ };
156
+ }
157
+ function withMeta(ctx, mission, data) {
158
+ const value = data && typeof data === 'object' && !Array.isArray(data) ? data : { value: data };
159
+ return {
160
+ ...value,
161
+ generated_at: value.generated_at || new Date().toISOString(),
162
+ package_version: PACKAGE_VERSION,
163
+ mission_id: mission.id,
164
+ route: routeForMode(ctx.mode),
165
+ root: mission.root,
166
+ target: ctx.target,
167
+ source_commit: null,
168
+ input_hashes: {},
169
+ tool_versions: { sneakoscope: PACKAGE_VERSION },
170
+ network_used: Boolean(ctx.origin && !ctx.offline),
171
+ browser_used: false,
172
+ status: value.status || 'verified_partial',
173
+ blockers: Array.isArray(value.blockers) ? value.blockers : [],
174
+ unverified: Array.isArray(value.unverified) ? value.unverified : [],
175
+ };
176
+ }
177
+ function countFindings(findings) {
178
+ const counts = {};
179
+ for (const finding of findings)
180
+ counts[finding.severity] = (counts[finding.severity] || 0) + 1;
181
+ return counts;
182
+ }
183
+ //# sourceMappingURL=artifacts.js.map
@@ -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