@polycode-projects/the-mechanical-code-talker 3.0.0 → 3.0.2
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 +11 -0
- package/bin/tmct.mjs +98 -0
- package/data/templates/constructions/digest-sentence-structures.toml +97 -0
- package/package.json +7 -1
- package/src/adapters/corpus/digest-bank.mjs +62 -0
- package/src/adapters/toml-config.mjs +13 -0
- package/src/domain/ask.mjs +53 -4
- package/src/domain/cli-verbs.mjs +11 -0
- package/src/domain/codeplan/graph-delta.mjs +239 -0
- package/src/domain/codeplan/graph-predicates.mjs +0 -0
- package/src/domain/codeplan/operators.mjs +232 -0
- package/src/domain/codeplan/planner.mjs +87 -0
- package/src/domain/digest/article.mjs +106 -0
- package/src/domain/digest/compose.mjs +103 -0
- package/src/domain/digest/config.json +13 -0
- package/src/domain/digest/index.mjs +38 -0
- package/src/domain/digest/select.mjs +233 -0
- package/src/domain/digest/store-stats.mjs +77 -0
- package/src/domain/digest/structures.mjs +100 -0
- package/src/domain/digest/words.mjs +40 -0
- package/src/domain/discourse.mjs +0 -0
- package/src/services/chat-session.mjs +13 -1
- package/src/services/chat.mjs +191 -12
- package/src/services/ledger-viz.mjs +14 -3
- package/src/services/sessions.mjs +3 -1
- package/src/surfaces/web/memory-ask-browser.bundle.js +121 -116
package/README.md
CHANGED
|
@@ -802,6 +802,17 @@ self-contained HTML file you can open in a browser:
|
|
|
802
802
|
rows; --term resolves via the same normalization chat uses.
|
|
803
803
|
```
|
|
804
804
|
|
|
805
|
+
`tmct digest <term>` turns what the graph knows about one term into a short, readable
|
|
806
|
+
paragraph — the vocabulary-side sibling of `tmct cli digest`'s code map. The narrative
|
|
807
|
+
leads, its sources follow, and the full fact count points at the ledger for the rest:
|
|
808
|
+
|
|
809
|
+
```output:help:digest
|
|
810
|
+
tmct digest <term> a readable digest of what the graph knows about one term:
|
|
811
|
+
[--repo <abs>] a bounded narrative first (selected, sense-filtered,
|
|
812
|
+
[--graph <path>] deduped), then its sources and the stored-fact count.
|
|
813
|
+
[--config <path>] The vocabulary-side sibling of `cli digest`'s code map.
|
|
814
|
+
```
|
|
815
|
+
|
|
805
816
|
`tmct serve` runs an Anthropic Messages API-compatible HTTP endpoint over the graph,
|
|
806
817
|
so a tool-loop client can call tmct like a model, at $0:
|
|
807
818
|
|
package/bin/tmct.mjs
CHANGED
|
@@ -920,6 +920,43 @@ async function main() {
|
|
|
920
920
|
// on disk: one copy-paste Bash invocation per tool, rewritten on every init.
|
|
921
921
|
process.stdout.write(`cold-tool catalog: ${await writeToolsCatalog(repoRoot)}\n`);
|
|
922
922
|
|
|
923
|
+
// `--with-persona code`: on top of the corpus-vocabulary bias `initRepo` just wrote,
|
|
924
|
+
// also run the repository INDEXER (`tmct index`'s own machinery) against this repo's
|
|
925
|
+
// real source, so one command produces a `.tmct/graph.json` backed by the repo itself —
|
|
926
|
+
// not just a bias preset. `chat --repo` already reads whatever graph is on disk; this
|
|
927
|
+
// is the onboarding path that puts one there. Failure-tolerant like the corpus seed
|
|
928
|
+
// above: a repo that can't be indexed (no supported source, or a parse error) degrades
|
|
929
|
+
// to an initialized-but-graphless repo, never a crashed init.
|
|
930
|
+
if (personaName === "code") {
|
|
931
|
+
try {
|
|
932
|
+
const { indexRepository } = await import("../src/index/index-repo.mjs");
|
|
933
|
+
const stats = await indexRepository(repoRoot);
|
|
934
|
+
for (const { pass, message } of stats.gitErrors || []) {
|
|
935
|
+
process.stderr.write(`tmct init: WARNING git history pass '${pass}' — ${message} (graph built without those edges)\n`);
|
|
936
|
+
}
|
|
937
|
+
const perLang = Object.entries(stats.perLang)
|
|
938
|
+
.map(([lang, s]) => `${lang}: ${s.modules} modules, ${s.symbols} symbols`).join("; ");
|
|
939
|
+
const kib = (stats.bytes / 1024).toFixed(1);
|
|
940
|
+
process.stdout.write(
|
|
941
|
+
`code persona: indexed the repo — wrote ${stats.graphFile} (${stats.modules} modules, ${stats.symbols} symbols; ${kib} KiB)\n`
|
|
942
|
+
+ (perLang ? `${perLang}\n` : "no supported source found under the repo\n"),
|
|
943
|
+
);
|
|
944
|
+
// The capability the graph just unlocked, said out loud — a graphless
|
|
945
|
+
// repo (no supported source) gets no such promise.
|
|
946
|
+
if (stats.modules > 0) {
|
|
947
|
+
process.stdout.write(
|
|
948
|
+
`indexed ${stats.modules} modules (${stats.symbols} symbols) — code questions now work in \`tmct chat\`: `
|
|
949
|
+
+ `try "which modules import <path>" or "what does <module> do".\n`,
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
if (stats.failures?.length) {
|
|
953
|
+
process.stderr.write(`tmct init: ${stats.failures.length} file(s) failed to parse (skipped): ${stats.failures.slice(0, 5).join(", ")}${stats.failures.length > 5 ? ", …" : ""}\n`);
|
|
954
|
+
}
|
|
955
|
+
} catch (e) {
|
|
956
|
+
process.stderr.write(`tmct init: code persona indexing skipped (${e?.message || e})\n`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
923
960
|
// `--corpus`/`--ontology`/`--lexicon` now mean "activate this bundle and
|
|
924
961
|
// PERSIST that into tmct.toml" — so a second `tmct init` (or the next chat
|
|
925
962
|
// bootstrap) remembers the choice, unlike the old ad hoc path, which had
|
|
@@ -1011,6 +1048,14 @@ async function main() {
|
|
|
1011
1048
|
`tmct index — wrote ${stats.graphFile} (${stats.modules} modules, ${stats.symbols} symbols; ${kib} KiB)\n`
|
|
1012
1049
|
+ (perLang ? `${perLang}\n` : "no supported source found under the repo\n"),
|
|
1013
1050
|
);
|
|
1051
|
+
// Same discoverability line the code-persona init prints: the graph just
|
|
1052
|
+
// unlocked code questions, so say so — but never over an empty graph.
|
|
1053
|
+
if (stats.modules > 0) {
|
|
1054
|
+
process.stdout.write(
|
|
1055
|
+
`indexed ${stats.modules} modules (${stats.symbols} symbols) — code questions now work in \`tmct chat\`: `
|
|
1056
|
+
+ `try "which modules import <path>" or "what does <module> do".\n`,
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1014
1059
|
if (stats.failures?.length) {
|
|
1015
1060
|
process.stderr.write(`tmct index: ${stats.failures.length} file(s) failed to parse (skipped): ${stats.failures.slice(0, 5).join(", ")}${stats.failures.length > 5 ? ", …" : ""}\n`);
|
|
1016
1061
|
}
|
|
@@ -1283,6 +1328,59 @@ async function main() {
|
|
|
1283
1328
|
return;
|
|
1284
1329
|
}
|
|
1285
1330
|
|
|
1331
|
+
if (mode === "digest") {
|
|
1332
|
+
// `tmct digest <term>` — the vocabulary-side digest: turn what the graph
|
|
1333
|
+
// knows about one term into a bounded, readable paragraph, deterministically
|
|
1334
|
+
// (src/domain/digest, wired through corpus/digest-bank.mjs), beside the
|
|
1335
|
+
// code-side `tmct cli digest`. Same repo/backend resolution as `viz`.
|
|
1336
|
+
const rest = process.argv.slice(3);
|
|
1337
|
+
const term = rest.find((a) => !a.startsWith("-"));
|
|
1338
|
+
if (!term) {
|
|
1339
|
+
process.stderr.write("tmct digest — name a term: `tmct digest <term>`\n");
|
|
1340
|
+
process.exitCode = 1;
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
const { resolveRuntimeConfig } = await import("../src/services/cli-args.mjs");
|
|
1344
|
+
const { openMemoryBackend, loadMemory, readFactRows, normFactTerm } = await import("../src/adapters/memory/core.mjs");
|
|
1345
|
+
const { digestTermFromRows } = await import("../src/adapters/corpus/digest-bank.mjs");
|
|
1346
|
+
const { repo, toml } = await resolveRuntimeConfig({ argv: rest });
|
|
1347
|
+
const backendChoice = String(process.env.TMCT_MEMORY_BACKEND || toml?.memory?.backend || "").trim().toLowerCase();
|
|
1348
|
+
const { dir: memoryDir, close: closeMemoryStore } = await openMemoryBackend(repo, backendChoice);
|
|
1349
|
+
let rows;
|
|
1350
|
+
try { rows = readFactRows(await loadMemory(memoryDir)); } finally { await closeMemoryStore(); }
|
|
1351
|
+
// Fold a naive plural to its base on BOTH sides so "aardvark" reaches a
|
|
1352
|
+
// stored "aardvarks" and vice versa — the store keeps whichever number the
|
|
1353
|
+
// teaching sentence used.
|
|
1354
|
+
const baseTerm = (s) => {
|
|
1355
|
+
const x = normFactTerm(s);
|
|
1356
|
+
if (x.endsWith("es")) return x.slice(0, -2);
|
|
1357
|
+
if (x.endsWith("s")) return x.slice(0, -1);
|
|
1358
|
+
return x;
|
|
1359
|
+
};
|
|
1360
|
+
const wantedBase = baseTerm(term);
|
|
1361
|
+
const termRows = rows.filter((r) => baseTerm(r.subject) === wantedBase);
|
|
1362
|
+
if (!termRows.length) {
|
|
1363
|
+
process.stdout.write(`I don't have anything stored about "${term}".\n`);
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
// Render under the term's own stored spelling, not the query's — a plural
|
|
1367
|
+
// query ("doctors") reads back as the singular the store holds ("doctor").
|
|
1368
|
+
const subject = termRows[0].subject;
|
|
1369
|
+
const article = digestTermFromRows(subject, termRows, rows, { budget: 12 });
|
|
1370
|
+
if (!article || !article.paragraphs.length) {
|
|
1371
|
+
// No structure bank, or nothing the selector kept — surface the plain
|
|
1372
|
+
// fact count rather than an empty digest.
|
|
1373
|
+
process.stdout.write(`${subject} — ${termRows.length} fact(s) stored. Run \`tmct viz --focus ${subject}\` for the full ledger.\n`);
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
const sources = article.sources.map((s) => s.provenance).filter(Boolean);
|
|
1377
|
+
const out = [`${subject}`, ...article.paragraphs];
|
|
1378
|
+
if (sources.length) out.push("", `Sources: ${[...new Set(sources)].join("; ")}`);
|
|
1379
|
+
out.push(`${article.detail.factCount} fact(s) stored — \`tmct viz --focus ${subject}\` shows them all.`);
|
|
1380
|
+
process.stdout.write(out.join("\n") + "\n");
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1286
1384
|
if (mode === "serve") {
|
|
1287
1385
|
// `tmct serve` — the Phase-A capability-router interface: an Anthropic
|
|
1288
1386
|
// Messages API-compatible HTTP endpoint (POST /v1/messages) over the graph.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# data/templates/constructions/digest-sentence-structures.toml — the digest
|
|
2
|
+
# layer's sentence-structure bank (PLAN_DIGEST.md stage 2). A closed table of
|
|
3
|
+
# sentence skeletons keyed by relation FAMILY and FORM, hand-authored and
|
|
4
|
+
# reviewed as DATA, in the same committed-TOML idiom the construction banks in
|
|
5
|
+
# this directory already use. A new family or a reworded frame is an edit to
|
|
6
|
+
# this file, never a code change — src/domain/digest/structures.mjs validates
|
|
7
|
+
# and renders these skeletons but never invents one.
|
|
8
|
+
#
|
|
9
|
+
# The existing construction-bank loader (adapters/corpus/construction-banks.mjs)
|
|
10
|
+
# reads only [[relation]] and [[construction]] tables, so it ignores this file's
|
|
11
|
+
# [[structure]] rows; the digest layer reads them through its own path.
|
|
12
|
+
#
|
|
13
|
+
# Each row is one (family, form) pairing:
|
|
14
|
+
# family one of the digest families (isa | location | partOf | capableOf |
|
|
15
|
+
# usedFor) — the closed set stage 1 groups predicates under. Each is
|
|
16
|
+
# verb-coherent: every predicate in it shares this frame's verb.
|
|
17
|
+
# form single — one fact of the family
|
|
18
|
+
# several — two or more facts of the family, merged into one clause
|
|
19
|
+
# chained — one isa fact plus its rendered ancestry chain
|
|
20
|
+
# template the skeleton. Placeholders the renderer fills (words.mjs supplies
|
|
21
|
+
# the article, plural and casing):
|
|
22
|
+
# {TERM} {TERM_CAP} the term, lower / sentence-leading
|
|
23
|
+
# {A_TERM} {A_TERM_CAP} "an aardvark" / "An aardvark"
|
|
24
|
+
# {TERMS} {TERMS_CAP} "aardvarks" / "Aardvarks"
|
|
25
|
+
# {PRONOUN} {PRONOUN_CAP} "it" / "It" (after first mention)
|
|
26
|
+
# {OBJECT} the first object, raw
|
|
27
|
+
# {A_OBJECT} "a mammal" (article chosen per word)
|
|
28
|
+
# {OBJECTS_A} "a mammal, a burrowing animal, and …"
|
|
29
|
+
# {OBJECTS_PLURAL} "mammals, burrowing animals, and …"
|
|
30
|
+
# {OBJECTS_RAW} "dig, and swim" (no article)
|
|
31
|
+
# {CHAIN} "a mammal, and so an animal"
|
|
32
|
+
# Any placeholder with no value for this call renders empty.
|
|
33
|
+
|
|
34
|
+
# ---- isa: the definition frame (copula) -----------------------------------
|
|
35
|
+
|
|
36
|
+
[[structure]]
|
|
37
|
+
family = "isa"
|
|
38
|
+
form = "single"
|
|
39
|
+
template = "{A_TERM_CAP} is {A_OBJECT}."
|
|
40
|
+
|
|
41
|
+
[[structure]]
|
|
42
|
+
family = "isa"
|
|
43
|
+
form = "several"
|
|
44
|
+
template = "{TERMS_CAP} are {OBJECTS_A}."
|
|
45
|
+
|
|
46
|
+
[[structure]]
|
|
47
|
+
family = "isa"
|
|
48
|
+
form = "chained"
|
|
49
|
+
template = "{A_TERM_CAP} is {CHAIN}."
|
|
50
|
+
|
|
51
|
+
# ---- location: where it is found (atLocation / locatedNear) ----------------
|
|
52
|
+
|
|
53
|
+
[[structure]]
|
|
54
|
+
family = "location"
|
|
55
|
+
form = "single"
|
|
56
|
+
template = "{PRONOUN_CAP} is found in {OBJECT}."
|
|
57
|
+
|
|
58
|
+
[[structure]]
|
|
59
|
+
family = "location"
|
|
60
|
+
form = "several"
|
|
61
|
+
template = "{PRONOUN_CAP} is found in {OBJECTS_RAW}."
|
|
62
|
+
|
|
63
|
+
# ---- partOf: what it belongs to -------------------------------------------
|
|
64
|
+
|
|
65
|
+
[[structure]]
|
|
66
|
+
family = "partOf"
|
|
67
|
+
form = "single"
|
|
68
|
+
template = "{PRONOUN_CAP} is part of {A_OBJECT}."
|
|
69
|
+
|
|
70
|
+
[[structure]]
|
|
71
|
+
family = "partOf"
|
|
72
|
+
form = "several"
|
|
73
|
+
template = "{PRONOUN_CAP} is part of {OBJECTS_A}."
|
|
74
|
+
|
|
75
|
+
# ---- capableOf: what it can do --------------------------------------------
|
|
76
|
+
|
|
77
|
+
[[structure]]
|
|
78
|
+
family = "capableOf"
|
|
79
|
+
form = "single"
|
|
80
|
+
template = "{PRONOUN_CAP} can {OBJECT}."
|
|
81
|
+
|
|
82
|
+
[[structure]]
|
|
83
|
+
family = "capableOf"
|
|
84
|
+
form = "several"
|
|
85
|
+
template = "{PRONOUN_CAP} can {OBJECTS_RAW}."
|
|
86
|
+
|
|
87
|
+
# ---- usedFor: what it is used for -----------------------------------------
|
|
88
|
+
|
|
89
|
+
[[structure]]
|
|
90
|
+
family = "usedFor"
|
|
91
|
+
form = "single"
|
|
92
|
+
template = "{PRONOUN_CAP} is used for {OBJECT}."
|
|
93
|
+
|
|
94
|
+
[[structure]]
|
|
95
|
+
family = "usedFor"
|
|
96
|
+
form = "several"
|
|
97
|
+
template = "{PRONOUN_CAP} is used for {OBJECTS_RAW}."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
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; indexes a repo on request (tmct index) or reads any producer's graph.",
|
|
@@ -130,8 +130,14 @@
|
|
|
130
130
|
"example:polyglot": "node --disable-warning=ExperimentalWarning bin/tmct.mjs chat --repo examples/polyglot --ephemeral",
|
|
131
131
|
"chatbench:run": "node chatbench/run.mjs",
|
|
132
132
|
"chatbench:judge": "node chatbench/judge.mjs",
|
|
133
|
+
"chatbench:judge:cached": "node chatbench/judge.mjs --cache chatbench/verdict-cache.json",
|
|
133
134
|
"agentbench:run": "node agentbench/run.mjs",
|
|
134
135
|
"infbench": "node infbench/generate-cases.mjs && node infbench/run.mjs",
|
|
136
|
+
"idxbench:run": "node idxbench/run.mjs",
|
|
137
|
+
"researchbench:run": "node researchbench/run.mjs",
|
|
138
|
+
"synthbench:code": "node synthbench/code/run.mjs",
|
|
139
|
+
"ingestbench:run": "node ingestbench/run.mjs",
|
|
140
|
+
"ingestbench:judge": "node ingestbench/judge.mjs",
|
|
135
141
|
"corpus:matrix": "node scripts/corpus-matrix.mjs",
|
|
136
142
|
"corpus:matrix:gaps": "node scripts/corpus-matrix.mjs --gaps",
|
|
137
143
|
"template:coverage": "node scripts/template-coverage.mjs",
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// corpus/digest-bank.mjs — the filesystem side of the digest layer's stage-5
|
|
2
|
+
// wiring: read the committed sentence-structure bank and build the pre-parsed
|
|
3
|
+
// table the pure layer (src/domain/digest/) expects, then run one term end to
|
|
4
|
+
// end from a set of fact rows. The pure store scan and the pure pipeline live
|
|
5
|
+
// in src/domain/digest; only the TOML read lives here, so every node surface
|
|
6
|
+
// (chat, `tmct digest`, the page generators) shares one seam.
|
|
7
|
+
//
|
|
8
|
+
// A browser bundle stubs this module out (the ask/chat dock never digests
|
|
9
|
+
// in-page — the pages digest client-side from a table embedded at build time),
|
|
10
|
+
// so every consumer treats a null article as "fall back to the flat fact list".
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { join, dirname } from "node:path";
|
|
15
|
+
import { parse as parseToml } from "smol-toml";
|
|
16
|
+
import { buildStructureTable, digestTerm } from "../../domain/digest/index.mjs";
|
|
17
|
+
import { digestStoreStats, chainsForObjects, isaObjectsOf } from "../../domain/digest/store-stats.mjs";
|
|
18
|
+
|
|
19
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const BANK_FILE = join(HERE, "..", "..", "..", "data", "templates", "constructions", "digest-sentence-structures.toml");
|
|
21
|
+
|
|
22
|
+
/** The raw [[structure]] rows parsed from the committed bank, once. A missing or
|
|
23
|
+
* unparseable bank yields an empty list — the digest degrades to the caller's
|
|
24
|
+
* flat fallback rather than throwing, the same posture the construction-bank
|
|
25
|
+
* loader takes. The raw rows (not the built table) are cached so a surface that
|
|
26
|
+
* needs to embed them for a browser render gets them without re-reading. */
|
|
27
|
+
let cachedStructures = null;
|
|
28
|
+
export function readDigestStructures() {
|
|
29
|
+
if (cachedStructures) return cachedStructures;
|
|
30
|
+
try {
|
|
31
|
+
const parsed = parseToml(readFileSync(BANK_FILE, "utf8"));
|
|
32
|
+
cachedStructures = Array.isArray(parsed.structure) ? parsed.structure : [];
|
|
33
|
+
} catch {
|
|
34
|
+
cachedStructures = [];
|
|
35
|
+
}
|
|
36
|
+
return cachedStructures;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let cachedTable = null;
|
|
40
|
+
/** The built structure table, once. Empty when the bank is unavailable. */
|
|
41
|
+
export function loadDigestStructureTable() {
|
|
42
|
+
if (cachedTable) return cachedTable;
|
|
43
|
+
cachedTable = buildStructureTable(readDigestStructures());
|
|
44
|
+
return cachedTable;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Digest one term end to end from fact rows. `termRows` are the rows whose
|
|
49
|
+
* subject is the term; `allRows` is the whole store the statistics scan over
|
|
50
|
+
* (pass the same array for both when the caller has already narrowed to one
|
|
51
|
+
* term). Returns the term-article shape, or null when the structure bank is
|
|
52
|
+
* unavailable (the browser-stub case) so the surface falls back to its flat
|
|
53
|
+
* rendering. `opts.budget` picks the per-surface fact budget.
|
|
54
|
+
*/
|
|
55
|
+
export function digestTermFromRows(term, termRows, allRows, opts = {}) {
|
|
56
|
+
const table = loadDigestStructureTable();
|
|
57
|
+
if (!table || table.size === 0) return null;
|
|
58
|
+
const rows = termRows || [];
|
|
59
|
+
const store = digestStoreStats(allRows || rows);
|
|
60
|
+
const chains = chainsForObjects(store.subClassEdges, isaObjectsOf(rows));
|
|
61
|
+
return digestTerm(term, rows, store, table, { ...opts, chains });
|
|
62
|
+
}
|
|
@@ -102,6 +102,14 @@ export async function normalizeConfig(raw, { configDir } = {}) {
|
|
|
102
102
|
const arr = Array.isArray(src.graph_files) ? src.graph_files : [src.graph_files];
|
|
103
103
|
cfg.graphFiles = arr.map((p) => resolve(dir, String(p)));
|
|
104
104
|
}
|
|
105
|
+
// [graph] read_only — a chat session against this repo READS the graph but
|
|
106
|
+
// writes nothing back into its .tmct/: no per-turn session upsert into
|
|
107
|
+
// graph.json, no transcript/sidecar logs, no memory droppings. A committed
|
|
108
|
+
// example fixture sets it so a plain `tmct chat --repo examples/<x>` (no
|
|
109
|
+
// --ephemeral) can never rewrite the hand-stamped graph. Sparse like the
|
|
110
|
+
// rest: absent when unset, so "unset" stays distinguishable from "false".
|
|
111
|
+
const graph = src.graph || {};
|
|
112
|
+
if (graph.read_only !== undefined) cfg.graph = { readOnly: graph.read_only };
|
|
105
113
|
const corpus = src.corpus || {};
|
|
106
114
|
if (corpus.tier !== undefined) cfg.corpus = { tier: corpus.tier };
|
|
107
115
|
const seed = src.seed || {};
|
|
@@ -134,6 +142,11 @@ export async function normalizeConfig(raw, { configDir } = {}) {
|
|
|
134
142
|
// resolveResearchConfig's job.
|
|
135
143
|
if (src.research !== undefined) cfg.research = src.research;
|
|
136
144
|
|
|
145
|
+
// Discourse-record knob (src/domain/discourse.mjs): sparse PASS-THROUGH,
|
|
146
|
+
// same discipline — the raw `[discourse]` table (max_referents) rides
|
|
147
|
+
// through; default-filling is resolveDiscourseConfig's job.
|
|
148
|
+
if (src.discourse !== undefined) cfg.discourse = src.discourse;
|
|
149
|
+
|
|
137
150
|
const idx = src.index || {};
|
|
138
151
|
const index = {};
|
|
139
152
|
if (idx.languages !== undefined) index.languages = idx.languages;
|
package/src/domain/ask.mjs
CHANGED
|
@@ -1841,18 +1841,26 @@ function evalRecentCommits(graph) {
|
|
|
1841
1841
|
const COMMIT_FILTER_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
1842
1842
|
/** Resolves the pivot (a literal ISO date, or a named commit whose own date
|
|
1843
1843
|
* becomes the pivot), then filters every Commit's date against it. An
|
|
1844
|
-
* unresolvable pivot declines honestly (pivotResolved:false).
|
|
1844
|
+
* unresolvable pivot declines honestly (pivotResolved:false).
|
|
1845
|
+
*
|
|
1846
|
+
* A resolved answer also carries `referents` — the typed discourse referents
|
|
1847
|
+
* this answer establishes (the result set, and the pivot commit the question
|
|
1848
|
+
* was ABOUT, which the focus rules deliberately refuse to hold). Emitted
|
|
1849
|
+
* here, beside the fully typed result, before render() flattens it all to a
|
|
1850
|
+
* sentence; the session layer is what registers them into its record. */
|
|
1845
1851
|
function evalCommitFilter(graph, ast) {
|
|
1846
1852
|
const { op, pivotRaw } = ast;
|
|
1847
1853
|
const dateOf = (c) => String((c.attributes || []).find((a) => a.key === "date")?.value || "").slice(0, 10);
|
|
1848
1854
|
let pivotDate = null;
|
|
1849
1855
|
let pivotId = null;
|
|
1856
|
+
let pivotLabel = null;
|
|
1850
1857
|
if (COMMIT_FILTER_DATE_RE.test(pivotRaw)) {
|
|
1851
1858
|
pivotDate = pivotRaw;
|
|
1852
1859
|
} else {
|
|
1853
1860
|
const { match, ambiguous } = resolveObject(graph, pivotRaw, { expectedClass: "Commit" });
|
|
1854
1861
|
if (match && !ambiguous && match.class === "Commit" && dateOf(match)) {
|
|
1855
1862
|
pivotId = match.id;
|
|
1863
|
+
pivotLabel = match.label;
|
|
1856
1864
|
pivotDate = dateOf(match);
|
|
1857
1865
|
}
|
|
1858
1866
|
}
|
|
@@ -1867,7 +1875,24 @@ function evalCommitFilter(graph, ast) {
|
|
|
1867
1875
|
return d === pivotDate; // "on"
|
|
1868
1876
|
})
|
|
1869
1877
|
.sort((a, b) => dateOf(b).localeCompare(dateOf(a)));
|
|
1870
|
-
|
|
1878
|
+
const referents = [];
|
|
1879
|
+
if (matches.length) {
|
|
1880
|
+
referents.push({
|
|
1881
|
+
kind: "set", class: "Commit",
|
|
1882
|
+
label: `${matches.length} commit${matches.length === 1 ? "" : "s"} ${op} ${pivotRaw}`,
|
|
1883
|
+
ids: matches.map((c) => c.id),
|
|
1884
|
+
attrs: { count: matches.length, op, ...(pivotId ? { pivot: pivotId } : {}) },
|
|
1885
|
+
lane: "commitFilter",
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
if (pivotId) {
|
|
1889
|
+
referents.push({
|
|
1890
|
+
kind: "event", class: "Commit", label: pivotLabel,
|
|
1891
|
+
ids: [pivotId], attrs: { date: pivotDate },
|
|
1892
|
+
lane: "commitFilter",
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
return { compositeKind: "commitFilter", op, pivotRaw, pivotDate, pivotResolved: true, matches, referents };
|
|
1871
1896
|
}
|
|
1872
1897
|
|
|
1873
1898
|
/** Temporal over a nested set: the commits that touched any member of the
|
|
@@ -2371,6 +2396,14 @@ function joinedQueryForm(term) {
|
|
|
2371
2396
|
.replace(/[\s\-_]+/g, "");
|
|
2372
2397
|
}
|
|
2373
2398
|
|
|
2399
|
+
/** Ceiling of tier 3's weakest scoring band (the term-component-overlap
|
|
2400
|
+
* fraction): a candidate scores exactly this when EVERY component of the
|
|
2401
|
+
* term appears in its label, strictly less on a partial overlap. Every
|
|
2402
|
+
* NAME-evidence band (stem/containment/joined/derivational) scores
|
|
2403
|
+
* hundreds and up, so score < this ceiling means the candidate shares only
|
|
2404
|
+
* some generic path components with the term. */
|
|
2405
|
+
const COMPONENT_OVERLAP_MAX = 10;
|
|
2406
|
+
|
|
2374
2407
|
/** Resolve a free-text object/subject term against the graph's individuals, in
|
|
2375
2408
|
* priority order: a sha-shaped term first resolves against Commit
|
|
2376
2409
|
* individuals by unique id/label prefix, then (1) exact label/id match, (2)
|
|
@@ -2549,7 +2582,7 @@ function resolveObjectCore(graph, term, { expectedClass = null } = {}) {
|
|
|
2549
2582
|
// stem hit from the tier above, nor a fuller-fraction overlap on another
|
|
2550
2583
|
// candidate. termComps.length > 0 is guaranteed here (overlap > 0 requires
|
|
2551
2584
|
// at least one termComps entry to have matched).
|
|
2552
|
-
scored.push({ ind: m, score: (overlap / termComps.length) *
|
|
2585
|
+
scored.push({ ind: m, score: (overlap / termComps.length) * COMPONENT_OVERLAP_MAX });
|
|
2553
2586
|
}
|
|
2554
2587
|
}
|
|
2555
2588
|
}
|
|
@@ -2557,9 +2590,20 @@ function resolveObjectCore(graph, term, { expectedClass = null } = {}) {
|
|
|
2557
2590
|
if (scored.length) {
|
|
2558
2591
|
const [best, ...rest] = scored;
|
|
2559
2592
|
const tied = rest.filter((x) => x.score === best.score);
|
|
2593
|
+
// A PARTIAL component-overlap runner-up (score < COMPONENT_OVERLAP_MAX:
|
|
2594
|
+
// some but not all of the term's own tokens, e.g. graph-merge.mjs
|
|
2595
|
+
// sharing only "graph"/"mjs" with "graph-build.mjs") is not another
|
|
2596
|
+
// reading of the term, and disclosing it in the "(answering for X — N
|
|
2597
|
+
// other matches)" note reads as leakage. A candidate that matched EVERY
|
|
2598
|
+
// term token (score === COMPONENT_OVERLAP_MAX) or matched by name
|
|
2599
|
+
// evidence (score above the band) stays disclosed. A partial-overlap
|
|
2600
|
+
// winner keeps its partial peers — they are all the evidence there is.
|
|
2601
|
+
const disclosable = best.score >= COMPONENT_OVERLAP_MAX
|
|
2602
|
+
? rest.filter((x) => x.score >= COMPONENT_OVERLAP_MAX)
|
|
2603
|
+
: rest;
|
|
2560
2604
|
return {
|
|
2561
2605
|
match: best.ind,
|
|
2562
|
-
candidates:
|
|
2606
|
+
candidates: disclosable.slice(0, 4).map((x) => x.ind),
|
|
2563
2607
|
tier: 3,
|
|
2564
2608
|
ambiguous: tied.length > 0,
|
|
2565
2609
|
};
|
|
@@ -4384,6 +4428,11 @@ export function ask(graph, query, { contextId = null, nlp = undefined, prev = nu
|
|
|
4384
4428
|
// edit-distance, announced in the content as "assuming you meant …");
|
|
4385
4429
|
// null for every literal-identifier tier.
|
|
4386
4430
|
matchedVia: result.matchedVia || null,
|
|
4431
|
+
// The typed discourse referents this answer established (see
|
|
4432
|
+
// evalCommitFilter) — additive, present only when a lane emitted them,
|
|
4433
|
+
// so a session layer can register them without re-deriving the answer's
|
|
4434
|
+
// typed content from its rendered sentence.
|
|
4435
|
+
...(Array.isArray(result.referents) && result.referents.length ? { discourse: result.referents } : {}),
|
|
4387
4436
|
...(rendered.ambiguous ? { candidates: rendered.candidates, candidateParses: rendered.candidateParses } : {}),
|
|
4388
4437
|
},
|
|
4389
4438
|
};
|
package/src/domain/cli-verbs.mjs
CHANGED
|
@@ -137,6 +137,17 @@ export const CLI_VERBS = [
|
|
|
137
137
|
{ flag: "[--config <path>]", prose: ["--term <word> override it); --output defaults to", "ledger.html in the cwd; --limit caps the embedded fact", "rows; --term resolves via the same normalization chat uses."] },
|
|
138
138
|
],
|
|
139
139
|
},
|
|
140
|
+
{
|
|
141
|
+
mode: "digest",
|
|
142
|
+
errorLabel: "digest",
|
|
143
|
+
usage: "tmct digest <term>",
|
|
144
|
+
prose: ["a readable digest of what the graph knows about one term:"],
|
|
145
|
+
flags: [
|
|
146
|
+
{ flag: "[--repo <abs>]", prose: ["a bounded narrative first (selected, sense-filtered,"] },
|
|
147
|
+
{ flag: "[--graph <path>]", prose: ["deduped), then its sources and the stored-fact count."] },
|
|
148
|
+
{ flag: "[--config <path>]", prose: ["The vocabulary-side sibling of `cli digest`'s code map."] },
|
|
149
|
+
],
|
|
150
|
+
},
|
|
140
151
|
{
|
|
141
152
|
mode: "serve",
|
|
142
153
|
errorLabel: "serve",
|