@polycode-projects/seonix 0.9.0 → 0.9.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/README.md CHANGED
@@ -325,7 +325,7 @@ set `SEONIX_GRAPH_FORMAT=1` to opt back out to the legacy v1 form.
325
325
  An optional `seonix.toml` at a repo (or estate) root steers indexing and
326
326
  scoring. Absent file = shipped defaults, byte-for-byte today's behaviour.
327
327
  `seonix init` (or `seonix init --dotnet` for an estate) seeds a commented
328
- template — see `packages/seonix/templates/seonix.toml` and
328
+ template — see `templates/seonix.toml` and
329
329
  `templates/seonix-dotnet.toml` for the full annotated reference.
330
330
 
331
331
  - **`repositories`** — the repo set for an estate config: an inline array of
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/seonix",
3
- "version": "0.9.0",
3
+ "version": "0.9.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.",
@@ -9,8 +9,7 @@
9
9
  "homepage": "https://seonix.polycode.co.uk",
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "git+https://gitlab.com/polycode-projects/seonix.git",
13
- "directory": "packages/seonix"
12
+ "url": "git+https://gitlab.com/polycode-projects/seonix.git"
14
13
  },
15
14
  "bugs": {
16
15
  "url": "https://gitlab.com/polycode-projects/seonix/-/issues"
@@ -42,9 +41,58 @@
42
41
  "publishConfig": {
43
42
  "access": "public"
44
43
  },
44
+ "workspaces": [
45
+ "infra"
46
+ ],
47
+ "scripts": {
48
+ "setup": "node scripts/setup.mjs",
49
+ "bench": "node bench/run.mjs",
50
+ "bench:quick": "node bench/run.mjs --instances 1 --runs 1",
51
+ "join:telemetry": "node scripts/join-telemetry.mjs",
52
+ "bench:lite": "node bench/run.mjs --suite lite",
53
+ "bench:seonix": "node bench/run.mjs --suite django-lh --arms seonix,seonix-min --runs 5",
54
+ "bench:b010": "node bench/run.mjs --suite django-lh --arms otb,seonix,seonix-min,seonix-b010 --models claude-opus-4-8,claude-sonnet-4-6 --runs 5 --concurrency 10",
55
+ "bench:b011": "node bench/run.mjs --suite django-lh --arms seonix,seonix-min --models claude-opus-4-8,claude-sonnet-4-6,claude-haiku-4-5 --runs 5 --concurrency 15",
56
+ "bench:b011-haiku-refs": "node bench/run.mjs --suite django-lh --arms otb,seonix-b010 --models claude-haiku-4-5 --runs 5 --concurrency 10",
57
+ "bench:b015": "node scripts/bench-b015.mjs",
58
+ "bench:b016p1": "node scripts/bench-b016-p1.mjs",
59
+ "bench:b016h": "node scripts/bench-b016-h.mjs",
60
+ "bench:b016p2": "node scripts/bench-b016-p2.mjs",
61
+ "bench:baseline": "node scripts/use-baseline.mjs",
62
+ "proof": "node scripts/proof-demo.mjs",
63
+ "smoke": "node bench/smoke.mjs --suite django-lh",
64
+ "report": "node bench/report.mjs",
65
+ "all": "npm run setup && npm run bench && npm run report",
66
+ "clean": "node scripts/clean.mjs",
67
+ "verify:counts": "node scripts/verify-counts.mjs",
68
+ "verify:sanity": "node scripts/verify-sanity.mjs",
69
+ "verify:index": "node scripts/verify-index.mjs",
70
+ "verify:query": "node scripts/verify-query.mjs",
71
+ "verify:review": "node scripts/verify-review.mjs",
72
+ "verify:loop": "node scripts/verify-loop.mjs",
73
+ "refs": "node scripts/fetch-references.mjs",
74
+ "refs:embeddings": "node scripts/fetch-embeddings.mjs",
75
+ "audit": "npm audit --audit-level=high",
76
+ "audit:fix": "npm audit fix",
77
+ "test:root": "node --test \"test/**/*.test.mjs\"",
78
+ "test": "npm run test:root && npm run chrono:test",
79
+ "chrono:build": "node chronograph/build.mjs",
80
+ "chrono:serve": "node chronograph/serve.mjs",
81
+ "chrono:test": "node --test \"chronograph/test/**/*.test.mjs\"",
82
+ "test:all": "npm run test",
83
+ "seonix": "node bin/cli.mjs",
84
+ "seonix:viz": "node bin/cli.mjs viz --",
85
+ "e2e:install": "npx playwright install chromium --with-deps",
86
+ "e2e": "playwright test",
87
+ "e2e:site": "playwright test site.behaviour.test.js",
88
+ "e2e:prod": "SEONIX_E2E_BASE_URL=https://seonix.polycode.co.uk playwright test site.behaviour.test.js",
89
+ "site:prompts": "node scripts/site-prompts.mjs",
90
+ "build:ask-bundle": "node scripts/build-ask-bundle.mjs",
91
+ "site:gen": "npm run build:ask-bundle && node bin/cli.mjs viz --focus buildContextBundle --depth 2 --repo-url https://gitlab.com/polycode-projects/seonix --site-nav --out site/seonix-graph.html --data-out site/seonix-graph-data.json --browser-out site/code-browser.html --browser-data-out site/code-browser-data.json --timeline-out site/timeline.html && npm run site:prompts"
92
+ },
45
93
  "dependencies": {
46
94
  "@modelcontextprotocol/sdk": "^1.29.0",
47
- "@polycode-projects/the-mechanical-code-talker": "^0.8.0",
95
+ "@polycode-projects/the-mechanical-code-talker": "^0.8.1",
48
96
  "cytoscape": "^3.30.0",
49
97
  "smol-toml": "^1.7.0",
50
98
  "tree-sitter": "^0.21.1",
@@ -52,8 +100,8 @@
52
100
  "tree-sitter-java": "^0.21.0",
53
101
  "typescript": "~5.6.2"
54
102
  },
55
- "scripts": {
56
- "test": "node --test \"test/**/*.test.mjs\"",
57
- "viz": "node bin/cli.mjs viz"
103
+ "devDependencies": {
104
+ "@playwright/test": "^1.56.0",
105
+ "esbuild": "^0.28.1"
58
106
  }
59
107
  }
@@ -50,6 +50,8 @@
50
50
  };
51
51
  var createRequire2 = unavailable2("createRequire");
52
52
  var readFileSync2 = unavailable2("readFileSync");
53
+ var join = (...a) => a.join("/");
54
+ var dirname = (p) => String(p).replace(/\/[^/]*$/, "");
53
55
  var randomBytes2 = unavailable2("randomBytes");
54
56
 
55
57
  // node-stub:node:url
@@ -58,6 +60,7 @@
58
60
  };
59
61
  var createRequire3 = unavailable3("createRequire");
60
62
  var readFileSync3 = unavailable3("readFileSync");
63
+ var fileURLToPath = (u) => String(u);
61
64
  var randomBytes3 = unavailable3("randomBytes");
62
65
 
63
66
  // node_modules/@polycode-projects/the-mechanical-code-talker/src/codegraph.mjs
@@ -972,6 +975,92 @@
972
975
  }
973
976
  return text;
974
977
  }
