@polycode-projects/seonix 0.1.0 → 0.2.1
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/bin/cli.mjs +95 -12
- package/package.json +5 -2
- package/src/ask-vocab.mjs +258 -0
- package/src/ask.mjs +762 -0
- package/src/browser.mjs +769 -0
- package/src/codegraph.mjs +352 -23
- package/src/cs_treesitter.mjs +2 -2
- package/src/extract.mjs +51 -8
- package/src/extract_lang.mjs +17 -2
- package/src/java_javaparser.mjs +54 -0
- package/src/java_treesitter.mjs +216 -0
- package/src/jsts_tsc.mjs +2 -2
- package/src/prose.mjs +145 -0
- package/src/schema-docs.mjs +225 -0
- package/src/server.mjs +35 -4
- package/src/temporal.mjs +559 -0
- package/src/viz.mjs +666 -94
- package/src/walk.mjs +52 -5
package/bin/cli.mjs
CHANGED
|
@@ -4,11 +4,24 @@
|
|
|
4
4
|
//
|
|
5
5
|
// seonix → stdio MCP server
|
|
6
6
|
// seonix cli index_repository '{"repo_path":"<abs>"}' → deterministic index
|
|
7
|
+
// (honours <repo>/.seonixignore — gitignore-subset patterns; pass
|
|
8
|
+
// `"ignores":false` in the JSON arg to index everything regardless)
|
|
7
9
|
// seonix cli digest '{"repo_path":"<abs>","modules":[…]}' → architecture map + per-module
|
|
8
10
|
// context bundles to stdout (no-MCP arm)
|
|
11
|
+
// seonix cli digest '{"repo_path":"<abs>","query":"<free text>"}' → same, but auto-locates +
|
|
12
|
+
// score-gap-selects the modules (R1b, the shipped default) instead of requiring an explicit
|
|
13
|
+
// `modules` list — one call from a question to a digest. `modules` wins if both are given.
|
|
14
|
+
// seonix cli browser_link '{"query":"<nl-ish or grammar>","at":"<sha>","base":"<url>"}' → NL→query→URL:
|
|
15
|
+
// map a natural-language-ish request to the code-browser query grammar (deterministic, no model),
|
|
16
|
+
// SELF-TEST it against the local temporal graph (≥1 match, cursors resolve), print the URL;
|
|
17
|
+
// exit 1 on a link that would render empty. `"grammar":true` takes the query verbatim.
|
|
9
18
|
// seonix cli <toolName> '{…args}' → invoke ANY MCP tool via Bash (no MCP)
|
|
19
|
+
// (e.g. seonix cli seonix_ask '{"query":"<free text>"}' — mechanical, no-LLM NL question
|
|
20
|
+
// over the graph, PLAN_MECHANICAL_CHAT.md; no bespoke wiring needed, this fallback covers it)
|
|
10
21
|
// seonix hook-augment → PreToolUse Grep/Glob augmenter (stdin→stdout)
|
|
11
|
-
// seonix viz [--focus <sym>] [--depth N] [--out f.html] → render a focused sub-graph to HTML
|
|
22
|
+
// seonix viz [--focus <sym>] [--depth N] [--out f.html] [--data-out f.json] [--repo-url <gitlab url> --ref main] [--site-nav] → render a focused sub-graph to HTML (one shared viewer; data embedded, or split out with --data-out)
|
|
23
|
+
// seonix viz --browser-out f.html [--browser-data-out f.json] [--scope product|<prefixes>] → also render the Chronograph code browser (temporal scrub + two-cursor diff + narration + ghost-branch merges + keyboard nav; PLAN_CODE_BROWSER.md)
|
|
24
|
+
// seonix viz --serve [--port N] [--focus <sym>] → serve the same viewer against THIS repo's index (the local equivalent of the site's #viz; code browser at /code-browser.html, live-reannotating on HEAD change via /code-browser-version)
|
|
12
25
|
//
|
|
13
26
|
// The index step is offline and model-free (Python ast + git). It writes the
|
|
14
27
|
// graph artifact to <repo_path>/.seonix/graph.json; the server (started with
|
|
@@ -20,7 +33,7 @@ import { dispatchTool, buildContextBundle } from "../src/server.mjs";
|
|
|
20
33
|
import { indexRepository } from "../src/extract.mjs";
|
|
21
34
|
import { loadConfig, DEFAULT_GRAPH_REL } from "../src/config.mjs";
|
|
22
35
|
import * as source from "../src/source.mjs";
|
|
23
|
-
import { parseEntities, renderSearch, rankModulesByProximity, searchModulesRanked } from "../src/codegraph.mjs";
|
|
36
|
+
import { parseEntities, renderSearch, rankModulesByProximity, searchModulesRanked, selectRankedModules, DEFAULT_SCORE_GAP } from "../src/codegraph.mjs";
|
|
24
37
|
|
|
25
38
|
/** Build a config pointed at a specific repo's artifact (for `cli` sub-commands that
|
|
26
39
|
* take a repo_path), or fall back to the cwd-derived default. */
|
|
@@ -44,6 +57,11 @@ const TIER_RANK = { NONE: 0, TINY: 1, MID: 2, LARGE: 3, FULL: 4 };
|
|
|
44
57
|
* renderer via buildContextBundle, so no render logic is duplicated). This stdout is injected
|
|
45
58
|
* into the no-MCP arm's prompt.
|
|
46
59
|
*
|
|
60
|
+
* Two ways to say which modules: an explicit `modules` array (unchanged), or a `query` string —
|
|
61
|
+
* auto-locate + score-gap-select (R1b, the shipped default as of 2026-07-02) in one call, so a
|
|
62
|
+
* real caller no longer has to run `seonix_locate` and hand-pick a module themselves. `modules`
|
|
63
|
+
* wins if both are given. The header reports which modules were actually selected either way.
|
|
64
|
+
*
|
|
47
65
|
* B2: the FIRST (primary) module gets a full size-adaptive bundle; the remaining modules are
|
|
48
66
|
* RANKED by import/cochange proximity to the primary and only the top few get a TRIMMED
|
|
49
67
|
* (signatures + insertion region) bundle — so a 2-module task no longer pays for two full
|
|
@@ -51,12 +69,25 @@ const TIER_RANK = { NONE: 0, TINY: 1, MID: 2, LARGE: 3, FULL: 4 };
|
|
|
51
69
|
async function runDigest(args) {
|
|
52
70
|
const repoPath = args.repo_path;
|
|
53
71
|
if (!repoPath) { process.stderr.write("seonix: digest requires repo_path\n"); process.exit(2); }
|
|
54
|
-
|
|
72
|
+
let modules = Array.isArray(args.modules) ? args.modules.slice(0, DIGEST_MODULE_CAP) : [];
|
|
73
|
+
let autoSelected = null; // for the header, when `query` drove selection
|
|
74
|
+
if (!modules.length && args.query) {
|
|
75
|
+
const graph = parseEntities(await source.fetchEntities(configFor(repoPath)));
|
|
76
|
+
const ranked = searchModulesRanked(graph, args.query);
|
|
77
|
+
const scoreGapK = args.score_gap === false ? null : (Number.isFinite(args.score_gap) ? args.score_gap : DEFAULT_SCORE_GAP);
|
|
78
|
+
modules = selectRankedModules(ranked, { top_k: Number.isFinite(args.top_k) ? args.top_k : 2, scoreGapK }).slice(0, DIGEST_MODULE_CAP);
|
|
79
|
+
autoSelected = modules;
|
|
80
|
+
if (!modules.length) process.stderr.write(`seonix: digest query "${args.query}" matched no modules — empty digest\n`);
|
|
81
|
+
}
|
|
55
82
|
// Tuning-flag contract (threaded to buildContextBundle → sizeBundle): `min` → leanest TINY/no
|
|
56
|
-
// top-up; `untuned` →
|
|
83
|
+
// top-up; `untuned` → the earlier escalation. Neither → the tuned default. The digest header still
|
|
57
84
|
// reports the EFFECTIVE tier/topup returned per module, so rig telemetry stays correct.
|
|
58
85
|
const min = Boolean(args.min);
|
|
59
86
|
const untuned = Boolean(args.untuned);
|
|
87
|
+
// seonix-max: the injection CEILING — every requested module gets a FULL (untrimmed) bundle,
|
|
88
|
+
// not just the primary + 2 trimmed secondaries. Tests whether maximal injection re-bloats.
|
|
89
|
+
const max = Boolean(args.max);
|
|
90
|
+
const secondaryCap = max ? modules.length : DIGEST_SECONDARY_CAP;
|
|
60
91
|
const config = configFor(repoPath);
|
|
61
92
|
const body = [];
|
|
62
93
|
let effTier = "NONE"; // largest tier emitted across all modules
|
|
@@ -71,7 +102,7 @@ async function runDigest(args) {
|
|
|
71
102
|
|
|
72
103
|
const emit = async (m, trim) => {
|
|
73
104
|
try {
|
|
74
|
-
const { text, tier, topup: t } = await buildContextBundle({ symbol: m, min, untuned }, { config, source, trim });
|
|
105
|
+
const { text, tier, topup: t } = await buildContextBundle({ symbol: m, min, untuned, max }, { config, source, trim });
|
|
75
106
|
body.push(`\n# Context bundle: ${m}${trim ? " (secondary, trimmed)" : ""}\n` + text);
|
|
76
107
|
if ((TIER_RANK[tier] || 0) > (TIER_RANK[effTier] || 0)) effTier = tier;
|
|
77
108
|
if (t) topup = true;
|
|
@@ -89,15 +120,18 @@ async function runDigest(args) {
|
|
|
89
120
|
try { ranked = rankModulesByProximity(parseEntities(await source.fetchEntities(config)), primary, rest); }
|
|
90
121
|
catch { ranked = rest; }
|
|
91
122
|
}
|
|
92
|
-
const secondaries = ranked.slice(0,
|
|
93
|
-
const overflow = ranked.slice(
|
|
123
|
+
const secondaries = ranked.slice(0, secondaryCap);
|
|
124
|
+
const overflow = ranked.slice(secondaryCap);
|
|
94
125
|
await emit(primary, false);
|
|
95
|
-
for (const m of secondaries) await emit(m, true);
|
|
126
|
+
for (const m of secondaries) await emit(m, max ? false : true);
|
|
96
127
|
if (overflow.length) body.push(`\n# Related modules (not expanded; query seonix_context if needed): ${overflow.join(", ")}`);
|
|
97
128
|
}
|
|
98
129
|
|
|
99
|
-
// HARD CONTRACT: first line is the machine-readable digest header the rig greps.
|
|
100
|
-
|
|
130
|
+
// HARD CONTRACT: first line is the machine-readable digest header the rig greps. Fields are
|
|
131
|
+
// append-only — `selected=` is new (query mode only) and never changes the existing ones the
|
|
132
|
+
// rig's own parser depends on.
|
|
133
|
+
const header = `# seonix-digest tier=${effTier} topup=${topup} modules=${emitted}`
|
|
134
|
+
+ (autoSelected ? ` selected=${autoSelected.join(",") || "(none)"}` : "");
|
|
101
135
|
process.stdout.write([header, ...body].join("\n") + "\n");
|
|
102
136
|
}
|
|
103
137
|
|
|
@@ -152,7 +186,7 @@ async function main() {
|
|
|
152
186
|
process.exit(2);
|
|
153
187
|
}
|
|
154
188
|
const t0 = Date.now();
|
|
155
|
-
const { graphFile, counts } = await indexRepository(repoPath);
|
|
189
|
+
const { graphFile, counts } = await indexRepository(repoPath, { ignores: args.ignores !== false });
|
|
156
190
|
process.stderr.write(
|
|
157
191
|
`seonix: indexed ${counts.modules} modules, ${counts.functions} symbols, ` +
|
|
158
192
|
`${counts.edges} edges (${counts.callsSymbol} callsSymbol, ${counts.touchesSymbol} touchesSymbol), ` +
|
|
@@ -189,7 +223,14 @@ async function main() {
|
|
|
189
223
|
const config = configFor(args.repo_path);
|
|
190
224
|
try {
|
|
191
225
|
const graph = parseEntities(await source.fetchEntities(config));
|
|
192
|
-
|
|
226
|
+
// B016 recall-lever flags (LOCATE-phase, per-arm; output byte-identical when absent).
|
|
227
|
+
const ranked = searchModulesRanked(graph, args.query || "", {
|
|
228
|
+
demoteNonProd: !!args.demote_nonprod, // R1a: demote examples//fixtures//test-* paths
|
|
229
|
+
callAdjacency: !!args.call_adjacency, // E1a: resolved-call adjacency bonus
|
|
230
|
+
implOfInterface: !!args.impl_of_interface, // E1b: C# impl-of-interface boost
|
|
231
|
+
beamSearch: !!args.beam_search, // §5.15: multi-ply discriminative expansion
|
|
232
|
+
...(Number.isFinite(Number(args.beam_width)) ? { beamWidth: Number(args.beam_width) } : {}),
|
|
233
|
+
});
|
|
193
234
|
process.stdout.write(ranked.map((r) => `${r.path}\t${r.score}`).join("\n") + "\n");
|
|
194
235
|
} catch (e) {
|
|
195
236
|
process.stderr.write(`seonix: ${e?.message || e}\n`);
|
|
@@ -198,6 +239,48 @@ async function main() {
|
|
|
198
239
|
return;
|
|
199
240
|
}
|
|
200
241
|
|
|
242
|
+
// browser_link (Chronograph P2): NL-ish text → query grammar → SELF-TESTED URL.
|
|
243
|
+
// The mapping is deterministic keyword handling (nlToQuery — never a model call;
|
|
244
|
+
// a model-driven wrapper can sit on top and pass `grammar:true` with grammar it
|
|
245
|
+
// wrote itself). The link is printed ONLY if it will render something: the query
|
|
246
|
+
// must match ≥1 node of the local temporal graph and every cursor must resolve —
|
|
247
|
+
// the agent contract from PLAN_CODE_BROWSER.md ("no dead links").
|
|
248
|
+
if (sub === "browser_link") {
|
|
249
|
+
const args = parsePayload(payload);
|
|
250
|
+
if (args === null || !String(args?.query || "").trim()) {
|
|
251
|
+
process.stderr.write("seonix: browser_link expects a JSON arg, e.g. '{\"query\":\"classes that change with render\"}'\n");
|
|
252
|
+
process.exit(2);
|
|
253
|
+
}
|
|
254
|
+
const config = configFor(args.repo_path);
|
|
255
|
+
const { buildTemporalGraph, gitCommitOrder } = await import("../src/browser.mjs");
|
|
256
|
+
const { nlToQuery, validateLink, encodeViewState, VIEW_DEFAULTS } = await import("../src/temporal.mjs");
|
|
257
|
+
try {
|
|
258
|
+
const raw = await source.fetchEntities(config);
|
|
259
|
+
const commitIds = (raw.individuals || []).filter((i) => i.class === "Commit").map((i) => i.id);
|
|
260
|
+
const order = await gitCommitOrder(args.repo_path || process.cwd(), commitIds);
|
|
261
|
+
const tg = buildTemporalGraph(raw, order, { scope: args.scope });
|
|
262
|
+
const { q, notes } = args.grammar ? { q: String(args.query).trim(), notes: [] } : nlToQuery(args.query);
|
|
263
|
+
for (const n of notes) process.stderr.write(`seonix: note: ${n}\n`);
|
|
264
|
+
if (!q) {
|
|
265
|
+
process.stderr.write("seonix: the request maps to an empty query — nothing to link\n");
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
const v = validateLink(tg, { q, at: args.at || "", b: args.b || "" });
|
|
269
|
+
if (!v.ok) {
|
|
270
|
+
for (const r of v.reasons) process.stderr.write(`seonix: link self-test FAILED: ${r}\n`);
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
const qs = encodeViewState({ ...VIEW_DEFAULTS, q, at: args.at || "", b: args.b || "" });
|
|
274
|
+
const base = String(args.base || "").replace(/\/+$/, "");
|
|
275
|
+
process.stdout.write(`${base}/code-browser.html?${qs}\n`);
|
|
276
|
+
process.stderr.write(`seonix: link self-test OK — q="${q}" matches ${v.matches} node(s)\n`);
|
|
277
|
+
} catch (e) {
|
|
278
|
+
process.stderr.write(`seonix: ${e?.message || e}\n`);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
201
284
|
// tool-query fallback: any other `cli <toolName> '{…}'` routes to the MCP dispatcher,
|
|
202
285
|
// so "cold" tools are invokable from Bash without an MCP connection.
|
|
203
286
|
if (sub) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/seonix",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "SEONIX — a deterministic, offline, $0 code-graph tool that makes a cheap coding agent edit like an expensive one. Indexes a repo with Python ast + git (zero model calls) and renders a bounded edit-digest.",
|
|
@@ -45,7 +45,10 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
47
47
|
"cytoscape": "^3.30.0",
|
|
48
|
-
"ts-morph": "^24.0.0"
|
|
48
|
+
"ts-morph": "^24.0.0",
|
|
49
|
+
"tree-sitter": "^0.21.1",
|
|
50
|
+
"tree-sitter-c-sharp": "^0.21.3",
|
|
51
|
+
"tree-sitter-java": "^0.21.0"
|
|
49
52
|
},
|
|
50
53
|
"scripts": {
|
|
51
54
|
"test": "node --test \"test/**/*.test.mjs\"",
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// ask-vocab.mjs — the single committed vocabulary `ask.mjs`'s grammar, rephrase
|
|
2
|
+
// hint, and renderer noun forms all derive from (§3 of PLAN_MECHANICAL_CHAT.md).
|
|
3
|
+
// A shipped, hand-curated resource — the same "closed is deliberate" ethos as
|
|
4
|
+
// this repo's other closed vocabularies (SEON/mgx predicates, `.seonixignore`'s
|
|
5
|
+
// gitignore-subset grammar): an open/unbounded phrase list is unvalidatable and
|
|
6
|
+
// eventually produces a false-positive match against an unrelated code
|
|
7
|
+
// identifier, so each relation carries a curated (not exhaustive) set of real
|
|
8
|
+
// phrasings a developer would type, not a general-English thesaurus dump.
|
|
9
|
+
//
|
|
10
|
+
// Modeled on marginalia's app/lib/vocab.mjs pattern (a single source of truth
|
|
11
|
+
// with a `verbs: [...]` trigger-phrase array per token) — minus the RDF/OWL
|
|
12
|
+
// external-alignment machinery, which doesn't apply here: seonix's vocabulary
|
|
13
|
+
// is code-relationship-specific (imports/calls/inherits/…), not a general
|
|
14
|
+
// knowledge-graph ontology. No NLP library, no lemmatiser — plain phrase lists
|
|
15
|
+
// feeding the same fixed-precedence regex grammar `ask.mjs` already used, so
|
|
16
|
+
// broadening coverage never risks the "closed, deterministic, zero model
|
|
17
|
+
// calls" contract this repo holds at every layer (extraction through query).
|
|
18
|
+
|
|
19
|
+
/** relation token -> { comment, verbs[] }. Every phrase in `verbs` must be
|
|
20
|
+
* something a developer would plausibly type asking about THIS relation in
|
|
21
|
+
* THIS codebase's terms — not a synonym so generic it risks matching an
|
|
22
|
+
* unrelated question (see the file-level comment; judgment calls on omitted
|
|
23
|
+
* phrases are logged inline below, not silently dropped).
|
|
24
|
+
*
|
|
25
|
+
* Register spread (2026-07-02, ELIZA/PARRY-style breadth, PLAN_MECHANICAL_CHAT.md
|
|
26
|
+
* §3.5): each relation's `verbs` list deliberately spans formal, neutral, and
|
|
27
|
+
* casual/colloquial phrasings — not just formal synonyms — because the value of
|
|
28
|
+
* a keyword-spotting matcher is SURFACE-FORM tolerance for the same underlying
|
|
29
|
+
* intent, not narrow correctness. Registers are grouped with an inline comment
|
|
30
|
+
* per relation rather than a per-phrase tag (kept flat, matching marginalia's
|
|
31
|
+
* `verbs: [...]` shape) since ask.mjs only needs phrase -> kind, never the
|
|
32
|
+
* register itself. A misparsed casual phrase costs nothing beyond an honest
|
|
33
|
+
* object-miss (resolveObject never guesses), so breadth here is genuinely low-risk. */
|
|
34
|
+
export const RELATIONS = {
|
|
35
|
+
imports: {
|
|
36
|
+
comment: "Module -> Module: subject's import graph references object (usesComplexType).",
|
|
37
|
+
verbs: [
|
|
38
|
+
// formal/neutral
|
|
39
|
+
"couples to", "couple to", "uses", "use", "depends on", "imports", "import",
|
|
40
|
+
"relies on", "rely on", "requires", "require", "references", "reference",
|
|
41
|
+
"pulls in", "pull in", "built on", "builds on", "build on", "uses code from",
|
|
42
|
+
// casual/colloquial
|
|
43
|
+
"grabs", "grab", "pulls from", "pull from", "leans on", "lean on",
|
|
44
|
+
"is wired to", "are wired to", "is hooked up to", "are hooked up to",
|
|
45
|
+
// gerund (g-drop normalization turns dialectal "importin'" into this — §3.5)
|
|
46
|
+
"importing",
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
calls: {
|
|
50
|
+
comment: "Function/Method -> Function/Class (symbol-grain) or Module -> Module (coarse): subject invokes object.",
|
|
51
|
+
verbs: [
|
|
52
|
+
// formal
|
|
53
|
+
"invokes", "invoke",
|
|
54
|
+
// neutral
|
|
55
|
+
"calls", "call", "runs", "run", "executes", "execute",
|
|
56
|
+
// casual/colloquial
|
|
57
|
+
"hits", "hit", "triggers", "trigger", "fires", "fire", "kicks off", "kick off",
|
|
58
|
+
// gerund (g-drop normalization turns dialectal "callin'" into this — §3.5)
|
|
59
|
+
"calling",
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
defines: {
|
|
63
|
+
comment: "Module -> top-level Function/Class/Method/Attribute: subject declares object.",
|
|
64
|
+
verbs: [
|
|
65
|
+
"defines", "define", "declares", "declare",
|
|
66
|
+
"has", "have", "holds", "hold",
|
|
67
|
+
// gerund (g-drop normalization — §3.5)
|
|
68
|
+
"defining",
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
contains: {
|
|
72
|
+
comment: "Class -> Method/Attribute: subject's membership includes object.",
|
|
73
|
+
verbs: [
|
|
74
|
+
"contains", "contain", "lives in", "live in", "is defined in", "are defined in",
|
|
75
|
+
"is part of", "are part of", "sits in", "sit in", "sits inside", "sit inside",
|
|
76
|
+
// gerund (g-drop normalization — §3.5)
|
|
77
|
+
"containing",
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
tests: {
|
|
81
|
+
comment: "Module -> Module: subject is a test module importing/covering object.",
|
|
82
|
+
verbs: [
|
|
83
|
+
"tests", "test", "covers", "cover", "verifies", "verify", "exercises", "exercise",
|
|
84
|
+
"checks", "check", "makes sure of", "make sure of",
|
|
85
|
+
// gerund (g-drop normalization — §3.5)
|
|
86
|
+
"testing",
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
inherits: {
|
|
90
|
+
comment: "Class -> Class: subject's declared base resolves to object (subclassOf).",
|
|
91
|
+
verbs: [
|
|
92
|
+
"inherits from", "inherit from", "extends", "extend", "subclasses", "subclass",
|
|
93
|
+
"derives from", "derive from", "is a subclass of", "are a subclass of",
|
|
94
|
+
"is a kind of", "are a kind of", "is built off", "are built off",
|
|
95
|
+
"is built on top of", "are built on top of",
|
|
96
|
+
// gerund (g-drop normalization — §3.5)
|
|
97
|
+
"extending",
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
touches: {
|
|
101
|
+
comment: "Commit -> Module (coarse) or Commit -> symbol (fine, touchesSymbol): a commit's changed-line-range intersects object.",
|
|
102
|
+
verbs: [
|
|
103
|
+
"touched", "touches", "changed", "change", "modified", "modifies",
|
|
104
|
+
"was changed in", "were changed in", "was edited in", "were edited in",
|
|
105
|
+
"was modified by", "were modified by", "was tweaked in", "were tweaked in",
|
|
106
|
+
"got changed in", "got edited in",
|
|
107
|
+
// gerund (g-drop normalization — §3.5)
|
|
108
|
+
"touching",
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
cochange: {
|
|
112
|
+
comment: "Module -> Module: subject and object are frequently committed together (changeCoupledWith).",
|
|
113
|
+
verbs: [
|
|
114
|
+
"changed with", "co-changes with", "co-change with", "changes alongside",
|
|
115
|
+
"change alongside", "shares commits with", "share commits with",
|
|
116
|
+
"tends to change together with", "tend to change together with",
|
|
117
|
+
"moves together with", "move together with",
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
reexports: {
|
|
121
|
+
comment: "Module -> Module: subject re-exports a name it imported from object.",
|
|
122
|
+
verbs: [
|
|
123
|
+
"exports", "export", "re-exports", "re-export", "passes through", "pass through",
|
|
124
|
+
// gerund (g-drop normalization — §3.5)
|
|
125
|
+
"exporting",
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** relation token -> flat verb-phrase list, the shape ask.mjs's VERB_TO_KIND
|
|
131
|
+
* table needs (phrase -> kind), derived once from RELATIONS. */
|
|
132
|
+
export const VERB_TO_KIND = Object.freeze(
|
|
133
|
+
Object.fromEntries(
|
|
134
|
+
Object.entries(RELATIONS).flatMap(([kind, { verbs }]) => verbs.map((v) => [v, kind])),
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
export const ENTITY_TO_TYPE = Object.freeze({
|
|
139
|
+
function: "Function", functions: "Function",
|
|
140
|
+
method: "Method", methods: "Method",
|
|
141
|
+
class: "Class", classes: "Class",
|
|
142
|
+
module: "Module", modules: "Module", file: "Module", files: "Module",
|
|
143
|
+
attribute: "Attribute", attributes: "Attribute", field: "Attribute", fields: "Attribute",
|
|
144
|
+
variable: "GlobalVariable", variables: "GlobalVariable", global: "GlobalVariable", globals: "GlobalVariable",
|
|
145
|
+
commit: "Commit", commits: "Commit",
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
export const MODIFIER_TO_KIND = Object.freeze({
|
|
149
|
+
explicitly: "direct", directly: "direct",
|
|
150
|
+
transitively: "transitive", indirectly: "transitive",
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ---- §3.5 normalization — contractions/informal spellings that would otherwise
|
|
154
|
+
// block a match, expanded BEFORE parsing (BOTH the anchored-template strategy
|
|
155
|
+
// and the independent keyword-spotting strategy see the same normalized text —
|
|
156
|
+
// this table is not owned by either). Small and code-question-scoped, not a
|
|
157
|
+
// general slang dictionary — every entry here exists because it appears in an
|
|
158
|
+
// actual worked example this file's matcher must handle. ----
|
|
159
|
+
export const CONTRACTIONS = Object.freeze({
|
|
160
|
+
"ain't": "is not", "aint": "is not",
|
|
161
|
+
"isn't": "is not", "isnt": "is not",
|
|
162
|
+
"aren't": "are not", "arent": "are not",
|
|
163
|
+
"doesn't": "does not", "doesnt": "does not",
|
|
164
|
+
"don't": "do not", "dont": "do not",
|
|
165
|
+
"didn't": "did not", "didnt": "did not",
|
|
166
|
+
"there's": "there is", "theres": "there is",
|
|
167
|
+
"what's": "what is", "whats": "what is",
|
|
168
|
+
"who's": "who is", "whos": "who is",
|
|
169
|
+
"gonna": "going to",
|
|
170
|
+
"wanna": "want to",
|
|
171
|
+
"gotta": "got to",
|
|
172
|
+
"yer": "your", "ur": "your",
|
|
173
|
+
"gimme": "give me",
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
/** Trailing g-drop ("callin'", "hittin'") — a plain suffix restore, not a
|
|
177
|
+
* lemmatiser: -in' -> -ing, applied per-word during normalization. The
|
|
178
|
+
* apostrophe is REQUIRED (not optional): bare "-in" endings with no
|
|
179
|
+
* apostrophe are real English words far more often than dropped g's
|
|
180
|
+
* ("cabin", "robin", "chin", "twin") — restoring those would be a false
|
|
181
|
+
* positive the anchored templates never had to worry about. No trailing
|
|
182
|
+
* `\b` after the apostrophe: `'` is a non-word character, so a `\b` there
|
|
183
|
+
* can never match the (non-word) whitespace/end-of-string that follows —
|
|
184
|
+
* the apostrophe itself already delimits the word, a second boundary check
|
|
185
|
+
* after it is redundant and unmatchable. */
|
|
186
|
+
export const G_DROP = /\b([a-z]{3,})in'/gi;
|
|
187
|
+
|
|
188
|
+
/** Words stripped during normalization/keyword-spotting once they carry no
|
|
189
|
+
* grammatical weight for this grammar — greetings, politeness, hedges, and
|
|
190
|
+
* the discourse fillers a spoken-style question picks up. Never strips a
|
|
191
|
+
* relation verb, entity noun, or modifier (those are checked first). */
|
|
192
|
+
export const FILLER_WORDS = Object.freeze([
|
|
193
|
+
"um", "uh", "erm", "so", "like", "yo", "hey", "bru", "bro", "fam", "mate",
|
|
194
|
+
"please", "could you", "can you", "would you", "tell me", "i wonder",
|
|
195
|
+
"just wondering", "quickly", "real quick", "kinda", "sorta",
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
/** Deictic/pronoun terms that refer to a context entity rather than naming one
|
|
199
|
+
* directly — resolved against an optional `contextId` (ask.mjs §keyword-
|
|
200
|
+
* spotting; wired from the graph viewer's currently-selected node, when the
|
|
201
|
+
* chat panel is asking "about" whatever the user last clicked). With no
|
|
202
|
+
* context available (the bare CLI/MCP surface), these produce an honest
|
|
203
|
+
* "which node does 'this' refer to?" miss — never a guess. */
|
|
204
|
+
export const CONTEXT_PRONOUNS = Object.freeze(["this", "it", "that", "here", "this one", "that one"]);
|
|
205
|
+
|
|
206
|
+
// ---- §3.6 negation frames — a SMALL, pattern-based set of recognized
|
|
207
|
+
// double-negative / negative-rhetorical constructions, normalized to the
|
|
208
|
+
// affirmative form of the SAME question (not a general negation-scope parser
|
|
209
|
+
// — ELIZA/PARRY's own scope discipline: a handful of recognized frames, not
|
|
210
|
+
// an attempt at full logical negation handling). Each frame is tried in
|
|
211
|
+
// order; the first match rewrites the whole string and normalization stops
|
|
212
|
+
// (a second negation frame firing on an already-rewritten string would be a
|
|
213
|
+
// design smell, not a feature). ----
|
|
214
|
+
export const NEGATION_FRAMES = Object.freeze([
|
|
215
|
+
// "there ain't nothin' calling it" / "there isn't nothing that calls it"
|
|
216
|
+
// -> "what calls it"
|
|
217
|
+
{ re: /\bthere\s+is\s+not\s+(?:anything|nothing)\s+(?:that\s+)?(\S.*)$/i, to: (m) => `what ${m[1]}` },
|
|
218
|
+
// "there's no module that imports auth" -> "what imports auth"
|
|
219
|
+
{ re: /\bthere\s+is\s+no\s+\S+\s+(?:that\s+)?(\S.*)$/i, to: (m) => `what ${m[1]}` },
|
|
220
|
+
// "isn't there anything calling it" -> "what calls it" (question-inverted form)
|
|
221
|
+
{ re: /\bis\s+not\s+there\s+(?:anything|nothing)\s+(?:that\s+)?(\S.*)$/i, to: (m) => `what ${m[1]}` },
|
|
222
|
+
// "nobody/nothing calls it, does it" (tag-question double-negative) -> "what calls it"
|
|
223
|
+
{ re: /\b(?:nobody|nothing|no one)\s+(\S.*?)(?:,?\s+does\s+it\??|,?\s+do\s+they\??)?$/i, to: (m) => `what ${m[1]}` },
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
// ---- §7 meta-vocabulary — trigger phrases for the "meta" query shape (asking what a
|
|
227
|
+
// graph vocabulary term itself means: "what does cochange mean", "what does mgx:
|
|
228
|
+
// callsSymbol mean"). Deliberately NOT folded into RELATIONS/VERB_TO_KIND: these
|
|
229
|
+
// phrases don't name a graph relation kind for edgesOfKind to traverse, they name
|
|
230
|
+
// "explain this term" intent. Adding them to VERB_TO_KIND would make
|
|
231
|
+
// parseKeywordSpot's decomposition matcher independently derive a *different* shape
|
|
232
|
+
// ("forward", the only shape its before-only-text case produces) for the very same
|
|
233
|
+
// sentence the anchored meta template resolves to shape:"meta" — a spurious strategy
|
|
234
|
+
// DISAGREEMENT (ambiguousParse) on every meta question. So this table is read directly
|
|
235
|
+
// by ask.mjs's own meta template, never merged into the shared verb tables. ----
|
|
236
|
+
export const META_MEANING_VERBS = Object.freeze([
|
|
237
|
+
"mean", "means", "stand for", "stands for", "signify", "signifies",
|
|
238
|
+
"represent", "represents", "refer to", "refers to",
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
// ---- omitted-on-purpose (judgment calls, not oversights) -------------------
|
|
242
|
+
// "runs"/"executes" (calls) are common English words with many non-code
|
|
243
|
+
// senses — accepted anyway, formal-template AND keyword-spotting alike,
|
|
244
|
+
// because a misparse costs nothing beyond an honest object-miss
|
|
245
|
+
// (resolveObject never guesses); the risk is bounded by the render layer,
|
|
246
|
+
// not by narrowing the vocabulary.
|
|
247
|
+
// NOT added: "needs"/"wants" for imports (too weakly code-specific — "which
|
|
248
|
+
// modules need auth" reads as a feature request, not a graph query, in a way
|
|
249
|
+
// "which modules depend on auth" doesn't); "wraps"/"decorates" for calls (a
|
|
250
|
+
// real but distinct relationship codegraph.mjs doesn't classify separately —
|
|
251
|
+
// adding the phrase would silently misroute it onto plain call edges);
|
|
252
|
+
// "belongs to" for contains (already claimed by a hypothetical membership
|
|
253
|
+
// verb elsewhere — kept out to avoid a future collision if one is added).
|
|
254
|
+
// NOT added: a cochange gerund ("co-changing with") — every cochange phrase
|
|
255
|
+
// is already 2-3 words with its own internal "-ing"/"-s" (e.g. "changes
|
|
256
|
+
// alongside"), so g-drop normalization has no bare stem to dialectally
|
|
257
|
+
// contract in the first place; only relations with a genuinely bare-verb
|
|
258
|
+
// casual form ("call", "touch") needed one.
|