@polycode-projects/the-mechanical-code-talker 1.0.4 → 1.0.5
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/package.json +1 -1
- package/src/ask.mjs +96 -12
- package/src/chat.mjs +41 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
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.",
|
package/src/ask.mjs
CHANGED
|
@@ -291,6 +291,14 @@ const NEST_SENTINEL = "zzinnerset";
|
|
|
291
291
|
// then"/"what about X though".
|
|
292
292
|
const PRED_LEAD_SKIP = new Set(["that", "which", "who", "are", "is", "was", "were", "do", "does", "also", "still", "both", "and", "then", "though"]);
|
|
293
293
|
const FRAME_WORDS = new Set(["which", "what", "who", "list", "show", "find", "give", "me", "us", "all"]);
|
|
294
|
+
// A bare copula leading a boolean branch ("...and ARE untested") is discourse glue,
|
|
295
|
+
// not part of the qualifier — dropped before a branch is tested/used as a
|
|
296
|
+
// qualifier-only atom (both the marker-gate probe below and the two atom-building
|
|
297
|
+
// folds — buildPredicateAtoms, parseRelationalOrQualified's own fold — apply it).
|
|
298
|
+
const COPULA_WORDS = new Set(["are", "is", "was", "were"]);
|
|
299
|
+
function dropLeadCopula(bw, blc) {
|
|
300
|
+
return blc.length && COPULA_WORDS.has(blc[0]) ? { bw: bw.slice(1), blc: blc.slice(1) } : { bw, blc };
|
|
301
|
+
}
|
|
294
302
|
|
|
295
303
|
const entityNoun = (w) => (ENTITY_TO_TYPE[w] ? { entityType: ENTITY_TO_TYPE[w], placeholder: false }
|
|
296
304
|
: (PLACEHOLDER_NOUNS.includes(w) ? { entityType: null, placeholder: true } : null));
|
|
@@ -930,7 +938,8 @@ function buildPredicateAtoms(entityType, subjPrefix, predLc, predWords, nlp, dep
|
|
|
930
938
|
const bw = branches[b];
|
|
931
939
|
const blc = bw.map((x) => x.toLowerCase());
|
|
932
940
|
const op = b === 0 ? "intersection" : ops[b - 1];
|
|
933
|
-
|
|
941
|
+
const qc = dropLeadCopula(bw, blc);
|
|
942
|
+
if (qc.blc.length && qc.blc.every((x) => QUALIFIERS[x])) { atoms.push({ op, kind: "qual", filters: qc.blc }); continue; }
|
|
934
943
|
if (blc[0] === "of" || blc[0] === "in") {
|
|
935
944
|
atoms.push({ op, kind: "set", ast: { node: "membership", entityType, term: bw.slice(1).join(" ") } });
|
|
936
945
|
continue;
|
|
@@ -1005,9 +1014,20 @@ function parseRelationalOrQualified(w, lc, nlp, depth) {
|
|
|
1005
1014
|
if (predLc.length && RELATIVE_PRONOUNS.includes(predLc[0])) { relFlag = true; predLc = predLc.slice(1); predWords = predWords.slice(1); }
|
|
1006
1015
|
const membershipLed = predLc[0] === "of" || predLc[0] === "in";
|
|
1007
1016
|
const gerundLed = predLc.length > 0 && isGerundVerb(predLc[0]);
|
|
1017
|
+
// A boolean branch whose OWN content — past an optional leading copula ("and ARE
|
|
1018
|
+
// untested") — collapses to qualifier words alone is the compositional shape too
|
|
1019
|
+
// ("functions that call X and are untested": verb clause AND qualifier, same
|
|
1020
|
+
// subject). Probe with the same splitBoolean+QUALIFIERS fold the atoms loop below
|
|
1021
|
+
// uses, so a bare verb+verb boolean chain with NO qualifier signal anywhere
|
|
1022
|
+
// ("which classes extends Base and couples to logging") still has no marker here
|
|
1023
|
+
// and correctly stays on the legacy ambiguous-parse path, untouched.
|
|
1024
|
+
const boolQualLed = predWords.length > 0 && splitBoolean(predLc, predWords).branches.some((bw) => {
|
|
1025
|
+
const { blc } = dropLeadCopula(bw, bw.map((x) => x.toLowerCase()));
|
|
1026
|
+
return blc.length && blc.every((x) => QUALIFIERS[x]);
|
|
1027
|
+
});
|
|
1008
1028
|
// marker gate — the crux of backward-compat: without one of these, this is not a
|
|
1009
1029
|
// compositional query and we must NOT hijack it from the existing parser.
|
|
1010
|
-
if (!(quals.length || relFlag || membershipLed || gerundLed)) return null;
|
|
1030
|
+
if (!(quals.length || relFlag || membershipLed || gerundLed || boolQualLed)) return null;
|
|
1011
1031
|
|
|
1012
1032
|
// empty predicate → a bare qualified class ("public methods")
|
|
1013
1033
|
if (!predWords.length) {
|
|
@@ -1026,7 +1046,8 @@ function parseRelationalOrQualified(w, lc, nlp, depth) {
|
|
|
1026
1046
|
const bw = branches[b];
|
|
1027
1047
|
const blc = bw.map((x) => x.toLowerCase());
|
|
1028
1048
|
const op = b === 0 ? "seed" : ops[b - 1];
|
|
1029
|
-
|
|
1049
|
+
const qc = dropLeadCopula(bw, blc);
|
|
1050
|
+
if (qc.blc.length && qc.blc.every((x) => QUALIFIERS[x])) { atoms.push({ op, kind: "qual", filters: qc.blc }); continue; }
|
|
1030
1051
|
if (blc[0] === "of" || blc[0] === "in") {
|
|
1031
1052
|
atoms.push({ op, kind: "set", ast: { node: "membership", entityType, term: bw.slice(1).join(" ") } });
|
|
1032
1053
|
continue;
|
|
@@ -1861,15 +1882,66 @@ export function resolveObject(graph, term, { expectedClass = null } = {}) {
|
|
|
1861
1882
|
}
|
|
1862
1883
|
}
|
|
1863
1884
|
} else {
|
|
1864
|
-
|
|
1885
|
+
// Root-cause fix (Tier-2 playtest cycle 9, targeted substring-match sweep):
|
|
1886
|
+
// this raw containment check has no minimum-length floor, so a short
|
|
1887
|
+
// closed-vocabulary word is a near-certain ACCIDENTAL substring of SOME
|
|
1888
|
+
// real label — confirmed empirically against the shipped mini-webapp
|
|
1889
|
+
// fixture: "so"->sendJson, "or"->Store, "a"->Task, "is"->listTasks
|
|
1890
|
+
// (ambiguous), "in"->Logger.info, "on"->sendJson, "at"->createApp,
|
|
1891
|
+
// "to"->Store, all via this exact branch. Cycle 8 patched three of these
|
|
1892
|
+
// ("and"/"also"/"so"/"then"/"now" and bare "it") one word at a time at
|
|
1893
|
+
// individual chat.mjs CALL SITES (STACCATO_LEAKED_CONNECTIVES, the
|
|
1894
|
+
// pronoun-reuse guard) — necessary there because those bugs are about
|
|
1895
|
+
// FOCUS bookkeeping, not resolution per se, but leaving resolveObject
|
|
1896
|
+
// itself unguarded meant every OTHER caller (existence checks, expectedClass
|
|
1897
|
+
// lookups, etc.) stayed exposed to the same trap for every not-yet-hit
|
|
1898
|
+
// short word. Gating the containment check at the same floor tier 5's own
|
|
1899
|
+
// fuzzy pass already uses (`tLc.length >= 4` below) closes it at the
|
|
1900
|
+
// source. A whole-token component match (just below) is unaffected by this
|
|
1901
|
+
// floor — it requires an EXACT path/identifier segment equality, never a
|
|
1902
|
+
// raw substring, so a genuinely short real identifier ("db", "fs") is
|
|
1903
|
+
// still resolvable by literally matching a whole segment.
|
|
1904
|
+
const termComps = [...componentSet(t)];
|
|
1905
|
+
// A SLASHED term's final path segment, extension stripped ("src/nope.mjs"
|
|
1906
|
+
// -> "nope", "cover app/lib/b.mjs" -> "b") — the semantically load-bearing
|
|
1907
|
+
// FILENAME STEM, as opposed to a directory segment or a leaked verb noise
|
|
1908
|
+
// word. Isolated from the actual whitespace-delimited PATH TOKEN (not the
|
|
1909
|
+
// raw multi-word string as a whole) — "app/lib/f.mjs but untested" (a
|
|
1910
|
+
// trailing-noise leak, distinct from the leading-verb-noise shape above)
|
|
1911
|
+
// has no extension at the end of the whole string, so splitting the whole
|
|
1912
|
+
// string would strand "f.mjs but untested" as a bogus non-matching "stem";
|
|
1913
|
+
// finding the one token that itself contains "/" keeps the path term
|
|
1914
|
+
// intact regardless of what noise surrounds it on either side. Only
|
|
1915
|
+
// slash-shaped terms compute this; a bare identifier/multi-word query has
|
|
1916
|
+
// no path structure to anchor on, so it's null and the gate below is a
|
|
1917
|
+
// no-op for those (unaffected — original ANY-overlap behavior).
|
|
1918
|
+
const pathToken = tLc.split(/\s+/).find((tok) => tok.includes("/"));
|
|
1919
|
+
const slashStem = pathToken ? pathToken.split("/").pop().replace(/\.[a-z0-9]+$/, "") : null;
|
|
1865
1920
|
for (const m of pool) {
|
|
1866
1921
|
const label = String(m.label || "").toLowerCase();
|
|
1867
|
-
if (label.includes(tLc)) {
|
|
1922
|
+
if (tLc.length >= 4 && label.includes(tLc)) {
|
|
1868
1923
|
scored.push({ ind: m, score: 1000 - Math.abs(label.length - tLc.length) });
|
|
1869
1924
|
continue;
|
|
1870
1925
|
}
|
|
1871
|
-
const
|
|
1872
|
-
|
|
1926
|
+
const labelComps = componentSet(m.label);
|
|
1927
|
+
const overlap = termComps.filter((c) => labelComps.has(c)).length;
|
|
1928
|
+
// For a slashed term, the stem MUST be among the label's own components
|
|
1929
|
+
// — an ANY-overlap match otherwise lets a NONEXISTENT path land on a
|
|
1930
|
+
// real module that merely shares its generic directory/extension
|
|
1931
|
+
// segments ("src", "mjs") with every other module in the pool (found
|
|
1932
|
+
// via the existence recognizer's "is there a class in src/nope.mjs"
|
|
1933
|
+
// scope clause, Tier-2 playtest cycle 9: it ambiguously "matched"
|
|
1934
|
+
// src/core/model.mjs even though no such module exists — the same
|
|
1935
|
+
// accidental-match disease as the short-word substring bug just above,
|
|
1936
|
+
// just triggered by GENERIC components instead of raw containment).
|
|
1937
|
+
// A leaked leading VERB ("cover app/lib/b.mjs", "touch app/lib/f.mjs" —
|
|
1938
|
+
// router-interface.test.mjs's own frozen contract) still resolves: the
|
|
1939
|
+
// stem ("b"/"f") is a real component of the target label even though
|
|
1940
|
+
// "cover"/"touch" themselves never overlap anything, so overlap>0 and
|
|
1941
|
+
// the stem gate both hold.
|
|
1942
|
+
if (overlap > 0 && (!slashStem || labelComps.has(slashStem))) {
|
|
1943
|
+
scored.push({ ind: m, score: overlap * 10 });
|
|
1944
|
+
}
|
|
1873
1945
|
}
|
|
1874
1946
|
}
|
|
1875
1947
|
scored.sort((a, b) => b.score - a.score);
|
|
@@ -1890,12 +1962,24 @@ export function resolveObject(graph, term, { expectedClass = null } = {}) {
|
|
|
1890
1962
|
// browser `lookupByProseTokens` is an undeclared identifier — without the guard,
|
|
1891
1963
|
// ANY term reaching this tier threw a ReferenceError in the page instead of
|
|
1892
1964
|
// rendering the honest miss (a real, previously-untested viewer bug).
|
|
1893
|
-
// DOTTED terms never consult prose: "res.json" word-matches
|
|
1894
|
-
// own path tokens, which is the tier-3 phantom-path bug
|
|
1895
|
-
// side door — a dotted term names an identifier, and
|
|
1896
|
-
// label (tiers above) or the bounded fuzzy pass
|
|
1965
|
+
// DOTTED or SLASHED terms never consult prose: "res.json" word-matches
|
|
1966
|
+
// test/res.json.js's own path tokens, which is the tier-3 phantom-path bug
|
|
1967
|
+
// reappearing through a side door — a dotted term names an identifier, and
|
|
1968
|
+
// identifiers resolve by label (tiers above) or the bounded fuzzy pass
|
|
1969
|
+
// below, or they honestly miss. The SAME side door was open for SLASHED
|
|
1970
|
+
// path terms too (Tier-2 playtest cycle 9, existence-recognizer follow-up):
|
|
1971
|
+
// "src/nope.mjs" — a nonexistent module — no longer false-matches tier 3
|
|
1972
|
+
// (the AND-across-components fix just above), but fell through to THIS
|
|
1973
|
+
// prose tier and ambiguously "matched" real modules anyway, because
|
|
1974
|
+
// lookupByProseTokens scores by ANY-token overlap (sum-scored, by design,
|
|
1975
|
+
// for genuine prose ranking) and "src"/"mjs" are near-universal path/
|
|
1976
|
+
// extension tokens shared by every module in the pool — the identical
|
|
1977
|
+
// accidental-match disease, just one tier further down. A slash-shaped term
|
|
1978
|
+
// names a literal path exactly like a dotted term names a literal symbol;
|
|
1979
|
+
// neither is prose, so neither should ever reach the prose fallback.
|
|
1980
|
+
const pathShaped = dotted || tLc.includes("/");
|
|
1897
1981
|
let proseResult = null;
|
|
1898
|
-
const proseHits = !
|
|
1982
|
+
const proseHits = !pathShaped && typeof lookupByProseTokens === "function"
|
|
1899
1983
|
? lookupByProseTokens(graph.proseIndex, t).filter((h) => !expectedClass || graph.byId.get(h.id)?.class === expectedClass)
|
|
1900
1984
|
: [];
|
|
1901
1985
|
if (proseHits.length) {
|
package/src/chat.mjs
CHANGED
|
@@ -3300,7 +3300,13 @@ async function describeWrapperAnswer(query, { config, source, focus }) {
|
|
|
3300
3300
|
* known edge concept (RELATION_TERM), no curated definition, or the graph has NO
|
|
3301
3301
|
* edges of that kind (composeRelation's own honest-miss gate). Loads the definition
|
|
3302
3302
|
* from the shipped corpus/seon/relations.jsonl, so it works without per-repo memory
|
|
3303
|
-
* seeding. Lazy + failure-tolerated throughout. Returns { text, pending }
|
|
3303
|
+
* seeding. Lazy + failure-tolerated throughout. Returns { text, pending, kind } —
|
|
3304
|
+
* `kind` is the resolved RELATION_TERM canonical kind (imports/calls/…), the SAME
|
|
3305
|
+
* vocabulary GOAL_BY_KIND keys on, so a caller whose own envelope.parsed never
|
|
3306
|
+
* stood (this force's whole reason to exist — see relationTermOf/
|
|
3307
|
+
* isVagueRelationTouch's own docs) can still deduce the correct "Goal (inferred):
|
|
3308
|
+
* …" line instead of silently carrying forward a null goal from earlier in the
|
|
3309
|
+
* turn. */
|
|
3304
3310
|
async function relationForceAnswer(query, envelope, { graph, config, source, templates }) {
|
|
3305
3311
|
const rawTerm = relationTermOf(query, envelope);
|
|
3306
3312
|
if (!rawTerm) return null;
|
|
@@ -3308,8 +3314,9 @@ async function relationForceAnswer(query, envelope, { graph, config, source, tem
|
|
|
3308
3314
|
try { ({ composeRelation, RELATION_TERM } = await import("./concept.mjs")); }
|
|
3309
3315
|
catch { return null; }
|
|
3310
3316
|
const term = String(rawTerm).toLowerCase();
|
|
3311
|
-
|
|
3312
|
-
|
|
3317
|
+
const kind = RELATION_TERM[term];
|
|
3318
|
+
if (!kind) return null; // not an enumerable edge concept — ordinary path owns it
|
|
3319
|
+
const definition = (await relationDefinitions()).get(kind) ?? null;
|
|
3313
3320
|
if (!definition) return null;
|
|
3314
3321
|
// Same graph-load fallback as conceptForceAnswer: the shell hands the loaded graph
|
|
3315
3322
|
// straight in; the pure runTurn(config) path loads it the way dispatchTool does.
|
|
@@ -3329,7 +3336,7 @@ async function relationForceAnswer(query, envelope, { graph, config, source, tem
|
|
|
3329
3336
|
const pending = composed.remainder && composed.remainder.length
|
|
3330
3337
|
? { items: composed.remainder, noun: composed.noun }
|
|
3331
3338
|
: null;
|
|
3332
|
-
return { text, pending };
|
|
3339
|
+
return { text, pending, kind };
|
|
3333
3340
|
}
|
|
3334
3341
|
|
|
3335
3342
|
/** THE CONCEPT FORCE — compose the three-band answer (corpus/seon definition + real
|
|
@@ -3571,7 +3578,23 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
3571
3578
|
// SAME value the debug trace's own "goal:" line uses (one deduction, two
|
|
3572
3579
|
// presentations) — null here (no parse stood at all) means withGoalLine
|
|
3573
3580
|
// shows nothing, never a "Goal (inferred): unclear" line.
|
|
3574
|
-
const
|
|
3581
|
+
// `let`, not `const`: the RELATION CONCEPT FORCE (relationForceAnswer, below)
|
|
3582
|
+
// can answer a turn CORRECTLY with no envelope.parsed at all to deduce from —
|
|
3583
|
+
// a staccato relation-chain continuation whose leading connective never
|
|
3584
|
+
// itself parses ("and inherits?": ask()'s raw grammar has no production for
|
|
3585
|
+
// a bare relation word with no verb, exactly like the "cochange" vague-touch
|
|
3586
|
+
// gap composeRelation's own degrade fix addressed) still reaches the SAME
|
|
3587
|
+
// relation force a normally-parsed "what about inherits" would. Tier-2
|
|
3588
|
+
// playtest cycle 9, the Goal-line gap cycle 8 flagged: the answer content
|
|
3589
|
+
// was always correct here — only the cosmetic trailing "Goal (inferred): …"
|
|
3590
|
+
// line went missing, because it was computed once, this early, straight off
|
|
3591
|
+
// envelope.parsed and never revisited even when a LATER lane went on to
|
|
3592
|
+
// answer the turn through a completely different path. Reassigned at the
|
|
3593
|
+
// relation-force call site below (never overwritten with something worse:
|
|
3594
|
+
// only filled in from the SAME GOAL_BY_KIND table deduceGoalFromParsed
|
|
3595
|
+
// itself already uses for a normally-parsed relation query, so the two
|
|
3596
|
+
// never disagree on the cases where both would fire).
|
|
3597
|
+
let deduced = deduceGoalFromParsed(envelope?.parsed);
|
|
3575
3598
|
note(trace, `goal: ${deduced ?? "unclear — the phrasing didn't resolve to a known query shape"}`);
|
|
3576
3599
|
// MISS handling. The intent lanes + short-miss are RECOGNIZER-gated on the query
|
|
3577
3600
|
// text AND only consulted on a would-miss, so a real graph query — a hit, an honest
|
|
@@ -3800,6 +3823,19 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
3800
3823
|
conceptPending = relation.pending;
|
|
3801
3824
|
note(trace, "lane: THE RELATION CONCEPT FORCE — the touched word named a known, edge-bearing relation kind");
|
|
3802
3825
|
note(trace, "source: relationForceAnswer over the loaded graph's own edges (not corpus)");
|
|
3826
|
+
// Goal-line gap fix (Tier-2 playtest cycle 9): this force just answered
|
|
3827
|
+
// the turn CORRECTLY over a query shape ask()'s own grammar may never
|
|
3828
|
+
// have parsed at all (a staccato relation-chain continuation whose
|
|
3829
|
+
// leading connective never itself parses, "and inherits?") — `deduced`
|
|
3830
|
+
// was computed way above, off envelope.parsed alone, and would
|
|
3831
|
+
// otherwise stay null forever here even though the answer is real.
|
|
3832
|
+
// relation.kind is the SAME GOAL_BY_KIND vocabulary a normally-parsed
|
|
3833
|
+
// relation query already deduces its goal line from, so this can never
|
|
3834
|
+
// disagree with the ordinary path on a case where both would fire.
|
|
3835
|
+
if (relation.kind && GOAL_BY_KIND[relation.kind]) {
|
|
3836
|
+
deduced = GOAL_BY_KIND[relation.kind];
|
|
3837
|
+
note(trace, `goal: ${deduced} (revised — the relation concept force answered where the raw parse never stood)`);
|
|
3838
|
+
}
|
|
3803
3839
|
}
|
|
3804
3840
|
}
|
|
3805
3841
|
}
|