knodin 0.7.4 → 0.7.6
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/dist/bin/cli.js +108 -1859
- package/dist/bin/launcher.js +3 -10
- package/dist/chunks/chunk-DMQAGX77.js +654 -0
- package/dist/chunks/chunk-F4Z3Z766.js +4 -0
- package/dist/chunks/chunk-SIJAQVSX.js +3 -0
- package/dist/chunks/chunk-X6M4HUUE.js +2 -0
- package/dist/chunks/chunk-YPRMY2LP.js +8 -0
- package/dist/chunks/pure-compression-cli-4TA2TQD5.js +5 -0
- package/dist/chunks/server-7EDF4CBY.js +14 -0
- package/dist/chunks/structural-fast-path-KD5KQSPX.js +4 -0
- package/dist/src/init-progress-worker.js +2 -106
- package/dist/src/repair-progress-worker.js +2 -120
- package/docs/INSTALLATION.md +17 -1
- package/docs/releases/0.7.6.md +25 -0
- package/package.json +1 -110
- package/dist/src/agent-integration.js +0 -238
- package/dist/src/artifact-refresh.js +0 -81
- package/dist/src/cli-args.js +0 -274
- package/dist/src/cli-model.js +0 -358
- package/dist/src/codeflow-replay.js +0 -80
- package/dist/src/compact-structural.js +0 -96
- package/dist/src/competitive-cold-mcp.js +0 -40
- package/dist/src/competitive-constraints.js +0 -20
- package/dist/src/competitive-manifest.js +0 -411
- package/dist/src/competitive-measurement.js +0 -183
- package/dist/src/competitive-runner.js +0 -487
- package/dist/src/competitive-sandbox.js +0 -108
- package/dist/src/context-export.js +0 -422
- package/dist/src/context.js +0 -102
- package/dist/src/diagnostics.js +0 -449
- package/dist/src/docs-sections.js +0 -141
- package/dist/src/doctor.js +0 -380
- package/dist/src/engine/ann-hnsw.js +0 -271
- package/dist/src/engine/embeddings.js +0 -193
- package/dist/src/engine/file-walker.js +0 -43
- package/dist/src/engine/git-history.js +0 -289
- package/dist/src/engine/index.js +0 -13377
- package/dist/src/engine/perf.js +0 -115
- package/dist/src/engine/prune.js +0 -112
- package/dist/src/engine/scip-import.js +0 -408
- package/dist/src/engine/source-policy.js +0 -69
- package/dist/src/engine/sqlite.js +0 -71
- package/dist/src/engine/symbol-delete.js +0 -58
- package/dist/src/execution-profile.js +0 -203
- package/dist/src/failure-diagnosis.js +0 -649
- package/dist/src/fleet.js +0 -7
- package/dist/src/git-executable.js +0 -31
- package/dist/src/graph-query-health.js +0 -115
- package/dist/src/hook-manager-integration.js +0 -156
- package/dist/src/index-activity.js +0 -125
- package/dist/src/init-progress.js +0 -155
- package/dist/src/init.js +0 -1254
- package/dist/src/lifecycle-health.js +0 -250
- package/dist/src/lsp-readonly.js +0 -217
- package/dist/src/output-compression.js +0 -629
- package/dist/src/output-telemetry.js +0 -363
- package/dist/src/pr-triage.js +0 -638
- package/dist/src/progressive-evidence.js +0 -473
- package/dist/src/pure-compression-cli.js +0 -101
- package/dist/src/relationship-adapters.js +0 -370
- package/dist/src/release-attestation.js +0 -533
- package/dist/src/release-preflight.js +0 -510
- package/dist/src/repair-progress.js +0 -262
- package/dist/src/repository-init-process.js +0 -173
- package/dist/src/repository-management.js +0 -1231
- package/dist/src/response-budget.js +0 -194
- package/dist/src/server.js +0 -73
- package/dist/src/structural-fast-path.js +0 -338
- package/dist/src/structural-snapshot.js +0 -33
- package/dist/src/system-config.js +0 -615
- package/dist/src/terminal-help.js +0 -83
- package/dist/src/tools/knodin-tools.js +0 -1529
- package/dist/src/update-ceremony.js +0 -158
- package/dist/src/update-policy.js +0 -944
- package/dist/src/update-trust.js +0 -503
- package/dist/src/version.js +0 -13
- package/dist/src/visualization.js +0 -162
- package/dist/src/wait-for-fresh.js +0 -98
- package/dist/src/worktree-lifecycle.js +0 -231
- package/docs/CONTAINED-EXECUTION.md +0 -77
- package/docs/DIAGNOSTICS.md +0 -45
- package/docs/GIT-HISTORY-REVIEW.md +0 -39
- package/docs/PROGRESSIVE-EVIDENCE.md +0 -37
- package/docs/PT-ACCESS-RECOMMENDATION.md +0 -91
- package/docs/RELEASE-0.3-EVIDENCE.md +0 -73
- package/docs/SCIP-IMPORT.md +0 -57
- package/docs/SIGNED-UPDATES.md +0 -151
- package/docs/TELEMETRY.md +0 -46
- package/docs/assets/knodin-favicon.svg +0 -4
- package/docs/releases/0.3.0.md +0 -46
- package/docs/releases/0.4.0.md +0 -68
- package/docs/releases/0.4.1.md +0 -28
- package/docs/releases/0.4.2.md +0 -27
- package/docs/releases/0.4.3.md +0 -23
- package/docs/releases/0.5.0.md +0 -29
- package/docs/releases/0.5.1.md +0 -17
- package/docs/releases/0.6.0.md +0 -18
- package/docs/releases/0.7.0.md +0 -24
- package/docs/releases/0.7.1.md +0 -21
- package/docs/releases/0.7.2.md +0 -21
- package/docs/releases/0.7.3.md +0 -23
- package/docs/releases/0.7.4.md +0 -17
- package/schemas/release-attestation-v1.schema.json +0 -210
package/dist/src/engine/perf.js
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/** Opt-in phase timing used by the R43 benchmark harness. */
|
|
2
|
-
export const PERF_PHASES = [
|
|
3
|
-
"db_open_migration",
|
|
4
|
-
"file_collection",
|
|
5
|
-
"parser_wasm_init",
|
|
6
|
-
"read",
|
|
7
|
-
"parse_extract",
|
|
8
|
-
"sqlite_write",
|
|
9
|
-
"model_load",
|
|
10
|
-
"embedding_inference_write",
|
|
11
|
-
"search_row_fetch_decode",
|
|
12
|
-
"similarity",
|
|
13
|
-
"fts",
|
|
14
|
-
"map_community",
|
|
15
|
-
"fusion",
|
|
16
|
-
"freshness_probe",
|
|
17
|
-
"graph_analytics",
|
|
18
|
-
"traversal_snapshot",
|
|
19
|
-
"architecture_facets",
|
|
20
|
-
"architecture_edge_materialization",
|
|
21
|
-
"context_composition",
|
|
22
|
-
"status_audit",
|
|
23
|
-
"pack_walk",
|
|
24
|
-
"pack_serialize",
|
|
25
|
-
"artifact_read",
|
|
26
|
-
"artifact_grep",
|
|
27
|
-
"review_diff_discovery",
|
|
28
|
-
"review_churn",
|
|
29
|
-
"review_parsing",
|
|
30
|
-
"review_flow_lookup",
|
|
31
|
-
"review_serialization",
|
|
32
|
-
];
|
|
33
|
-
let active = null;
|
|
34
|
-
function emptyPhases() {
|
|
35
|
-
return Object.fromEntries(PERF_PHASES.map((phase) => [phase, 0]));
|
|
36
|
-
}
|
|
37
|
-
function peakRss() {
|
|
38
|
-
try {
|
|
39
|
-
const kilobytes = process.resourceUsage().maxRSS;
|
|
40
|
-
return Number.isFinite(kilobytes) && kilobytes >= 0 ? kilobytes * 1024 : undefined;
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
/** Start a measurement explicitly. Starting while active resets that measurement. */
|
|
47
|
-
export function beginPerfSession() {
|
|
48
|
-
active = { startedAt: performance.now(), phases: emptyPhases(), peakRssBytes: peakRss() };
|
|
49
|
-
}
|
|
50
|
-
/** Finish the active measurement, or return null when instrumentation is disabled. */
|
|
51
|
-
export function endPerfSession() {
|
|
52
|
-
const session = active;
|
|
53
|
-
if (!session)
|
|
54
|
-
return null;
|
|
55
|
-
active = null;
|
|
56
|
-
const currentRss = peakRss();
|
|
57
|
-
if (currentRss !== undefined) {
|
|
58
|
-
session.peakRssBytes = Math.max(session.peakRssBytes ?? 0, currentRss);
|
|
59
|
-
}
|
|
60
|
-
return {
|
|
61
|
-
phases: { ...session.phases },
|
|
62
|
-
totalMs: Math.max(0, performance.now() - session.startedAt),
|
|
63
|
-
...(session.peakRssBytes === undefined ? {} : { peakRssBytes: session.peakRssBytes }),
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
function record(phase, startedAt) {
|
|
67
|
-
const session = active;
|
|
68
|
-
if (!session)
|
|
69
|
-
return;
|
|
70
|
-
session.phases[phase] += Math.max(0, performance.now() - startedAt);
|
|
71
|
-
const currentRss = peakRss();
|
|
72
|
-
if (currentRss !== undefined)
|
|
73
|
-
session.peakRssBytes = Math.max(session.peakRssBytes ?? 0, currentRss);
|
|
74
|
-
}
|
|
75
|
-
/** Start a phase that spans an existing try/finally block. The returned closer is idempotent. */
|
|
76
|
-
export function beginPerfPhase(phase) {
|
|
77
|
-
if (!active)
|
|
78
|
-
return () => undefined;
|
|
79
|
-
const startedAt = performance.now();
|
|
80
|
-
let closed = false;
|
|
81
|
-
return () => {
|
|
82
|
-
if (closed)
|
|
83
|
-
return;
|
|
84
|
-
closed = true;
|
|
85
|
-
record(phase, startedAt);
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
/** Time an async phase. Nested phases are retained independently and errors still close timers. */
|
|
89
|
-
export async function measurePerfPhase(phase, work) {
|
|
90
|
-
if (!active)
|
|
91
|
-
return work();
|
|
92
|
-
const startedAt = performance.now();
|
|
93
|
-
try {
|
|
94
|
-
return await work();
|
|
95
|
-
}
|
|
96
|
-
finally {
|
|
97
|
-
record(phase, startedAt);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
/** Synchronous counterpart for filesystem, parsing, and SQLite calls. */
|
|
101
|
-
export function measurePerfPhaseSync(phase, work) {
|
|
102
|
-
if (!active)
|
|
103
|
-
return work();
|
|
104
|
-
const startedAt = performance.now();
|
|
105
|
-
try {
|
|
106
|
-
return work();
|
|
107
|
-
}
|
|
108
|
-
finally {
|
|
109
|
-
record(phase, startedAt);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
/** Test-only state reset; deliberately does not enable timing. */
|
|
113
|
-
export function resetPerfForTests() {
|
|
114
|
-
active = null;
|
|
115
|
-
}
|
package/dist/src/engine/prune.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
/**
|
|
3
|
-
* Single source of truth for "which directories does knodin ignore" (R14).
|
|
4
|
-
*
|
|
5
|
-
* Before this module, the same intent — skip node_modules, .git, .knodin,
|
|
6
|
-
* build output, caches, VCS internals — was encoded THREE separate times:
|
|
7
|
-
* 1. src/__tests__/setup.ts's `PRUNE_DIRS` regex (glob-shim exclude filter)
|
|
8
|
-
* 2. the indexer's own directory filter (`EXCLUDED_DIR_SEGMENTS` / `isIndexablePath`)
|
|
9
|
-
* 3. the chokidar watcher's `makeWatchIgnorePredicate` (added by R13)
|
|
10
|
-
*
|
|
11
|
-
* They drifted. R13's bug was exactly this shape: the watcher's copy silently
|
|
12
|
-
* stopped matching anything for an entire chokidar major version while the
|
|
13
|
-
* indexer's copy kept working, so indexing looked correct and only the
|
|
14
|
-
* watcher melted (it registered ~12.6k files and starved the event loop under
|
|
15
|
-
* Bun). This module is the ONE list; every consumer below is a thin adapter
|
|
16
|
-
* over it, so a future drift is structurally impossible rather than merely
|
|
17
|
-
* discouraged.
|
|
18
|
-
*/
|
|
19
|
-
/**
|
|
20
|
-
* Directory segments never worth indexing or watching: build output, deps,
|
|
21
|
-
* VCS/tool state, caches. Matched by whole path SEGMENT, never substring, so
|
|
22
|
-
* a real file/dir like `distributor.ts` or `build-graph.ts` is never wrongly
|
|
23
|
-
* pruned.
|
|
24
|
-
*/
|
|
25
|
-
export const PRUNE_DIR_NAMES = new Set([
|
|
26
|
-
"node_modules",
|
|
27
|
-
".git",
|
|
28
|
-
".knodin",
|
|
29
|
-
// Local state maintained by other code-intelligence tools. It is neither
|
|
30
|
-
// repository source nor stable project configuration, and indexing it makes
|
|
31
|
-
// a healthy checkout look perpetually stale as those tools update databases
|
|
32
|
-
// and caches in the background.
|
|
33
|
-
".gitnexus",
|
|
34
|
-
".codegraph",
|
|
35
|
-
".code-review-graph",
|
|
36
|
-
".aider.tags.cache.v4",
|
|
37
|
-
"dist",
|
|
38
|
-
"build",
|
|
39
|
-
"coverage",
|
|
40
|
-
".next",
|
|
41
|
-
".turbo",
|
|
42
|
-
".svelte-kit",
|
|
43
|
-
"vendor",
|
|
44
|
-
"__pycache__",
|
|
45
|
-
]);
|
|
46
|
-
/** True when a single path segment (one directory name) is a pruned directory. */
|
|
47
|
-
export function isPrunedSegment(segment) {
|
|
48
|
-
return PRUNE_DIR_NAMES.has(segment);
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Agent worktrees are checkouts of this same repo living under
|
|
52
|
-
* `.claude/worktrees/*`; indexing/watching them would double-count every
|
|
53
|
-
* symbol. `segments` is the full split relative path; `i` is the segment
|
|
54
|
-
* index currently being tested against the two-segment `.claude/worktrees`
|
|
55
|
-
* pattern.
|
|
56
|
-
*/
|
|
57
|
-
function isClaudeWorktreeSegment(segments, i) {
|
|
58
|
-
return segments[i] === ".claude" && segments[i + 1] === "worktrees";
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* True when ANY segment of an already-split repo-relative path trips the
|
|
62
|
-
* ignore policy (a pruned directory name, or the `.claude/worktrees` root).
|
|
63
|
-
* This is the one walk both `isIndexablePath` (indexer) and
|
|
64
|
-
* `makeWatchIgnorePredicate` (watcher) run — the code that used to be
|
|
65
|
-
* duplicated between them.
|
|
66
|
-
*/
|
|
67
|
-
export function pathSegmentsAreIgnored(segments) {
|
|
68
|
-
for (let i = 0; i < segments.length; i++) {
|
|
69
|
-
if (isPrunedSegment(segments[i]))
|
|
70
|
-
return true;
|
|
71
|
-
if (isClaudeWorktreeSegment(segments, i))
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Indexer adapter: true when a repo-relative path (either OS separator)
|
|
78
|
-
* should be indexed.
|
|
79
|
-
*/
|
|
80
|
-
export function isIndexablePath(relPath) {
|
|
81
|
-
return !pathSegmentsAreIgnored(relPath.split(/[/\\]/));
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Watcher adapter: builds chokidar's `ignored` predicate for a given repo
|
|
85
|
-
* root.
|
|
86
|
-
*
|
|
87
|
-
* chokidar v4 removed glob support; v5 accepts only a function, a regex, or a
|
|
88
|
-
* literal path. Matching here is done on the path RELATIVE to the repo, so a
|
|
89
|
-
* repo that happens to live under a directory named e.g. `build/` does not
|
|
90
|
-
* prune its own root (the R13 fix this preserves).
|
|
91
|
-
*/
|
|
92
|
-
export function makeWatchIgnorePredicate(repoPath) {
|
|
93
|
-
return (targetPath) => {
|
|
94
|
-
const relative = path.relative(repoPath, targetPath);
|
|
95
|
-
// The watch root itself ("") and anything outside it are never pruned.
|
|
96
|
-
if (relative === "" || relative.startsWith(".."))
|
|
97
|
-
return false;
|
|
98
|
-
return pathSegmentsAreIgnored(relative.split(path.sep));
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Policy-test adapter: a regex equivalent to the segment-based policy above.
|
|
103
|
-
* Anchored on `/` boundaries so it only matches whole segments — same guarantee
|
|
104
|
-
* as `isPrunedSegment` — never a substring inside an unrelated name.
|
|
105
|
-
*/
|
|
106
|
-
export function makePruneDirsRegex() {
|
|
107
|
-
const escaped = [...PRUNE_DIR_NAMES].map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
108
|
-
// `.claude/worktrees` is a two-segment pattern, expressed directly rather
|
|
109
|
-
// than through the single-segment name list.
|
|
110
|
-
escaped.push("\\.claude/worktrees");
|
|
111
|
-
return new RegExp(`(?:^|/)(?:${escaped.join("|")})(?:/|$)`);
|
|
112
|
-
}
|
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
export const SCIP_DEFAULT_LIMITS = {
|
|
4
|
-
maxBytes: 64 * 1024 * 1024,
|
|
5
|
-
maxFiles: 10_000,
|
|
6
|
-
maxFacts: 250_000,
|
|
7
|
-
timeoutMs: 30_000,
|
|
8
|
-
};
|
|
9
|
-
const textDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
10
|
-
let activeDeadline = Number.POSITIVE_INFINITY;
|
|
11
|
-
let activeTimeoutMs = 0;
|
|
12
|
-
function fail(message) {
|
|
13
|
-
activeDeadline = Number.POSITIVE_INFINITY;
|
|
14
|
-
activeTimeoutMs = 0;
|
|
15
|
-
throw new Error(`knodin SCIP import: ${message}`);
|
|
16
|
-
}
|
|
17
|
-
function checkDeadline() {
|
|
18
|
-
if (performance.now() > activeDeadline)
|
|
19
|
-
fail(`import exceeded ${activeTimeoutMs} ms limit`);
|
|
20
|
-
}
|
|
21
|
-
function readVarint(bytes, offset) {
|
|
22
|
-
let value = 0;
|
|
23
|
-
let shift = 0;
|
|
24
|
-
for (let count = 0; count < 10 && offset < bytes.length; count++) {
|
|
25
|
-
const byte = bytes[offset++];
|
|
26
|
-
if (shift < 53)
|
|
27
|
-
value += (byte & 0x7f) * 2 ** shift;
|
|
28
|
-
if ((byte & 0x80) === 0)
|
|
29
|
-
return [value, offset];
|
|
30
|
-
shift += 7;
|
|
31
|
-
}
|
|
32
|
-
return fail("malformed protobuf varint");
|
|
33
|
-
}
|
|
34
|
-
function visitFields(bytes, visitor) {
|
|
35
|
-
let offset = 0;
|
|
36
|
-
while (offset < bytes.length) {
|
|
37
|
-
checkDeadline();
|
|
38
|
-
let tag;
|
|
39
|
-
[tag, offset] = readVarint(bytes, offset);
|
|
40
|
-
const number = Math.floor(tag / 8);
|
|
41
|
-
const wire = tag & 7;
|
|
42
|
-
if (number < 1)
|
|
43
|
-
fail("malformed protobuf field number");
|
|
44
|
-
if (wire === 0) {
|
|
45
|
-
let value;
|
|
46
|
-
[value, offset] = readVarint(bytes, offset);
|
|
47
|
-
visitor(number, wire, value);
|
|
48
|
-
}
|
|
49
|
-
else if (wire === 2) {
|
|
50
|
-
let length;
|
|
51
|
-
[length, offset] = readVarint(bytes, offset);
|
|
52
|
-
if (!Number.isSafeInteger(length) || length < 0 || offset + length > bytes.length)
|
|
53
|
-
fail("malformed protobuf length");
|
|
54
|
-
visitor(number, wire, bytes.subarray(offset, offset + length));
|
|
55
|
-
offset += length;
|
|
56
|
-
}
|
|
57
|
-
else if (wire === 1) {
|
|
58
|
-
offset += 8;
|
|
59
|
-
}
|
|
60
|
-
else if (wire === 5) {
|
|
61
|
-
offset += 4;
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
fail(`unsupported protobuf wire type ${wire}`);
|
|
65
|
-
}
|
|
66
|
-
if (offset > bytes.length)
|
|
67
|
-
fail("truncated protobuf field");
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
function countLengthDelimitedFields(bytes, target, maximum) {
|
|
71
|
-
let count = 0;
|
|
72
|
-
let offset = 0;
|
|
73
|
-
while (offset < bytes.length) {
|
|
74
|
-
checkDeadline();
|
|
75
|
-
let tag;
|
|
76
|
-
[tag, offset] = readVarint(bytes, offset);
|
|
77
|
-
const number = Math.floor(tag / 8);
|
|
78
|
-
const wire = tag & 7;
|
|
79
|
-
if (number < 1)
|
|
80
|
-
fail("malformed protobuf field number");
|
|
81
|
-
if (wire === 0) {
|
|
82
|
-
[, offset] = readVarint(bytes, offset);
|
|
83
|
-
}
|
|
84
|
-
else if (wire === 2) {
|
|
85
|
-
let length;
|
|
86
|
-
[length, offset] = readVarint(bytes, offset);
|
|
87
|
-
if (!Number.isSafeInteger(length) || length < 0 || offset + length > bytes.length)
|
|
88
|
-
fail("malformed protobuf length");
|
|
89
|
-
if (number === target && ++count > maximum)
|
|
90
|
-
return count;
|
|
91
|
-
offset += length;
|
|
92
|
-
}
|
|
93
|
-
else if (wire === 1)
|
|
94
|
-
offset += 8;
|
|
95
|
-
else if (wire === 5)
|
|
96
|
-
offset += 4;
|
|
97
|
-
else
|
|
98
|
-
fail(`unsupported protobuf wire type ${wire}`);
|
|
99
|
-
if (offset > bytes.length)
|
|
100
|
-
fail("truncated protobuf field");
|
|
101
|
-
}
|
|
102
|
-
return count;
|
|
103
|
-
}
|
|
104
|
-
function strings(message, number) {
|
|
105
|
-
const result = [];
|
|
106
|
-
visitFields(message, (fieldNumber, wire, value) => {
|
|
107
|
-
if (fieldNumber === number && wire === 2)
|
|
108
|
-
result.push(textDecoder.decode(value));
|
|
109
|
-
});
|
|
110
|
-
return result;
|
|
111
|
-
}
|
|
112
|
-
function messages(message, number) {
|
|
113
|
-
const result = [];
|
|
114
|
-
visitFields(message, (fieldNumber, wire, value) => {
|
|
115
|
-
if (fieldNumber === number && wire === 2)
|
|
116
|
-
result.push(value);
|
|
117
|
-
});
|
|
118
|
-
return result;
|
|
119
|
-
}
|
|
120
|
-
function numberField(message, number) {
|
|
121
|
-
let result = 0;
|
|
122
|
-
let found = false;
|
|
123
|
-
visitFields(message, (fieldNumber, wire, value) => {
|
|
124
|
-
if (!found && fieldNumber === number && wire === 0) {
|
|
125
|
-
result = value;
|
|
126
|
-
found = true;
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
return result;
|
|
130
|
-
}
|
|
131
|
-
function packedNumbers(message, number) {
|
|
132
|
-
const result = [];
|
|
133
|
-
visitFields(message, (fieldNumber, wire, value) => {
|
|
134
|
-
if (fieldNumber !== number)
|
|
135
|
-
return;
|
|
136
|
-
if (wire === 0)
|
|
137
|
-
result.push(value);
|
|
138
|
-
if (wire === 2) {
|
|
139
|
-
const bytes = value;
|
|
140
|
-
let offset = 0;
|
|
141
|
-
while (offset < bytes.length) {
|
|
142
|
-
let packedValue;
|
|
143
|
-
[packedValue, offset] = readVarint(bytes, offset);
|
|
144
|
-
result.push(packedValue);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
return result;
|
|
149
|
-
}
|
|
150
|
-
function parseRange(values) {
|
|
151
|
-
if (values.length !== 3 && values.length !== 4)
|
|
152
|
-
fail("range must contain three or four integers");
|
|
153
|
-
const [startLine, startCol] = values;
|
|
154
|
-
const endLine = values.length === 3 ? startLine : values[2];
|
|
155
|
-
const endCol = values.length === 3 ? values[2] : values[3];
|
|
156
|
-
if ([startLine, startCol, endLine, endCol].some((value) => !Number.isSafeInteger(value)))
|
|
157
|
-
fail("range contains an unsafe integer");
|
|
158
|
-
if (endLine < startLine || (endLine === startLine && endCol < startCol))
|
|
159
|
-
fail("range is reversed");
|
|
160
|
-
return { startLine, startCol, endLine, endCol };
|
|
161
|
-
}
|
|
162
|
-
function parseOccurrence(message) {
|
|
163
|
-
const singleLine = messages(message, 8)[0];
|
|
164
|
-
const multiLine = messages(message, 9)[0];
|
|
165
|
-
const symbol = strings(message, 2)[0] ?? "";
|
|
166
|
-
let range;
|
|
167
|
-
if (singleLine) {
|
|
168
|
-
const line = numberField(singleLine, 1);
|
|
169
|
-
range = parseRange([line, numberField(singleLine, 2), numberField(singleLine, 3)]);
|
|
170
|
-
}
|
|
171
|
-
else if (multiLine) {
|
|
172
|
-
range = parseRange([
|
|
173
|
-
numberField(multiLine, 1),
|
|
174
|
-
numberField(multiLine, 2),
|
|
175
|
-
numberField(multiLine, 3),
|
|
176
|
-
numberField(multiLine, 4),
|
|
177
|
-
]);
|
|
178
|
-
}
|
|
179
|
-
else {
|
|
180
|
-
range = parseRange(packedNumbers(message, 1));
|
|
181
|
-
}
|
|
182
|
-
if (!symbol)
|
|
183
|
-
fail("occurrence is missing its symbol");
|
|
184
|
-
return { range, symbol, roles: numberField(message, 3) };
|
|
185
|
-
}
|
|
186
|
-
function parseSymbolInfo(message) {
|
|
187
|
-
const symbol = strings(message, 1)[0] ?? "";
|
|
188
|
-
if (!symbol)
|
|
189
|
-
fail("symbol information is missing its symbol");
|
|
190
|
-
return {
|
|
191
|
-
symbol,
|
|
192
|
-
displayName: strings(message, 6)[0] ?? "",
|
|
193
|
-
kind: numberField(message, 5),
|
|
194
|
-
relationships: messages(message, 4).map((relationship) => ({
|
|
195
|
-
symbol: strings(relationship, 1)[0] ?? "",
|
|
196
|
-
isReference: numberField(relationship, 2) !== 0,
|
|
197
|
-
isImplementation: numberField(relationship, 3) !== 0,
|
|
198
|
-
isTypeDefinition: numberField(relationship, 4) !== 0,
|
|
199
|
-
isDefinition: numberField(relationship, 5) !== 0,
|
|
200
|
-
})),
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
function safeRelativePath(repoPath, value) {
|
|
204
|
-
if (!value || value.includes("\0") || path.isAbsolute(value))
|
|
205
|
-
fail(`hostile document path ${JSON.stringify(value)}`);
|
|
206
|
-
const normalized = value.replaceAll("\\", "/");
|
|
207
|
-
if (normalized.split("/").some((part) => part === ".." || part === ""))
|
|
208
|
-
fail(`hostile document path ${JSON.stringify(value)}`);
|
|
209
|
-
const absolute = path.resolve(repoPath, normalized);
|
|
210
|
-
const relative = path.relative(repoPath, absolute);
|
|
211
|
-
if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
212
|
-
fail(`document escapes repository: ${value}`);
|
|
213
|
-
let stat;
|
|
214
|
-
try {
|
|
215
|
-
stat = fs.lstatSync(absolute);
|
|
216
|
-
}
|
|
217
|
-
catch {
|
|
218
|
-
return fail(`document is missing: ${normalized}`);
|
|
219
|
-
}
|
|
220
|
-
if (!stat.isFile() || stat.isSymbolicLink())
|
|
221
|
-
fail(`document is not a regular repository file: ${normalized}`);
|
|
222
|
-
return normalized;
|
|
223
|
-
}
|
|
224
|
-
function symbolName(symbol, displayName) {
|
|
225
|
-
if (displayName.trim())
|
|
226
|
-
return displayName.trim();
|
|
227
|
-
if (symbol.startsWith("local "))
|
|
228
|
-
return symbol;
|
|
229
|
-
const descriptor = symbol.trim().split(/\s+/).at(-1) ?? symbol;
|
|
230
|
-
const withoutSuffix = descriptor.replace(/\([^)]*\)\.$|[#/.:!]$/, "");
|
|
231
|
-
const cleaned = (withoutSuffix.split(/[/#]/).at(-1) ?? descriptor).replaceAll("`", "");
|
|
232
|
-
return cleaned || symbol;
|
|
233
|
-
}
|
|
234
|
-
function symbolKind(_kind) {
|
|
235
|
-
// Do not translate a versioned producer enum into a potentially false native
|
|
236
|
-
// semantic kind; the original SCIP symbol remains in provenance.
|
|
237
|
-
return "SCIP_Symbol";
|
|
238
|
-
}
|
|
239
|
-
function parseMetadata(index) {
|
|
240
|
-
const metadata = messages(index, 1)[0];
|
|
241
|
-
if (!metadata)
|
|
242
|
-
return [];
|
|
243
|
-
const toolInfo = messages(metadata, 2)[0];
|
|
244
|
-
if (!toolInfo)
|
|
245
|
-
return [];
|
|
246
|
-
const name = strings(toolInfo, 1)[0];
|
|
247
|
-
const version = strings(toolInfo, 2)[0];
|
|
248
|
-
return name ? [`${name}${version ? `@${version}` : ""}`] : [];
|
|
249
|
-
}
|
|
250
|
-
export function readScipIndex(repoPath, options) {
|
|
251
|
-
const started = performance.now();
|
|
252
|
-
const limits = { ...SCIP_DEFAULT_LIMITS, ...options };
|
|
253
|
-
for (const [name, value] of Object.entries(limits)) {
|
|
254
|
-
if (name === "path")
|
|
255
|
-
continue;
|
|
256
|
-
if (!Number.isSafeInteger(value) || value < 1)
|
|
257
|
-
fail(`${name} must be a positive integer`);
|
|
258
|
-
}
|
|
259
|
-
activeTimeoutMs = limits.timeoutMs;
|
|
260
|
-
activeDeadline = started + limits.timeoutMs;
|
|
261
|
-
const resolvedRepo = path.resolve(repoPath);
|
|
262
|
-
const inputPath = path.resolve(resolvedRepo, options.path);
|
|
263
|
-
const inputRelative = path.relative(resolvedRepo, inputPath);
|
|
264
|
-
if (inputRelative.startsWith("..") || path.isAbsolute(inputRelative))
|
|
265
|
-
fail("input path escapes repository");
|
|
266
|
-
let inputStat;
|
|
267
|
-
try {
|
|
268
|
-
inputStat = fs.lstatSync(inputPath);
|
|
269
|
-
}
|
|
270
|
-
catch {
|
|
271
|
-
return fail(`input is missing: ${inputRelative}`);
|
|
272
|
-
}
|
|
273
|
-
if (!inputStat.isFile() || inputStat.isSymbolicLink())
|
|
274
|
-
fail("input must be a regular repository file");
|
|
275
|
-
if (inputStat.size > limits.maxBytes)
|
|
276
|
-
fail(`input exceeds ${limits.maxBytes} byte limit`);
|
|
277
|
-
const bytes = fs.readFileSync(inputPath);
|
|
278
|
-
const index = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
279
|
-
const documents = [];
|
|
280
|
-
let parsedFacts = 0;
|
|
281
|
-
const documentCount = countLengthDelimitedFields(index, 2, limits.maxFiles);
|
|
282
|
-
if (documentCount > limits.maxFiles)
|
|
283
|
-
fail(`index exceeds ${limits.maxFiles} file limit`);
|
|
284
|
-
for (const message of messages(index, 2)) {
|
|
285
|
-
const filePath = safeRelativePath(resolvedRepo, strings(message, 1)[0] ?? "");
|
|
286
|
-
const sourceStat = fs.statSync(path.join(resolvedRepo, filePath));
|
|
287
|
-
if (sourceStat.mtimeMs > inputStat.mtimeMs + 1)
|
|
288
|
-
fail(`input is stale for ${filePath}`);
|
|
289
|
-
const occurrenceCount = countLengthDelimitedFields(message, 2, limits.maxFacts - parsedFacts);
|
|
290
|
-
parsedFacts += occurrenceCount;
|
|
291
|
-
if (parsedFacts > limits.maxFacts)
|
|
292
|
-
fail(`index exceeds ${limits.maxFacts} fact limit`);
|
|
293
|
-
const symbolCount = countLengthDelimitedFields(message, 3, limits.maxFacts - parsedFacts);
|
|
294
|
-
parsedFacts += symbolCount;
|
|
295
|
-
if (parsedFacts > limits.maxFacts)
|
|
296
|
-
fail(`index exceeds ${limits.maxFacts} fact limit`);
|
|
297
|
-
const occurrenceMessages = messages(message, 2);
|
|
298
|
-
const symbolMessages = messages(message, 3);
|
|
299
|
-
for (const symbolMessage of symbolMessages) {
|
|
300
|
-
const remainingRelationships = Math.floor((limits.maxFacts - parsedFacts) / 4);
|
|
301
|
-
parsedFacts += countLengthDelimitedFields(symbolMessage, 4, remainingRelationships) * 4;
|
|
302
|
-
if (parsedFacts > limits.maxFacts)
|
|
303
|
-
fail(`index exceeds ${limits.maxFacts} fact limit`);
|
|
304
|
-
}
|
|
305
|
-
documents.push({
|
|
306
|
-
filePath,
|
|
307
|
-
language: strings(message, 4)[0] ?? "unknown",
|
|
308
|
-
occurrences: occurrenceMessages.map(parseOccurrence),
|
|
309
|
-
symbols: symbolMessages.map(parseSymbolInfo),
|
|
310
|
-
});
|
|
311
|
-
if (performance.now() - started > limits.timeoutMs)
|
|
312
|
-
fail(`import exceeded ${limits.timeoutMs} ms limit`);
|
|
313
|
-
}
|
|
314
|
-
const scopedSymbol = (document, symbol) => symbol.startsWith("local ") ? `${document.filePath}\0${symbol}` : `\0${symbol}`;
|
|
315
|
-
const definitions = new Map();
|
|
316
|
-
for (const document of documents) {
|
|
317
|
-
const infos = new Map(document.symbols.map((info) => [info.symbol, info]));
|
|
318
|
-
for (const occurrence of document.occurrences) {
|
|
319
|
-
const key = scopedSymbol(document, occurrence.symbol);
|
|
320
|
-
if ((occurrence.roles & 1) !== 0 && !definitions.has(key)) {
|
|
321
|
-
definitions.set(key, {
|
|
322
|
-
document,
|
|
323
|
-
occurrence,
|
|
324
|
-
info: infos.get(occurrence.symbol),
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
const symbols = [...definitions.values()].map((definition) => ({
|
|
330
|
-
symbol: definition.occurrence.symbol,
|
|
331
|
-
name: symbolName(definition.occurrence.symbol, definition.info?.displayName ?? ""),
|
|
332
|
-
kind: symbolKind(definition.info?.kind ?? 0),
|
|
333
|
-
filePath: definition.document.filePath,
|
|
334
|
-
startLine: definition.occurrence.range.startLine + 1,
|
|
335
|
-
endLine: definition.occurrence.range.endLine + 1,
|
|
336
|
-
startCol: definition.occurrence.range.startCol + 1,
|
|
337
|
-
endCol: definition.occurrence.range.endCol + 1,
|
|
338
|
-
}));
|
|
339
|
-
const references = [];
|
|
340
|
-
for (const document of documents) {
|
|
341
|
-
for (const occurrence of document.occurrences) {
|
|
342
|
-
if ((occurrence.roles & 1) !== 0)
|
|
343
|
-
continue;
|
|
344
|
-
const target = definitions.get(scopedSymbol(document, occurrence.symbol));
|
|
345
|
-
if (!target)
|
|
346
|
-
continue;
|
|
347
|
-
references.push({
|
|
348
|
-
callerSymbol: null,
|
|
349
|
-
callerFile: document.filePath,
|
|
350
|
-
calleeSymbol: symbolName(occurrence.symbol, target.info?.displayName ?? ""),
|
|
351
|
-
calleeFile: target.document.filePath,
|
|
352
|
-
line: occurrence.range.startLine + 1,
|
|
353
|
-
column: occurrence.range.startCol + 1,
|
|
354
|
-
kind: "scip_reference",
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
for (const info of document.symbols) {
|
|
358
|
-
const source = definitions.get(scopedSymbol(document, info.symbol));
|
|
359
|
-
if (!source)
|
|
360
|
-
continue;
|
|
361
|
-
for (const relationship of info.relationships) {
|
|
362
|
-
const target = definitions.get(scopedSymbol(document, relationship.symbol));
|
|
363
|
-
if (!target)
|
|
364
|
-
continue;
|
|
365
|
-
const kinds = [];
|
|
366
|
-
if (relationship.isReference)
|
|
367
|
-
kinds.push("scip_relationship_reference");
|
|
368
|
-
if (relationship.isImplementation)
|
|
369
|
-
kinds.push("scip_implementation");
|
|
370
|
-
if (relationship.isTypeDefinition)
|
|
371
|
-
kinds.push("scip_type_definition");
|
|
372
|
-
if (relationship.isDefinition)
|
|
373
|
-
kinds.push("scip_definition");
|
|
374
|
-
for (const kind of kinds)
|
|
375
|
-
references.push({
|
|
376
|
-
callerSymbol: symbolName(info.symbol, info.displayName),
|
|
377
|
-
callerFile: source.document.filePath,
|
|
378
|
-
calleeSymbol: symbolName(relationship.symbol, target.info?.displayName ?? ""),
|
|
379
|
-
calleeFile: target.document.filePath,
|
|
380
|
-
line: source.occurrence.range.startLine + 1,
|
|
381
|
-
column: source.occurrence.range.startCol + 1,
|
|
382
|
-
kind,
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
const factCount = symbols.length + references.length;
|
|
388
|
-
if (factCount > limits.maxFacts)
|
|
389
|
-
fail(`index exceeds ${limits.maxFacts} fact limit`);
|
|
390
|
-
if (performance.now() - started > limits.timeoutMs)
|
|
391
|
-
fail(`import exceeded ${limits.timeoutMs} ms limit`);
|
|
392
|
-
const result = {
|
|
393
|
-
inputPath: inputRelative.replaceAll("\\", "/"),
|
|
394
|
-
bytes: inputStat.size,
|
|
395
|
-
files: documents.length,
|
|
396
|
-
facts: factCount,
|
|
397
|
-
languages: [...new Set(documents.map((document) => document.language))].sort(),
|
|
398
|
-
indexers: parseMetadata(index).sort(),
|
|
399
|
-
symbols: symbols.sort((a, b) => a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine),
|
|
400
|
-
references: references.sort((a, b) => a.callerFile.localeCompare(b.callerFile) ||
|
|
401
|
-
a.line - b.line ||
|
|
402
|
-
a.calleeSymbol.localeCompare(b.calleeSymbol)),
|
|
403
|
-
elapsedMs: Math.round((performance.now() - started) * 1000) / 1000,
|
|
404
|
-
};
|
|
405
|
-
activeDeadline = Number.POSITIVE_INFINITY;
|
|
406
|
-
activeTimeoutMs = 0;
|
|
407
|
-
return result;
|
|
408
|
-
}
|