bantamkit-mcp 0.29.2 → 0.30.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 +1 -1
- package/assets/tools/memory_dream.json +20 -0
- package/assets/tools/repo_map.json +44 -0
- package/dist/eventlog.js +5 -4
- package/dist/index.d.ts +5 -3
- package/dist/index.js +2 -1
- package/dist/mcp/pyargs.d.ts +1 -1
- package/dist/mcp/pyargs.js +68 -0
- package/dist/mcp/server.d.ts +36 -6
- package/dist/mcp/server.js +152 -5
- package/dist/memory/cli.js +1 -1
- package/dist/memory/component.d.ts +73 -2
- package/dist/memory/component.js +157 -5
- package/dist/memory/dream.d.ts +247 -0
- package/dist/memory/dream.js +903 -0
- package/dist/memory/store.d.ts +45 -1
- package/dist/memory/store.js +107 -2
- package/dist/repomap.d.ts +247 -0
- package/dist/repomap.js +1040 -0
- package/package.json +1 -1
package/dist/memory/store.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare const VALID_TYPES: readonly ["feedback", "project", "reference",
|
|
|
15
15
|
*/
|
|
16
16
|
export declare const DURABLE_TYPES: readonly ["feedback", "user"];
|
|
17
17
|
export declare const DUPLICATE_JACCARD = 0.5;
|
|
18
|
+
export declare const RECALL_MIN_SCORE_RATIO = 0;
|
|
18
19
|
/** See the reference's comment: measured against a real store, not chosen. */
|
|
19
20
|
export declare const DEFAULT_INDEX_BUDGET = 24000;
|
|
20
21
|
export declare class MemoryValidationError extends BantamError {
|
|
@@ -55,6 +56,17 @@ export interface CompactResult {
|
|
|
55
56
|
headroom: number;
|
|
56
57
|
archiveDir: string;
|
|
57
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The store members `dream.ts` needs and no caller outside it should have. See
|
|
61
|
+
* `MemoryStore.internals`.
|
|
62
|
+
*/
|
|
63
|
+
export interface StoreInternals {
|
|
64
|
+
facts(): Fact[];
|
|
65
|
+
factPath(name: FactValue): string;
|
|
66
|
+
indexLine(fact: Fact): string;
|
|
67
|
+
writeFact(fact: Fact): void;
|
|
68
|
+
rebuildIndex(): void;
|
|
69
|
+
}
|
|
58
70
|
export interface MemoryStoreOptions {
|
|
59
71
|
indexBudget?: number;
|
|
60
72
|
k?: number;
|
|
@@ -186,8 +198,25 @@ export declare class MemoryStore {
|
|
|
186
198
|
* `[]` — the same answer as a real miss, which `component.Memory` used to turn into "no
|
|
187
199
|
* memories matched. Try different words", telling a person to rephrase a question at a
|
|
188
200
|
* filing cabinet nobody could open.
|
|
201
|
+
*
|
|
202
|
+
* `minRatio` is roadmap #6's precision gate: keep a fact only if its score is at least
|
|
203
|
+
* that fraction of the BEST score in this same recall. The default is
|
|
204
|
+
* `RECALL_MIN_SCORE_RATIO`, which is 0.0 and gates nothing — see the paragraph at that
|
|
205
|
+
* constant for why the number is a no-op today and what has to be measured before it
|
|
206
|
+
* stops being one. The comparison is relative to this store's own best because that is
|
|
207
|
+
* the only quantity in reach here that cancels query length; a layered `Memory` therefore
|
|
208
|
+
* applies the gate once per layer, against each layer's own top hit, and never across
|
|
209
|
+
* layers.
|
|
210
|
+
*
|
|
211
|
+
* THE RANGE CHECK RUNS BEFORE ANY FILE IS READ. A ratio outside `[0.0, 1.0]` is a
|
|
212
|
+
* caller's bug, and reporting it as "your store has no matches" would send someone to
|
|
213
|
+
* look at their memories for a defect that is in the argument. It is spelled
|
|
214
|
+
* `!(minRatio >= 0 && minRatio <= 1)` and NOT `(minRatio < 0 || minRatio > 1)`: every
|
|
215
|
+
* comparison against `NaN` is false, so the second spelling ADMITS `NaN` where the
|
|
216
|
+
* reference's `not 0.0 <= min_ratio <= 1.0` refuses it. That is a real mutant (M7b) and
|
|
217
|
+
* it is killed on both sides.
|
|
189
218
|
*/
|
|
190
|
-
recall(query: string, k?: number | null, stamp?: boolean): Fact[];
|
|
219
|
+
recall(query: string, k?: number | null, stamp?: boolean, minRatio?: number): Fact[];
|
|
191
220
|
/**
|
|
192
221
|
* `len(store._facts())` at the operator CLI, which is the only caller.
|
|
193
222
|
*
|
|
@@ -388,6 +417,21 @@ export declare class MemoryStore {
|
|
|
388
417
|
* Registered as a runtime-py defect, not fixed here (invariant 8).
|
|
389
418
|
*/
|
|
390
419
|
indexText(): string;
|
|
420
|
+
/**
|
|
421
|
+
* The `_`-prefixed members `dream.ts` reaches for, handed out as ONE named seam.
|
|
422
|
+
*
|
|
423
|
+
* The reference's `dream.py` calls `store._facts()`, `_fact_path`, `_index_line`,
|
|
424
|
+
* `_write_fact` and `_rebuild_index` directly — Python's underscore is a convention and
|
|
425
|
+
* costs nothing to cross. TypeScript's `private` is enforced, so the port needs a door,
|
|
426
|
+
* and this is deliberately one door rather than five relaxed modifiers: it is greppable,
|
|
427
|
+
* it names its one caller, and a sixth internal does not become reachable by accident.
|
|
428
|
+
*
|
|
429
|
+
* `_fact_text` has no member here because it never needed splitting out on this side —
|
|
430
|
+
* `formatFact` in `factfile.ts` has always been the serialiser `writeFact` calls, so the
|
|
431
|
+
* bytes `dream` MEASURES and the bytes the store WRITES are already one function. That is
|
|
432
|
+
* the code-shape difference the Python half had to manufacture, not a behaviour one.
|
|
433
|
+
*/
|
|
434
|
+
internals(): StoreInternals;
|
|
391
435
|
private factPath;
|
|
392
436
|
private indexLine;
|
|
393
437
|
/**
|
package/dist/memory/store.js
CHANGED
|
@@ -68,6 +68,55 @@ export const DURABLE_TYPES = ['feedback', 'user'];
|
|
|
68
68
|
const NAME_RE = /^[a-z0-9][a-z0-9-]*\n?$/;
|
|
69
69
|
const NAME_PATTERN = '^[a-z0-9][a-z0-9-]*$';
|
|
70
70
|
export const DUPLICATE_JACCARD = 0.5;
|
|
71
|
+
// ---- the precision gate (roadmap #6) ---------------------------------------------
|
|
72
|
+
//
|
|
73
|
+
// `recall` keeps a fact whose score is at least `minRatio` of the BEST score in the same
|
|
74
|
+
// recall. WHY THIS NUMBER IS 0.0, and what will replace it:
|
|
75
|
+
//
|
|
76
|
+
// 0.0 IS A DELIBERATE NO-OP, not a tuned value. `score >= 0.0 * best` is true for every
|
|
77
|
+
// fact `recall` was ever going to return — the scoring loop keeps only `score > 0` — so
|
|
78
|
+
// shipping this gate at its default changes not one recall, not one injected header, and
|
|
79
|
+
// not one byte of any reply. That is the whole point. The threshold that would actually
|
|
80
|
+
// cut something has to come from `tools/ledger/injection-precision.mjs`, and on the day
|
|
81
|
+
// that tool shipped (2026-09-06) it REFUSED to report a rate: 491 injection records, 3 of
|
|
82
|
+
// them carrying names+scores+session, 2 distinct sessions, and no control arm, against a
|
|
83
|
+
// floor of 100 joinable injections across 5 sessions. There is no retroactive baseline —
|
|
84
|
+
// the 488 older records carry `hits` and `bytes` and nothing joinable — so a number chosen
|
|
85
|
+
// today would be a number chosen off three rows, shipped as a silent suppressor of memory
|
|
86
|
+
// injection. The mechanism lands now; the number lands when that tool answers instead of
|
|
87
|
+
// refusing.
|
|
88
|
+
//
|
|
89
|
+
// WHY A RATIO AND NOT A COUNT. `score` is an unnormalised intersection size,
|
|
90
|
+
// `len(tokens(name + " " + description) & tokens(query))`, so it scales with how long the
|
|
91
|
+
// QUERY is. Measured on the three instrumented records: the same two-fact shape scored 2
|
|
92
|
+
// and 2 on a 452-character prompt, 4 and 4 on a 453-character prompt, and 22 and 21 on a
|
|
93
|
+
// 7855-character one. An absolute cut of, say, 5 would gate out both short prompts
|
|
94
|
+
// entirely and admit everything on the long one, which is a rule about prompt length
|
|
95
|
+
// wearing a relevance costume. Within ONE recall the query is fixed, so dividing by the
|
|
96
|
+
// best score in that same recall cancels the length term exactly. Jaccard was the other
|
|
97
|
+
// candidate and was rejected for the mirror-image bias: `|a & b| / |a | b|` puts the
|
|
98
|
+
// query's own token count in the denominator, so it would gate out LONG prompts instead.
|
|
99
|
+
//
|
|
100
|
+
// THE GATE IS RELATIVE, SO IT CAN NEVER EMPTY A RECALL. The top hit IS the max, so
|
|
101
|
+
// `best >= minRatio * best` holds for every ratio in range, 1.0 included. It narrows an
|
|
102
|
+
// injection; it never suppresses one, and the COUNT of prompts that get an injection is
|
|
103
|
+
// invariant at every setting.
|
|
104
|
+
//
|
|
105
|
+
// WHAT THE RATIO DOES NOT FIX: `tokens` is `re.findall(r"[a-z0-9]+", text.lower())`,
|
|
106
|
+
// ASCII-only. A wholly non-Latin prompt tokenises to the empty set and scores 0 against
|
|
107
|
+
// every fact, so it never reaches this gate at all — it is already an empty recall. This
|
|
108
|
+
// store's operator writes Thai; a threshold tuned on English prompts would be tuned on a
|
|
109
|
+
// population that structurally excludes theirs.
|
|
110
|
+
//
|
|
111
|
+
// The number lives in TWO places, one per runtime, and they must hold the SAME float:
|
|
112
|
+
// `runtime-py/src/bantamkit/memory/store.py`'s `RECALL_MIN_SCORE_RATIO` is the reference.
|
|
113
|
+
// `Memory.recallOutcome` and the `UserPromptSubmit` hook that calls it inherit it rather
|
|
114
|
+
// than restating it.
|
|
115
|
+
export const RECALL_MIN_SCORE_RATIO = 0.0;
|
|
116
|
+
// Spelled once because both runtimes raise it verbatim. The offending value is NOT
|
|
117
|
+
// interpolated: Python renders `2.0` as `2.0` and JavaScript renders it as `2`, so a
|
|
118
|
+
// sentence carrying the number would be a divergence manufactured by float formatting.
|
|
119
|
+
const MIN_RATIO_RANGE = 'recall min-score ratio must be between 0.0 and 1.0';
|
|
71
120
|
/** See the reference's comment: measured against a real store, not chosen. */
|
|
72
121
|
export const DEFAULT_INDEX_BUDGET = 24_000;
|
|
73
122
|
// The second half of the "unreadable" sentence, one per directory this store lists — and
|
|
@@ -494,11 +543,30 @@ export class MemoryStore {
|
|
|
494
543
|
* `[]` — the same answer as a real miss, which `component.Memory` used to turn into "no
|
|
495
544
|
* memories matched. Try different words", telling a person to rephrase a question at a
|
|
496
545
|
* filing cabinet nobody could open.
|
|
546
|
+
*
|
|
547
|
+
* `minRatio` is roadmap #6's precision gate: keep a fact only if its score is at least
|
|
548
|
+
* that fraction of the BEST score in this same recall. The default is
|
|
549
|
+
* `RECALL_MIN_SCORE_RATIO`, which is 0.0 and gates nothing — see the paragraph at that
|
|
550
|
+
* constant for why the number is a no-op today and what has to be measured before it
|
|
551
|
+
* stops being one. The comparison is relative to this store's own best because that is
|
|
552
|
+
* the only quantity in reach here that cancels query length; a layered `Memory` therefore
|
|
553
|
+
* applies the gate once per layer, against each layer's own top hit, and never across
|
|
554
|
+
* layers.
|
|
555
|
+
*
|
|
556
|
+
* THE RANGE CHECK RUNS BEFORE ANY FILE IS READ. A ratio outside `[0.0, 1.0]` is a
|
|
557
|
+
* caller's bug, and reporting it as "your store has no matches" would send someone to
|
|
558
|
+
* look at their memories for a defect that is in the argument. It is spelled
|
|
559
|
+
* `!(minRatio >= 0 && minRatio <= 1)` and NOT `(minRatio < 0 || minRatio > 1)`: every
|
|
560
|
+
* comparison against `NaN` is false, so the second spelling ADMITS `NaN` where the
|
|
561
|
+
* reference's `not 0.0 <= min_ratio <= 1.0` refuses it. That is a real mutant (M7b) and
|
|
562
|
+
* it is killed on both sides.
|
|
497
563
|
*/
|
|
498
|
-
recall(query, k = null, stamp = true) {
|
|
564
|
+
recall(query, k = null, stamp = true, minRatio = RECALL_MIN_SCORE_RATIO) {
|
|
565
|
+
if (!(minRatio >= 0 && minRatio <= 1))
|
|
566
|
+
throw new MemoryValidationError(MIN_RATIO_RANGE);
|
|
499
567
|
const limit = k ?? this.k;
|
|
500
568
|
const q = tokens(query);
|
|
501
|
-
|
|
569
|
+
let scored = [];
|
|
502
570
|
for (const fact of this.facts()) {
|
|
503
571
|
let score = 0;
|
|
504
572
|
for (const t of tokens(`${pyText(fact.name)} ${pyText(fact.description)}`))
|
|
@@ -507,6 +575,20 @@ export class MemoryStore {
|
|
|
507
575
|
if (score > 0)
|
|
508
576
|
scored.push({ score, name: fact.name, fact });
|
|
509
577
|
}
|
|
578
|
+
if (scored.length > 0) {
|
|
579
|
+
// No `if (minRatio > 0)` shortcut on purpose: at 0.0 this line still runs and still
|
|
580
|
+
// keeps everything, so "the default gates nothing" is a fact about the arithmetic
|
|
581
|
+
// rather than about a branch that could be edited away. `Math.max(...scored)` is NOT
|
|
582
|
+
// used: a spread is one argument per fact and a large store would overflow the
|
|
583
|
+
// argument limit, which is a crash the reference's `max(...)` over a generator does
|
|
584
|
+
// not have.
|
|
585
|
+
let best = scored[0].score;
|
|
586
|
+
for (const s of scored)
|
|
587
|
+
if (s.score > best)
|
|
588
|
+
best = s.score;
|
|
589
|
+
const floor = minRatio * best;
|
|
590
|
+
scored = scored.filter((s) => s.score >= floor);
|
|
591
|
+
}
|
|
510
592
|
// `key=lambda pair: (-pair[0], pair[1].name)`. The name comparison is CODEPOINT order
|
|
511
593
|
// for two strings — which is not what `Array.prototype.sort` does — and Python's `<`
|
|
512
594
|
// for everything else, refusals included. See `sortScored`.
|
|
@@ -839,6 +921,29 @@ export class MemoryStore {
|
|
|
839
921
|
indexText() {
|
|
840
922
|
return this.facts().map((fact) => this.indexLine(fact)).join('');
|
|
841
923
|
}
|
|
924
|
+
/**
|
|
925
|
+
* The `_`-prefixed members `dream.ts` reaches for, handed out as ONE named seam.
|
|
926
|
+
*
|
|
927
|
+
* The reference's `dream.py` calls `store._facts()`, `_fact_path`, `_index_line`,
|
|
928
|
+
* `_write_fact` and `_rebuild_index` directly — Python's underscore is a convention and
|
|
929
|
+
* costs nothing to cross. TypeScript's `private` is enforced, so the port needs a door,
|
|
930
|
+
* and this is deliberately one door rather than five relaxed modifiers: it is greppable,
|
|
931
|
+
* it names its one caller, and a sixth internal does not become reachable by accident.
|
|
932
|
+
*
|
|
933
|
+
* `_fact_text` has no member here because it never needed splitting out on this side —
|
|
934
|
+
* `formatFact` in `factfile.ts` has always been the serialiser `writeFact` calls, so the
|
|
935
|
+
* bytes `dream` MEASURES and the bytes the store WRITES are already one function. That is
|
|
936
|
+
* the code-shape difference the Python half had to manufacture, not a behaviour one.
|
|
937
|
+
*/
|
|
938
|
+
internals() {
|
|
939
|
+
return {
|
|
940
|
+
facts: () => this.facts(),
|
|
941
|
+
factPath: (name) => this.factPath(name),
|
|
942
|
+
indexLine: (fact) => this.indexLine(fact),
|
|
943
|
+
writeFact: (fact) => this.writeFact(fact),
|
|
944
|
+
rebuildIndex: () => this.rebuildIndex(),
|
|
945
|
+
};
|
|
946
|
+
}
|
|
842
947
|
// ---- internals ----
|
|
843
948
|
factPath(name) {
|
|
844
949
|
return pyJoin(this.root, 'facts', `${pyText(name)}.md`);
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
export declare const DAMPING = 0.2;
|
|
2
|
+
export declare const ITERATIONS = 30;
|
|
3
|
+
export declare const MIN_REFERENCE_LENGTH = 3;
|
|
4
|
+
export declare const REFERENCE_DF_MAX_NUM = 1;
|
|
5
|
+
export declare const REFERENCE_DF_MAX_DEN = 8;
|
|
6
|
+
export declare const MAX_DEFINITIONS_PER_FILE = 4;
|
|
7
|
+
export declare const DEFAULT_BUDGET = 4000;
|
|
8
|
+
export declare const SCORE_SCALE = 1000000000;
|
|
9
|
+
export declare const MAX_FILE_BYTES = 1048576;
|
|
10
|
+
export declare const SKIP_DIRS: readonly string[];
|
|
11
|
+
/**
|
|
12
|
+
* Suffix -> dialect. Two dialects only: `python` and `ecma`. TypeScript and JavaScript
|
|
13
|
+
* share a scanner because their DECLARATION forms are the same set; the extra TypeScript
|
|
14
|
+
* keywords (`interface`, `type`, `enum`) are simply absent from a `.js` file.
|
|
15
|
+
*/
|
|
16
|
+
export declare const LANGUAGES: Readonly<Record<string, string>>;
|
|
17
|
+
export declare const OMIT_UNKNOWN_LANGUAGE = "unknown-language";
|
|
18
|
+
export declare const OMIT_UNREADABLE = "unreadable-bytes";
|
|
19
|
+
export declare const OMIT_SIZE_CAP = "size-cap";
|
|
20
|
+
export declare const OMIT_NO_DEFINITIONS = "no-definitions";
|
|
21
|
+
export declare const OMIT_UNREACHABLE = "unreachable";
|
|
22
|
+
export declare const OMIT_PER_FILE_CAP = "per-file-cap";
|
|
23
|
+
export declare const OMIT_BUDGET = "budget";
|
|
24
|
+
/**
|
|
25
|
+
* Fixed render order for the footer, so two runs of the same map cannot disagree about the
|
|
26
|
+
* order of their disclosures.
|
|
27
|
+
*/
|
|
28
|
+
export declare const OMISSION_ORDER: readonly string[];
|
|
29
|
+
/**
|
|
30
|
+
* One definition, at the line it was declared on. `line` is 1-based and counts lines of the
|
|
31
|
+
* ORIGINAL file, not of the comment-stripped view: a caller that jumps to it must land
|
|
32
|
+
* where the reader would.
|
|
33
|
+
*/
|
|
34
|
+
export interface Definition {
|
|
35
|
+
readonly path: string;
|
|
36
|
+
readonly line: number;
|
|
37
|
+
readonly kind: string;
|
|
38
|
+
readonly name: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Something the tree holds that the map does not carry, as a COUNT and the fact behind it —
|
|
42
|
+
* `docread`'s `Omission` discipline, deliberately re-stated here rather than imported,
|
|
43
|
+
* because these subjects are this module's and a shared vocabulary would make a change to
|
|
44
|
+
* one reader a change to the other.
|
|
45
|
+
*
|
|
46
|
+
* Never an adjective. `subject` is one of the `OMIT_*` tokens; a renderer that meets a token
|
|
47
|
+
* it does not know must still print the count. A silent skip is how a reader lies about its
|
|
48
|
+
* coverage.
|
|
49
|
+
*/
|
|
50
|
+
export interface Omission {
|
|
51
|
+
readonly subject: string;
|
|
52
|
+
readonly count: number;
|
|
53
|
+
readonly size: number;
|
|
54
|
+
readonly facts: readonly (readonly [string, number])[];
|
|
55
|
+
}
|
|
56
|
+
export interface RankedFile {
|
|
57
|
+
readonly path: string;
|
|
58
|
+
readonly rankUnits: number;
|
|
59
|
+
readonly definitions: readonly Definition[];
|
|
60
|
+
/**
|
|
61
|
+
* The raw double. Reported for a caller that wants it; NEVER rendered, and never the sort
|
|
62
|
+
* key — see trap (8).
|
|
63
|
+
*/
|
|
64
|
+
readonly score: number;
|
|
65
|
+
}
|
|
66
|
+
export interface RepoMap {
|
|
67
|
+
readonly text: string;
|
|
68
|
+
readonly ranked: readonly RankedFile[];
|
|
69
|
+
readonly omissions: readonly Omission[];
|
|
70
|
+
readonly focus: readonly string[];
|
|
71
|
+
readonly nodes: number;
|
|
72
|
+
readonly edges: number;
|
|
73
|
+
readonly definitions: number;
|
|
74
|
+
readonly damping: number;
|
|
75
|
+
readonly iterations: number;
|
|
76
|
+
readonly budget: number;
|
|
77
|
+
readonly perFile: number;
|
|
78
|
+
readonly listingBytes: number;
|
|
79
|
+
readonly filesRendered: number;
|
|
80
|
+
readonly definitionsRendered: number;
|
|
81
|
+
}
|
|
82
|
+
export interface RepoMapOptions {
|
|
83
|
+
readonly focus?: readonly string[];
|
|
84
|
+
readonly budget?: number;
|
|
85
|
+
readonly files?: readonly string[] | null;
|
|
86
|
+
readonly damping?: number;
|
|
87
|
+
readonly iterations?: number;
|
|
88
|
+
readonly perFile?: number;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The dialect for a path, or `null` when this scanner has no reader for it.
|
|
92
|
+
*
|
|
93
|
+
* Dispatch is on SUFFIX, and that is a deliberate departure from `docread`, which sniffs
|
|
94
|
+
* because a suffix is measurably a lie about a document's container. A source file is
|
|
95
|
+
* different: `.py` and `.ts` name a GRAMMAR, and there is no magic number for a grammar. A
|
|
96
|
+
* file whose suffix is unknown is reported, never guessed at.
|
|
97
|
+
*/
|
|
98
|
+
export declare function languageOf(path: string): string | null;
|
|
99
|
+
/**
|
|
100
|
+
* The file's lines with comments and docstrings blanked, one output line per input line so
|
|
101
|
+
* line numbers survive.
|
|
102
|
+
*
|
|
103
|
+
* Line-oriented and stateful across lines, because both traps that matter span lines: a
|
|
104
|
+
* Python triple-quoted docstring and an ECMAScript block comment. What is removed:
|
|
105
|
+
*
|
|
106
|
+
* - `python` — `'''`/`"""` blocks (the delimiter that opens is the only one that can close),
|
|
107
|
+
* and everything from an unquoted `#` to end of line.
|
|
108
|
+
* - `ecma` — block comments, and everything from `//` to end of line.
|
|
109
|
+
*
|
|
110
|
+
* What is NOT removed, and is declared rather than hidden: the contents of a SINGLE-line
|
|
111
|
+
* string literal. Stripping those would lose the real references inside an f-string, and
|
|
112
|
+
* keeping them costs a definition-shaped line inside a one-line string being read as a
|
|
113
|
+
* definition. Backslash escapes are NOT honoured: a `"""` inside a string toggles the state
|
|
114
|
+
* machine. Both runtimes get that wrong identically, which is the requirement.
|
|
115
|
+
*/
|
|
116
|
+
export declare function stripNoncode(text: string, language: string): string[];
|
|
117
|
+
/**
|
|
118
|
+
* Every definition in `text`, in source order.
|
|
119
|
+
*
|
|
120
|
+
* `python` — `def` and `class` at ANY indentation, so a method counts. Methods are the
|
|
121
|
+
* signal that made the difference in measurement: a focus on `memory/dream.py` reaches
|
|
122
|
+
* `memory/store.py` through `_write_fact`, `_fact_path` and `_check_index_budget`, none of
|
|
123
|
+
* which is top-level. Plus a bare `NAME =` or `NAME: T =` at column 0, which is how this
|
|
124
|
+
* repository declares its constants.
|
|
125
|
+
*
|
|
126
|
+
* `ecma` — `function class interface type const let var enum` at COLUMN 0 only, optionally
|
|
127
|
+
* behind `export` and `default` and `async`. Restricting to column 0 was the second-largest
|
|
128
|
+
* measured improvement in this module (recall@10 0.361 -> 0.410 on its own): an indented
|
|
129
|
+
* `const` is a local, and `runtime-ts/src/docread.ts` was contributing 452 of them.
|
|
130
|
+
*
|
|
131
|
+
* The asymmetry is real and is not an oversight: an ECMAScript class member cannot be told
|
|
132
|
+
* from a call at the same indentation without tracking braces, and a scanner that guessed
|
|
133
|
+
* would invent definitions. Python's `def` keyword makes the same question free.
|
|
134
|
+
*/
|
|
135
|
+
export declare function scanDefinitions(text: string, language: string, path?: string): Definition[];
|
|
136
|
+
/**
|
|
137
|
+
* Every identifier of at least `MIN_REFERENCE_LENGTH` characters outside a comment.
|
|
138
|
+
*
|
|
139
|
+
* A SET, not a count. The edge weight below is "how many DISTINCT names of B does A
|
|
140
|
+
* mention", so a file that repeats one name a hundred times does not out-weigh a file that
|
|
141
|
+
* mentions ten. That is the choice the ambiguity filter and `MIN_REFERENCE_LENGTH` were
|
|
142
|
+
* tuned against and it keeps every weight an exact integer.
|
|
143
|
+
*/
|
|
144
|
+
export declare function referenceNames(text: string, language: string): Set<string>;
|
|
145
|
+
/**
|
|
146
|
+
* Every path under `root`, relative, POSIX, code-point sorted.
|
|
147
|
+
*
|
|
148
|
+
* DIRECTORY SYMLINKS ARE NOT FOLLOWED, AND ON THIS SIDE THAT IS REAL LOGIC RATHER THAN A
|
|
149
|
+
* FLAG. `os.walk(followlinks=False)` gives CPython the whole rule for free, and J45-9's
|
|
150
|
+
* mutation 24 — delete the explicit `islink` filter — SURVIVED there for exactly that
|
|
151
|
+
* reason. Node has no equivalent, and the Dirent that looks like the answer is the wrong
|
|
152
|
+
* one: `readdirSync(..., {withFileTypes: true})` reports from **lstat**, so a symlink to a
|
|
153
|
+
* directory answers `isDirectory() === false` and a naive port hands it to the scanner as a
|
|
154
|
+
* source FILE. CPython does the opposite — `os.scandir`'s `entry.is_dir()` FOLLOWS, so the
|
|
155
|
+
* link lands in `dirs`, and `followlinks=False` then declines to descend, so it appears in
|
|
156
|
+
* the output neither as a file nor as a directory.
|
|
157
|
+
*
|
|
158
|
+
* Measured on node v25.2.1 and CPython 3.12.13 over one tree holding `real/`,
|
|
159
|
+
* `linkdir -> real`, `linkfile.py -> plain.py` and a dangling `dangling.py`: CPython's walk
|
|
160
|
+
* yields `dirs=['linkdir', 'real'] files=['dangling.py', 'linkfile.py', 'plain.py']` and
|
|
161
|
+
* `walk_sources` returns `['dangling.py', 'linkfile.py', 'plain.py', 'real/a.py']`. So the
|
|
162
|
+
* three cases are separated by a FOLLOW-`stat`, not by the Dirent:
|
|
163
|
+
*
|
|
164
|
+
* - symlink whose target is a directory -> a directory, and not descended into: gone.
|
|
165
|
+
* - symlink whose target is a file -> a file: kept.
|
|
166
|
+
* - dangling symlink -> `stat` throws, CPython's `is_dir()` raises and
|
|
167
|
+
* is caught as False: a file, kept, and it will
|
|
168
|
+
* be counted as `unreadable-bytes` downstream.
|
|
169
|
+
*
|
|
170
|
+
* Two reasons the rule exists and both are measured elsewhere in this repository: a symlink
|
|
171
|
+
* cycle makes the walk non-terminating, and a dangling directory symlink is one error shape
|
|
172
|
+
* on POSIX and two on Windows (`reference-windows-dangling-symlink-two-shapes`).
|
|
173
|
+
*
|
|
174
|
+
* A directory whose listing throws is skipped whole, silently, because that is what
|
|
175
|
+
* `os.walk`'s default `onerror=None` does.
|
|
176
|
+
*/
|
|
177
|
+
export declare function walkSources(root: string): string[];
|
|
178
|
+
/**
|
|
179
|
+
* `source -> {target: weight}`, weight being the number of surviving names the source
|
|
180
|
+
* mentions that ONLY the target defines.
|
|
181
|
+
*
|
|
182
|
+
* Weights are exact integers, so `out[i]` in `pagerank` is an exact integer too and the only
|
|
183
|
+
* inexact step in the whole pass is the one division.
|
|
184
|
+
*/
|
|
185
|
+
export declare function buildGraph(definitions: Map<string, readonly Definition[]>, references: Map<string, ReadonlySet<string>>): Map<string, Map<string, number>>;
|
|
186
|
+
/**
|
|
187
|
+
* At most `limit` of a file's definitions, chosen by kind then reach, returned in SOURCE
|
|
188
|
+
* order.
|
|
189
|
+
*
|
|
190
|
+
* The key is `(kind rank, -reference count, line, name)`, and every part of it is an integer
|
|
191
|
+
* or a code-point comparison, so the choice is TOTAL — which is why this port does not lean
|
|
192
|
+
* on `Array.prototype.sort` being stable. `references` is the RAW count from `nameIndex`;
|
|
193
|
+
* see there for why it is not the filtered one.
|
|
194
|
+
*
|
|
195
|
+
* **This ordering is argued, not measured, and the difference matters.** Every other
|
|
196
|
+
* constant in this module beat a named alternative on a yardstick. This one cannot: the
|
|
197
|
+
* yardstick is "does the focus file's own import appear in the rendered map", which is a
|
|
198
|
+
* question about which FILES are listed, and selection only decides which lines appear UNDER
|
|
199
|
+
* a file already listed. Measured, it moves that number by zero. So it rests on a stated
|
|
200
|
+
* principle instead — name the file's subject before its details — and a later unit with a
|
|
201
|
+
* definition-level yardstick is entitled to overturn it. What it is NOT is source order,
|
|
202
|
+
* which put `VALID_TYPES`, `DURABLE_TYPES`, `NAME_RE` and `DUPLICATE_JACCARD` at the head of
|
|
203
|
+
* `memory/store.py`'s block.
|
|
204
|
+
*
|
|
205
|
+
* The selected definitions are re-sorted into SOURCE order for rendering: a block whose line
|
|
206
|
+
* numbers ran backwards would be harder to read for no gain, and the order a block is chosen
|
|
207
|
+
* in is not the order it is read in.
|
|
208
|
+
*/
|
|
209
|
+
export declare function selectDefinitions(definitions: readonly Definition[], references: Map<string, number>, limit: number): readonly Definition[];
|
|
210
|
+
/**
|
|
211
|
+
* Personalised PageRank, bit-identical to `repomap.py` by construction.
|
|
212
|
+
*
|
|
213
|
+
* The port contract, in the order the loop performs it:
|
|
214
|
+
*
|
|
215
|
+
* 1. `nodes` is already sorted; index by position in it.
|
|
216
|
+
* 2. `out[i]` is the exact integer sum of node i's out-weights.
|
|
217
|
+
* 3. Each node's out-edges are visited in sorted TARGET order.
|
|
218
|
+
* 4. The personalisation vector is `1/len(personal)` on each focus node and 0 elsewhere;
|
|
219
|
+
* with no focus it is `1/n` on every node. The INITIAL vector is that same vector.
|
|
220
|
+
* 5. Per round: zero `next`; walk sources in node order; a node with no out-edges adds its
|
|
221
|
+
* whole score to `dangling`; otherwise each out-edge adds `score[i] * weight / out[i]`,
|
|
222
|
+
* evaluated strictly left to right.
|
|
223
|
+
* 6. `tele = damping * dangling + (1.0 - damping)`, then
|
|
224
|
+
* `next[i] = next[i] * damping + tele * personal[i]`.
|
|
225
|
+
* 7. Exactly `iterations` rounds. No early stop, no convergence test — a branch on a float
|
|
226
|
+
* is one more thing for a port to get wrong, and the contraction bound (the linear part
|
|
227
|
+
* is `damping * column-stochastic`, so the residual after 30 rounds is under
|
|
228
|
+
* `2 * 0.2 ** 30` = 2.1e-21) makes the test unnecessary.
|
|
229
|
+
*
|
|
230
|
+
* THERE IS NO `reduce` HERE AND THERE MUST NOT BE ONE: every accumulation is a written-out
|
|
231
|
+
* loop in the same order as CPython's, because CPython's `sum()` is Neumaier-compensated and
|
|
232
|
+
* nothing in JavaScript is (trap 7). The Python side is held to the same rule by
|
|
233
|
+
* `test_no_float_summation_or_rounding_in_the_source`, which reads that function's body.
|
|
234
|
+
*/
|
|
235
|
+
export declare function pagerank(nodes: readonly string[], edges: Map<string, Map<string, number>>, personal: readonly string[], damping?: number, iterations?: number): number[];
|
|
236
|
+
/**
|
|
237
|
+
* Scan `root`, rank its files against `focus`, and render a listing within `budget`.
|
|
238
|
+
*
|
|
239
|
+
* `focus` is the current unit's files, relative to `root` and POSIX-separated; a focus entry
|
|
240
|
+
* that is not a scanned source is ignored for the personalisation vector but is still
|
|
241
|
+
* excluded from the listing, because a caller naming a file has it open already. An empty
|
|
242
|
+
* focus gives the uniform vector — plain centrality over the whole tree.
|
|
243
|
+
*
|
|
244
|
+
* `budget` is UTF-8 BYTES of the listing. See the header: this runtime has no model
|
|
245
|
+
* tokenizer and will not pretend to one.
|
|
246
|
+
*/
|
|
247
|
+
export declare function repoMap(root: string, options?: RepoMapOptions): RepoMap;
|