978
+ var PHRASING_FRAMES = Object.freeze([
979
+ // MEMBERS-of-class → "what does X contain".
980
+ // "what functions are in Task", "what methods are inside X", "what attributes are in X"
981
+ { re: /^what\s+(?:functions?|methods?|members?|attributes?|fields?|properties)\s+(?:are|is)\s+(?:in|inside|within)\s+(?:the\s+)?(.+?)\??$/i, to: (m) => `what does ${m[1]} contain` },
982
+ // "what functions does Task have", "what methods does X have"
983
+ { re: /^what\s+(?:functions?|methods?|members?|attributes?|fields?|properties)\s+(?:does|do)\s+(.+?)\s+have\??$/i, to: (m) => `what does ${m[1]} contain` },
984
+ // "what are the members of X", "what are the methods in X"
985
+ { re: /^what\s+are\s+(?:the\s+)?(?:functions?|methods?|members?|attributes?|fields?|properties)\s+(?:of|in|inside|within)\s+(?:the\s+)?(.+?)\??$/i, to: (m) => `what does ${m[1]} contain` },
986
+ // "members of X", "methods of X", "contents of X"
987
+ { re: /^(?:the\s+)?(?:members?|methods?|attributes?|contents)\s+of\s+(?:the\s+)?(.+?)\??$/i, to: (m) => `what does ${m[1]} contain` },
988
+ // "what's in X" / "what is in X" (contraction already expanded; sha handled above)
989
+ { re: /^what\s+is\s+(?:in|inside)\s+(?:the\s+)?(.+?)\??$/i, to: (m) => `what does ${m[1]} contain` },
990
+ // WHERE-DEFINED → "where is X defined". PAST TENSE ONLY ("what defined X", "what
991
+ // declared X"): the PRESENT "what defines X" already parses as a reverse-defines
992
+ // query (the module defining symbol X — test/ask.test.mjs pins that), so rewriting
993
+ // it would change that receipt. The past-tense form is the one that hit the wall.
994
+ { re: /^what\s+(?:defined|declared)\s+(?:the\s+)?(?:function\s+|method\s+|class\s+|module\s+|variable\s+|constant\s+)?(.+?)\??$/i, to: (m) => `where is ${m[1]} defined` },
995
+ // "where's X defined" (the "where's" contraction is not in the contraction table)
996
+ { re: /^where'?s\s+(?:the\s+)?(.+?)\s+(defined|declared|located|implemented)\??$/i, to: (m) => `where is ${m[1]} ${m[2]}` },
997
+ // PREDICATIVE QUALIFIER → the ATTRIBUTIVE form the grammar already answers. The
998
+ // adjective-qualifier post-filters (ask-vocab.mjs QUALIFIERS: tested/untested,
999
+ // public/private, exported, static/abstract/constant, …) parse in the ATTRIBUTIVE
1000
+ // slot — "untested modules", "public methods" — but a developer just as naturally
1001
+ // asks the PREDICATIVE "which modules are untested" / "what functions are tested",
1002
+ // which hit the grammar wall (and, worse, the wall's own hint SUGGESTED "which
1003
+ // functions are tested" — a shape it could not then answer). Rewriting the
1004
+ // predicative "<which|what> <kind> are <QUALIFIER>" to "<QUALIFIER> <kind>" routes
1005
+ // it onto the working attributive filter. Closed to the known qualifier adjectives
1006
+ // (not a general "… are X" catch), and the QUALIFIER must sit immediately after
1007
+ // are/is, so "which modules are NOT tested" never matches here — that keeps its own
1008
+ // set-complement handler (matchNegationSet, downstream in ask.mjs's parseNegation).
1009
+ {
1010
+ re: /^(?:which|what)\s+(?:the\s+|all\s+)?([a-z][a-z-]*?)\s+(?:are|is)\s+(public|private|protected|static|abstract|constant|exported|re-?exported|tested|covered|untested|uncovered)\??$/i,
1011
+ to: (m) => `${m[2].toLowerCase()} ${m[1].toLowerCase()}`
1012
+ },
1013
+ // BARE COVERAGE SURVEY (no entity kind) → the attributive "<qualifier> modules"
1014
+ // the grammar already answers. Once "what is a test" opens the topic, a developer
1015
+ // asks the survey the plainest way — "what is untested", "what's not tested",
1016
+ // "what isn't covered", "what is covered" — with NO entity noun at all, so the
1017
+ // predicative-qualifier frame above (which needs a KIND between what/which and
1018
+ // are/is) can't catch it, and it fell through to a soft wall ("no module matching
1019
+ // 'not'…" / the "I answer questions…" orientation). Default the surveyed kind to
1020
+ // modules (the same set "which modules are not tested" / "untested modules" return)
1021
+ // and fold the negation into the qualifier (not tested → untested, not covered →
1022
+ // uncovered). Anchored with no object, so "what tests cover X" / "what is a test"
1023
+ // never match here.
1024
+ {
1025
+ re: /^what\s+(?:is|are)\s+(not\s+)?(tested|untested|covered|uncovered)\??$/i,
1026
+ to: (m) => {
1027
+ const q = m[2].toLowerCase();
1028
+ const flipped = m[1] ? q === "tested" ? "untested" : q === "covered" ? "uncovered" : q : q;
1029
+ return `${flipped} modules`;
1030
+ }
1031
+ },
1032
+ // CO-CHANGE → the "co-changes with" canonical the RELATIONS table answers. The
1033
+ // cochange verb synonyms (ask-vocab.mjs) include "co-changes with" / "moves
1034
+ // together with" / "tends to change together with", but NOT the plainest form a
1035
+ // developer types — the one the README itself prints and the relation renders as:
1036
+ // "what does X change together with" / "what changes together with X". Both hit a
1037
+ // dead-end ("couldn't resolve one of the terms" / the grammar wall); rewriting them
1038
+ // onto "what co-changes with X" routes them to the working change-coupling query.
1039
+ { re: /^what\s+does\s+(.+?)\s+changes?\s+together\s+with\??$/i, to: (m) => `what co-changes with ${m[1]}` },
1040
+ { re: /^what\s+changes?\s+together\s+with\s+(.+?)\??$/i, to: (m) => `what co-changes with ${m[1]}` },
1041
+ // AUTHORSHIP → the "who touched X" churn query. "who touched X" now names the
1042
+ // commit author beside the sha (the 0.8.1 commit-ref quick-win), which invites the
1043
+ // synonyms a developer reaches for next — "who wrote X", "who authored X", "who is
1044
+ // the author of X" — and every one of them hit the grammar wall. tmct has no
1045
+ // separate authorship edge; "touched" IS the authorship signal (the churn commits
1046
+ // carry the author), so these are true synonyms of "who touched X", not a new
1047
+ // capability. Anaphora rides through untouched ("who wrote it" → "who touched it").
1048
+ { re: /^who\s+(?:wrote|authored)\s+(?:the\s+)?(.+?)\??$/i, to: (m) => `who touched ${m[1]}` },
1049
+ { re: /^who\s+is\s+the\s+authors?\s+of\s+(?:the\s+)?(.+?)\??$/i, to: (m) => `who touched ${m[1]}` },
1050
+ // NEEDS-TESTS → the untested-module survey. "what needs tests" / "what needs
1051
+ // testing" is the plainest way to ask which modules are uncovered, and it hit the
1052
+ // grammar wall ("no module matching 'needs'…"). Route it onto the same attributive
1053
+ // survey the bare "what is untested" frame lands on. Closed to the tests/coverage
1054
+ // object, so it can't swallow a general "what needs X".
1055
+ { re: /^what\s+needs\s+(?:to\s+be\s+)?(?:a\s+)?(?:tested|tests?|testing|coverage|covering)\??$/i, to: () => "untested modules" }
1056
+ ]);
1057
+ function applyPhrasingFrames(text) {
1058
+ for (const frame of PHRASING_FRAMES) {
1059
+ const m = text.match(frame.re);
1060
+ if (m) return frame.to(m).replace(/\s+/g, " ").trim();
1061
+ }
1062
+ return text;
1063
+ }
975
1064
  var NEGATION_SET_RE = new RegExp(
976
1065
  "^(?:which|what|who|list|show(?:\\s+me)?|find|give\\s+me)?\\s*(?:the\\s+|all\\s+)?([a-z][a-z-]*)\\s+(?:(?:that|which|who)\\s+)?(?:(?:do|does|did|are|is|was|were|have|has)\\s+)?not\\s+(.+)$",
977
1066
  // the negation marker + (2) the predicate
@@ -1058,17 +1147,17 @@
1058
1147
  function fuzzyVocabWord(w) {
1059
1148
  const bound = fuzzyBound(w);
1060
1149
  let best = bound + 1;
1061
- let hit = null;
1150
+ let hit2 = null;
1062
1151
  let tied = false;
1063
1152
  for (const target of FUZZY_TARGET_WORDS) {
1064
1153
  const d = editDistance(w, target, Math.min(best, bound));
1065
1154
  if (d < best) {
1066
1155
  best = d;
1067
- hit = target;
1156
+ hit2 = target;
1068
1157
  tied = false;
1069
- } else if (d === best && d <= bound && target !== hit) tied = true;
1158
+ } else if (d === best && d <= bound && target !== hit2) tied = true;
1070
1159
  }
1071
- return best <= bound && !tied ? hit : null;
1160
+ return best <= bound && !tied ? hit2 : null;
1072
1161
  }
1073
1162
 
1074
1163
  // node_modules/@polycode-projects/the-mechanical-code-talker/src/interpret/strategies/grammar.mjs
@@ -1253,8 +1342,8 @@
1253
1342
  }
