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.
- package/README.md +35 -8
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/bin/sks.js +1 -1
- package/dist/cli/command-registry.js +3 -1
- package/dist/cli/ultra-search-command.js +163 -0
- package/dist/cli/xai-command.js +28 -168
- package/dist/core/agents/agent-codex-cockpit.js +3 -3
- package/dist/core/agents/agent-runner-ollama.js +2 -0
- package/dist/core/agents/agent-wrongness.js +1 -1
- package/dist/core/agents/native-worker-backend-router.js +3 -0
- package/dist/core/bench.js +115 -0
- package/dist/core/code-structure.js +399 -11
- package/dist/core/codex-control/codex-app-server-v2-client.js +86 -2
- package/dist/core/codex-control/codex-fake-sdk-adapter.js +67 -9
- package/dist/core/codex-control/codex-reliability-shield.js +26 -5
- package/dist/core/codex-control/codex-task-runner.js +7 -1
- package/dist/core/codex-control/gpt-final-arbiter.js +4 -1
- package/dist/core/codex-control/gpt-final-review-schema.js +58 -0
- package/dist/core/codex-control/model-call-concurrency.js +1 -1
- package/dist/core/codex-native/core-skill-manifest.js +23 -0
- package/dist/core/commands/bench-command.js +11 -2
- package/dist/core/commands/code-structure-command.js +34 -2
- package/dist/core/commands/qa-loop-command.js +23 -7
- package/dist/core/commands/run-command.js +92 -2
- package/dist/core/commands/seo-command.js +130 -0
- package/dist/core/feature-fixtures.js +6 -0
- package/dist/core/feature-registry.js +3 -1
- package/dist/core/fsx.js +1 -1
- package/dist/core/hooks-runtime.js +9 -1
- package/dist/core/init.js +8 -6
- package/dist/core/lean-engineering-policy.js +159 -0
- package/dist/core/pipeline-internals/runtime-core.js +15 -5
- package/dist/core/proof/auto-finalize.js +3 -2
- package/dist/core/proof/proof-schema.js +2 -1
- package/dist/core/proof/proof-writer.js +1 -0
- package/dist/core/proof/route-adapter.js +4 -2
- package/dist/core/proof/route-finalizer.js +35 -3
- package/dist/core/qa-loop/qa-app-server-driver.js +134 -0
- package/dist/core/qa-loop/qa-contract-v2.js +231 -0
- package/dist/core/qa-loop/qa-gate-v2.js +132 -0
- package/dist/core/qa-loop/qa-runtime-artifacts.js +53 -0
- package/dist/core/qa-loop/qa-surface-router.js +114 -0
- package/dist/core/qa-loop/qa-types.js +18 -0
- package/dist/core/qa-loop.js +83 -26
- package/dist/core/release/gate-manifest.js +1 -0
- package/dist/core/release/sla-scheduler.js +1 -1
- package/dist/core/release-parallel-full-coverage.js +1 -1
- package/dist/core/routes.js +96 -14
- package/dist/core/search-visibility/adapter-registry.js +26 -0
- package/dist/core/search-visibility/adapters/next-app.js +6 -0
- package/dist/core/search-visibility/adapters/next-pages.js +6 -0
- package/dist/core/search-visibility/adapters/static-site.js +6 -0
- package/dist/core/search-visibility/analyzers.js +377 -0
- package/dist/core/search-visibility/artifacts.js +183 -0
- package/dist/core/search-visibility/discovery.js +347 -0
- package/dist/core/search-visibility/index.js +199 -0
- package/dist/core/search-visibility/mission.js +67 -0
- package/dist/core/search-visibility/mutation.js +314 -0
- package/dist/core/search-visibility/types.js +2 -0
- package/dist/core/search-visibility/verifier.js +60 -0
- package/dist/core/source-intelligence/source-intelligence-policy.js +45 -26
- package/dist/core/source-intelligence/source-intelligence-proof.js +10 -16
- package/dist/core/source-intelligence/source-intelligence-runner.js +56 -42
- package/dist/core/triwiki/triwiki-affected-graph.js +3 -2
- package/dist/core/trust-kernel/trust-report.js +3 -5
- package/dist/core/ultra-search/index.js +3 -0
- package/dist/core/ultra-search/runtime.js +502 -0
- package/dist/core/ultra-search/types.js +3 -0
- package/dist/core/version.js +1 -1
- package/dist/scripts/agent-visual-consistency-check.js +1 -1
- package/dist/scripts/check-architecture.js +40 -7
- package/dist/scripts/check-command-module-budget.js +43 -5
- package/dist/scripts/check-pipeline-budget.js +17 -30
- package/dist/scripts/check-publish-tag.js +33 -6
- package/dist/scripts/check-route-modularity.js +25 -33
- package/dist/scripts/check-runtime-schemas.js +22 -0
- package/dist/scripts/codex-control-all-pipelines-check.js +1 -0
- package/dist/scripts/codex-control-model-capacity-fallback-check.js +53 -0
- package/dist/scripts/config-managed-merge-callsite-coverage-check.js +7 -1
- package/dist/scripts/core-skill-immutable-sync-check.js +3 -2
- package/dist/scripts/core-skill-integrity-blackbox.js +3 -2
- package/dist/scripts/core-skill-manifest-check.js +7 -2
- package/dist/scripts/geo-claim-evidence-check.js +18 -0
- package/dist/scripts/geo-cli-blackbox-check.js +18 -0
- package/dist/scripts/geo-crawler-policy-check.js +16 -0
- package/dist/scripts/geo-llms-txt-optional-check.js +19 -0
- package/dist/scripts/gpt-final-arbiter-check.js +4 -1
- package/dist/scripts/loop-directive-check-lib.js +78 -1
- package/dist/scripts/qa-loop-app-server-driver-check.js +74 -0
- package/dist/scripts/qa-loop-surface-router-check.js +49 -0
- package/dist/scripts/release-check-dynamic-execute.js +1 -1
- package/dist/scripts/release-metadata-1-19-check.js +2 -2
- package/dist/scripts/release-parallel-check.js +17 -2
- package/dist/scripts/release-parallel-full-coverage-check.js +1 -1
- package/dist/scripts/release-readiness-report.js +6 -6
- package/dist/scripts/release-registry-check.js +33 -14
- package/dist/scripts/runtime-ts-rust-boundary-check.js +1 -1
- package/dist/scripts/search-visibility-gate-lib.js +124 -0
- package/dist/scripts/seo-audit-fixture-check.js +16 -0
- package/dist/scripts/seo-canonical-locale-check.js +19 -0
- package/dist/scripts/seo-cli-blackbox-check.js +18 -0
- package/dist/scripts/seo-geo-feature-fixture-quality-check.js +18 -0
- package/dist/scripts/seo-geo-geo-disambiguation-check.js +12 -0
- package/dist/scripts/seo-geo-no-unsupported-ranking-claims-check.js +18 -0
- package/dist/scripts/seo-geo-route-identity-check.js +12 -0
- package/dist/scripts/seo-geo-skill-rich-content-check.js +22 -0
- package/dist/scripts/seo-mutation-rollback-check.js +23 -0
- package/dist/scripts/seo-no-mutation-by-default-check.js +17 -0
- package/dist/scripts/seo-structured-data-visible-content-check.js +19 -0
- package/dist/scripts/sks-1-18-gate-lib.js +2 -2
- package/dist/scripts/sks-3-1-5-directive-check-lib.js +10 -1
- package/dist/scripts/source-intelligence-all-modes-check.js +9 -19
- package/dist/scripts/source-intelligence-policy-check.js +6 -6
- package/dist/scripts/triwiki-affected-graph-check.js +2 -2
- package/dist/scripts/ultra-search-provider-interface-check.js +27 -0
- package/package.json +26 -5
- package/schemas/search-visibility/finding-ledger.schema.json +36 -0
- package/schemas/search-visibility/gate.schema.json +22 -0
- package/schemas/search-visibility/mutation-plan.schema.json +27 -0
- package/schemas/search-visibility/site-inventory.schema.json +21 -0
- package/schemas/search-visibility/verification-report.schema.json +23 -0
- package/dist/core/mcp/xai-mcp-detector.js +0 -157
- package/dist/core/mcp/xai-search-adapter.js +0 -100
- package/dist/scripts/xai-mcp-capability-check.js +0 -14
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readText, sha256 } from '../fsx.js';
|
|
3
|
+
import { officialEvidence, sourceEvidence } from './discovery.js';
|
|
4
|
+
export const GOOGLE_AI_FEATURES_URL = 'https://developers.google.com/search/docs/appearance/ai-features';
|
|
5
|
+
export const GOOGLE_AI_OPTIMIZATION_URL = 'https://developers.google.com/search/docs/fundamentals/ai-optimization-guide';
|
|
6
|
+
export const GOOGLE_STRUCTURED_DATA_URL = 'https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data';
|
|
7
|
+
export const GOOGLE_STRUCTURED_DATA_POLICIES_URL = 'https://developers.google.com/search/docs/appearance/structured-data/sd-policies';
|
|
8
|
+
export const GOOGLE_SITEMAP_URL = 'https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview';
|
|
9
|
+
export const GOOGLE_HREFLANG_URL = 'https://developers.google.com/search/docs/specialty/international/localized-versions';
|
|
10
|
+
export const GOOGLE_CANONICAL_URL = 'https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls';
|
|
11
|
+
export const GOOGLE_ROBOTS_URL = 'https://developers.google.com/search/docs/crawling-indexing/robots/intro';
|
|
12
|
+
export const GOOGLE_SPAM_POLICIES_URL = 'https://developers.google.com/search/docs/essentials/spam-policies';
|
|
13
|
+
export const OPENAI_BOTS_URL = 'https://developers.openai.com/api/docs/bots';
|
|
14
|
+
export const ANTHROPIC_CRAWLERS_URL = 'https://support.claude.com/en/articles/8896518-does-anthropic-crawl-data-from-the-web-and-how-can-site-owners-block-the-crawler';
|
|
15
|
+
export const LLMS_TXT_URL = 'https://llmstxt.org/';
|
|
16
|
+
export async function auditSeo(root, inventory) {
|
|
17
|
+
const findings = [];
|
|
18
|
+
const evidenceBase = packageOrInventoryEvidence(inventory);
|
|
19
|
+
if (inventory.target === 'package' || inventory.package.path) {
|
|
20
|
+
if (!inventory.package.description)
|
|
21
|
+
findings.push(finding('seo-package-description', 'metadata', 'medium', 'Package description is missing.', evidenceBase, ['package.json'], 'Add a concise factual package description.', true));
|
|
22
|
+
if (!inventory.package.keywords.length)
|
|
23
|
+
findings.push(finding('seo-package-keywords', 'metadata', 'low', 'Package keywords are missing.', evidenceBase, ['package.json'], 'Add source-backed npm keywords without stuffing.', true));
|
|
24
|
+
if (!inventory.package.repository)
|
|
25
|
+
findings.push(finding('seo-package-repository', 'metadata', 'medium', 'Repository metadata is missing.', evidenceBase, ['package.json'], 'Set package.json repository to the public project URL.', true));
|
|
26
|
+
if (!inventory.readme.h1)
|
|
27
|
+
findings.push(finding('seo-readme-h1', 'package-docs', 'medium', 'README H1 is missing.', evidenceBase, [inventory.readme.path || 'README.md'], 'Add a clear H1 matching the package/entity name.', false));
|
|
28
|
+
if (!inventory.readme.command_mentions.length)
|
|
29
|
+
findings.push(finding('seo-readme-quickstart', 'package-docs', 'low', 'README quickstart command surface is hard to detect.', evidenceBase, [inventory.readme.path || 'README.md'], 'Include exact install and command spellings.', false));
|
|
30
|
+
}
|
|
31
|
+
const robots = inventory.policy_files.filter((file) => file.kind === 'robots' && file.exists);
|
|
32
|
+
const sitemap = inventory.policy_files.filter((file) => file.kind === 'sitemap' && file.exists);
|
|
33
|
+
if (inventory.target !== 'package') {
|
|
34
|
+
if (!robots.length)
|
|
35
|
+
findings.push(finding('seo-robots-missing', 'indexability', 'low', 'robots.txt was not found; this is not a security issue, but crawl policy is undocumented.', [officialEvidence(GOOGLE_ROBOTS_URL, 'robots.txt is crawl management, not a private-content protection mechanism')], ['robots.txt'], 'Create a managed robots.txt only when project ownership is clear.', true, 'robotsMutation'));
|
|
36
|
+
if (!sitemap.length)
|
|
37
|
+
findings.push(finding('seo-sitemap-missing', 'sitemap', 'medium', 'Sitemap was not found; sitemap discovery signals are absent.', [officialEvidence(GOOGLE_SITEMAP_URL, 'Sitemaps help discovery but do not guarantee indexing')], ['sitemap.xml'], 'Create a sitemap from confirmed canonical/indexable routes.', true, 'sitemapMutation'));
|
|
38
|
+
}
|
|
39
|
+
for (const html of inventory.html_files) {
|
|
40
|
+
const evidence = [sourceEvidence(html.path, 'HTML file inspected')];
|
|
41
|
+
if (!html.title)
|
|
42
|
+
findings.push(finding(`seo-title-missing-${slug(html.path)}`, 'metadata', 'medium', `HTML page ${html.path} is missing a title.`, evidence, [html.path], 'Add a factual title aligned with visible page intent.', true, 'metadataMutation'));
|
|
43
|
+
if (!html.description)
|
|
44
|
+
findings.push(finding(`seo-description-missing-${slug(html.path)}`, 'metadata', 'low', `HTML page ${html.path} is missing a meta description.`, evidence, [html.path], 'Add a concise factual meta description.', true, 'metadataMutation'));
|
|
45
|
+
if (!html.canonical && inventory.origin)
|
|
46
|
+
findings.push(finding(`seo-canonical-missing-${slug(html.path)}`, 'canonical', 'medium', `HTML page ${html.path} has no canonical URL.`, [sourceEvidence(html.path, 'No canonical link found'), officialEvidence(GOOGLE_CANONICAL_URL, 'Canonical links are signals for preferred duplicate URLs')], [html.path], 'Add an absolute canonical URL only from a verified canonical host.', true, 'metadataMutation'));
|
|
47
|
+
if (html.canonical && !/^https?:\/\//i.test(html.canonical))
|
|
48
|
+
findings.push(finding(`seo-canonical-relative-${slug(html.path)}`, 'canonical', 'medium', `HTML page ${html.path} uses a non-absolute canonical URL.`, [sourceEvidence(html.path, `canonical=${html.canonical}`)], [html.path], 'Prefer absolute canonical URLs derived from the verified origin.', true, 'metadataMutation'));
|
|
49
|
+
if (html.jsonLdParseErrors.length)
|
|
50
|
+
findings.push(finding(`seo-jsonld-parse-${slug(html.path)}`, 'structured-data', 'high', `HTML page ${html.path} has invalid JSON-LD.`, evidence, [html.path], 'Fix JSON syntax before rich-result eligibility claims.', false, 'structuredDataMutation'));
|
|
51
|
+
if (html.jsonLdCount && !html.visibleTextSample)
|
|
52
|
+
findings.push(finding(`seo-jsonld-visible-parity-${slug(html.path)}`, 'structured-data', 'high', `HTML page ${html.path} has JSON-LD but little visible text evidence.`, [sourceEvidence(html.path, 'Structured data must describe visible/source-backed content'), officialEvidence(GOOGLE_STRUCTURED_DATA_POLICIES_URL, 'Structured data quality guidelines require truthful page content')], [html.path], 'Verify visible-content parity before adding or relying on structured data.', false));
|
|
53
|
+
for (const href of html.links) {
|
|
54
|
+
if (href.startsWith('/') && href !== '/' && href.includes('//'))
|
|
55
|
+
findings.push(finding(`seo-link-variant-${slug(html.path)}-${slug(href)}`, 'internal-links', 'low', `Internal link ${href} may contain a malformed duplicate slash variant.`, evidence, [html.path], 'Normalize internal links to canonical route variants.', true));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (inventory.locale_candidates.length > 1 && !inventory.html_files.some((html) => /alternate/i.test(html.visibleTextSample) || html.links.some((href) => /hreflang/i.test(href)))) {
|
|
59
|
+
findings.push(finding('seo-locale-hreflang-unverified', 'locale', 'medium', 'Multiple locale candidates exist, but hreflang reciprocity is not verified from source inventory.', [officialEvidence(GOOGLE_HREFLANG_URL, 'Localized versions should provide self/reciprocal alternate signals where applicable')], inventory.locale_candidates.map((candidate) => candidate.source), 'Verify self, reciprocal, and x-default locale graph before mutating locale metadata.', false, 'localeMutation'));
|
|
60
|
+
}
|
|
61
|
+
const unsupportedClaims = unsupportedRankingClaims(inventory);
|
|
62
|
+
for (const claim of unsupportedClaims) {
|
|
63
|
+
findings.push(finding(`seo-unsupported-claim-${slug(claim.path)}`, 'truthfulness', 'critical', `Unsupported ranking/traffic guarantee found in ${claim.path}.`, [sourceEvidence(claim.path, claim.text)], [claim.path], 'Remove ranking, traffic, citation, or indexing guarantees unless backed by measured evidence.', false));
|
|
64
|
+
}
|
|
65
|
+
return findings;
|
|
66
|
+
}
|
|
67
|
+
export async function auditGeo(root, inventory) {
|
|
68
|
+
const entityFacts = await buildEntityFacts(root, inventory);
|
|
69
|
+
const claims = await buildClaimEvidence(root, inventory, entityFacts);
|
|
70
|
+
const crawlers = buildCrawlerPolicyRegistry();
|
|
71
|
+
const findings = [];
|
|
72
|
+
const baseEvidence = packageOrInventoryEvidence(inventory);
|
|
73
|
+
if (!entityFacts.canonical_name)
|
|
74
|
+
findings.push(finding('geo-entity-name-missing', 'entity-facts', 'high', 'Canonical entity name could not be established from source evidence.', baseEvidence, ['package.json', 'README.md'], 'Add or align source-backed entity naming before GEO publishing claims.', false));
|
|
75
|
+
if (!entityFacts.facts.length)
|
|
76
|
+
findings.push(finding('geo-entity-facts-empty', 'entity-facts', 'high', 'No source-backed entity facts were found.', baseEvidence, ['package.json', 'README.md'], 'Record official entity facts with visible source locations.', false));
|
|
77
|
+
if (entityFacts.conflicts.length)
|
|
78
|
+
findings.push(finding('geo-entity-fact-conflict', 'entity-facts', 'high', 'Entity facts conflict across package/docs/source evidence.', baseEvidence, entityFacts.conflicts.flatMap((conflict) => conflict.sources), 'Resolve conflicting names, URLs, or claims before generating structured or llms.txt outputs.', false));
|
|
79
|
+
const unsafeClaims = claims.filter((claim) => !claim.safe_to_publish);
|
|
80
|
+
for (const claim of unsafeClaims) {
|
|
81
|
+
findings.push(finding(`geo-unsafe-claim-${slug(claim.id)}`, 'claim-evidence', 'critical', `Claim is not safe to publish: ${claim.claim}`, [sourceEvidence(claim.supporting_source, claim.claim, claim.source_hash)], [claim.supporting_source], 'Do not publish commercial, ranking, pricing, review, or availability claims without source evidence and visible parity.', false));
|
|
82
|
+
}
|
|
83
|
+
if (!inventory.html_files.length && !inventory.readme.path)
|
|
84
|
+
findings.push(finding('geo-answerability-no-visible-source', 'answerability', 'medium', 'No visible README or HTML answer surface was found.', baseEvidence, ['README.md', 'index.html'], 'Add human-visible source content before AI answerability claims.', false));
|
|
85
|
+
const llms = inventory.policy_files.find((file) => file.kind === 'llms' && file.exists);
|
|
86
|
+
if (!llms) {
|
|
87
|
+
findings.push(finding('geo-llms-txt-optional-missing', 'llms-txt', 'info', 'llms.txt is absent; this is not a blocker because llms.txt is optional and experimental.', [officialEvidence(LLMS_TXT_URL, 'llms.txt is a proposal for optional inference-time guidance'), officialEvidence(GOOGLE_AI_OPTIMIZATION_URL, 'Google generative AI search does not require special AI schema or files')], ['llms.txt'], 'Only plan llms.txt when explicitly requested with --include-llms-txt --apply.', true));
|
|
88
|
+
}
|
|
89
|
+
else if (!llms.managed) {
|
|
90
|
+
findings.push(finding('geo-llms-txt-user-authored', 'llms-txt', 'medium', 'Existing llms.txt has no SKS managed marker; full overwrite is blocked.', [sourceEvidence(llms.path, 'Existing llms.txt is user-authored or unmanaged', llms.hash)], [llms.path], 'Preserve user-authored content and use managed merge only when ownership is clear.', false));
|
|
91
|
+
}
|
|
92
|
+
const answerability = buildAnswerabilityReport(inventory, entityFacts, claims);
|
|
93
|
+
return { findings, entityFacts, claims, crawlers, answerability };
|
|
94
|
+
}
|
|
95
|
+
export function buildRouteGraph(inventory) {
|
|
96
|
+
return {
|
|
97
|
+
schema: 'sks.search-visibility.route-graph.v1',
|
|
98
|
+
routes: inventory.routes,
|
|
99
|
+
canonical_edges: inventory.html_files.filter((html) => html.canonical).map((html) => ({ from: routeFromHtmlPath(html.path), to: String(html.canonical), source: html.path })),
|
|
100
|
+
internal_links: inventory.html_files.flatMap((html) => html.links.filter((href) => href.startsWith('/')).map((href) => ({ from: routeFromHtmlPath(html.path), to: href, source: html.path }))),
|
|
101
|
+
redirects: [],
|
|
102
|
+
generated_at: new Date().toISOString(),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export function buildCanonicalMap(inventory) {
|
|
106
|
+
return {
|
|
107
|
+
schema: 'sks.search-visibility.canonical-map.v1',
|
|
108
|
+
generated_at: new Date().toISOString(),
|
|
109
|
+
origin: inventory.origin,
|
|
110
|
+
groups: inventory.html_files.map((html) => ({
|
|
111
|
+
source: html.path,
|
|
112
|
+
route: routeFromHtmlPath(html.path),
|
|
113
|
+
canonical: html.canonical,
|
|
114
|
+
confidence: html.canonical ? 0.9 : 0.3,
|
|
115
|
+
note: html.canonical ? 'source canonical observed' : 'canonical missing or not verified',
|
|
116
|
+
})),
|
|
117
|
+
warning: 'Canonical signals express preference; final search-engine canonical selection is not guaranteed.',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export function buildLocaleGraph(inventory) {
|
|
121
|
+
return {
|
|
122
|
+
schema: 'sks.search-visibility.locale-graph.v1',
|
|
123
|
+
generated_at: new Date().toISOString(),
|
|
124
|
+
locales: inventory.locale_candidates,
|
|
125
|
+
checks: {
|
|
126
|
+
self_hreflang_verified: false,
|
|
127
|
+
reciprocal_hreflang_verified: false,
|
|
128
|
+
x_default_verified: false,
|
|
129
|
+
localized_sitemap_rows_verified: false,
|
|
130
|
+
},
|
|
131
|
+
unverified: inventory.locale_candidates.length ? ['hreflang reciprocity requires framework/source-specific verification'] : [],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export function buildSitemapAudit(inventory) {
|
|
135
|
+
const files = inventory.policy_files.filter((file) => file.kind === 'sitemap' && file.exists);
|
|
136
|
+
return {
|
|
137
|
+
schema: 'sks.search-visibility.sitemap-audit.v1',
|
|
138
|
+
generated_at: new Date().toISOString(),
|
|
139
|
+
sitemap_files: files,
|
|
140
|
+
route_count: inventory.routes.length,
|
|
141
|
+
status: files.length ? 'present_unverified_rows' : 'missing',
|
|
142
|
+
indexing_guarantee: false,
|
|
143
|
+
note: 'Sitemaps are discovery signals and do not guarantee indexing.',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
export function buildRobotsPolicy(inventory, crawlers = buildCrawlerPolicyRegistry()) {
|
|
147
|
+
return {
|
|
148
|
+
schema: 'sks.search-visibility.robots-policy.v1',
|
|
149
|
+
generated_at: new Date().toISOString(),
|
|
150
|
+
robots_files: inventory.policy_files.filter((file) => file.kind === 'robots'),
|
|
151
|
+
crawler_policy_sources: crawlers,
|
|
152
|
+
security_note: 'robots.txt is crawl management, not authentication or private-content protection.',
|
|
153
|
+
mutations_blocked_when_unmanaged: true,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export function buildStructuredDataLedger(inventory) {
|
|
157
|
+
return {
|
|
158
|
+
schema: 'sks.search-visibility.structured-data-ledger.v1',
|
|
159
|
+
generated_at: new Date().toISOString(),
|
|
160
|
+
pages: inventory.html_files.map((html) => ({
|
|
161
|
+
path: html.path,
|
|
162
|
+
json_ld_count: html.jsonLdCount,
|
|
163
|
+
parse_errors: html.jsonLdParseErrors,
|
|
164
|
+
visible_text_sample_present: Boolean(html.visibleTextSample),
|
|
165
|
+
visible_content_parity: html.jsonLdCount ? (html.visibleTextSample ? 'needs_field_level_review' : 'blocked') : 'not_applicable',
|
|
166
|
+
})),
|
|
167
|
+
policies: [GOOGLE_STRUCTURED_DATA_URL, GOOGLE_STRUCTURED_DATA_POLICIES_URL],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
export function buildInternalLinkGraph(inventory) {
|
|
171
|
+
return {
|
|
172
|
+
schema: 'sks.search-visibility.internal-link-graph.v1',
|
|
173
|
+
generated_at: new Date().toISOString(),
|
|
174
|
+
links: inventory.html_files.flatMap((html) => html.links.map((href) => ({ source: html.path, href, internal: href.startsWith('/') }))),
|
|
175
|
+
orphan_routes: inventory.routes.filter((route) => !inventory.html_files.some((html) => html.links.includes(route.path))).map((route) => route.path),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export function buildAiCrawlerPolicy() {
|
|
179
|
+
return {
|
|
180
|
+
schema: 'sks.search-visibility.ai-crawler-policy.v1',
|
|
181
|
+
generated_at: new Date().toISOString(),
|
|
182
|
+
entries: buildCrawlerPolicyRegistry(),
|
|
183
|
+
policy: {
|
|
184
|
+
single_allow_ai_toggle: false,
|
|
185
|
+
training_auto_allow: false,
|
|
186
|
+
purpose_split_required: true,
|
|
187
|
+
stale_registry_blocks_mutation: true,
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
export function buildLlmsTxtPlan(inventory, facts) {
|
|
192
|
+
const existing = inventory.policy_files.find((file) => file.kind === 'llms' && file.exists);
|
|
193
|
+
return {
|
|
194
|
+
schema: 'sks.search-visibility.llms-txt-plan.v1',
|
|
195
|
+
generated_at: new Date().toISOString(),
|
|
196
|
+
status: existing && !existing.managed ? 'blocked_user_authored_file' : 'optional_candidate',
|
|
197
|
+
experimental_assistive_surface: true,
|
|
198
|
+
required_for_gate: false,
|
|
199
|
+
source: LLMS_TXT_URL,
|
|
200
|
+
candidate_facts: facts.facts,
|
|
201
|
+
privacy_check: {
|
|
202
|
+
private_urls_included: false,
|
|
203
|
+
credentials_included: false,
|
|
204
|
+
},
|
|
205
|
+
blockers: existing && !existing.managed ? ['existing_llms_txt_without_managed_marker'] : [],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
async function buildEntityFacts(root, inventory) {
|
|
209
|
+
const now = new Date().toISOString();
|
|
210
|
+
const facts = [];
|
|
211
|
+
if (inventory.package.name)
|
|
212
|
+
facts.push(entityFact('official_package_name', inventory.package.name, 'package.json#name', [inventory.package.path || 'package.json'], now));
|
|
213
|
+
if (inventory.package.description)
|
|
214
|
+
facts.push(entityFact('official_description', inventory.package.description, 'package.json#description', [inventory.package.path || 'package.json'], now));
|
|
215
|
+
if (inventory.package.repository)
|
|
216
|
+
facts.push(entityFact('official_repository', inventory.package.repository, 'package.json#repository', [inventory.package.path || 'package.json'], now));
|
|
217
|
+
if (inventory.package.homepage)
|
|
218
|
+
facts.push(entityFact('official_homepage', inventory.package.homepage, 'package.json#homepage', [inventory.package.path || 'package.json'], now));
|
|
219
|
+
if (inventory.readme.h1)
|
|
220
|
+
facts.push(entityFact('readme_heading', inventory.readme.h1, `${inventory.readme.path || 'README.md'}#h1`, [inventory.readme.path || 'README.md'], now));
|
|
221
|
+
const conflicts = detectFactConflicts(facts);
|
|
222
|
+
const canonicalName = inventory.readme.h1 || inventory.package.name;
|
|
223
|
+
const canonicalUrl = inventory.package.homepage || inventory.package.repository || inventory.origin;
|
|
224
|
+
return {
|
|
225
|
+
schema: 'sks.search-visibility.entity-facts.v1',
|
|
226
|
+
entity_id: canonicalName ? `entity:${slug(canonicalName)}` : 'entity:unknown',
|
|
227
|
+
type: inventory.package.bin.length ? 'SoftwareApplication' : 'Unknown',
|
|
228
|
+
canonical_name: canonicalName,
|
|
229
|
+
canonical_url: canonicalUrl,
|
|
230
|
+
facts,
|
|
231
|
+
conflicts,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async function buildClaimEvidence(root, inventory, facts) {
|
|
235
|
+
const claims = facts.facts.map((fact, index) => ({
|
|
236
|
+
id: `claim-${String(index + 1).padStart(3, '0')}`,
|
|
237
|
+
claim: `${fact.key}: ${fact.value}`,
|
|
238
|
+
claim_type: fact.key.includes('repository') || fact.key.includes('homepage') ? 'supporting_source' : 'identity',
|
|
239
|
+
supporting_source: fact.source,
|
|
240
|
+
source_hash: null,
|
|
241
|
+
visible_location: fact.visible_on[0] || null,
|
|
242
|
+
confidence: fact.confidence,
|
|
243
|
+
freshness: fact.freshness,
|
|
244
|
+
contradiction: null,
|
|
245
|
+
safe_to_publish: true,
|
|
246
|
+
verification_level: 'locally_verified',
|
|
247
|
+
}));
|
|
248
|
+
const unsupported = unsupportedRankingClaims(inventory);
|
|
249
|
+
for (const item of unsupported) {
|
|
250
|
+
const full = path.join(root, item.path);
|
|
251
|
+
const text = await readText(full, '');
|
|
252
|
+
claims.push({
|
|
253
|
+
id: `unsafe-${slug(item.path)}`,
|
|
254
|
+
claim: item.text,
|
|
255
|
+
claim_type: 'capability',
|
|
256
|
+
supporting_source: item.path,
|
|
257
|
+
source_hash: text ? sha256(text) : null,
|
|
258
|
+
visible_location: item.path,
|
|
259
|
+
confidence: 0.9,
|
|
260
|
+
freshness: 'unknown',
|
|
261
|
+
contradiction: 'Unsupported ranking, traffic, indexing, or AI citation outcome claim.',
|
|
262
|
+
safe_to_publish: false,
|
|
263
|
+
verification_level: 'implemented',
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return claims;
|
|
267
|
+
}
|
|
268
|
+
function buildAnswerabilityReport(inventory, facts, claims) {
|
|
269
|
+
const questions = [
|
|
270
|
+
{
|
|
271
|
+
representative_question: `What is ${facts.canonical_name || inventory.package.name || 'this project'}?`,
|
|
272
|
+
intent_class: 'entity_identity',
|
|
273
|
+
official_answer_page: inventory.readme.path || inventory.html_files[0]?.path || null,
|
|
274
|
+
answer_section: inventory.readme.h1 || inventory.html_files[0]?.title || null,
|
|
275
|
+
supporting_claim_ids: claims.filter((claim) => claim.safe_to_publish).slice(0, 5).map((claim) => claim.id),
|
|
276
|
+
source_freshness: 'stable',
|
|
277
|
+
internal_discovery_path: inventory.readme.path ? ['README.md'] : inventory.routes.map((route) => route.path).slice(0, 5),
|
|
278
|
+
text_availability: Boolean(inventory.readme.path || inventory.html_files.some((html) => html.visibleTextSample)),
|
|
279
|
+
structured_entity_linkage: facts.canonical_url ? 'present_source_backed' : 'missing',
|
|
280
|
+
gaps: facts.facts.length ? [] : ['entity_facts_missing'],
|
|
281
|
+
},
|
|
282
|
+
];
|
|
283
|
+
return {
|
|
284
|
+
schema: 'sks.search-visibility.answerability-report.v1',
|
|
285
|
+
generated_at: new Date().toISOString(),
|
|
286
|
+
questions,
|
|
287
|
+
ranking_or_citation_claim: false,
|
|
288
|
+
external_observation: 'not_verified',
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function buildCrawlerPolicyRegistry() {
|
|
292
|
+
const observedAt = '2026-06-26T00:00:00.000Z';
|
|
293
|
+
const expiresAt = '2026-09-24T00:00:00.000Z';
|
|
294
|
+
return [
|
|
295
|
+
crawler('OAI-SearchBot', 'OpenAI', 'search', OPENAI_BOTS_URL, observedAt, expiresAt, 'Used for ChatGPT search visibility; robots.txt can express crawl preference for this user agent.'),
|
|
296
|
+
crawler('GPTBot', 'OpenAI', 'training', OPENAI_BOTS_URL, observedAt, expiresAt, 'Used for model training related crawling; do not auto-allow training crawler without user choice.'),
|
|
297
|
+
crawler('ChatGPT-User', 'OpenAI', 'user_retrieval', OPENAI_BOTS_URL, observedAt, expiresAt, 'User-directed retrieval agent; not the same as search-index eligibility.'),
|
|
298
|
+
crawler('OAI-AdsBot', 'OpenAI', 'ads_validation', OPENAI_BOTS_URL, observedAt, expiresAt, 'Ads/policy validation crawler; separated from search and training purposes.'),
|
|
299
|
+
crawler('Claude-SearchBot', 'Anthropic', 'search', ANTHROPIC_CRAWLERS_URL, observedAt, expiresAt, 'Search indexing crawler for Claude search surfaces.'),
|
|
300
|
+
crawler('ClaudeBot', 'Anthropic', 'training', ANTHROPIC_CRAWLERS_URL, observedAt, expiresAt, 'Model development/training crawler; do not auto-allow without user choice.'),
|
|
301
|
+
crawler('Claude-User', 'Anthropic', 'user_retrieval', ANTHROPIC_CRAWLERS_URL, observedAt, expiresAt, 'User-directed retrieval agent; policy semantics differ from search crawler.'),
|
|
302
|
+
];
|
|
303
|
+
}
|
|
304
|
+
function crawler(userAgent, provider, purpose, officialSource, observedAt, expiresAt, robotsSemantics) {
|
|
305
|
+
return { userAgent, provider, purpose, officialSource, observedAt, expiresAt, robotsSemantics };
|
|
306
|
+
}
|
|
307
|
+
function packageOrInventoryEvidence(inventory) {
|
|
308
|
+
if (inventory.package.path)
|
|
309
|
+
return [sourceEvidence(inventory.package.path, 'package metadata inspected')];
|
|
310
|
+
if (inventory.readme.path)
|
|
311
|
+
return [sourceEvidence(inventory.readme.path, 'README inspected')];
|
|
312
|
+
return [sourceEvidence('.', 'project inventory inspected')];
|
|
313
|
+
}
|
|
314
|
+
function finding(ruleId, category, severity, summary, evidence, affected, remediation, autoFixable, requiredCapability) {
|
|
315
|
+
const result = {
|
|
316
|
+
id: `F-${ruleId}`,
|
|
317
|
+
ruleId,
|
|
318
|
+
domain: ruleId.startsWith('geo') ? 'geo' : ruleId.startsWith('seo') ? 'seo' : 'shared',
|
|
319
|
+
category,
|
|
320
|
+
severity,
|
|
321
|
+
confidence: evidence.length ? 0.9 : 0.4,
|
|
322
|
+
blocking: severity === 'critical',
|
|
323
|
+
summary,
|
|
324
|
+
evidence,
|
|
325
|
+
affected,
|
|
326
|
+
remediation,
|
|
327
|
+
autoFixable,
|
|
328
|
+
status: evidence.length ? 'confirmed' : 'not_verified',
|
|
329
|
+
};
|
|
330
|
+
if (requiredCapability)
|
|
331
|
+
result.requiredCapability = requiredCapability;
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
function unsupportedRankingClaims(inventory) {
|
|
335
|
+
const out = [];
|
|
336
|
+
const candidates = [
|
|
337
|
+
...(inventory.package.description ? [{ path: inventory.package.path || 'package.json', text: inventory.package.description }] : []),
|
|
338
|
+
...(inventory.readme.h1 ? [{ path: inventory.readme.path || 'README.md', text: inventory.readme.h1 }] : []),
|
|
339
|
+
];
|
|
340
|
+
for (const html of inventory.html_files) {
|
|
341
|
+
if (html.visibleTextSample)
|
|
342
|
+
candidates.push({ path: html.path, text: html.visibleTextSample });
|
|
343
|
+
}
|
|
344
|
+
for (const candidate of candidates) {
|
|
345
|
+
if (/(guarantee[ds]?|보장|1위|#1|top\s*rank|rank\s*#?1|traffic\s+(?:lift|increase)|AI\s+(?:citation|answer)\s+guarantee|검색\s*유입\s*증가\s*보장|인용\s*보장)/i.test(candidate.text))
|
|
346
|
+
out.push(candidate);
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
350
|
+
function detectFactConflicts(facts) {
|
|
351
|
+
const byKey = new Map();
|
|
352
|
+
for (const fact of facts) {
|
|
353
|
+
const key = fact.key.replace(/^readme_heading$/, 'official_package_name');
|
|
354
|
+
const current = byKey.get(key) || [];
|
|
355
|
+
current.push(fact);
|
|
356
|
+
byKey.set(key, current);
|
|
357
|
+
}
|
|
358
|
+
const conflicts = [];
|
|
359
|
+
for (const [key, rows] of byKey) {
|
|
360
|
+
const values = Array.from(new Set(rows.map((row) => row.value).filter(Boolean)));
|
|
361
|
+
if (values.length > 1)
|
|
362
|
+
conflicts.push({ key, values, sources: rows.map((row) => row.source) });
|
|
363
|
+
}
|
|
364
|
+
return conflicts;
|
|
365
|
+
}
|
|
366
|
+
function entityFact(key, value, source, visibleOn, observedAt) {
|
|
367
|
+
return { key, value, source, visible_on: visibleOn, observed_at: observedAt, confidence: 1, freshness: 'stable' };
|
|
368
|
+
}
|
|
369
|
+
function routeFromHtmlPath(pathValue) {
|
|
370
|
+
const clean = pathValue.replace(/^public\//, '').replace(/index\.html$/, '').replace(/\.html$/, '');
|
|
371
|
+
const route = `/${clean}`.replace(/\/+/g, '/');
|
|
372
|
+
return route === '/' ? '/' : route.replace(/\/$/, '');
|
|
373
|
+
}
|
|
374
|
+
function slug(value) {
|
|
375
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 64) || 'item';
|
|
376
|
+
}
|
|
377
|
+
//# sourceMappingURL=analyzers.js.map
|
|
@@ -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
|