@polycode-projects/the-mechanical-code-talker 2.5.0 → 2.5.3
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 +15 -11
- package/bin/tmct.mjs +8 -5
- package/corpus/LICENSES.json +7 -0
- package/corpus/README.md +3 -3
- package/corpus/reference/LICENSE-NOTICE +36 -0
- package/corpus/reference/README.md +19 -0
- package/corpus/reference/index.json.gz +0 -0
- package/corpus/reference/manifest.json +367 -0
- package/corpus/reference/shards/ref-00.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-01.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-02.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-03.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-04.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-05.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-06.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-07.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-08.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-09.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-0a.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-0b.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-0c.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-0d.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-0e.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-0f.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-10.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-11.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-12.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-13.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-14.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-15.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-16.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-17.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-18.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-19.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-1a.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-1b.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-1c.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-1d.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-1e.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-1f.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-20.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-21.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-22.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-23.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-24.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-25.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-26.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-27.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-28.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-29.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-2a.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-2b.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-2c.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-2d.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-2e.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-2f.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-30.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-31.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-32.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-33.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-34.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-35.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-36.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-37.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-38.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-39.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-3a.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-3b.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-3c.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-3d.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-3e.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-3f.jsonl.gz +0 -0
- package/package.json +8 -4
- package/src/adapters/corpus/reference-pack.mjs +107 -0
- package/src/adapters/memory/core.mjs +104 -10
- package/src/domain/ask.mjs +66 -9
- package/src/domain/cli-verbs.mjs +3 -3
- package/src/domain/codegraph.mjs +6 -0
- package/src/domain/dialogue-acts.mjs +155 -0
- package/src/domain/grammar/lexicon-core.json +1 -1
- package/src/domain/interpret/normalize.mjs +14 -2
- package/src/domain/interpret/strategies/keywords.mjs +10 -1
- package/src/domain/memory/trust.mjs +12 -0
- package/src/domain/reference-pack.mjs +72 -0
- package/src/domain/skos-view.mjs +111 -0
- package/src/domain/syllogise.mjs +647 -56
- package/src/services/chat.mjs +499 -123
- package/src/services/init.mjs +2 -2
- package/src/surfaces/web/chat-browser-entry.mjs +87 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +865 -74
- package/src/tools/definitions.mjs +13 -0
- package/src/tools/handlers/index.mjs +2 -0
- package/src/tools/handlers/tmct-related.mjs +30 -0
- package/ROADMAP.md +0 -130
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.3",
|
|
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.",
|
|
@@ -63,7 +63,6 @@
|
|
|
63
63
|
"!src/domain/schemaorg/turtle.mjs",
|
|
64
64
|
"!src/domain/version-stamp.mjs",
|
|
65
65
|
"README.md",
|
|
66
|
-
"ROADMAP.md",
|
|
67
66
|
"LICENSE",
|
|
68
67
|
"corpus/",
|
|
69
68
|
"!corpus/wordnet/generate.mjs",
|
|
@@ -133,19 +132,24 @@
|
|
|
133
132
|
"audit": "npm audit --audit-level=high",
|
|
134
133
|
"audit:fix": "npm audit fix",
|
|
135
134
|
"demo:build": "node scripts/build-demo-site.mjs",
|
|
135
|
+
"roll": "node scripts/roll.mjs",
|
|
136
136
|
"build:ask-bundle": "node scripts/build-ask-bundle.mjs",
|
|
137
|
+
"build:chat-bundle": "node scripts/build-chat-bundle.mjs",
|
|
138
|
+
"build:chat-seed": "node scripts/build-chat-seed.mjs",
|
|
137
139
|
"build:demo-graph": "node scripts/build-demo-graph.mjs",
|
|
140
|
+
"build:demo-pack": "node scripts/build-demo-pack.mjs",
|
|
138
141
|
"build:demo-memory": "node scripts/build-demo-memory.mjs",
|
|
139
142
|
"gen:tool-docs": "node scripts/generate-tool-docs.mjs",
|
|
140
143
|
"gen:collisions": "node scripts/generate-real-word-collisions.mjs",
|
|
141
144
|
"gen:prose-corpus": "node scripts/fetch-prose-corpus.mjs",
|
|
145
|
+
"gen:reference-pack": "node scripts/fetch-reference-pack.mjs",
|
|
142
146
|
"gen:variants": "node scripts/generate-template-variants.mjs",
|
|
143
147
|
"extract": "node bin/tmct.mjs extract",
|
|
144
148
|
"extract:facts": "node bin/tmct.mjs extract"
|
|
145
149
|
},
|
|
146
150
|
"devDependencies": {
|
|
147
|
-
"esbuild": "
|
|
148
|
-
"ink-testing-library": "
|
|
151
|
+
"esbuild": "0.28.1",
|
|
152
|
+
"ink-testing-library": "4.0.0",
|
|
149
153
|
"playwright": "1.61.1",
|
|
150
154
|
"publint": "0.3.21",
|
|
151
155
|
"yaml": "2.9.0"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// corpus/reference-pack.mjs — lazy, failure-tolerated loader for the shipped
|
|
2
|
+
// reference pack (corpus/reference/): a gzipped term index consulted first,
|
|
3
|
+
// then exactly one gzipped JSONL shard per hit. Nothing here ever throws at a
|
|
4
|
+
// caller — an absent, truncated or corrupt pack reads as null, and a null is
|
|
5
|
+
// the ordinary honest miss.
|
|
6
|
+
//
|
|
7
|
+
// The provider seam: registerReferencePackProvider swaps the whole lookup
|
|
8
|
+
// behind one async `{ lookup(normTerm) }` contract. It exists because the
|
|
9
|
+
// browser demo cannot read this filesystem layout — the web surface registers
|
|
10
|
+
// a fetch-backed provider over public/reference-pack/ instead, and chat code
|
|
11
|
+
// never knows which one it is talking to. No provider registered = the fs
|
|
12
|
+
// loader below.
|
|
13
|
+
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
15
|
+
import { gunzipSync } from "node:zlib";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { isReferenceArticleRow, isReferenceIndexEntry } from "../../domain/reference-pack.mjs";
|
|
19
|
+
|
|
20
|
+
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
21
|
+
|
|
22
|
+
/** The pack directory: TMCT_REFERENCE_PACK_DIR when set, else the package's
|
|
23
|
+
* own corpus/reference/. */
|
|
24
|
+
export function referencePackDir(env = process.env) {
|
|
25
|
+
return env?.TMCT_REFERENCE_PACK_DIR || join(PKG_ROOT, "corpus", "reference");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const indexCacheByDir = new Map(); // dir -> { term: {s, t, r} } | null
|
|
29
|
+
const shardCacheByKey = new Map(); // `${dir}\0${shard}` -> Map(term -> row) | null
|
|
30
|
+
|
|
31
|
+
/** Drop every cached index/shard — for tests that mutate a pack dir. */
|
|
32
|
+
export function clearReferencePackCache() {
|
|
33
|
+
indexCacheByDir.clear();
|
|
34
|
+
shardCacheByKey.clear();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readGunzipped(file) {
|
|
38
|
+
try {
|
|
39
|
+
return gunzipSync(readFileSync(file));
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The pack's term index, lazily read and cached per dir; null (cached) when
|
|
46
|
+
* the pack is absent or unreadable. Never throws. */
|
|
47
|
+
export function loadReferenceIndex(dir) {
|
|
48
|
+
if (indexCacheByDir.has(dir)) return indexCacheByDir.get(dir);
|
|
49
|
+
let index = null;
|
|
50
|
+
const body = readGunzipped(join(dir, "index.json.gz"));
|
|
51
|
+
if (body) {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(body.toString("utf8"));
|
|
54
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) index = parsed;
|
|
55
|
+
} catch { /* tolerated: a corrupt index is an absent pack */ }
|
|
56
|
+
}
|
|
57
|
+
indexCacheByDir.set(dir, index);
|
|
58
|
+
return index;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function loadShard(dir, shardName) {
|
|
62
|
+
const key = `${dir}\0${shardName}`;
|
|
63
|
+
if (shardCacheByKey.has(key)) return shardCacheByKey.get(key);
|
|
64
|
+
let rows = null;
|
|
65
|
+
const body = readGunzipped(join(dir, "shards", `${shardName}.jsonl.gz`));
|
|
66
|
+
if (body) {
|
|
67
|
+
rows = new Map();
|
|
68
|
+
for (const line of body.toString("utf8").split("\n")) {
|
|
69
|
+
if (!line.trim()) continue;
|
|
70
|
+
try {
|
|
71
|
+
const row = JSON.parse(line);
|
|
72
|
+
if (isReferenceArticleRow(row)) rows.set(row.term, row);
|
|
73
|
+
} catch { /* tolerated: a bad line loses one row, not the shard */ }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
shardCacheByKey.set(key, rows);
|
|
77
|
+
return rows;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** One article by its normalised term: index hit -> the one shard the index
|
|
81
|
+
* names (cached) -> the row. Null on an index miss, so an unknown term never
|
|
82
|
+
* costs a shard read. Never throws. */
|
|
83
|
+
export function loadReferenceArticle(dir, term) {
|
|
84
|
+
const index = loadReferenceIndex(dir);
|
|
85
|
+
if (!index) return null;
|
|
86
|
+
const entry = index[String(term ?? "")];
|
|
87
|
+
if (!isReferenceIndexEntry(entry)) return null;
|
|
88
|
+
const rows = loadShard(dir, entry.s);
|
|
89
|
+
return rows?.get(entry.t) ?? null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const fsProvider = {
|
|
93
|
+
lookup: async (normTerm) => loadReferenceArticle(referencePackDir(), normTerm),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
let registeredProvider = null;
|
|
97
|
+
|
|
98
|
+
/** Swap the pack lookup: provider = { lookup: async (normTerm) => row|null }.
|
|
99
|
+
* Pass null to restore the default fs loader. */
|
|
100
|
+
export function registerReferencePackProvider(provider) {
|
|
101
|
+
registeredProvider = provider && typeof provider.lookup === "function" ? provider : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The active provider — the registered one, else the lazy fs loader. */
|
|
105
|
+
export function getReferencePackProvider() {
|
|
106
|
+
return registeredProvider ?? fsProvider;
|
|
107
|
+
}
|
|
@@ -79,6 +79,7 @@ const MEMORY_VOCABULARY = [
|
|
|
79
79
|
{ prop: "rdf:object", note: "reified fact: the triple's object term" },
|
|
80
80
|
{ prop: "mgx:factProvenance", note: "LEGACY COMPAT SHIM: the ' | '-joined provenance tag string a fact came from; the source-of-truth is now the mgx:statedBy edges derived from it" },
|
|
81
81
|
{ prop: "mgx:factQuantifier", note: "OPTIONAL: the quantifier word a plural class-membership teach used ('every'/'some'/'a few'), for literal recall by 'how many Xs are Ys' — never real cardinality counting" },
|
|
82
|
+
{ prop: "mgx:factJustification", note: "an entailed Fact's supporting premise fact ids: ' | '-separated environments, one space-separated premise-id list per independent derivation, capped by syllogise's maxEnvironments knob; a value with no ' | ' is a single environment" },
|
|
82
83
|
{ prop: "mgx:ruleName", note: "a taught Rule's own name (e.g. 'grandparent') — the query-dispatcher's lookup key, PLAN_TAUGHT_RELATIONS.md §2/§3" },
|
|
83
84
|
{ prop: "mgx:ruleKind", note: "a taught Rule's SHAPE tag — the closed vocabulary compose2 | filter | recursive (structural, like 'Fact'/'Rule' themselves, never a domain word)" },
|
|
84
85
|
{ prop: "mgx:ruleBase1", note: "compose2: the first hop's base relation name; filter: the base rule/relation being filtered (same 'base relation' role in both kinds, so the name is shared)" },
|
|
@@ -91,7 +92,7 @@ const MEMORY_VOCABULARY = [
|
|
|
91
92
|
{ prop: DERIVED_FROM_PROP, predicate: "derivedFrom", note: "umbrella: a Fact derived from a Source (or another Fact). ext ref prov:wasDerivedFrom (UNVERIFIED-pending-web-check)" },
|
|
92
93
|
{ prop: STATED_BY_PROP, predicate: "statedBy", note: "subPropertyOf derivedFrom: a Source directly asserts this Fact (one edge per independent source — replaces the factProvenance union)" },
|
|
93
94
|
{ prop: CANONICALISED_FROM_PROP, predicate: "canonicalisedFrom", note: "subPropertyOf derivedFrom: a canonical Fact cleaned from a raw Block/Source, never replacing it" },
|
|
94
|
-
{ prop: "mgx:sourceType", note: "a Source's kind: operator | teach | provider | corpus | corpusWeak | extracted | web | entailed (the trust-prior key)" },
|
|
95
|
+
{ prop: "mgx:sourceType", note: "a Source's kind: operator | teach | provider | corpus | corpusWeak | reference | extracted | web | entailed (the trust-prior key)" },
|
|
95
96
|
{ prop: "mgx:sourceUrl", note: "a web Source's URL" },
|
|
96
97
|
{ prop: "mgx:sourceRule", note: "an entailed Source's rule id" },
|
|
97
98
|
{ prop: "mgx:sourceReliability", note: "actor-level (session-scoped) trust nudge in [0.5,1.5], neutral 1.0 when absent — materialised by recomputeSourceReliability from a session's asserted-vs-contradicted track record (memory/trust.mjs's sessionReliabilityFrom); folds into computeTrust's per-source prior" },
|
|
@@ -576,7 +577,14 @@ function migrateLegacyFactIds(payload) {
|
|
|
576
577
|
for (const ind of payload.individuals) {
|
|
577
578
|
if (Array.isArray(ind?.derived_from) && ind.derived_from.length) ind.derived_from = ind.derived_from.map(remapId);
|
|
578
579
|
const just = (ind?.attributes || []).find((a) => a?.prop === "mgx:factJustification");
|
|
579
|
-
if (just?.value)
|
|
580
|
+
if (just?.value) {
|
|
581
|
+
// Environment-aware: the value is ' | '-separated premise-id lists, one
|
|
582
|
+
// per independent derivation — remap the ids inside each, keep the shape.
|
|
583
|
+
just.value = just.value.split(" | ")
|
|
584
|
+
.map((env) => env.split(" ").filter(Boolean).map(remapId).join(" "))
|
|
585
|
+
.filter(Boolean)
|
|
586
|
+
.join(" | ");
|
|
587
|
+
}
|
|
580
588
|
}
|
|
581
589
|
return payload;
|
|
582
590
|
}
|
|
@@ -591,6 +599,43 @@ async function persistMemory(dir, payload) {
|
|
|
591
599
|
await atomicWriteJson(memoryGraphFile(dir), payload);
|
|
592
600
|
}
|
|
593
601
|
|
|
602
|
+
// ---- Syllogise watermark state: a small backend-dispatched sidecar ---------
|
|
603
|
+
// { version, factIds, completedAt } — the fact ids at the end of the last
|
|
604
|
+
// COMPLETE syllogise pass. syllogise() diffs it against the live store to
|
|
605
|
+
// pick delta or full evaluation; a missing/removed-id state means full.
|
|
606
|
+
|
|
607
|
+
export const SYLLOGISE_STATE_REL = join(MEMORY_DIR_REL, "syllogise-state.json");
|
|
608
|
+
const SQLITE_SYLLOGISE_STATE_KEY = "syllogiseState";
|
|
609
|
+
|
|
610
|
+
/** Load the syllogise watermark for a repo dir OR a Backend B/C handle.
|
|
611
|
+
* Null when no complete pass has recorded one. */
|
|
612
|
+
export async function loadSyllogiseState(dir) {
|
|
613
|
+
if (isMemoryHandle(dir)) return dir.syllogiseState ? structuredClone(dir.syllogiseState) : null;
|
|
614
|
+
if (isSqliteHandle(dir)) {
|
|
615
|
+
const row = dir.db.prepare("SELECT v FROM meta WHERE k = ?").get(SQLITE_SYLLOGISE_STATE_KEY);
|
|
616
|
+
return row?.v ? JSON.parse(row.v) : null;
|
|
617
|
+
}
|
|
618
|
+
try {
|
|
619
|
+
return JSON.parse(await readFile(join(dir, SYLLOGISE_STATE_REL), "utf8"));
|
|
620
|
+
} catch (e) {
|
|
621
|
+
if (e?.code === "ENOENT") return null;
|
|
622
|
+
throw e;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** Persist the syllogise watermark — atomic file write (Backend A), a cloned
|
|
627
|
+
* handle field (Backend B), or a meta-table row (Backend C). */
|
|
628
|
+
export async function saveSyllogiseState(dir, state) {
|
|
629
|
+
if (isMemoryHandle(dir)) { dir.syllogiseState = structuredClone(state); return; }
|
|
630
|
+
if (isSqliteHandle(dir)) {
|
|
631
|
+
dir.db.prepare("INSERT OR REPLACE INTO meta(k, v) VALUES (?, ?)").run(SQLITE_SYLLOGISE_STATE_KEY, JSON.stringify(state));
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const file = join(dir, SYLLOGISE_STATE_REL);
|
|
635
|
+
await mkdir(dirname(file), { recursive: true });
|
|
636
|
+
await atomicWriteJson(file, state);
|
|
637
|
+
}
|
|
638
|
+
|
|
594
639
|
/** Fresh read -> mutate -> atomic write. Serialized per call; every public
|
|
595
640
|
* append goes through here, including the lazy legacy-provenance migration
|
|
596
641
|
* and actor-level Source reliability recompute. `fn` may be async (the
|
|
@@ -667,6 +712,9 @@ function sourceIdFor(desc) {
|
|
|
667
712
|
case "teach": return { id: desc.sessionId ? `${TEACH_SOURCE_ID}:${desc.sessionId}` : TEACH_SOURCE_ID, type: "teach" };
|
|
668
713
|
case "provider": return { id: `src:provider:${desc.name}`, type: "provider" };
|
|
669
714
|
case "corpus": return { id: `src:corpus:${desc.name}`, type: "corpus" };
|
|
715
|
+
// One Source per pack article (the @revid stays in the article segment),
|
|
716
|
+
// so two facts from the same article corroborate nothing extra.
|
|
717
|
+
case "reference": return { id: `src:reference:${desc.pack}:${desc.article}`, type: "reference" };
|
|
670
718
|
// One Source per source-file basename, not per extraction run.
|
|
671
719
|
case "extracted": return { id: `src:extracted:${desc.name}`, type: "extracted" };
|
|
672
720
|
case "web": return { id: `src:learned:web:${fnv1aHex(String(desc.url || ""))}`, type: "web", url: String(desc.url || "") };
|
|
@@ -688,6 +736,7 @@ const PROV_CLASS_BY_SOURCE_TYPE = Object.freeze({
|
|
|
688
736
|
provider: { subClass: "tmct:AgentSource", prov: "prov:Agent" },
|
|
689
737
|
corpus: { subClass: "tmct:DocumentSource", prov: "prov:Entity" },
|
|
690
738
|
corpusWeak: { subClass: "tmct:DocumentSource", prov: "prov:Entity" },
|
|
739
|
+
reference: { subClass: "tmct:DocumentSource", prov: "prov:Entity" },
|
|
691
740
|
web: { subClass: "tmct:DocumentSource", prov: "prov:Entity" },
|
|
692
741
|
extracted: { subClass: "tmct:DocumentSource", prov: "prov:Entity" },
|
|
693
742
|
entailed: { subClass: "tmct:ActivitySource", prov: "prov:Activity" },
|
|
@@ -1074,13 +1123,38 @@ export async function appendFact(dir, { subject, predicate, object, provenance =
|
|
|
1074
1123
|
return { id };
|
|
1075
1124
|
}
|
|
1076
1125
|
|
|
1126
|
+
/** Normalize appendFacts' `justification` input — either a flat premise-id
|
|
1127
|
+
* list (one derivation) or a list of premise-id lists (one per independent
|
|
1128
|
+
* derivation) — into the string[][] environment shape: empty/non-string ids
|
|
1129
|
+
* dropped, environments deduped by canonical key (sorted-id join) with
|
|
1130
|
+
* within-environment citation order preserved. Undefined when nothing
|
|
1131
|
+
* storable remains. */
|
|
1132
|
+
function normalizeJustificationEnvironments(justification) {
|
|
1133
|
+
if (!Array.isArray(justification)) return undefined;
|
|
1134
|
+
const rawEnvs = justification.some(Array.isArray)
|
|
1135
|
+
? justification.filter(Array.isArray)
|
|
1136
|
+
: [justification];
|
|
1137
|
+
const envs = [];
|
|
1138
|
+
const seen = new Set();
|
|
1139
|
+
for (const raw of rawEnvs) {
|
|
1140
|
+
const env = raw.filter((id) => typeof id === "string" && id);
|
|
1141
|
+
if (!env.length) continue;
|
|
1142
|
+
const key = [...env].sort().join(" ");
|
|
1143
|
+
if (seen.has(key)) continue;
|
|
1144
|
+
seen.add(key);
|
|
1145
|
+
envs.push(env);
|
|
1146
|
+
}
|
|
1147
|
+
return envs.length ? envs : undefined;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1077
1150
|
/** Batch append of grammar/corpus-derived triples — ONE read-modify-write for
|
|
1078
1151
|
* a whole seed, collapsing looping appendFact's O(N²) I/O to a single
|
|
1079
1152
|
* mutate (same resulting ids/provenance/trust). Malformed facts are skipped,
|
|
1080
1153
|
* not thrown. Optional per-fact `premiseTrusts`/`ruleConfidence` (batched
|
|
1081
|
-
* entailed-hook passthrough) and `justification` (premise fact ids
|
|
1082
|
-
*
|
|
1083
|
-
*
|
|
1154
|
+
* entailed-hook passthrough) and `justification` (premise fact ids — a flat
|
|
1155
|
+
* list, or a list of lists for multiple independent derivations — stored as
|
|
1156
|
+
* mgx:factJustification's ' | '-separated environments, last-write-wins).
|
|
1157
|
+
* Returns { ids, appended, skipped }. */
|
|
1084
1158
|
export async function appendFacts(dir, facts) {
|
|
1085
1159
|
const prepared = [];
|
|
1086
1160
|
let skipped = 0;
|
|
@@ -1099,7 +1173,7 @@ export async function appendFacts(dir, facts) {
|
|
|
1099
1173
|
quantifier: normText(f?.quantifier),
|
|
1100
1174
|
premiseTrusts: Array.isArray(f?.premiseTrusts) ? f.premiseTrusts : undefined,
|
|
1101
1175
|
ruleConfidence: typeof f?.ruleConfidence === "number" ? f.ruleConfidence : undefined,
|
|
1102
|
-
|
|
1176
|
+
environments: normalizeJustificationEnvironments(f?.justification),
|
|
1103
1177
|
});
|
|
1104
1178
|
}
|
|
1105
1179
|
const ids = [];
|
|
@@ -1138,7 +1212,7 @@ export async function appendFacts(dir, facts) {
|
|
|
1138
1212
|
...(provs.length ? [{ prop: "mgx:factProvenance", key: "provenance", value: provs.join(" | ") }] : []),
|
|
1139
1213
|
...(f.tokens.length ? [{ prop: "mgx:hasProseTokens", key: "prose_tokens", value: f.tokens.join(" ") }] : []),
|
|
1140
1214
|
...(qVal ? [{ prop: "mgx:factQuantifier", key: "quantifier", value: qVal }] : []),
|
|
1141
|
-
...(f.
|
|
1215
|
+
...(f.environments ? [{ prop: "mgx:factJustification", key: "justification", value: f.environments.map((e) => e.join(" ")).join(" | ") }] : []),
|
|
1142
1216
|
],
|
|
1143
1217
|
};
|
|
1144
1218
|
// Upsert via the shared helper — O(1) via the index (Object.assign in
|
|
@@ -1545,6 +1619,24 @@ export function readFactRows(memory) {
|
|
|
1545
1619
|
.map((id) => (sourcesById.get(id)?.attributes || []).find((a) => a?.prop === "mgx:sourceType")?.value)
|
|
1546
1620
|
.filter(Boolean);
|
|
1547
1621
|
const justificationRaw = get("justification");
|
|
1622
|
+
// ' | '-separated environments, one premise-id list per independent
|
|
1623
|
+
// derivation; a legacy value with no ' | ' parses as one environment.
|
|
1624
|
+
const environments = [];
|
|
1625
|
+
if (justificationRaw) {
|
|
1626
|
+
for (const chunk of justificationRaw.split(" | ")) {
|
|
1627
|
+
const env = chunk.split(" ").filter(Boolean);
|
|
1628
|
+
if (env.length) environments.push(env);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
const justification = [];
|
|
1632
|
+
const seenPremise = new Set();
|
|
1633
|
+
for (const env of environments) {
|
|
1634
|
+
for (const id of env) {
|
|
1635
|
+
if (seenPremise.has(id)) continue;
|
|
1636
|
+
seenPremise.add(id);
|
|
1637
|
+
justification.push(id);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1548
1640
|
rows.push({
|
|
1549
1641
|
id: ind.id,
|
|
1550
1642
|
subject: get("subject"), predicate: get("predicate"), object: get("object"),
|
|
@@ -1552,9 +1644,11 @@ export function readFactRows(memory) {
|
|
|
1552
1644
|
quantifier: get("quantifier"), // "" unless a plural class-membership teach set one
|
|
1553
1645
|
sourceIds, sourceTypes,
|
|
1554
1646
|
trust: Number((ind.attributes || []).find((a) => a?.prop === TRUST_SCORE_PROP)?.value) || 0,
|
|
1555
|
-
//
|
|
1556
|
-
//
|
|
1557
|
-
|
|
1647
|
+
// `environments`: every persisted premise set (empty unless entailed);
|
|
1648
|
+
// `justification`: their deduped union in first-occurrence order, for
|
|
1649
|
+
// readers that only need "which premises does this fact cite at all".
|
|
1650
|
+
environments,
|
|
1651
|
+
justification,
|
|
1558
1652
|
});
|
|
1559
1653
|
}
|
|
1560
1654
|
return rows;
|
package/src/domain/ask.mjs
CHANGED
|
@@ -74,6 +74,7 @@ const FINE_CLASS_SIBLING = { Function: "Method", Method: "Function" };
|
|
|
74
74
|
// + calls together).
|
|
75
75
|
const KIND_UNIONS = { uses: ["imports", "calls", "callsSymbol"] };
|
|
76
76
|
const kindsFor = (kind) => KIND_UNIONS[kind] || [kind];
|
|
77
|
+
const SYMBOL_GRAIN_KINDS = new Set(Object.values(SYMBOL_GRAIN_SIBLING));
|
|
77
78
|
|
|
78
79
|
const OVERFLOW_CAP = 12;
|
|
79
80
|
|
|
@@ -377,9 +378,17 @@ function classesForKinds(graph, kinds) {
|
|
|
377
378
|
/** The single OBJECT class a forward relation kind points at across the loaded graph
|
|
378
379
|
* (imports → Module), or null when its objects span more than one class (an ambiguous
|
|
379
380
|
* grain the complement's universe can't be pinned to). Used by the forwardComplement
|
|
380
|
-
* evaluator to bound the universe it differences the positive forward set out of.
|
|
381
|
+
* evaluator to bound the universe it differences the positive forward set out of.
|
|
382
|
+
*
|
|
383
|
+
* A union kind's symbol-grain member (callsSymbol) points at symbols by design —
|
|
384
|
+
* the fine view, served by its own reverse branch. The union's canonical object
|
|
385
|
+
* grain is defined by its coarse members, so the vote excludes the symbol-grain
|
|
386
|
+
* siblings; without this "uses" collapses to null and a Class-resolved object
|
|
387
|
+
* never up-refines to its containing module. */
|
|
381
388
|
function kindObjectClass(graph, kind) {
|
|
382
|
-
const
|
|
389
|
+
const kinds = kindsFor(kind);
|
|
390
|
+
const coarse = kinds.filter((k) => !SYMBOL_GRAIN_KINDS.has(k));
|
|
391
|
+
const classes = classesForKinds(graph, coarse.length ? coarse : kinds);
|
|
383
392
|
return classes.size === 1 ? [...classes][0] : null;
|
|
384
393
|
}
|
|
385
394
|
|
|
@@ -886,7 +895,14 @@ function parseSuperlative(w, lc, nlp) {
|
|
|
886
895
|
if (!entityType) return { node: "miss", reason: "a superlative needs an entity kind (module, class, function, …)" };
|
|
887
896
|
}
|
|
888
897
|
if (!metric) return { node: "miss", reason: "name what to rank by (imports, callers, methods, tests, or connections)" };
|
|
889
|
-
|
|
898
|
+
// A need/lack verb measures the ABSENCE of the metric, so it inverts the
|
|
899
|
+
// ranking direction: "what most needs a test" asks for the FEWEST tests,
|
|
900
|
+
// and answering the most-tested module is the exact inverse of the question.
|
|
901
|
+
const NEED_LACK_WORDS = ["needs", "need", "needing", "lacks", "lack", "lacking", "misses", "missing"];
|
|
902
|
+
const extreme = lc.some((x) => NEED_LACK_WORDS.includes(x))
|
|
903
|
+
? (ext === "most" ? "fewest" : "most")
|
|
904
|
+
: ext;
|
|
905
|
+
return { node: "superlative", entityType, metric, metricNoun, extreme };
|
|
890
906
|
}
|
|
891
907
|
|
|
892
908
|
// Predicate-find: "find me the payment class" (trailing-type) or "find the
|
|
@@ -2536,9 +2552,27 @@ function unplacedTermWords(term, label) {
|
|
|
2536
2552
|
*
|
|
2537
2553
|
* Tier 3 only: the prose and fuzzy tiers resolve BY not matching the label
|
|
2538
2554
|
* (doc-comment words, a typo'd spelling), and announce themselves in the
|
|
2539
|
-
* answer where tier 3 says nothing.
|
|
2555
|
+
* answer where tier 3 says nothing.
|
|
2556
|
+
*
|
|
2557
|
+
* The AMBIGUOUS case is the same failure with several candidates instead of
|
|
2558
|
+
* one: a tie spread over candidates that ALL leave the same words unread is
|
|
2559
|
+
* not a real ambiguity between readings of the question — it is the question
|
|
2560
|
+
* not matching, several ways at once, and enumerating every candidate would
|
|
2561
|
+
* answer each of them to a question none of them is. A word placed by ANY
|
|
2562
|
+
* candidate keeps the honest ambiguity (that candidate may be the one meant). */
|
|
2540
2563
|
function declineOnUnplacedWords(result, term) {
|
|
2541
|
-
if (!result?.match || result.
|
|
2564
|
+
if (!result?.match || result.tier !== 3 || result.matchedVia) return result;
|
|
2565
|
+
if (result.ambiguous) {
|
|
2566
|
+
const pool = [result.match, ...(result.candidates || [])];
|
|
2567
|
+
const shared = pool
|
|
2568
|
+
.map((m) => new Set(unplacedTermWords(term, m.label)))
|
|
2569
|
+
.reduce((acc, s) => acc.filter((w) => s.has(w)), unplacedTermWords(term, pool[0].label));
|
|
2570
|
+
if (!shared.length) return result;
|
|
2571
|
+
return {
|
|
2572
|
+
match: null, candidates: [], tier: null, ambiguous: false,
|
|
2573
|
+
unplacedWords: shared, nearestLabel: listJoin(pool.slice(0, 4).map((m) => m.label)),
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2542
2576
|
const unplaced = unplacedTermWords(term, result.match.label);
|
|
2543
2577
|
if (!unplaced.length) return result;
|
|
2544
2578
|
return {
|
|
@@ -2998,8 +3032,11 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
|
|
|
2998
3032
|
// symbol-grain sibling reads off the sibling when a fine subject grain was
|
|
2999
3033
|
// asked for, or when the resolved object is itself a fine symbol — the
|
|
3000
3034
|
// module-coarse edge can never point at a function/method, so a bare "what
|
|
3001
|
-
// calls fnAlpha" would otherwise return a false empty.
|
|
3002
|
-
|
|
3035
|
+
// calls fnAlpha" would otherwise return a false empty. A union kind's own
|
|
3036
|
+
// symbol-grain member ("uses" carries callsSymbol) plays the same sibling
|
|
3037
|
+
// role here, so "what uses <symbol>" walks the same grain ladder as "what
|
|
3038
|
+
// calls <symbol>" instead of skipping it.
|
|
3039
|
+
const symbolKind = SYMBOL_GRAIN_SIBLING[kind] || kindsFor(kind).find((k) => SYMBOL_GRAIN_KINDS.has(k)) || null;
|
|
3003
3040
|
const objIsFineSymbol = !!(objMatch.class && FINE_ENTITY_TYPES.has(objMatch.class));
|
|
3004
3041
|
if (symbolKind && (FINE_ENTITY_TYPES.has(entityType) || objIsFineSymbol)) {
|
|
3005
3042
|
const edges = edgesOfKind(graph, symbolKind).filter((e) => e.object === objMatch.id);
|
|
@@ -3022,7 +3059,7 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
|
|
|
3022
3059
|
// rather than a confident-looking-but-wrong "nothing touched/calls it".
|
|
3023
3060
|
// Not widened to Function/Method — symbol-level counting precision there
|
|
3024
3061
|
// is a separate, deliberate guarantee this must not erode.
|
|
3025
|
-
const upRefineEligible = (kind === "touches" || kind === "calls") && objMatch.class === "Class"
|
|
3062
|
+
const upRefineEligible = (kind === "touches" || kind === "calls" || kind === "uses") && objMatch.class === "Class"
|
|
3026
3063
|
&& !edgesOfKind(graph, "contains").some((e) => e.subject === objMatch.id);
|
|
3027
3064
|
const upRefineModule = upRefineEligible ? graph.byId.get(moduleIdOf(graph, objMatch) || "") : null;
|
|
3028
3065
|
if (matches.length || !(upRefineEligible && upRefineModule)) {
|
|
@@ -3538,6 +3575,20 @@ function renderCore(parsed, result, graph) {
|
|
|
3538
3575
|
// Forward: parsed.object is the given subject, not a search target, so it
|
|
3539
3576
|
// gets its own subject-first phrasing rather than reverse's template.
|
|
3540
3577
|
if (parsed.shape === "forward") {
|
|
3578
|
+
// The index stores membership on either contains (Class -> member) or
|
|
3579
|
+
// defines (Module -> symbol), and MEMBERSHIP_KINDS declares the two
|
|
3580
|
+
// equivalent for a members-of question — so before reporting "no
|
|
3581
|
+
// contains edges" for a subject whose members live on defines, the
|
|
3582
|
+
// sibling kind is consulted over the SAME resolved subject. Adopted
|
|
3583
|
+
// only on a real match; anything else keeps the honest empty.
|
|
3584
|
+
if (MEMBERSHIP_KINDS.includes(parsed.kind) && graph && result.objMatch) {
|
|
3585
|
+
for (const alt of MEMBERSHIP_KINDS) {
|
|
3586
|
+
if (alt === parsed.kind) continue;
|
|
3587
|
+
const altParsed = { ...parsed, kind: alt };
|
|
3588
|
+
const altResult = traverse(graph, altParsed, { pinnedObjMatch: result.objMatch });
|
|
3589
|
+
if (altResult?.matches?.length && !altResult.ambiguous) return renderCore(altParsed, altResult, graph);
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3541
3592
|
return {
|
|
3542
3593
|
content: `${result.objMatch.label} has no ${verbFor(parsed.kind)} edges in the index.`,
|
|
3543
3594
|
miss: true, ambiguous: false,
|
|
@@ -3636,6 +3687,12 @@ const CASCADE_FUZZY_TARGETS = [...new Set([
|
|
|
3636
3687
|
...TRIGGER_FUZZY_WORDS,
|
|
3637
3688
|
])].filter((wd) => /^[a-z]+$/.test(wd) && wd.length >= 4 && !STOPWORDS.has(wd));
|
|
3638
3689
|
|
|
3690
|
+
/** Real words that carry their own intent and are never a typo of the closed
|
|
3691
|
+
* vocabulary: the corrector must not rewrite them at all. "impact" sits two
|
|
3692
|
+
* edits from "import", and rewriting it answers the reverse question in the
|
|
3693
|
+
* asker's own words — an unrecognised impact phrasing must decline instead. */
|
|
3694
|
+
const CASCADE_FUZZY_REAL_WORDS = new Set(["impact", "impacts", "impacted"]);
|
|
3695
|
+
|
|
3639
3696
|
/** Unique within-bound fuzzy correction of `w` toward CASCADE_FUZZY_TARGETS,
|
|
3640
3697
|
* or null — a distance tie between two distinct targets is refused. The
|
|
3641
3698
|
* 4-char floor holds on the word being corrected as well as on the targets:
|
|
@@ -3643,7 +3700,7 @@ const CASCADE_FUZZY_TARGETS = [...new Set([
|
|
|
3643
3700
|
* were never a typo of it ("old" -> "hold"), and the correction is announced
|
|
3644
3701
|
* as though the asker had typed it. */
|
|
3645
3702
|
function fuzzyCascadeWord(w) {
|
|
3646
|
-
if (w.length < 4) return null;
|
|
3703
|
+
if (w.length < 4 || CASCADE_FUZZY_REAL_WORDS.has(w)) return null;
|
|
3647
3704
|
const bound = fuzzyBound(w);
|
|
3648
3705
|
let best = bound + 1; let hit = null; let tied = false;
|
|
3649
3706
|
for (const target of CASCADE_FUZZY_TARGETS) {
|
package/src/domain/cli-verbs.mjs
CHANGED
|
@@ -104,10 +104,10 @@ export const CLI_VERBS = [
|
|
|
104
104
|
mode: "syllogise",
|
|
105
105
|
errorLabel: "syllogise",
|
|
106
106
|
usage: "tmct syllogise [--repo <abs>]",
|
|
107
|
-
prose: ["speculative inference (offline maintenance job):
|
|
107
|
+
prose: ["speculative inference (offline maintenance job): a deterministic"],
|
|
108
108
|
flags: [
|
|
109
|
-
{ flag: "[--depth <n>] [--budget <n>]", prose: ["
|
|
110
|
-
{ flag: "[--config <path>]", prose: ["bounded, low-trust, retractable entailed facts (never on the chat path)"] },
|
|
109
|
+
{ flag: "[--depth <n>] [--budget <n>]", prose: ["forward-chaining materialisation over OWL 2 RL rule kernels"] },
|
|
110
|
+
{ flag: "[--config <path>]", prose: ["(the classical syllogism among them), writing bounded, low-trust,", "retractable entailed facts (never on the chat path)"] },
|
|
111
111
|
],
|
|
112
112
|
},
|
|
113
113
|
{
|
package/src/domain/codegraph.mjs
CHANGED
|
@@ -362,6 +362,12 @@ export function impactClosure(graph, ind, { maxDepth = 8 } = {}) {
|
|
|
362
362
|
if (!subjModId || !objModId) continue;
|
|
363
363
|
const subjLabel = graph.byId.get(subjModId)?.label || subjModId;
|
|
364
364
|
addDependent(objModId, subjModId, subjLabel, g.predicate);
|
|
365
|
+
// Keyed under the callee's SYMBOL id too: an impact query resolving a
|
|
366
|
+
// function name seeds the BFS from the symbol, and the module-coarse
|
|
367
|
+
// key above is invisible from there — the caller's module was a real
|
|
368
|
+
// dependent that read back as "no dependents found". Same-module
|
|
369
|
+
// calls stay skipped, mirroring the self-skip on the coarse key.
|
|
370
|
+
if (subjModId !== objModId) addDependent(e.object, subjModId, subjLabel, g.predicate);
|
|
365
371
|
}
|
|
366
372
|
} else if (kind === "tests") {
|
|
367
373
|
for (const e of g.edges) {
|