@polycode-projects/the-mechanical-code-talker 0.3.0 → 0.4.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 +77 -3
- package/ROADMAP.md +411 -1
- package/bin/tmct.mjs +56 -1
- package/data/templates/grammar-rules.toml +89 -0
- package/data/templates/responses.jsonl +13 -0
- package/package.json +30 -2
- package/src/ask-nlp.mjs +8 -10
- package/src/ask-vocab.mjs +22 -0
- package/src/ask.mjs +80 -2
- package/src/chat.mjs +576 -50
- package/src/corpus/conceptnet.mjs +14 -2
- package/src/corpus/templates.mjs +94 -10
- package/src/finish.mjs +443 -0
- package/src/hash.mjs +32 -0
- package/src/init.mjs +264 -0
- package/src/interpret/normalize.mjs +34 -0
- package/src/interpret/strategies/keywords.mjs +57 -1
- package/src/memory/blocks.mjs +23 -3
- package/src/memory/core.mjs +257 -16
- package/src/memory/fold.mjs +0 -0
- package/src/memory/inspect.mjs +141 -0
- package/src/memory/trust.mjs +113 -0
- package/src/prose-nlp.mjs +14 -16
- package/src/providers/bootstrap.mjs +24 -0
- package/src/providers/fixture.mjs +118 -0
- package/src/providers/graph-service.mjs +312 -0
- package/src/repository-interface.mjs +318 -0
- package/src/server.mjs +44 -28
- package/src/sessions.mjs +13 -2
- package/src/syllogise.mjs +0 -0
- package/src/toml-config.mjs +14 -0
- package/src/wink-model.mjs +74 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# grammar-rules.toml — the data-driven grammar-rule table (Phase 7, lever 2)
|
|
2
|
+
# (PLAN_RESPONSE_FINISHING.md, "The grammar pass (lever 2)").
|
|
3
|
+
#
|
|
4
|
+
# Each [[rule]] is a corrective grammar rule applied by applyGrammar() in
|
|
5
|
+
# src/finish.mjs over the PROSE spans of a segmented answer — NEVER the flat
|
|
6
|
+
# string, and NEVER a protected span (entity / path / number / code / provenance
|
|
7
|
+
# / receipt). The engine reads STRUCTURE, never guesses from surface: an article
|
|
8
|
+
# rule that would touch the word inside the following protected span refuses to
|
|
9
|
+
# fire, agreement reads the following number's value, and so on.
|
|
10
|
+
#
|
|
11
|
+
# CONTRACT (the plan's law): a rule's NEUTRAL behaviour is BYTE-STABLE. The only
|
|
12
|
+
# byte changes a rule may introduce are GENUINE fixes to defects tmct itself
|
|
13
|
+
# generates ("a artifact"). `kind` selects the built-in handler; `enabled=false`
|
|
14
|
+
# PARKS a rule out of the live answer path; the remaining keys are that handler's
|
|
15
|
+
# applicability conditions + parameters. Rules apply in file order; the set is
|
|
16
|
+
# chosen to commute so finish() is idempotent: finish(finish(x)) === finish(x).
|
|
17
|
+
#
|
|
18
|
+
# SEQUENCING (PLAN_RESPONSE_FINISHING.md, "one grammar rule per tuning cycle"):
|
|
19
|
+
# only ARTICLE-SELECTION is live this cycle — it fixes a genuine defect with a
|
|
20
|
+
# narrow, safe blast radius. AGREEMENT, CAPITALISATION, LIST and TERMINAL are
|
|
21
|
+
# fully implemented and golden-tested IN ISOLATION, but PARKED (enabled=false):
|
|
22
|
+
# each rewrites established product bytes (tmct's lowercase openers and repeated
|
|
23
|
+
# "and" joins are an intentional VOICE, not a grammar defect), so activating them
|
|
24
|
+
# is a per-rule tuning-cycle decision with its own bench + showcase reconcile,
|
|
25
|
+
# not a blanket flip. `enabled=false` keeps them inert in finish(); the goldens
|
|
26
|
+
# force-enable each rule to prove its behaviour independent of the live flag.
|
|
27
|
+
|
|
28
|
+
# 1. Article selection — a/an by the following word's phonetic onset. The live
|
|
29
|
+
# defect this fixes: the assert echo "every module is a artifact" -> "an
|
|
30
|
+
# artifact". Reads the next word (whether in-span or the leading token of the
|
|
31
|
+
# following protected span); refuses at a boundary it cannot read safely.
|
|
32
|
+
[[rule]]
|
|
33
|
+
id = "article-selection"
|
|
34
|
+
kind = "article"
|
|
35
|
+
enabled = true
|
|
36
|
+
registers = [] # [] = every register
|
|
37
|
+
description = "a/an agreement with the following word's phonetic onset"
|
|
38
|
+
# Spelling-vowel words that begin with a CONSONANT sound (take 'a').
|
|
39
|
+
consonant_sound_vowels = ["uni", "use", "user", "usa", "usu", "ubi", "eu", "ewe", "one", "once"]
|
|
40
|
+
# Spelling-consonant words that begin with a VOWEL sound (take 'an'): silent h.
|
|
41
|
+
vowel_sound_consonants = ["hour", "honest", "honour", "honor", "heir", "herb"]
|
|
42
|
+
|
|
43
|
+
# 2. Subject–verb agreement — an existential copula agrees with the count that
|
|
44
|
+
# follows it ("there is 3 classes" -> "there are 3 classes"; "there are 1
|
|
45
|
+
# class" -> "there is 1 class"). Structure-driven: the plurality is READ from
|
|
46
|
+
# the following number span's value (or a protected span's explicit `plural`
|
|
47
|
+
# flag), never guessed. Neutral on already-correct agreement.
|
|
48
|
+
[[rule]]
|
|
49
|
+
id = "subject-verb-agreement"
|
|
50
|
+
kind = "agreement"
|
|
51
|
+
enabled = false # PARKED — implemented + golden-tested, not live this cycle
|
|
52
|
+
registers = []
|
|
53
|
+
description = "existential copula agrees with the following count/plurality"
|
|
54
|
+
singular = ["is", "was", "has"]
|
|
55
|
+
plural = ["are", "were", "have"]
|
|
56
|
+
|
|
57
|
+
# 3. Sentence capitalisation — capitalise the first alphabetic character of a
|
|
58
|
+
# sentence-initial prose span. Never fires when the answer opens on a
|
|
59
|
+
# protected span (a path/entity opener is left exactly as grounded).
|
|
60
|
+
[[rule]]
|
|
61
|
+
id = "sentence-capitalisation"
|
|
62
|
+
kind = "capitalise"
|
|
63
|
+
enabled = false # PARKED — implemented + golden-tested, not live this cycle
|
|
64
|
+
registers = []
|
|
65
|
+
description = "capitalise the first alphabetic of a prose-initial span"
|
|
66
|
+
|
|
67
|
+
# 4. List punctuation — a series joined by repeated " and " connectives becomes
|
|
68
|
+
# a comma series with a single terminal conjunction ("a and b and c" -> "a, b
|
|
69
|
+
# and c"). Operates ONLY on the prose connective spans, never the entity spans
|
|
70
|
+
# they join; a two-item list ("a and b") is already correct and untouched.
|
|
71
|
+
[[rule]]
|
|
72
|
+
id = "list-punctuation"
|
|
73
|
+
kind = "list"
|
|
74
|
+
enabled = false # PARKED — implemented + golden-tested, not live this cycle
|
|
75
|
+
registers = []
|
|
76
|
+
description = "repeated 'and' joins in a 3+ item series become a comma series"
|
|
77
|
+
connective = " and "
|
|
78
|
+
separator = ", "
|
|
79
|
+
|
|
80
|
+
# 5. Terminal punctuation — exactly one sentence-final stop: a run of 2+ trailing
|
|
81
|
+
# stops in the final prose span collapses to one ("done.." -> "done."). Adds
|
|
82
|
+
# nothing where a fragment/list answer legitimately ends without a stop.
|
|
83
|
+
[[rule]]
|
|
84
|
+
id = "terminal-punctuation"
|
|
85
|
+
kind = "terminal"
|
|
86
|
+
enabled = false # PARKED — implemented + golden-tested, not live this cycle
|
|
87
|
+
registers = []
|
|
88
|
+
description = "collapse a run of trailing sentence stops to a single stop"
|
|
89
|
+
stops = [".", "!", "?"]
|
|
@@ -53,3 +53,16 @@
|
|
|
53
53
|
{"id":"nudge-precision","class":"nudge","register":"friendly","template":"The closer you get to a shape like \"{example}\", the sharper my answer gets."}
|
|
54
54
|
{"id":"nudge-commands","class":"nudge","register":"friendly","template":"If prose fails you, the slash commands always work — try {command}."}
|
|
55
55
|
{"id":"nudge-narrower","class":"nudge","register":"friendly","template":"That matched {count} things — too many to be useful. Narrow it with a module or class name."}
|
|
56
|
+
{"id":"conversational-greeting","class":"conversational","register":"friendly","template":"Hi. Ask me about this codebase — imports, calls, definitions, history — or /help."}
|
|
57
|
+
{"id":"conversational-greeting-hello-there","class":"conversational","register":"friendly","template":"Hello there. (A hollow voice says, \"fool.\") Ask me about this codebase, or /help."}
|
|
58
|
+
{"id":"conversational-greeting-good-morning","class":"conversational","register":"friendly","template":"Good morning. Ask me about this codebase, or /help."}
|
|
59
|
+
{"id":"conversational-greeting-good-afternoon","class":"conversational","register":"friendly","template":"Good afternoon. Ask me about this codebase, or /help."}
|
|
60
|
+
{"id":"conversational-greeting-good-evening","class":"conversational","register":"friendly","template":"Good evening. Ask me about this codebase, or /help."}
|
|
61
|
+
{"id":"conversational-thanks","class":"conversational","register":"friendly","template":"Any time. Ask another, or /help for what I can do."}
|
|
62
|
+
{"id":"conversational-farewell","class":"conversational","register":"friendly","template":"Bye — flushing the session log. Come back with a question any time."}
|
|
63
|
+
{"id":"orientation-friendly","class":"orientation","register":"friendly","template":"I answer questions about THIS codebase's structure — imports, calls, definitions,\nhistory and counts. For example:\n which modules import walk.mjs\n what calls buildContextBundle\n how many classes are there\n/help for commands, /stats for an overview of the graph."}
|
|
64
|
+
{"id":"miss-no-previous-answer","class":"miss","register":"friendly","template":"No previous answer to expand yet — ask me a question first, then say \"why\" or \"say more\"."}
|
|
65
|
+
{"id":"technical-density","class":"count","register":"technical","template":"{subject} carries {count} {noun} across {scope} — a concentration well above what a codebase of this size typically sustains ({provenance})."}
|
|
66
|
+
{"id":"technical-comparison","class":"count","register":"technical","template":"At {count} {noun}, {subject} sits {comparison} the comparable-project baseline, a divergence that reflects deliberate structure rather than measurement noise ({provenance})."}
|
|
67
|
+
{"id":"technical-superlative","class":"count","register":"technical","template":"No {noun} in {scope} is more {metric} than {subject}; it leads the next candidate by a clear margin of {count} ({provenance})."}
|
|
68
|
+
{"id":"technical-ratio","class":"count","register":"technical","template":"{subject} sustains a ratio of {count} {noun} per {unit}, placing it in the upper band for projects of comparable {scope} ({provenance})."}
|
package/package.json
CHANGED
|
@@ -1,12 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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.",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"chatbot",
|
|
9
|
+
"no-llm",
|
|
10
|
+
"offline",
|
|
11
|
+
"deterministic",
|
|
12
|
+
"eliza",
|
|
13
|
+
"parry",
|
|
14
|
+
"nlp",
|
|
15
|
+
"wink-nlp",
|
|
16
|
+
"owl",
|
|
17
|
+
"rdf",
|
|
18
|
+
"ontology",
|
|
19
|
+
"controlled-natural-language",
|
|
20
|
+
"ace",
|
|
21
|
+
"knowledge-graph",
|
|
22
|
+
"provenance",
|
|
23
|
+
"code-navigation",
|
|
24
|
+
"cli"
|
|
25
|
+
],
|
|
7
26
|
"license": "MPL-2.0",
|
|
8
27
|
"author": "Polycode Limited",
|
|
9
28
|
"homepage": "https://polycode-projects.gitlab.io/the-mechanical-code-talker/",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://gitlab.com/polycode-projects/the-mechanical-code-talker.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://gitlab.com/polycode-projects/the-mechanical-code-talker/-/issues"
|
|
35
|
+
},
|
|
10
36
|
"engines": {
|
|
11
37
|
"node": ">=24"
|
|
12
38
|
},
|
|
@@ -47,7 +73,9 @@
|
|
|
47
73
|
"test": "node --test \"test/**/*.test.mjs\"",
|
|
48
74
|
"chat": "node bin/tmct.mjs",
|
|
49
75
|
"chatbench:run": "node chatbench/run.mjs",
|
|
50
|
-
"chatbench:judge": "node chatbench/judge.mjs"
|
|
76
|
+
"chatbench:judge": "node chatbench/judge.mjs",
|
|
77
|
+
"audit": "npm audit --audit-level=high",
|
|
78
|
+
"audit:fix": "npm audit fix"
|
|
51
79
|
},
|
|
52
80
|
"devDependencies": {
|
|
53
81
|
"ink-testing-library": "^4.0.0"
|
package/src/ask-nlp.mjs
CHANGED
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
// the bounded edit-distance tier still work, browser and Node alike). Keeping the
|
|
10
10
|
// ~1MB CJS model out of the page is the point of the split.
|
|
11
11
|
//
|
|
12
|
-
// wink-nlp and wink-eng-lite-web-model are
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// first use and failure is cached as null: a checkout without the optional
|
|
16
|
-
// installed answers exactly like the browser bundle, it never throws.
|
|
12
|
+
// wink-nlp and wink-eng-lite-web-model are loaded through the shared leaf loader
|
|
13
|
+
// src/wink-model.mjs (Node `createRequire` fallback + a browser registration seam),
|
|
14
|
+
// so this file no longer carries its own Node-only load block. The load happens
|
|
15
|
+
// lazily on first use and failure is cached as null: a checkout without the optional
|
|
16
|
+
// deps installed answers exactly like the browser bundle, it never throws.
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import { winkInstance } from "./wink-model.mjs";
|
|
19
19
|
|
|
20
20
|
let cached; // undefined = not tried yet; null = unavailable (tried once, honestly off)
|
|
21
21
|
|
|
@@ -25,10 +25,8 @@ let cached; // undefined = not tried yet; null = unavailable (tried once, honest
|
|
|
25
25
|
export function nlpAdapter() {
|
|
26
26
|
if (cached !== undefined) return cached;
|
|
27
27
|
try {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
const model = require("wink-eng-lite-web-model");
|
|
31
|
-
const nlp = winkNLP(model);
|
|
28
|
+
const nlp = winkInstance();
|
|
29
|
+
if (!nlp) { cached = null; return cached; }
|
|
32
30
|
const its = nlp.its;
|
|
33
31
|
cached = {
|
|
34
32
|
/** Lowercase lemma of a single token ("imported" -> "import"); the word
|
package/src/ask-vocab.mjs
CHANGED
|
@@ -220,6 +220,28 @@ export const MODIFIER_TO_KIND = Object.freeze({
|
|
|
220
220
|
transitively: "transitive", indirectly: "transitive",
|
|
221
221
|
});
|
|
222
222
|
|
|
223
|
+
// ---- reversible-passive participles (Cycle 6, PLAN_CYCLE_4.md) — past participles ->
|
|
224
|
+
// relation kind, for the agent-marked passive "X is <participle> by Y". Kept SEPARATE
|
|
225
|
+
// from VERB_TO_KIND on purpose: these forms are NOT standalone active verbs in this
|
|
226
|
+
// grammar ("defined" belongs to the multi-word "is defined in" and to the WHERE_MARKERS
|
|
227
|
+
// location routing; bare "inherited" has no active key), so folding them into
|
|
228
|
+
// VERB_TO_KIND would silently re-route "where is X defined" and other queries. This
|
|
229
|
+
// table is consulted ONLY by the keyword strategy's passive path, which has already
|
|
230
|
+
// confirmed a passive auxiliary AND an agent-marking "by" — so an active query is never
|
|
231
|
+
// affected. Most common participles ("imported"/"tested"/"called"/"covered") already
|
|
232
|
+
// reach VERB_TO_KIND via the lemma tier; this table backfills the two families the lemma
|
|
233
|
+
// tier can't (defines/inherits) plus the obvious siblings, so the passive works
|
|
234
|
+
// adapter-free too. ----
|
|
235
|
+
export const PASSIVE_PARTICIPLE_TO_KIND = Object.freeze({
|
|
236
|
+
imported: "imports", called: "calls", used: "uses",
|
|
237
|
+
tested: "tests", covered: "tests", verified: "tests", exercised: "tests", checked: "tests",
|
|
238
|
+
defined: "defines", declared: "defines",
|
|
239
|
+
inherited: "inherits", extended: "inherits", subclassed: "inherits",
|
|
240
|
+
contained: "contains",
|
|
241
|
+
exported: "reexports", "re-exported": "reexports", exposed: "reexports",
|
|
242
|
+
touched: "touches", changed: "touches", modified: "touches", edited: "touches", updated: "touches",
|
|
243
|
+
});
|
|
244
|
+
|
|
223
245
|
// ---- §3.5 normalization — contractions/informal spellings that would otherwise
|
|
224
246
|
// block a match, expanded BEFORE parsing (BOTH the anchored-template strategy
|
|
225
247
|
// and the independent keyword-spotting strategy see the same normalized text —
|
package/src/ask.mjs
CHANGED
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
// grammar, split out of this file: normalization pre-pass, the two parsing
|
|
52
52
|
// strategies, and the bounded-fuzzy service. Re-exported below where existing
|
|
53
53
|
// callers/tests import them from here.
|
|
54
|
-
import { normalizeQuery, applyNegationFrames, STOPWORDS, splitWords, wordsOf } from "./interpret/normalize.mjs";
|
|
54
|
+
import { normalizeQuery, applyNegationFrames, matchNegationSet, STOPWORDS, splitWords, wordsOf } from "./interpret/normalize.mjs";
|
|
55
55
|
import { editDistance, fuzzyBound } from "./interpret/fuzzy.mjs";
|
|
56
56
|
import { parseAnchored } from "./interpret/strategies/grammar.mjs";
|
|
57
57
|
import { parseKeywordSpot, findPhrase } from "./interpret/strategies/keywords.mjs";
|
|
@@ -244,7 +244,8 @@ function parseSimpleClause(text, nlp) {
|
|
|
244
244
|
function parseComposite(text, nlp) {
|
|
245
245
|
const w = splitWords(text);
|
|
246
246
|
const lc = w.map((x) => x.toLowerCase());
|
|
247
|
-
return
|
|
247
|
+
return parseNegation(text, nlp, 0)
|
|
248
|
+
|| parseAnaphora(w, lc, nlp)
|
|
248
249
|
|| parseAggregate(w, lc, nlp)
|
|
249
250
|
|| parseSuperlative(w, lc, nlp)
|
|
250
251
|
|| parseList(w, lc, nlp, 0)
|
|
@@ -252,11 +253,81 @@ function parseComposite(text, nlp) {
|
|
|
252
253
|
|| parseRelationalOrQualified(w, lc, nlp, 0);
|
|
253
254
|
}
|
|
254
255
|
|
|
256
|
+
// B1 NEGATION (Cycle 5, PLAN_CYCLE_4.md) — the SET COMPLEMENT. "which X do not <verb>
|
|
257
|
+
// Y" / "X that don't <verb> Y" / "modules not importing Y" / "which X are not
|
|
258
|
+
// <qualifier>" compiles to allOfClass(kind) DIFFERENCE (the positive result set),
|
|
259
|
+
// reusing the EXISTING machinery: evalBoolean already folds a "difference" atom, and
|
|
260
|
+
// the allOfClass node is a ready-made bounded universe of a kind. The only new work is
|
|
261
|
+
// recognizing the negation marker (matchNegationSet, normalize.mjs) and assembling the
|
|
262
|
+
// boolean-difference AST — no new traversal primitive. Regression guards, all tested:
|
|
263
|
+
// (1) honest-empty stays honest — an EMPTY complement ("which functions are not
|
|
264
|
+
// exported", where the only function is exported) renders the standard honest
|
|
265
|
+
// "nothing matches" miss, never invents a member and never re-trips the literal-
|
|
266
|
+
// 'not' trap (the "not" is consumed here, so it can't leak into an object term);
|
|
267
|
+
// (2) BOUNDED UNIVERSE only — the universe is the queried kind within the loaded
|
|
268
|
+
// graph; the "Change" pseudo-type (ask-vocab.mjs) is a wildcard, not a stored
|
|
269
|
+
// enumerable class, so a complement over "changes" is REFUSED honestly rather
|
|
270
|
+
// than answered over an empty universe;
|
|
271
|
+
// (3) active-voice/positive queries are untouched — parseNegation returns null unless
|
|
272
|
+
// matchNegationSet finds an explicit set-negation marker.
|
|
273
|
+
function complementAst(entityType, diffAtom) {
|
|
274
|
+
return {
|
|
275
|
+
node: "boolean",
|
|
276
|
+
entityType,
|
|
277
|
+
atoms: [
|
|
278
|
+
{ op: "seed", kind: "set", ast: { node: "allOfClass", entityType } },
|
|
279
|
+
diffAtom,
|
|
280
|
+
],
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function parseNegation(text, nlp, depth = 0) {
|
|
285
|
+
const neg = matchNegationSet(text);
|
|
286
|
+
if (!neg) return null; // no set-negation marker → not this shape
|
|
287
|
+
const noun = entityNoun(neg.entWord);
|
|
288
|
+
// a set complement needs a CONCRETE, enumerable kind. A placeholder ("things") has no
|
|
289
|
+
// bounded universe; the "Change" pseudo-type is a wildcard over the touch traversal,
|
|
290
|
+
// never a stored class, so its complement is ill-defined and must be refused honestly.
|
|
291
|
+
if (!noun || noun.placeholder || !noun.entityType) return null;
|
|
292
|
+
const entityType = noun.entityType;
|
|
293
|
+
if (entityType === "Change") {
|
|
294
|
+
return { node: "miss", reason: `"${neg.entWord}" isn't an enumerable kind — a set complement needs a concrete kind (functions, classes, modules, …)` };
|
|
295
|
+
}
|
|
296
|
+
const predWords = splitWords(neg.predicate);
|
|
297
|
+
const predLc = predWords.map((x) => x.toLowerCase());
|
|
298
|
+
// (a) qualifier negation ("not tested" / "not exported"): difference the qualifier
|
|
299
|
+
// set off the class — equivalent to the negated qualifier, an honest empty when none.
|
|
300
|
+
if (predLc.length && predLc.every((x) => QUALIFIERS[x])) {
|
|
301
|
+
return complementAst(entityType, { op: "difference", kind: "qual", filters: predLc });
|
|
302
|
+
}
|
|
303
|
+
const vh = findPhrase(predLc, VERB_TO_KIND);
|
|
304
|
+
if (!vh) return { node: "miss", reason: "a negated set query needs a known relation verb (import, call, inherit from, test, …)" };
|
|
305
|
+
const objWords = predWords.filter((_, i) => (i < vh.start || i >= vh.end) && !STOPWORDS.has(predLc[i]) && predLc[i] !== "from");
|
|
306
|
+
// (b) existential object ("do not import anything" / "define nothing"): the complement
|
|
307
|
+
// is the class MINUS the subjects that have ANY edge of this kind.
|
|
308
|
+
if (!objWords.length) {
|
|
309
|
+
return complementAst(entityType, { op: "difference", kind: "set", ast: { node: "existsEdge", entityType, kind: vh.kind } });
|
|
310
|
+
}
|
|
311
|
+
// (c) concrete object ("do not import a.mjs"): the class MINUS the POSITIVE result
|
|
312
|
+
// set, parsed through the existing clause/relational machinery (never re-negating —
|
|
313
|
+
// the reconstructed positive text carries no "not").
|
|
314
|
+
const positive = parseSetPhrase(`which ${neg.entWord} ${neg.predicate}`, nlp, depth + 1);
|
|
315
|
+
if (!positive || positive.node === "miss") {
|
|
316
|
+
return { node: "miss", reason: (positive && positive.reason) || "the negated clause didn't parse" };
|
|
317
|
+
}
|
|
318
|
+
return complementAst(entityType, { op: "difference", kind: "set", ast: positive });
|
|
319
|
+
}
|
|
320
|
+
|
|
255
321
|
/** A set-producing sub-expression (used for nested inner clauses, boolean branches,
|
|
256
322
|
* and count restrictors): nested first, then the relational/qualifier/boolean
|
|
257
323
|
* parser, then a bare simple clause. Carries `depth` for the nesting cap. */
|
|
258
324
|
function parseSetPhrase(text, nlp, depth) {
|
|
259
325
|
if (depth > MAX_COMPOSE_DEPTH) return { node: "miss", reason: "too deep to resolve" };
|
|
326
|
+
// a set-negation clause can appear as a count restrictor ("how many classes are not
|
|
327
|
+
// tested"), a list filter, or a boolean branch — try the complement frame first so
|
|
328
|
+
// those compositions get the bounded-complement for free.
|
|
329
|
+
const negated = parseNegation(text, nlp, depth);
|
|
330
|
+
if (negated) return negated;
|
|
260
331
|
const w = splitWords(text);
|
|
261
332
|
const lc = w.map((x) => x.toLowerCase());
|
|
262
333
|
const nested = parseNested(w, lc, nlp, depth);
|
|
@@ -694,6 +765,13 @@ function evalSet(graph, ast, opts) {
|
|
|
694
765
|
switch (ast.node) {
|
|
695
766
|
case "clause": return traverse(graph, ast.clause, opts).matches || [];
|
|
696
767
|
case "allOfClass": return graph.individuals.filter((i) => i.class === ast.entityType);
|
|
768
|
+
// the SUBJECTS that have ANY edge of a kind (the existential "modules that import
|
|
769
|
+
// anything") — the positive set an existential negation ("do not import anything")
|
|
770
|
+
// differences off allOfClass to yield "modules that import nothing".
|
|
771
|
+
case "existsEdge": {
|
|
772
|
+
const subs = new Set(kindsFor(ast.kind).flatMap((k) => edgesOfKind(graph, k)).map((e) => e.subject));
|
|
773
|
+
return graph.individuals.filter((i) => subs.has(i.id) && (!ast.entityType || i.class === ast.entityType));
|
|
774
|
+
}
|
|
697
775
|
case "reverseSet": {
|
|
698
776
|
const ids = new Set(evalSet(graph, ast.inner, opts).map((i) => i.id));
|
|
699
777
|
return reverseOverSet(graph, ast.kind, ast.entityType, ids);
|