@polycode-projects/the-mechanical-code-talker 6.0.19 → 6.0.21
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 +20 -23
- package/bin/tmct.mjs +16 -33
- package/corpus/LICENSES.json +0 -21
- package/corpus/README.md +10 -13
- package/corpus/reference/manifest.json +19 -19
- package/corpus/reference/shards/ref-01.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-04.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-08.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-10.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-11.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-17.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-20.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-25.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-2c.jsonl.gz +0 -0
- package/corpus/tier2/generate.mjs +6 -142
- package/corpus/tier2/manifest.json +0 -42
- package/package.json +4 -4
- package/src/adapters/corpus/child-seed.mjs +74 -0
- package/src/adapters/corpus/conceptnet.mjs +45 -26
- package/src/adapters/memory/blocks.mjs +7 -1
- package/src/adapters/memory/core.mjs +453 -103
- package/src/adapters/memory/corpus-bands.mjs +33 -10
- package/src/adapters/memory/inspect.mjs +24 -5
- package/src/adapters/memory/rows.mjs +106 -9
- package/src/adapters/memory/shacl.mjs +10 -3
- package/src/domain/ask.mjs +27 -10
- package/src/domain/cli-verbs.mjs +3 -4
- package/src/domain/completions/group.mjs +8 -3
- package/src/domain/completions/infer.mjs +7 -2
- package/src/domain/completions/prune.mjs +5 -1
- package/src/domain/completions/rank.mjs +7 -2
- package/src/domain/digest/compose.mjs +5 -1
- package/src/domain/digest/select.mjs +12 -6
- package/src/domain/domain.mjs +15 -8
- package/src/domain/el-classify.mjs +11 -2
- package/src/domain/fact-phrase.mjs +86 -4
- package/src/domain/hash.mjs +9 -0
- package/src/domain/memory/bias.mjs +8 -4
- package/src/domain/memory/capability.mjs +12 -6
- package/src/domain/memory/fact-order.mjs +29 -0
- package/src/domain/memory/resolution.mjs +3 -0
- package/src/domain/news-feed.mjs +422 -56
- package/src/domain/reference-pack.mjs +5 -0
- package/src/domain/sense-scope.mjs +116 -0
- package/src/domain/sense-split.mjs +1 -1
- package/src/domain/syllogise.mjs +21 -13
- package/src/domain/tableau.mjs +23 -14
- package/src/domain/worlds-pack.mjs +5 -1
- package/src/services/adventure-autoplay.mjs +6 -1
- package/src/services/adventure-editor.mjs +43 -21
- package/src/services/adventure-viz.mjs +26 -9
- package/src/services/adventure.mjs +40 -10
- package/src/services/chat.mjs +253 -113
- package/src/services/extensions.mjs +51 -58
- package/src/services/extract-facts.mjs +670 -95
- package/src/services/init.mjs +4 -4
- package/src/services/ledger-viz.mjs +9 -4
- package/src/services/memory-panel-viz.mjs +4 -5
- package/src/services/mud-editor.mjs +40 -16
- package/src/services/mud-viz.mjs +8 -2
- package/src/services/mudiii-turn.mjs +5 -3
- package/src/services/mudiii-viz.mjs +8 -2
- package/src/services/news.mjs +277 -11
- package/src/services/research-viz.mjs +1 -1
- package/src/services/sprite-catalog-viz.mjs +10 -5
- package/src/surfaces/web/adventure-browser-entry.mjs +6 -12
- package/src/surfaces/web/memory-ask-browser.bundle.js +152 -151
- package/src/surfaces/web/mud-browser-entry.mjs +7 -11
- package/src/surfaces/web/research-browser-entry.mjs +5 -2
- package/corpus/tier2/aws.jsonl +0 -39
- package/corpus/tier2/java.jsonl +0 -31
- package/corpus/tier2/python.jsonl +0 -30
|
@@ -2,48 +2,6 @@
|
|
|
2
2
|
"version": 1,
|
|
3
3
|
"generated": "by corpus/tier2/generate.mjs",
|
|
4
4
|
"corpuses": [
|
|
5
|
-
{
|
|
6
|
-
"id": "aws",
|
|
7
|
-
"kind": "domain",
|
|
8
|
-
"description": "Amazon Web Services core services and primitives (S3, Lambda, DynamoDB, EC2, IAM, SQS) mapped to general cloud/CS concepts.",
|
|
9
|
-
"source": {
|
|
10
|
-
"kind": "curated",
|
|
11
|
-
"tool": "corpus/tier2/generate.mjs"
|
|
12
|
-
},
|
|
13
|
-
"file": "aws.jsonl",
|
|
14
|
-
"facts": 39,
|
|
15
|
-
"bytes": 4802,
|
|
16
|
-
"sha256": "7ab3e656bc4bc717be3c50d97af030e8592d6597cde182b15390a1dfaca9755a",
|
|
17
|
-
"license": "MPL-2.0"
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
"id": "python",
|
|
21
|
-
"kind": "language",
|
|
22
|
-
"description": "Python language constructs and stdlib types mapped to the shared CS concept vocabulary (list->array, dict->hash table, …).",
|
|
23
|
-
"source": {
|
|
24
|
-
"kind": "curated",
|
|
25
|
-
"tool": "corpus/tier2/generate.mjs"
|
|
26
|
-
},
|
|
27
|
-
"file": "python.jsonl",
|
|
28
|
-
"facts": 30,
|
|
29
|
-
"bytes": 3794,
|
|
30
|
-
"sha256": "009245d024357bc4285e967fba486e32349ccec8fbe2796ba13011cb2be4c19c",
|
|
31
|
-
"license": "MPL-2.0"
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"id": "java",
|
|
35
|
-
"kind": "language",
|
|
36
|
-
"description": "Java language and JVM constructs mapped to the shared CS concept vocabulary (ArrayList->list, HashMap->hash table, …).",
|
|
37
|
-
"source": {
|
|
38
|
-
"kind": "curated",
|
|
39
|
-
"tool": "corpus/tier2/generate.mjs"
|
|
40
|
-
},
|
|
41
|
-
"file": "java.jsonl",
|
|
42
|
-
"facts": 31,
|
|
43
|
-
"bytes": 3920,
|
|
44
|
-
"sha256": "d089426833c393f6f1574fe75c4ac94a1483ee80f72f79bd7fd32aaaa24a6e44",
|
|
45
|
-
"license": "MPL-2.0"
|
|
46
|
-
},
|
|
47
5
|
{
|
|
48
6
|
"id": "general",
|
|
49
7
|
"kind": "domain",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.21",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; indexes a repo on request (tmct index) or reads any producer's graph.",
|
|
@@ -140,14 +140,14 @@
|
|
|
140
140
|
"chat:repo": "node --disable-warning=ExperimentalWarning bin/tmct.mjs chat --repo",
|
|
141
141
|
"chat:plain": "node --disable-warning=ExperimentalWarning bin/tmct.mjs chat --plain",
|
|
142
142
|
"chat:narrate": "node --disable-warning=ExperimentalWarning bin/tmct.mjs chat --narrate",
|
|
143
|
-
"init": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus
|
|
143
|
+
"init": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus code && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus conceptnet",
|
|
144
144
|
"init:small": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init",
|
|
145
145
|
"init:sqlite": "npm run init:small",
|
|
146
146
|
"init:persona:human": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --with-persona human",
|
|
147
147
|
"init:persona:empty": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --with-persona empty",
|
|
148
148
|
"init:large": "npm run init",
|
|
149
|
-
"init:xl": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --persona-size large && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus
|
|
150
|
-
"init:xxl": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --persona-size large && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus
|
|
149
|
+
"init:xl": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --persona-size large && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus code && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus conceptnet && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus wordnet-xl && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus namenet && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus child",
|
|
150
|
+
"init:xxl": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --persona-size large && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus code && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus conceptnet && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus wordnet-full && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus namenet && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus child",
|
|
151
151
|
"memory": "node --disable-warning=ExperimentalWarning bin/tmct.mjs memory",
|
|
152
152
|
"memory:verbose": "node --disable-warning=ExperimentalWarning bin/tmct.mjs memory --verbose",
|
|
153
153
|
"syllogise": "node --disable-warning=ExperimentalWarning bin/tmct.mjs syllogise",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// corpus/child-seed.mjs — the BULK reader for the shipped CHILD triples pack
|
|
2
|
+
// (corpus/child/), used when the pack seeds a store as an ordinary corpus band.
|
|
3
|
+
// src/adapters/corpus/child-pack.mjs is the other half: one term at a time, on a
|
|
4
|
+
// miss, for the clean-miss cascade. This one walks every shard once.
|
|
5
|
+
//
|
|
6
|
+
// The shards already carry tmct-vocabulary triples ({subject, predicate,
|
|
7
|
+
// object}), so there is no conceptnet-map.toml step here — the rows come out of
|
|
8
|
+
// the pack build already mapped.
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { gunzipSync } from "node:zlib";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { CHILD_SHARD_COUNT, isChildFactsRow } from "../../domain/child-pack.mjs";
|
|
14
|
+
import { appendNewFacts, preferThenLimit } from "./conceptnet.mjs";
|
|
15
|
+
|
|
16
|
+
/** Every shard basename the pack can hold, in order — the naming contract
|
|
17
|
+
* shardNameFor writes, walked rather than listed, so this reader needs no
|
|
18
|
+
* directory listing and runs in a browser bundle. */
|
|
19
|
+
function shardBasenames() {
|
|
20
|
+
return Array.from(
|
|
21
|
+
{ length: CHILD_SHARD_COUNT },
|
|
22
|
+
(_, i) => `child-${i.toString(16).padStart(2, "0")}`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Every distinct triple in `packDir`'s shards, in shard then row order, as
|
|
27
|
+
* appendFact-shaped facts. A term appears in one shard as a subject and in
|
|
28
|
+
* another as an object, so the same edge arrives twice — the first spelling
|
|
29
|
+
* wins and the duplicate is dropped here, before the store ever sees it.
|
|
30
|
+
* Absent or malformed shards contribute nothing rather than throwing, the same
|
|
31
|
+
* tolerance the per-term loader gives. */
|
|
32
|
+
export function loadChildPackFacts(packDir, provenancePrefix = "corpus:child") {
|
|
33
|
+
const facts = [];
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
for (const shard of shardBasenames()) {
|
|
36
|
+
let body;
|
|
37
|
+
try {
|
|
38
|
+
body = gunzipSync(readFileSync(join(packDir, "shards", `${shard}.jsonl.gz`))).toString("utf8");
|
|
39
|
+
} catch {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
for (const line of body.split("\n")) {
|
|
43
|
+
if (!line.trim()) continue;
|
|
44
|
+
let row;
|
|
45
|
+
try {
|
|
46
|
+
row = JSON.parse(line);
|
|
47
|
+
} catch {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (!isChildFactsRow(row)) continue;
|
|
51
|
+
for (const f of row.facts) {
|
|
52
|
+
const key = `${f.subject} ${f.predicate} ${f.object}`;
|
|
53
|
+
if (seen.has(key)) continue;
|
|
54
|
+
seen.add(key);
|
|
55
|
+
facts.push({
|
|
56
|
+
subject: f.subject,
|
|
57
|
+
predicate: f.predicate,
|
|
58
|
+
object: f.object,
|
|
59
|
+
provenance: `${provenancePrefix} ${row.term}`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return facts;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Seed `packDir`'s triples into `dir`'s memory through the same idempotent
|
|
68
|
+
* append every other corpus band uses. `limit`/`prefer` behave exactly as they
|
|
69
|
+
* do for a slice-shaped band. Returns { appended, skipped, total }. */
|
|
70
|
+
export async function seedChildPack(dir, { packDir, provenancePrefix, limit, prefer } = {}) {
|
|
71
|
+
const facts = preferThenLimit(loadChildPackFacts(packDir, provenancePrefix), prefer, limit);
|
|
72
|
+
const { appended, skipped } = await appendNewFacts(dir, facts);
|
|
73
|
+
return { appended, skipped, total: facts.length };
|
|
74
|
+
}
|
|
@@ -25,7 +25,7 @@ export const MAP_FILE = join(PKG_ROOT, "src", "adapters", "corpus", "conceptnet-
|
|
|
25
25
|
// The tier-1 curated Software-Engineering ontology (SEON): concepts.jsonl shares
|
|
26
26
|
// ConceptNet's slice shape and loads through the same loadSlice/loadMap/toFacts path.
|
|
27
27
|
// definitions.jsonl is a separate {term, definition, sense} list for lexicon lookups.
|
|
28
|
-
// Tier-2 corpuses
|
|
28
|
+
// Tier-2 corpuses share the slice shape too. The data lives under
|
|
29
29
|
// corpus/domains/code/ (the code domain pack's own directory); the bundle name
|
|
30
30
|
// "seon" and its "corpus:seon" provenance prefix are unchanged by the move —
|
|
31
31
|
// both are declared literally in src/services/extensions.mjs, not derived from
|
|
@@ -127,35 +127,30 @@ export function toFacts(assertions, map, provenancePrefix = "corpus:conceptnet")
|
|
|
127
127
|
return facts;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
/**
|
|
131
|
-
* `limit`
|
|
132
|
-
*
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
* `provenancePrefix` tags facts (default "corpus:conceptnet").
|
|
136
|
-
*
|
|
137
|
-
* `captureUnknownContext` (default false): also runs unknown-ingest.mjs's
|
|
138
|
-
* ingestUnknownFromAssertions so a term that only appears in an `ace="none"` row (never
|
|
139
|
-
* reified as a Fact) still lands in memory, tagged with the passage it was found in.
|
|
140
|
-
* `unknownContextLimit` bounds how many distinct terms one call captures (default 500).
|
|
141
|
-
* Loaded dynamically to avoid a load-time import cycle with unknown-ingest.mjs. */
|
|
142
|
-
export async function seedMemory(dir, {
|
|
143
|
-
limit, slicePath = SLICE_FILE, mapPath = MAP_FILE, prefer, provenancePrefix,
|
|
144
|
-
captureUnknownContext = false, unknownContextLimit,
|
|
145
|
-
} = {}) {
|
|
146
|
-
const [assertions, map] = await Promise.all([loadSlice(slicePath), loadMap(mapPath)]);
|
|
147
|
-
let facts = toFacts(assertions, map, provenancePrefix);
|
|
130
|
+
/** Stable-partition `facts` so the `prefer` predicates come first, then take the
|
|
131
|
+
* first `limit` of them. A capped band therefore buys its definitional backbone
|
|
132
|
+
* before it buys trivia. Either argument may be absent. */
|
|
133
|
+
export function preferThenLimit(facts, prefer, limit) {
|
|
134
|
+
let out = facts;
|
|
148
135
|
if (Array.isArray(prefer) && prefer.length) {
|
|
149
136
|
const rank = new Map(prefer.map((p, i) => [p, i]));
|
|
150
|
-
|
|
137
|
+
out = out.slice().sort((a, b) => (rank.get(a.predicate) ?? prefer.length) - (rank.get(b.predicate) ?? prefer.length));
|
|
151
138
|
}
|
|
152
|
-
|
|
139
|
+
return limit === undefined ? out : out.slice(0, limit);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Keyed with normFactTerm so it matches the store's own normalized read-back.
|
|
143
|
+
const factKey = (s, p, o) => `${normFactTerm(s)} ${p} ${normFactTerm(o)}`;
|
|
153
144
|
|
|
154
|
-
|
|
155
|
-
|
|
145
|
+
/** Write every fact `dir`'s store does not already hold, in one batched append,
|
|
146
|
+
* so re-seeding a band is idempotent and a term shared by two bands converges
|
|
147
|
+
* to one fact. `memory` is the store's already-loaded contents (callers that
|
|
148
|
+
* need it for their own work pass theirs rather than paying a second read).
|
|
149
|
+
* Returns { appended, skipped }. */
|
|
150
|
+
export async function appendNewFacts(dir, facts, memory) {
|
|
151
|
+
const store = memory ?? await loadMemory(dir);
|
|
156
152
|
const existing = new Set();
|
|
157
|
-
const
|
|
158
|
-
for (const ind of memory.individuals || []) {
|
|
153
|
+
for (const ind of store.individuals || []) {
|
|
159
154
|
if (ind?.class !== "Fact") continue;
|
|
160
155
|
const get = (key) => (ind.attributes || []).find((x) => x.key === key)?.value;
|
|
161
156
|
existing.add(factKey(get("subject"), get("predicate"), get("object")));
|
|
@@ -173,6 +168,30 @@ export async function seedMemory(dir, {
|
|
|
173
168
|
toWrite.push(fact);
|
|
174
169
|
}
|
|
175
170
|
const res = await appendFacts(dir, toWrite);
|
|
171
|
+
return { appended: res.appended, skipped: skipped + res.skipped };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Seed a repo's memory graph (<dir>/.tmct/memory/graph.json) from the committed slice.
|
|
175
|
+
* `limit` caps the facts written; `prefer` stable-partitions facts by predicate (so a
|
|
176
|
+
* capped seed favors the definitional band over whatever trivia the slice opens with).
|
|
177
|
+
* Idempotent: pre-reads the store to skip triples already there, then writes survivors
|
|
178
|
+
* in one batched appendFacts call. Returns { appended, skipped, total }.
|
|
179
|
+
* `provenancePrefix` tags facts (default "corpus:conceptnet").
|
|
180
|
+
*
|
|
181
|
+
* `captureUnknownContext` (default false): also runs unknown-ingest.mjs's
|
|
182
|
+
* ingestUnknownFromAssertions so a term that only appears in an `ace="none"` row (never
|
|
183
|
+
* reified as a Fact) still lands in memory, tagged with the passage it was found in.
|
|
184
|
+
* `unknownContextLimit` bounds how many distinct terms one call captures (default 500).
|
|
185
|
+
* Loaded dynamically to avoid a load-time import cycle with unknown-ingest.mjs. */
|
|
186
|
+
export async function seedMemory(dir, {
|
|
187
|
+
limit, slicePath = SLICE_FILE, mapPath = MAP_FILE, prefer, provenancePrefix,
|
|
188
|
+
captureUnknownContext = false, unknownContextLimit,
|
|
189
|
+
} = {}) {
|
|
190
|
+
const [assertions, map] = await Promise.all([loadSlice(slicePath), loadMap(mapPath)]);
|
|
191
|
+
const facts = preferThenLimit(toFacts(assertions, map, provenancePrefix), prefer, limit);
|
|
192
|
+
|
|
193
|
+
const memory = await loadMemory(dir);
|
|
194
|
+
const { appended, skipped } = await appendNewFacts(dir, facts, memory);
|
|
176
195
|
|
|
177
196
|
let unknown;
|
|
178
197
|
if (captureUnknownContext) {
|
|
@@ -185,7 +204,7 @@ export async function seedMemory(dir, {
|
|
|
185
204
|
}
|
|
186
205
|
|
|
187
206
|
return {
|
|
188
|
-
appended
|
|
207
|
+
appended, skipped, total: facts.length,
|
|
189
208
|
...(unknown ? { unknown } : {}),
|
|
190
209
|
};
|
|
191
210
|
}
|
|
@@ -27,6 +27,12 @@ const MAX_TOKENS_PER_BLOCK = 800; // beyond tokenizeProse's per-doc cap: union o
|
|
|
27
27
|
|
|
28
28
|
const blocksDir = (dir) => join(dir, BLOCKS_DIR_REL);
|
|
29
29
|
|
|
30
|
+
// Codepoint order, never localeCompare — the block ids this settles ties on
|
|
31
|
+
// decide WHICH blocks retrieveBlocks hands back, so a locale-dependent
|
|
32
|
+
// comparison would let two readers retrieve different text from one store.
|
|
33
|
+
// src/domain/memory/fact-order.mjs states the rule for the fact rows.
|
|
34
|
+
const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
35
|
+
|
|
30
36
|
/** Only safe, filesystem-friendly block file names (ids are session uuids or
|
|
31
37
|
* corpus slugs; anything else is normalized, never trusted into a path). */
|
|
32
38
|
const safeName = (id) => String(id).replace(/[^A-Za-z0-9._-]+/g, "_").slice(0, 120) || "_";
|
|
@@ -216,7 +222,7 @@ export async function retrieveBlocks(dir, query, k = 3) {
|
|
|
216
222
|
id, score: (idfSum * (1 + rank) * trustFactor) / Math.sqrt(1 + degree), rank, trust, file: b.file,
|
|
217
223
|
});
|
|
218
224
|
}
|
|
219
|
-
scored.sort((a, b) => b.score - a.score || b.rank - a.rank || a.id
|
|
225
|
+
scored.sort((a, b) => b.score - a.score || b.rank - a.rank || byCodepoint(a.id, b.id));
|
|
220
226
|
const top = scored.slice(0, Math.max(1, k));
|
|
221
227
|
for (const hit of top) {
|
|
222
228
|
try { hit.text = await readFile(join(blocksDir(dir), hit.file), "utf8"); }
|