@polycode-projects/the-mechanical-code-talker 0.4.0 → 0.6.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/src/ask.mjs CHANGED
@@ -245,6 +245,7 @@ function parseComposite(text, nlp) {
245
245
  const w = splitWords(text);
246
246
  const lc = w.map((x) => x.toLowerCase());
247
247
  return parseNegation(text, nlp, 0)
248
+ || parseForwardNegation(w, lc, nlp)
248
249
  || parseAnaphora(w, lc, nlp)
249
250
  || parseAggregate(w, lc, nlp)
250
251
  || parseSuperlative(w, lc, nlp)
@@ -318,6 +319,54 @@ function parseNegation(text, nlp, depth = 0) {
318
319
  return complementAst(entityType, { op: "difference", kind: "set", ast: positive });
319
320
  }
320
321
 
322
+ // B1 FORWARD NEGATION (Cycle 5, pron+neg) — the SUBJECT-side complement's mirror: "what
323
+ // does[n't] <subj> <verb>" ("what doesn't it import", "what does app/lib/e.mjs not import")
324
+ // is every individual of the verb's OBJECT grain that <subj> does NOT reach via that verb.
325
+ // Distinct from parseNegation (which negates a queried KIND — "which modules do not import
326
+ // X"): here the negation sits on a FORWARD clause whose subject is a named term or a focus
327
+ // pronoun, so the universe is inferred from the verb's own edges (imports → Module) rather
328
+ // than a stated kind noun. The subject is resolved LATE (at eval, through the same
329
+ // contextId a plain "it" uses), so pronoun-binding composes with the complement for free.
330
+ // Refused honestly (empty) when the verb's object grain is ambiguous or the subject can't
331
+ // resolve — never a guess. Runs AFTER parseNegation, so the stated-kind form is unaffected.
332
+ const FWD_NEG_FRAME = new Set(["what", "which", "thing", "things", "one", "ones", "stuff"]);
333
+ function parseForwardNegation(w, lc, nlp) {
334
+ let i = 0;
335
+ while (i < lc.length && FWD_NEG_FRAME.has(lc[i])) i += 1;
336
+ if (!["do", "does", "did"].includes(lc[i])) return null; // need the auxiliary lead
337
+ i += 1;
338
+ const rest = w.slice(i);
339
+ const restLc = lc.slice(i);
340
+ const notIdx = restLc.indexOf("not");
341
+ if (notIdx < 0) return null; // no negation → not this shape
342
+ const vh = findPhrase(restLc, VERB_TO_KIND);
343
+ if (!vh) return null; // no relation verb → not this shape
344
+ // the subject term is whatever survives after removing "not", the verb phrase, "from",
345
+ // and question scaffolding — a bare pronoun "it" (not a stopword) survives and binds to
346
+ // the focus at eval time; a named module/symbol survives and resolves directly.
347
+ const subjTokens = rest.filter((_, j) => j !== notIdx && (j < vh.start || j >= vh.end)
348
+ && restLc[j] !== "from" && !STOPWORDS.has(restLc[j]));
349
+ const subjectTerm = subjTokens.join(" ").trim();
350
+ if (!subjectTerm) return null;
351
+ return { node: "forwardComplement", kind: vh.kind, subjectTerm };
352
+ }
353
+
354
+ /** The single OBJECT class a forward relation kind points at across the loaded graph
355
+ * (imports → Module), or null when its objects span more than one class (an ambiguous
356
+ * grain the complement's universe can't be pinned to). Ext: endpoints have no individual,
357
+ * so they don't muddy the class vote. Used by the forwardComplement evaluator to bound
358
+ * the universe it differences the positive forward set out of. */
359
+ function kindObjectClass(graph, kind) {
360
+ const classes = new Set();
361
+ for (const k of kindsFor(kind)) {
362
+ for (const e of edgesOfKind(graph, k)) {
363
+ const o = graph.byId.get(e.object);
364
+ if (o && o.class) classes.add(o.class);
365
+ }
366
+ }
367
+ return classes.size === 1 ? [...classes][0] : null;
368
+ }
369
+
321
370
  /** A set-producing sub-expression (used for nested inner clauses, boolean branches,
322
371
  * and count restrictors): nested first, then the relational/qualifier/boolean
323
372
  * parser, then a bare simple clause. Carries `depth` for the nesting cap. */
@@ -373,12 +422,21 @@ function parseNested(w, lc, nlp, depth) {
373
422
  * uncompilable), or null. */
374
423
  function parseAnaphora(w, lc, nlp) {
375
424
  let p = -1;
425
+ let viaOf = false;
376
426
  for (let i = 1; i < lc.length; i += 1) {
377
- if (ANAPHORA_TRIGGERS.includes(lc[i]) && lc[i - 1] === "of") { p = i; break; }
427
+ if (!ANAPHORA_TRIGGERS.includes(lc[i])) continue;
428
+ if (lc[i - 1] === "of") { p = i; viaOf = true; break; } // "how many of those", "which of them"
429
+ // BARE anaphoric pronoun as the FINAL word, directly after a count/list trigger
430
+ // ("count them", "count those", "list them") — the discourse-reference count/list over
431
+ // the previous answer with no "of" (Cycle 5, disc+count). Pinned to the terminal
432
+ // position so a mid-sentence "these"/"those" used as a determiner ("list these
433
+ // functions") is left for the ordinary list/clause path, not seized as an anaphor.
434
+ const headSoFar = lc.slice(0, i).join(" ");
435
+ if (i === lc.length - 1 && (AGGREGATE_TRIGGERS.includes(headSoFar) || LIST_TRIGGERS.includes(headSoFar))) { p = i; break; }
378
436
  }
379
437
  if (p < 0) return null;
380
- const head = lc.slice(0, p - 1).join(" ");
381
- const mode = /^(how many|how much|count)\b/.test(head) ? "count" : "list";
438
+ const head = (viaOf ? lc.slice(0, p - 1) : lc.slice(0, p)).join(" ");
439
+ const mode = AGGREGATE_TRIGGERS.includes(head) || /^(how many|how much|count|number|quantity|total)\b/.test(head) ? "count" : "list";
382
440
  const filter = parsePredicateFilter(w.slice(p + 1), nlp);
383
441
  if (filter === undefined) return { node: "miss", reason: "the follow-up filter didn't parse" };
384
442
  return { node: "anaphora", mode, filter };
@@ -772,6 +830,16 @@ function evalSet(graph, ast, opts) {
772
830
  const subs = new Set(kindsFor(ast.kind).flatMap((k) => edgesOfKind(graph, k)).map((e) => e.subject));
773
831
  return graph.individuals.filter((i) => subs.has(i.id) && (!ast.entityType || i.class === ast.entityType));
774
832
  }
833
+ // forward complement: the verb's object-grain universe MINUS what the (late-resolved,
834
+ // focus-bindable) subject reaches via that verb — "what doesn't it import".
835
+ case "forwardComplement": {
836
+ const r = resolveTermOrContext(graph, ast.subjectTerm, opts && opts.contextId);
837
+ if (!r.match) return []; // unresolved subject / focus-less pronoun → honest empty
838
+ const universeType = kindObjectClass(graph, ast.kind);
839
+ if (!universeType) return []; // ambiguous object grain → refuse honestly
840
+ const positive = new Set(forwardOverSet(graph, ast.kind, new Set([r.match.id])).map((x) => x.id));
841
+ return graph.individuals.filter((i) => i.class === universeType && !positive.has(i.id));
842
+ }
775
843
  case "reverseSet": {
776
844
  const ids = new Set(evalSet(graph, ast.inner, opts).map((i) => i.id));
777
845
  return reverseOverSet(graph, ast.kind, ast.entityType, ids);
@@ -1401,11 +1469,23 @@ export function traverse(graph, parsed, { contextId = null, prev = null } = {})
1401
1469
  };
1402
1470
  }
1403
1471
 
1404
- // reverse: "which <entityType> R <objMatch>"
1472
+ // reverse: "which <entityType> R <objMatch>". GRAIN-AWARE (Cycle 5, lever 3): a kind
1473
+ // that carries a symbol-grain sibling reads off the SIBLING when a fine SUBJECT grain
1474
+ // was asked for ("which functions call X" → callsSymbol). It ALSO reads off the sibling
1475
+ // when the RESOLVED OBJECT is itself a fine symbol, for EVERY kind with a sibling — not
1476
+ // only touches: the module-coarse edge (calls Module→Module, touches Commit→Module) can
1477
+ // NEVER point at a function/method, so a bare "what calls fnAlpha" scanning the coarse
1478
+ // `calls` edges returned a FALSE empty ("No modules found …") while the graph records a
1479
+ // real symbol-level caller (Widget.render --callsSymbol--> fnAlpha). The honest answer
1480
+ // reads off callsSymbol at symbol grain; a truly-uncalled symbol still renders the
1481
+ // honest empty, now with the accurate callsSymbol receipt. (Previously scoped to touches
1482
+ // only, which left this exact callsSymbol caller invisible — a genuine correctness bug.)
1405
1483
  const symbolKind = SYMBOL_GRAIN_SIBLING[kind];
1406
- if (symbolKind && FINE_ENTITY_TYPES.has(entityType)) {
1484
+ const objIsFineSymbol = !!(objMatch.class && FINE_ENTITY_TYPES.has(objMatch.class));
1485
+ if (symbolKind && (FINE_ENTITY_TYPES.has(entityType) || objIsFineSymbol)) {
1407
1486
  const edges = edgesOfKind(graph, symbolKind).filter((e) => e.object === objMatch.id);
1408
- const matches = edges.map((e) => graph.byId.get(e.subject)).filter((i) => i && i.class === entityType);
1487
+ const subjects = uniqueById(edges.map((e) => graph.byId.get(e.subject)).filter(Boolean));
1488
+ const matches = (!entityType || entityType === "Change") ? subjects : subjects.filter((i) => i.class === entityType);
1409
1489
  return { matches, objMatch, candidates, traversal: `${symbolKind} edges where object = ${objMatch.label}`, ambiguous, matchedVia };
1410
1490
  }
1411
1491
 
@@ -1662,6 +1742,10 @@ function renderCore(parsed, result) {
1662
1742
  miss: true, ambiguous: false,
1663
1743
  };
1664
1744
  }
1745
+ // NOTE (Cycle 5): a voice-nit rephrasing ("that directly <verb>") was reverted —
1746
+ // the frozen v1 cases.jsonl pins the "whose module directly <verb>s X" wording
1747
+ // (hm-empty-result-calls / tf-wat-calls / ns-wondering), and the case set is
1748
+ // append-only/sacred mid-arc, so the honest-miss phrasing stays as-is.
1665
1749
  const entityWord = nounFor(parsed.entityType || "Module", 2);
1666
1750
  return {
1667
1751
  content: `No ${entityWord} found whose module directly ${verbFor(parsed.kind)} ${parsed.object}. (traversal: ${result.traversal || "no traversal resolved"})`,