@polycode-projects/the-mechanical-code-talker 2.0.3 → 2.3.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/ROADMAP.md +27 -4
- package/bin/tmct.mjs +4 -5
- package/corpus/generated/ace-surface-variants.jsonl +1 -0
- package/corpus/generated/manifest.json +3 -3
- package/corpus/wordnet/generate.mjs +6 -7
- package/package.json +30 -2
- package/src/adapters/corpus/conceptnet.mjs +1 -1
- package/src/adapters/graph-build.mjs +3 -3
- package/src/adapters/memory/blocks.mjs +2 -2
- package/src/adapters/memory/core.mjs +5 -5
- package/src/adapters/providers/bootstrap.mjs +1 -1
- package/src/adapters/providers/fixture.mjs +1 -1
- package/src/adapters/toml-config.mjs +0 -1
- package/src/adapters/wink-model.mjs +1 -1
- package/src/adapters/wordnet-source.mjs +70 -0
- package/src/domain/answer-variants.json +1 -1
- package/src/domain/ask-vocab.mjs +2 -2
- package/src/domain/ask.mjs +4 -4
- package/src/domain/codegraph.mjs +7 -71
- package/src/domain/corpus-matrix.mjs +87 -0
- package/src/domain/grammar/ace.mjs +11 -11
- package/src/domain/grammar/lexicon.mjs +3 -3
- package/src/domain/inflect.mjs +67 -0
- package/src/domain/interpret/fuzzy.mjs +1 -1
- package/src/domain/interpret/merge.mjs +1 -1
- package/src/domain/interpret/normalize.mjs +1 -1
- package/src/domain/licences.mjs +68 -0
- package/src/domain/markdown-links.mjs +55 -0
- package/src/domain/memory/capability.mjs +1 -1
- package/src/domain/memory/trust.mjs +2 -2
- package/src/domain/persona/codegen.mjs +123 -0
- package/src/domain/persona/examples.mjs +26 -0
- package/src/domain/persona/tiers.mjs +270 -0
- package/src/domain/publish-gate.mjs +41 -0
- package/src/domain/router/call-validator.mjs +1 -1
- package/src/domain/router/drive.mjs +3 -4
- package/src/domain/router/registry.mjs +12 -13
- package/src/domain/router/resolver.mjs +18 -5
- package/src/domain/router/results.mjs +3 -3
- package/src/domain/router/taught.mjs +4 -3
- package/src/domain/schemaorg/turtle.mjs +25 -0
- package/src/domain/semcor/parse.mjs +87 -0
- package/src/domain/syllogise.mjs +6 -6
- package/src/domain/version-stamp.mjs +36 -0
- package/src/domain/wordnet/yaml.mjs +133 -0
- package/src/services/chat-session.mjs +2 -2
- package/src/services/chat.mjs +2 -2
- package/src/services/cli-args.mjs +4 -4
- package/src/services/finish.mjs +1 -1
- package/src/services/ledger-viz.mjs +2 -3
- package/src/services/sessions.mjs +4 -4
- package/src/services/viz-theme.mjs +3 -4
- package/src/surfaces/web/memory-ask-browser.bundle.js +4 -94
- package/src/adapters/embed.mjs +0 -169
- package/src/domain/router/guardrail.mjs +0 -116
- package/src/domain/vector.mjs +0 -12
package/README.md
CHANGED
|
@@ -180,7 +180,7 @@ resolves to a real graph traversal or declines honestly:
|
|
|
180
180
|
(*because/although/while*), conditionals, and false-premise flags ("why
|
|
181
181
|
does X still import Y" when it no longer does).
|
|
182
182
|
|
|
183
|
-
The full catalog with measured coverage lives in `
|
|
183
|
+
The full catalog with measured coverage lives in `CAPABILITIES_2.0.3.md` and
|
|
184
184
|
the `BENCHMARK_*.md` reports.
|
|
185
185
|
|
|
186
186
|
**Response finishing.** Before an answer prints, it is segmented into typed
|
package/ROADMAP.md
CHANGED
|
@@ -100,7 +100,30 @@ blending web-sourced facts with graph/operator facts.
|
|
|
100
100
|
|
|
101
101
|
## Design docs
|
|
102
102
|
|
|
103
|
-
Every substantial design lives in its own `PLAN_*.md` at the repo root
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
Every substantial design lives in its own `PLAN_*.md` at the repo root; `archive/` holds the shipped
|
|
104
|
+
and closed ones. This file points to them, it doesn't repeat their content. Each plan states its own
|
|
105
|
+
status in its opening lines — read it there, because a status quoted here would rot.
|
|
106
|
+
|
|
107
|
+
| Plan | What it's for |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| [PLAN_ADVENTURE.md](PLAN_ADVENTURE.md) | a text adventure as an architectural stretch: imperative command grammar, NPC turn scheduler, room-look digest |
|
|
110
|
+
| [PLAN_AGENTS.md](PLAN_AGENTS.md) | the governing plan for the multi-repo arc (marginalia, seonix, a pluggable LLM rung), with its own phase sequencing |
|
|
111
|
+
| [PLAN_CHILD_CORPUS.md](PLAN_CHILD_CORPUS.md) | a wider default seed corpus, chosen by age of acquisition |
|
|
112
|
+
| [PLAN_CLASS_QUERY.md](PLAN_CLASS_QUERY.md) | "list/count all X of class Y", reconciled against what already shipped |
|
|
113
|
+
| [PLAN_CODE.md](PLAN_CODE.md) | program synthesis over tmct's closed DSLs, plus JS/HTML/CSS fragments and goal-directed program repair |
|
|
114
|
+
| [PLAN_CONSISTENCY_CHECK.md](PLAN_CONSISTENCY_CHECK.md) | tmct as a consistency service for an LLM tool loop |
|
|
115
|
+
| [PLAN_EMBEDDINGS.md](PLAN_EMBEDDINGS.md) | the semantic-similarity axis, and the way back to it |
|
|
116
|
+
| [PLAN_GRAPH_SCAN.md](PLAN_GRAPH_SCAN.md) | seed and query cost at `init:xl`/`init:xxl` corpus scale |
|
|
117
|
+
| [PLAN_GUESS_NUMBER.md](PLAN_GUESS_NUMBER.md) | closed-loop planning over hidden state, via belief-interval bisection |
|
|
118
|
+
| [PLAN_MUD.md](PLAN_MUD.md) | persistent, shared tmct worlds over a `server:` memory backend |
|
|
119
|
+
| [PLAN_NLU_BENCHMARKS.md](PLAN_NLU_BENCHMARKS.md) | scoring tmct on the CLINC150 and HWU64 intent sets |
|
|
120
|
+
| [PLAN_OPEN_ITEMS.md](PLAN_OPEN_ITEMS.md) | the build order closing the backlog `HANDOVER.md` carries |
|
|
121
|
+
| [PLAN_PARAPHRASE_VERIFICATION.md](PLAN_PARAPHRASE_VERIFICATION.md) | checking a paraphrase against the graph before it prints |
|
|
122
|
+
| [PLAN_PURGE.md](PLAN_PURGE.md) | promote the load-bearing code, delete the dead weight |
|
|
123
|
+
| [PLAN_REPO_INDEX.md](PLAN_REPO_INDEX.md) | tmct grows its own code parsers, ported from seonix |
|
|
124
|
+
| [PLAN_SYLLOGIST.md](PLAN_SYLLOGIST.md) | the reasoning engine's incrementality and retraction horizon |
|
|
125
|
+
| [PLAN_SYLLOGIST_EL_DL.md](PLAN_SYLLOGIST_EL_DL.md) | beyond OWL 2 RL: an EL classifier, then a DL tableau prover |
|
|
126
|
+
|
|
127
|
+
`SKILL_*.md` docs specify the repeatable measurement and build cycles (the benchmarks, the capability
|
|
128
|
+
audit, the background strategy advisor, plain-prose writing). `HANDOVER.md` is the single
|
|
129
|
+
current-open-items list.
|
package/bin/tmct.mjs
CHANGED
|
@@ -167,7 +167,7 @@ const TIER_RANK = { NONE: 0, TINY: 1, MID: 2, LARGE: 3, FULL: 4 };
|
|
|
167
167
|
* into a caller's prompt.
|
|
168
168
|
*
|
|
169
169
|
* Two ways to say which modules: an explicit `modules` array (unchanged), or a `query` string —
|
|
170
|
-
* auto-locate + score-gap-select (
|
|
170
|
+
* auto-locate + score-gap-select (the shipped default) in one call, so a
|
|
171
171
|
* real caller no longer has to run `tmct_locate` and hand-pick a module themselves. `modules`
|
|
172
172
|
* wins if both are given. The header reports which modules were actually selected either way.
|
|
173
173
|
*
|
|
@@ -741,8 +741,7 @@ async function main() {
|
|
|
741
741
|
const lexiconVal = strFlag(rest, ["--lexicon"]);
|
|
742
742
|
const graphFlags = repeatedFlag(rest, ["--graph"]);
|
|
743
743
|
|
|
744
|
-
// `--memory-backend <default|memory|sqlite
|
|
745
|
-
// seam, now reachable from `tmct init`): validated BEFORE touching disk, same
|
|
744
|
+
// `--memory-backend <default|memory|sqlite>`: validated BEFORE touching disk, same
|
|
746
745
|
// discipline as every other pluggable input below. Written into tmct.toml's
|
|
747
746
|
// `[memory] backend` (src/services/init.mjs's renderTomlConfig); chat.mjs's
|
|
748
747
|
// createSession reads it back at CLI-flag > TMCT_MEMORY_BACKEND env >
|
|
@@ -798,7 +797,7 @@ async function main() {
|
|
|
798
797
|
personaPreset = PERSONA_PRESETS[personaName];
|
|
799
798
|
}
|
|
800
799
|
|
|
801
|
-
// `--persona-size <medium|large
|
|
800
|
+
// `--persona-size <medium|large>`: Small/Medium/Large are
|
|
802
801
|
// SIZES of the one `human` bundle, not separate corpus ids — human.jsonl
|
|
803
802
|
// (Small, the default) stays exactly as-is; human-medium.jsonl/
|
|
804
803
|
// human-large.jsonl hold ONLY the facts each size adds beyond the previous
|
|
@@ -1121,7 +1120,7 @@ async function main() {
|
|
|
1121
1120
|
if (mode === "viz") {
|
|
1122
1121
|
// `tmct viz` — the ledger explorer: one self-contained HTML page rendering
|
|
1123
1122
|
// the memory graph as readable fact-sentences around a focus term, with
|
|
1124
|
-
// the in-browser chat dock
|
|
1123
|
+
// the in-browser chat dock. Same repo resolution as
|
|
1125
1124
|
// `memory`/`syllogise` — resolveRuntimeConfig: --repo > git root > cwd.
|
|
1126
1125
|
// `--ledger` is accepted as a no-op: the ledger IS the viz surface now.
|
|
1127
1126
|
const rest = process.argv.slice(3);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
{"kind":"rescue","sentence":"Code search is a retrieval task","rescued":"Code hunt is a retrieval task","from":"search","to":"hunt","pos":"noun","synsetId":"00947217-n","sourceFile":"PLAN_EMBEDDINGS.md","provenance":"wordnet:00947217-n"}
|
|
1
2
|
{"kind":"variant","seed":"the ace of trumps is a sure winner","generated":"the one of trumps is a sure winner","from":"ace","to":"one","pos":"noun","synsetId":"13764713-n","sourceCorpus":"corpus/tier2/human-examples-large.jsonl","provenance":"wordnet:13764713-n"}
|
|
2
3
|
{"kind":"variant","seed":"the ace of trumps is a sure winner","generated":"the single of trumps is a sure winner","from":"ace","to":"single","pos":"noun","synsetId":"13764713-n","sourceCorpus":"corpus/tier2/human-examples-large.jsonl","provenance":"wordnet:13764713-n"}
|
|
3
4
|
{"kind":"variant","seed":"the ace of trumps is a sure winner","generated":"the unity of trumps is a sure winner","from":"ace","to":"unity","pos":"noun","synsetId":"13764713-n","sourceCorpus":"corpus/tier2/human-examples-large.jsonl","provenance":"wordnet:13764713-n"}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"version": 1,
|
|
3
3
|
"generated": "by scripts/generate-template-variants.mjs",
|
|
4
4
|
"file": "ace-surface-variants.jsonl",
|
|
5
|
-
"rows":
|
|
6
|
-
"bytes":
|
|
7
|
-
"sha256": "
|
|
5
|
+
"rows": 18,
|
|
6
|
+
"bytes": 5012,
|
|
7
|
+
"sha256": "28fae5a3353f5dd1df365b4407e0ac2a5c66b98b232d8bf454fe3573f2c16760",
|
|
8
8
|
"license": "CC-BY-4.0 (WordNet-derived synonym substitutions of Open English WordNet / SemCor example sentences and this repo's own MPL-2.0 docs prose — see corpus/generated/README.md)"
|
|
9
9
|
}
|
|
@@ -30,12 +30,11 @@
|
|
|
30
30
|
// emitting one row per edge (see RELATION_MAP / synonymPairs below).
|
|
31
31
|
//
|
|
32
32
|
// The hand-rolled `parseYaml` this file reuses (imported, not duplicated) is
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// that already reads this same source.
|
|
33
|
+
// src/domain/wordnet/yaml.mjs's tiny YAML-subset reader — already proven
|
|
34
|
+
// against this exact OEWN dump shape by the persona-tier tooling. Reusing it
|
|
35
|
+
// (rather than adding a general YAML dependency, or re-deriving a second
|
|
36
|
+
// hand-rolled parser) keeps this converter self-consistent with the rest of
|
|
37
|
+
// the tooling that already reads this same source.
|
|
39
38
|
//
|
|
40
39
|
// Licence: Open English WordNet content is CC-BY-4.0 (Princeton WordNet +
|
|
41
40
|
// Open English Wordnet team) — see LICENSE-NOTICE in this directory. The
|
|
@@ -47,7 +46,7 @@ import { homedir } from "node:os";
|
|
|
47
46
|
import { createHash } from "node:crypto";
|
|
48
47
|
import { fileURLToPath } from "node:url";
|
|
49
48
|
import { dirname, join } from "node:path";
|
|
50
|
-
import { parseYaml } from "../../
|
|
49
|
+
import { parseYaml } from "../../src/domain/wordnet/yaml.mjs";
|
|
51
50
|
|
|
52
51
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
53
52
|
export const WORDNET_OUT_DIR = HERE;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.3.0",
|
|
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; no codebase index of its own.",
|
|
@@ -72,8 +72,19 @@
|
|
|
72
72
|
"test": "node --test \"test/**/*.test.mjs\"",
|
|
73
73
|
"test:e2e": "node --test \"e2e/**/*.test.mjs\"",
|
|
74
74
|
"e2e:browsers": "playwright install chromium",
|
|
75
|
+
"check:links": "node scripts/check-links.mjs",
|
|
76
|
+
"check:pii": "node scripts/pii-lint.mjs",
|
|
77
|
+
"check:pack": "node scripts/check-pack-manifest.mjs",
|
|
78
|
+
"check:licences": "node scripts/check-licences.mjs",
|
|
79
|
+
"check:publint": "npx --no-install publint",
|
|
80
|
+
"check:tool-docs": "node scripts/generate-tool-docs.mjs --check",
|
|
81
|
+
"check:publish": "node scripts/check-publish.mjs",
|
|
82
|
+
"check:all": "npm run check:links && npm run check:pii && npm run check:licences && npm run check:publint && npm run check:tool-docs",
|
|
83
|
+
"smoke:deploy": "node scripts/post-deploy-smoke.mjs",
|
|
75
84
|
"chat": "node bin/tmct.mjs",
|
|
76
85
|
"chat:repo": "node bin/tmct.mjs chat --repo",
|
|
86
|
+
"chat:plain": "node bin/tmct.mjs chat --plain",
|
|
87
|
+
"chat:narrate": "node bin/tmct.mjs chat --narrate",
|
|
77
88
|
"init": "node bin/tmct.mjs init",
|
|
78
89
|
"init:sqlite": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --memory-backend sqlite",
|
|
79
90
|
"init:persona:human": "node bin/tmct.mjs init --with-persona human",
|
|
@@ -82,19 +93,36 @@
|
|
|
82
93
|
"init:xl": "node bin/tmct.mjs init --persona-size large && node bin/tmct.mjs import --corpus seon && node bin/tmct.mjs import --corpus conceptnet && node bin/tmct.mjs import --corpus aws && node bin/tmct.mjs import --corpus python && node bin/tmct.mjs import --corpus java && node bin/tmct.mjs import --corpus wordnet-xl",
|
|
83
94
|
"init:xxl": "node bin/tmct.mjs init --persona-size large && node bin/tmct.mjs import --corpus seon && node bin/tmct.mjs import --corpus conceptnet && node bin/tmct.mjs import --corpus aws && node bin/tmct.mjs import --corpus python && node bin/tmct.mjs import --corpus java && node bin/tmct.mjs import --corpus wordnet-full && node bin/tmct.mjs import --corpus namenet",
|
|
84
95
|
"memory": "node bin/tmct.mjs memory",
|
|
96
|
+
"memory:verbose": "node bin/tmct.mjs memory --verbose",
|
|
85
97
|
"syllogise": "node bin/tmct.mjs syllogise",
|
|
86
98
|
"viz": "node bin/tmct.mjs viz",
|
|
99
|
+
"viz:term": "node bin/tmct.mjs viz --term",
|
|
100
|
+
"plan": "node bin/tmct.mjs plan",
|
|
101
|
+
"plan:json": "node bin/tmct.mjs plan --json",
|
|
102
|
+
"cli": "node bin/tmct.mjs cli",
|
|
103
|
+
"cli:digest": "node bin/tmct.mjs cli digest",
|
|
104
|
+
"import": "node bin/tmct.mjs import",
|
|
105
|
+
"extend": "node bin/tmct.mjs extend --validate",
|
|
106
|
+
"serve": "node bin/tmct.mjs serve",
|
|
107
|
+
"serve:public": "node bin/tmct.mjs serve --host 0.0.0.0 --port 8787",
|
|
87
108
|
"example:mini": "node bin/tmct.mjs chat --repo examples/mini-webapp --ephemeral",
|
|
88
109
|
"example:polyglot": "node bin/tmct.mjs chat --repo examples/polyglot --ephemeral",
|
|
89
110
|
"chatbench:run": "node chatbench/run.mjs",
|
|
90
111
|
"chatbench:judge": "node chatbench/judge.mjs",
|
|
91
|
-
"serve": "node bin/tmct.mjs serve",
|
|
92
112
|
"agentbench:run": "node agentbench/run.mjs",
|
|
93
113
|
"infbench": "node infbench/generate-cases.mjs && node infbench/run.mjs",
|
|
114
|
+
"corpus:matrix": "node scripts/corpus-matrix.mjs",
|
|
115
|
+
"corpus:matrix:gaps": "node scripts/corpus-matrix.mjs --gaps",
|
|
116
|
+
"template:coverage": "node scripts/template-coverage.mjs",
|
|
94
117
|
"audit": "npm audit --audit-level=high",
|
|
95
118
|
"audit:fix": "npm audit fix",
|
|
96
119
|
"demo:build": "node scripts/build-demo-site.mjs",
|
|
97
120
|
"build:ask-bundle": "node scripts/build-ask-bundle.mjs",
|
|
121
|
+
"build:demo-graph": "node scripts/build-demo-graph.mjs",
|
|
122
|
+
"build:demo-memory": "node scripts/build-demo-memory.mjs",
|
|
123
|
+
"gen:tool-docs": "node scripts/generate-tool-docs.mjs",
|
|
124
|
+
"gen:collisions": "node scripts/generate-real-word-collisions.mjs",
|
|
125
|
+
"gen:variants": "node scripts/generate-template-variants.mjs",
|
|
98
126
|
"extract:facts": "node scripts/extract-facts-from-text.mjs"
|
|
99
127
|
},
|
|
100
128
|
"devDependencies": {
|
|
@@ -35,7 +35,7 @@ export const TIER2_MANIFEST_FILE = join(TIER2_DIR, "manifest.json");
|
|
|
35
35
|
// conversion, same slice shape/loader path as tier-1/tier-2. "wordnet-xl"/"wordnet-full"
|
|
36
36
|
// are wired as BUILTIN_EXTENSIONS corpus entries in src/extensions.mjs.
|
|
37
37
|
export const WORDNET_DIR = join(PKG_ROOT, "corpus", "wordnet");
|
|
38
|
-
|
|
38
|
+
const WORDNET_MANIFEST_FILE = join(WORDNET_DIR, "manifest.json");
|
|
39
39
|
|
|
40
40
|
const ACE_PATTERNS = new Set(["subClassOf", "type", "ObjectProperty", "someValuesFrom", "disjointWith", "property", "none"]);
|
|
41
41
|
|
|
@@ -352,7 +352,7 @@ export function buildEntities(modules, commits, { generatedAt = "", symbolHistor
|
|
|
352
352
|
const countClass = (c) => fnIndividuals.filter((i) => i.class === c).length;
|
|
353
353
|
const sampleClass = (c) => fnIndividuals.filter((i) => i.class === c).slice(0, 3).map((i) => i.label);
|
|
354
354
|
|
|
355
|
-
// Second pass
|
|
355
|
+
// Second pass — see the returned `proseIndex` field's comment below.
|
|
356
356
|
const allIndividuals = attachProseTokens(
|
|
357
357
|
[...moduleIndividuals, ...fnIndividuals, ...commitIndividuals], { enabled: prose },
|
|
358
358
|
);
|
|
@@ -361,7 +361,7 @@ export function buildEntities(modules, commits, { generatedAt = "", symbolHistor
|
|
|
361
361
|
return {
|
|
362
362
|
generated_at: generatedAt,
|
|
363
363
|
// SEON (se-on.org, FAMIX-derived) vocabulary + our `mgx:` extension, documented
|
|
364
|
-
// for readers; the graph is JSON-label-only (no RDF store
|
|
364
|
+
// for readers; the graph is JSON-label-only (no RDF store).
|
|
365
365
|
prefixes: {
|
|
366
366
|
seon: "http://se-on.org/ontologies/seon.owl#",
|
|
367
367
|
mgx: "urn:tmct:mgx#",
|
|
@@ -419,7 +419,7 @@ export function buildEntities(modules, commits, { generatedAt = "", symbolHistor
|
|
|
419
419
|
rel("reexports", "mgx:reExports", reExportEdges),
|
|
420
420
|
],
|
|
421
421
|
individuals: allIndividuals,
|
|
422
|
-
// Second pass
|
|
422
|
+
// Second pass: word -> [individual ids], inverted from the
|
|
423
423
|
// `prose_tokens` attribute attachProseTokens just attached. Disable via
|
|
424
424
|
// TMCT_PROSE_INDEX=0 (indexRepository, below) — {} when off. The typed graph above
|
|
425
425
|
// (individuals' core fields, all edges) is byte-identical either way.
|
|
@@ -18,8 +18,8 @@ const trustFactorOf = (trust) => 0.5 + (typeof trust === "number" ? trust : SOUR
|
|
|
18
18
|
export const BLOCKS_DIR_REL = join(".tmct", "memory", "blocks");
|
|
19
19
|
const INDEX_NAME = "index.json";
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const PAGERANK_DAMPING = 0.85;
|
|
22
|
+
const PAGERANK_ITERATIONS = 20;
|
|
23
23
|
export const OVERLAP_MIN = 2; // shared tokens for a similarity edge
|
|
24
24
|
const MAX_TOKENS_PER_BLOCK = 800; // beyond tokenizeProse's per-doc cap: union over lines
|
|
25
25
|
|
|
@@ -43,7 +43,7 @@ export const IN_REPLY_TO_PROP = "mgx:inReplyTo";
|
|
|
43
43
|
// The provenance-link predicate family: one umbrella object property with two
|
|
44
44
|
// workhorse subproperties, minted in the owned mgx: namespace to match
|
|
45
45
|
// tmct-core.ttl's object-property style.
|
|
46
|
-
|
|
46
|
+
const DERIVED_FROM_PROP = "mgx:derivedFrom"; // umbrella: Fact → Source|Fact
|
|
47
47
|
export const STATED_BY_PROP = "mgx:statedBy"; // a Source directly asserts a Fact
|
|
48
48
|
export const CANONICALISED_FROM_PROP = "mgx:canonicalisedFrom"; // a canonical Fact ← its raw form
|
|
49
49
|
export const SOURCE_RELIABILITY_PROP = "mgx:sourceReliability"; // actor-level (session-scoped) trust nudge on a Source, [0.5,1.5]
|
|
@@ -52,7 +52,7 @@ export const SOURCE_RELIABILITY_PROP = "mgx:sourceReliability"; // actor-level (
|
|
|
52
52
|
// with no session-id segment. A tag that does carry one mints its own
|
|
53
53
|
// per-session Source instead (`${ID}:<sessionId>`, sourceIdFor below).
|
|
54
54
|
export const OPERATOR_SOURCE_ID = "src:operator-chat";
|
|
55
|
-
|
|
55
|
+
const TEACH_SOURCE_ID = "src:teach-chat";
|
|
56
56
|
|
|
57
57
|
const ROLES = new Set(["visitor", "tmct"]);
|
|
58
58
|
const LABEL_CAP = 48; // utterance/fact labels stay skimmable in renders
|
|
@@ -1113,13 +1113,13 @@ export const RULE_KIND_ACTION_SIGNATURE = "action-signature";
|
|
|
1113
1113
|
export const RULE_KIND_ACTION_PRECOND = "action-precond";
|
|
1114
1114
|
export const RULE_KIND_ACTION_EFFECT = "action-effect";
|
|
1115
1115
|
export const RULE_KIND_ACTION_CONSTRAINT = "action-constraint";
|
|
1116
|
-
|
|
1116
|
+
const RULE_KINDS = Object.freeze([
|
|
1117
1117
|
RULE_KIND_COMPOSE2, RULE_KIND_FILTER, RULE_KIND_RECURSIVE,
|
|
1118
1118
|
RULE_KIND_ACTION_SIGNATURE, RULE_KIND_ACTION_PRECOND, RULE_KIND_ACTION_EFFECT,
|
|
1119
1119
|
RULE_KIND_ACTION_CONSTRAINT,
|
|
1120
1120
|
]);
|
|
1121
1121
|
|
|
1122
|
-
|
|
1122
|
+
const RULE_NAME_PROP = "mgx:ruleName";
|
|
1123
1123
|
export const RULE_KIND_PROP = "mgx:ruleKind";
|
|
1124
1124
|
|
|
1125
1125
|
// Per-kind slot contract: JS slot key -> the mgx: attribute it's written under.
|
|
@@ -1514,7 +1514,7 @@ export async function removeFacts(dir, ids) {
|
|
|
1514
1514
|
|
|
1515
1515
|
/** The trust floor a fact must clear before a differing object counts as a real
|
|
1516
1516
|
* contradiction (below it the fact is too weak to contradict anything). */
|
|
1517
|
-
|
|
1517
|
+
const CONTRADICTION_TRUST_FLOOR = 0.5;
|
|
1518
1518
|
|
|
1519
1519
|
export const HAS_A_PREDICATE = "mgx:hasA";
|
|
1520
1520
|
export const CAPABLE_OF_PREDICATE = "mgx:capableOf";
|
|
@@ -13,7 +13,7 @@ import { emptyEntities } from "../source.mjs";
|
|
|
13
13
|
import { createGraphService } from "./graph-service.mjs";
|
|
14
14
|
|
|
15
15
|
/** The parsed empty bootstrap graph. */
|
|
16
|
-
|
|
16
|
+
function bootstrapGraph() {
|
|
17
17
|
return parseEntities(emptyEntities());
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -15,7 +15,7 @@ import { createGraphService } from "./graph-service.mjs";
|
|
|
15
15
|
/** A compact but type-complete entities payload: modules, a class hierarchy
|
|
16
16
|
* (Base ← Widget ← Button), a method with a full signature, an attribute, a
|
|
17
17
|
* module global, and a commit — wired by one edge of every closed kind. */
|
|
18
|
-
|
|
18
|
+
const FIXTURE_ENTITIES = Object.freeze({
|
|
19
19
|
generated_at: "2026-07-05T00:00:00.000Z",
|
|
20
20
|
bootstrap: false,
|
|
21
21
|
prefixes: { seon: "http://se-on.org/ontologies/seon.owl#", mgx: "urn:tmct:mgx#" },
|
|
@@ -139,7 +139,6 @@ export async function normalizeConfig(raw, { configDir } = {}) {
|
|
|
139
139
|
if (t.impl_of_interface !== undefined) tune.implOfInterface = t.impl_of_interface;
|
|
140
140
|
if (t.beam_search !== undefined) tune.beamSearch = t.beam_search;
|
|
141
141
|
if (t.beam_width !== undefined) tune.beamWidth = t.beam_width;
|
|
142
|
-
if (t.embed_rank !== undefined) tune.embedRank = t.embed_rank;
|
|
143
142
|
if (t.prose_layers !== undefined) tune.proseLayers = t.prose_layers;
|
|
144
143
|
const exp = t.expansion || {};
|
|
145
144
|
const expansion = {};
|
|
@@ -26,7 +26,7 @@ export function registerWinkModel(factory) {
|
|
|
26
26
|
|
|
27
27
|
/** Load `{ winkNLP, model }` once, or null when wink isn't available. Prefers a
|
|
28
28
|
* registered browser factory; otherwise falls back to Node module resolution. */
|
|
29
|
-
|
|
29
|
+
function loadWinkModel() {
|
|
30
30
|
if (cached !== undefined) return cached;
|
|
31
31
|
try {
|
|
32
32
|
const pair = injected ? injected() : nodeRequireWink();
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// wordnet-source.mjs — reads a LOCAL Open English WordNet clone off disk and
|
|
2
|
+
// indexes it. The clone is never vendored, never committed, never part of the
|
|
3
|
+
// npm package: point TMCT_WORDNET_SRC at it, or keep it at the default path.
|
|
4
|
+
//
|
|
5
|
+
// This is the disk half of the WordNet reader. The parsing half is pure and
|
|
6
|
+
// lives in src/domain/wordnet/yaml.mjs, so it is testable with no clone
|
|
7
|
+
// present; everything here needs the real files.
|
|
8
|
+
|
|
9
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { parseYaml } from "../domain/wordnet/yaml.mjs";
|
|
14
|
+
|
|
15
|
+
export const WORDNET_SRC = process.env.TMCT_WORDNET_SRC || join(homedir(), "projects", "globalwordnet", "english-wordnet");
|
|
16
|
+
export const WORDNET_YAML_DIR = join(WORDNET_SRC, "src", "yaml");
|
|
17
|
+
|
|
18
|
+
/** True iff a WordNet clone is readable at `yamlDir`. Callers use this to fail
|
|
19
|
+
* with a one-line message rather than a stack trace: these are maintainer
|
|
20
|
+
* tools, never a build dependency. */
|
|
21
|
+
export function hasWordnetSource(yamlDir = WORDNET_YAML_DIR) {
|
|
22
|
+
return existsSync(yamlDir);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Load one or more noun.<x>/verb.<x>.yaml files into a flat synset-id -> record map. */
|
|
26
|
+
export async function loadSynsets(files, yamlDir = WORDNET_YAML_DIR) {
|
|
27
|
+
const map = new Map();
|
|
28
|
+
for (const f of files) {
|
|
29
|
+
const path = join(yamlDir, f);
|
|
30
|
+
if (!existsSync(path)) continue;
|
|
31
|
+
const parsed = parseYaml(await readFile(path, "utf8"));
|
|
32
|
+
for (const [id, rec] of Object.entries(parsed)) map.set(id, rec);
|
|
33
|
+
}
|
|
34
|
+
return map;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Load the entries-<letter>.yaml files that could contain any of `words`
|
|
38
|
+
* (only the letters actually needed — 28 files, ~1MB-3MB each, no reason to
|
|
39
|
+
* load all 28 when a clump only needs a handful of letters). Returns
|
|
40
|
+
* word -> { n: [{id, synset}], v: [...], a: [...] }. */
|
|
41
|
+
export async function loadEntriesFor(words, yamlDir = WORDNET_YAML_DIR) {
|
|
42
|
+
const letters = new Set();
|
|
43
|
+
for (const w of words) {
|
|
44
|
+
const c = w[0].toLowerCase();
|
|
45
|
+
letters.add(/[a-z]/.test(c) ? c : "0");
|
|
46
|
+
}
|
|
47
|
+
const index = new Map();
|
|
48
|
+
for (const letter of letters) {
|
|
49
|
+
const path = join(yamlDir, `entries-${letter}.yaml`);
|
|
50
|
+
if (!existsSync(path)) continue;
|
|
51
|
+
const parsed = parseYaml(await readFile(path, "utf8"));
|
|
52
|
+
for (const [word, byPos] of Object.entries(parsed)) {
|
|
53
|
+
if (!words.has(word)) continue;
|
|
54
|
+
const senses = {};
|
|
55
|
+
for (const [pos, rec] of Object.entries(byPos || {})) {
|
|
56
|
+
if (pos === "form") continue;
|
|
57
|
+
const list = Array.isArray(rec?.sense) ? rec.sense : [];
|
|
58
|
+
senses[pos] = list.map((s) => ({ id: s.id, synset: s.synset })).filter((s) => s.synset);
|
|
59
|
+
}
|
|
60
|
+
index.set(word, senses);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return index;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Every noun.*.yaml synset in the clone. */
|
|
67
|
+
export async function loadAllNounSynsets(yamlDir = WORDNET_YAML_DIR) {
|
|
68
|
+
const files = (await readdir(yamlDir)).filter((f) => f.startsWith("noun."));
|
|
69
|
+
return loadSynsets(files, yamlDir);
|
|
70
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"curation": "Hand-curated
|
|
3
|
+
"curation": "Hand-curated, and it has to stay that way: each pool was cross-checked against real Open English WordNet synsets where a sense fit, but most of these are domain-specific code-graph connector phrases whose sense-1 convention resolves to the wrong sense -- e.g. 'locate' -> 'turn up', 'record' -> 'enter, put down', 'module' -> 'mental faculty'. So every entry below was accepted or rejected by hand against the exact rendered sentence it replaces, never auto-accepted from a raw synset match. That is why no generator reproduces this file and no drift guard can check it. See src/domain/answer-variants.mjs for how these are selected at render time.",
|
|
4
4
|
"pools": {
|
|
5
5
|
"defined-in": {
|
|
6
6
|
"base": "defined in",
|
package/src/domain/ask-vocab.mjs
CHANGED
|
@@ -211,7 +211,7 @@ export function stripTrailingScopeFiller(text) {
|
|
|
211
211
|
|
|
212
212
|
/** Trailing bare discourse tags ("how many of those then"). "too" can stack
|
|
213
213
|
* ("is UserController a validator too then"), hence the double pass below. */
|
|
214
|
-
|
|
214
|
+
const TRAILING_DISCOURSE_TAG = Object.freeze(["then", "though", "too"]);
|
|
215
215
|
|
|
216
216
|
const TRAILING_DISCOURSE_TAG_RE = new RegExp(
|
|
217
217
|
`\\s+(?:${TRAILING_DISCOURSE_TAG.join("|")})\\s*[?.!]*$`, "i",
|
|
@@ -219,7 +219,7 @@ const TRAILING_DISCOURSE_TAG_RE = new RegExp(
|
|
|
219
219
|
|
|
220
220
|
/** Trailing comma-delimited discourse clauses ("what is a class, please
|
|
221
221
|
* explain"), anchored on a literal comma so this never fires mid-phrase. */
|
|
222
|
-
|
|
222
|
+
const TRAILING_DISCOURSE_CLAUSE = Object.freeze(["please explain", "explain"]);
|
|
223
223
|
|
|
224
224
|
const TRAILING_DISCOURSE_CLAUSE_RE = new RegExp(
|
|
225
225
|
`,\\s*(?:${TRAILING_DISCOURSE_CLAUSE.join("|")})\\s*[?.!]*$`, "i",
|
package/src/domain/ask.mjs
CHANGED
|
@@ -1786,7 +1786,7 @@ function evalQualCheck(graph, ast, opts) {
|
|
|
1786
1786
|
|
|
1787
1787
|
/** Compile any compositional AST to a result object traverse() returns for the
|
|
1788
1788
|
* simple path — {matches, …} plus compositeKind/compositeMiss flags render() reads. */
|
|
1789
|
-
|
|
1789
|
+
function evalComposite(graph, ast, opts = {}) {
|
|
1790
1790
|
if (ast.node === "miss") return { compositeMiss: true, reason: ast.reason || null, matches: [] };
|
|
1791
1791
|
if (ast.node === "exists") return evalExists(graph, ast);
|
|
1792
1792
|
if (ast.node === "qualCheck") return evalQualCheck(graph, ast, opts);
|
|
@@ -1828,7 +1828,7 @@ const compositeList = (matches) => listJoin(matches.slice(0, OVERFLOW_CAP)
|
|
|
1828
1828
|
+ (matches.length > OVERFLOW_CAP ? `, …and ${matches.length - OVERFLOW_CAP} more` : "");
|
|
1829
1829
|
|
|
1830
1830
|
/** A compositional worked example for the rephrase hint. */
|
|
1831
|
-
|
|
1831
|
+
function compositionalHint() {
|
|
1832
1832
|
return 'compositional queries also work: "which functions call X and call Y", "what calls something that imports X", "public methods of X", "list functions" / "show me the classes", "how many classes", "which module has the most imports", "find me the payment class", or (after a listing) "which of those are tested"';
|
|
1833
1833
|
}
|
|
1834
1834
|
|
|
@@ -2047,7 +2047,7 @@ export function rephraseHint() {
|
|
|
2047
2047
|
* It gets a line that says what the store actually holds instead. A NULL
|
|
2048
2048
|
* graph is UNKNOWN, not empty (see chat.mjs's noCodeGraph), so it keeps the
|
|
2049
2049
|
* index-shaped advice. */
|
|
2050
|
-
|
|
2050
|
+
function touchesRephraseHint(graph = null) {
|
|
2051
2051
|
if (graph && moduleCountOf(graph) === 0) {
|
|
2052
2052
|
return "This store holds no code index, so it records no modules or commits to look through.";
|
|
2053
2053
|
}
|
|
@@ -3516,7 +3516,7 @@ function isHelpRequest(query) {
|
|
|
3516
3516
|
* they are content that may honestly fail to resolve.
|
|
3517
3517
|
* 3. SYNONYM — rewrite surviving near-canonical words to the closed vocab.
|
|
3518
3518
|
* Bounded (one token removed per noise iteration; hard guard) and deterministic. */
|
|
3519
|
-
|
|
3519
|
+
function relaxParse(graph, query, { nlp = undefined, contextId = null, prev = null } = {}) {
|
|
3520
3520
|
const from = applyNegationFrames(normalizeQuery(String(query || "")));
|
|
3521
3521
|
let tokens = splitWords(from);
|
|
3522
3522
|
if (!tokens.length) return null;
|
package/src/domain/codegraph.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { lookupByProseTokens, proseLayerHits, splitIdentifierWords } from "./prose.mjs";
|
|
2
|
-
import { cosine } from "./vector.mjs";
|
|
3
2
|
import { CREATED_AT_PROP, UPDATED_AT_PROP } from "./memory/trust.mjs";
|
|
4
3
|
|
|
5
4
|
// Pure (no-network, no-fs) query logic over the typed `entities` payload that the
|
|
@@ -129,11 +128,11 @@ function basename(p) {
|
|
|
129
128
|
|
|
130
129
|
const isProvRef = (r) => /^(git|turn):/.test(String(r || ""));
|
|
131
130
|
|
|
132
|
-
|
|
131
|
+
function turnRefCount(ind) {
|
|
133
132
|
return (ind?.derived_from || []).filter(isProvRef).length;
|
|
134
133
|
}
|
|
135
134
|
|
|
136
|
-
|
|
135
|
+
function mentionTotal(ind) {
|
|
137
136
|
const fromMentions = (ind?.mentions || []).reduce((n, m) => n + (Number(m?.count) || 0), 0);
|
|
138
137
|
return fromMentions + turnRefCount(ind);
|
|
139
138
|
}
|
|
@@ -511,15 +510,6 @@ const LIT_COMP_CAP = 4;
|
|
|
511
510
|
const LIT_FRAC = 1.0;
|
|
512
511
|
const LIT_CAP_FRAC = 0.9;
|
|
513
512
|
|
|
514
|
-
// opt-in via embedRank + an injected embedder: static-embedding re-rank over
|
|
515
|
-
// path/symbol/doc text read from the graph (never source), cached per-process in EMB_CACHE
|
|
516
|
-
const EMB_FRAC = 0.2;
|
|
517
|
-
const EMB_CAP_FRAC = 0.35;
|
|
518
|
-
const EMB_TEXT_SYMBOL_CAP = 64;
|
|
519
|
-
const EMB_TEXT_DOC_CAP = 12;
|
|
520
|
-
const EMB_CACHE = new WeakMap(); // graph -> { embedder, texts, vecs: Map<moduleId, Float32Array> }
|
|
521
|
-
let embedWarned = false;
|
|
522
|
-
|
|
523
513
|
// opt-in via beamSearch: multi-ply adaptive expansion of the proximity nudge above.
|
|
524
514
|
// Beam width is a margin relative to each ply's best score (not a fixed count), so a
|
|
525
515
|
// weak-then-strong candidate isn't prematurely discarded. Successors are generated
|
|
@@ -544,30 +534,6 @@ const SPIRAL_HOP_DECAY = 0.6;
|
|
|
544
534
|
const SPIRAL_PROX_FRAC = 0.2;
|
|
545
535
|
const SPIRAL_PROX_CAP_FRAC = 0.35;
|
|
546
536
|
|
|
547
|
-
/** embedRank: per-module embeddable text from path components + defined symbol
|
|
548
|
-
* names + doc first-lines, cached alongside the vectors in EMB_CACHE. */
|
|
549
|
-
function moduleEmbedTexts(graph) {
|
|
550
|
-
const texts = new Map(); // moduleId -> text
|
|
551
|
-
const defIdx = definesIndex(graph);
|
|
552
|
-
const docs = new Map(); // moduleId -> [doc first-lines]
|
|
553
|
-
for (const ind of graph.individuals) {
|
|
554
|
-
const doc = (ind.attributes || []).find((a) => a.key === "doc")?.value;
|
|
555
|
-
if (!doc) continue;
|
|
556
|
-
const modId = (ind.class || "") === "Module" ? ind.id : moduleIdOf(graph, ind);
|
|
557
|
-
if (!modId) continue;
|
|
558
|
-
let arr = docs.get(modId);
|
|
559
|
-
if (!arr) docs.set(modId, (arr = []));
|
|
560
|
-
if (arr.length < EMB_TEXT_DOC_CAP) arr.push(String(doc).split("\n")[0]);
|
|
561
|
-
}
|
|
562
|
-
for (const ind of graph.individuals) {
|
|
563
|
-
if ((ind.class || "") !== "Module") continue;
|
|
564
|
-
const parts = String(ind.label).split(/[^a-zA-Z0-9_]+/).filter(Boolean);
|
|
565
|
-
const syms = (defIdx.get(ind.id) || []).slice(0, EMB_TEXT_SYMBOL_CAP);
|
|
566
|
-
texts.set(ind.id, [...parts, ...syms, ...(docs.get(ind.id) || [])].join(" "));
|
|
567
|
-
}
|
|
568
|
-
return texts;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
537
|
/** Split a lowercased path label into boundary components: django/utils/text.py →
|
|
572
538
|
* {django,utils,text,py}. Component equality (not substring) stops "text" matching "ci<text>". */
|
|
573
539
|
function pathComponents(labelLc) {
|
|
@@ -773,7 +739,7 @@ export function spiralExpand(graph, scored = [], {
|
|
|
773
739
|
* IDF-weights each query token, scores path/symbol/exact-symbol matches, and
|
|
774
740
|
* re-ranks with a bounded import-proximity bonus. Pure; deterministic. */
|
|
775
741
|
function scoreModules(graph, tokens, opts = {}) {
|
|
776
|
-
const { demoteNonProd = false, callAdjacency = false, implOfInterface = false, beamSearch = false, spiral = false, proseBoost = false, proseLayers = false, literalMention = false,
|
|
742
|
+
const { demoteNonProd = false, callAdjacency = false, implOfInterface = false, beamSearch = false, spiral = false, proseBoost = false, proseLayers = false, literalMention = false, rawQuery = "" } = opts;
|
|
777
743
|
const beamWidth = Number.isFinite(opts.beamWidth) && opts.beamWidth > 0 ? opts.beamWidth : 8;
|
|
778
744
|
const defIdx = definesIndex(graph);
|
|
779
745
|
// Precompute each module's path components + defined-symbol exact/component sets, once.
|
|
@@ -967,36 +933,6 @@ function scoreModules(graph, tokens, opts = {}) {
|
|
|
967
933
|
s.score += Math.min(signal * PROSE_LAYER_FRAC, s.score * PROSE_LAYER_CAP_FRAC);
|
|
968
934
|
}
|
|
969
935
|
}
|
|
970
|
-
// embedRank: the embedder is injected so this module stays fs-free; absent -> a one-time
|
|
971
|
-
// stderr note, never a failure
|
|
972
|
-
if (embedRank) {
|
|
973
|
-
if (!opts.embedder) {
|
|
974
|
-
if (!embedWarned) {
|
|
975
|
-
embedWarned = true;
|
|
976
|
-
process.stderr.write("tmct: embedRank requested but no embedder available (weights not fetched? see `npm run refs:embeddings`) — flag is a no-op\n");
|
|
977
|
-
}
|
|
978
|
-
} else if (scored.length) {
|
|
979
|
-
const embedder = opts.embedder;
|
|
980
|
-
let cache = EMB_CACHE.get(graph);
|
|
981
|
-
if (!cache || cache.embedder !== embedder) {
|
|
982
|
-
cache = { embedder, texts: moduleEmbedTexts(graph), vecs: new Map() };
|
|
983
|
-
EMB_CACHE.set(graph, cache);
|
|
984
|
-
}
|
|
985
|
-
const qv = embedder.embed(rawQuery || tokens.join(" "));
|
|
986
|
-
let maxBase = 0;
|
|
987
|
-
for (const s of scored) maxBase = Math.max(maxBase, s.score);
|
|
988
|
-
for (const s of scored) {
|
|
989
|
-
let v = cache.vecs.get(s.ind.id);
|
|
990
|
-
if (!v) {
|
|
991
|
-
v = embedder.embed(cache.texts.get(s.ind.id) || String(s.ind.label));
|
|
992
|
-
cache.vecs.set(s.ind.id, v);
|
|
993
|
-
}
|
|
994
|
-
const sim = Math.max(0, cosine(qv, v)); // negative similarity never penalises
|
|
995
|
-
if (!sim) continue;
|
|
996
|
-
s.score += Math.min(sim * maxBase * EMB_FRAC, s.score * EMB_CAP_FRAC);
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
936
|
// beamSearch (opt-in): multi-ply generalization of the single-hop families above.
|
|
1001
937
|
if (beamSearch && scored.length > 1) beamExpand(graph, scored, beamWidth);
|
|
1002
938
|
// SPIRAL (opt-in): bounded-radius ego walk that may introduce lexically-invisible modules — runs
|
|
@@ -1024,9 +960,9 @@ export function searchModulesRanked(graph, query, opts = {}) {
|
|
|
1024
960
|
const tokens = raw.toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean);
|
|
1025
961
|
if (!tokens.length) return [];
|
|
1026
962
|
// literalMention needs the query BEFORE tokenization (the tokenizer destroys the dotted refs
|
|
1027
|
-
// it matches on)
|
|
1028
|
-
//
|
|
1029
|
-
const effOpts =
|
|
963
|
+
// it matches on); threaded only when a flag that consumes it is on, so the OFF path is
|
|
964
|
+
// provably unchanged.
|
|
965
|
+
const effOpts = opts.literalMention ? { ...opts, rawQuery: raw } : opts;
|
|
1030
966
|
return scoreModules(graph, tokens, effOpts).map((s) => ({ path: String(s.ind.label), score: s.score }));
|
|
1031
967
|
}
|
|
1032
968
|
|
|
@@ -1466,7 +1402,7 @@ const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "
|
|
|
1466
1402
|
|
|
1467
1403
|
/** Map of lowercased author name → that author's Commit individuals (payload order).
|
|
1468
1404
|
* Tolerates both attribute-key conventions (author / commitAuthor), like commitLine. */
|
|
1469
|
-
|
|
1405
|
+
function authorIndex(graph) {
|
|
1470
1406
|
const idx = new Map();
|
|
1471
1407
|
for (const ind of graph?.individuals || []) {
|
|
1472
1408
|
if ((ind.class || "") !== "Commit") continue;
|