@polycode-projects/the-mechanical-code-talker 1.10.4 → 1.10.7
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/chat.mjs +116 -14
- package/src/completions/group.mjs +1 -1
- package/src/completions/rank.mjs +3 -3
- package/src/corpus/unknown-ingest.mjs +1 -1
- package/src/finish.mjs +1 -1
- package/src/grammar/ace.mjs +31 -2
- package/src/grammar/lexicon.mjs +1 -1
- package/src/init.mjs +1 -2
- package/src/interpret/normalize.mjs +1 -1
- package/src/router/guardrail.mjs +3 -4
- package/src/router/registry.mjs +14 -21
- package/src/router/resolver.mjs +1 -1
- package/src/source-slice.mjs +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.7",
|
|
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/chat.mjs
CHANGED
|
@@ -81,7 +81,7 @@ function note(trace, text) { if (trace) trace.push(text); }
|
|
|
81
81
|
|
|
82
82
|
/** relation `kind` (ask-vocab.mjs RELATIONS) -> a short, deterministic
|
|
83
83
|
* statement of what a person asking that KIND of question is probably after.
|
|
84
|
-
*
|
|
84
|
+
* A small bucket lookup over the query SHAPE the engine
|
|
85
85
|
* already computed — tmct is no-LLM, so goal deduction is table-driven, never
|
|
86
86
|
* free-text generation. A kind/shape this table doesn't recognise falls
|
|
87
87
|
* through to a generic line in deduceGoalFromParsed, never a fabricated guess. */
|
|
@@ -417,8 +417,7 @@ const ANAPHORA_COUNT_RE = /\b(?:how many|how much|count|number of)\s+(?:of\s+)?(
|
|
|
417
417
|
* filter on) is untouched. */
|
|
418
418
|
const IMPLICIT_ANAPHORA_COUNT_RE = /^(?:(?:and|so|then|also)\s+)?how many (?:are|is|were|was)\s+(?!there\b)(\S.*)$/i;
|
|
419
419
|
|
|
420
|
-
/** "have"/"has"/"holds"/"hold" are excluded from RESTRICTOR_VERB_RE below
|
|
421
|
-
* deliberately treated as non-restrictor cues, not a bug fix skipped.
|
|
420
|
+
/** "have"/"has"/"holds"/"hold" are excluded from RESTRICTOR_VERB_RE below.
|
|
422
421
|
* Ask-vocab's VERB_TO_KIND maps them to "defines" unconditionally, but the
|
|
423
422
|
* graph's actual "have" semantics are subject-type-dependent (a Module "has"
|
|
424
423
|
* things it defines; a Class "has" things it contains) — ask.mjs's own
|
|
@@ -797,8 +796,8 @@ const AI_IDENTITY_PHRASES = [
|
|
|
797
796
|
* two-sentence turn like "are you an AI? like chatgpt?" could never match
|
|
798
797
|
* the whole raw string even though its first clause alone is an exact "are
|
|
799
798
|
* you an AI" hit. Used ONLY by aiIdentityMatch below — every OTHER
|
|
800
|
-
* closed-set match in this file stays whole-string
|
|
801
|
-
*
|
|
799
|
+
* closed-set match in this file stays whole-string (scoped to the one
|
|
800
|
+
* family that's shown up broken this way). */
|
|
802
801
|
function splitClauses(text) {
|
|
803
802
|
return String(text).split(/[?!.]+\s*/).map((c) => c.trim()).filter(Boolean);
|
|
804
803
|
}
|
|
@@ -1848,11 +1847,10 @@ const UNKNOWN_SUBJECT_RE = /^(every\s+|each\s+|all\s+|a\s+|an\s+)?([\w-]+(?:\s+[
|
|
|
1848
1847
|
const MINT_ISA_PREDICATES = new Set(["rdfs:subClassOf", "rdf:type"]);
|
|
1849
1848
|
|
|
1850
1849
|
/** Small CLOSED set of generic English root nouns that count as always-
|
|
1851
|
-
* grounded anchor terms for the mint-fallbacks below
|
|
1852
|
-
*
|
|
1853
|
-
*
|
|
1854
|
-
*
|
|
1855
|
-
* today). Their only job is to give a user who hits the "both sides
|
|
1850
|
+
* grounded anchor terms for the mint-fallbacks below — not in
|
|
1851
|
+
* lexicon-core.json (these are ordinary-English root nouns with no code
|
|
1852
|
+
* meaning, absent from that code vocabulary). Their only job is to give a
|
|
1853
|
+
* user who hits the "both sides
|
|
1856
1854
|
* ungrounded" decline (groundingSuggestionMiss, below) an honest, guessable
|
|
1857
1855
|
* way in: ground one brand-new term via one of THESE words first ("every
|
|
1858
1856
|
* zorp is a thing"), then chain the other new term off the now-grounded one. */
|
|
@@ -2314,6 +2312,11 @@ const GENERAL_VERB_NOT_A_VERB_RE = new RegExp(
|
|
|
2314
2312
|
async function generalVerbPredicate(verb) {
|
|
2315
2313
|
const v = String(verb || "").toLowerCase();
|
|
2316
2314
|
if (v === "has" || v === "have") return HAS_A_PREDICATE;
|
|
2315
|
+
// The modal maps onto the corpus's own capability predicate — "dog can
|
|
2316
|
+
// swim" is a capability claim, not a transitive "to can" — so a taught
|
|
2317
|
+
// capability reads back interoperably with /r/CapableOf data and the
|
|
2318
|
+
// "can a X <verb>" reader finds it (same reasoning as HAS_A above).
|
|
2319
|
+
if (v === "can") return "mgx:capableOf";
|
|
2317
2320
|
try {
|
|
2318
2321
|
const { proseLemma } = await import("./prose-nlp.mjs");
|
|
2319
2322
|
const lemma = proseLemma();
|
|
@@ -2345,6 +2348,10 @@ async function generalVerbTeach(payload) {
|
|
|
2345
2348
|
const verb = verbRaw.toLowerCase();
|
|
2346
2349
|
if (GENERAL_VERB_EXCLUDE_RE.test(verb)) return null; // owned by a more specific frame above
|
|
2347
2350
|
if (GENERAL_VERB_NOT_A_VERB_RE.test(verb)) return null; // a closed-class word can never be the real verb
|
|
2351
|
+
// "cannot" would mint a nonsense mgx:cannot fact whose read-back silently
|
|
2352
|
+
// INVERTS the taught meaning — the vocabulary has no negative-capability
|
|
2353
|
+
// predicate, so an honest decline is the only correct move.
|
|
2354
|
+
if (verb === "cannot") return null;
|
|
2348
2355
|
if (GENERAL_VERB_DETERMINER_RE.test(subjectRaw)) return null; // not a bare-name subject
|
|
2349
2356
|
const subject = subjectRaw.trim();
|
|
2350
2357
|
const object = objectRaw.replace(/^an?\s+/i, "").trim();
|
|
@@ -4341,12 +4348,27 @@ export async function factAnswer(memoryDir, query, envelope, miss, biasByBundle
|
|
|
4341
4348
|
// single-hit lookup, same "never a guessed no" discipline).
|
|
4342
4349
|
const can = q.match(CAN_ASK_RE);
|
|
4343
4350
|
if (can) {
|
|
4351
|
+
const facts = await memoryFacts(memoryDir);
|
|
4344
4352
|
const subj = factTermVariants(normFactTerm, can[1]);
|
|
4345
4353
|
const obj = factTermVariants(normFactTerm, can[2]);
|
|
4346
|
-
const hit =
|
|
4354
|
+
const hit = facts.find(
|
|
4347
4355
|
(f) => f.predicate === "mgx:capableOf" && subj.has(f.subject) && obj.has(f.object),
|
|
4348
4356
|
);
|
|
4349
4357
|
if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
|
|
4358
|
+
// A KNOWN subject with capability facts, none matching: an honest,
|
|
4359
|
+
// specific miss citing what it CAN do — the same closer the is-a ladder
|
|
4360
|
+
// answers with, instead of the misleading structural parse wall. An
|
|
4361
|
+
// unknown subject still declines. Never a guessed "no": absence of a
|
|
4362
|
+
// capableOf fact proves nothing.
|
|
4363
|
+
const knownCan = facts.filter((f) => f.predicate === "mgx:capableOf" && subj.has(f.subject));
|
|
4364
|
+
if (knownCan.length) {
|
|
4365
|
+
const shown = knownCan.slice(0, 3).map(renderFactLine).join("; ");
|
|
4366
|
+
return {
|
|
4367
|
+
text: `I can't confirm that — nothing I remember says ${can[1]} can ${can[2]}. I do know: ${shown}. If it's true, teach me: "a ${can[1]} can ${can[2]}".`,
|
|
4368
|
+
replace: true,
|
|
4369
|
+
miss: true,
|
|
4370
|
+
};
|
|
4371
|
+
}
|
|
4350
4372
|
return null;
|
|
4351
4373
|
}
|
|
4352
4374
|
|
|
@@ -5370,7 +5392,45 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
5370
5392
|
// to the honest miss below, never a guessed "no".
|
|
5371
5393
|
const { deriveDisjointViolations, DISJOINT_PREDICATE } = await import("./syllogise.mjs");
|
|
5372
5394
|
const disjointRows = rows.filter((f) => f.predicate === DISJOINT_PREDICATE && isTaught(f));
|
|
5395
|
+
// NEGATED membership — "is a dog not a cat". ISA_ASK_RE captures the
|
|
5396
|
+
// subject as "dog not" (the "not" glues onto the subject because the
|
|
5397
|
+
// article anchors the kind), so without this the negated question walks
|
|
5398
|
+
// the positive ladder with a garbage subject and lands on a nonsense
|
|
5399
|
+
// teach hint. Strip the "not", then answer INVERTED: a positive isa fact
|
|
5400
|
+
// refutes it ("no — dog is a kind of cat"), a taught disjointness
|
|
5401
|
+
// confirms it ("yes — dog is not a cat"), anything else is an honest
|
|
5402
|
+
// can't-confirm pointing at the already-supported "no X is a Y" teach
|
|
5403
|
+
// shape. Deliberately shallow — no chain chases on the negated side; a
|
|
5404
|
+
// negative proved through a multi-hop positive chain stays an honest
|
|
5405
|
+
// miss rather than a guess.
|
|
5406
|
+
const negSubject = isaAsk[1].match(/^(.*\S)\s+not$/i);
|
|
5407
|
+
if (negSubject) {
|
|
5408
|
+
const negSubjVariants = factTermVariants(normFactTerm, negSubject[1]);
|
|
5409
|
+
const negObjVariants = objVariants;
|
|
5410
|
+
const posHit = isa
|
|
5411
|
+
.filter((f) => negSubjVariants.has(f.subject) && negObjVariants.has(f.object))
|
|
5412
|
+
.sort(byTrust)[0];
|
|
5413
|
+
if (posHit) return { text: `no — ${renderFactLine(posHit)}`, replace: true };
|
|
5414
|
+
const negDisjoint = disjointRows.find((f) => (negSubjVariants.has(f.subject) && negObjVariants.has(f.object))
|
|
5415
|
+
|| (negSubjVariants.has(f.object) && negObjVariants.has(f.subject)));
|
|
5416
|
+
if (negDisjoint) return { text: `yes — ${renderFactLine(negDisjoint)}`, replace: true };
|
|
5417
|
+
const negSubjectWord = negSubject[1].trim();
|
|
5418
|
+
const negKindWord = stripTrailingDiscourseTag(isaAsk[2]).trim();
|
|
5419
|
+
return {
|
|
5420
|
+
text: `I can't confirm that either way — nothing I remember links ${negSubjectWord} and ${negKindWord}. If no ${negSubjectWord} is a ${negKindWord}, teach me: "no ${negSubjectWord} is a ${negKindWord}".`,
|
|
5421
|
+
replace: true,
|
|
5422
|
+
miss: true,
|
|
5423
|
+
};
|
|
5424
|
+
}
|
|
5373
5425
|
if (disjointRows.length) {
|
|
5426
|
+
// A DIRECT taught disjointness between the asked subject and kind is a
|
|
5427
|
+
// provable "no" on its own — deriveDisjointViolations only ever fires
|
|
5428
|
+
// through a taught rdf:type premise, so without this check "no dog is
|
|
5429
|
+
// a cat" followed by "is a dog a cat" fell through to the can't-confirm
|
|
5430
|
+
// closer instead of the honest no.
|
|
5431
|
+
const directDisjoint = disjointRows.find((f) => (subjCandidates.has(f.subject) && objVariants.has(f.object))
|
|
5432
|
+
|| (subjCandidates.has(f.object) && objVariants.has(f.subject)));
|
|
5433
|
+
if (directDisjoint) return { text: `no — ${renderFactLine(directDisjoint)}`, replace: true };
|
|
5374
5434
|
const disjointEdges = disjointRows.map((f) => [f.subject, f.object]);
|
|
5375
5435
|
const violations = deriveDisjointViolations(chainTypeEdges, chainSubClassEdges, disjointEdges, { budget: 10 });
|
|
5376
5436
|
for (const subj of subjCandidates) {
|
|
@@ -5503,7 +5563,42 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
5503
5563
|
}
|
|
5504
5564
|
}
|
|
5505
5565
|
}
|
|
5506
|
-
|
|
5566
|
+
// Every yes-chase and the disjoint "no" above missed. The old
|
|
5567
|
+
// unconditional decline here fell through to the structural "couldn't
|
|
5568
|
+
// parse this as a graph question" wall — actively misleading for a KNOWN
|
|
5569
|
+
// subject, twice over: the question DID parse, and the wall's hint
|
|
5570
|
+
// suggests the exact shape the user just typed. When the subject has
|
|
5571
|
+
// remembered isa-family facts, answer with an honest, specific miss that
|
|
5572
|
+
// cites what IS remembered instead. An unknown subject still declines to
|
|
5573
|
+
// the standing wall/teach-hint path — and this never guesses a "no".
|
|
5574
|
+
// The SUBJECT'S OWN variants only — subjCandidates was augmented above
|
|
5575
|
+
// with the graph entity's class noun (the CLASS↔INSTANCE bridge), and
|
|
5576
|
+
// filtering on it here would cite facts about that noun ("class ⊑
|
|
5577
|
+
// component") as if they were facts about the asked subject ("Widget").
|
|
5578
|
+
const directSubjVariants = factTermVariants(normFactTerm, isaAsk[1]);
|
|
5579
|
+
const knownSubjectIsa = isa.filter((f) => directSubjVariants.has(f.subject)).sort(byTrust);
|
|
5580
|
+
const subjectWord = isaAsk[1].trim();
|
|
5581
|
+
const kindWord = stripTrailingDiscourseTag(isaAsk[2]).trim();
|
|
5582
|
+
if (knownSubjectIsa.length) {
|
|
5583
|
+
const shown = knownSubjectIsa.slice(0, 3).map(renderFactLine).join("; ");
|
|
5584
|
+
return {
|
|
5585
|
+
text: `I can't confirm that — nothing I remember says ${subjectWord} is a ${kindWord}. I do know: ${shown}. If it's true, teach me: "${subjectWord} is a kind of ${kindWord}".`,
|
|
5586
|
+
replace: true,
|
|
5587
|
+
miss: true, // still a MISS in the turn record — honest wording, not an answer
|
|
5588
|
+
};
|
|
5589
|
+
}
|
|
5590
|
+
// Subject with NO isa facts: only divert when it's mentioned NOWHERE at
|
|
5591
|
+
// all (no fact row on either side, no code entity by id OR class noun) —
|
|
5592
|
+
// a subject known via OTHER predicates ("ahab is male") or the code graph
|
|
5593
|
+
// keeps the old decline, so nothing downstream is ever shadowed.
|
|
5594
|
+
if (!ent && !noun && !rows.some((f) => subjCandidates.has(f.subject) || subjCandidates.has(f.object))) {
|
|
5595
|
+
return {
|
|
5596
|
+
text: `I can't confirm that — I don't know "${subjectWord}" at all yet. If it's true, teach me: "${subjectWord} is a kind of ${kindWord}".`,
|
|
5597
|
+
replace: true,
|
|
5598
|
+
miss: true,
|
|
5599
|
+
};
|
|
5600
|
+
}
|
|
5601
|
+
return null; // the honest miss stands (never a guessed "no")
|
|
5507
5602
|
}
|
|
5508
5603
|
|
|
5509
5604
|
// (a1c-i) CARDINALITY MONOTONICITY — "does every X have at least N Y" over
|
|
@@ -7105,6 +7200,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7105
7200
|
if (memoryDir) {
|
|
7106
7201
|
bareMetaHit = (await factAnswer(memoryDir, gateQuery, envelope, miss, biasByBundle, cache))
|
|
7107
7202
|
?? (await factReadBack(memoryDir, gateQuery, envelope, miss, graph, newFocus?.label, biasByBundle, cache));
|
|
7203
|
+
if (bareMetaHit?.miss) bareMetaHit = null; // an honest-miss return never diverts the gate
|
|
7108
7204
|
// A bare "what is X" with NO taught fact but a KNOWN curated corpus term
|
|
7109
7205
|
// ("what is cache", no article) needs the same "only diverts on a REAL
|
|
7110
7206
|
// hit" treatment — curatedDefinitionAnswer otherwise only ever runs once
|
|
@@ -7175,8 +7271,14 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7175
7271
|
?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label, biasByBundle, cache));
|
|
7176
7272
|
if (fact) {
|
|
7177
7273
|
answer = fact.replace ? fact.text : `${answer}\n${fact.text}`;
|
|
7178
|
-
|
|
7179
|
-
|
|
7274
|
+
// A fact-lane return flagged `miss` is an HONEST MISS in better words
|
|
7275
|
+
// (the isa ladder's "I can't confirm that" closers) — the turn record
|
|
7276
|
+
// keeps miss=true and via stays untouched, so miss-rate metrics and
|
|
7277
|
+
// recall's own miss-gated lanes see it exactly like the wall it replaced.
|
|
7278
|
+
if (!fact.miss) {
|
|
7279
|
+
via = "fact";
|
|
7280
|
+
recordMiss = false;
|
|
7281
|
+
}
|
|
7180
7282
|
if (fact.pending) factPending = fact.pending; // a truncated fact list → paginable remainder
|
|
7181
7283
|
if (typeof fact.trust === "number") entailedTrust = fact.trust; // live-chase trust (see the `entailedTrust` declaration above)
|
|
7182
7284
|
note(trace, `lane: (3) memory facts — factAnswer/factReadBack matched (memoryDir=${memoryDir})`);
|
|
@@ -19,7 +19,7 @@ function contentTokens(text) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/** Plain union-find (path halving, union-by-index) — small N here (a single broad search's
|
|
22
|
-
* hit count)
|
|
22
|
+
* hit count). */
|
|
23
23
|
function unionFind(n) {
|
|
24
24
|
const parent = Array.from({ length: n }, (_, i) => i);
|
|
25
25
|
function find(x) {
|
package/src/completions/rank.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// within a group.mjs group. Reuses memory/blocks.mjs's rankBlocks() (PageRank) and degreeOf()
|
|
3
3
|
// (hub dampening) verbatim at sentence granularity, combined with group-scoped IDF the same
|
|
4
4
|
// way retrieveBlocks() fuses relevance/centrality/hub-dampening into one score.
|
|
5
|
-
// splitSentences() is a simple regex splitter
|
|
5
|
+
// splitSentences() is a simple regex splitter.
|
|
6
6
|
|
|
7
7
|
import { degreeOf, rankBlocks, tokenizeBlock, OVERLAP_MIN } from "../memory/blocks.mjs";
|
|
8
8
|
import { STOPWORDS } from "../prose.mjs";
|
|
@@ -15,8 +15,8 @@ function contentTokens(text) {
|
|
|
15
15
|
return tokenizeBlock(text).filter(isContentToken);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
// Sentence boundary: a run of [.!?] followed by whitespace and an uppercase letter or digit
|
|
19
|
-
//
|
|
18
|
+
// Sentence boundary: a run of [.!?] followed by whitespace and an uppercase letter or digit
|
|
19
|
+
// (no abbreviation dictionary).
|
|
20
20
|
const SENTENCE_SPLIT_RE = /(?<=[.!?])\s+(?=[A-Z0-9])/;
|
|
21
21
|
|
|
22
22
|
/**
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// `mgx:contextPassage` fact (the passage it was found in — ConceptNet's own surfaceText,
|
|
10
10
|
// or the map's `surface` template) and `mgx:coOccursWith` links to its row's other
|
|
11
11
|
// endpoint plus any already-known terms recognizable in the same passage (bounded) —
|
|
12
|
-
//
|
|
12
|
+
// closed-vocabulary co-occurrence.
|
|
13
13
|
//
|
|
14
14
|
// Wired as an opt-in `captureUnknownContext: true` on seedMemory (conceptnet.mjs),
|
|
15
15
|
// dynamically imported to avoid a load-time cycle with conceptnet.mjs.
|
package/src/finish.mjs
CHANGED
|
@@ -251,7 +251,7 @@ function ruleArticle(segments, rule) {
|
|
|
251
251
|
return out;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
// Rule 2 — subject–verb agreement. STRUCTURE-DRIVEN
|
|
254
|
+
// Rule 2 — subject–verb agreement. STRUCTURE-DRIVEN:
|
|
255
255
|
// (i) an existential "there is/are/was/were" agrees with the FOLLOWING number
|
|
256
256
|
// span's value (the number is genuinely the subject there);
|
|
257
257
|
// (ii) any listed copula agrees with a following span that carries an explicit
|
package/src/grammar/ace.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// grammar/ace.mjs — tmct's deterministic ACE-OWL sub-fragment parser.
|
|
2
|
-
// Implements the
|
|
2
|
+
// Implements the 9 controlled-English sentence patterns of
|
|
3
3
|
// docs/references/schemas/ace-owl-fragment.md and nothing more: fitting the
|
|
4
4
|
// grammar is a strong signal, missing it is a FEATURE — parseAce returns null
|
|
5
5
|
// (or an empty-triples result carrying the unknown words as `residue`) and the
|
|
@@ -44,11 +44,13 @@ export const PATTERN_CARDINALITY = "cardinality";
|
|
|
44
44
|
export const PATTERN_DISJOINT_WITH = "disjointWith";
|
|
45
45
|
export const PATTERN_POSSESSIVE = "possessive";
|
|
46
46
|
export const PATTERN_ADJECTIVE = "adjective";
|
|
47
|
+
export const PATTERN_CAPABILITY = "capability";
|
|
47
48
|
|
|
48
49
|
/** The pattern field's full domain, in the README's table order. */
|
|
49
50
|
export const PATTERNS = Object.freeze([
|
|
50
51
|
PATTERN_SUB_CLASS_OF, PATTERN_TYPE_ASSERTION, PATTERN_RELATION, PATTERN_SOME_VALUES_FROM,
|
|
51
52
|
PATTERN_CARDINALITY, PATTERN_DISJOINT_WITH, PATTERN_POSSESSIVE, PATTERN_ADJECTIVE,
|
|
53
|
+
PATTERN_CAPABILITY,
|
|
52
54
|
]);
|
|
53
55
|
|
|
54
56
|
const DET = new Set(["a", "an", "the"]);
|
|
@@ -413,7 +415,7 @@ function parseCopula(lexicon, toks, lower, isIdx) {
|
|
|
413
415
|
]);
|
|
414
416
|
}
|
|
415
417
|
|
|
416
|
-
/** Parse one sentence against the
|
|
418
|
+
/** Parse one sentence against the 9-pattern ACE-OWL sub-fragment. See the file
|
|
417
419
|
* header for the result contract; `lexicon` defaults to the committed core
|
|
418
420
|
* under the library's own neutral DEFAULT_NS ("ex:") when the caller doesn't
|
|
419
421
|
* supply one. */
|
|
@@ -429,5 +431,32 @@ export function parseAce(sentence, lexicon = loadLexicon()) {
|
|
|
429
431
|
}
|
|
430
432
|
const isIdx = lower.indexOf("is");
|
|
431
433
|
if (isIdx > 0) return parseCopula(lexicon, toks, lower, isIdx);
|
|
434
|
+
const canIdx = lower.indexOf("can");
|
|
435
|
+
if (canIdx > 0 && canIdx < toks.length - 1) {
|
|
436
|
+
const cap = parseCapability(lexicon, toks, canIdx);
|
|
437
|
+
if (cap) return cap;
|
|
438
|
+
}
|
|
432
439
|
return parseRelation(lexicon, toks, lower);
|
|
433
440
|
}
|
|
441
|
+
|
|
442
|
+
/** Pattern 9 — "N can VERB" → mgx:capableOf. The modal is not a relation
|
|
443
|
+
* verb: without this, parseRelation reads "can" through lookupVerb and
|
|
444
|
+
* asserts a generic object property ("dog cans swim") that no capability
|
|
445
|
+
* reader ever finds. Returns null (never a miss record) unless BOTH sides
|
|
446
|
+
* resolve, so a noun "can" ("trash can holds garbage") still falls through
|
|
447
|
+
* to parseRelation. "cannot"/"can't" stays unparsed — the fact vocabulary
|
|
448
|
+
* has no negative-capability predicate, and a silently dropped negation
|
|
449
|
+
* would invert the taught meaning. */
|
|
450
|
+
function parseCapability(lexicon, toks, canIdx) {
|
|
451
|
+
const np1 = resolveNP(lexicon, toks.slice(0, canIdx));
|
|
452
|
+
if (np1.term == null) return null;
|
|
453
|
+
// The capability's object is a VERB ("swim"), not a lexicon noun, so
|
|
454
|
+
// resolveNP is the wrong resolver for it: accept exactly one bare word,
|
|
455
|
+
// stored as a plain term — the same grain the corpus's own CapableOf
|
|
456
|
+
// objects ("bark", "run") already use.
|
|
457
|
+
const rest = toks.slice(canIdx + 1);
|
|
458
|
+
if (rest.length !== 1 || !/^[a-z][a-z-]*$/i.test(rest[0])) return null;
|
|
459
|
+
return hit(PATTERN_CAPABILITY, [np1], [
|
|
460
|
+
{ subject: np1.term, predicate: "mgx:capableOf", object: `${lexicon.ns}${rest[0].toLowerCase()}`, kind: "mgx:capableOf" },
|
|
461
|
+
]);
|
|
462
|
+
}
|
package/src/grammar/lexicon.mjs
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// Namespace: every lexicon carries a `.ns` field (the CURIE prefix ace.mjs
|
|
15
15
|
// stamps onto every term it mints) — always "tmct:" here (DEFAULT_NS).
|
|
16
16
|
//
|
|
17
|
-
// Morphology is
|
|
17
|
+
// Morphology is deterministic (no NLP dependency): a
|
|
18
18
|
// suffix-fold for plurals/3rd-person-singular ("repositories"→repository,
|
|
19
19
|
// "relies"→rely, "classes"→class, "uses"→use) plus an optional declared
|
|
20
20
|
// irregular `plural` ("indices"). Anything the fold can't reach is simply not
|
package/src/init.mjs
CHANGED
|
@@ -47,8 +47,7 @@ export function defaultConfig() {
|
|
|
47
47
|
/** `tmct init --with-persona <name>` presets: a named bundle of `extensions`/`bias`
|
|
48
48
|
* overrides written into tmct.toml. `human` makes the implicit default explicit; `code`
|
|
49
49
|
* re-activates the software-domain `seon`+`conceptnet` bundles; `empty` deactivates
|
|
50
|
-
* `human`, leaving a repo genuinely empty of corpus facts.
|
|
51
|
-
* the persona seam, not a curated library of presets. */
|
|
50
|
+
* `human`, leaving a repo genuinely empty of corpus facts. */
|
|
52
51
|
export const PERSONA_PRESETS = Object.freeze({
|
|
53
52
|
human: { extensions: {}, bias: { human: 1.0 } },
|
|
54
53
|
code: { extensions: { seon: { active: true }, conceptnet: { active: true } }, bias: { seon: 1.0, conceptnet: 1.0 } },
|
|
@@ -239,7 +239,7 @@ export function applySubordinationFrames(text) {
|
|
|
239
239
|
* delimiter are both required — an ordinary em-dash aside is common prose,
|
|
240
240
|
* not a restart, and treating every dash as a delimiter would be a guess.
|
|
241
241
|
* The trailing delimiter also means an object-only restart with no comma
|
|
242
|
-
* after "i mean" isn't rescued here
|
|
242
|
+
* after "i mean" isn't rescued here. */
|
|
243
243
|
const SELF_CORRECTION_RE =
|
|
244
244
|
/^.+?(?:\s*(?:--|—|-)\s*)?\b(?:sorry|i\s+mean)\b\s*(?:--|—|-|,|:)\s*(.+)$/i;
|
|
245
245
|
|
package/src/router/guardrail.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// src/router/guardrail.mjs — the guardrail. Validate an
|
|
2
|
-
// EXTERNALLY-proposed `tool_use` against the registry's declared preconditions
|
|
3
|
-
// DEFAULT-DENY anything outside the declared, registered envelope.
|
|
2
|
+
// EXTERNALLY-proposed `tool_use` against the registry's declared preconditions.
|
|
4
3
|
//
|
|
5
4
|
// Proves RESOLVABILITY (the tool is registered/declared, args are well-formed, every
|
|
6
5
|
// `resolves(param, as)` precondition binds to a real graph entity) — NOT antecedent
|
|
@@ -14,8 +13,8 @@ import { capabilityByName, preconditionsOf, PRECOND } from "./registry.mjs";
|
|
|
14
13
|
import { hallucinationsIn } from "./call-validator.mjs";
|
|
15
14
|
|
|
16
15
|
/** The same read-only breadth-first enrichment as resolver.mjs's `dispatchEachCandidate`:
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* dispatching the SAME tool once per tied candidate is safe for `readOnly` capabilities
|
|
17
|
+
* (dispatch performs no writes). Returns `[{candidate, result}, ...]`, or
|
|
19
18
|
* undefined when there is no dispatcher to run it with. */
|
|
20
19
|
async function dispatchEachCandidate(pool, capName, arg, ctx) {
|
|
21
20
|
if (!ctx.dispatch) return undefined;
|
package/src/router/registry.mjs
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Each tmct tool is modelled as a STRIPS/PDDL operator declared as DATA: a `Capability` with
|
|
4
4
|
// typed `Parameter`s, `Precondition`s, and `Effect`s (add-list/delete-list). Preconditions are
|
|
5
|
-
// the safety gate guardrail.mjs checks before a call fires;
|
|
6
|
-
//
|
|
7
|
-
// from a goal `(knows <topic> ?x)` to the capability whose add-list achieves it.
|
|
5
|
+
// the safety gate guardrail.mjs checks before a call fires; resolver.mjs backward-chains from
|
|
6
|
+
// a goal to a capability whose add-list achieves it.
|
|
8
7
|
//
|
|
9
|
-
//
|
|
8
|
+
// Plain data + pure accessors, no I/O. Tool names + parameter arg keys are the
|
|
10
9
|
// exact ones src/server.mjs `dispatchTool` reads, so a bound call this registry validates is
|
|
11
10
|
// directly dispatchable.
|
|
12
11
|
|
|
@@ -45,7 +44,7 @@ export const PRECOND = Object.freeze({
|
|
|
45
44
|
|
|
46
45
|
// ---- capability builder (returns PLAIN FROZEN data) -------------------------
|
|
47
46
|
|
|
48
|
-
/** A parameter slot. `arg` is the
|
|
47
|
+
/** A parameter slot. `arg` is the exact key src/server.mjs `dispatchTool` reads. */
|
|
49
48
|
const param = (name, kind, { arg = name, required = true, note = "" } = {}) =>
|
|
50
49
|
Object.freeze({ type: VOCAB.Parameter, name, kind, arg, required, note });
|
|
51
50
|
|
|
@@ -58,31 +57,29 @@ const resolves = (paramName, as) =>
|
|
|
58
57
|
const anyPresent = (params) =>
|
|
59
58
|
Object.freeze({ type: VOCAB.Precondition, pred: PRECOND.anyPresent, params: Object.freeze([...params]) });
|
|
60
59
|
|
|
61
|
-
/**
|
|
60
|
+
/** Add-effect: after the call the agent knows `topic` about `?of`. */
|
|
62
61
|
const knows = (topic, ofParam = null) =>
|
|
63
62
|
Object.freeze({ type: VOCAB.Effect, pred: "cap:knows", topic, of: ofParam ? `?${ofParam}` : null });
|
|
64
63
|
|
|
65
|
-
/** Declare one capability as
|
|
66
|
-
* empty delete-list (`del: []`) — the closed-world "queries mutate nothing". */
|
|
64
|
+
/** Declare one capability as STRIPS data. */
|
|
67
65
|
function capability({ name, label, question, params = [], preconditions = [], add = [], del = [] }) {
|
|
68
66
|
return Object.freeze({
|
|
69
67
|
type: VOCAB.Capability,
|
|
70
68
|
name, // the dispatchTool tool name — directly callable
|
|
71
69
|
label, // human label (the slash-command verb)
|
|
72
70
|
question, // one-line "what question does this answer"
|
|
73
|
-
readOnly: true, //
|
|
71
|
+
readOnly: true, // dispatching this capability performs no writes
|
|
74
72
|
parameters: Object.freeze(params),
|
|
75
73
|
preconditions: Object.freeze(preconditions),
|
|
76
74
|
effects: Object.freeze({ add: Object.freeze(add), del: Object.freeze(del) }),
|
|
77
75
|
});
|
|
78
76
|
}
|
|
79
77
|
|
|
80
|
-
// ---- the
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
// takes an optional `package`; untested takes nothing.
|
|
78
|
+
// ---- the declared capabilities -----------------------------------------------
|
|
79
|
+
// Arg keys verified against src/server.mjs `dispatchTool`'s switch: describe/callers/
|
|
80
|
+
// callees/tests/history/… take `symbol`; impact/exports take `module`; members/
|
|
81
|
+
// subclasses take `class`; search takes `query` (+ optional kind/name/decorator);
|
|
82
|
+
// architecture takes an optional `package`; untested takes nothing.
|
|
86
83
|
|
|
87
84
|
const CAPABILITIES = Object.freeze([
|
|
88
85
|
capability({
|
|
@@ -187,12 +184,8 @@ const BY_NAME = Object.freeze(
|
|
|
187
184
|
CAPABILITIES.reduce((m, c) => { m[c.name] = c; return m; }, Object.create(null)),
|
|
188
185
|
);
|
|
189
186
|
|
|
190
|
-
// ----
|
|
191
|
-
//
|
|
192
|
-
// not registered here is treated as unknown/hallucinated, never dispatchable.
|
|
193
|
-
//
|
|
194
|
-
// The following dispatch tools are INTENTIONALLY UNREGISTERED — they emit unbounded raw
|
|
195
|
-
// output, the most hallucination-prone surface, not a clean bounded-epistemic-effect query.
|
|
187
|
+
// ---- unregistered dispatch tools ---------------------------------------------
|
|
188
|
+
// Dispatch tools not yet registered; each names the precondition work it needs first.
|
|
196
189
|
export const EXCLUDED_FROM_REGISTRY = Object.freeze({
|
|
197
190
|
tmct_context: "unbounded edit-context bundle (multi-file); needs a size/budget precondition",
|
|
198
191
|
tmct_context_more: "unbounded context continuation; same as tmct_context",
|
package/src/router/resolver.mjs
CHANGED
|
@@ -45,7 +45,7 @@ export const NL_INTENTS = Object.freeze({
|
|
|
45
45
|
"reverse:cochange": { topic: "cochanges", arg: "symbol" },
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
// ---- ask-vocab RELATION kinds with NO capability
|
|
48
|
+
// ---- ask-vocab RELATION kinds with NO capability ------------------------------
|
|
49
49
|
// Every ask-vocab.mjs RELATIONS key must be either mapped (above) or listed here with a
|
|
50
50
|
// reason (the conformance test enforces the partition). Refuse rather than mis-route.
|
|
51
51
|
export const UNMAPPED_KINDS = Object.freeze({
|
package/src/source-slice.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Shared, safe source-span slicing for the tool layer (src/server.mjs) and the
|
|
2
2
|
// source-capable Repository Interface provider (src/providers/graph-service.mjs).
|
|
3
3
|
//
|
|
4
|
-
// Two halves
|
|
4
|
+
// Two halves:
|
|
5
5
|
// - sliceSpan — PURE. Given an in-memory `lines` array, extracts + line-numbers
|
|
6
6
|
// one span. No fs, no path logic.
|
|
7
7
|
// - readSpanSafe — the fs-touching half. Resolves `join(repoRoot, path)` with Node's
|