1254
1343
  }
1255
1344
  const consumed = /* @__PURE__ */ new Set();
1256
- const mark = (hit) => {
1257
- if (hit) for (let i = hit.start; i < hit.end; i += 1) consumed.add(i);
1345
+ const mark = (hit2) => {
1346
+ if (hit2) for (let i = hit2.start; i < hit2.end; i += 1) consumed.add(i);
1258
1347
  };
1259
1348
  mark(verbHit);
1260
1349
  const entityHit = findPhrase(canonWords, ENTITY_TO_TYPE, consumed);
@@ -1356,6 +1445,423 @@
1356
1445
  }
1357
1446
  };
1358
1447
 
1448
+ // node_modules/@polycode-projects/the-mechanical-code-talker/src/grammar/lexicon.mjs
1449
+ var import_meta = {};
1450
+ var CORE_FILE = join(dirname(fileURLToPath(import_meta.url)), "lexicon-core.json");
1451
+ var DETERMINERS = Object.freeze({
1452
+ every: "universal",
1453
+ a: "indefinite",
1454
+ an: "indefinite",
1455
+ the: "definite",
1456
+ no: "negative"
1457
+ });
1458
+ var QUANTIFIERS = Object.freeze({
1459
+ "at least": "owl:minCardinality",
1460
+ "at most": "owl:maxCardinality",
1461
+ exactly: "owl:cardinality"
1462
+ });
1463
+ var NUMBER_WORDS = Object.freeze({
1464
+ one: 1,
1465
+ two: 2,
1466
+ three: 3,
1467
+ four: 4,
1468
+ five: 5,
1469
+ six: 6,
1470
+ seven: 7,
1471
+ eight: 8,
1472
+ nine: 9,
1473
+ ten: 10
1474
+ });
1475
+ function numberOf(word) {
1476
+ const w = String(word ?? "").trim().toLowerCase();
1477
+ if (/^\d+$/.test(w)) return Number(w);
1478
+ return NUMBER_WORDS[w] ?? null;
1479
+ }
1480
+ function thirdPerson(base) {
1481
+ const b = String(base);
1482
+ if (b === "have") return "has";
1483
+ if (/[^aeiou]y$/.test(b)) return `${b.slice(0, -1)}ies`;
1484
+ if (/(s|x|z|ch|sh)$/.test(b)) return `${b}es`;
1485
+ return `${b}s`;
1486
+ }
1487
+ function predicateOf(verbEntry) {
1488
+ if (verbEntry.predicate) return verbEntry.predicate;
1489
+ const prep = verbEntry.prep ? verbEntry.prep[0].toUpperCase() + verbEntry.prep.slice(1) : "";
1490
+ return `tmct:${thirdPerson(verbEntry.lemma)}${prep}`;
1491
+ }
1492
+ function foldCandidates(word) {
1493
+ const w = String(word);
1494
+ const out = [w];
1495
+ if (w.length > 4 && /[a-z]ies$/.test(w)) out.push(`${w.slice(0, -3)}y`);
1496
+ if (/(ses|xes|zes|ches|shes)$/.test(w)) out.push(w.slice(0, -2));
1497
+ if (/[a-z]s$/.test(w) && !/ss$/.test(w)) out.push(w.slice(0, -1));
1498
+ if (w === "has") out.push("have");
1499
+ return out;
1500
+ }
1501
+ var NOUN_PROPERTY_TYPES = /* @__PURE__ */ new Set(["data", "object"]);
1502
+ var ADJECTIVE_TYPES = /* @__PURE__ */ new Set(["subclass", "data"]);
1503
+ function ingest(lex, raw = {}) {
1504
+ for (const [lemma, e] of Object.entries(raw.nouns || {})) {
1505
+ const entry = { lemma, ...e || {} };
1506
+ if (entry.property && !NOUN_PROPERTY_TYPES.has(entry.property)) {
1507
+ throw new Error(`lexicon noun "${lemma}": property must be "data" or "object", got ${JSON.stringify(entry.property)}`);
1508
+ }
1509
+ lex.nouns.set(lemma, entry);
1510
+ if (entry.plural) lex.nounPlurals.set(entry.plural, lemma);
1511
+ }
1512
+ for (const [lemma, e] of Object.entries(raw.verbs || {})) {
1513
+ lex.verbs.set(lemma, { lemma, ...e || {} });
1514
+ }
1515
+ for (const [lemma, e] of Object.entries(raw.adjectives || {})) {
1516
+ const entry = { lemma, ...e || {} };
1517
+ if (!ADJECTIVE_TYPES.has(entry.type)) {
1518
+ throw new Error(`lexicon adjective "${lemma}": type must be "subclass" or "data", got ${JSON.stringify(entry.type)}`);
1519
+ }
1520
+ lex.adjectives.set(lemma, entry);
1521
+ }
1522
+ for (const name of raw.properNames || []) {
1523
+ lex.properNames.set(String(name).toLowerCase(), String(name));
1524
+ }
1525
+ }
1526
+ var coreCache = null;
1527
+ function loadLexicon(extra) {
1528
+ if (!extra && coreCache) return coreCache;
1529
+ const raw = JSON.parse(readFileSync(CORE_FILE, "utf8"));
1530
+ const lex = {
1531
+ nouns: /* @__PURE__ */ new Map(),
1532
+ nounPlurals: /* @__PURE__ */ new Map(),
1533
+ verbs: /* @__PURE__ */ new Map(),
1534
+ adjectives: /* @__PURE__ */ new Map(),
1535
+ properNames: /* @__PURE__ */ new Map()
1536
+ // lowercased → canonical spelling
1537
+ };
1538
+ ingest(lex, raw);
1539
+ if (extra) {
1540
+ ingest(lex, extra);
1541
+ return lex;
1542
+ }
1543
+ coreCache = lex;
1544
+ return lex;
1545
+ }
1546
+ function lookupNoun(lexicon, word) {
1547
+ const w = String(word ?? "").toLowerCase();
1548
+ const irregular = lexicon.nounPlurals.get(w);
1549
+ if (irregular) return lexicon.nouns.get(irregular) ?? null;
1550
+ for (const cand of foldCandidates(w)) {
1551
+ const hit2 = lexicon.nouns.get(cand);
1552
+ if (hit2) return hit2;
1553
+ }
1554
+ return null;
1555
+ }
1556
+ function lookupVerb(lexicon, word) {
1557
+ const w = String(word ?? "").toLowerCase();
1558
+ for (const cand of foldCandidates(w)) {
1559
+ const hit2 = lexicon.verbs.get(cand);
1560
+ if (hit2) return hit2;
1561
+ }
1562
+ return null;
1563
+ }
1564
+ function lookupAdjective(lexicon, word) {
1565
+ return lexicon.adjectives.get(String(word ?? "").toLowerCase()) ?? null;
1566
+ }
1567
+ function lookupProperName(lexicon, word) {
1568
+ return lexicon.properNames.get(String(word ?? "").toLowerCase()) ?? null;
1569
+ }
1570
+ function classify(word, lexicon = loadLexicon()) {
1571
+ const w = String(word ?? "").trim();
1572
+ if (!w) return null;
1573
+ const lower = w.toLowerCase();
1574
+ if (DETERMINERS[lower]) return { pos: "determiner", type: DETERMINERS[lower] };
1575
+ if (QUANTIFIERS[lower]) return { pos: "quantifier", type: QUANTIFIERS[lower] };
1576
+ const n = numberOf(lower);
1577
+ if (n != null) return { pos: "number", type: "cardinal", value: n };
1578
+ const proper = lookupProperName(lexicon, w);
1579
+ if (proper) return { pos: "properName", type: "individual", canonical: proper };
1580
+ const noun = lookupNoun(lexicon, lower);
1581
+ if (noun) {
1582
+ return noun.property ? { pos: "noun", type: `${noun.property}-property`, lemma: noun.lemma, property: noun.property } : { pos: "noun", type: "class", lemma: noun.lemma };
1583
+ }
1584
+ const verb = lookupVerb(lexicon, lower);
1585
+ if (verb) {
1586
+ return { pos: "verb", type: "objectProperty", lemma: verb.lemma, predicate: predicateOf(verb), ...verb.prep ? { prep: verb.prep } : {} };
1587
+ }
1588
+ const adj = lookupAdjective(lexicon, lower);
1589
+ if (adj) return { pos: "adjective", type: adj.type, lemma: adj.lemma };
1590
+ return null;
1591
+ }
1592
+
1593
+ // node_modules/@polycode-projects/the-mechanical-code-talker/src/grammar/ace.mjs
1594
+ var DET = /* @__PURE__ */ new Set(["a", "an", "the"]);
1595
+ var CODE_REF = /[./\\#:@]/;
1596
+ function tokenize(sentence) {
1597
+ return String(sentence ?? "").replace(/[‘’]/g, "'").replace(/[,;]/g, " ").replace(/[?!.]+\s*$/, "").trim().split(/\s+/).filter(Boolean);
1598
+ }
1599
+ var local = (term) => String(term).replace(/^tmct:/, "");
1600
+ var stripDet = (tokens) => tokens.length > 1 && DET.has(tokens[0].toLowerCase()) ? tokens.slice(1) : tokens;
1601
+ function resolveNP(lexicon, tokensIn) {
1602
+ const tokens = stripDet(tokensIn);
1603
+ if (tokens.length === 1) {
1604
+ const t = tokens[0];
1605
+ const proper = lookupProperName(lexicon, t);
1606
+ if (proper) return { term: `tmct:${proper}`, individual: true, extras: [], unknown: [] };
1607
+ if (CODE_REF.test(t)) return { term: `tmct:${t}`, individual: true, extras: [], unknown: [] };
1608
+ const noun = lookupNoun(lexicon, t);
1609
+ if (noun) return { term: `tmct:${noun.lemma}`, individual: false, noun, extras: [], unknown: [] };
1610
+ return { term: null, individual: false, extras: [], unknown: [t] };
1611
+ }
1612
+ if (tokens.length === 2) {
1613
+ const adj = lookupAdjective(lexicon, tokens[0]);
1614
+ const noun = lookupNoun(lexicon, tokens[1]);
1615
+ if (adj && noun) {
1616
+ const term = `tmct:${adj.lemma}-${noun.lemma}`;
1617
+ const extras = [
1618
+ { subject: term, predicate: "rdfs:subClassOf", object: `tmct:${noun.lemma}`, kind: "rdfs:subClassOf" }
1619
+ ];
1620
+ if (adj.type === "subclass") {
1621
+ extras.push({ subject: term, predicate: "rdfs:subClassOf", object: `tmct:${adj.lemma}`, kind: "rdfs:subClassOf" });
1622
+ } else {
1623
+ const r = `tmct:has-${adj.lemma}`;
1624
+ extras.push(
1625
+ { subject: r, predicate: "rdf:type", object: "owl:Restriction", kind: "owl:hasValue" },
1626
+ { subject: r, predicate: "owl:onProperty", object: adj.property || `tmct:${adj.lemma}`, kind: "owl:hasValue" },
1627
+ { subject: r, predicate: "owl:hasValue", object: adj.value ?? "true", kind: "owl:hasValue" },
1628
+ { subject: term, predicate: "rdfs:subClassOf", object: r, kind: "owl:hasValue" }
1629
+ );
1630
+ }
1631
+ return { term, individual: false, noun, extras, unknown: [] };
1632
+ }
1633
+ const unknown = tokens.filter((t) => !classify(t, lexicon));
1634
+ return { term: null, individual: false, extras: [], unknown };
1635
+ }
1636
+ return { term: null, individual: false, extras: [], unknown: tokens.filter((t) => !classify(t, lexicon)) };
1637
+ }
1638
+ function missOrNull(pattern, nps, extraUnknown = []) {
1639
+ const residue = [...extraUnknown, ...nps.flatMap((np) => np.unknown)];
1640
+ return residue.length ? { pattern, triples: [], residue } : null;
1641
+ }
1642
+ var hit = (pattern, nps, triples, more = {}) => ({
1643
+ pattern,
1644
+ triples: [...nps.flatMap((np) => np.extras), ...triples],
1645
+ residue: [],
1646
+ ...more
1647
+ });
1648
+ function parseRelation(lexicon, toks, lower) {
1649
+ for (let i = 1; i < toks.length - 1; i += 1) {
1650
+ const verb = lookupVerb(lexicon, lower[i]);
1651
+ if (!verb) continue;
1652
+ let objStart = i + 1;
1653
+ if (verb.prep) {
1654
+ if (lower[objStart] !== verb.prep) continue;
1655
+ objStart += 1;
1656
+ if (objStart >= toks.length) continue;
1657
+ }
1658
+ const np1 = resolveNP(lexicon, toks.slice(0, i));
1659
+ const np2 = resolveNP(lexicon, toks.slice(objStart));
1660
+ if (np1.term == null || np2.term == null) return missOrNull("relation", [np1, np2]);
1661
+ return hit("relation", [np1, np2], [
1662
+ { subject: np1.term, predicate: predicateOf(verb), object: np2.term, kind: "owl:ObjectProperty" }
1663
+ ]);
1664
+ }
1665
+ const content = toks.filter((t) => !DET.has(t.toLowerCase()));
1666
+ if (content.length === 3 && !classify(content[1], lexicon)) {
1667
+ const np1 = resolveNP(lexicon, [content[0]]);
1668
+ const np2 = resolveNP(lexicon, [content[2]]);
1669
+ if (np1.term != null && np2.term != null) return { pattern: "relation", triples: [], residue: [content[1]] };
1670
+ }
1671
+ return null;
1672
+ }
1673
+ function adjectiveCopula(pattern, np1, adj) {
1674
+ if (np1.term == null) return missOrNull(pattern, [np1]);
1675
+ if (adj.type === "data") {
1676
+ return hit(pattern, [np1], [
1677
+ { subject: np1.term, predicate: adj.property || `tmct:${adj.lemma}`, object: adj.value ?? "true", kind: "owl:DatatypeProperty" }
1678
+ ]);
1679
+ }
1680
+ const predicate = np1.individual ? "rdf:type" : "rdfs:subClassOf";
1681
+ return hit(pattern, [np1], [
1682
+ { subject: np1.term, predicate, object: `tmct:${adj.lemma}`, kind: predicate }
1683
+ ]);
1684
+ }
1685
+ function parseRestriction(lexicon, toks, lower, thatIdx) {
1686
+ const isIdx = lower.indexOf("is", thatIdx + 2);
1687
+ if (isIdx < 0 || thatIdx + 1 >= isIdx) return null;
1688
+ const verb = lookupVerb(lexicon, lower[thatIdx + 1]);
1689
+ const np1 = resolveNP(lexicon, toks.slice(1, thatIdx));
1690
+ let objStart = thatIdx + 2;
1691
+ if (verb?.prep) {
1692
+ if (lower[objStart] !== verb.prep) return null;
1693
+ objStart += 1;
1694
+ }
1695
+ const np2 = resolveNP(lexicon, toks.slice(objStart, isIdx));
1696
+ const np3 = resolveNP(lexicon, toks.slice(isIdx + 1));
1697
+ if (!verb) return missOrNull("someValuesFrom", [np1, np2, np3], [toks[thatIdx + 1]]);
1698
+ if (np1.term == null || np2.term == null || np3.term == null) {
1699
+ return missOrNull("someValuesFrom", [np1, np2, np3]);
1700
+ }
1701
+ if (np1.individual || np2.individual || np3.individual) return null;
1702
+ const pred = predicateOf(verb);
1703
+ const k = "owl:someValuesFrom";
1704
+ const r = `tmct:some-${local(pred)}-${local(np2.term)}`;
1705
+ const inter = `tmct:${local(np1.term)}-that-${local(pred)}-${local(np2.term)}`;
1706
+ return hit("someValuesFrom", [np1, np2, np3], [
1707
+ { subject: r, predicate: "rdf:type", object: "owl:Restriction", kind: k },
1708
+ { subject: r, predicate: "owl:onProperty", object: pred, kind: k },
1709
+ { subject: r, predicate: "owl:someValuesFrom", object: np2.term, kind: k },
1710
+ { subject: inter, predicate: "owl:intersectionOf", object: np1.term, kind: k },
1711
+ { subject: inter, predicate: "owl:intersectionOf", object: r, kind: k },
1712
+ { subject: inter, predicate: "rdfs:subClassOf", object: np3.term, kind: k }
1713
+ ]);
1714
+ }
1715
+ function parseCardinality(lexicon, toks, lower, hasIdx) {
1716
+ let kind = null;
1717
+ let nIdx = -1;
1718
+ if (lower[hasIdx + 1] === "at" && lower[hasIdx + 2] === "least") {
1719
+ kind = "owl:minCardinality";
1720
+ nIdx = hasIdx + 3;
1721
+ } else if (lower[hasIdx + 1] === "at" && lower[hasIdx + 2] === "most") {
1722
+ kind = "owl:maxCardinality";
1723
+ nIdx = hasIdx + 3;
1724
+ } else if (lower[hasIdx + 1] === "exactly") {
1725
+ kind = "owl:cardinality";
1726
+ nIdx = hasIdx + 2;
1727
+ } else return null;
1728
+ const n = numberOf(lower[nIdx]);
1729
+ if (n == null || nIdx + 1 >= toks.length) return null;
1730
+ const np1 = resolveNP(lexicon, toks.slice(1, hasIdx));
1731
+ const np2 = resolveNP(lexicon, toks.slice(nIdx + 1));
1732
+ if (np1.term == null || np2.term == null) return missOrNull("cardinality", [np1, np2]);
1733
+ if (np1.individual || np2.individual) return null;
1734
+ const tag = { "owl:minCardinality": "min", "owl:maxCardinality": "max", "owl:cardinality": "exactly" }[kind];
1735
+ const r = `tmct:${tag}-${n}-${local(np2.term)}`;
1736
+ return hit("cardinality", [np1, np2], [
1737
+ { subject: r, predicate: "rdf:type", object: "owl:Restriction", kind },
1738
+ { subject: r, predicate: "owl:onProperty", object: "tmct:has", kind },
1739
+ { subject: r, predicate: kind, object: String(n), kind, n },
1740
+ { subject: r, predicate: "owl:onClass", object: np2.term, kind },
1741
+ { subject: np1.term, predicate: "rdfs:subClassOf", object: r, kind }
1742
+ ], { n });
1743
+ }
1744
+ function parseEvery(lexicon, toks, lower) {
1745
+ const thatIdx = lower.indexOf("that");
1746
+ if (thatIdx > 1) return parseRestriction(lexicon, toks, lower, thatIdx);
1747
+ const hasIdx = lower.indexOf("has");
1748
+ if (hasIdx > 1 && (lower[hasIdx + 1] === "at" || lower[hasIdx + 1] === "exactly")) {
1749
+ return parseCardinality(lexicon, toks, lower, hasIdx);
1750
+ }
1751
+ const isIdx = lower.indexOf("is");
1752
+ if (isIdx <= 1 || isIdx === toks.length - 1) return null;
1753
+ const np1 = resolveNP(lexicon, toks.slice(1, isIdx));
1754
+ const rest = toks.slice(isIdx + 1);
1755
+ if (rest.length === 1) {
1756
+ const adj = lookupAdjective(lexicon, rest[0]);
1757
+ if (adj) return adjectiveCopula("adjective", np1, adj);
1758
+ }
1759
+ const np2 = resolveNP(lexicon, rest);
1760
+ if (np1.term == null || np2.term == null) return missOrNull("subClassOf", [np1, np2]);
1761
+ if (np1.individual || np2.individual) return null;
1762
+ return hit("subClassOf", [np1, np2], [
1763
+ { subject: np1.term, predicate: "rdfs:subClassOf", object: np2.term, kind: "rdfs:subClassOf" }
1764
+ ]);
1765
+ }
1766
+ function parseDisjoint(lexicon, toks, lower) {
1767
+ const isIdx = lower.indexOf("is");
1768
+ if (isIdx <= 1 || isIdx === toks.length - 1) return null;
1769
+ const np1 = resolveNP(lexicon, toks.slice(1, isIdx));
1770
+ const np2 = resolveNP(lexicon, toks.slice(isIdx + 1));
1771
+ if (np1.term == null || np2.term == null) return missOrNull("disjointWith", [np1, np2]);
1772
+ if (np1.individual || np2.individual) return null;
1773
+ return hit("disjointWith", [np1, np2], [
1774
+ { subject: np1.term, predicate: "owl:disjointWith", object: np2.term, kind: "owl:disjointWith" }
1775
+ ]);
1776
+ }
1777
+ function buildPossessive(lexicon, ownerToks, headToks, valueToks) {
1778
+ const owner = resolveNP(lexicon, ownerToks);
1779
+ if (headToks.length !== 1) return null;
1780
+ const head = lookupNoun(lexicon, headToks[0]);
1781
+ if (!head) return missOrNull("possessive", [owner], [headToks[0]]);
1782
+ if (owner.term == null) return missOrNull("possessive", [owner]);
1783
+ if (!valueToks.length) return null;
1784
+ const predicate = `tmct:${head.lemma}`;
1785
+ if ((head.property || "data") === "object") {
1786
+ const value = resolveNP(lexicon, valueToks);
1787
+ if (value.term == null) return missOrNull("possessive", [owner, value]);
1788
+ return hit("possessive", [owner, value], [
1789
+ { subject: owner.term, predicate, object: value.term, kind: "owl:ObjectProperty" }
1790
+ ]);
1791
+ }
1792
+ return hit("possessive", [owner], [
1793
+ { subject: owner.term, predicate, object: valueToks.join(" "), kind: "owl:DatatypeProperty" }
1794
+ ]);
1795
+ }
1796
+ function parsePossessive(lexicon, toks, lower) {
1797
+ const ownerRaw = toks[0].replace(/'s$/i, "");
1798
+ const isIdx = lower.indexOf("is");
1799
+ if (isIdx < 2 || !ownerRaw) return null;
1800
+ return buildPossessive(lexicon, [ownerRaw], toks.slice(1, isIdx), toks.slice(isIdx + 1));
1801
+ }
1802
+ function parseOfForm(lexicon, toks, lower) {
1803
+ const ofIdx = lower.indexOf("of");
1804
+ const isIdx = lower.indexOf("is", ofIdx + 1);
1805
+ if (ofIdx < 2 || isIdx < ofIdx + 2) return null;
1806
+ return buildPossessive(lexicon, toks.slice(ofIdx + 1, isIdx), toks.slice(1, ofIdx), toks.slice(isIdx + 1));
1807
+ }
1808
+ function parseCopula(lexicon, toks, lower, isIdx) {
1809
+ const np1 = resolveNP(lexicon, toks.slice(0, isIdx));
1810
+ const rest = toks.slice(isIdx + 1);
1811
+ if (!rest.length) return null;
1812
+ if (rest.length === 1) {
1813
+ const adj = lookupAdjective(lexicon, rest[0]);
1814
+ if (adj) return adjectiveCopula("adjective", np1, adj);
1815
+ }
1816
+ const np2 = resolveNP(lexicon, rest);
1817
+ if (np1.term == null || np2.term == null) {
1818
+ return missOrNull(np1.individual ? "typeAssertion" : "subClassOf", [np1, np2]);
1819
+ }
1820
+ if (np2.individual) return null;
1821
+ if (np1.individual) {
1822
+ return hit("typeAssertion", [np1, np2], [
1823
+ { subject: np1.term, predicate: "rdf:type", object: np2.term, kind: "rdf:type" }
1824
+ ]);
1825
+ }
1826
+ return hit("subClassOf", [np1, np2], [
1827
+ { subject: np1.term, predicate: "rdfs:subClassOf", object: np2.term, kind: "rdfs:subClassOf" }
1828
+ ]);
1829
+ }
1830
+ function parseAce(sentence, lexicon = loadLexicon()) {
1831
+ const toks = tokenize(sentence);
1832
+ if (toks.length < 3) return null;
1833
+ const lower = toks.map((t) => t.toLowerCase());
1834
+ if (lower[0] === "every") return parseEvery(lexicon, toks, lower);
1835
+ if (lower[0] === "no") return parseDisjoint(lexicon, toks, lower);
1836
+ if (/'s$/.test(lower[0]) && lower[0].length > 2) return parsePossessive(lexicon, toks, lower);
1837
+ if (lower[0] === "the" && lower.includes("of") && lower.includes("is")) {
1838
+ return parseOfForm(lexicon, toks, lower);
1839
+ }
1840
+ const isIdx = lower.indexOf("is");
1841
+ if (isIdx > 0) return parseCopula(lexicon, toks, lower, isIdx);
1842
+ return parseRelation(lexicon, toks, lower);
1843
+ }
1844
+
1845
+ // node_modules/@polycode-projects/the-mechanical-code-talker/src/interpret/strategies/ace.mjs
1846
+ function runAce(text) {
1847
+ let parsed = null;
1848
+ try {
1849
+ parsed = parseAce(text);
1850
+ } catch {
1851
+ return null;
1852
+ }
1853
+ if (!parsed || !Array.isArray(parsed.triples) || parsed.triples.length === 0) return null;
1854
+ return { strategyId: "ace", class: "ace-fact", candidates: [{ parsed, confidence: 0.85, via: "exact", note: `ACE ${parsed.pattern}` }] };
1855
+ }
1856
+ var aceStrategy = {
1857
+ id: "ace",
1858
+ class: "ace-fact",
1859
+ // eslint-disable-next-line require-await
1860
+ async run(text) {
1861
+ return runAce(text);
1862
+ }
1863
+ };
1864
+
1359
1865
  // node_modules/@polycode-projects/the-mechanical-code-talker/src/interpret/merge.mjs
1360
1866
  var DEFAULT_CONFIDENCE = 0.5;
1361
1867
  var cmpTerm = (s) => String(s || "").trim().toLowerCase().replace(/\s+/g, " ").replace(/^commit\s+(?=[0-9a-f]{7,40}$)/, "");
@@ -1422,7 +1928,7 @@
1422
1928
  var randomBytes4 = unavailable4("randomBytes");
1423
1929
 
1424
1930
  // node_modules/@polycode-projects/the-mechanical-code-talker/src/wink-model.mjs
1425
- var import_meta = {};
1931
+ var import_meta2 = {};
1426
1932
  var injected;
1427
1933
  var cached;
1428
1934
  function loadWinkModel() {
@@ -1436,7 +1942,7 @@
1436
1942
  return cached;
1437
1943
  }
1438
1944
  function nodeRequireWink() {
1439
- const require2 = createRequire4(import_meta.url);
1945
+ const require2 = createRequire4(import_meta2.url);
1440
1946
  return {
1441
1947
  winkNLP: require2("wink-nlp"),
1442
1948
  model: require2("wink-eng-lite-web-model")
@@ -1526,7 +2032,8 @@
1526
2032
  }
1527
2033
 
1528
2034
  // node_modules/@polycode-projects/the-mechanical-code-talker/src/interpret/pipeline.mjs
1529
- var STRATEGIES = [grammarStrategy, keywordSpotStrategy, noiseStripStrategy];
2035
+ var OPTIONAL_STRATEGIES = typeof aceStrategy !== "undefined" ? [aceStrategy] : [];
2036
+ var STRATEGIES = [grammarStrategy, keywordSpotStrategy, noiseStripStrategy, ...OPTIONAL_STRATEGIES];
1530
2037
  function runStrategiesSync(text, ctx = {}, strategies = STRATEGIES) {
1531
2038
  const results = [];
1532
2039
  for (const s of strategies) {
@@ -1577,7 +2084,7 @@
1577
2084
  const adapter = nlp === void 0 ? defaultNlp() : nlp;
1578
2085
  const raw = String(query || "").trim().replace(/\s+/g, " ");
1579
2086
  if (!raw) return null;
1580
- const text = applyNegationFrames(normalizeQuery(raw));
2087
+ const text = applyPhrasingFrames(applyNegationFrames(normalizeQuery(raw)));
1581
2088
  if (!text) return null;
1582
2089
  const composite = parseComposite(text, adapter);
1583
2090
  if (composite) return composite;
@@ -1596,7 +2103,7 @@
1596
2103
  function parseComposite(text, nlp) {
1597
2104
  const w = splitWords(text);
1598
2105
  const lc = w.map((x) => x.toLowerCase());
1599
- return parseNegation(text, nlp, 0) || parseForwardNegation(w, lc, nlp) || parseAnaphora(w, lc, nlp) || parseAggregate(w, lc, nlp) || parseSuperlative(w, lc, nlp) || parseList(w, lc, nlp, 0) || parseNested(w, lc, nlp, 0) || parseRelationalOrQualified(w, lc, nlp, 0);
2106
+ return parseNegation(text, nlp, 0) || parseForwardNegation(w, lc, nlp) || parseTemporal(w, lc, nlp, 0) || parseAnaphora(w, lc, nlp) || parseAggregate(w, lc, nlp) || parseSuperlative(w, lc, nlp) || parseList(w, lc, nlp, 0) || parseNested(w, lc, nlp, 0) || parseRelationalOrQualified(w, lc, nlp, 0);
1600
2107
  }
1601
2108
  function complementAst(entityType, diffAtom) {
1602
2109
  return {
@@ -1693,6 +2200,58 @@
1693
2200
  }
1694
2201
  return null;
1695
2202
  }
2203
+ var TEMPORAL_AUX = /* @__PURE__ */ new Set(["did", "was", "were", "do", "does", "has", "have", "had"]);
2204
+ var TEMPORAL_TAIL = /* @__PURE__ */ new Set([
2205
+ "change",
2206
+ "changed",
2207
+ "changes",
2208
+ "update",
2209
+ "updated",
2210
+ "updates",
2211
+ "modify",
2212
+ "modified",
2213
+ "modifies",
2214
+ "touch",
2215
+ "touched",
2216
+ "touches",
2217
+ "edit",
2218
+ "edited",
2219
+ "revise",
2220
+ "revised"
2221
+ ]);
2222
+ var TEMPORAL_TRAIL_FILLER = /* @__PURE__ */ new Set(["last", "recently", "ever", "get", "got", "been", "then", "now", "already"]);
2223
+ var TEMPORAL_DET = /* @__PURE__ */ new Set(["the", "a", "an", "all", "those", "these", "any"]);
2224
+ function parseTemporal(w, lc, nlp, depth = 0) {
2225
+ if (lc[0] !== "when") return null;
2226
+ let i = 1;
2227
+ if (!TEMPORAL_AUX.has(lc[i])) return null;
2228
+ i += 1;
2229
+ let t = -1;
2230
+ for (let k = lc.length - 1; k >= i; k -= 1) {
2231
+ if (TEMPORAL_TAIL.has(lc[k])) {
2232
+ t = k;
2233
+ break;
2234
+ }
2235
+ }
2236
+ if (t < 0) return null;
2237
+ let subjWords = w.slice(i, t);
2238
+ let subjLc = lc.slice(i, t);
2239
+ while (subjLc.length && TEMPORAL_TRAIL_FILLER.has(subjLc[subjLc.length - 1])) {
2240
+ subjWords = subjWords.slice(0, -1);
2241
+ subjLc = subjLc.slice(0, -1);
2242
+ }
2243
+ while (subjLc.length && TEMPORAL_DET.has(subjLc[0])) {
2244
+ subjWords = subjWords.slice(1);
2245
+ subjLc = subjLc.slice(1);
2246
+ }
2247
+ if (!subjWords.length) return null;
2248
+ if (!subjLc.some((x) => RELATIVE_PRONOUNS.includes(x))) return null;
2249
+ const framed = FRAME_WORDS.has(subjLc[0]) ? subjWords.join(" ") : `which ${subjWords.join(" ")}`;
2250
+ const inner = parseSetPhrase(framed, nlp, depth + 1);
2251
+ if (!inner || inner.node === "miss") return inner ? { node: "miss", reason: inner.reason || "the inner set of the temporal query didn't parse" } : { node: "miss", reason: "the inner set of the temporal query didn't parse" };
2252
+ const noun = entityNoun(subjLc[0]);
2253
+ return { node: "temporal", inner, entityType: noun && noun.entityType || null };
2254
+ }
1696
2255
  function parseAnaphora(w, lc, nlp) {
1697
2256
  let p = -1;
1698
2257
  let viaOf = false;
@@ -1958,21 +2517,21 @@
1958
2517
  let start = 0;
1959
2518
  let i = 0;
1960
2519
  while (i < predLc.length) {
1961
- let hit = null;
2520
+ let hit2 = null;
1962
2521
  for (const c of conns) {
1963
2522
  const cw = c.split(" ");
1964
2523
  if (predLc.slice(i, i + cw.length).join(" ") === c) {
1965
- hit = { c, len: cw.length };
2524
+ hit2 = { c, len: cw.length };
1966
2525
  break;
1967
2526
  }
1968
2527
  }
1969
- if (hit && i > start) {
2528
+ if (hit2 && i > start) {
1970
2529
  branches.push(predWords.slice(start, i));
1971
- ops.push(BOOLEAN_CONNECTIVES[hit.c]);
1972
- i += hit.len;
2530
+ ops.push(BOOLEAN_CONNECTIVES[hit2.c]);
2531
+ i += hit2.len;
1973
2532
  start = i;
1974
- } else if (hit) {
1975
- i += hit.len;
2533
+ } else if (hit2) {
2534
+ i += hit2.len;
1976
2535
  start = i;
1977
2536
  } else i += 1;
1978
2537
  }
@@ -1985,7 +2544,9 @@
1985
2544
  const edges2 = edgesOfKind(graph, symbolKind).filter((e) => objectIds.has(e.object));
1986
2545
  return uniqueById(edges2.map((e) => graph.byId.get(e.subject)).filter((s) => s && s.class === entityType));
1987
2546
  }
1988
- const edges = kindsFor(kind).flatMap((k) => edgesOfKind(graph, k)).filter((e) => objectIds.has(e.object));
2547
+ const objHasFine = !!symbolKind && [...objectIds].some((id) => FINE_ENTITY_TYPES.has(graph.byId.get(id)?.class));
2548
+ const scanKinds = objHasFine ? [...kindsFor(kind), symbolKind] : kindsFor(kind);
2549
+ const edges = scanKinds.flatMap((k) => edgesOfKind(graph, k)).filter((e) => objectIds.has(e.object));
1989
2550
  const subjects = uniqueById(edges.map((e) => graph.byId.get(e.subject)).filter(Boolean));
1990
2551
  if (!entityType || entityType === "Change") return subjects;
1991
2552
  const direct = subjects.filter((s) => s.class === entityType);
@@ -2165,6 +2726,15 @@
2165
2726
  }
2166
2727
  return n;
2167
2728
  }
2729
+ function evalTemporal(graph, ast, opts) {
2730
+ const inner = evalSet(graph, ast.inner, opts);
2731
+ const ids = new Set(inner.map((i) => i.id));
2732
+ if (!ids.size) return { compositeKind: "temporal", matches: [], entityType: ast.entityType, innerCount: 0 };
2733
+ const commits = reverseOverSet(graph, "touches", "Commit", ids);
2734
+ const dateOf = (c) => String((c.attributes || []).find((a) => a.key === "date")?.value || "");
2735
+ commits.sort((a, b) => dateOf(b).localeCompare(dateOf(a)));
2736
+ return { compositeKind: "temporal", matches: commits, entityType: ast.entityType, innerCount: inner.length };
2737
+ }
2168
2738
  function evalSuperlative(graph, ast) {
2169
2739
  const pool = graph.individuals.filter((i) => i.class === ast.entityType);
2170
2740
  const scored = pool.map((ind) => ({ ind, score: degreeMetric(graph, ind, ast.metric) })).sort((a, z) => ast.extreme === "most" ? z.score - a.score : a.score - z.score);
@@ -2178,6 +2748,7 @@
2178
2748
  if (ast.node === "count") return { compositeKind: "count", count: evalSet(graph, ast.base, opts).length, entityType: ast.entityType, matches: [] };
2179
2749
  if (ast.node === "list") return { compositeKind: "list", matches: evalSet(graph, ast.base, opts), entityType: ast.entityType, scoped: ast.scoped };
2180
2750
  if (ast.node === "superlative") return evalSuperlative(graph, ast);
2751
+ if (ast.node === "temporal") return evalTemporal(graph, ast, opts);
2181
2752
  if (ast.node === "anaphora") return evalAnaphora(graph, ast, opts);
2182
2753
  return { compositeKind: "set", matches: evalSet(graph, ast, opts), entityType: ast.entityType || null };
2183
2754
  }
@@ -2215,6 +2786,31 @@
2215
2786
  matches: result.matches
2216
2787
  };
2217
2788
  }
2789
+ if (result.compositeKind === "temporal") {
2790
+ const n = result.innerCount || 0;
2791
+ const setNoun = result.entityType ? nounFor(result.entityType, n || 2) : n === 1 ? "entity" : "entities";
2792
+ const wasWere = n === 1 ? "was" : "were";
2793
+ if (!n) {
2794
+ return { content: `nothing in the index matches the inner set, so there is no change history to date.`, miss: true, ambiguous: false, matches: [] };
2795
+ }
2796
+ if (!result.matches.length) {
2797
+ return { content: `no recorded commit touched the ${n} ${setNoun} in that set in this index.`, miss: true, ambiguous: false, matches: [] };
2798
+ }
2799
+ const newest = result.matches[0];
2800
+ const date = (newest.attributes || []).find((a) => a.key === "date")?.value || "";
2801
+ if (!date) {
2802
+ return { content: `the ${setNoun} in that set ${wasWere} last touched by commit ${newest.label}, but this index records no commit dates \u2014 regenerate the graph to attach mgx:commitDate.`, miss: true, ambiguous: false, matches: result.matches };
2803
+ }
2804
+ const msg = (newest.attributes || []).find((a) => a.key === "message")?.value || "";
2805
+ const day = String(date).slice(0, 10);
2806
+ const more = result.matches.length - 1;
2807
+ return {
2808
+ content: `the ${setNoun} in that set ${wasWere} last touched by commit ${newest.label} on ${day}${msg ? ` ("${msg}")` : ""}${more ? `; ${more} earlier commit${more === 1 ? "" : "s"} recorded` : ""}.`,
2809
+ miss: false,
2810
+ ambiguous: false,
2811
+ matches: result.matches
2812
+ };
2813
+ }
2218
2814
  if (!result.matches.length) {
2219
2815
  return { content: `nothing in the index matches that${result.entityType ? ` (${nounFor(result.entityType, 2)})` : ""}.`, miss: true, ambiguous: false, matches: [] };
2220
2816
  }
@@ -2562,6 +3158,11 @@
2562
3158
  const label = String(ind.label || ind.id || "");
2563
3159
  return ["Function", "Method"].includes(ind.class) ? `function ${label}()` : label;
2564
3160
  }
3161
+ function commitRefOf(ind) {
3162
+ const sha = String(ind.label || ind.id || "");
3163
+ const author = (ind.attributes || []).find((a) => a.key === "author")?.value;
3164
+ return author ? `${sha} (${author})` : sha;
3165
+ }
2565
3166
  function listJoin(syms) {
2566
3167
  return syms.length > 1 ? `${syms.slice(0, -1).join(", ")} and ${syms[syms.length - 1]}` : syms[0];
2567
3168
  }
@@ -2738,6 +3339,14 @@
2738
3339
  ambiguous: false
2739
3340
  };
2740
3341
  }
