sneakoscope 4.4.0 → 4.6.1
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 +28 -2
- 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 +1 -0
- package/dist/core/agents/agent-runner-ollama.js +2 -0
- 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-fake-sdk-adapter.js +67 -9
- 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-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/run-command.js +92 -2
- package/dist/core/commands/seo-command.js +130 -0
- package/dist/core/db-safety.js +8 -6
- 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 +8 -0
- package/dist/core/init.js +8 -6
- package/dist/core/lean-engineering-policy.js +159 -0
- package/dist/core/mad-db/mad-db-policy-resolver.js +23 -2
- 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/routes.js +75 -9
- 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/version.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/config-managed-merge-callsite-coverage-check.js +2 -2
- 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/mad-db-direct-apply-migration-hook-check.js +50 -0
- package/dist/scripts/release-dag-full-coverage-check.js +1 -0
- package/dist/scripts/release-parallel-check.js +15 -0
- package/dist/scripts/release-registry-check.js +33 -14
- 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-3-1-5-directive-check-lib.js +10 -1
- package/package.json +21 -2
- 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
|
@@ -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
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { appendJsonl, ensureDir, exists, readJson, readText, sha256, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
|
|
5
|
+
import { routeForMode } from './mission.js';
|
|
6
|
+
export async function buildMutationPlan(mode, missionId, artifactDir, inventory, findings, options, entityFacts) {
|
|
7
|
+
const operations = [];
|
|
8
|
+
const blockers = [];
|
|
9
|
+
if (inventory.detected_adapter.confidence < 0.6)
|
|
10
|
+
blockers.push('adapter_detection_confidence_too_low_for_mutation');
|
|
11
|
+
if (inventory.detected_adapter.adapterId === 'unsupported')
|
|
12
|
+
blockers.push('unsupported_framework_mutation_blocked');
|
|
13
|
+
if (!blockers.length && mode === 'seo') {
|
|
14
|
+
operations.push(...await seoOperations(inventory, findings, options));
|
|
15
|
+
}
|
|
16
|
+
if (!blockers.length && mode === 'geo') {
|
|
17
|
+
operations.push(...await geoOperations(inventory, findings, options, entityFacts));
|
|
18
|
+
}
|
|
19
|
+
const plan = {
|
|
20
|
+
schema: 'sks.search-visibility.mutation-plan.v1',
|
|
21
|
+
generated_at: new Date().toISOString(),
|
|
22
|
+
mission_id: missionId,
|
|
23
|
+
route: routeForMode(mode),
|
|
24
|
+
mode,
|
|
25
|
+
adapter: inventory.detected_adapter.adapterId,
|
|
26
|
+
detection_confidence: inventory.detected_adapter.confidence,
|
|
27
|
+
status: blockers.length ? 'blocked' : 'planned',
|
|
28
|
+
operations: operations.filter((op) => scopeAllowed(op, options.scope)),
|
|
29
|
+
blockers,
|
|
30
|
+
unverified: ['production deployment and measured outcomes are outside mutation apply'],
|
|
31
|
+
};
|
|
32
|
+
await writeJsonAtomic(path.join(artifactDir, 'mutation-plan.json'), plan);
|
|
33
|
+
return plan;
|
|
34
|
+
}
|
|
35
|
+
export async function applyMutationPlan(root, missionId, artifactDir, plan, options) {
|
|
36
|
+
const blockers = [...plan.blockers];
|
|
37
|
+
if (!options.apply)
|
|
38
|
+
blockers.push('apply_requires_explicit_--apply');
|
|
39
|
+
const previousRollback = await readJson(path.join(artifactDir, 'rollback-manifest.json'), null);
|
|
40
|
+
const rollback = {
|
|
41
|
+
schema: 'sks.search-visibility.rollback-manifest.v1',
|
|
42
|
+
generated_at: new Date().toISOString(),
|
|
43
|
+
mission_id: missionId,
|
|
44
|
+
route: plan.route,
|
|
45
|
+
operations: previousRollback?.operations || [],
|
|
46
|
+
blockers,
|
|
47
|
+
};
|
|
48
|
+
if (blockers.length) {
|
|
49
|
+
await writeJsonAtomic(path.join(artifactDir, 'rollback-manifest.json'), rollback);
|
|
50
|
+
await appendJournal(artifactDir, blockedEvent(plan.operations[0] || null, 'mutation preconditions failed'));
|
|
51
|
+
return { ok: false, status: 'blocked', applied: 0, rollback, blockers };
|
|
52
|
+
}
|
|
53
|
+
await ensureDir(path.join(artifactDir, 'backups'));
|
|
54
|
+
let applied = 0;
|
|
55
|
+
let idempotent = 0;
|
|
56
|
+
for (const op of plan.operations) {
|
|
57
|
+
const full = path.resolve(root, op.path);
|
|
58
|
+
if (!full.startsWith(path.resolve(root) + path.sep) && full !== path.resolve(root)) {
|
|
59
|
+
blockers.push(`path_outside_root:${op.path}`);
|
|
60
|
+
await appendJournal(artifactDir, blockedEvent(op, 'path outside root'));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const beforeExists = await exists(full);
|
|
64
|
+
const before = beforeExists ? await readText(full, '') : null;
|
|
65
|
+
const beforeSha = before == null ? null : sha256(before);
|
|
66
|
+
if (op.baseSha256 !== beforeSha) {
|
|
67
|
+
if (op.kind === 'create' && beforeExists && beforeSha === op.proposedSha256) {
|
|
68
|
+
idempotent += 1;
|
|
69
|
+
await appendJournal(artifactDir, {
|
|
70
|
+
schema: 'sks.search-visibility.mutation-journal-event.v1',
|
|
71
|
+
ts: new Date().toISOString(),
|
|
72
|
+
operation_id: op.id,
|
|
73
|
+
event: 'applied',
|
|
74
|
+
path: op.path,
|
|
75
|
+
before_sha256: beforeSha,
|
|
76
|
+
after_sha256: beforeSha,
|
|
77
|
+
message: 'operation already applied; idempotent no-op preserved existing rollback manifest',
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
blockers.push(`base_hash_mismatch:${op.path}`);
|
|
82
|
+
await appendJournal(artifactDir, blockedEvent(op, 'base hash mismatch'));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const dirty = await gitDirtyStatus(root, op.path);
|
|
86
|
+
if (dirty && !options.allowDirtyTouched) {
|
|
87
|
+
blockers.push(`dirty_touched_path:${op.path}`);
|
|
88
|
+
await appendJournal(artifactDir, blockedEvent(op, `dirty touched path blocked: ${dirty}`));
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (op.kind === 'create' && beforeExists) {
|
|
92
|
+
blockers.push(`create_would_overwrite_existing:${op.path}`);
|
|
93
|
+
await appendJournal(artifactDir, blockedEvent(op, 'existing user-authored path'));
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const backupPath = before == null ? null : path.join('backups', `${op.id}-${path.basename(op.path)}.bak`);
|
|
97
|
+
if (backupPath && before != null)
|
|
98
|
+
await writeTextAtomic(path.join(artifactDir, backupPath), before);
|
|
99
|
+
if (op.content == null) {
|
|
100
|
+
blockers.push(`operation_content_missing:${op.id}`);
|
|
101
|
+
await appendJournal(artifactDir, blockedEvent(op, 'operation content missing'));
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
await writeTextAtomic(full, op.content);
|
|
105
|
+
const after = await readText(full, '');
|
|
106
|
+
const afterSha = sha256(after);
|
|
107
|
+
rollback.operations.push({
|
|
108
|
+
operation_id: op.id,
|
|
109
|
+
path: op.path,
|
|
110
|
+
inverse: before == null ? 'delete-created' : 'restore-content',
|
|
111
|
+
before_sha256: beforeSha,
|
|
112
|
+
after_sha256: afterSha,
|
|
113
|
+
backup_path: backupPath,
|
|
114
|
+
});
|
|
115
|
+
applied += 1;
|
|
116
|
+
await appendJournal(artifactDir, {
|
|
117
|
+
schema: 'sks.search-visibility.mutation-journal-event.v1',
|
|
118
|
+
ts: new Date().toISOString(),
|
|
119
|
+
operation_id: op.id,
|
|
120
|
+
event: 'applied',
|
|
121
|
+
path: op.path,
|
|
122
|
+
before_sha256: beforeSha,
|
|
123
|
+
after_sha256: afterSha,
|
|
124
|
+
message: 'operation applied with base hash verification',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
rollback.blockers = blockers;
|
|
128
|
+
await writeJsonAtomic(path.join(artifactDir, 'rollback-manifest.json'), rollback);
|
|
129
|
+
const completed = applied + idempotent;
|
|
130
|
+
return { ok: blockers.length === 0 && completed === plan.operations.length, status: blockers.length ? 'blocked' : 'applied', applied, rollback, blockers };
|
|
131
|
+
}
|
|
132
|
+
export async function rollbackMutationPlan(root, artifactDir, apply) {
|
|
133
|
+
const manifest = await readJson(path.join(artifactDir, 'rollback-manifest.json'), {
|
|
134
|
+
schema: 'sks.search-visibility.rollback-manifest.v1',
|
|
135
|
+
generated_at: new Date().toISOString(),
|
|
136
|
+
mission_id: 'unknown',
|
|
137
|
+
route: '$SEO-GEO-OPTIMIZER',
|
|
138
|
+
operations: [],
|
|
139
|
+
blockers: ['rollback_manifest_missing'],
|
|
140
|
+
});
|
|
141
|
+
const blockers = [...(manifest.blockers || [])].filter((blocker) => blocker !== 'apply_requires_explicit_--apply');
|
|
142
|
+
if (!apply)
|
|
143
|
+
return { ok: true, status: 'planned', rolled_back: 0, blockers: ['rollback_requires_explicit_--apply'] };
|
|
144
|
+
let rolledBack = 0;
|
|
145
|
+
for (const op of [...manifest.operations].reverse()) {
|
|
146
|
+
const full = path.resolve(root, op.path);
|
|
147
|
+
const current = await exists(full) ? await readText(full, '') : null;
|
|
148
|
+
const currentSha = current == null ? null : sha256(current);
|
|
149
|
+
if (op.after_sha256 !== currentSha) {
|
|
150
|
+
blockers.push(`rollback_hash_mismatch:${op.path}`);
|
|
151
|
+
await appendJournal(artifactDir, {
|
|
152
|
+
schema: 'sks.search-visibility.mutation-journal-event.v1',
|
|
153
|
+
ts: new Date().toISOString(),
|
|
154
|
+
operation_id: op.operation_id,
|
|
155
|
+
event: 'blocked',
|
|
156
|
+
path: op.path,
|
|
157
|
+
before_sha256: currentSha,
|
|
158
|
+
after_sha256: null,
|
|
159
|
+
message: 'rollback blocked because current hash differs from manifest',
|
|
160
|
+
});
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (op.inverse === 'delete-created') {
|
|
164
|
+
await fs.rm(full, { force: true });
|
|
165
|
+
}
|
|
166
|
+
else if (op.inverse === 'restore-content' && op.backup_path) {
|
|
167
|
+
const backup = await readText(path.join(artifactDir, op.backup_path), null);
|
|
168
|
+
if (backup == null) {
|
|
169
|
+
blockers.push(`rollback_backup_missing:${op.path}`);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
await writeTextAtomic(full, backup);
|
|
173
|
+
}
|
|
174
|
+
rolledBack += 1;
|
|
175
|
+
await appendJournal(artifactDir, {
|
|
176
|
+
schema: 'sks.search-visibility.mutation-journal-event.v1',
|
|
177
|
+
ts: new Date().toISOString(),
|
|
178
|
+
operation_id: op.operation_id,
|
|
179
|
+
event: 'rolled_back',
|
|
180
|
+
path: op.path,
|
|
181
|
+
before_sha256: op.after_sha256,
|
|
182
|
+
after_sha256: op.before_sha256,
|
|
183
|
+
message: 'operation rolled back from manifest',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return { ok: blockers.length === 0, status: blockers.length ? 'blocked' : 'rolled_back', rolled_back: rolledBack, blockers };
|
|
187
|
+
}
|
|
188
|
+
async function seoOperations(inventory, findings, options) {
|
|
189
|
+
const operations = [];
|
|
190
|
+
const robotsMissing = findings.some((finding) => finding.ruleId === 'seo-robots-missing');
|
|
191
|
+
const sitemapMissing = findings.some((finding) => finding.ruleId === 'seo-sitemap-missing');
|
|
192
|
+
if (robotsMissing && inventory.detected_adapter.capabilities.robotsMutation) {
|
|
193
|
+
const rel = await preferredPolicyPath(options.root, 'robots.txt');
|
|
194
|
+
const content = managedHeader('robots.txt') + [
|
|
195
|
+
'User-agent: *',
|
|
196
|
+
'Allow: /',
|
|
197
|
+
'',
|
|
198
|
+
inventory.origin ? `Sitemap: ${trimSlash(inventory.origin)}/sitemap.xml` : '# Sitemap: add verified origin before publishing sitemap directive',
|
|
199
|
+
'',
|
|
200
|
+
].join('\n');
|
|
201
|
+
operations.push(createOperation('seo-create-robots', rel, content, ['F-seo-robots-missing'], ['sks seo-geo-optimizer verify <mission> --mode seo --strict']));
|
|
202
|
+
}
|
|
203
|
+
if (sitemapMissing && inventory.detected_adapter.capabilities.sitemapMutation && inventory.origin) {
|
|
204
|
+
const rel = await preferredPolicyPath(options.root, 'sitemap.xml');
|
|
205
|
+
const urls = (inventory.routes.length ? inventory.routes : [{ path: '/', source: 'fallback', kind: 'static', locale: null }]).filter((route) => route.kind === 'static').slice(0, 500);
|
|
206
|
+
const content = [
|
|
207
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
208
|
+
'<!-- sks-search-visibility managed sitemap; sitemap is discovery evidence, not an indexing guarantee. -->',
|
|
209
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
210
|
+
...urls.map((route) => ` <url><loc>${xmlEscape(trimSlash(inventory.origin || '') + (route.path === '/' ? '/' : route.path))}</loc></url>`),
|
|
211
|
+
'</urlset>',
|
|
212
|
+
'',
|
|
213
|
+
].join('\n');
|
|
214
|
+
operations.push(createOperation('seo-create-sitemap', rel, content, ['F-seo-sitemap-missing'], ['sks seo-geo-optimizer verify <mission> --mode seo --strict']));
|
|
215
|
+
}
|
|
216
|
+
return operations;
|
|
217
|
+
}
|
|
218
|
+
async function geoOperations(inventory, findings, options, entityFacts) {
|
|
219
|
+
if (!options.includeLlmsTxt)
|
|
220
|
+
return [];
|
|
221
|
+
const existing = inventory.policy_files.find((file) => file.kind === 'llms' && file.exists);
|
|
222
|
+
if (existing && !existing.managed)
|
|
223
|
+
return [];
|
|
224
|
+
if (existing)
|
|
225
|
+
return [];
|
|
226
|
+
const facts = entityFacts?.facts || [];
|
|
227
|
+
const rel = await preferredRootPath(options.root, 'llms.txt');
|
|
228
|
+
const factLines = facts.slice(0, 20).map((fact) => `- ${fact.key}: ${fact.value} (source: ${fact.source})`);
|
|
229
|
+
const content = managedHeader('llms.txt') + [
|
|
230
|
+
`# ${entityFacts?.canonical_name || inventory.package.name || 'Project'} llms.txt`,
|
|
231
|
+
'',
|
|
232
|
+
'> Optional experimental assistant surface generated from source-backed facts. It does not guarantee AI search visibility, citation, ranking, or traffic.',
|
|
233
|
+
'',
|
|
234
|
+
'## Official Sources',
|
|
235
|
+
...(inventory.package.repository ? [`- Repository: ${inventory.package.repository}`] : []),
|
|
236
|
+
...(inventory.package.homepage ? [`- Homepage: ${inventory.package.homepage}`] : []),
|
|
237
|
+
'',
|
|
238
|
+
'## Source-Backed Facts',
|
|
239
|
+
...(factLines.length ? factLines : ['- No publish-safe facts were available.']),
|
|
240
|
+
'',
|
|
241
|
+
].join('\n');
|
|
242
|
+
return [createOperation('geo-create-llms-txt', rel, content, findings.filter((finding) => finding.ruleId === 'geo-llms-txt-optional-missing').map((finding) => finding.id), ['sks seo-geo-optimizer verify <mission> --mode geo --strict'])];
|
|
243
|
+
}
|
|
244
|
+
function createOperation(id, rel, content, findingIds, requiredVerification) {
|
|
245
|
+
return {
|
|
246
|
+
id,
|
|
247
|
+
path: rel,
|
|
248
|
+
baseSha256: null,
|
|
249
|
+
proposedSha256: sha256(content),
|
|
250
|
+
kind: 'create',
|
|
251
|
+
owner: 'sks-search-visibility',
|
|
252
|
+
findingIds,
|
|
253
|
+
reversible: true,
|
|
254
|
+
preview: `Create ${rel} with SKS managed search-visibility content.`,
|
|
255
|
+
content,
|
|
256
|
+
risk: 'low',
|
|
257
|
+
requiredVerification,
|
|
258
|
+
scopeAuthorization: [id, rel],
|
|
259
|
+
ownershipStrategy: 'create-only; never overwrite user-authored files',
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
async function preferredPolicyPath(root, file) {
|
|
263
|
+
return await exists(path.join(root, 'public')) ? `public/${file}` : file;
|
|
264
|
+
}
|
|
265
|
+
async function preferredRootPath(_root, file) {
|
|
266
|
+
return file;
|
|
267
|
+
}
|
|
268
|
+
function scopeAllowed(op, scope) {
|
|
269
|
+
if (!scope.length)
|
|
270
|
+
return true;
|
|
271
|
+
return scope.some((item) => op.scopeAuthorization.includes(item) || op.path === item || op.id === item);
|
|
272
|
+
}
|
|
273
|
+
function managedHeader(label) {
|
|
274
|
+
return [
|
|
275
|
+
`# sks-search-visibility managed ${label}`,
|
|
276
|
+
'# owner: sks-search-visibility',
|
|
277
|
+
'# edit policy: generated only after explicit --apply; safe to remove through sks seo-geo-optimizer rollback',
|
|
278
|
+
'',
|
|
279
|
+
].join('\n');
|
|
280
|
+
}
|
|
281
|
+
function trimSlash(value) {
|
|
282
|
+
return value.replace(/\/+$/, '');
|
|
283
|
+
}
|
|
284
|
+
function xmlEscape(value) {
|
|
285
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
286
|
+
}
|
|
287
|
+
async function appendJournal(artifactDir, event) {
|
|
288
|
+
await appendJsonl(path.join(artifactDir, 'mutation-journal.jsonl'), event);
|
|
289
|
+
}
|
|
290
|
+
function blockedEvent(op, message) {
|
|
291
|
+
return {
|
|
292
|
+
schema: 'sks.search-visibility.mutation-journal-event.v1',
|
|
293
|
+
ts: new Date().toISOString(),
|
|
294
|
+
operation_id: op?.id || 'none',
|
|
295
|
+
event: 'blocked',
|
|
296
|
+
path: op?.path || '',
|
|
297
|
+
before_sha256: null,
|
|
298
|
+
after_sha256: null,
|
|
299
|
+
message,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
async function gitDirtyStatus(root, rel) {
|
|
303
|
+
if (!(await exists(path.join(root, '.git'))))
|
|
304
|
+
return '';
|
|
305
|
+
const result = spawnSync('git', ['status', '--porcelain', '--', rel], {
|
|
306
|
+
cwd: root,
|
|
307
|
+
encoding: 'utf8',
|
|
308
|
+
stdio: 'pipe',
|
|
309
|
+
});
|
|
310
|
+
if (result.status !== 0)
|
|
311
|
+
return '';
|
|
312
|
+
return String(result.stdout || '').trim();
|
|
313
|
+
}
|
|
314
|
+
//# sourceMappingURL=mutation.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { exists } from '../fsx.js';
|
|
3
|
+
import { SEARCH_VISIBILITY_DIR, routeForMode } from './mission.js';
|
|
4
|
+
const COMMON_REQUIRED = [
|
|
5
|
+
'intake.json',
|
|
6
|
+
'adapter-detection.json',
|
|
7
|
+
'site-inventory.json',
|
|
8
|
+
'route-graph.json',
|
|
9
|
+
'robots-policy.json',
|
|
10
|
+
'structured-data-ledger.json',
|
|
11
|
+
];
|
|
12
|
+
export async function verifySearchVisibility(ctx, inventory, mission) {
|
|
13
|
+
const route = routeForMode(ctx.mode);
|
|
14
|
+
const required = [
|
|
15
|
+
...COMMON_REQUIRED,
|
|
16
|
+
ctx.mode === 'seo' ? 'seo-findings.json' : 'geo-findings.json',
|
|
17
|
+
];
|
|
18
|
+
const checked = mission
|
|
19
|
+
? await Promise.all(required.map(async (artifact) => {
|
|
20
|
+
const file = path.join(mission.artifactDir, artifact);
|
|
21
|
+
const present = await exists(file);
|
|
22
|
+
return { path: path.relative(mission.dir, file).split(path.sep).join('/'), ok: present, message: present ? 'present' : 'missing' };
|
|
23
|
+
}))
|
|
24
|
+
: [];
|
|
25
|
+
const blockers = checked.filter((item) => !item.ok).map((item) => `missing_artifact:${item.path}`);
|
|
26
|
+
const productionVerified = Boolean(ctx.origin && !ctx.offline);
|
|
27
|
+
const unverified = [
|
|
28
|
+
...(ctx.origin && !ctx.offline ? [] : ['production_http_not_verified']),
|
|
29
|
+
...(ctx.framework === 'unsupported' ? ['framework_specific_mutation_not_verified'] : []),
|
|
30
|
+
...(ctx.mode === 'geo' ? ['external_ai_answer_observation_not_verified', 'measured_outcome_pending'] : ['search_ranking_or_traffic_outcome_not_measured']),
|
|
31
|
+
...(!ctx.strict ? ['strict_mode_not_requested'] : []),
|
|
32
|
+
];
|
|
33
|
+
const status = blockers.length
|
|
34
|
+
? 'blocked'
|
|
35
|
+
: productionVerified
|
|
36
|
+
? 'production_verified'
|
|
37
|
+
: 'verified_partial';
|
|
38
|
+
return {
|
|
39
|
+
schema: 'sks.search-visibility.verification-report.v1',
|
|
40
|
+
generated_at: new Date().toISOString(),
|
|
41
|
+
mission_id: mission?.id || 'ad-hoc',
|
|
42
|
+
route,
|
|
43
|
+
status,
|
|
44
|
+
source_verified: inventory.detected_adapter.capabilities.sourceAudit,
|
|
45
|
+
build_verified: false,
|
|
46
|
+
http_verified: productionVerified,
|
|
47
|
+
browser_verified: false,
|
|
48
|
+
production_verified: productionVerified,
|
|
49
|
+
measured_outcome: 'pending',
|
|
50
|
+
checked_artifacts: checked,
|
|
51
|
+
blockers,
|
|
52
|
+
unverified,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function expectedArtifactPath(missionId, artifact) {
|
|
56
|
+
if (artifact.endsWith('-gate.json') || artifact === 'completion-proof.json')
|
|
57
|
+
return `.sneakoscope/missions/${missionId}/${artifact}`;
|
|
58
|
+
return `.sneakoscope/missions/${missionId}/${SEARCH_VISIBILITY_DIR}/${artifact}`;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=verifier.js.map
|
package/dist/core/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '4.
|
|
1
|
+
export const PACKAGE_VERSION = '4.6.1';
|
|
2
2
|
//# sourceMappingURL=version.js.map
|