sneakoscope 5.5.2 → 5.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.
- package/README.md +4 -1
- 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 +7 -1
- package/dist/cli/router.js +28 -0
- package/dist/commands/doctor.js +65 -3
- package/dist/config/skills-manifest.json +58 -58
- package/dist/core/agent-bridge/agent-manifest.js +64 -0
- package/dist/core/agent-bridge/agent-mode.js +29 -0
- package/dist/core/agent-bridge/mcp-server.js +156 -0
- package/dist/core/agents/agent-orchestrator.js +1 -11
- package/dist/core/commands/agent-bridge-command.js +86 -0
- package/dist/core/commands/image-ux-review-command.js +6 -6
- package/dist/core/commands/loop-command.js +2 -6
- package/dist/core/commands/mad-sks-command.js +3 -3
- package/dist/core/commands/mcp-server-command.js +13 -0
- package/dist/core/commands/naruto-command.js +4 -4
- package/dist/core/commands/ppt-command.js +6 -3
- package/dist/core/commands/qa-loop-command.js +64 -26
- package/dist/core/commands/team-command.js +1 -1
- package/dist/core/commands/wiki-command.js +71 -3
- package/dist/core/config/secret-preservation.js +43 -1
- package/dist/core/doctor/browser-use-repair.js +149 -0
- package/dist/core/doctor/computer-use-repair.js +166 -0
- package/dist/core/doctor/imagegen-repair.js +9 -0
- package/dist/core/doctor/mcp-transport-collision-repair.js +129 -0
- package/dist/core/doctor/supabase-mcp-repair.js +85 -16
- package/dist/core/feature-fixtures.js +8 -0
- package/dist/core/feature-registry.js +21 -0
- package/dist/core/fsx.js +1 -1
- package/dist/core/mad-db/mad-db-capability.js +1 -1
- package/dist/core/mission.js +19 -3
- package/dist/core/naruto/naruto-real-worker-child.js +5 -4
- package/dist/core/naruto/naruto-real-worker-runtime.js +5 -4
- package/dist/core/naruto/normalize-worker-prompt-text.js +12 -0
- package/dist/core/routes.js +3 -1
- package/dist/core/triwiki/code-index-scanner.js +313 -0
- package/dist/core/triwiki/code-pack.js +169 -0
- package/dist/core/triwiki/triwiki-cache-key.js +24 -2
- package/dist/core/triwiki-attention.js +34 -5
- package/dist/core/update-check.js +50 -0
- package/dist/core/version.js +1 -1
- package/dist/scripts/doctor-imagegen-repair-check.js +6 -1
- package/package.json +2 -2
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const WORKER_PROMPT_TEXT_MAX_CHARS = 32000;
|
|
2
|
+
export function normalizeWorkerPromptText(value) {
|
|
3
|
+
const normalized = String(value || '').replace(/[^\S\n]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim();
|
|
4
|
+
const truncated = normalized.length > WORKER_PROMPT_TEXT_MAX_CHARS;
|
|
5
|
+
const text = truncated ? normalized.slice(0, WORKER_PROMPT_TEXT_MAX_CHARS) : normalized;
|
|
6
|
+
return {
|
|
7
|
+
text,
|
|
8
|
+
truncated,
|
|
9
|
+
dropped_chars: truncated ? normalized.length - WORKER_PROMPT_TEXT_MAX_CHARS : 0
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=normalize-worker-prompt-text.js.map
|
package/dist/core/routes.js
CHANGED
|
@@ -643,7 +643,9 @@ export const COMMAND_CATALOG = [
|
|
|
643
643
|
{ name: 'gx', usage: 'sks gx init|render|validate|drift|snapshot [name]', description: 'Create and verify deterministic SVG/HTML visual context cartridges.' },
|
|
644
644
|
{ name: 'profile', usage: 'sks profile show|set <model>', description: 'Inspect or set the current SKS model profile metadata.' },
|
|
645
645
|
{ name: 'gc', usage: 'sks gc [--dry-run] [--json]', description: 'Compact oversized logs and prune stale runtime artifacts.' },
|
|
646
|
-
{ name: 'stats', usage: 'sks stats [--json]', description: 'Show package and .sneakoscope storage size.' }
|
|
646
|
+
{ name: 'stats', usage: 'sks stats [--json]', description: 'Show package and .sneakoscope storage size.' },
|
|
647
|
+
{ name: 'mcp-server', usage: 'sks mcp-server [--expose-exec] [--probe]', description: 'Run a stdio MCP server exposing SKS read-only commands as tools for any MCP-capable agent host; --expose-exec also exposes non-read-only commands; --probe round-trips initialize/tools-list and exits.' },
|
|
648
|
+
{ name: 'agent-bridge', usage: 'sks agent-bridge setup [--json]', description: 'Publish the agent-bridge manifest, print host registration snippets (generic MCP host, Codex CLI, non-interactive contract), and run a live non-interactive smoke test.' }
|
|
647
649
|
];
|
|
648
650
|
export function routeById(id) {
|
|
649
651
|
const key = String(id || '').replace(/^\$/, '').toLowerCase();
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { collectInputFiles } from './triwiki-cache-key.js';
|
|
5
|
+
export const CODE_INDEX_SCHEMA = 'sks.code-index.v1';
|
|
6
|
+
const DEFAULT_MAX_LINES_PER_FILE = 120;
|
|
7
|
+
const DEFAULT_MAX_FILES = 4000;
|
|
8
|
+
const CODE_EXT_RE = /\.(js|ts|tsx|jsx|cjs|mjs)$/;
|
|
9
|
+
const ENTRY_FILE_RE = /^(index|mod)\.(ts|tsx|js|jsx|mjs|cjs)$/;
|
|
10
|
+
const EXPORT_LINE_RE = /^\s*export\s+(?:default\s+)?(?:const|let|var|function|class|interface|type|enum|abstract\s+class|async\s+function)\s+[A-Za-z0-9_$]+/;
|
|
11
|
+
const EXPORT_BRACE_RE = /^\s*export\s*\{/;
|
|
12
|
+
const EXPORT_STAR_RE = /^\s*export\s*\*/;
|
|
13
|
+
const IMPORT_SPEC_RE = /(?:from\s+|require\()\s*['"](\.\.?\/[^'"]+)['"]/g;
|
|
14
|
+
// High fan-in or a large file surface both correlate with change blast-radius; simple counts avoid needing a real dependency graph.
|
|
15
|
+
const RISK_HIGH_FAN_IN = 5;
|
|
16
|
+
const RISK_HIGH_FILE_COUNT = 40;
|
|
17
|
+
const RISK_MEDIUM_FAN_IN = 2;
|
|
18
|
+
const RISK_MEDIUM_FILE_COUNT = 15;
|
|
19
|
+
export async function scanCodebaseIndex(root, opts = {}) {
|
|
20
|
+
const resolvedRoot = path.resolve(root);
|
|
21
|
+
const maxLinesPerFile = opts.maxLinesPerFile ?? DEFAULT_MAX_LINES_PER_FILE;
|
|
22
|
+
const maxFiles = opts.maxFiles ?? DEFAULT_MAX_FILES;
|
|
23
|
+
const inventory = listSourceFiles(resolvedRoot);
|
|
24
|
+
const truncatedByCap = inventory.length > maxFiles;
|
|
25
|
+
const scannedFiles = truncatedByCap ? inventory.slice(0, maxFiles) : inventory;
|
|
26
|
+
const boundaries = inferModuleBoundaries(resolvedRoot);
|
|
27
|
+
const fileRecords = readFileRecords(resolvedRoot, scannedFiles, maxLinesPerFile);
|
|
28
|
+
const modulesByPath = assignFilesToModules(boundaries, fileRecords);
|
|
29
|
+
const moduleIdForFile = buildFileToModuleIdIndex(modulesByPath);
|
|
30
|
+
const cards = buildModuleCards(resolvedRoot, modulesByPath, moduleIdForFile);
|
|
31
|
+
return {
|
|
32
|
+
schema: CODE_INDEX_SCHEMA,
|
|
33
|
+
generated_at: new Date().toISOString(),
|
|
34
|
+
root: resolvedRoot,
|
|
35
|
+
modules: cards,
|
|
36
|
+
truncated: truncatedByCap,
|
|
37
|
+
scanned_file_count: scannedFiles.length,
|
|
38
|
+
scanned_files_cap: maxFiles
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function listSourceFiles(root) {
|
|
42
|
+
const gitFiles = tryGitLsFiles(root);
|
|
43
|
+
const all = gitFiles !== null ? gitFiles : walkFallback(root);
|
|
44
|
+
return all.filter((rel) => CODE_EXT_RE.test(rel)).sort();
|
|
45
|
+
}
|
|
46
|
+
function tryGitLsFiles(root) {
|
|
47
|
+
try {
|
|
48
|
+
const result = spawnSync('git', ['ls-files'], { cwd: root, encoding: 'utf8' });
|
|
49
|
+
if (result.status !== 0 || result.error)
|
|
50
|
+
return null;
|
|
51
|
+
return String(result.stdout || '').split('\n').map((line) => line.trim()).filter(Boolean);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function walkFallback(root) {
|
|
58
|
+
const collected = collectInputFiles(root, ['.']);
|
|
59
|
+
return collected.records.map((record) => record.path);
|
|
60
|
+
}
|
|
61
|
+
function inferModuleBoundaries(root) {
|
|
62
|
+
const boundaries = [];
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
const addBoundary = (dir) => {
|
|
65
|
+
const normalized = dir.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
66
|
+
if (!normalized || seen.has(normalized))
|
|
67
|
+
return;
|
|
68
|
+
seen.add(normalized);
|
|
69
|
+
boundaries.push({ module_id: moduleIdFromDir(normalized), dir: normalized });
|
|
70
|
+
};
|
|
71
|
+
for (const sourceRoot of listDirs(root, '.').filter((name) => name === 'src' || name === 'source' || name === 'lib')) {
|
|
72
|
+
for (const topLevel of listDirs(root, sourceRoot)) {
|
|
73
|
+
addBoundary(topLevel);
|
|
74
|
+
for (const nested of listDirs(root, topLevel))
|
|
75
|
+
addBoundary(nested);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const workspaces = readWorkspaceGlobs(root);
|
|
79
|
+
for (const glob of workspaces) {
|
|
80
|
+
for (const dir of resolveWorkspaceGlob(root, glob))
|
|
81
|
+
addBoundary(dir);
|
|
82
|
+
}
|
|
83
|
+
return boundaries.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
84
|
+
}
|
|
85
|
+
function listDirs(root, rel) {
|
|
86
|
+
const absolute = rel === '.' ? root : path.join(root, rel);
|
|
87
|
+
let entries;
|
|
88
|
+
try {
|
|
89
|
+
entries = fs.readdirSync(absolute, { withFileTypes: true });
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (isMissingOrInaccessible(error))
|
|
93
|
+
return [];
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
return entries
|
|
97
|
+
.filter((entry) => entry.isDirectory() && !isExcludedDirName(entry.name))
|
|
98
|
+
.map((entry) => (rel === '.' ? entry.name : `${rel}/${entry.name}`))
|
|
99
|
+
.sort();
|
|
100
|
+
}
|
|
101
|
+
function isExcludedDirName(name) {
|
|
102
|
+
return name === '.git' || name === 'node_modules' || name === 'dist' || name === 'build' || name === 'coverage' || name === '__tests__' || name.startsWith('.');
|
|
103
|
+
}
|
|
104
|
+
function isMissingOrInaccessible(error) {
|
|
105
|
+
const code = error?.code;
|
|
106
|
+
return code === 'ENOENT' || code === 'EACCES' || code === 'ENOTDIR' || code === 'EPERM' || code === 'EISDIR';
|
|
107
|
+
}
|
|
108
|
+
function readWorkspaceGlobs(root) {
|
|
109
|
+
try {
|
|
110
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
111
|
+
if (!pkg.workspaces)
|
|
112
|
+
return [];
|
|
113
|
+
if (Array.isArray(pkg.workspaces))
|
|
114
|
+
return pkg.workspaces;
|
|
115
|
+
return pkg.workspaces.packages || [];
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function resolveWorkspaceGlob(root, glob) {
|
|
122
|
+
const normalized = glob.replace(/\\/g, '/');
|
|
123
|
+
if (!normalized.includes('*')) {
|
|
124
|
+
return fs.existsSync(path.join(root, normalized)) ? [normalized.replace(/\/+$/, '')] : [];
|
|
125
|
+
}
|
|
126
|
+
const starIndex = normalized.indexOf('*');
|
|
127
|
+
const prefix = normalized.slice(0, starIndex).replace(/\/+$/, '');
|
|
128
|
+
return listDirs(root, prefix || '.');
|
|
129
|
+
}
|
|
130
|
+
function moduleIdFromDir(dir) {
|
|
131
|
+
return dir.replace(/^src\//, '').replace(/\//g, '-') || 'root';
|
|
132
|
+
}
|
|
133
|
+
function readFileRecords(root, files, maxLinesPerFile) {
|
|
134
|
+
const out = [];
|
|
135
|
+
for (const rel of files) {
|
|
136
|
+
const absolute = path.join(root, rel);
|
|
137
|
+
let text = '';
|
|
138
|
+
try {
|
|
139
|
+
text = fs.readFileSync(absolute, 'utf8');
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (isMissingOrInaccessible(error))
|
|
143
|
+
continue;
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
const lines = text.split(/\r?\n/);
|
|
147
|
+
const loc = lines.length && lines[lines.length - 1] === '' ? lines.length - 1 : lines.length;
|
|
148
|
+
const scanLines = lines.slice(0, maxLinesPerFile);
|
|
149
|
+
const exportLines = [];
|
|
150
|
+
for (const line of scanLines) {
|
|
151
|
+
if (EXPORT_LINE_RE.test(line) || EXPORT_BRACE_RE.test(line) || EXPORT_STAR_RE.test(line)) {
|
|
152
|
+
exportLines.push(line.trim());
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const importSpecifiers = [];
|
|
156
|
+
for (const line of scanLines) {
|
|
157
|
+
IMPORT_SPEC_RE.lastIndex = 0;
|
|
158
|
+
let match;
|
|
159
|
+
while ((match = IMPORT_SPEC_RE.exec(line))) {
|
|
160
|
+
if (match[1])
|
|
161
|
+
importSpecifiers.push(match[1]);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
out.push({ rel, loc, exportLines, importSpecifiers });
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
function assignFilesToModules(boundaries, files) {
|
|
169
|
+
const sortedBoundaries = [...boundaries].sort((a, b) => b.dir.length - a.dir.length);
|
|
170
|
+
const map = new Map();
|
|
171
|
+
for (const boundary of boundaries)
|
|
172
|
+
map.set(boundary.module_id, { boundary, files: [] });
|
|
173
|
+
for (const file of files) {
|
|
174
|
+
const owner = sortedBoundaries.find((boundary) => file.rel === boundary.dir || file.rel.startsWith(`${boundary.dir}/`));
|
|
175
|
+
if (!owner)
|
|
176
|
+
continue;
|
|
177
|
+
map.get(owner.module_id).files.push(file);
|
|
178
|
+
}
|
|
179
|
+
for (const [moduleId, entry] of map) {
|
|
180
|
+
if (!entry.files.length)
|
|
181
|
+
map.delete(moduleId);
|
|
182
|
+
}
|
|
183
|
+
return map;
|
|
184
|
+
}
|
|
185
|
+
function buildFileToModuleIdIndex(modulesByPath) {
|
|
186
|
+
const index = new Map();
|
|
187
|
+
for (const [moduleId, entry] of modulesByPath) {
|
|
188
|
+
for (const file of entry.files)
|
|
189
|
+
index.set(file.rel, moduleId);
|
|
190
|
+
}
|
|
191
|
+
return index;
|
|
192
|
+
}
|
|
193
|
+
function buildModuleCards(root, modulesByPath, moduleIdForFile) {
|
|
194
|
+
const fanIn = new Map();
|
|
195
|
+
const dependencyEdgesByModule = new Map();
|
|
196
|
+
for (const [moduleId, entry] of modulesByPath) {
|
|
197
|
+
const edges = new Set();
|
|
198
|
+
for (const file of entry.files) {
|
|
199
|
+
for (const specifier of file.importSpecifiers) {
|
|
200
|
+
const resolvedRel = resolveRelativeImport(root, file.rel, specifier);
|
|
201
|
+
if (!resolvedRel)
|
|
202
|
+
continue;
|
|
203
|
+
const targetModuleId = moduleIdForFile.get(resolvedRel);
|
|
204
|
+
if (!targetModuleId || targetModuleId === moduleId)
|
|
205
|
+
continue;
|
|
206
|
+
edges.add(targetModuleId);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
dependencyEdgesByModule.set(moduleId, edges);
|
|
210
|
+
for (const target of edges)
|
|
211
|
+
fanIn.set(target, (fanIn.get(target) || 0) + 1);
|
|
212
|
+
}
|
|
213
|
+
const cards = [];
|
|
214
|
+
for (const [moduleId, entry] of modulesByPath) {
|
|
215
|
+
const files = entry.files;
|
|
216
|
+
const loc = files.reduce((sum, file) => sum + file.loc, 0);
|
|
217
|
+
const exportsSummary = files.flatMap((file) => file.exportLines);
|
|
218
|
+
const dependencyEdges = [...(dependencyEdgesByModule.get(moduleId) || [])].sort();
|
|
219
|
+
const entryPoints = collectEntryPoints(root, entry.boundary.dir, files);
|
|
220
|
+
const fanInCount = fanIn.get(moduleId) || 0;
|
|
221
|
+
const risk = computeRisk(fanInCount, files.length);
|
|
222
|
+
cards.push({
|
|
223
|
+
module_id: moduleId,
|
|
224
|
+
paths: [entry.boundary.dir],
|
|
225
|
+
entry_points: entryPoints,
|
|
226
|
+
exports_summary: exportsSummary,
|
|
227
|
+
dependency_edges: dependencyEdges,
|
|
228
|
+
file_count: files.length,
|
|
229
|
+
loc,
|
|
230
|
+
risk
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return cards.sort((a, b) => a.module_id.localeCompare(b.module_id));
|
|
234
|
+
}
|
|
235
|
+
function computeRisk(fanIn, fileCount) {
|
|
236
|
+
if (fanIn >= RISK_HIGH_FAN_IN || fileCount >= RISK_HIGH_FILE_COUNT)
|
|
237
|
+
return 'high';
|
|
238
|
+
if (fanIn >= RISK_MEDIUM_FAN_IN || fileCount >= RISK_MEDIUM_FILE_COUNT)
|
|
239
|
+
return 'medium';
|
|
240
|
+
return 'low';
|
|
241
|
+
}
|
|
242
|
+
function collectEntryPoints(root, dir, files) {
|
|
243
|
+
const entryPoints = new Set();
|
|
244
|
+
const pkgPath = path.join(root, dir, 'package.json');
|
|
245
|
+
if (fs.existsSync(pkgPath)) {
|
|
246
|
+
try {
|
|
247
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
248
|
+
if (typeof pkg.main === 'string')
|
|
249
|
+
entryPoints.add(normalizeEntryPath(dir, pkg.main));
|
|
250
|
+
if (typeof pkg.bin === 'string')
|
|
251
|
+
entryPoints.add(normalizeEntryPath(dir, pkg.bin));
|
|
252
|
+
else if (pkg.bin && typeof pkg.bin === 'object') {
|
|
253
|
+
for (const value of Object.values(pkg.bin))
|
|
254
|
+
if (typeof value === 'string')
|
|
255
|
+
entryPoints.add(normalizeEntryPath(dir, value));
|
|
256
|
+
}
|
|
257
|
+
for (const value of collectExportsPaths(pkg.exports))
|
|
258
|
+
entryPoints.add(normalizeEntryPath(dir, value));
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// malformed package.json at a module boundary is not this scanner's concern; skip entry-point inference for it
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (const file of files) {
|
|
265
|
+
const base = path.basename(file.rel);
|
|
266
|
+
if (ENTRY_FILE_RE.test(base))
|
|
267
|
+
entryPoints.add(file.rel);
|
|
268
|
+
}
|
|
269
|
+
return [...entryPoints].sort();
|
|
270
|
+
}
|
|
271
|
+
function collectExportsPaths(exportsField) {
|
|
272
|
+
if (typeof exportsField === 'string')
|
|
273
|
+
return [exportsField];
|
|
274
|
+
if (!exportsField || typeof exportsField !== 'object')
|
|
275
|
+
return [];
|
|
276
|
+
const out = [];
|
|
277
|
+
for (const value of Object.values(exportsField)) {
|
|
278
|
+
if (typeof value === 'string')
|
|
279
|
+
out.push(value);
|
|
280
|
+
else
|
|
281
|
+
out.push(...collectExportsPaths(value));
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
function normalizeEntryPath(dir, entry) {
|
|
286
|
+
const cleaned = entry.replace(/^\.\//, '');
|
|
287
|
+
return `${dir}/${cleaned}`.replace(/\\/g, '/');
|
|
288
|
+
}
|
|
289
|
+
function resolveRelativeImport(root, fromRel, specifier) {
|
|
290
|
+
const fromDir = path.posix.dirname(fromRel);
|
|
291
|
+
const joined = path.posix.normalize(path.posix.join(fromDir, specifier));
|
|
292
|
+
const candidates = [
|
|
293
|
+
`${joined}.ts`,
|
|
294
|
+
`${joined}.tsx`,
|
|
295
|
+
`${joined}.js`,
|
|
296
|
+
`${joined}.jsx`,
|
|
297
|
+
`${joined}/index.ts`,
|
|
298
|
+
`${joined}/index.tsx`,
|
|
299
|
+
`${joined}/index.js`,
|
|
300
|
+
`${joined}/index.jsx`,
|
|
301
|
+
joined
|
|
302
|
+
];
|
|
303
|
+
for (const candidate of candidates) {
|
|
304
|
+
const absolute = path.join(root, candidate);
|
|
305
|
+
if (!fs.existsSync(absolute))
|
|
306
|
+
continue;
|
|
307
|
+
// only resolve to a real file — a bare directory match (no extension) can't be looked up in the file->module index
|
|
308
|
+
if (fs.statSync(absolute).isFile())
|
|
309
|
+
return candidate;
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
//# sourceMappingURL=code-index-scanner.js.map
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { ensureDir, exists, nowIso, readJson, sha256, writeJsonAtomic } from '../fsx.js';
|
|
5
|
+
export const CODE_PACK_SCHEMA = 'sks.code-pack.v1';
|
|
6
|
+
export const DEFAULT_CODE_PACK_TOKEN_BUDGET = 8000;
|
|
7
|
+
export function codePackDir(root) {
|
|
8
|
+
return path.join(root, '.sneakoscope', 'wiki');
|
|
9
|
+
}
|
|
10
|
+
export function codePackPath(root) {
|
|
11
|
+
return path.join(codePackDir(root), 'code-pack.json');
|
|
12
|
+
}
|
|
13
|
+
export function codePackPrevPath(root) {
|
|
14
|
+
return path.join(codePackDir(root), 'code-pack.prev.json');
|
|
15
|
+
}
|
|
16
|
+
export function buildCodePack(root, index, tokenBudget = DEFAULT_CODE_PACK_TOKEN_BUDGET) {
|
|
17
|
+
const entries = [];
|
|
18
|
+
for (const card of index.modules) {
|
|
19
|
+
const entry = buildEntryForModule(card);
|
|
20
|
+
if (!entry)
|
|
21
|
+
continue;
|
|
22
|
+
entries.push(entry);
|
|
23
|
+
}
|
|
24
|
+
const totalTokenCost = entries.reduce((sum, entry) => sum + entry.token_cost, 0);
|
|
25
|
+
return {
|
|
26
|
+
schema: CODE_PACK_SCHEMA,
|
|
27
|
+
generated_at: nowIso(),
|
|
28
|
+
git_head_sha: readGitHeadSha(root),
|
|
29
|
+
source_file_count: index.scanned_file_count,
|
|
30
|
+
index_digest: computeIndexDigest(index),
|
|
31
|
+
entries,
|
|
32
|
+
token_budget: tokenBudget,
|
|
33
|
+
total_token_cost: totalTokenCost
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function buildEntryForModule(card) {
|
|
37
|
+
const citations = collectCitations(card);
|
|
38
|
+
// openwiki principle: an entry with no real repository citation is worse than no entry at all
|
|
39
|
+
if (!citations.length)
|
|
40
|
+
return null;
|
|
41
|
+
const text = summarizeModule(card);
|
|
42
|
+
return {
|
|
43
|
+
id: `code:${card.module_id}`,
|
|
44
|
+
text,
|
|
45
|
+
citations,
|
|
46
|
+
trust_score: computeTrustScore(card),
|
|
47
|
+
freshness: 'unknown',
|
|
48
|
+
token_cost: Math.ceil(text.length / 4)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function collectCitations(card) {
|
|
52
|
+
const paths = [...card.paths, ...card.entry_points].filter(Boolean);
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
const citations = [];
|
|
55
|
+
for (const p of paths) {
|
|
56
|
+
if (seen.has(p))
|
|
57
|
+
continue;
|
|
58
|
+
seen.add(p);
|
|
59
|
+
citations.push({ path: p });
|
|
60
|
+
}
|
|
61
|
+
return citations;
|
|
62
|
+
}
|
|
63
|
+
function summarizeModule(card) {
|
|
64
|
+
const role = inferRole(card);
|
|
65
|
+
const primaryPath = card.paths[0] || card.module_id;
|
|
66
|
+
const exportsPart = summarizeExports(card.exports_summary);
|
|
67
|
+
const depsPart = summarizeDependencies(card.dependency_edges);
|
|
68
|
+
const sizePart = `${card.file_count} file${card.file_count === 1 ? '' : 's'}, ${card.loc} loc, ${card.risk} risk`;
|
|
69
|
+
return `${card.module_id} is ${role} at ${primaryPath} (${sizePart}). ${exportsPart} ${depsPart}`.trim().replace(/\s+/g, ' ');
|
|
70
|
+
}
|
|
71
|
+
function inferRole(card) {
|
|
72
|
+
const idLower = card.module_id.toLowerCase();
|
|
73
|
+
const pathLower = card.paths.join(' ').toLowerCase();
|
|
74
|
+
if (idLower.includes('test') || pathLower.includes('__tests__'))
|
|
75
|
+
return 'a test module';
|
|
76
|
+
if (idLower.includes('cli') || pathLower.includes('cli'))
|
|
77
|
+
return 'a CLI module';
|
|
78
|
+
if (idLower.includes('command'))
|
|
79
|
+
return 'a command module';
|
|
80
|
+
if (idLower.includes('core'))
|
|
81
|
+
return 'a core module';
|
|
82
|
+
return 'a module';
|
|
83
|
+
}
|
|
84
|
+
function summarizeExports(exportsSummary) {
|
|
85
|
+
if (!exportsSummary.length)
|
|
86
|
+
return 'It has no detected top-level exports.';
|
|
87
|
+
const preview = exportsSummary.slice(0, 5).map((line) => line.replace(/^export\s+/, '').replace(/\s*\{\s*$/, '').trim());
|
|
88
|
+
const suffix = exportsSummary.length > preview.length ? `, and ${exportsSummary.length - preview.length} more` : '';
|
|
89
|
+
return `Key exports: ${preview.join('; ')}${suffix}.`;
|
|
90
|
+
}
|
|
91
|
+
function summarizeDependencies(dependencyEdges) {
|
|
92
|
+
if (!dependencyEdges.length)
|
|
93
|
+
return 'It has no detected cross-module dependencies.';
|
|
94
|
+
const preview = dependencyEdges.slice(0, 5);
|
|
95
|
+
const suffix = dependencyEdges.length > preview.length ? `, and ${dependencyEdges.length - preview.length} more` : '';
|
|
96
|
+
return `Depends on: ${preview.join(', ')}${suffix}.`;
|
|
97
|
+
}
|
|
98
|
+
function computeTrustScore(card) {
|
|
99
|
+
// more citable surface (entry points, real exports) correlates with a more reliably summarizable module
|
|
100
|
+
let score = 0.5;
|
|
101
|
+
if (card.entry_points.length > 0)
|
|
102
|
+
score += 0.2;
|
|
103
|
+
if (card.exports_summary.length > 0)
|
|
104
|
+
score += 0.2;
|
|
105
|
+
if (card.risk === 'high')
|
|
106
|
+
score -= 0.15;
|
|
107
|
+
else if (card.risk === 'low')
|
|
108
|
+
score += 0.1;
|
|
109
|
+
return Math.max(0, Math.min(1, Number(score.toFixed(2))));
|
|
110
|
+
}
|
|
111
|
+
function readGitHeadSha(root) {
|
|
112
|
+
try {
|
|
113
|
+
const result = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' });
|
|
114
|
+
if (result.status !== 0 || result.error)
|
|
115
|
+
return null;
|
|
116
|
+
const sha = String(result.stdout || '').trim();
|
|
117
|
+
return sha || null;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function computeIndexDigest(index) {
|
|
124
|
+
const stable = index.modules
|
|
125
|
+
.map((card) => ({ module_id: card.module_id, paths: [...card.paths].sort() }))
|
|
126
|
+
.sort((a, b) => a.module_id.localeCompare(b.module_id));
|
|
127
|
+
return sha256(JSON.stringify(stable));
|
|
128
|
+
}
|
|
129
|
+
export async function validateCodePack(pack, root) {
|
|
130
|
+
const issues = [];
|
|
131
|
+
const seenIds = new Set();
|
|
132
|
+
for (const entry of pack.entries) {
|
|
133
|
+
if (seenIds.has(entry.id)) {
|
|
134
|
+
issues.push(`duplicate entry id: ${entry.id}`);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
seenIds.add(entry.id);
|
|
138
|
+
}
|
|
139
|
+
if (!entry.citations.length) {
|
|
140
|
+
issues.push(`entry ${entry.id} has no citations`);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
for (const citation of entry.citations) {
|
|
144
|
+
const absolute = path.join(root, citation.path);
|
|
145
|
+
if (!fs.existsSync(absolute)) {
|
|
146
|
+
issues.push(`entry ${entry.id} citation path does not exist: ${citation.path}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const totalTokenCost = pack.entries.reduce((sum, entry) => sum + entry.token_cost, 0);
|
|
151
|
+
if (totalTokenCost > pack.token_budget) {
|
|
152
|
+
issues.push(`total_token_cost ${totalTokenCost} exceeds token_budget ${pack.token_budget}`);
|
|
153
|
+
}
|
|
154
|
+
return { ok: issues.length === 0, issues };
|
|
155
|
+
}
|
|
156
|
+
export async function writeCodePackAtomic(root, pack) {
|
|
157
|
+
const targetPath = codePackPath(root);
|
|
158
|
+
const prevPath = codePackPrevPath(root);
|
|
159
|
+
await ensureDir(codePackDir(root));
|
|
160
|
+
let prevWritten = null;
|
|
161
|
+
if (await exists(targetPath)) {
|
|
162
|
+
const previous = await readJson(targetPath);
|
|
163
|
+
await writeJsonAtomic(prevPath, previous);
|
|
164
|
+
prevWritten = prevPath;
|
|
165
|
+
}
|
|
166
|
+
await writeJsonAtomic(targetPath, pack);
|
|
167
|
+
return { ok: true, path: targetPath, prev_path: prevWritten };
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=code-pack.js.map
|
|
@@ -125,7 +125,15 @@ function listFiles(root, start) {
|
|
|
125
125
|
const out = [];
|
|
126
126
|
if (!fs.existsSync(start))
|
|
127
127
|
return out;
|
|
128
|
-
|
|
128
|
+
let stat;
|
|
129
|
+
try {
|
|
130
|
+
stat = fs.lstatSync(start);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (isMissingOrInaccessible(error))
|
|
134
|
+
return out;
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
129
137
|
if (stat.isSymbolicLink() || stat.isFile())
|
|
130
138
|
return [relativeUnix(root, start)];
|
|
131
139
|
if (!stat.isDirectory())
|
|
@@ -138,7 +146,17 @@ function listFiles(root, start) {
|
|
|
138
146
|
const relDir = relativeUnix(root, dir);
|
|
139
147
|
if (relDir && isExcluded(relDir))
|
|
140
148
|
continue;
|
|
141
|
-
|
|
149
|
+
let entries;
|
|
150
|
+
try {
|
|
151
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
// dir can vanish or become unreadable between stack push and pop (TOCTOU) — contribute zero files, not a crash
|
|
155
|
+
if (isMissingOrInaccessible(error))
|
|
156
|
+
continue;
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
for (const entry of entries) {
|
|
142
160
|
const absolute = path.join(dir, entry.name);
|
|
143
161
|
const rel = relativeUnix(root, absolute);
|
|
144
162
|
if (isExcluded(rel))
|
|
@@ -151,6 +169,10 @@ function listFiles(root, start) {
|
|
|
151
169
|
}
|
|
152
170
|
return out.sort();
|
|
153
171
|
}
|
|
172
|
+
function isMissingOrInaccessible(error) {
|
|
173
|
+
const code = error?.code;
|
|
174
|
+
return code === 'ENOENT' || code === 'EACCES' || code === 'ENOTDIR' || code === 'EPERM';
|
|
175
|
+
}
|
|
154
176
|
function isExcluded(rel) {
|
|
155
177
|
for (const dir of DEFAULT_EXCLUDED_DIRS) {
|
|
156
178
|
if (rel === dir || rel.startsWith(`${dir}/`))
|
|
@@ -224,7 +224,33 @@ function uniqueAttentionRows(rows = [], max = 4) {
|
|
|
224
224
|
}
|
|
225
225
|
return out;
|
|
226
226
|
}
|
|
227
|
-
|
|
227
|
+
/** Ranks code-pack entries (LLM-consumable, source-cited codebase summaries — see
|
|
228
|
+
* src/core/triwiki/code-pack.ts) by trust_score into a dedicated attention sub-budget,
|
|
229
|
+
* independent of the policy-claim RGBA/geometric selection above: code entries carry
|
|
230
|
+
* no meaningful mission-coordinate proximity, so competing them against policy claims
|
|
231
|
+
* for the same fixed slot budget would make their appearance arbitrary. Each surviving
|
|
232
|
+
* entry becomes a use_first row (bare id, no fabricated RGBA) plus a hydrate_first row
|
|
233
|
+
* pointing at its source citations, so a consumer knows which real files back it. */
|
|
234
|
+
function codePackAttentionRows(codePackEntries = [], tokenBudget = 2000) {
|
|
235
|
+
const sorted = [...(codePackEntries || [])]
|
|
236
|
+
.filter((entry) => entry && typeof entry.id === 'string')
|
|
237
|
+
.sort((a, b) => Number(b.trust_score || 0) - Number(a.trust_score || 0));
|
|
238
|
+
const useFirst = [];
|
|
239
|
+
const hydrateFirst = [];
|
|
240
|
+
let tokens = 0;
|
|
241
|
+
for (const entry of sorted) {
|
|
242
|
+
const cost = Number(entry.token_cost) || Math.max(1, Math.ceil(String(entry.text || '').length / 4));
|
|
243
|
+
if (tokens + cost > tokenBudget)
|
|
244
|
+
continue;
|
|
245
|
+
tokens += cost;
|
|
246
|
+
useFirst.push([entry.id, null, null]);
|
|
247
|
+
const citationPaths = Array.isArray(entry.citations) ? entry.citations.map((c) => c?.path).filter(Boolean) : [];
|
|
248
|
+
if (citationPaths.length)
|
|
249
|
+
hydrateFirst.push([entry.id, `code_citations:${citationPaths.join(',')}`]);
|
|
250
|
+
}
|
|
251
|
+
return { useFirst, hydrateFirst };
|
|
252
|
+
}
|
|
253
|
+
export function buildTriWikiAttention({ selected = [], wiki = {}, role = 'worker', maxUse = 4, maxHydrate = 4, codePackEntries = [], codePackTokenBudget = 2000 } = {}) {
|
|
228
254
|
const anchors = attentionAnchorMap(wiki);
|
|
229
255
|
const ranked = [...(selected || [])]
|
|
230
256
|
.map((claim, index) => ({ claim, index }))
|
|
@@ -250,10 +276,11 @@ export function buildTriWikiAttention({ selected = [], wiki = {}, role = 'worker
|
|
|
250
276
|
...voxelRows,
|
|
251
277
|
...selectedHydrateRows
|
|
252
278
|
], maxHydrate);
|
|
279
|
+
const codeRows = codePackAttentionRows(codePackEntries, codePackTokenBudget);
|
|
253
280
|
return {
|
|
254
281
|
mode: 'aggressive_triwiki_active_recall',
|
|
255
|
-
use_first: useFirst,
|
|
256
|
-
hydrate_first: hydrateFirst
|
|
282
|
+
use_first: uniqueAttentionRows([...useFirst, ...codeRows.useFirst], maxUse + codeRows.useFirst.length),
|
|
283
|
+
hydrate_first: uniqueAttentionRows([...hydrateFirst, ...codeRows.hydrateFirst], maxHydrate + codeRows.hydrateFirst.length)
|
|
257
284
|
};
|
|
258
285
|
}
|
|
259
286
|
export function selectClaims(mission, claims, budget = {}) {
|
|
@@ -282,7 +309,7 @@ export function geometricOffsets(max = 65536) {
|
|
|
282
309
|
out.push(x);
|
|
283
310
|
return out;
|
|
284
311
|
}
|
|
285
|
-
export function contextCapsule({ mission, role = 'worker', contractHash = null, claims = [], q4 = {}, q3 = [], budget = {} }) {
|
|
312
|
+
export function contextCapsule({ mission, role = 'worker', contractHash = null, claims = [], q4 = {}, q3 = [], budget = {}, codePackEntries = [] }) {
|
|
286
313
|
const trustPolicy = budget.trustPolicy || DEFAULT_TRUST_POLICY;
|
|
287
314
|
const claimsWithTrust = (claims || []).map((claim) => withTrust(claim, trustPolicy));
|
|
288
315
|
const selected = selectClaims(mission, claims, { maxClaims: budget.maxClaims ?? (role.includes('verifier') ? 16 : 9), trustPolicy });
|
|
@@ -313,7 +340,9 @@ export function contextCapsule({ mission, role = 'worker', contractHash = null,
|
|
|
313
340
|
wiki,
|
|
314
341
|
role,
|
|
315
342
|
maxUse: budget.maxAttentionUse ?? (role.includes('verifier') ? 7 : 4),
|
|
316
|
-
maxHydrate: budget.maxAttentionHydrate ?? (role.includes('verifier') ? 7 : 4)
|
|
343
|
+
maxHydrate: budget.maxAttentionHydrate ?? (role.includes('verifier') ? 7 : 4),
|
|
344
|
+
codePackEntries,
|
|
345
|
+
codePackTokenBudget: budget.codePackTokenBudget ?? 2000
|
|
317
346
|
}),
|
|
318
347
|
claims: selected.map((c) => {
|
|
319
348
|
const anchor = anchorsById.get(c.id);
|