3342
+ if (parsed.kind === "tests" && !parsed.entityType) {
3343
+ const obj = String(parsed.object || "").replace(/^cover(?:s|ing)?\s+/i, "").trim();
3344
+ return {
3345
+ content: `No tests cover ${obj}. (traversal: ${result.traversal || "no traversal resolved"})`,
3346
+ miss: true,
3347
+ ambiguous: false
3348
+ };
3349
+ }
2741
3350
  const entityWord = nounFor(parsed.entityType || "Module", 2);
2742
3351
  return {
2743
3352
  content: `No ${entityWord} found whose module directly ${verbFor(parsed.kind)} ${parsed.object}. (traversal: ${result.traversal || "no traversal resolved"})`,
@@ -2746,7 +3355,7 @@
2746
3355
  };
2747
3356
  }
2748
3357
  if (parsed.shape === "forward" || parsed.entityType === "Module" || result.matches.every((m) => !FINE_ENTITY_TYPES.has(m.class))) {
2749
- const shown = result.matches.slice(0, OVERFLOW_CAP).map((m) => m.label);
3358
+ const shown = result.matches.slice(0, OVERFLOW_CAP).map((m) => m.class === "Commit" ? commitRefOf(m) : m.label);
2750
3359
  const extra2 = result.matches.length > OVERFLOW_CAP ? `, \u2026and ${result.matches.length - OVERFLOW_CAP} more` : "";
2751
3360
  return { content: shown.join(" and ") + extra2 + ".", miss: false, ambiguous: false, matches: result.matches };
2752
3361
  }
