@polycode-projects/seonix 0.3.0 → 0.5.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 +93 -23
- package/bin/cli.mjs +27 -4
- package/package.json +1 -1
- package/src/ask-vocab.mjs +262 -2
- package/src/ask.mjs +1025 -8
- package/src/browser.mjs +87 -13
- package/src/chat.mjs +644 -47
- package/src/codegraph.mjs +177 -2
- package/src/nlp-bundle.mjs +120 -0
- package/src/prose.mjs +42 -0
- package/src/temporal.mjs +70 -0
- package/src/timeline.mjs +51 -8
- package/src/viz.mjs +88 -19
package/README.md
CHANGED
|
@@ -7,13 +7,16 @@ bounded **edit digest** you inject once at session start. Your agent edits from
|
|
|
7
7
|
turn 1 without fishing through the repo.
|
|
8
8
|
|
|
9
9
|
> **Headline.** Measured as **mean** input tokens per both-solved task (paired
|
|
10
|
-
> arms, identical model pin)
|
|
11
|
-
>
|
|
12
|
-
>
|
|
13
|
-
>
|
|
14
|
-
> Sonnet 4.6
|
|
15
|
-
>
|
|
16
|
-
>
|
|
10
|
+
> arms, identical model pin). Under the shipped score-gap default (R1b), B017
|
|
11
|
+
> re-tested across three models on a held-out TypeScript corpus and the existing
|
|
12
|
+
> suites: two cells clear the ≥50% decision bar, both on exploration-bound
|
|
13
|
+
> complex tasks — a TypeScript middleware task on **Haiku 4.5 at 59.6%** and a C#
|
|
14
|
+
> service task on **Sonnet 4.6 at 53.5%** — with several more in the 35 to 48%
|
|
15
|
+
> range. Across all measured cells the token cut tracks the turn cut
|
|
16
|
+
> (Pearson 0.91): seonix saves tokens by removing exploration turns, so tasks
|
|
17
|
+
> whose edit site is obvious change little. Indexing costs **$0**. The win is
|
|
18
|
+
> task- and model-conditional; full method, numbers and the measurement contract
|
|
19
|
+
> are in `BENCHMARK_017.md`.
|
|
17
20
|
|
|
18
21
|
- **Website / live demo:** https://seonix.polycode.co.uk
|
|
19
22
|
- **Source + benchmarks:** https://gitlab.com/polycode-projects/seonix
|
|
@@ -49,7 +52,7 @@ removes no turns there is little to save.
|
|
|
49
52
|
```bash
|
|
50
53
|
npm i -g @polycode-projects/seonix # global — gives you the `seonix` bin on PATH
|
|
51
54
|
# or one-shot, no install:
|
|
52
|
-
npx @polycode-projects/seonix
|
|
55
|
+
npx @polycode-projects/seonix cli index_repository '{"repo_path":"/abs/path/to/repo"}'
|
|
53
56
|
```
|
|
54
57
|
|
|
55
58
|
**Runtime prerequisites:** Node ≥ 24, **Python 3**, and **git** on `PATH` (the
|
|
@@ -63,7 +66,7 @@ network calls, **$0**.
|
|
|
63
66
|
|
|
64
67
|
```bash
|
|
65
68
|
# 1. index your repo (deterministic, offline) — writes a machine-local .seonix/
|
|
66
|
-
seonix
|
|
69
|
+
seonix cli index_repository '{"repo_path":"/abs/path/to/repo"}'
|
|
67
70
|
|
|
68
71
|
# 2. self-locate the touched modules from the task text (no human-chosen target)
|
|
69
72
|
seonix cli seonix_search '{"repo_path":"/abs/path/to/repo","query":"<your task>"}'
|
|
@@ -118,23 +121,90 @@ in production; pass a known target (`oracle`) only to measure the headroom.
|
|
|
118
121
|
seonix viz --focus <Symbol> --out graph.html
|
|
119
122
|
```
|
|
120
123
|
|
|
121
|
-
Renders a focused, portable Cytoscape sub-graph of the typed code-map
|
|
122
|
-
|
|
124
|
+
Renders a focused, portable Cytoscape sub-graph of the typed code-map, the same
|
|
125
|
+
view that runs live on the website. By default it also writes a `code-browser.html`
|
|
126
|
+
and a `timeline.html` beside it with a working header nav; pass `--graph-only`
|
|
127
|
+
for just the single self-contained graph file.
|
|
123
128
|
|
|
124
129
|
## Chat
|
|
125
130
|
|
|
126
131
|
```bash
|
|
127
|
-
seonix chat [--repo /abs/path]
|
|
132
|
+
seonix chat [--repo /abs/path] [--with-claude | --with-copilot]
|
|
128
133
|
```
|
|
129
134
|
|
|
130
|
-
An interactive
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
(
|
|
135
|
+
An interactive client over the typed code-graph (deterministic, no model calls).
|
|
136
|
+
With no `--repo` the target defaults to the **git root** (`git rev-parse
|
|
137
|
+
--show-toplevel`), so running it from a nested package dir still indexes the whole
|
|
138
|
+
repo; pass `--repo <path>` to override. Any **bare line** is a plain-English
|
|
139
|
+
question dispatched through the mechanical `seonix_ask` engine — e.g. `which
|
|
140
|
+
modules import walk.mjs` — and the graph answers. Aggregate questions are answered
|
|
141
|
+
straight off the graph header: `how many classes are there` → `20 classes.`
|
|
142
|
+
(`how many <classes|functions|modules|methods|commits>`, `count the …`, `number of
|
|
143
|
+
…`). A near-miss structural question still gets the precise rephrase hint.
|
|
144
|
+
|
|
145
|
+
**Conversational manners** (deterministic, zero-model, recognised *before* any graph
|
|
146
|
+
dispatch). A small closed set of everyday expressions gets a templated reply rather
|
|
147
|
+
than a grammar hint:
|
|
148
|
+
|
|
149
|
+
| you say | you get |
|
|
150
|
+
| --- | --- |
|
|
151
|
+
| `hi` / `hello` / `hey` / `yo` / `g'day` / `good morning` (and `hello there`, with a small nod) | a short greeting + a nudge to ask or `/help` |
|
|
152
|
+
| `thanks` / `ta` / `cheers` / `ty` | an "any time" acknowledgement |
|
|
153
|
+
| `help` / `what can you do` / `who are you` / `?` | the orientation (the friendly help text; see also `/help`, `/stats`) |
|
|
154
|
+
| `bye` / `goodbye` / `quit` / `see ya` | a farewell **and a clean end of session** (flushes the log, closes — the `/exit` path) |
|
|
155
|
+
| `why` / `how` / `explain` / `say more` / `go on` / `elaborate` | **re-renders the last answer verbosely** — repeats it with the ask engine's traversal receipt and the full matched-entity list the terse render trims (no previous answer → it says so plainly) |
|
|
156
|
+
|
|
157
|
+
These turns resolve no code entity, so — like `/help` — they are recorded as plain
|
|
158
|
+
turns and never add `mgx:asksAbout` edges to the session graph.
|
|
159
|
+
|
|
160
|
+
**Slash-commands** reach every other query tool in one line each: `/find <query>`
|
|
161
|
+
(or `/search`) for a lexical search; `/context <symbol>` for the sized edit bundle
|
|
162
|
+
(start here to change code) and `/snippet <symbol>` for exact source; `/describe`,
|
|
163
|
+
`/signature`, `/members <class>`, `/subclasses <class>`, `/impact <module>`,
|
|
164
|
+
`/callers`, `/callees`, `/tests <symbol>`, `/untested`, `/history <symbol>`,
|
|
165
|
+
`/exports <module>` and `/arch [package]`, plus `/stats` for a one-screen overview
|
|
166
|
+
(entity counts, relationship counts, package totals). An unknown `/command` just
|
|
167
|
+
nudges you to `/help`; a bad symbol prints the tool's own clean error, never a stack.
|
|
168
|
+
|
|
169
|
+
The client tracks a **focus** entity for multi-turn context. A command or an answer
|
|
170
|
+
that resolves a primary entity remembers it (the prompt shows it, `seon(walk.mjs)>`);
|
|
171
|
+
a bare `it`/`this`/`that` in a question then binds to it, and a no-argument entity
|
|
172
|
+
command (`/context`, `/impact`, …) reuses it — so `what calls it` works straight
|
|
173
|
+
after `/describe walk.mjs`. Set it explicitly with `/focus <symbol>`. With no focus
|
|
174
|
+
set the client stays single-shot. `/help` lists the commands and the question
|
|
175
|
+
shapes; `/exit` (or Ctrl+C/Ctrl+D) leaves.
|
|
176
|
+
|
|
177
|
+
Each session is appended per turn to `<repo>/.seonix/session-<uuidv7>.log`, and
|
|
178
|
+
becomes first-class graph data like a commit — **slash-command turns included**: a
|
|
179
|
+
structured sidecar (`.seonix/sessions/*.jsonl`) records each turn's query, the
|
|
180
|
+
command used, and the entities it resolved/answered; the session enters `graph.json`
|
|
181
|
+
as a `Session` individual with `mgx:asksAbout` edges wherever a turn resolved an
|
|
182
|
+
entity (re-attached on every re-index), and it appears on the `seonix viz` timeline.
|
|
183
|
+
|
|
184
|
+
### Opt-in LLM fallback (`--with-claude` / `--with-copilot`)
|
|
185
|
+
|
|
186
|
+
**Default OFF. Chat-only. Never in the benchmark, digest, or MCP path.** With a flag
|
|
187
|
+
set, and *only* when the mechanical engine returns a genuine **miss** on a bare
|
|
188
|
+
question (after the conversational layer and slash-commands have had their turn),
|
|
189
|
+
chat consults an external LLM CLI — spawned in the target repo as cwd, with a
|
|
190
|
+
timeout. To keep answers **graph-grounded** it asks the model to *compile one seonix
|
|
191
|
+
command* (given the tool surface), then runs that command mechanically and shows the
|
|
192
|
+
grounded result, **labelled `[LLM-assisted via …]`**. If the model can't produce a
|
|
193
|
+
valid command, its raw text is shown instead, clearly marked `[answer from external
|
|
194
|
+
model … — NOT the deterministic seonix engine]`; the two are never silently blended.
|
|
195
|
+
|
|
196
|
+
- `--with-claude` → `claude -p "<compile-prompt>" --model haiku` (cheapest model).
|
|
197
|
+
- `--with-copilot` → `gh copilot -- -p "<compile-prompt>" --allow-all-tools --no-color`.
|
|
198
|
+
**Limitation, honestly:** the GitHub Copilot CLI is an *agentic shell assistant*
|
|
199
|
+
(its native verbs are running commands and editing files), not a graph Q&A tool —
|
|
200
|
+
it has no knowledge of the seonix query surface beyond what the prompt hands it, so
|
|
201
|
+
it's a poorer fit than `claude -p` and is wired best-effort. Prefer `--with-claude`.
|
|
202
|
+
|
|
203
|
+
Any failure — tool binary absent, non-zero exit, unparseable output, or timeout —
|
|
204
|
+
degrades to the normal honest-miss hint; the flag never crashes or hangs the chat. If
|
|
205
|
+
the flag is set but the binary is missing, chat says so **once at startup** and
|
|
206
|
+
continues mechanical-only. No LLM call is ever made unless the flag is set **and** the
|
|
207
|
+
mechanical engine missed — the hot path (and every benchmark run) stays zero-model.
|
|
138
208
|
|
|
139
209
|
A real transcript against this repository's own index (verified answers):
|
|
140
210
|
|
|
@@ -170,9 +240,9 @@ a rephrase hint, never a guess.
|
|
|
170
240
|
|
|
171
241
|
## Languages
|
|
172
242
|
|
|
173
|
-
Python (stdlib `ast`), JS/TS (
|
|
174
|
-
|
|
175
|
-
graph. The C# Roslyn helper's source ships in the package under `roslyn/`; build
|
|
243
|
+
Python (stdlib `ast`), JS/TS (the TypeScript compiler API, including CommonJS
|
|
244
|
+
`require`/`module.exports`), C# (Roslyn, with a tree-sitter fallback) and Java
|
|
245
|
+
(JavaParser, with a tree-sitter fallback), all merged into the one typed graph. The C# Roslyn helper's source ships in the package under `roslyn/`; build
|
|
176
246
|
it on the consumer side (needs the .NET SDK) if you index C#. The Java JavaParser
|
|
177
247
|
helper is a jar built from the repository's `java/` directory (`mvn package`) and
|
|
178
248
|
is not included in the npm package, so a package install indexes Java with the
|
package/bin/cli.mjs
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// 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). By default also writes code-browser.html + timeline.html next to --out with a working header nav; --graph-only suppresses the siblings
|
|
30
30
|
// seonix viz --browser-out f.html [--browser-data-out f.json] [--timeline-out f.html] [--scope product|<prefixes>] → override the sibling artifact paths (Chronograph code browser: temporal scrub + two-cursor diff + narration + ghost-branch merges + keyboard nav; archive/PLAN_CODE_BROWSER.md)
|
|
31
31
|
// 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)
|
|
32
|
-
// seonix chat [--repo <abs>]
|
|
32
|
+
// seonix chat [--repo <abs>] [--with-claude|--with-copilot] → interactive prompt over the mechanical seonix_ask engine; /exit to leave; session log → <repo>/.seonix/session-<uuidv7>.log. The optional --with-* flags add an opt-in LLM fallback used ONLY when the mechanical engine misses a bare question (chat-only, never in the benchmark path)
|
|
33
33
|
//
|
|
34
34
|
// The index step is offline and model-free (Python ast + git). It writes the
|
|
35
35
|
// graph artifact to <repo_path>/.seonix/graph.json; the server (started with
|
|
@@ -81,7 +81,12 @@ async function runDigest(args) {
|
|
|
81
81
|
let autoSelected = null; // for the header, when `query` drove selection
|
|
82
82
|
if (!modules.length && args.query) {
|
|
83
83
|
const graph = parseEntities(await source.fetchEntities(configFor(repoPath)));
|
|
84
|
-
|
|
84
|
+
// SHIPPED DEFAULT (0.5.0): the no-MCP digest's query-mode auto-locate resolves literal-mention ON
|
|
85
|
+
// (a fresh invocation with no seonix.toml), disable-able via `literal_mention:false`. Kept in
|
|
86
|
+
// lockstep with the `seonix_locate` handler so `cli digest '{query}'` ≡ `cli seonix_locate` for the
|
|
87
|
+
// same query. A strict no-op unless the query carries a ≥3-component dotted path / repo-relative
|
|
88
|
+
// path; searchModulesRanked derives rawQuery from the query when literalMention is on.
|
|
89
|
+
const ranked = searchModulesRanked(graph, args.query, { literalMention: args.literal_mention !== false });
|
|
85
90
|
const scoreGapK = args.score_gap === false ? null : (Number.isFinite(args.score_gap) ? args.score_gap : DEFAULT_SCORE_GAP);
|
|
86
91
|
modules = selectRankedModules(ranked, { top_k: Number.isFinite(args.top_k) ? args.top_k : 2, scoreGapK }).slice(0, DIGEST_MODULE_CAP);
|
|
87
92
|
autoSelected = modules;
|
|
@@ -277,6 +282,18 @@ async function main() {
|
|
|
277
282
|
implOfInterface: !!args.impl_of_interface, // E1b: C# impl-of-interface boost
|
|
278
283
|
beamSearch: !!args.beam_search, // §5.15: multi-ply discriminative expansion
|
|
279
284
|
...(Number.isFinite(Number(args.beam_width)) ? { beamWidth: Number(args.beam_width) } : {}),
|
|
285
|
+
// B018 §8.1.3 literal-mention lever: match verbatim dotted-name/path mentions in the RAW
|
|
286
|
+
// query (which the locate tokenizer destroys). searchModulesRanked derives rawQuery from the
|
|
287
|
+
// query arg when literalMention is on; the rig passes the raw problem as the query, so literal
|
|
288
|
+
// matching keys off the untokenized text. raw_query is forwarded too for callers that normalize
|
|
289
|
+
// the query arg separately from the raw problem text.
|
|
290
|
+
// SHIPPED DEFAULT (0.5.0): literal-mention is ON for a fresh invocation (no arg, no
|
|
291
|
+
// seonix.toml) — pass `literal_mention:false` to disable. It is a strict no-op on queries
|
|
292
|
+
// with no ≥3-component dotted path / repo-relative path, so it never perturbs the cells the
|
|
293
|
+
// headline B018 numbers were measured on. The low-level scoreModules default (codegraph.mjs)
|
|
294
|
+
// stays literalMention=false; the product surface opts in explicitly, right here.
|
|
295
|
+
literalMention: args.literal_mention !== false,
|
|
296
|
+
...(args.raw_query != null ? { rawQuery: String(args.raw_query) } : {}),
|
|
280
297
|
});
|
|
281
298
|
process.stdout.write(ranked.map((r) => `${r.path}\t${r.score}`).join("\n") + "\n");
|
|
282
299
|
} catch (e) {
|
|
@@ -365,7 +382,13 @@ async function main() {
|
|
|
365
382
|
if (mode === "chat") {
|
|
366
383
|
const { runChat } = await import("../src/chat.mjs");
|
|
367
384
|
const i = process.argv.indexOf("--repo");
|
|
368
|
-
|
|
385
|
+
// Opt-in LLM fallback (chat-only, default OFF, miss-gated — never in the benchmark
|
|
386
|
+
// path): --with-claude shells `claude -p`, --with-copilot shells `gh copilot -- -p`.
|
|
387
|
+
await runChat({
|
|
388
|
+
repoPath: i !== -1 ? process.argv[i + 1] : undefined,
|
|
389
|
+
withClaude: process.argv.includes("--with-claude"),
|
|
390
|
+
withCopilot: process.argv.includes("--with-copilot"),
|
|
391
|
+
});
|
|
369
392
|
return;
|
|
370
393
|
}
|
|
371
394
|
|
|
@@ -375,7 +398,7 @@ async function main() {
|
|
|
375
398
|
}
|
|
376
399
|
|
|
377
400
|
process.stderr.write(`seonix: unknown invocation "${process.argv.slice(2).join(" ")}". ` +
|
|
378
|
-
"Use bare (MCP server), `cli index_repository …`, `hook-augment`, or `viz`.\n");
|
|
401
|
+
"Use bare (MCP server), `cli index_repository …`, `cli <tool> …`, `chat`, `hook-augment`, or `viz`.\n");
|
|
379
402
|
process.exit(2);
|
|
380
403
|
}
|
|
381
404
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/seonix",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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.",
|
package/src/ask-vocab.mjs
CHANGED
|
@@ -109,8 +109,13 @@ export const RELATIONS = {
|
|
|
109
109
|
"derives from", "derive from", "is a subclass of", "are a subclass of",
|
|
110
110
|
"is a kind of", "are a kind of", "is built off", "are built off",
|
|
111
111
|
"is built on top of", "are built on top of",
|
|
112
|
-
// gerund (g-drop normalization — §3.5
|
|
113
|
-
"
|
|
112
|
+
// gerund (g-drop normalization — §3.5, and the compositional grammar's
|
|
113
|
+
// gerund-led boolean gate: "classes inheriting from Base but not tested").
|
|
114
|
+
// Both the two-word "inheriting from" (so "from" is consumed into the verb
|
|
115
|
+
// phrase, not the object term) and the bare "inheriting" (the single token
|
|
116
|
+
// the gerund-lead check reads) are listed; longest-match-first prefers the
|
|
117
|
+
// two-word form when "from" follows.
|
|
118
|
+
"extending", "inheriting from", "inheriting", "subclassing", "extends from",
|
|
114
119
|
],
|
|
115
120
|
},
|
|
116
121
|
touches: {
|
|
@@ -293,6 +298,18 @@ export const MISSPELLINGS = Object.freeze({
|
|
|
293
298
|
"waht": "what",
|
|
294
299
|
"dose": "does", "doess": "does",
|
|
295
300
|
"teh": "the",
|
|
301
|
+
// aggregate/list TRIGGER words (2026-07-02, trigger-typo work) — a typo of a count
|
|
302
|
+
// or list trigger used to be DROPPED as unmatched by the relaxation cascade, losing
|
|
303
|
+
// the aggregate/list INTENT entirely ("how manyn classes" → the count was lost);
|
|
304
|
+
// curated here so the intended trigger is restored BEFORE parsing (the general
|
|
305
|
+
// bounded fuzzy path in ask.mjs's cascade is the backstop for uncurated typos).
|
|
306
|
+
"manyn": "many", "mnay": "many", "amny": "many", "mnany": "many",
|
|
307
|
+
"coutn": "count", "conut": "count", "cuont": "count", "ocunt": "count",
|
|
308
|
+
"numer": "number", "nubmer": "number", "numbr": "number", "nmuber": "number",
|
|
309
|
+
"lst": "list", "lsit": "list", "ilst": "list",
|
|
310
|
+
"shwo": "show", "hsow": "show",
|
|
311
|
+
"dispaly": "display", "dsiplay": "display",
|
|
312
|
+
"funtcions": "functions", "funciton": "function", "funcitons": "functions",
|
|
296
313
|
});
|
|
297
314
|
|
|
298
315
|
/** Words used INCORRECTLY but with clear intent, mapped to the canonical schema
|
|
@@ -401,6 +418,249 @@ export const META_MEANING_VERBS = Object.freeze([
|
|
|
401
418
|
"represent", "represents", "refer to", "refers to",
|
|
402
419
|
]);
|
|
403
420
|
|
|
421
|
+
// ---- §compositional grammar vocabulary (PLAN §5.16 P3 — the ask engine's step up
|
|
422
|
+
// from ELIZA keyword-spotting to a real recursive-descent grammar over CLAUSES).
|
|
423
|
+
// ask.mjs's parseComposite reads these tables to recognize the compositional
|
|
424
|
+
// MARKERS — relative clauses, boolean connectives, subject qualifiers, aggregates,
|
|
425
|
+
// superlatives, and anaphora — that compose the SAME closed clause vocabulary
|
|
426
|
+
// (RELATIONS/ENTITY_TO_TYPE above) into multi-hop / set-algebra queries. The
|
|
427
|
+
// grammar COMPOSES the closed vocabulary; it never opens it. Every table here is
|
|
428
|
+
// curated + closed, same discipline as everything above: a marker the tables don't
|
|
429
|
+
// carry is an honest miss, never a guess. ----
|
|
430
|
+
|
|
431
|
+
/** Relative-clause introducers: "<noun> that/which/who <predicate>". Only ever
|
|
432
|
+
* read as a relative pronoun when it sits AFTER a noun and BEFORE a predicate
|
|
433
|
+
* (ask.mjs enforces both) — "that" is also a CONTEXT_PRONOUN ("what calls that"),
|
|
434
|
+
* so position, not mere presence, decides. This gates the nested/relative and
|
|
435
|
+
* same-subject boolean shapes: their absence is exactly what keeps the bare
|
|
436
|
+
* reverse-template query "which classes extends Base and couples to logging" out
|
|
437
|
+
* of the compositional path (it stays the existing two-strategy ambiguous parse). */
|
|
438
|
+
export const RELATIVE_PRONOUNS = Object.freeze(["that", "which", "who"]);
|
|
439
|
+
|
|
440
|
+
/** Placeholder object nouns — the indefinite "something" in "what calls something
|
|
441
|
+
* that imports X": they name NO entity, they stand in for the inner clause's
|
|
442
|
+
* result set. Mapped to entityType null (any grain); a real entity noun in the
|
|
443
|
+
* same slot ("what calls modules that import X") narrows the grain instead. */
|
|
444
|
+
export const PLACEHOLDER_NOUNS = Object.freeze([
|
|
445
|
+
"something", "anything", "everything", "somethings", "things", "thing",
|
|
446
|
+
"entities", "entity", "nodes", "node", "stuff", "code",
|
|
447
|
+
// "symbol(s)" is a grain-agnostic stand-in for any code entity — "exported
|
|
448
|
+
// symbols of X" means whatever X defines, at any grain, filtered by the qualifier.
|
|
449
|
+
"symbol", "symbols",
|
|
450
|
+
]);
|
|
451
|
+
|
|
452
|
+
/** Boolean connectives over same-subject clauses -> a set operation on result ids.
|
|
453
|
+
* "and" = intersection, "or" = union, "but not"/"and not"/"without"/"except" =
|
|
454
|
+
* difference. Multi-word keys are matched longest-first by ask.mjs so "but not"
|
|
455
|
+
* wins over a bare "not". Left-associative in ask.mjs's fold. */
|
|
456
|
+
export const BOOLEAN_CONNECTIVES = Object.freeze({
|
|
457
|
+
"but not": "difference", "and not": "difference", "except": "difference",
|
|
458
|
+
"without": "difference",
|
|
459
|
+
"and": "intersection", "plus": "intersection",
|
|
460
|
+
"or": "union",
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
/** Subject QUALIFIERS (adjectives) -> a post-filter over the result set, read off
|
|
464
|
+
* attributes/edges the graph already carries (codegraph.mjs): `visibility`
|
|
465
|
+
* (private/protected present; public is the default/absent), `isStatic`/
|
|
466
|
+
* `isConstant`/`isAbstract` boolean attrs, the reexports edge set (exported), and
|
|
467
|
+
* the tests edge set (tested/untested). `isAbstract` is never populated by any
|
|
468
|
+
* extractor today (codegraph.mjs says so) — "abstract methods" therefore honestly
|
|
469
|
+
* returns an empty set, not an error, exactly like any other zero-hit answer. An
|
|
470
|
+
* UNKNOWN qualifier is an honest miss naming these supported ones (ask.mjs). */
|
|
471
|
+
export const QUALIFIERS = Object.freeze({
|
|
472
|
+
public: { via: "visibility", value: "public" },
|
|
473
|
+
private: { via: "visibility", value: "private" },
|
|
474
|
+
protected: { via: "visibility", value: "protected" },
|
|
475
|
+
static: { via: "attr", attr: "isStatic" },
|
|
476
|
+
abstract: { via: "attr", attr: "isAbstract" },
|
|
477
|
+
constant: { via: "attr", attr: "isConstant" },
|
|
478
|
+
exported: { via: "exported" },
|
|
479
|
+
"re-exported": { via: "exported" },
|
|
480
|
+
tested: { via: "tested", value: true },
|
|
481
|
+
covered: { via: "tested", value: true },
|
|
482
|
+
untested: { via: "tested", value: false },
|
|
483
|
+
uncovered: { via: "tested", value: false },
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
/** Aggregate/count triggers: "how many <kind> …", "count <kind>s", "number of
|
|
487
|
+
* <kind>". Answered by counting a class of individuals or a clause's result set —
|
|
488
|
+
* no header magic, a straight count over the graph (ask.mjs). Register-spread the
|
|
489
|
+
* same way RELATIONS' verbs are (2026-07-02): the count question has a formal
|
|
490
|
+
* ("how many"/"number of"), a neutral/imperative ("count"/"count up"), and a
|
|
491
|
+
* quantity ("quantity of"/"total number of") register a developer actually types.
|
|
492
|
+
* Judgement calls, kept OUT deliberately: bare "tally"/"sum"/"total" — those are
|
|
493
|
+
* already mapped to "count" by CASCADE_SYNONYMS (ask.mjs's relaxation layer), and a
|
|
494
|
+
* cascade test asserts "tally the classes" reaches the count via that synonym path,
|
|
495
|
+
* so promoting them to direct triggers here would both duplicate the mapping and
|
|
496
|
+
* break that test; and single-word "total"/"sum" would false-match identifier
|
|
497
|
+
* fragments ("total price", "sum of squares") the count intent never meant. */
|
|
498
|
+
export const AGGREGATE_TRIGGERS = Object.freeze([
|
|
499
|
+
// formal ("the number of classes" reaches "number of" once the cascade strips the
|
|
500
|
+
// leading article — keeping the trigger list clear of "the" so it never enters
|
|
501
|
+
// CONTENT_VOCAB and blocks the article's own noise-strip)
|
|
502
|
+
"how many", "how much", "how many of", "number of",
|
|
503
|
+
"total number of", "quantity of",
|
|
504
|
+
// neutral / imperative
|
|
505
|
+
"count", "count up", "count of", "tot up",
|
|
506
|
+
]);
|
|
507
|
+
|
|
508
|
+
/** LIST triggers (2026-07-02, list shape) — the many ways a developer asks to SEE the
|
|
509
|
+
* individuals of a kind ("list functions", "show me the classes", "what are the
|
|
510
|
+
* modules"). Read by ask.mjs's parseList (a sibling of the count node): a trigger
|
|
511
|
+
* followed by an entity kind noun lists that class (capped at OVERFLOW_CAP), a
|
|
512
|
+
* trailing scope/predicate narrows it, and an unknown kind after a clear list trigger
|
|
513
|
+
* is an honest miss naming the kinds. Two registers, wide-but-deliberate (the operator's
|
|
514
|
+
* "err toward inclusion", bounded by the file-header discipline: a phrase earns its
|
|
515
|
+
* place only if it genuinely means "enumerate these" and won't false-match an unrelated
|
|
516
|
+
* identifier — parseList further requires a real entity kind to follow, so a stray
|
|
517
|
+
* "show"/"name" in another question is never seized):
|
|
518
|
+
* · IMPERATIVE — "<verb> [me/us] [the] <kind>". Bare determiners/objects (the/a/all/
|
|
519
|
+
* me/us) after the verb are skipped by parseList's LIST_SKIP, so only the verb stem
|
|
520
|
+
* is listed here (not every "... the"/"... all" inflection).
|
|
521
|
+
* · INTERROGATIVE — "what/which are [the] <kind>"; the bare "what <kind>"/"which
|
|
522
|
+
* <kind>" (+ optional "are there") form is handled directly in parseList, not here.
|
|
523
|
+
* Kept OUT on purpose: "tell me" / "give me" collide only where normalizeQuery already
|
|
524
|
+
* strips them as FILLER_WORDS ("tell me the classes" → "the classes"), so "tell me" is
|
|
525
|
+
* omitted (it never survives to parseList); "gimme" is omitted because CONTRACTIONS
|
|
526
|
+
* rewrites it to "give me" before parseList runs. */
|
|
527
|
+
export const LIST_TRIGGERS = Object.freeze([
|
|
528
|
+
// imperative — "<verb> [me/us] [the] <kind>"
|
|
529
|
+
"list", "show", "show me", "show us", "display", "print", "print out",
|
|
530
|
+
"dump", "enumerate", "name", "give me", "get me", "spit out", "rattle off",
|
|
531
|
+
"run down", "run through", "ls",
|
|
532
|
+
// interrogative — "what/which are [the] <kind>"
|
|
533
|
+
"what are", "which are",
|
|
534
|
+
]);
|
|
535
|
+
|
|
536
|
+
/** Superlative extremes -> ranking direction. "most/greatest/highest/biggest/
|
|
537
|
+
* largest/most-connected" rank descending; "fewest/least/smallest/lowest" rank
|
|
538
|
+
* ascending. Read by ask.mjs's parseSuperlative alongside EDGE_NOUN_TO_METRIC. */
|
|
539
|
+
export const SUPERLATIVE_EXTREMES = Object.freeze({
|
|
540
|
+
most: "most", greatest: "most", highest: "most", biggest: "most",
|
|
541
|
+
largest: "most", "most-connected": "most", "most connected": "most",
|
|
542
|
+
fewest: "fewest", least: "fewest", smallest: "fewest", lowest: "fewest",
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
/** Degree-metric nouns for superlatives ("which module has the most <noun>") ->
|
|
546
|
+
* {kind, dir} over the SAME classified edge groups. dir "out" counts edges where
|
|
547
|
+
* the ranked entity is the subject (its own imports/calls); "in" counts edges
|
|
548
|
+
* where it is the object (its importers/callers/tests). "connections"/"edges"/
|
|
549
|
+
* "connected" is the total (both directions, all structural kinds). A `sibling`
|
|
550
|
+
* fine-grained kind is added to the tally when present (callers include the
|
|
551
|
+
* symbol-grain callsSymbol edges, not just module-coarse calls); a `filter`
|
|
552
|
+
* restricts the counted objects to one class ("methods"). */
|
|
553
|
+
export const EDGE_NOUN_TO_METRIC = Object.freeze({
|
|
554
|
+
imports: { kind: "imports", dir: "out" },
|
|
555
|
+
dependencies: { kind: "imports", dir: "out" },
|
|
556
|
+
importers: { kind: "imports", dir: "in" },
|
|
557
|
+
dependents: { kind: "imports", dir: "in" },
|
|
558
|
+
callers: { kind: "calls", dir: "in", sibling: "callsSymbol" },
|
|
559
|
+
callees: { kind: "calls", dir: "out", sibling: "callsSymbol" },
|
|
560
|
+
calls: { kind: "calls", dir: "out", sibling: "callsSymbol" },
|
|
561
|
+
methods: { kind: "contains", dir: "out", filter: "Method" },
|
|
562
|
+
members: { kind: "contains", dir: "out" },
|
|
563
|
+
tests: { kind: "tests", dir: "in" },
|
|
564
|
+
subclasses: { kind: "inherits", dir: "in" },
|
|
565
|
+
connections: { kind: "*", dir: "both" },
|
|
566
|
+
edges: { kind: "*", dir: "both" },
|
|
567
|
+
connected: { kind: "*", dir: "both" },
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
/** Anaphora triggers over the PREVIOUS result set (ask()'s `prev` id array):
|
|
571
|
+
* "which of those/them/these …", "how many of those …". The pronoun refers to the
|
|
572
|
+
* last answer's ids, not a graph term — with no prev supplied it is an honest miss
|
|
573
|
+
* (ask.mjs), never a guess, exactly like an unresolved context pronoun. */
|
|
574
|
+
export const ANAPHORA_TRIGGERS = Object.freeze(["those", "them", "these"]);
|
|
575
|
+
|
|
576
|
+
/** Membership relations for "<entity> of/in <term>" (qualifier/relative inner
|
|
577
|
+
* clauses like "public methods of Widget", "untested functions in walk.mjs") ->
|
|
578
|
+
* the forward edge kinds whose subject is <term> and whose objects are the
|
|
579
|
+
* members. contains (Class->member) and defines (Module->symbol) are both tried;
|
|
580
|
+
* the asked entity type narrows the result. */
|
|
581
|
+
export const MEMBERSHIP_KINDS = Object.freeze(["contains", "defines"]);
|
|
582
|
+
|
|
583
|
+
// ---- §progressive-relaxation cascade vocabulary (SHRDLU-in-a-code-graph, with a
|
|
584
|
+
// Zork parser's forgiveness) — the three closed, curated tables ask.mjs's relaxParse
|
|
585
|
+
// reads when the DIRECT parse of a query would MISS. The cascade only ever DROPS
|
|
586
|
+
// noise/unmatched words or NORMALISES a near-canonical word to the closed vocabulary;
|
|
587
|
+
// it never invents a term or guesses an entity, and it bottoms out in the same honest
|
|
588
|
+
// rephrase hint. Every entry is hand-curated with inline provenance, same "closed is
|
|
589
|
+
// deliberate" ethos as everything above: a word these tables don't carry is left for
|
|
590
|
+
// the honest miss, never a general-English stoplist that would silently eat a real
|
|
591
|
+
// code term. ----
|
|
592
|
+
|
|
593
|
+
/** Politeness / filler / vocative / presentation-frame tokens the cascade may strip
|
|
594
|
+
* ONE AT A TIME (leftmost first) when a query misses — but only ever a token that is
|
|
595
|
+
* NOT itself a content vocabulary word (a relation verb, entity noun, modifier,
|
|
596
|
+
* qualifier, aggregate/superlative trigger, …) and does NOT resolve to a graph entity
|
|
597
|
+
* (ask.mjs guards both), so a module literally named "show" or "the" is never eaten.
|
|
598
|
+
* This is a SUPERSET of the multi-word politeness FILLER_WORDS strips up-front during
|
|
599
|
+
* normalization: those handle "could you"/"tell me"/"please" before either parse runs;
|
|
600
|
+
* these single tokens catch what a spoken-style question keeps AFTER that pass — the
|
|
601
|
+
* bare vocative ("matey"), the article ("the"/"a"), and the presentation frame words
|
|
602
|
+
* ("show"/"me"/"list") that the compositional grammar already skips as FRAME_WORDS but
|
|
603
|
+
* the keyword-spotting strategy does not, so an un-stripped "show me" otherwise
|
|
604
|
+
* decomposes to a bogus ask{subject:"show me"}. Curated, not a general stoplist:
|
|
605
|
+
* question words (what/which/…), connectives (and/or), and pronouns (this/it/that)
|
|
606
|
+
* are deliberately ABSENT — they carry grammatical weight and must survive. */
|
|
607
|
+
export const CASCADE_NOISE = Object.freeze([
|
|
608
|
+
// articles / vague determiners (kept OUT of content vocab so they're strippable;
|
|
609
|
+
// the aggregate/where parsers already tolerate a stray "the"/"a", so stripping is
|
|
610
|
+
// belt-and-braces, not load-bearing)
|
|
611
|
+
"the", "a", "an", "some",
|
|
612
|
+
// topic lead-in filler — "what about the modules", "how about classes": "about"
|
|
613
|
+
// carries no graph meaning here, so stripping it lets the bare kind noun surface for
|
|
614
|
+
// the cascade's bare-kind-noun terminal rule (ask.mjs). ("what"/"how" are structural
|
|
615
|
+
// question words the drop-pass keeps; only the "about" between them and the kind is
|
|
616
|
+
// noise.) A module literally named "about" is safe-listed by relaxParse's resolvesExact
|
|
617
|
+
// guard, same as every other noise token.
|
|
618
|
+
"about",
|
|
619
|
+
// politeness / hedges (single-token; multi-word "could you"/"please" etc. are
|
|
620
|
+
// FILLER_WORDS, stripped earlier during normalization)
|
|
621
|
+
"please", "pls", "plz", "kindly", "just", "simply", "maybe", "perhaps",
|
|
622
|
+
"thanks", "thank", "ta", "cheers",
|
|
623
|
+
// greetings a question sometimes opens with (chat.mjs owns standalone greetings;
|
|
624
|
+
// here they're only stripped when embedded in an otherwise-real question)
|
|
625
|
+
"hi", "hello", "hey", "yo", "hiya", "howdy", "ok", "okay",
|
|
626
|
+
// vocatives / terms of address (the "matey" of the worked example, and its kin)
|
|
627
|
+
"matey", "mate", "buddy", "pal", "dude", "man", "bro", "bru", "fam",
|
|
628
|
+
"friend", "sir", "maam", "folks", "guys", "everyone", "dear",
|
|
629
|
+
// presentation frames — the keyword-spotting strategy's blind spot: the
|
|
630
|
+
// compositional grammar skips these as FRAME_WORDS, but "show me what imports X"
|
|
631
|
+
// otherwise decomposes (via keyword-spot) to ask{subject:"show me"}. Stripping
|
|
632
|
+
// them on a miss recovers the underlying reverse/forward question. ("count" is NOT
|
|
633
|
+
// here — it is an aggregate trigger; "find"/"search" are here as presentation
|
|
634
|
+
// verbs, not the seonix_search tool, which ask.mjs never dispatches.)
|
|
635
|
+
"show", "tell", "give", "list", "find", "me", "us", "lemme",
|
|
636
|
+
]);
|
|
637
|
+
|
|
638
|
+
/** Near-canonical words the cascade REWRITES to the closed vocabulary once noise and
|
|
639
|
+
* unmatched tokens are gone (SYNONYM-NORMALISE, the cascade's third layer). Kept
|
|
640
|
+
* deliberately TINY and aggregate-flavoured: the relation/entity synonyms a developer
|
|
641
|
+
* actually types already live in RELATIONS/ENTITY_TO_TYPE (and "uses"/"depends on" are
|
|
642
|
+
* mapped there); this table only closes the gap for the count family, whose triggers
|
|
643
|
+
* (AGGREGATE_TRIGGERS) don't include the "tally the classes"/"total number of classes"
|
|
644
|
+
* register. Each key is also kept OUT of the drop pass (ask.mjs treats a synonym key as
|
|
645
|
+
* meaningful) so it survives to be normalised rather than dropped as unmatched. The
|
|
646
|
+
* rewrite is guarded by ask.mjs (never applied to a token that resolves to a graph
|
|
647
|
+
* entity), so a symbol named "total" is never bent into "count". */
|
|
648
|
+
export const CASCADE_SYNONYMS = Object.freeze({
|
|
649
|
+
tally: "count", tallies: "count", sum: "count", total: "count", totals: "count",
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
/** Explicit help / orientation requests — when the WHOLE query is one of these, ask.mjs
|
|
653
|
+
* shows the rephrase hint DIRECTLY (the honest bottom of the cascade, reached on
|
|
654
|
+
* demand) rather than pretending to answer or running the relaxation loop. A closed
|
|
655
|
+
* set matched against the whole normalized query only, so "which functions call help"
|
|
656
|
+
* (a real question about a symbol named "help") is untouched. Standalone greetings and
|
|
657
|
+
* the chat "/help" command are chat.mjs's own surface; this is the bare CLI/MCP ask()
|
|
658
|
+
* entry point's equivalent. */
|
|
659
|
+
export const HELP_TRIGGERS = Object.freeze([
|
|
660
|
+
"help", "help me", "how do i ask", "how do i use this", "what can i ask",
|
|
661
|
+
"what can you ask", "usage", "commands", "examples", "syntax", "options",
|
|
662
|
+
]);
|
|
663
|
+
|
|
404
664
|
// ---- omitted-on-purpose (judgment calls, not oversights) -------------------
|
|
405
665
|
// "runs"/"executes" (calls) are common English words with many non-code
|
|
406
666
|
// senses — accepted anyway, formal-template AND keyword-spotting alike,
|