@polycode-projects/the-mechanical-code-talker 2.3.1 → 2.5.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 +131 -32
- package/bin/tmct.mjs +18 -91
- package/corpus/README.md +3 -3
- package/corpus/seon/README.md +1 -0
- package/corpus/tier2/generate.mjs +18 -18
- package/corpus/tier2/manifest.json +3 -3
- package/data/games/hanoi-3.txt +8 -2
- package/package.json +26 -8
- package/src/adapters/corpus-lanes.mjs +13 -0
- package/src/adapters/graph-build.mjs +5 -7
- package/src/adapters/import-closure.mjs +28 -0
- package/src/adapters/memory/blocks.mjs +5 -4
- package/src/adapters/memory/core.mjs +78 -5
- package/src/adapters/memory/shacl.mjs +12 -0
- package/src/adapters/providers/graph-service.mjs +12 -5
- package/src/adapters/tracked-files.mjs +17 -0
- package/src/domain/ask-vocab.mjs +2 -0
- package/src/domain/ask.mjs +225 -13
- package/src/domain/cli-verbs.mjs +201 -0
- package/src/domain/codegraph.mjs +142 -56
- package/src/domain/completions/graph-adapter.mjs +1 -1
- package/src/domain/completions/group.mjs +3 -17
- package/src/domain/completions/infer.mjs +4 -13
- package/src/domain/completions/rank.mjs +6 -19
- package/src/domain/grammar/lexicon-core.json +1 -1
- package/src/domain/hash.mjs +36 -13
- package/src/domain/interpret/fuzzy.mjs +7 -2
- package/src/domain/interpret/normalize.mjs +9 -0
- package/src/domain/interpret/strategies/keywords.mjs +19 -9
- package/src/domain/memory/capability.mjs +22 -3
- package/src/domain/memory/touched-facts.mjs +17 -0
- package/src/domain/module-paths.mjs +9 -0
- package/src/domain/persona/tiers.mjs +1 -1
- package/src/domain/planning.mjs +37 -0
- package/src/domain/prose.mjs +10 -2
- package/src/domain/relative-specifiers.mjs +12 -0
- package/src/domain/router/registry.mjs +3 -2
- package/src/domain/router/results.mjs +5 -18
- package/src/domain/seeded-random.mjs +33 -0
- package/src/domain/syllogise.mjs +10 -7
- package/src/domain/text-stats.mjs +31 -0
- package/src/services/chat.mjs +722 -184
- package/src/services/extract-facts.mjs +155 -0
- package/src/services/import-file.mjs +2 -2
- package/src/services/init.mjs +2 -2
- package/src/services/ledger-viz.mjs +6 -1
- package/src/services/sentences.mjs +26 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +11390 -360
- package/src/tools/graph-load.mjs +7 -1
- package/src/tools/readme-docs.mjs +113 -0
- package/src/tools/schema-docs.mjs +2 -2
- package/ROADMAP.md +0 -129
- package/corpus/namenet/generate.mjs +0 -309
- package/corpus/wordnet/generate.mjs +0 -332
- package/src/adapters/prose-tokens.mjs +0 -98
- package/src/adapters/wordnet-source.mjs +0 -70
- package/src/domain/corpus-matrix.mjs +0 -87
- package/src/domain/inflect.mjs +0 -67
- package/src/domain/licences.mjs +0 -68
- package/src/domain/markdown-links.mjs +0 -55
- package/src/domain/persona/codegen.mjs +0 -123
- package/src/domain/publish-gate.mjs +0 -41
- package/src/domain/schemaorg/turtle.mjs +0 -25
- package/src/domain/semcor/parse.mjs +0 -87
- package/src/domain/version-stamp.mjs +0 -36
- package/src/domain/wordnet/yaml.mjs +0 -133
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// `tmct extract` — turn a plain text file into facts by reusing the SAME
|
|
2
|
+
// deterministic recognizer the interactive chat's "teach" lane already has
|
|
3
|
+
// (runTurn, src/services/chat.mjs) — no new NLU, no LLM, no guessing.
|
|
4
|
+
//
|
|
5
|
+
// tmct extract <text-file> [--repo <path>] [--out <file.jsonl>]
|
|
6
|
+
//
|
|
7
|
+
// The text file is named positionally, or with --file, the way `tmct import`
|
|
8
|
+
// names one.
|
|
9
|
+
//
|
|
10
|
+
// How it works: the file is split into sentences with wink-nlp's own
|
|
11
|
+
// sentence-boundary detection (src/adapters/wink-model.mjs — the same leaf loader
|
|
12
|
+
// src/adapters/ask-nlp.mjs/src/adapters/prose-nlp.mjs already use; never a naive regex split).
|
|
13
|
+
// Each sentence is fed through runTurn() exactly as if an operator had typed
|
|
14
|
+
// it into the live chat. A sentence the recognizer turns into a stored fact
|
|
15
|
+
// (record.via === "assert", record.miss === false) is kept; every other
|
|
16
|
+
// sentence — a question, a fragment, a scene-setting clause, anything
|
|
17
|
+
// outside the recognized teach-frame surface — is silently SKIPPED. This is
|
|
18
|
+
// an honest partial extraction, an "attempt", never full NLU: nothing here
|
|
19
|
+
// ever paraphrases or invents a fact the recognizer itself didn't produce.
|
|
20
|
+
//
|
|
21
|
+
// --repo <path> write straight into that repo's own tmct memory (runTurn's
|
|
22
|
+
// normal memoryDir write path — "grow my own tmct memory
|
|
23
|
+
// from a document").
|
|
24
|
+
// (no --repo) nothing on disk is mutated. Each sentence runs against an
|
|
25
|
+
// ephemeral scratch memory dir (deleted when the run ends);
|
|
26
|
+
// whatever gets recognized is printed as ConceptNet/tmct-
|
|
27
|
+
// shape JSONL ({subject, predicate, object, provenance}) —
|
|
28
|
+
// to stdout, or to --out <file.jsonl> if given.
|
|
29
|
+
//
|
|
30
|
+
// Every recognized fact ALSO gets a second, additive provenance tag —
|
|
31
|
+
// extracted:<source-file-basename> — layered on top of whatever the
|
|
32
|
+
// recognizer itself already wrote (ace:chat:…/teach:chat:…), via appendFact's
|
|
33
|
+
// existing provenance UNION (memory/core.mjs). That keeps an extracted fact
|
|
34
|
+
// auditable as "this document evidenced this claim", distinct from live
|
|
35
|
+
// operator speech or a curated corpus, at its own trust-prior tier
|
|
36
|
+
// (SOURCE_PRIOR.extracted, memory/trust.mjs — see that file's comment for
|
|
37
|
+
// the reasoning: same closed-set recognizer as `teach`, but an unvetted
|
|
38
|
+
// source document, so it sits just above `web`).
|
|
39
|
+
//
|
|
40
|
+
// Never claims full coverage: the summary this prints always states how many
|
|
41
|
+
// sentences were found, how many were recognized, and how many were honestly
|
|
42
|
+
// skipped.
|
|
43
|
+
|
|
44
|
+
import { readFile, writeFile, mkdtemp, rm } from "node:fs/promises";
|
|
45
|
+
import { tmpdir } from "node:os";
|
|
46
|
+
import { basename, join, resolve } from "node:path";
|
|
47
|
+
|
|
48
|
+
import { runTurn, uuidv7 } from "./chat.mjs";
|
|
49
|
+
import { splitSentencesPreservingPaths } from "./sentences.mjs";
|
|
50
|
+
import { loadMemory, readFactRows, appendFact } from "../adapters/memory/core.mjs";
|
|
51
|
+
import { loadConfig } from "../adapters/config.mjs";
|
|
52
|
+
import { touchedFactRows } from "../domain/memory/touched-facts.mjs";
|
|
53
|
+
|
|
54
|
+
export const USAGE = "usage: tmct extract <text-file>|--file <text-file> [--repo <path>] [--out <file.jsonl>]";
|
|
55
|
+
|
|
56
|
+
export function parseArgs(argv) {
|
|
57
|
+
const args = { file: null, repo: null, out: null };
|
|
58
|
+
const rest = [];
|
|
59
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
60
|
+
const a = argv[i];
|
|
61
|
+
if (a === "--repo") args.repo = argv[i += 1];
|
|
62
|
+
else if (a === "--out") args.out = argv[i += 1];
|
|
63
|
+
else if (a === "--file") args.file = argv[i += 1];
|
|
64
|
+
else rest.push(a);
|
|
65
|
+
}
|
|
66
|
+
args.file = args.file || rest[0] || null;
|
|
67
|
+
return args;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Run one already-split sentence through runTurn against `memoryDir`, and
|
|
72
|
+
* report the Fact rows THIS turn actually touched. Returns { recognized, rows }
|
|
73
|
+
* — `recognized` true iff runTurn's own record called this a stored assertion,
|
|
74
|
+
* `rows` the Fact rows it touched (possibly empty for a Rule-only write).
|
|
75
|
+
*/
|
|
76
|
+
async function runSentence(sentence, { config, memoryDir }) {
|
|
77
|
+
const before = readFactRows(await loadMemory(memoryDir));
|
|
78
|
+
const { record } = await runTurn(sentence, { config, memoryDir, sessionId: uuidv7() });
|
|
79
|
+
if (record?.via !== "assert" || record?.miss) return { recognized: false, rows: [] };
|
|
80
|
+
const after = readFactRows(await loadMemory(memoryDir));
|
|
81
|
+
return { recognized: true, rows: touchedFactRows(before, after) };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* `argv` defaults to the real CLI args (process.argv.slice(2)) but takes an
|
|
86
|
+
* explicit array too, so a test can drive this exactly like the CLI does
|
|
87
|
+
* without touching global process.argv. Returns { sentences, recognized,
|
|
88
|
+
* extracted } (the same counts the printed summary reports) so a test can
|
|
89
|
+
* assert on structured results instead of scraping console output.
|
|
90
|
+
*/
|
|
91
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
92
|
+
const { file, repo, out } = parseArgs(argv);
|
|
93
|
+
if (!file) {
|
|
94
|
+
console.error(USAGE);
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
return { sentences: 0, recognized: 0, extracted: [] };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const filePath = resolve(process.cwd(), file);
|
|
100
|
+
const text = await readFile(filePath, "utf8");
|
|
101
|
+
const sourceTag = basename(filePath);
|
|
102
|
+
const sentences = splitSentencesPreservingPaths(text);
|
|
103
|
+
|
|
104
|
+
const ephemeral = !repo;
|
|
105
|
+
const memoryDir = repo ? resolve(process.cwd(), repo) : await mkdtemp(join(tmpdir(), "tmct-extract-"));
|
|
106
|
+
const config = loadConfig(process.env, memoryDir);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const extracted = [];
|
|
110
|
+
let recognizedSentences = 0;
|
|
111
|
+
|
|
112
|
+
for (const sentence of sentences) {
|
|
113
|
+
const { recognized, rows } = await runSentence(sentence, { config, memoryDir });
|
|
114
|
+
if (!recognized || !rows.length) continue; // unrecognized shape, or a Rule (not a Fact) — skip, honest
|
|
115
|
+
recognizedSentences += 1;
|
|
116
|
+
const tag = `extracted:${sourceTag}`;
|
|
117
|
+
for (const row of rows) {
|
|
118
|
+
// Additive: layers the audit tag onto the SAME (subject, predicate,
|
|
119
|
+
// object) the recognizer just stored — appendFact unions provenance
|
|
120
|
+
// by id, so this never duplicates or overwrites the recognizer's own
|
|
121
|
+
// ace:chat:/teach:chat: entry, and re-running this over the same
|
|
122
|
+
// file/fact is idempotent (the union simply dedupes the tag).
|
|
123
|
+
await appendFact(memoryDir, {
|
|
124
|
+
subject: row.subject, predicate: row.predicate, object: row.object,
|
|
125
|
+
provenance: tag, quantifier: row.quantifier || "",
|
|
126
|
+
});
|
|
127
|
+
extracted.push({
|
|
128
|
+
subject: row.subject, predicate: row.predicate, object: row.object,
|
|
129
|
+
provenance: tag, quantifier: row.quantifier || "", sentence,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (out) {
|
|
135
|
+
const body = extracted.map((f) => JSON.stringify(f)).join("\n") + (extracted.length ? "\n" : "");
|
|
136
|
+
await writeFile(resolve(process.cwd(), out), body, "utf8");
|
|
137
|
+
} else if (ephemeral) {
|
|
138
|
+
for (const f of extracted) console.log(JSON.stringify(f));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const skipped = sentences.length - recognizedSentences;
|
|
142
|
+
console.error(
|
|
143
|
+
`${sentences.length} sentence${sentences.length === 1 ? "" : "s"} found, `
|
|
144
|
+
+ `${recognizedSentences} recognized as fact${recognizedSentences === 1 ? "" : "s"} `
|
|
145
|
+
+ `(${extracted.length} fact row${extracted.length === 1 ? "" : "s"}), `
|
|
146
|
+
+ `${skipped} skipped — not a recognized declarative shape (an honest, expected gap; this is `
|
|
147
|
+
+ `an attempt, not full NLU).`,
|
|
148
|
+
);
|
|
149
|
+
if (repo) console.error(`facts written into ${memoryDir}'s tmct memory, tagged ${sourceTag}`);
|
|
150
|
+
if (out) console.error(`facts written to ${out}`);
|
|
151
|
+
return { sentences: sentences.length, recognized: recognizedSentences, extracted };
|
|
152
|
+
} finally {
|
|
153
|
+
if (ephemeral) await rm(memoryDir, { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -15,7 +15,7 @@ import { basename, resolve } from "node:path";
|
|
|
15
15
|
import { runTurn, uuidv7 } from "./chat.mjs";
|
|
16
16
|
import { loadMemory, readFactRows, appendFact, openMemoryBackend } from "../adapters/memory/core.mjs";
|
|
17
17
|
import { loadConfig } from "../adapters/config.mjs";
|
|
18
|
-
import {
|
|
18
|
+
import { splitSentencesPreservingPaths } from "./sentences.mjs";
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Teach every sentence of `filePath` into `repoRoot`'s memory store.
|
|
@@ -34,7 +34,7 @@ export async function importDefinitionFile(repoRoot, filePath, { env = process.e
|
|
|
34
34
|
const lines = text.split("\n");
|
|
35
35
|
const commentLines = lines.filter((l) => l.trim().startsWith("#"));
|
|
36
36
|
const body = lines.filter((l) => !l.trim().startsWith("#")).join("\n");
|
|
37
|
-
const sentences =
|
|
37
|
+
const sentences = splitSentencesPreservingPaths(body);
|
|
38
38
|
|
|
39
39
|
const { loadTomlConfig } = await import("../adapters/toml-config.mjs");
|
|
40
40
|
const raw = await loadTomlConfig(root).catch(() => null);
|
package/src/services/init.mjs
CHANGED
|
@@ -85,7 +85,7 @@ export function renderTomlConfig(config = defaultConfig()) {
|
|
|
85
85
|
const base = `# tmct.toml — the mechanical code talker, project configuration.
|
|
86
86
|
# Written by \`tmct init\`. An ABSENT file means shipped defaults (this file
|
|
87
87
|
# just makes them explicit and editable). Documented in the repository-interface
|
|
88
|
-
# onboarding surface
|
|
88
|
+
# onboarding surface.
|
|
89
89
|
|
|
90
90
|
# Where the code-graph JSON artifact lives, relative to this file. The
|
|
91
91
|
# TMCT_GRAPH_FILE environment variable overrides it at runtime.
|
|
@@ -99,7 +99,7 @@ ${Array.isArray(c.graphFiles) && c.graphFiles.length ? `
|
|
|
99
99
|
graph_files = ${JSON.stringify(c.graphFiles)}
|
|
100
100
|
` : ""}
|
|
101
101
|
[corpus]
|
|
102
|
-
# Corpus-tiering policy
|
|
102
|
+
# Corpus-tiering policy. The $0-offline default is inviolable;
|
|
103
103
|
# higher tiers are ADDITIVE and never required to answer.
|
|
104
104
|
# "tier1" — committed slice only. Offline, $0. The default.
|
|
105
105
|
# "tier2" — also fetch growable corpora at seed time (network, once, cached).
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// ledger-viz.mjs — `tmct viz`: the memory graph as a readable ledger of
|
|
2
2
|
// fact-sentences around one focus term, with the in-page chat dock.
|
|
3
3
|
//
|
|
4
|
+
// "ledger" is a UI label for this VIEW, not a storage claim. The memory graph
|
|
5
|
+
// underneath is mutable — upsert rewrites a fact in place and removeFacts
|
|
6
|
+
// deletes — so this renders a report over the current graph, not an
|
|
7
|
+
// append-only or immutable log (the ISO 22739 sense of the word does not hold).
|
|
8
|
+
//
|
|
4
9
|
// Three pure/impure-separated pieces:
|
|
5
10
|
// - computeLedgerData(repoDir, opts) — I/O (loadMemory) + derivation
|
|
6
11
|
// - computeLedgerDataFromPayload(payload) — the pure derivation half
|
|
@@ -409,7 +414,7 @@ ${hasMemChat ? `<script>\n${bundleStr}\n</script>` : ""}
|
|
|
409
414
|
const DAY = 86400000;
|
|
410
415
|
const facetCounts = ${facetCounts.toString()};
|
|
411
416
|
const el = (id) => document.getElementById(id);
|
|
412
|
-
const esc =
|
|
417
|
+
const esc = ${escapeHtml.toString()};
|
|
413
418
|
const FAMS = ["is-a", "has", "can", "used-for", "rests-on", "role", "other"];
|
|
414
419
|
const FAM_LABEL = { "is-a": "is a kind of", has: "has", can: "can", "used-for": "used for", "rests-on": "rests on", role: "role / property", other: "other" };
|
|
415
420
|
const PROVS = [["taught", "you taught"], ["corpus", "corpus"], ["entail", "entailed"]];
|
|
@@ -17,3 +17,29 @@ export function splitSentences(text) {
|
|
|
17
17
|
const doc = nlp.readDoc(raw);
|
|
18
18
|
return doc.sentences().out().map((s) => s.trim()).filter(Boolean);
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
/** Does this line actually carry a sentence boundary — a terminator, then
|
|
22
|
+
* whitespace, then the next sentence's first word? wink's own splitter breaks
|
|
23
|
+
* "src/core/store.mjs" into "src/core/store." + "mjs …", so a line naming a
|
|
24
|
+
* file splits into sentences that were never there. Requiring the whitespace
|
|
25
|
+
* keeps a dotted identifier whole, and nothing that holds no boundary is ever
|
|
26
|
+
* handed to the splitter's judgement. */
|
|
27
|
+
export const carriesASentenceBoundary = (line) => /[.!?]\s+\w/.test(String(line));
|
|
28
|
+
|
|
29
|
+
/** Split multi-line text into sentences the way splitSentences does, but never
|
|
30
|
+
* let wink shatter a line that has no real sentence boundary. A bare dotted
|
|
31
|
+
* module path (`src/core/store.mjs`) would otherwise come back as
|
|
32
|
+
* ["src/core/store.", "mjs"]. Each line is only handed to wink when it carries
|
|
33
|
+
* a real boundary; otherwise the trimmed line stands as one sentence. */
|
|
34
|
+
export function splitSentencesPreservingPaths(text) {
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const line of String(text ?? "").split("\n")) {
|
|
37
|
+
if (carriesASentenceBoundary(line)) {
|
|
38
|
+
out.push(...splitSentences(line));
|
|
39
|
+
} else {
|
|
40
|
+
const trimmed = line.trim();
|
|
41
|
+
if (trimmed) out.push(trimmed);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|