@@ -2806,17 +3415,17 @@
2806
3415
  function fuzzyCascadeWord(w) {
2807
3416
  const bound = fuzzyBound(w);
2808
3417
  let best = bound + 1;
2809
- let hit = null;
3418
+ let hit2 = null;
2810
3419
  let tied = false;
2811
3420
  for (const target of CASCADE_FUZZY_TARGETS) {
2812
3421
  const d = editDistance(w, target, Math.min(best, bound));
2813
3422
  if (d < best) {
2814
3423
  best = d;
2815
- hit = target;
3424
+ hit2 = target;
2816
3425
  tied = false;
2817
- } else if (d === best && d <= bound && target !== hit) tied = true;
3426
+ } else if (d === best && d <= bound && target !== hit2) tied = true;
2818
3427
  }
2819
- return best <= bound && !tied ? hit : null;
3428
+ return best <= bound && !tied ? hit2 : null;
2820
3429
  }
2821
3430
  function schemaTypoTrap(resolution, term) {
2822
3431
  if (!resolution?.match || resolution.matchedVia !== "fuzzy" || resolution.ambiguous) return false;
@@ -2875,7 +3484,7 @@
2875
3484
  const rendered = render(p, traverse(graph, p, { contextId, prev }));
2876
3485
  return rendered.miss ? null : { parsed: p, text };
2877
3486
  };
