@polycode-projects/the-mechanical-code-talker 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +142 -8
- package/ROADMAP.md +35 -0
- package/bin/tmct.mjs +128 -18
- package/package.json +1 -1
- package/src/chat.mjs +119 -72
- package/src/codegraph.mjs +73 -4
- package/src/config.mjs +7 -2
- package/src/conformance.mjs +59 -15
- package/src/corpus/templates.mjs +38 -0
- package/src/extensions.mjs +348 -0
- package/src/init.mjs +92 -7
- package/src/memory/bias.mjs +77 -0
- package/src/memory/blocks.mjs +57 -18
- package/src/memory/core.mjs +237 -20
- package/src/memory/fold.mjs +0 -0
- package/src/memory/trust.mjs +94 -6
- package/src/providers/bootstrap.mjs +5 -3
- package/src/providers/fixture.mjs +7 -3
- package/src/providers/graph-service.mjs +205 -28
- package/src/repository-interface.mjs +21 -7
- package/src/server.mjs +39 -29
- package/src/source-slice.mjs +68 -0
- package/src/telemetry.mjs +5 -2
- package/src/toml-config.mjs +17 -0
package/src/chat.mjs
CHANGED
|
@@ -51,6 +51,8 @@ import { uuidv7 } from "./uuid.mjs";
|
|
|
51
51
|
import { createTelemetry } from "./telemetry.mjs";
|
|
52
52
|
import * as defaultSource from "./source.mjs";
|
|
53
53
|
import { loadTemplates, render as renderTemplate } from "./corpus/templates.mjs";
|
|
54
|
+
import { resolveExtensions, seedActiveCorpusEntries, mergedLexiconExtra } from "./extensions.mjs";
|
|
55
|
+
import { rankByBiasThenTrust } from "./memory/bias.mjs";
|
|
54
56
|
import { finish, beginsWithVowelSound, grammarRules } from "./finish.mjs";
|
|
55
57
|
import {
|
|
56
58
|
VERB_TO_KIND, WHERE_MARKERS, MENTION_MARKERS, ENTITY_TO_TYPE, PASSIVE_PARTICIPLE_TO_KIND,
|
|
@@ -565,7 +567,7 @@ export function answerCount(graph, query) {
|
|
|
565
567
|
* Consulted only when answerCount can't map the noun to a graph class (an unknown
|
|
566
568
|
* kind) AND a session's memory is in hand. Returns the count string or null (no
|
|
567
569
|
* such fact → the honest "I can't count …" from answerCount stands). */
|
|
568
|
-
async function countFromFacts(graph, memoryDir, query) {
|
|
570
|
+
async function countFromFacts(graph, memoryDir, query, biasByBundle = {}) {
|
|
569
571
|
if (!graph || !memoryDir) return null;
|
|
570
572
|
const m = String(query).match(/\b(?:how many|number of|count(?:\s+the)?)\s+([a-z]+)\b/i);
|
|
571
573
|
if (!m) return null;
|
|
@@ -576,8 +578,10 @@ async function countFromFacts(graph, memoryDir, query) {
|
|
|
576
578
|
const objVariants = factTermVariants(normFactTerm, asked);
|
|
577
579
|
const isa = (await factRows(memoryDir))
|
|
578
580
|
.filter((f) => ISA_PREDICATES.has(f.predicate) && objVariants.has(f.object));
|
|
579
|
-
// pick the highest-trust asserted subject that maps to a
|
|
580
|
-
|
|
581
|
+
// pick the highest-bias, then highest-trust asserted subject that maps to a
|
|
582
|
+
// countable graph class (rankByBiasThenTrust: bias-tied/unconfigured degrades
|
|
583
|
+
// to the same trust-desc scan this always ran).
|
|
584
|
+
for (const f of rankByBiasThenTrust(isa, biasByBundle)) {
|
|
581
585
|
const cls = COUNT_NOUNS[String(f.subject).toLowerCase()];
|
|
582
586
|
if (cls) { const n = countClass(graph, cls); return `${n} ${asked}.`; }
|
|
583
587
|
}
|
|
@@ -607,7 +611,7 @@ async function countFromFacts(graph, memoryDir, query) {
|
|
|
607
611
|
// graph-cardinality count untouched — same honest-decline discipline as
|
|
608
612
|
// every other lane here).
|
|
609
613
|
const HOW_MANY_ARE_RE = /^how\s+many\s+([\w-]+)\s+(?:are|is)\s+(.+?)[?.!\s]*$/i;
|
|
610
|
-
async function answerQuantifierRecall(memoryDir, query) {
|
|
614
|
+
async function answerQuantifierRecall(memoryDir, query, biasByBundle = {}) {
|
|
611
615
|
if (!memoryDir) return null;
|
|
612
616
|
const m = String(query).trim().match(HOW_MANY_ARE_RE);
|
|
613
617
|
if (!m) return null;
|
|
@@ -619,7 +623,7 @@ async function answerQuantifierRecall(memoryDir, query) {
|
|
|
619
623
|
const rows = (await factRows(memoryDir)).filter((f) => ISA_PREDICATES.has(f.predicate) && subjVariants.has(f.subject));
|
|
620
624
|
if (!rows.length) return null; // never heard of this subject at all — let answerCount own the shape
|
|
621
625
|
const objVariants = factTermVariants(normFactTerm, m[2]);
|
|
622
|
-
const hit = rows.filter((f) => objVariants.has(f.object))
|
|
626
|
+
const hit = rankByBiasThenTrust(rows.filter((f) => objVariants.has(f.object)), biasByBundle)[0];
|
|
623
627
|
const q = hit?.quantifier;
|
|
624
628
|
if (!q) return "I don't know — I was never told a quantifier for that.";
|
|
625
629
|
return `${q.charAt(0).toUpperCase()}${q.slice(1)}.`;
|
|
@@ -3491,7 +3495,7 @@ const FACT_ANSWER_CAP = 32;
|
|
|
3491
3495
|
* graph's Facts. Returns { text, replace } — `replace:false` means the engine's
|
|
3492
3496
|
* own (schema-docs) answer stands and the fact lines are appended under it —
|
|
3493
3497
|
* or null when memory holds nothing relevant (misses stay unchanged). */
|
|
3494
|
-
async function factAnswer(memoryDir, query, envelope, miss) {
|
|
3498
|
+
async function factAnswer(memoryDir, query, envelope, miss, biasByBundle = {}) {
|
|
3495
3499
|
let normFactTerm;
|
|
3496
3500
|
try { ({ normFactTerm } = await import("./memory/core.mjs")); } catch { return null; }
|
|
3497
3501
|
const q = String(query).trim();
|
|
@@ -3523,8 +3527,11 @@ async function factAnswer(memoryDir, query, envelope, miss) {
|
|
|
3523
3527
|
// relation about the subject undifferentiated.
|
|
3524
3528
|
const { subject, predicate } = splitMetaPredicate(metaTerm);
|
|
3525
3529
|
const variants = factTermVariants(normFactTerm, subject);
|
|
3526
|
-
|
|
3527
|
-
|
|
3530
|
+
// factRows (trust+sourceIds-bearing), not the plain memoryFacts shape — the
|
|
3531
|
+
// bias-weighted ranking below needs each hit's sourceIds to resolve which
|
|
3532
|
+
// bundle it came from (memory/bias.mjs's biasForRow).
|
|
3533
|
+
const subjectHits = (await factRows(memoryDir)).filter((f) => variants.has(f.subject));
|
|
3534
|
+
let hits = predicate ? subjectHits.filter((f) => f.predicate === predicate) : subjectHits;
|
|
3528
3535
|
if (!hits.length) {
|
|
3529
3536
|
// The subject itself is known, but not under this specific relation —
|
|
3530
3537
|
// an honest, specific "no" rather than falling through to the generic
|
|
@@ -3538,6 +3545,10 @@ async function factAnswer(memoryDir, query, envelope, miss) {
|
|
|
3538
3545
|
}
|
|
3539
3546
|
return null;
|
|
3540
3547
|
}
|
|
3548
|
+
// Bias only REORDERS — every hit still renders and is cited (Part 6's
|
|
3549
|
+
// "disclosed, never dropped" contract). Unconfigured/tied bias degrades to
|
|
3550
|
+
// trust-desc, byte-identical to before this feature existed.
|
|
3551
|
+
hits = rankByBiasThenTrust(hits, biasByBundle);
|
|
3541
3552
|
const lines = hits.map(renderFactLine);
|
|
3542
3553
|
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
3543
3554
|
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
@@ -3563,7 +3574,7 @@ async function factAnswer(memoryDir, query, envelope, miss) {
|
|
|
3563
3574
|
const know = q.match(KNOW_ABOUT_RE);
|
|
3564
3575
|
if (know) {
|
|
3565
3576
|
const variants = factTermVariants(normFactTerm, know[1]);
|
|
3566
|
-
const rows = await
|
|
3577
|
+
const rows = await factRows(memoryDir);
|
|
3567
3578
|
// Bug E subtype walk (operator follow-up request, this session): a
|
|
3568
3579
|
// cycle-safe BFS DOWNWARD over isa-family facts from the term's own
|
|
3569
3580
|
// variants — every fact whose OBJECT is in the current frontier
|
|
@@ -3585,11 +3596,11 @@ async function factAnswer(memoryDir, query, envelope, miss) {
|
|
|
3585
3596
|
// (the ORIGINAL, non-subtype half of the filter below) still include
|
|
3586
3597
|
// corpus facts exactly as before — only the SUBTYPE DISCOVERY chain is
|
|
3587
3598
|
// taught-only.
|
|
3588
|
-
// `rows` here is
|
|
3589
|
-
//
|
|
3590
|
-
//
|
|
3591
|
-
// convention renderFactLine
|
|
3592
|
-
// on, just above.
|
|
3599
|
+
// `rows` here is factRows()'s trust+sourceIds-bearing shape (Part 6: the
|
|
3600
|
+
// bias-weighted ranking below needs sourceIds) — it still carries the SAME
|
|
3601
|
+
// `provenance` legacy-compat string the taught/corpus distinction below
|
|
3602
|
+
// reads, the SAME convention renderFactLine keys its own corpus-vs-taught
|
|
3603
|
+
// framing on, just above.
|
|
3593
3604
|
const isTaughtFact = (f) => !String(f.provenance || "").includes("corpus:") && !String(f.provenance || "").includes("web:");
|
|
3594
3605
|
const isaRows = rows.filter((f) => ISA_PREDICATES.has(f.predicate) && isTaughtFact(f));
|
|
3595
3606
|
const subtypeSubjects = new Set();
|
|
@@ -3643,6 +3654,10 @@ async function factAnswer(memoryDir, query, envelope, miss) {
|
|
|
3643
3654
|
// match wouldn't have found, say so — lets the reader tell subtype-derived
|
|
3644
3655
|
// facts apart from literal mentions.
|
|
3645
3656
|
const viaSubtype = hits.some((f) => subtypeSubjects.has(f.subject) && !variants.has(f.subject) && !variants.has(f.object));
|
|
3657
|
+
// Bias only REORDERS, right before render/cite — every hit above still
|
|
3658
|
+
// renders (Part 6's "disclosed, never dropped" contract); literalHit/
|
|
3659
|
+
// viaSubtype above already resolved off the pre-rank order.
|
|
3660
|
+
hits = rankByBiasThenTrust(hits, biasByBundle);
|
|
3646
3661
|
const lines = hits.map((f) => ` ${renderFactLine(f)}`);
|
|
3647
3662
|
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
3648
3663
|
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
@@ -3879,7 +3894,7 @@ function inheritsChain(graph, startId) {
|
|
|
3879
3894
|
* "what kind of thing is an X" reports X's own type (subject-side first).
|
|
3880
3895
|
* Miss-only and run AFTER factAnswer returns null, so it never shadows the
|
|
3881
3896
|
* subject-side answer or a schema hit. Returns { text, replace:true } or null. */
|
|
3882
|
-
async function factReadBack(memoryDir, query, envelope, miss, graph = null, focusLabel = null) {
|
|
3897
|
+
async function factReadBack(memoryDir, query, envelope, miss, graph = null, focusLabel = null, biasByBundle = {}) {
|
|
3883
3898
|
if (!miss) return null;
|
|
3884
3899
|
let normFactTerm;
|
|
3885
3900
|
try { ({ normFactTerm } = await import("./memory/core.mjs")); } catch { return null; }
|
|
@@ -3970,7 +3985,10 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
3970
3985
|
// "what facts do you know": no term to bind, so list every remembered fact,
|
|
3971
3986
|
// higher-trust first, each cited. Answers the cross-session assert-recall surfaces.
|
|
3972
3987
|
if (WHOLE_RECALL_RE.test(q)) {
|
|
3973
|
-
|
|
3988
|
+
// Part 6: bias-then-trust — every hit still renders (renderMany caps for
|
|
3989
|
+
// display, never drops), just reordered; unconfigured bias degrades to the
|
|
3990
|
+
// same trust-desc order this always used.
|
|
3991
|
+
const hits = rankByBiasThenTrust(isa.length ? isa : rows, biasByBundle);
|
|
3974
3992
|
if (!hits.length) return null;
|
|
3975
3993
|
return renderMany(hits);
|
|
3976
3994
|
}
|
|
@@ -4699,7 +4717,7 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
4699
4717
|
if (subject) {
|
|
4700
4718
|
const predicate = await generalVerbPredicate(verb);
|
|
4701
4719
|
const subjVariants = factTermVariants(normFactTerm, subject);
|
|
4702
|
-
const hits = rows.filter((f) => f.predicate === predicate && subjVariants.has(f.subject))
|
|
4720
|
+
const hits = rankByBiasThenTrust(rows.filter((f) => f.predicate === predicate && subjVariants.has(f.subject)), biasByBundle);
|
|
4703
4721
|
if (hits.length) return { ...renderMany(hits), generalVerbQuery: true };
|
|
4704
4722
|
}
|
|
4705
4723
|
}
|
|
@@ -4709,7 +4727,7 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
4709
4727
|
const told = q.match(TOLD_ABOUT_RE);
|
|
4710
4728
|
if (told) {
|
|
4711
4729
|
const variants = factTermVariants(normFactTerm, told[1]);
|
|
4712
|
-
const hits = rows.filter((f) => variants.has(f.subject) || variants.has(f.object))
|
|
4730
|
+
const hits = rankByBiasThenTrust(rows.filter((f) => variants.has(f.subject) || variants.has(f.object)), biasByBundle);
|
|
4713
4731
|
if (!hits.length) return null;
|
|
4714
4732
|
const term = variants.has(hits[0].subject) ? hits[0].subject : hits[0].object;
|
|
4715
4733
|
const lines = hits.map((f) => ` ${renderFactLine(f)}`);
|
|
@@ -4735,8 +4753,8 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
4735
4753
|
}
|
|
4736
4754
|
if (!term) return null;
|
|
4737
4755
|
const variants = factTermVariants(normFactTerm, term);
|
|
4738
|
-
const subjectHits = isa.filter((f) => variants.has(f.subject))
|
|
4739
|
-
const objectHits = isa.filter((f) => variants.has(f.object))
|
|
4756
|
+
const subjectHits = rankByBiasThenTrust(isa.filter((f) => variants.has(f.subject)), biasByBundle);
|
|
4757
|
+
const objectHits = rankByBiasThenTrust(isa.filter((f) => variants.has(f.object)), biasByBundle);
|
|
4740
4758
|
const hits = kindOf
|
|
4741
4759
|
? (subjectHits.length ? subjectHits : objectHits)
|
|
4742
4760
|
: (objectHits.length ? objectHits : subjectHits);
|
|
@@ -4756,13 +4774,13 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
4756
4774
|
* only (a `/describe` names ONE code entity as the subject of its own facts,
|
|
4757
4775
|
* not every fact that merely mentions it in passing) — null when memory holds
|
|
4758
4776
|
* nothing about this subject. */
|
|
4759
|
-
async function describedFacts(memoryDir, label) {
|
|
4777
|
+
async function describedFacts(memoryDir, label, biasByBundle = {}) {
|
|
4760
4778
|
let normFactTerm;
|
|
4761
4779
|
try { ({ normFactTerm } = await import("./memory/core.mjs")); } catch { return null; }
|
|
4762
4780
|
const rows = await factRows(memoryDir);
|
|
4763
4781
|
if (!rows.length) return null;
|
|
4764
4782
|
const variants = factTermVariants(normFactTerm, label);
|
|
4765
|
-
const hits = rows.filter((f) => variants.has(f.subject))
|
|
4783
|
+
const hits = rankByBiasThenTrust(rows.filter((f) => variants.has(f.subject)), biasByBundle);
|
|
4766
4784
|
if (!hits.length) return null;
|
|
4767
4785
|
return `taught facts:\n${hits.map((f) => ` ${renderFactLine(f)}`).join("\n")}`;
|
|
4768
4786
|
}
|
|
@@ -5347,7 +5365,7 @@ async function describeGrainRescue(graph, term) {
|
|
|
5347
5365
|
return null;
|
|
5348
5366
|
}
|
|
5349
5367
|
|
|
5350
|
-
async function describeWrapperAnswer(query, { config, source, focus, graph }) {
|
|
5368
|
+
async function describeWrapperAnswer(query, { config, source, focus, graph, tel = null }) {
|
|
5351
5369
|
// Tier 6 playtest: this lane is the LAST-RESORT rescue (4d, tried after every
|
|
5352
5370
|
// earlier lane declines on the ORIGINAL query) — but it tested its own
|
|
5353
5371
|
// DESCRIBE_WRAPPER_RE against the RAW, un-normalized text, so a preamble an
|
|
@@ -5382,7 +5400,7 @@ async function describeWrapperAnswer(query, { config, source, focus, graph }) {
|
|
|
5382
5400
|
}
|
|
5383
5401
|
}
|
|
5384
5402
|
try {
|
|
5385
|
-
const text = await dispatchTool("tmct_describe", { symbol: term }, { config, source });
|
|
5403
|
+
const text = await dispatchTool("tmct_describe", { symbol: term }, { config, source, tel });
|
|
5386
5404
|
return text ? { text } : null;
|
|
5387
5405
|
} catch {
|
|
5388
5406
|
return null; // unresolvable term — decline, the ordinary wall stands unchanged
|
|
@@ -5484,7 +5502,7 @@ async function conceptForceAnswer(query, envelope, { graph, config, source, memo
|
|
|
5484
5502
|
* otherwise the unchanged dispatchTool path (which also yields the no-graph error).
|
|
5485
5503
|
* A hit updates the focus to the resolved object. Grammar miss / ToolError → a
|
|
5486
5504
|
* normal answer, never a crash. */
|
|
5487
|
-
async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env, trace, vocabHint = null }) {
|
|
5505
|
+
async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env, trace, vocabHint = null, tel = null, biasByBundle = {} }) {
|
|
5488
5506
|
const ts = new Date().toISOString();
|
|
5489
5507
|
// DISCOURSE ANAPHORA (CHATBENCH_006 levers 1+2): a follow-up like "which of those
|
|
5490
5508
|
// are tested" / "how many of those" / "count them" filters or counts the PREVIOUS
|
|
@@ -5547,7 +5565,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
5547
5565
|
const r = ask(graph, askQuery, { contextId: focus?.id ?? null, prev });
|
|
5548
5566
|
text = `${r.content}${ASK_ENVELOPE_DELIM}${JSON.stringify(r.tmct_ask, null, 2)}`;
|
|
5549
5567
|
} else {
|
|
5550
|
-
text = await dispatchTool("tmct_ask", { query: askQuery }, { config, source });
|
|
5568
|
+
text = await dispatchTool("tmct_ask", { query: askQuery }, { config, source, tel });
|
|
5551
5569
|
}
|
|
5552
5570
|
const [content, envJson] = text.split(ASK_ENVELOPE_DELIM);
|
|
5553
5571
|
answer = content;
|
|
@@ -5852,8 +5870,8 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
5852
5870
|
const isAdjectiveShape = IS_ADJECTIVE_YESNO_RE.test(String(query).trim());
|
|
5853
5871
|
let bareMetaHit = null;
|
|
5854
5872
|
if (isConversationalCandidate && memoryDir && (bareWhatisShape || isAdjectiveShape)) {
|
|
5855
|
-
bareMetaHit = (await factAnswer(memoryDir, query, envelope, miss))
|
|
5856
|
-
?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label));
|
|
5873
|
+
bareMetaHit = (await factAnswer(memoryDir, query, envelope, miss, biasByBundle))
|
|
5874
|
+
?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label, biasByBundle));
|
|
5857
5875
|
}
|
|
5858
5876
|
if (bareMetaHit) {
|
|
5859
5877
|
answer = bareMetaHit.replace ? bareMetaHit.text : `${answer}\n${bareMetaHit.text}`;
|
|
@@ -5894,8 +5912,8 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
5894
5912
|
// reified fact is stronger evidence than a transcript echo. Subject-side facts
|
|
5895
5913
|
// first (factAnswer), then the reverse-membership read-back (factReadBack) so an
|
|
5896
5914
|
// asserted "every X is a Y" answers "what is a Y" too.
|
|
5897
|
-
const fact = (await factAnswer(memoryDir, query, envelope, miss))
|
|
5898
|
-
?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label));
|
|
5915
|
+
const fact = (await factAnswer(memoryDir, query, envelope, miss, biasByBundle))
|
|
5916
|
+
?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label, biasByBundle));
|
|
5899
5917
|
if (fact) {
|
|
5900
5918
|
answer = fact.replace ? fact.text : `${answer}\n${fact.text}`;
|
|
5901
5919
|
via = "fact";
|
|
@@ -6097,7 +6115,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
6097
6115
|
// for what would otherwise become the generic wall, never a competing route:
|
|
6098
6116
|
// it only claims the turn if /describe actually resolves the captured term.
|
|
6099
6117
|
if (miss && recordMiss && via === "composed") {
|
|
6100
|
-
const described = await describeWrapperAnswer(query, { config, source, focus: newFocus, graph });
|
|
6118
|
+
const described = await describeWrapperAnswer(query, { config, source, focus: newFocus, graph, tel });
|
|
6101
6119
|
if (described) {
|
|
6102
6120
|
answer = described.text; via = "describe"; recordMiss = false;
|
|
6103
6121
|
note(trace, "lane: (4d) DESCRIBE-WRAPPER RESCUE — a polite wrapper around \"describe/tell me about <symbol>\" resolved via /describe, tried last after every other lane declined");
|
|
@@ -6298,7 +6316,7 @@ const GOAL_BY_COMMAND = {
|
|
|
6298
6316
|
* field now (Bug F point 5) — mirrors runAsk's own `goal` field so
|
|
6299
6317
|
* withGoalLine's short "Goal (inferred): …" line fires for command
|
|
6300
6318
|
* dispatches too, not just ask()-parsed queries. */
|
|
6301
|
-
async function runCommand(line, { config, source, graph, focus, memoryDir, trace, narrate = false }) {
|
|
6319
|
+
async function runCommand(line, { config, source, graph, focus, memoryDir, trace, narrate = false, tel = null, biasByBundle = {} }) {
|
|
6302
6320
|
const ts = new Date().toISOString();
|
|
6303
6321
|
const sp = line.indexOf(" ");
|
|
6304
6322
|
const name = (sp === -1 ? line.slice(1) : line.slice(1, sp)).toLowerCase();
|
|
@@ -6390,7 +6408,7 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
6390
6408
|
|
|
6391
6409
|
let answer;
|
|
6392
6410
|
try {
|
|
6393
|
-
answer = await dispatchTool(spec.tool, spec.arg ? { [spec.arg]: value } : {}, { config, source });
|
|
6411
|
+
answer = await dispatchTool(spec.tool, spec.arg ? { [spec.arg]: value } : {}, { config, source, tel });
|
|
6394
6412
|
} catch (e) {
|
|
6395
6413
|
note(trace, `intermediate: dispatchTool("${spec.tool}") threw — ${String(e?.message || e)}`);
|
|
6396
6414
|
return mk(String(e?.message || e), { miss: true }); // the tool's own clean error, never a stack
|
|
@@ -6409,7 +6427,7 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
6409
6427
|
// matching taught facts (subject === the resolved entity, trust-ranked)
|
|
6410
6428
|
// under the code-map answer, mirroring the ask-path's fact-append pattern.
|
|
6411
6429
|
if (name === "describe" && memoryDir) {
|
|
6412
|
-
const facts = await describedFacts(memoryDir, ent.label);
|
|
6430
|
+
const facts = await describedFacts(memoryDir, ent.label, biasByBundle);
|
|
6413
6431
|
if (facts) { answer = `${answer}\n${facts}`; note(trace, "source: memory facts (describedFacts) appended to the code-map answer"); }
|
|
6414
6432
|
}
|
|
6415
6433
|
return mk(answer, { resolvedIds: [ent.id], newFocus: nextFocus(graph, focus, ent) });
|
|
@@ -6528,7 +6546,7 @@ function morePage(query, { last, focus }) {
|
|
|
6528
6546
|
// gain.
|
|
6529
6547
|
const INDIRECT_REQUEST_RE = /^(?:i\s+(?:want|wanted)\s+you\s+to\s+|i(?:'d|\s+would)\s+like\s+you\s+to\s+)\s*(.+)$/i;
|
|
6530
6548
|
|
|
6531
|
-
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, vocabHint = null } = {}) {
|
|
6549
|
+
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, vocabHint = null, tel = null, biasByBundle = {} } = {}) {
|
|
6532
6550
|
const line = String(input ?? "").trim();
|
|
6533
6551
|
// The captured residue is used for RECOGNITION at every dispatch site below
|
|
6534
6552
|
// (asBareCommand, conversationalTurn, assertTurn, the count lanes, runAsk);
|
|
@@ -6549,7 +6567,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
6549
6567
|
// pass one gets it computed here instead, so "try this vocabulary example" is
|
|
6550
6568
|
// never wrong regardless of caller.
|
|
6551
6569
|
const resolvedVocabHint = vocabHint ?? vocabExampleHint(await hasSeededVocabulary(memoryDir));
|
|
6552
|
-
const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate, vocabHint: resolvedVocabHint };
|
|
6570
|
+
const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate, vocabHint: resolvedVocabHint, tel, biasByBundle };
|
|
6553
6571
|
// A DISPATCHED turn (count / slash-command / ask) becomes the new "last answer"
|
|
6554
6572
|
// that why/say-more re-renders; a conversational turn does not (it preserves it).
|
|
6555
6573
|
// FINISH SEAM (PLAN_RESPONSE_FINISHING §"Where it lives"): every dispatched turn's
|
|
@@ -6640,7 +6658,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
6640
6658
|
// authority gate declines (returns null) for anything answerCount should own,
|
|
6641
6659
|
// so ordinary structural counts fall through completely unaffected.
|
|
6642
6660
|
if (memoryDir) {
|
|
6643
|
-
const quantifierRecall = await answerQuantifierRecall(memoryDir, workingLine);
|
|
6661
|
+
const quantifierRecall = await answerQuantifierRecall(memoryDir, workingLine, biasByBundle);
|
|
6644
6662
|
if (quantifierRecall != null) {
|
|
6645
6663
|
note(trace, 'goal: recall a taught quantifier for a class-membership pair ("how many Xs are Ys")');
|
|
6646
6664
|
note(trace, "lane: answerQuantifierRecall — matched HOW_MANY_ARE_RE with a subject tmct has facts about; literal recall, never real counting");
|
|
@@ -6655,7 +6673,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
6655
6673
|
// ASSERTED vocabulary fact ("every class is a type" → "how many types" = the
|
|
6656
6674
|
// class count). countFromFacts declines on a real graph kind, so ordinary
|
|
6657
6675
|
// counts are unaffected; it only speaks for a remembered object noun.
|
|
6658
|
-
const viaFact = memoryDir ? await countFromFacts(graph, memoryDir, workingLine) : null;
|
|
6676
|
+
const viaFact = memoryDir ? await countFromFacts(graph, memoryDir, workingLine, biasByBundle) : null;
|
|
6659
6677
|
if (viaFact != null) {
|
|
6660
6678
|
note(trace, 'goal: get a count of an asserted-vocabulary kind ("every X is a Y" inherited cardinality)');
|
|
6661
6679
|
note(trace, "lane: countFromFacts — the counted noun matched a remembered isa-fact's SUBJECT, whose class IS countable");
|
|
@@ -6690,20 +6708,19 @@ export const SEED_PREFER = ["rdfs:subClassOf", "rdf:type", "mgx:usedFor", "mgx:p
|
|
|
6690
6708
|
* corpus seed, so re-runs skip without even reading the slice. */
|
|
6691
6709
|
export const SEED_MARKER_REL = join(".tmct", "memory", "corpus-seed.json");
|
|
6692
6710
|
|
|
6693
|
-
/** Seed the starter corpus into <repo>/.tmct/memory once
|
|
6694
|
-
*
|
|
6695
|
-
*
|
|
6696
|
-
*
|
|
6697
|
-
*
|
|
6698
|
-
*
|
|
6699
|
-
*
|
|
6700
|
-
*
|
|
6701
|
-
*
|
|
6702
|
-
*
|
|
6703
|
-
*
|
|
6704
|
-
*
|
|
6705
|
-
*
|
|
6706
|
-
* skipped/failed. */
|
|
6711
|
+
/** Seed the starter corpus into <repo>/.tmct/memory once — a loop over every
|
|
6712
|
+
* ACTIVE `corpus`-kind extension entry (src/extensions.mjs's resolveExtensions,
|
|
6713
|
+
* the SAME seam `tmct init`'s seed step and `tmct init --corpus <id>` now
|
|
6714
|
+
* share), in the resolver's FIXED order: seon first, then conceptnet, then any
|
|
6715
|
+
* other active bundle sorted by name. seon runs first so its curated facts win
|
|
6716
|
+
* the content-hash idempotency race — a term the ConceptNet slice also carries
|
|
6717
|
+
* keeps the seon provenance. Idempotent twice over (the marker short-circuits;
|
|
6718
|
+
* seedMemory content-hashes fact ids) and failure-tolerated PER BUNDLE
|
|
6719
|
+
* (seedActiveCorpusEntries): one bad third-party pack degrades to "not seeded"
|
|
6720
|
+
* for that bundle alone (its error is recorded, not silently swallowed) while
|
|
6721
|
+
* every other bundle still lands — never an error before the prompt. Returns
|
|
6722
|
+
* { appended, skipped, total, seon, conceptnet, perBundle } on a fresh seed
|
|
6723
|
+
* (the banner counts stay honest), null when skipped/failed outright. */
|
|
6707
6724
|
async function seedBootstrapMemory(repo) {
|
|
6708
6725
|
const marker = join(repo, SEED_MARKER_REL);
|
|
6709
6726
|
try {
|
|
@@ -6711,22 +6728,21 @@ async function seedBootstrapMemory(repo) {
|
|
|
6711
6728
|
return null; // already seeded — the marker is authoritative
|
|
6712
6729
|
} catch { /* no marker → first run */ }
|
|
6713
6730
|
try {
|
|
6714
|
-
const {
|
|
6715
|
-
|
|
6716
|
-
const seon = await seedMemory(repo, { slicePath: SEON_CONCEPTS_FILE, provenancePrefix: "corpus:seon" });
|
|
6717
|
-
// (2) the capped ConceptNet band — byte-identical to the prior single seed.
|
|
6718
|
-
const conceptnet = await seedMemory(repo, { limit: SEED_LIMIT, prefer: SEED_PREFER });
|
|
6731
|
+
const { entries } = await resolveExtensions(repo);
|
|
6732
|
+
const { appended, skipped, total, perBundle } = await seedActiveCorpusEntries(repo, entries);
|
|
6719
6733
|
const res = {
|
|
6720
|
-
appended
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
seon
|
|
6724
|
-
|
|
6734
|
+
appended, skipped, total, perBundle,
|
|
6735
|
+
// seon/conceptnet stay named fields (not just perBundle lookups) so the
|
|
6736
|
+
// banner's default two-bundle rendering (below) — and any external
|
|
6737
|
+
// reader keyed on `.seon`/`.conceptnet` — stays byte-identical.
|
|
6738
|
+
seon: perBundle.seon?.appended || 0,
|
|
6739
|
+
conceptnet: perBundle.conceptnet?.appended || 0,
|
|
6725
6740
|
};
|
|
6726
6741
|
await mkdir(dirname(marker), { recursive: true });
|
|
6727
6742
|
await writeFile(marker, JSON.stringify({
|
|
6728
|
-
seededAt: new Date().toISOString(),
|
|
6743
|
+
seededAt: new Date().toISOString(),
|
|
6729
6744
|
appended: res.appended, skipped: res.skipped, seon: res.seon, conceptnet: res.conceptnet,
|
|
6745
|
+
perBundle,
|
|
6730
6746
|
}) + "\n");
|
|
6731
6747
|
return res;
|
|
6732
6748
|
} catch {
|
|
@@ -6734,6 +6750,20 @@ async function seedBootstrapMemory(repo) {
|
|
|
6734
6750
|
}
|
|
6735
6751
|
}
|
|
6736
6752
|
|
|
6753
|
+
/** The seed banner line — byte-identical to before for the default zero-config
|
|
6754
|
+
* seon+conceptnet case; a THIRD (or more) active bundle appends its own
|
|
6755
|
+
* "<n> <bundle-name>" clause rather than changing the base sentence, so a
|
|
6756
|
+
* fresh `TMCT_NO_SEED`-unset run with no tmct.toml renders EXACTLY what
|
|
6757
|
+
* test/wiring-seed.test.mjs's SEED_BANNER_RE already pins. */
|
|
6758
|
+
function seedBannerLine(seeded) {
|
|
6759
|
+
const extra = Object.entries(seeded.perBundle || {})
|
|
6760
|
+
.filter(([name]) => name !== "seon" && name !== "conceptnet")
|
|
6761
|
+
.filter(([, r]) => r && r.appended > 0)
|
|
6762
|
+
.map(([name, r]) => `${r.appended} ${name}`);
|
|
6763
|
+
const extraClause = extra.length ? ` + ${extra.join(" + ")}` : "";
|
|
6764
|
+
return `seeded ${seeded.appended} starter facts (${seeded.seon} curated SEON + ${seeded.conceptnet} ConceptNet${extraClause}) — /memory to inspect`;
|
|
6765
|
+
}
|
|
6766
|
+
|
|
6737
6767
|
/** Whether THIS repo's memory actually carries the corpus seed — the marker is
|
|
6738
6768
|
* authoritative regardless of whether the CURRENT run performed the seeding or
|
|
6739
6769
|
* an earlier run (or `tmct init`) did (seedBootstrapMemory short-circuits on an
|
|
@@ -6871,13 +6901,29 @@ export async function createSession({
|
|
|
6871
6901
|
const moduleCount = graph.individuals.filter((i) => (i.class || "") === "Module").length;
|
|
6872
6902
|
const { version } = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
6873
6903
|
|
|
6904
|
+
// Resolve this handle's extension entries + bias table ONCE per session
|
|
6905
|
+
// (src/extensions.mjs's resolveExtensions) — no new per-turn I/O. Failure-
|
|
6906
|
+
// tolerated: a malformed tmct.toml degrades to the shipped builtins with an
|
|
6907
|
+
// empty bias table (every bundle ranks at bias 1 — see memory/bias.mjs),
|
|
6908
|
+
// never an error before the prompt.
|
|
6909
|
+
let extEntries = null;
|
|
6910
|
+
let biasByBundle = {};
|
|
6911
|
+
try { ({ entries: extEntries, biasByBundle } = await resolveExtensions(repo)); }
|
|
6912
|
+
catch { extEntries = null; biasByBundle = {}; }
|
|
6913
|
+
|
|
6874
6914
|
// Load this handle's lexicon once (the immutable cached core vocabulary the ACE
|
|
6875
|
-
// assert path parses against)
|
|
6876
|
-
//
|
|
6877
|
-
//
|
|
6915
|
+
// assert path parses against), MERGED with any active lexicon/pack extension
|
|
6916
|
+
// entries (Part 3 — mergedLexiconExtra, ascending-bias merge order so a
|
|
6917
|
+
// higher-bias bundle's same-lemma entry wins deterministically). Threaded
|
|
6918
|
+
// into every turn so the grammar layer never re-imports per turn;
|
|
6919
|
+
// failure-tolerated — a broken lexicon degrades to the lazy per-turn load
|
|
6920
|
+
// inside assertTurn, never an error before the prompt.
|
|
6878
6921
|
let lexicon = null;
|
|
6879
|
-
try {
|
|
6880
|
-
|
|
6922
|
+
try {
|
|
6923
|
+
const { loadLexicon } = await import("./grammar/lexicon.mjs");
|
|
6924
|
+
const extra = extEntries ? await mergedLexiconExtra(extEntries, biasByBundle) : null;
|
|
6925
|
+
lexicon = loadLexicon(extra ?? undefined);
|
|
6926
|
+
} catch { lexicon = null; }
|
|
6881
6927
|
|
|
6882
6928
|
// Opt-in telemetry (default OFF → null → the sink's `tel?.record` is a no-op, and
|
|
6883
6929
|
// nothing is written). The conversational session log + sidecar above stay the
|
|
@@ -6945,8 +6991,9 @@ export async function createSession({
|
|
|
6945
6991
|
`the conversation is remembered to ${DEFAULT_GRAPH_REL} — log ${logFile}`
|
|
6946
6992
|
: `tmct chat — ${repo} — ${moduleCount} module(s) — log ${logFile}`,
|
|
6947
6993
|
// the honest seed line appears ONLY on the run that actually seeded — the count
|
|
6948
|
-
// is the TOTAL appended, split into the curated SEON ontology + the ConceptNet band
|
|
6949
|
-
|
|
6994
|
+
// is the TOTAL appended, split into the curated SEON ontology + the ConceptNet band
|
|
6995
|
+
// (+ any other active extension bundle, e.g. an activated tier-2 corpus).
|
|
6996
|
+
...(seeded ? [seedBannerLine(seeded)] : []),
|
|
6950
6997
|
// no code graph → point at how to GET one (a graph producer / --repo / the shipped
|
|
6951
6998
|
// example), and at what IS answerable now — `vocabHint` is only ever a term
|
|
6952
6999
|
// confirmed to resolve in THIS session's actual seed state (see vocabExampleHint),
|
|
@@ -6964,7 +7011,7 @@ export async function createSession({
|
|
|
6964
7011
|
|
|
6965
7012
|
return {
|
|
6966
7013
|
repo, config, graph, lexicon, memoryDir: repo, moduleCount, version, sessionId,
|
|
6967
|
-
logFile, sidecarFile, bannerLines, empty,
|
|
7014
|
+
logFile, sidecarFile, bannerLines, empty, biasByBundle,
|
|
6968
7015
|
// Mutable between-turn state — read-only to the caller, so a shell can render the
|
|
6969
7016
|
// prompt/expand-hint without reaching into runTurn's threading.
|
|
6970
7017
|
get focus() { return focus; },
|
|
@@ -6982,7 +7029,7 @@ export async function createSession({
|
|
|
6982
7029
|
async turn(line) {
|
|
6983
7030
|
let result;
|
|
6984
7031
|
try {
|
|
6985
|
-
result = await runTurn(line, { config, source, graph, focus, last, memoryDir: repo, sessionId, env, lexicon, narrate: narrateOn, vocabHint });
|
|
7032
|
+
result = await runTurn(line, { config, source, graph, focus, last, memoryDir: repo, sessionId, env, lexicon, narrate: narrateOn, vocabHint, tel, biasByBundle });
|
|
6986
7033
|
} catch (e) {
|
|
6987
7034
|
const ts = new Date().toISOString();
|
|
6988
7035
|
const message = e instanceof Error ? e.message : String(e);
|
package/src/codegraph.mjs
CHANGED
|
@@ -407,7 +407,7 @@ function definesIndex(graph) {
|
|
|
407
407
|
* with a penalty for test modules. Renders each hit compactly with the matching
|
|
408
408
|
* symbols, so the agent can jump straight to tmct_describe. No model calls.
|
|
409
409
|
*/
|
|
410
|
-
const SEARCH_LIMIT = 10;
|
|
410
|
+
export const SEARCH_LIMIT = 10;
|
|
411
411
|
const SEARCH_SYMBOLS_SHOWN = 8;
|
|
412
412
|
// Locate scoring — IDF-weighted, component-aware. The rig queries with the WHOLE problem
|
|
413
413
|
// statement, so ubiquitous tokens (template/filter/value/text) would swamp the score; weight each
|
|
@@ -1649,9 +1649,17 @@ export function renderCommitAuthor(graph, sha) {
|
|
|
1649
1649
|
|
|
1650
1650
|
const SYMBOL_CLASSES = { function: "Function", class: "Class", method: "Method", attribute: "Attribute" };
|
|
1651
1651
|
|
|
1652
|
-
|
|
1652
|
+
/** The structured scorer behind searchSymbols (kind=function/class/method/attribute, with
|
|
1653
|
+
* name/decorator filters): filters graph individuals to `kind`'s class, scores by token
|
|
1654
|
+
* substring hits (or 1 for an empty query, matching everything of that kind), and sorts
|
|
1655
|
+
* score desc, tie-broken by SHORTER label first (matches searchSymbols's original inline
|
|
1656
|
+
* sort exactly). An unrecognised `kind` yields an empty ranked list — callers that need to
|
|
1657
|
+
* distinguish "unknown kind" from "kind valid, nothing matched" check SYMBOL_CLASSES
|
|
1658
|
+
* themselves (see searchSymbols below). Pure; deterministic.
|
|
1659
|
+
* @returns {Array<{ind: object, score: number}>} */
|
|
1660
|
+
export function scoreSymbolsRanked(graph, tokens, { kind, decFilter = "", nameRe = null } = {}) {
|
|
1653
1661
|
const targetClass = SYMBOL_CLASSES[kind];
|
|
1654
|
-
if (!targetClass) return
|
|
1662
|
+
if (!targetClass) return [];
|
|
1655
1663
|
const hits = [];
|
|
1656
1664
|
for (const ind of graph.individuals) {
|
|
1657
1665
|
if ((ind.class || "") !== targetClass) continue;
|
|
@@ -1663,8 +1671,15 @@ function searchSymbols(graph, tokens, { limit = SEARCH_LIMIT, kind, decFilter, n
|
|
|
1663
1671
|
if (tokens.length && !score) continue;
|
|
1664
1672
|
hits.push({ ind, score });
|
|
1665
1673
|
}
|
|
1666
|
-
if (!hits.length) return `no ${kind} matches the given filters.`;
|
|
1667
1674
|
hits.sort((a, b) => b.score - a.score || String(a.ind.label).length - String(b.ind.label).length);
|
|
1675
|
+
return hits;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
function searchSymbols(graph, tokens, { limit = SEARCH_LIMIT, kind, decFilter, nameRe }) {
|
|
1679
|
+
const targetClass = SYMBOL_CLASSES[kind];
|
|
1680
|
+
if (!targetClass) return `unknown kind "${kind}" (use function, class, method, attribute, or module).`;
|
|
1681
|
+
const hits = scoreSymbolsRanked(graph, tokens, { kind, decFilter, nameRe });
|
|
1682
|
+
if (!hits.length) return `no ${kind} matches the given filters.`;
|
|
1668
1683
|
const top = hits.slice(0, limit);
|
|
1669
1684
|
const lines = [`${hits.length} ${kind}(s) match (top ${top.length}):`];
|
|
1670
1685
|
for (const { ind } of top) lines.push(`- ${ind.label}${spanTag(siteOf(ind))}`);
|
|
@@ -2025,6 +2040,60 @@ export function renderContextMore(plan) {
|
|
|
2025
2040
|
return out.join("\n");
|
|
2026
2041
|
}
|
|
2027
2042
|
|
|
2043
|
+
// ---- Repository Interface: graph-only context() bundle (no fs) -----------------
|
|
2044
|
+
|
|
2045
|
+
/** Render a graph-only edit bundle for `plan` — every contextPlan section EXCEPT the
|
|
2046
|
+
* fs-dependent anchor/exemplar/inlined-callee body text (registration globals, class
|
|
2047
|
+
* members, ranked siblings, __all__, re-exports, the insertion point, covering tests,
|
|
2048
|
+
* co-change neighbours), gated by `mask` (see bundleMask/sizeBundle). Pure — no fs.
|
|
2049
|
+
* Used by graph-service.mjs's context() service so a graph-only provider (no working
|
|
2050
|
+
* tree) can still return a real HIT instead of an NO_SOURCE miss — see PLAN item 2d /
|
|
2051
|
+
* INTERFACE_VERSION 1.1.0. A source-capable provider layers the body sections on top
|
|
2052
|
+
* (it has fs access this module deliberately does not). */
|
|
2053
|
+
export function renderGraphOnlyBundle(plan, mask) {
|
|
2054
|
+
const out = [
|
|
2055
|
+
`Edit context for ${plan.moduleLabel} (graph-only bundle — siblings/registration/tests are real graph truth; ` +
|
|
2056
|
+
"no source body without a source-capable provider).",
|
|
2057
|
+
];
|
|
2058
|
+
if (mask.registration && plan.globals.length) {
|
|
2059
|
+
out.push(`\n## registration / module globals (replicate this pattern):`);
|
|
2060
|
+
for (const g of plan.globals) out.push(` ${g.label} = ${g.value}${g.site ? ` [:${g.site.start}]` : ""}`);
|
|
2061
|
+
}
|
|
2062
|
+
if (mask.classMembers && plan.classMembers && plan.classMembers.members.length) {
|
|
2063
|
+
out.push(`\n## members of ${plan.classMembers.className}:`);
|
|
2064
|
+
for (const m of plan.classMembers.members) {
|
|
2065
|
+
const short = String(m.label).split(".").pop();
|
|
2066
|
+
const sig = m.params != null && m.params !== "" ? `(${m.params})${m.returns ? ` -> ${m.returns}` : ""}` : "";
|
|
2067
|
+
const dec = m.decorators ? `@${m.decorators} ` : "";
|
|
2068
|
+
const r = m.raises ? ` raises=${m.raises}` : "";
|
|
2069
|
+
out.push(` ${m.class} ${short}${m.site ? ` :${m.site.start}` : ""} ${dec}${short}${sig}${r}`);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
if (mask.siblings && plan.siblings.length) {
|
|
2073
|
+
out.push(`\n## sibling symbols to copy the style of (most relevant first; ${plan.siblings.length} total):`);
|
|
2074
|
+
for (const s of plan.siblings.slice(0, plan.siblingCap)) {
|
|
2075
|
+
const dec = s.decorators ? `@${s.decorators} ` : "";
|
|
2076
|
+
const r = s.raises ? ` raises=${s.raises}` : "";
|
|
2077
|
+
out.push(` ${s.class} ${s.label}${s.site ? ` :${s.site.start}` : ""} ${dec}${r}`);
|
|
2078
|
+
}
|
|
2079
|
+
if (plan.siblings.length > plan.siblingCap) out.push(` …+${plan.siblings.length - plan.siblingCap} more`);
|
|
2080
|
+
}
|
|
2081
|
+
if (mask.allExports && plan.allExports) out.push(`\n## module __all__: ${plan.allExports}`);
|
|
2082
|
+
if (mask.reexports && plan.exports && plan.exports.length) out.push(`\n## re-exported symbols: ${plan.exports.join(", ")}`);
|
|
2083
|
+
if (mask.insertionRegion) {
|
|
2084
|
+
if (plan.insertionRegion) {
|
|
2085
|
+
out.push(`\n## insertion region starts at ${plan.moduleLabel}:${plan.insertionRegion.start} (write your new sibling here — no source body in this graph-only bundle).`);
|
|
2086
|
+
} else if (plan.insertion) {
|
|
2087
|
+
out.push(`\n## insert the new sibling after line ~${plan.insertion} (end of the last top-level definition).`);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
if (mask.tests && plan.tests.length) out.push(`\n## covering tests: ${plan.tests.join(", ")}`);
|
|
2091
|
+
if (mask.cochange && plan.cochange && plan.cochange.length) {
|
|
2092
|
+
out.push(`\n## usually changed together (consider editing these too): ${plan.cochange.map((c) => `${c.label} (×${c.weight})`).join(", ")}`);
|
|
2093
|
+
}
|
|
2094
|
+
return out.join("\n");
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2028
2097
|
// ---- #7 cold-tool catalog (written to <repo>/.tmct/TOOLS.md by the index step) --
|
|
2029
2098
|
|
|
2030
2099
|
/** Markdown catalog of the COLD tools (everything except the hot catalog tools): each
|
package/src/config.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// <cwd>/.tmct/graph.json. Run with cwd = the repo, and the
|
|
7
7
|
// default resolves to that repo's artifact — no config needed.
|
|
8
8
|
|
|
9
|
-
import { join } from "node:path";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
10
|
|
|
11
11
|
export const DEFAULT_GRAPH_REL = join(".tmct", "graph.json");
|
|
12
12
|
|
|
@@ -20,8 +20,13 @@ export class ToolError extends Error {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function loadConfig(env = process.env, cwd = process.cwd()) {
|
|
23
|
+
// Always resolve to an absolute path, even when TMCT_GRAPH_FILE is set to a
|
|
24
|
+
// relative one — src/source-slice.mjs's path-traversal guard compares an
|
|
25
|
+
// always-absolute resolve(repoRoot, site.path) against repoRoot itself, so a
|
|
26
|
+
// relative repoRoot (derived from this graphFile) would make that guard
|
|
27
|
+
// reject every read, not just traversal attempts.
|
|
23
28
|
const graphFile = env.TMCT_GRAPH_FILE && env.TMCT_GRAPH_FILE.trim()
|
|
24
|
-
? env.TMCT_GRAPH_FILE.trim()
|
|
29
|
+
? resolve(cwd, env.TMCT_GRAPH_FILE.trim())
|
|
25
30
|
: join(cwd, DEFAULT_GRAPH_REL);
|
|
26
31
|
return { graphFile };
|
|
27
32
|
}
|