@polycode-projects/the-mechanical-code-talker 2.10.3 → 2.10.5
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 +66 -10
- package/bin/tmct.mjs +5 -2
- package/package.json +3 -1
- package/src/adapters/memory/core.mjs +8 -1
- package/src/domain/cli-verbs.mjs +2 -0
- package/src/domain/memory/trust.mjs +21 -2
- package/src/domain/sense-split.mjs +203 -0
- package/src/services/chat-page-viz.mjs +87 -19
- package/src/services/chat-session.mjs +12 -7
- package/src/services/chat.mjs +320 -30
- package/src/services/code-explorer-viz.mjs +16 -2
- package/src/services/extract-facts.mjs +293 -81
- package/src/services/fold.mjs +1 -1
- package/src/services/ingest-viz.mjs +388 -0
- package/src/services/ledger-viz.mjs +110 -0
- package/src/services/session-log-format.mjs +64 -0
- package/src/services/sessions.mjs +56 -22
- package/src/services/spider-fly-turn.mjs +54 -1
- package/src/services/spider-fly-viz.mjs +21 -19
- package/src/surfaces/web/chat-browser-entry.mjs +10 -6
- package/src/surfaces/web/ingest-browser-entry.mjs +126 -0
- package/src/surfaces/web/ledger-browser-entry.mjs +15 -2
- package/src/surfaces/web/memory-ask-browser.bundle.js +135 -126
- package/src/tools/definitions.mjs +14 -0
- package/src/tools/handlers/index.mjs +2 -0
- package/src/tools/handlers/tmct-ingest.mjs +43 -0
- package/src/tools/server.mjs +5 -2
|
@@ -1,22 +1,33 @@
|
|
|
1
|
-
// `tmct extract` — turn
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// `tmct extract` — turn plain text into facts by reusing the SAME deterministic
|
|
2
|
+
// recognizer the interactive chat's "teach" lane already has (runTurn,
|
|
3
|
+
// src/services/chat.mjs) — no new NLU, no LLM, no guessing.
|
|
4
4
|
//
|
|
5
5
|
// tmct extract <text-file> [--repo <path>] [--out <file.jsonl>]
|
|
6
|
+
// [--optimistic] [--canonical]
|
|
6
7
|
//
|
|
7
8
|
// The text file is named positionally, or with --file, the way `tmct import`
|
|
8
|
-
// names one.
|
|
9
|
+
// names one. The whole pipeline is exposed as one service seam, `ingestText`,
|
|
10
|
+
// so a browser or a tool layer can ground the same text without the CLI.
|
|
9
11
|
//
|
|
10
|
-
// How it works: the
|
|
11
|
-
// sentence-boundary detection (src/
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
12
|
+
// How it works: the text is split into sentences with wink-nlp's own
|
|
13
|
+
// sentence-boundary detection (src/services/sentences.mjs — never a naive regex
|
|
14
|
+
// split). Each sentence is fed through runTurn() exactly as if an operator had
|
|
15
|
+
// typed it into the live chat. A sentence the recognizer turns into a stored
|
|
16
|
+
// fact (record.via === "assert", record.miss === false) is kept; every other
|
|
17
|
+
// sentence is SKIPPED. This is an honest partial extraction, an "attempt", never
|
|
18
|
+
// full NLU: nothing here ever paraphrases or invents a fact the recognizer
|
|
19
|
+
// itself didn't produce.
|
|
20
|
+
//
|
|
21
|
+
// --optimistic ALSO run a bounded, lexicon-gated fuzzy tier over the sentences
|
|
22
|
+
// the strict recognizer skipped (optimisticTriples below): a
|
|
23
|
+
// copula or a known relation verb flanked by two resolvable
|
|
24
|
+
// entities becomes a candidate triple, stored under its OWN
|
|
25
|
+
// low-trust source kind (optimistic-extract:<source>, prior 0.35 —
|
|
26
|
+
// below every curated pack, memory/trust.mjs) with NO operator or
|
|
27
|
+
// teach tag riding alongside, so a fuzzy candidate can never
|
|
28
|
+
// corroborate a curated fact.
|
|
29
|
+
// --canonical Print each ingested fact in canonical triple form, enriched with
|
|
30
|
+
// how each endpoint already links into the store.
|
|
20
31
|
//
|
|
21
32
|
// --repo <path> write straight into that repo's own tmct memory (runTurn's
|
|
22
33
|
// normal memoryDir write path — "grow my own tmct memory
|
|
@@ -27,19 +38,16 @@
|
|
|
27
38
|
// shape JSONL ({subject, predicate, object, provenance}) —
|
|
28
39
|
// to stdout, or to --out <file.jsonl> if given.
|
|
29
40
|
//
|
|
30
|
-
// Every recognized fact ALSO gets a second, additive provenance tag —
|
|
31
|
-
// extracted:<source-file-basename> — layered on top of whatever the
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// the reasoning: same closed-set recognizer as `teach`, but an unvetted
|
|
38
|
-
// source document, so it sits just above `web`).
|
|
41
|
+
// Every strict-recognized fact ALSO gets a second, additive provenance tag —
|
|
42
|
+
// extracted:<source-file-basename> — layered on top of whatever the recognizer
|
|
43
|
+
// itself already wrote (ace:chat:…/teach:chat:…), via appendFact's provenance
|
|
44
|
+
// UNION (memory/core.mjs). That keeps an extracted fact auditable as "this
|
|
45
|
+
// document evidenced this claim" at its own trust-prior tier
|
|
46
|
+
// (SOURCE_PRIOR.extracted, memory/trust.mjs). An optimistic candidate carries
|
|
47
|
+
// ONLY its own low tag, because it was never operator speech.
|
|
39
48
|
//
|
|
40
49
|
// Never claims full coverage: the summary this prints always states how many
|
|
41
|
-
// sentences were found, how many were recognized, and how many were
|
|
42
|
-
// skipped.
|
|
50
|
+
// sentences were found, how many were recognized, and how many were skipped.
|
|
43
51
|
|
|
44
52
|
import { readFile, writeFile, mkdtemp, rm } from "node:fs/promises";
|
|
45
53
|
import { tmpdir } from "node:os";
|
|
@@ -50,17 +58,24 @@ import { splitSentencesPreservingPaths } from "./sentences.mjs";
|
|
|
50
58
|
import { loadMemory, readFactRows, appendFact } from "../adapters/memory/core.mjs";
|
|
51
59
|
import { loadConfig } from "../adapters/config.mjs";
|
|
52
60
|
import { touchedFactRows } from "../domain/memory/touched-facts.mjs";
|
|
61
|
+
import { normFactTerm } from "../domain/hash.mjs";
|
|
62
|
+
import { winkInstance } from "../adapters/wink-model.mjs";
|
|
63
|
+
import {
|
|
64
|
+
loadLexicon, lookupNoun, lookupVerb, lookupAdjective, lookupProperName, predicateOf,
|
|
65
|
+
} from "../domain/grammar/lexicon.mjs";
|
|
53
66
|
|
|
54
|
-
export const USAGE = "usage: tmct extract <text-file>|--file <text-file> [--repo <path>] [--out <file.jsonl>]";
|
|
67
|
+
export const USAGE = "usage: tmct extract <text-file>|--file <text-file> [--repo <path>] [--out <file.jsonl>] [--optimistic] [--canonical]";
|
|
55
68
|
|
|
56
69
|
export function parseArgs(argv) {
|
|
57
|
-
const args = { file: null, repo: null, out: null };
|
|
70
|
+
const args = { file: null, repo: null, out: null, optimistic: false, canonical: false };
|
|
58
71
|
const rest = [];
|
|
59
72
|
for (let i = 0; i < argv.length; i += 1) {
|
|
60
73
|
const a = argv[i];
|
|
61
74
|
if (a === "--repo") args.repo = argv[i += 1];
|
|
62
75
|
else if (a === "--out") args.out = argv[i += 1];
|
|
63
76
|
else if (a === "--file") args.file = argv[i += 1];
|
|
77
|
+
else if (a === "--optimistic") args.optimistic = true;
|
|
78
|
+
else if (a === "--canonical") args.canonical = true;
|
|
64
79
|
else rest.push(a);
|
|
65
80
|
}
|
|
66
81
|
args.file = args.file || rest[0] || null;
|
|
@@ -81,75 +96,272 @@ async function runSentence(sentence, { config, memoryDir }) {
|
|
|
81
96
|
return { recognized: true, rows: touchedFactRows(before, after) };
|
|
82
97
|
}
|
|
83
98
|
|
|
99
|
+
// The copula lemmas that read as class membership; a following noun phrase is
|
|
100
|
+
// the class the subject is-a. "has/have" and other verbs are relations, not isa.
|
|
101
|
+
const OPTIMISTIC_COPULAS = new Set(["is", "are", "was", "were", "be", "been", "being", "am"]);
|
|
102
|
+
// The determiners and generic classifier heads the lexical fallback reads
|
|
103
|
+
// through when a wink model isn't available to tag parts of speech.
|
|
104
|
+
const OPTIMISTIC_SKIP = new Set([
|
|
105
|
+
"a", "an", "the", "this", "that", "these", "those", "its", "his", "her", "their",
|
|
106
|
+
"our", "my", "your", "some", "any", "one", "kind", "sort", "type", "of",
|
|
107
|
+
]);
|
|
108
|
+
const OPTIMISTIC_ENTITY_HOPS = 4;
|
|
109
|
+
|
|
110
|
+
/** Fold an entity surface to its stored key: a lexicon noun's lemma, else the
|
|
111
|
+
* word's own normFactTerm (the optimistic tier mints unlisted content nouns
|
|
112
|
+
* the way the strict teach lane already mints "redis"). */
|
|
113
|
+
function foldEntity(word, lexicon) {
|
|
114
|
+
const noun = lookupNoun(lexicon, String(word).toLowerCase());
|
|
115
|
+
return normFactTerm(noun ? noun.lemma : word);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The precise tier: wink POS tags pick out the NOUN/PROPN either side of a
|
|
119
|
+
* copula (→ rdfs:subClassOf) or a lexicon-known relation verb (→ its
|
|
120
|
+
* predicate). Adjectives, determiners and prepositions are never mistaken for
|
|
121
|
+
* the entity, so "the quick brown fox jumps over something" yields nothing.
|
|
122
|
+
* The entity scan stops at punctuation so it never crosses a clause. */
|
|
123
|
+
function optimisticTriplesPos(sentence, lexicon, nlp) {
|
|
124
|
+
let values;
|
|
125
|
+
let pos;
|
|
126
|
+
try {
|
|
127
|
+
const doc = nlp.readDoc(String(sentence || ""));
|
|
128
|
+
values = doc.tokens().out(nlp.its.value);
|
|
129
|
+
pos = doc.tokens().out(nlp.its.pos);
|
|
130
|
+
} catch { return []; }
|
|
131
|
+
const nearestEntity = (idx, step) => {
|
|
132
|
+
for (let i = idx + step; i >= 0 && i < values.length; i += step) {
|
|
133
|
+
if (pos[i] === "PUNCT") break;
|
|
134
|
+
if (pos[i] === "NOUN" || pos[i] === "PROPN") return foldEntity(values[i], lexicon);
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
};
|
|
138
|
+
const tripleAt = (i, predicate) => {
|
|
139
|
+
const subject = nearestEntity(i, -1);
|
|
140
|
+
const object = nearestEntity(i, +1);
|
|
141
|
+
return subject && object && subject !== object ? { subject, predicate, object } : null;
|
|
142
|
+
};
|
|
143
|
+
for (let i = 1; i < values.length - 1; i += 1) {
|
|
144
|
+
if (pos[i] === "AUX" && OPTIMISTIC_COPULAS.has(values[i].toLowerCase())) {
|
|
145
|
+
const t = tripleAt(i, "rdfs:subClassOf");
|
|
146
|
+
if (t) return [t];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (let i = 1; i < values.length - 1; i += 1) {
|
|
150
|
+
if (pos[i] !== "VERB") continue;
|
|
151
|
+
const verb = lookupVerb(lexicon, values[i].toLowerCase());
|
|
152
|
+
if (!verb) continue;
|
|
153
|
+
const t = tripleAt(i, predicateOf(verb));
|
|
154
|
+
if (t) return [t];
|
|
155
|
+
}
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The lexical fallback for a checkout with no wink model: a copula flanked by
|
|
160
|
+
* two lexicon-known nouns (or a mid-sentence proper noun). Narrower and more
|
|
161
|
+
* cautious than the POS tier — without part-of-speech tags the only safe entity
|
|
162
|
+
* test is lexicon membership, so unlisted content nouns are skipped here. */
|
|
163
|
+
function optimisticTriplesLexical(sentence, lexicon) {
|
|
164
|
+
const raw = String(sentence || "").match(/[A-Za-z][A-Za-z'-]*/g) || [];
|
|
165
|
+
if (raw.length < 3) return [];
|
|
166
|
+
const lower = raw.map((w) => w.toLowerCase());
|
|
167
|
+
const nearestEntity = (idx, step) => {
|
|
168
|
+
for (let i = idx + step, hops = 0; i >= 0 && i < lower.length && hops < OPTIMISTIC_ENTITY_HOPS; i += step, hops += 1) {
|
|
169
|
+
const w = lower[i];
|
|
170
|
+
if (OPTIMISTIC_SKIP.has(w) || lookupAdjective(lexicon, w)) continue;
|
|
171
|
+
const noun = lookupNoun(lexicon, w);
|
|
172
|
+
if (noun) return normFactTerm(noun.lemma);
|
|
173
|
+
const proper = lookupProperName(lexicon, w);
|
|
174
|
+
if (proper) return normFactTerm(proper);
|
|
175
|
+
if (i > 0 && /^[A-Z]/.test(raw[i])) return normFactTerm(raw[i]);
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
};
|
|
180
|
+
for (let i = 1; i < lower.length - 1; i += 1) {
|
|
181
|
+
if (!OPTIMISTIC_COPULAS.has(lower[i])) continue;
|
|
182
|
+
const subject = nearestEntity(i, -1);
|
|
183
|
+
const object = nearestEntity(i, +1);
|
|
184
|
+
if (subject && object && subject !== object) return [{ subject, predicate: "rdfs:subClassOf", object }];
|
|
185
|
+
}
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* A bounded triple candidate from a sentence the strict recognizer skipped: a
|
|
191
|
+
* copula (→ rdfs:subClassOf) or a lexicon-known relation verb (→ its predicate)
|
|
192
|
+
* flanked by two entities. At most one triple per sentence; [] when nothing
|
|
193
|
+
* resolves both sides — no guessing past the shape. Uses wink POS tags when a
|
|
194
|
+
* model is available (the precise tier), else a narrower lexicon-only fallback.
|
|
195
|
+
*
|
|
196
|
+
* opts.lexicon a loaded lexicon (the core vocabulary when absent).
|
|
197
|
+
* opts.nlp a wink instance (winkInstance() when absent); null forces the
|
|
198
|
+
* lexical fallback.
|
|
199
|
+
*/
|
|
200
|
+
export function optimisticTriples(sentence, { lexicon = loadLexicon(), nlp } = {}) {
|
|
201
|
+
const engine = nlp === undefined ? winkInstance() : nlp;
|
|
202
|
+
return engine ? optimisticTriplesPos(sentence, lexicon, engine) : optimisticTriplesLexical(sentence, lexicon);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** A readable predicate for canonical output: the local part of an rdfs:/ace:
|
|
206
|
+
* CURIE, otherwise the predicate verbatim. */
|
|
207
|
+
const readablePredicate = (predicate) => String(predicate).replace(/^[a-z]+:/i, "");
|
|
208
|
+
|
|
209
|
+
/** One canonical line per ingested fact, enriched with how each endpoint links
|
|
210
|
+
* back into the store: `subject predicate object` plus, for each endpoint that
|
|
211
|
+
* the store also mentions elsewhere, how many OTHER facts touch it. */
|
|
212
|
+
function canonicalLines(facts, storeRows) {
|
|
213
|
+
const degree = new Map();
|
|
214
|
+
for (const row of storeRows) {
|
|
215
|
+
degree.set(row.subject, (degree.get(row.subject) || 0) + 1);
|
|
216
|
+
if (row.object !== row.subject) degree.set(row.object, (degree.get(row.object) || 0) + 1);
|
|
217
|
+
}
|
|
218
|
+
return facts.map((f) => {
|
|
219
|
+
const links = [];
|
|
220
|
+
for (const end of [f.subject, f.object]) {
|
|
221
|
+
const others = (degree.get(end) || 0) - 1; // exclude this fact's own edge
|
|
222
|
+
if (others > 0) links.push(`${end} links to ${others} other fact${others === 1 ? "" : "s"}`);
|
|
223
|
+
}
|
|
224
|
+
const line = `${f.subject} ${readablePredicate(f.predicate)} ${f.object}`;
|
|
225
|
+
return links.length ? `${line} [${links.join("; ")}]` : line;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* ingestText — the single ingest seam. Grounds `text` into facts and returns a
|
|
231
|
+
* structured result; the CLI, the browser and the tool layer all drive this.
|
|
232
|
+
*
|
|
233
|
+
* text the raw string to ground.
|
|
234
|
+
* options:
|
|
235
|
+
* memoryDir write grounded facts here (a real .tmct memory dir). Omit for
|
|
236
|
+
* an ephemeral scratch dir that is created and deleted inside
|
|
237
|
+
* this call, so nothing on disk is mutated — the returned facts
|
|
238
|
+
* are the only output.
|
|
239
|
+
* sourceTag the label the audit provenance carries (extracted:<tag> /
|
|
240
|
+
* optimistic-extract:<tag>). Default "text".
|
|
241
|
+
* optimistic also run the fuzzy tier over strict-skipped sentences.
|
|
242
|
+
* canonical include a `canonical` array: one enriched triple line per
|
|
243
|
+
* ingested fact.
|
|
244
|
+
* config a loaded config; derived from the write dir when absent.
|
|
245
|
+
* lexicon a loaded lexicon; the core vocabulary when absent.
|
|
246
|
+
*
|
|
247
|
+
* Returns { sentences, recognized, extracted, optimistic, skipped, canonical? }.
|
|
248
|
+
* recognized — strict-recognized sentence count.
|
|
249
|
+
* extracted — strict fact rows ({subject, predicate, object, provenance,
|
|
250
|
+
* quantifier, sentence}).
|
|
251
|
+
* optimistic — fuzzy candidate rows ({subject, predicate, object, provenance,
|
|
252
|
+
* sentence}); always [] unless options.optimistic.
|
|
253
|
+
* skipped — sentences neither tier grounded.
|
|
254
|
+
*/
|
|
255
|
+
export async function ingestText(text, {
|
|
256
|
+
memoryDir = null, sourceTag = "text", optimistic = false,
|
|
257
|
+
canonical = false, config = null, lexicon = null,
|
|
258
|
+
} = {}) {
|
|
259
|
+
const sentences = splitSentencesPreservingPaths(String(text ?? ""));
|
|
260
|
+
const ephemeral = !memoryDir;
|
|
261
|
+
const dir = memoryDir || await mkdtemp(join(tmpdir(), "tmct-ingest-"));
|
|
262
|
+
// `dir` may be a backend handle (a sqlite store), not a path — only a real
|
|
263
|
+
// directory string can seed loadConfig's cwd; a handle-holding caller passes
|
|
264
|
+
// its own config.
|
|
265
|
+
const cfg = config || loadConfig(process.env, typeof dir === "string" ? dir : process.cwd());
|
|
266
|
+
const lex = lexicon || loadLexicon();
|
|
267
|
+
const nlp = optimistic ? winkInstance() : null;
|
|
268
|
+
|
|
269
|
+
const extracted = [];
|
|
270
|
+
const optimisticFacts = [];
|
|
271
|
+
let recognizedSentences = 0;
|
|
272
|
+
let optimisticSentences = 0;
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
for (const sentence of sentences) {
|
|
276
|
+
const { recognized, rows } = await runSentence(sentence, { config: cfg, memoryDir: dir });
|
|
277
|
+
if (recognized && rows.length) {
|
|
278
|
+
recognizedSentences += 1;
|
|
279
|
+
const tag = `extracted:${sourceTag}`;
|
|
280
|
+
for (const row of rows) {
|
|
281
|
+
await appendFact(dir, {
|
|
282
|
+
subject: row.subject, predicate: row.predicate, object: row.object,
|
|
283
|
+
provenance: tag, quantifier: row.quantifier || "",
|
|
284
|
+
});
|
|
285
|
+
extracted.push({
|
|
286
|
+
subject: row.subject, predicate: row.predicate, object: row.object,
|
|
287
|
+
provenance: tag, quantifier: row.quantifier || "", sentence,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (!optimistic) continue;
|
|
293
|
+
const candidates = optimisticTriples(sentence, { lexicon: lex, nlp });
|
|
294
|
+
if (!candidates.length) continue;
|
|
295
|
+
optimisticSentences += 1;
|
|
296
|
+
const tag = `optimistic-extract:${sourceTag}`;
|
|
297
|
+
for (const t of candidates) {
|
|
298
|
+
await appendFact(dir, {
|
|
299
|
+
subject: t.subject, predicate: t.predicate, object: t.object, provenance: tag,
|
|
300
|
+
});
|
|
301
|
+
optimisticFacts.push({ ...t, provenance: tag, sentence });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const result = {
|
|
306
|
+
sentences: sentences.length,
|
|
307
|
+
recognized: recognizedSentences,
|
|
308
|
+
extracted,
|
|
309
|
+
optimistic: optimisticFacts,
|
|
310
|
+
skipped: sentences.length - recognizedSentences - optimisticSentences,
|
|
311
|
+
};
|
|
312
|
+
if (canonical) {
|
|
313
|
+
result.canonical = canonicalLines([...extracted, ...optimisticFacts], readFactRows(await loadMemory(dir)));
|
|
314
|
+
}
|
|
315
|
+
return result;
|
|
316
|
+
} finally {
|
|
317
|
+
if (ephemeral) await rm(dir, { recursive: true, force: true });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
84
321
|
/**
|
|
85
322
|
* `argv` defaults to the real CLI args (process.argv.slice(2)) but takes an
|
|
86
323
|
* explicit array too, so a test can drive this exactly like the CLI does
|
|
87
324
|
* without touching global process.argv. Returns { sentences, recognized,
|
|
88
|
-
* extracted } (the same counts the printed summary
|
|
89
|
-
* assert on structured results instead of scraping
|
|
325
|
+
* extracted, optimistic, skipped } (the same counts the printed summary
|
|
326
|
+
* reports) so a test can assert on structured results instead of scraping
|
|
327
|
+
* console output.
|
|
90
328
|
*/
|
|
91
329
|
export async function main(argv = process.argv.slice(2)) {
|
|
92
|
-
const { file, repo, out } = parseArgs(argv);
|
|
330
|
+
const { file, repo, out, optimistic, canonical } = parseArgs(argv);
|
|
93
331
|
if (!file) {
|
|
94
332
|
console.error(USAGE);
|
|
95
333
|
process.exitCode = 1;
|
|
96
|
-
return { sentences: 0, recognized: 0, extracted: [] };
|
|
334
|
+
return { sentences: 0, recognized: 0, extracted: [], optimistic: [], skipped: 0 };
|
|
97
335
|
}
|
|
98
336
|
|
|
99
337
|
const filePath = resolve(process.cwd(), file);
|
|
100
338
|
const text = await readFile(filePath, "utf8");
|
|
101
339
|
const sourceTag = basename(filePath);
|
|
102
|
-
const
|
|
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);
|
|
340
|
+
const memoryDir = repo ? resolve(process.cwd(), repo) : null;
|
|
107
341
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
let recognizedSentences = 0;
|
|
342
|
+
const result = await ingestText(text, { memoryDir, sourceTag, optimistic, canonical });
|
|
343
|
+
const emitted = optimistic ? [...result.extracted, ...result.optimistic] : result.extracted;
|
|
111
344
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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 });
|
|
345
|
+
if (out) {
|
|
346
|
+
const body = emitted.map((f) => JSON.stringify(f)).join("\n") + (emitted.length ? "\n" : "");
|
|
347
|
+
await writeFile(resolve(process.cwd(), out), body, "utf8");
|
|
348
|
+
} else if (canonical) {
|
|
349
|
+
for (const line of result.canonical) console.log(line);
|
|
350
|
+
} else if (!repo) {
|
|
351
|
+
for (const f of emitted) console.log(JSON.stringify(f));
|
|
154
352
|
}
|
|
353
|
+
|
|
354
|
+
const { sentences, recognized } = result;
|
|
355
|
+
const optimisticCount = result.optimistic.length;
|
|
356
|
+
console.error(
|
|
357
|
+
`${sentences} sentence${sentences === 1 ? "" : "s"} found, `
|
|
358
|
+
+ `${recognized} recognized as fact${recognized === 1 ? "" : "s"} `
|
|
359
|
+
+ `(${result.extracted.length} fact row${result.extracted.length === 1 ? "" : "s"})`
|
|
360
|
+
+ (optimistic ? `, ${optimisticCount} optimistic candidate${optimisticCount === 1 ? "" : "s"}` : "")
|
|
361
|
+
+ `, ${result.skipped} skipped — not a recognized declarative shape (an honest, expected gap; this is `
|
|
362
|
+
+ `an attempt, not full NLU).`,
|
|
363
|
+
);
|
|
364
|
+
if (repo) console.error(`facts written into ${memoryDir}'s tmct memory, tagged ${sourceTag}`);
|
|
365
|
+
if (out) console.error(`facts written to ${out}`);
|
|
366
|
+
return { sentences, recognized, extracted: result.extracted, optimistic: result.optimistic, skipped: result.skipped };
|
|
155
367
|
}
|
package/src/services/fold.mjs
CHANGED
|
@@ -113,7 +113,7 @@ export async function foldSessionLogs(repoDir, { sessionId = null } = {}) {
|
|
|
113
113
|
processed.push(record.id);
|
|
114
114
|
let answers = new Map();
|
|
115
115
|
try {
|
|
116
|
-
answers = parseSessionLog(await readFile(join(repoDir, LOG_DIR_REL, `session-${record.id}.
|
|
116
|
+
answers = parseSessionLog(await readFile(join(repoDir, LOG_DIR_REL, `session-${record.id}.md`), "utf8"));
|
|
117
117
|
} catch { /* transcript gone — fold question-only, honestly */ }
|
|
118
118
|
const text = cleanSessionText(record, answers);
|
|
119
119
|
if (text) {
|