2878
- const done = (hit) => ({ parsed: hit.parsed, from, to: hit.text, dropped: [...dropped], steps });
3487
+ const done = (hit2) => ({ parsed: hit2.parsed, from, to: hit2.text, dropped: [...dropped], steps });
2879
3488
  let guard = 0;
2880
3489
  const hardCap = Math.max(tokens.length, 1) + 12;
2881
3490
  for (; guard < hardCap; guard += 1) {
@@ -2892,8 +3501,8 @@
2892
3501
  tokens = tokens.filter((_, i) => i !== idx);
2893
3502
  dropped.push(removed);
2894
3503
  steps.push(`strip noise "${removed}" \u2192 "${tokens.join(" ")}"`);
2895
- const hit = attempt(tokens);
2896
- if (hit) return done(hit);
3504
+ const hit2 = attempt(tokens);
3505
+ if (hit2) return done(hit2);
2897
3506
  }
2898
3507
  const survivors = [];
2899
3508
  const nowDropped = [];
@@ -2918,8 +3527,8 @@
2918
3527
  dropped.push(...nowDropped);
2919
3528
  if (corrected.length) steps.push(`fuzzy-correct ${JSON.stringify(corrected)} \u2192 "${tokens.join(" ")}"`);
2920
3529
  if (nowDropped.length) steps.push(`drop unmatched ${JSON.stringify(nowDropped)} \u2192 "${tokens.join(" ")}"`);
2921
- const hit = attempt(tokens);
2922
- if (hit) return done(hit);
3530
+ const hit2 = attempt(tokens);
3531
+ if (hit2) return done(hit2);
2923
3532
  }
2924
3533
  let changed = false;
2925
3534
  const normed = tokens.map((t) => {
@@ -2932,8 +3541,8 @@
2932
3541
  });
2933
3542
  if (changed) {
2934
3543
  steps.push(`normalise synonyms \u2192 "${normed.join(" ")}"`);
2935
- const hit = attempt(normed);
2936
- if (hit) return done(hit);
3544
+ const hit2 = attempt(normed);
3545
+ if (hit2) return done(hit2);
2937
3546
  }
2938
3547
  const bareLc = splitWords(from).map((t) => t.toLowerCase());
2939
3548
  const kindWords = [];
@@ -2945,10 +3554,10 @@
2945
3554
  else others.push(t);
2946
3555
  }
2947
3556
  if (kindWords.length === 1 && others.length === 0) {
2948
- const hit = attempt(["count", kindWords[0]]);
2949
- if (hit) {
3557
+ const hit2 = attempt(["count", kindWords[0]]);
3558
+ if (hit2) {
2950
3559
  steps.push(`bare kind "${kindWords[0]}" \u2192 count`);
2951
- return done(hit);
3560
+ return done(hit2);
2952
3561
  }
2953
3562
  }
2954
3563
  return null;
@@ -3013,7 +3622,7 @@
3013
3622
  };
3014
3623
  }
3015
3624
 
3016
- // packages/seonix/src/ask-browser-entry.mjs
3625
+ // src/ask-browser-entry.mjs
3017
3626
  function parseEntities(payload) {
3018
3627
  const individuals = Array.isArray(payload?.individuals) ? payload.individuals : [];
3019
3628
  const byId = /* @__PURE__ */ new Map();