@polycode-projects/the-mechanical-code-talker 1.9.1 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +441 -217
- package/bin/tmct.mjs +126 -1
- package/corpus/seon/README.md +1 -2
- package/package.json +4 -2
- package/src/answer-variants.mjs +8 -36
- package/src/ask-browser-entry.mjs +5 -23
- package/src/ask-browser.bundle.js +1 -2
- package/src/ask-nlp.mjs +9 -23
- package/src/ask-vocab.mjs +139 -589
- package/src/ask.mjs +627 -1729
- package/src/chat.mjs +1684 -2874
- package/src/cli-args.mjs +14 -28
- package/src/codegraph.mjs +236 -644
- package/src/completions/complete.mjs +18 -62
- package/src/completions/graph-adapter.mjs +14 -60
- package/src/completions/group.mjs +12 -68
- package/src/completions/infer.mjs +38 -126
- package/src/completions/prune.mjs +17 -70
- package/src/completions/rank.mjs +16 -69
- package/src/completions/search.mjs +8 -31
- package/src/concept.mjs +32 -88
- package/src/conformance.mjs +11 -15
- package/src/corpus/conceptnet.mjs +31 -89
- package/src/corpus/templates.mjs +19 -45
- package/src/corpus/unknown-ingest.mjs +31 -92
- package/src/embed.mjs +10 -22
- package/src/extensions.mjs +50 -154
- package/src/finish.mjs +35 -91
- package/src/grammar/ace.mjs +16 -40
- package/src/grammar/assert.mjs +1 -1
- package/src/grammar/lexicon-core.json +1 -1
- package/src/grammar/lexicon.mjs +9 -27
- package/src/graph-merge.mjs +2 -3
- package/src/hash.mjs +6 -14
- package/src/index.mjs +6 -10
- package/src/init.mjs +38 -125
- package/src/interpret/fuzzy.mjs +10 -29
- package/src/interpret/merge.mjs +9 -27
- package/src/interpret/normalize.mjs +137 -585
- package/src/interpret/pipeline.mjs +23 -71
- package/src/interpret/strategies/ace.mjs +7 -31
- package/src/interpret/strategies/constructions.mjs +14 -41
- package/src/interpret/strategies/grammar.mjs +21 -60
- package/src/interpret/strategies/keywords.mjs +42 -131
- package/src/interpret/strategies/noise-strip.mjs +18 -89
- package/src/memory/bias.mjs +11 -54
- package/src/memory/blocks.mjs +18 -69
- package/src/memory/core.mjs +171 -591
- package/src/memory/fold.mjs +0 -0
- package/src/memory/inspect.mjs +7 -25
- package/src/memory/shacl.mjs +10 -39
- package/src/memory/trust.mjs +26 -127
- package/src/memory-ask-browser-entry.mjs +7 -30
- package/src/memory-ask-browser.bundle.js +1 -1
- package/src/paraphrase.mjs +20 -53
- package/src/planning.mjs +15 -157
- package/src/prose-nlp.mjs +4 -17
- package/src/prose.mjs +19 -67
- package/src/providers/bootstrap.mjs +1 -2
- package/src/providers/fixture.mjs +1 -2
- package/src/providers/graph-service.mjs +28 -59
- package/src/repository-interface.mjs +6 -8
- package/src/router/drive.mjs +183 -0
- package/src/router/goal-reasoner.mjs +66 -231
- package/src/router/guardrail.mjs +20 -58
- package/src/router/planner.mjs +15 -46
- package/src/router/registry.mjs +13 -43
- package/src/router/resolver.mjs +46 -131
- package/src/router/results.mjs +231 -0
- package/src/schema-docs.mjs +10 -27
- package/src/server-http.mjs +10 -19
- package/src/server.mjs +22 -28
- package/src/sessions.mjs +15 -30
- package/src/source-slice.mjs +5 -7
- package/src/source.mjs +10 -20
- package/src/syllogise.mjs +187 -575
- package/src/telemetry.mjs +3 -3
- package/src/toml-config.mjs +4 -4
- package/src/tui/app.mjs +9 -19
- package/src/viz.mjs +66 -123
- package/src/wink-model.mjs +10 -24
package/src/grammar/lexicon.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// grammar/lexicon.mjs — the declared lexicon behind tmct's ACE-OWL
|
|
2
|
-
// sub-fragment parser (
|
|
2
|
+
// sub-fragment parser (see ace.mjs). The lexicon is
|
|
3
3
|
// LOAD-BEARING: the grammar is only deterministic because every noun, verb
|
|
4
4
|
// (with any preposition), adjective (with its declared type) and proper name
|
|
5
5
|
// is DECLARED — the parser never guesses a word's category. Undeclared words
|
|
@@ -13,9 +13,6 @@
|
|
|
13
13
|
//
|
|
14
14
|
// Namespace: every lexicon carries a `.ns` field (the CURIE prefix ace.mjs
|
|
15
15
|
// stamps onto every term it mints) — always "tmct:" here (DEFAULT_NS).
|
|
16
|
-
// ace.mjs and predicateOf() read it off the lexicon rather than hardcoding
|
|
17
|
-
// the prefix inline, purely so a caller can supply its own already-namespaced
|
|
18
|
-
// lexicon; tmct itself only ever runs the one namespace.
|
|
19
16
|
//
|
|
20
17
|
// Morphology is deliberately tiny and deterministic (no NLP dependency): a
|
|
21
18
|
// suffix-fold for plurals/3rd-person-singular ("repositories"→repository,
|
|
@@ -154,26 +151,17 @@ export function loadLexicon(extra, ns = DEFAULT_NS) {
|
|
|
154
151
|
return lex;
|
|
155
152
|
}
|
|
156
153
|
|
|
157
|
-
/** Noun lookup with plural folding; returns the entry ({lemma, property?}) or
|
|
158
|
-
* `opts.singularOnly` (
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* standalone noun (die/dice, person/people, tooth/teeth): a singular-only
|
|
162
|
-
* determiner is incompatible with the plural-fold reading but fully compatible
|
|
163
|
-
* with the standalone-singular reading, so that's the one grammar agreement
|
|
164
|
-
* allows. A general rule keyed on determiner agreement, not a per-word carve-out
|
|
165
|
-
* — see lookupNounCandidates below for the multi-candidate form this wraps. */
|
|
154
|
+
/** Noun lookup with plural folding; returns the entry ({lemma, property?}) or
|
|
155
|
+
* null. `opts.singularOnly` (an "a"/"an" determiner) prunes the irregular-
|
|
156
|
+
* plural fold in favor of a standalone-singular entry when both exist for
|
|
157
|
+
* the same surface word (die/dice, person/people). */
|
|
166
158
|
export function lookupNoun(lexicon, word, opts = {}) {
|
|
167
159
|
return lookupNounCandidates(lexicon, word, opts)[0] ?? null;
|
|
168
160
|
}
|
|
169
161
|
|
|
170
|
-
/** Every lexicon entry `word` could plausibly resolve to, ranked
|
|
171
|
-
* top choice
|
|
172
|
-
*
|
|
173
|
-
* returns BOTH the `dice` and `die` entries (order depends on
|
|
174
|
-
* opts.singularOnly), a regular -s fold with both forms independently
|
|
175
|
-
* declared returns both. Additive: existing callers that only want the single
|
|
176
|
-
* best answer keep using lookupNoun untouched. */
|
|
162
|
+
/** Every lexicon entry `word` could plausibly resolve to, ranked the same as
|
|
163
|
+
* lookupNoun's top choice but without discarding a genuine alternate (e.g.
|
|
164
|
+
* die/dice returns both entries). */
|
|
177
165
|
export function lookupNounCandidates(lexicon, word, opts = {}) {
|
|
178
166
|
const w = String(word ?? "").toLowerCase();
|
|
179
167
|
const standalone = lexicon.nouns.get(w);
|
|
@@ -208,13 +196,7 @@ export function lookupVerb(lexicon, word) {
|
|
|
208
196
|
}
|
|
209
197
|
|
|
210
198
|
/** Every verb entry `word` could plausibly resolve to via foldCandidates,
|
|
211
|
-
* most-specific-fold-first
|
|
212
|
-
* used) — additive sibling of lookupNounCandidates, for a caller that wants
|
|
213
|
-
* to know about a genuine fold collision instead of only the first hit. In
|
|
214
|
-
* practice a verb fold rarely collides (unlike nouns' irregular-plural
|
|
215
|
-
* table), but the shape is symmetric with lookupNounCandidates on purpose —
|
|
216
|
-
* ace.mjs's multi-candidate relation search (parseRelationHits) reads
|
|
217
|
-
* whichever of the two a token's part of speech calls for. */
|
|
199
|
+
* most-specific-fold-first — the verb sibling of lookupNounCandidates. */
|
|
218
200
|
export function lookupVerbCandidates(lexicon, word) {
|
|
219
201
|
const w = String(word ?? "").toLowerCase();
|
|
220
202
|
const out = [];
|
package/src/graph-merge.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// graph-merge.mjs — multi-graph payload merging
|
|
2
|
-
// batch's multi-graph support). Used ONLY by source.mjs's fetchEntities when a
|
|
1
|
+
// graph-merge.mjs — multi-graph payload merging. Used ONLY by source.mjs's fetchEntities when a
|
|
3
2
|
// config carries more than one graph file (`config.graphFiles.length > 1`);
|
|
4
3
|
// the single-graph path never calls this — that byte-identical guarantee
|
|
5
4
|
// lives in source.mjs, not here.
|
|
@@ -8,7 +7,7 @@
|
|
|
8
7
|
// ids as `mod:${relativePath}` (repo-relative), so two graphs describing
|
|
9
8
|
// similarly-structured repos can collide. mergeEntityPayloads concatenates the
|
|
10
9
|
// straightforward arrays (classes/vocabulary/objectProperties/individuals),
|
|
11
|
-
// unions proseIndex (merging the id-array per word key), and —
|
|
10
|
+
// unions proseIndex (merging the id-array per word key), and — only
|
|
12
11
|
// on an ACTUAL collision — prefixes the specific colliding ids (and every
|
|
13
12
|
// in-payload reference to them: derived_from entries, mentions, edge subject/
|
|
14
13
|
// object, proseIndex entries) with `<graphName>/`. Ids that never collide pass
|
package/src/hash.mjs
CHANGED
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
// hash.mjs — the single home for tmct's content-address hash.
|
|
2
2
|
//
|
|
3
|
-
// FNV-1a 32-bit is deliberately home-grown
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// Two historical copies are reconciled here without changing a single output byte:
|
|
11
|
-
// - src/memory/core.mjs used the hex form for fact ids;
|
|
12
|
-
// - chatbench/graded.mjs used the integer form as a PRNG seed, with a redundant
|
|
13
|
-
// mid-loop `>>> 0`. That `>>> 0` was always a no-op: `^` and Math.imul both
|
|
14
|
-
// apply ToInt32 to their operands, so the 32-bit pattern is invariant between
|
|
15
|
-
// iterations whether the accumulator is stored signed or unsigned. The final
|
|
16
|
-
// `h >>> 0` therefore yields the same value either way — proven, not assumed.
|
|
3
|
+
// FNV-1a 32-bit is deliberately home-grown: it must be synchronous, browser-safe,
|
|
4
|
+
// dependency-free, and — critically — CROSS-VERSION STABLE, because fact ids are
|
|
5
|
+
// content-addressed by it and a fact's id is its identity across the whole memory
|
|
6
|
+
// graph. Every library candidate fails at least one of those; this eight-line
|
|
7
|
+
// function fails none. It lives here, once, so the fact-id contract has exactly
|
|
8
|
+
// one definition.
|
|
17
9
|
|
|
18
10
|
/** FNV-1a 32-bit. Returns the unsigned 32-bit integer (0 … 2^32−1). */
|
|
19
11
|
export function fnv1a32(str) {
|
package/src/index.mjs
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
// @polycode-projects/the-mechanical-code-talker (tmct) — library entry point.
|
|
2
2
|
//
|
|
3
|
-
// tmct began as a whole-package lift of an earlier chat surface (see README
|
|
4
|
-
// provenance). Internal module filenames and symbols were kept to preserve the
|
|
5
|
-
// shape and its green test suite; the branding throughout is now `tmct`.
|
|
6
|
-
//
|
|
7
3
|
// This entry re-exports the adapter primitives a library consumer needs. The
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
4
|
+
// movable conversational grammar lives in src/interpret/ (normalization
|
|
5
|
+
// pre-pass, the registered parsing strategies, the merge rule), while
|
|
6
|
+
// ask.mjs keeps the core primitives (resolveObject, traverse, render) and
|
|
7
|
+
// the ask() orchestration.
|
|
12
8
|
|
|
13
9
|
// Chat surface (also reachable as the `./chat` subpath export).
|
|
14
10
|
export { runChat, COMMANDS, answerCount, renderStats } from "./chat.mjs";
|
|
@@ -16,7 +12,7 @@ export { runChat, COMMANDS, answerCount, renderStats } from "./chat.mjs";
|
|
|
16
12
|
// Grammar / NL-over-graph primitives.
|
|
17
13
|
export { ask, resolveObject } from "./ask.mjs";
|
|
18
14
|
|
|
19
|
-
// The interpretation pipeline
|
|
15
|
+
// The interpretation pipeline: normalize once, run every
|
|
20
16
|
// registered strategy (grammar, keyword-spot, …) over the text, merge same-class
|
|
21
17
|
// results, surround distinct-class results — no graph access; pair it with ask()
|
|
22
18
|
// or the primitives to answer. `interpret(text, ctx)` returns the full record
|
|
@@ -29,7 +25,7 @@ export { relationKind, impactClosure } from "./codegraph.mjs";
|
|
|
29
25
|
// Tool dispatch (slash-commands and CLI tool calls route through here).
|
|
30
26
|
export { dispatchTool } from "./server.mjs";
|
|
31
27
|
|
|
32
|
-
// Conversational memory
|
|
28
|
+
// Conversational memory — tmct's OWN OWL-labelled graph under
|
|
33
29
|
// .tmct/memory/, distinct from any provider-supplied code graph.
|
|
34
30
|
export { loadMemory, appendUtterance, appendFact } from "./memory/core.mjs";
|
|
35
31
|
export { retrieveBlocks, saveBlock, rankBlocks } from "./memory/blocks.mjs";
|
package/src/init.mjs
CHANGED
|
@@ -1,29 +1,17 @@
|
|
|
1
1
|
// init.mjs — `tmct init`: the interface's onboarding surface.
|
|
2
2
|
//
|
|
3
|
-
// One command takes a bare directory
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// One command takes a bare directory to a WORKING tmct install: creates the `.tmct/`
|
|
4
|
+
// artifact tree, writes an externalised `tmct.toml`, seeds the committed tier-1 corpus
|
|
5
|
+
// into memory, and records provenance. Offline/deterministic/$0 (tiers 2-3 are additive
|
|
6
|
+
// config, never run here); idempotent and non-destructive (a benign re-init never
|
|
7
|
+
// throws); failure-tolerant seed (a missing/broken corpus degrades to unseeded, not a
|
|
8
|
+
// crash).
|
|
9
9
|
//
|
|
10
10
|
// initRepo(dir, { force?, seed?, env? }) → { created, config, seeded, ... }
|
|
11
11
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// inviolable (ROADMAP Phase 4); tiers 2-3 are additive config, never run here.
|
|
16
|
-
// - IDEMPOTENT and NON-DESTRUCTIVE. Safe to re-run. A benign re-init NEVER
|
|
17
|
-
// throws — it returns an honest result whose `message` says nothing changed.
|
|
18
|
-
// Existing `tmct.toml` and an existing seed are preserved unless `force`.
|
|
19
|
-
// - FAILURE-TOLERANT SEED. A missing/broken corpus degrades to an unseeded (but
|
|
20
|
-
// still initialised) repo — the directory scaffold and config always land.
|
|
21
|
-
//
|
|
22
|
-
// The seed marker + limit + prefer mirror src/chat.mjs's W3 bootstrap
|
|
23
|
-
// (SEED_MARKER_REL / SEED_LIMIT / SEED_PREFER) ON PURPOSE: both write the same
|
|
24
|
-
// `.tmct/memory/corpus-seed.json`, so whichever of `tmct init` and first-run
|
|
25
|
-
// bootstrap happens first wins and the other short-circuits. They are re-declared
|
|
26
|
-
// here (not imported) to keep init off chat.mjs's heavy module graph.
|
|
12
|
+
// The seed marker/limit/prefer mirror chat.mjs's W3 bootstrap constants on purpose: both
|
|
13
|
+
// write the same `.tmct/memory/corpus-seed.json`, so whichever runs first wins. Re-declared
|
|
14
|
+
// here rather than imported, to keep init off chat.mjs's heavy module graph.
|
|
27
15
|
|
|
28
16
|
import { mkdir, readFile, writeFile, stat } from "node:fs/promises";
|
|
29
17
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -56,31 +44,11 @@ export function defaultConfig() {
|
|
|
56
44
|
};
|
|
57
45
|
}
|
|
58
46
|
|
|
59
|
-
/** `tmct init --with-persona <name>` presets
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* - `human` — the NEW implicit default (src/extensions.mjs's
|
|
65
|
-
* BUILTIN_EXTENSIONS already ships `human` active and `seon`/`conceptnet`
|
|
66
|
-
* inactive) made EXPLICIT: an empty `extensions` override (nothing to
|
|
67
|
-
* add — the builtin defaults already are this persona) plus an explicit
|
|
68
|
-
* `[bias] human = 1.0`, so a repo that asks for `--with-persona human`
|
|
69
|
-
* has a self-documenting tmct.toml rather than relying on an unstated
|
|
70
|
-
* implicit default — the exact same discipline `code` below already used
|
|
71
|
-
* for the OLD default.
|
|
72
|
-
* - `code` — TODAY'S OLD IMPLICIT DEFAULT, now something a repo must
|
|
73
|
-
* request explicitly: re-activates `seon`+`conceptnet` (both now
|
|
74
|
-
* shipped inactive) and sets their bias, for a caller (e.g. seonix's own
|
|
75
|
-
* code-domain chat surface) that still wants the software-domain seed.
|
|
76
|
-
* - `empty` — the advanced escape hatch (PLAN_SEED.md §7): deactivates the
|
|
77
|
-
* one bundle now active by default (`human`), leaving a repo genuinely
|
|
78
|
-
* empty of corpus facts for a consumer bringing its own ontology/lexicon/
|
|
79
|
-
* corpus. Does NOT reduce npm package size (§7's own documented caveat —
|
|
80
|
-
* `corpus/` ships unconditionally either way).
|
|
81
|
-
*
|
|
82
|
-
* Kept minimal on purpose — this batch's job is the persona SEAM, not a
|
|
83
|
-
* curated library of presets. */
|
|
47
|
+
/** `tmct init --with-persona <name>` presets: a named bundle of `extensions`/`bias`
|
|
48
|
+
* overrides written into tmct.toml. `human` makes the implicit default explicit; `code`
|
|
49
|
+
* re-activates the software-domain `seon`+`conceptnet` bundles; `empty` deactivates
|
|
50
|
+
* `human`, leaving a repo genuinely empty of corpus facts. Kept minimal on purpose —
|
|
51
|
+
* the persona seam, not a curated library of presets. */
|
|
84
52
|
export const PERSONA_PRESETS = Object.freeze({
|
|
85
53
|
human: { extensions: {}, bias: { human: 1.0 } },
|
|
86
54
|
code: { extensions: { seon: { active: true }, conceptnet: { active: true } }, bias: { seon: 1.0, conceptnet: 1.0 } },
|
|
@@ -148,18 +116,12 @@ enabled = ${seed.enabled ? "true" : "false"}
|
|
|
148
116
|
# To cap it, uncomment and set a number (definitional band first):
|
|
149
117
|
${seed.limit != null ? `limit = ${Number(seed.limit)}` : "# limit = 500"}
|
|
150
118
|
`;
|
|
151
|
-
// [memory] backend —
|
|
152
|
-
// explicit `tmct init --memory-backend <...>`, or a manual override); the
|
|
153
|
-
// plain zero-flag `tmct init` output stays BYTE-IDENTICAL to before this
|
|
154
|
-
// knob existed, same discipline as the extras block below. "default" is
|
|
155
|
-
// written out explicitly rather than omitted, so `--memory-backend default`
|
|
156
|
-
// leaves a self-documenting trace of the choice (mirrors --with-persona's
|
|
157
|
-
// own "make the default explicit" behaviour).
|
|
119
|
+
// [memory] backend — only emitted when a caller actually supplies it.
|
|
158
120
|
let out = base;
|
|
159
121
|
if (config.memory && config.memory.backend !== undefined) {
|
|
160
122
|
out += `
|
|
161
123
|
[memory]
|
|
162
|
-
# Storage backend for taught facts + the memory graph
|
|
124
|
+
# Storage backend for taught facts + the memory graph.
|
|
163
125
|
# Precedence: --memory-backend flag > TMCT_MEMORY_BACKEND env > this file >
|
|
164
126
|
# "default" (the built-in fallback).
|
|
165
127
|
# "default" — the flat OWL-labelled JSON file under .tmct/memory/. The default.
|
|
@@ -169,12 +131,7 @@ backend = ${JSON.stringify(config.memory.backend)}
|
|
|
169
131
|
`;
|
|
170
132
|
}
|
|
171
133
|
|
|
172
|
-
// Extension-pack / bias sections
|
|
173
|
-
// caller actually supplies them (an explicit `--with-persona`, or a manual
|
|
174
|
-
// override); the plain zero-flag `tmct init` output stays BYTE-IDENTICAL to
|
|
175
|
-
// before this feature existed. Rendered via smol-toml's own stringify (not
|
|
176
|
-
// hand-written prose like the base file above) — a plain, uncommented
|
|
177
|
-
// config fragment is honest about being machine-written/round-tripped.
|
|
134
|
+
// Extension-pack / bias sections — only emitted when a caller supplies them.
|
|
178
135
|
const extras = {};
|
|
179
136
|
if (config.extensions !== undefined) extras.extensions = config.extensions;
|
|
180
137
|
if (config.bias !== undefined) extras.bias = config.bias;
|
|
@@ -204,32 +161,20 @@ function seedRequested({ optSeed, configEnabled, env }) {
|
|
|
204
161
|
*
|
|
205
162
|
* @param {string} dir target directory (a repo root, or a host package root).
|
|
206
163
|
* @param {object} [opts]
|
|
207
|
-
* @param {boolean} [opts.force] re-write tmct.toml + re-record provenance even
|
|
208
|
-
*
|
|
209
|
-
* @param {boolean} [opts.seed] force seeding on/off
|
|
210
|
-
*
|
|
211
|
-
* @param {object} [opts.
|
|
212
|
-
*
|
|
213
|
-
* @param {
|
|
214
|
-
*
|
|
215
|
-
* (
|
|
216
|
-
* and unknown-name validation are the CALLER'S job (bin/tmct.mjs) — this
|
|
217
|
-
* only ever sees an already-resolved preset object (or nothing). Has no
|
|
218
|
-
* effect when tmct.toml already exists and `force` isn't set (the existing
|
|
219
|
-
* "preserve a user's tmct.toml" rule wins, same as `seed`/`corpus.tier`).
|
|
220
|
-
* @param {string} [opts.memoryBackend] "default" | "memory" | "sqlite" — merged
|
|
221
|
-
* into the FRESH config's `[memory] backend` before it's written (same
|
|
222
|
-
* "fresh write only" rule as `persona`, above; `tmct init --memory-backend
|
|
223
|
-
* <...>` on an ALREADY-initialized repo is bin/tmct.mjs's own job, mirroring
|
|
224
|
-
* how `--graph` amends an existing tmct.toml post-hoc). Also selects which
|
|
225
|
-
* backend the corpus SEED below (step 3) writes into — src/memory/core.mjs's
|
|
226
|
-
* `openMemoryBackend`, the same resolver chat.mjs's createSession uses, so a
|
|
227
|
-
* seeded fact and a later chat-taught fact always land in the same store.
|
|
164
|
+
* @param {boolean} [opts.force] re-write tmct.toml + re-record provenance even when
|
|
165
|
+
* already initialised (never deletes memory/seed data).
|
|
166
|
+
* @param {boolean} [opts.seed] force seeding on/off (TMCT_NO_SEED still vetoes).
|
|
167
|
+
* @param {object} [opts.env] environment (for TMCT_NO_SEED); defaults to process.env.
|
|
168
|
+
* @param {object} [opts.persona] a resolved PERSONA_PRESETS entry ({extensions?, bias?})
|
|
169
|
+
* merged into a FRESH config only; name resolution is the caller's job (bin/tmct.mjs).
|
|
170
|
+
* @param {string} [opts.memoryBackend] "default" | "memory" | "sqlite" — merged into a
|
|
171
|
+
* FRESH config's `[memory] backend`, and selects which backend the corpus seed writes
|
|
172
|
+
* into (via src/memory/core.mjs's openMemoryBackend).
|
|
228
173
|
* @returns {Promise<{
|
|
229
174
|
* created: string[], config: object, seeded: boolean,
|
|
230
175
|
* alreadyInitialized: boolean, seedResult: (object|null), message: string
|
|
231
|
-
* }>} `created` lists the
|
|
232
|
-
*
|
|
176
|
+
* }>} `created` lists the absolute paths this call brought into being. Never throws on
|
|
177
|
+
* a benign re-init or a corpus failure.
|
|
233
178
|
*/
|
|
234
179
|
export async function initRepo(dir, { force = false, seed, env = process.env, persona = null, memoryBackend = null } = {}) {
|
|
235
180
|
const root = resolve(dir);
|
|
@@ -255,11 +200,7 @@ export async function initRepo(dir, { force = false, seed, env = process.env, pe
|
|
|
255
200
|
|
|
256
201
|
// ---- 2. The externalised config (preserve an existing file unless force) ----
|
|
257
202
|
let config = defaultConfig();
|
|
258
|
-
// Persona overrides
|
|
259
|
-
// field (e.g. `code`'s `extensions: {}`) is a genuine no-op, never an
|
|
260
|
-
// explicit-empty-section write (renderTomlConfig only emits [extensions]/
|
|
261
|
-
// [bias] when the merged config actually carries a non-empty one — the
|
|
262
|
-
// plain zero-flag `tmct init` output stays byte-identical either way).
|
|
203
|
+
// Persona overrides apply only to a fresh write.
|
|
263
204
|
if (persona) {
|
|
264
205
|
if (persona.extensions && Object.keys(persona.extensions).length) config.extensions = persona.extensions;
|
|
265
206
|
if (persona.bias && Object.keys(persona.bias).length) config.bias = persona.bias;
|
|
@@ -270,21 +211,11 @@ export async function initRepo(dir, { force = false, seed, env = process.env, pe
|
|
|
270
211
|
await writeFile(paths.toml, renderTomlConfig(config));
|
|
271
212
|
if (!tomlPresent) created.push(paths.toml);
|
|
272
213
|
} else {
|
|
273
|
-
// Honour the user's committed tmct.toml — read its knobs back
|
|
274
|
-
// config (and the seed decision) reflect what's actually on disk.
|
|
214
|
+
// Honour the user's committed tmct.toml — read its knobs back into the returned config.
|
|
275
215
|
config = await readWrittenConfig(paths.toml, config);
|
|
276
216
|
}
|
|
277
217
|
|
|
278
218
|
// ---- 3. Seed the committed corpus (offline, failure-tolerant) ----
|
|
279
|
-
// DELIBERATE BUG FIX (this batch): `tmct init`'s zero-flag seed step used to
|
|
280
|
-
// seed ONLY the ConceptNet band, never the curated SEON ontology — unlike
|
|
281
|
-
// chat.mjs's own first-run bootstrap (seedBootstrapMemory), which has always
|
|
282
|
-
// seeded BOTH. Both now go through the SAME unified loop
|
|
283
|
-
// (src/extensions.mjs's resolveExtensions + seedActiveCorpusEntries), so
|
|
284
|
-
// `tmct init`'s seed matches chat's bootstrap exactly — this changes
|
|
285
|
-
// `initRepo`'s seeded fact COUNT (test/init.test.mjs's seed-count assertions
|
|
286
|
-
// were updated for the larger post-fix totals, deliberately, in the same
|
|
287
|
-
// commit as this fix).
|
|
288
219
|
let seeded = false;
|
|
289
220
|
let seedResult = null;
|
|
290
221
|
let seedNote = "";
|
|
@@ -296,17 +227,8 @@ export async function initRepo(dir, { force = false, seed, env = process.env, pe
|
|
|
296
227
|
} else if ((await exists(paths.marker)) && !force) {
|
|
297
228
|
seedNote = "seed skipped (already seeded — marker present)";
|
|
298
229
|
} else {
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
// `config.memory.backend`. A `tmct init --memory-backend sqlite` repo ended
|
|
302
|
-
// up with its corpus facts trapped in an inert .tmct/memory/graph.json that
|
|
303
|
-
// a sqlite-backend chat session (createSession, which IS backend-aware)
|
|
304
|
-
// createSession resolves it — src/memory/core.mjs's openMemoryBackend — so
|
|
305
|
-
// the seed lands in whichever backend `config.memory.backend` actually
|
|
306
|
-
// names. "memory" is skipped outright: it's an in-process-only store that
|
|
307
|
-
// vanishes the moment this one-shot init process exits, so seeding it is
|
|
308
|
-
// pure wasted work — a later `tmct chat --memory-backend memory` opens a
|
|
309
|
-
// brand new, unrelated in-memory store anyway.
|
|
230
|
+
// Seeds into whichever backend config.memory.backend names. "memory" is skipped
|
|
231
|
+
// outright: it's an in-process-only store that vanishes when this process exits.
|
|
310
232
|
const backendChoice = String(config.memory?.backend || "").trim().toLowerCase();
|
|
311
233
|
if (backendChoice === "memory") {
|
|
312
234
|
seedNote = "seed skipped (memory backend is in-process only — nothing would persist past this command)";
|
|
@@ -316,22 +238,14 @@ export async function initRepo(dir, { force = false, seed, env = process.env, pe
|
|
|
316
238
|
try {
|
|
317
239
|
const { resolveExtensions, seedActiveCorpusEntries } = await import("./extensions.mjs");
|
|
318
240
|
const { entries } = await resolveExtensions(root);
|
|
319
|
-
// `
|
|
320
|
-
//
|
|
321
|
-
// always seeds whole) — so it overrides ONLY the resolved "conceptnet"
|
|
322
|
-
// entry's limit, exactly like the pre-fix single-corpus seed did.
|
|
241
|
+
// `[seed] limit` caps the tier-1 ConceptNet band specifically (SEON is small
|
|
242
|
+
// and always seeds whole).
|
|
323
243
|
if (config.seed?.limit != null && entries.has("conceptnet")) {
|
|
324
244
|
entries.set("conceptnet", { ...entries.get("conceptnet"), limit: Number(config.seed.limit) });
|
|
325
245
|
}
|
|
326
246
|
const { appended, skipped, total, perBundle } = await seedActiveCorpusEntries(memoryDir, entries);
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
// SEED" contract is about the SEED AS A WHOLE degrading honestly. If every
|
|
330
|
-
// active bundle failed (e.g. the memory graph file itself is unwritable —
|
|
331
|
-
// see test/init.test.mjs "seed failure degrades"), re-throw the first
|
|
332
|
-
// bundle's error so the SAME outer catch below reports the familiar "seed
|
|
333
|
-
// skipped (corpus unavailable: …)" note, rather than claiming success with
|
|
334
|
-
// zero facts actually written.
|
|
247
|
+
// If every active bundle failed, re-throw the first error so the outer catch
|
|
248
|
+
// reports it, rather than claiming success with zero facts written.
|
|
335
249
|
const bundleNames = Object.keys(perBundle);
|
|
336
250
|
const allFailed = bundleNames.length > 0 && bundleNames.every((n) => perBundle[n].error);
|
|
337
251
|
if (allFailed) throw new Error(perBundle[bundleNames[0]].error);
|
|
@@ -401,9 +315,8 @@ async function readWrittenConfig(tomlPath, base) {
|
|
|
401
315
|
if (raw.seed.enabled !== undefined) cfg.seed.enabled = Boolean(raw.seed.enabled);
|
|
402
316
|
if (raw.seed.limit !== undefined) cfg.seed.limit = Number(raw.seed.limit);
|
|
403
317
|
}
|
|
404
|
-
// Sparse pass-through
|
|
405
|
-
//
|
|
406
|
-
// normalizeConfig).
|
|
318
|
+
// Sparse pass-through — src/extensions.mjs validates; this layer just carries the
|
|
319
|
+
// raw tables through unmodified.
|
|
407
320
|
if (raw.extensions !== undefined) cfg.extensions = raw.extensions;
|
|
408
321
|
if (raw.bias !== undefined) cfg.bias = raw.bias;
|
|
409
322
|
if (raw.memory && raw.memory.backend !== undefined) {
|
package/src/interpret/fuzzy.mjs
CHANGED
|
@@ -1,22 +1,12 @@
|
|
|
1
|
-
// interpret/fuzzy.mjs — the bounded-edit-distance fuzzy tier
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// vocabulary rewrite and resolveObject's tier-5 label pass both read editDistance/
|
|
5
|
-
// fuzzyBound from here, and the "assuming you meant …" announcement discipline
|
|
6
|
-
// (a unique within-bound hit is announced, a tie is refused or surfaced as
|
|
7
|
-
// ambiguity, never a silently-broken guess) is enforced by the callers off these
|
|
8
|
-
// primitives. Deliberately coupled to the curated vocab tables via explicit
|
|
9
|
-
// imports — the fuzzy TARGETS are a closed, curated set, same ethos as the
|
|
10
|
-
// tables themselves. Pure JS, no deps.
|
|
1
|
+
// interpret/fuzzy.mjs — the bounded-edit-distance fuzzy tier, shared by the
|
|
2
|
+
// keyword-spotting and object-resolution strategies. A distance tie is always
|
|
3
|
+
// refused or surfaced as ambiguity, never broken by a guess.
|
|
11
4
|
|
|
12
5
|
import { VERB_TO_KIND, ENTITY_TO_TYPE, MODIFIER_TO_KIND } from "../ask-vocab.mjs";
|
|
13
6
|
import { STOPWORDS } from "./normalize.mjs";
|
|
14
7
|
|
|
15
|
-
// ---- bounded edit distance — hand-rolled Damerau-Levenshtein
|
|
16
|
-
//
|
|
17
|
-
// with an early row-minimum exit. Fires only after every exact/curated tier
|
|
18
|
-
// missed, and a distance TIE is refused (keyword) or surfaced as ambiguity
|
|
19
|
-
// (object), never broken by a guess. ----
|
|
8
|
+
// ---- bounded edit distance — hand-rolled Damerau-Levenshtein, bounded with an
|
|
9
|
+
// early row-minimum exit ----
|
|
20
10
|
|
|
21
11
|
/** Distance between a and b, or max+1 as soon as it provably exceeds `max`. */
|
|
22
12
|
export function editDistance(a, b, max) {
|
|
@@ -52,13 +42,9 @@ export const VOCAB_WORDS = new Set(
|
|
|
52
42
|
.flatMap((p) => p.split(" ")),
|
|
53
43
|
);
|
|
54
44
|
|
|
55
|
-
/** Fuzzy-correction TARGETS: verb-phrase and modifier constituents only,
|
|
56
|
-
* Entity nouns are
|
|
57
|
-
*
|
|
58
|
-
* "calls"-family words), and entity-noun typos are already owned by the curated
|
|
59
|
-
* MISSPELLINGS table where such calls are made deliberately. Short constituents
|
|
60
|
-
* ("of", "to", "in", "on") are excluded for the same reason: at bound 1 half of
|
|
61
|
-
* English is adjacent to them. */
|
|
45
|
+
/** Fuzzy-correction TARGETS: verb-phrase and modifier constituents only,
|
|
46
|
+
* length ≥4. Entity nouns and short words are excluded — real identifiers
|
|
47
|
+
* collide with them too easily at this distance bound. */
|
|
62
48
|
const FUZZY_TARGET_WORDS = [...new Set(
|
|
63
49
|
[...Object.keys(VERB_TO_KIND), ...Object.keys(MODIFIER_TO_KIND)]
|
|
64
50
|
.flatMap((p) => p.split(" "))
|
|
@@ -79,13 +65,8 @@ export function fuzzyVocabWord(w) {
|
|
|
79
65
|
return fuzzyMatchInSet(w, FUZZY_TARGET_WORDS, fuzzyBound(w));
|
|
80
66
|
}
|
|
81
67
|
|
|
82
|
-
/**
|
|
83
|
-
* list, or null
|
|
84
|
-
* (a distance tie between two distinct candidates is refused, never guessed),
|
|
85
|
-
* factored out so callers outside the ask/keyword pipeline (e.g. chat.mjs's
|
|
86
|
-
* conversational recognizers) can reuse the primitive without a bespoke
|
|
87
|
-
* target-list wrapper. `bound` defaults to fuzzyBound(w) but callers may pass a
|
|
88
|
-
* tighter budget (e.g. to keep short conversational tokens conservative). */
|
|
68
|
+
/** Generic unique-within-bound fuzzy match of `w` against an arbitrary
|
|
69
|
+
* candidate list, or null on a tie — never guessed. */
|
|
89
70
|
export function fuzzyMatchInSet(w, candidates, bound = fuzzyBound(w)) {
|
|
90
71
|
let best = bound + 1;
|
|
91
72
|
let hit = null;
|
package/src/interpret/merge.mjs
CHANGED
|
@@ -1,33 +1,15 @@
|
|
|
1
|
-
// interpret/merge.mjs — class-grouped merging of strategy results
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// · SAME-class candidates merge and rank — identical parses (sameParse) dedupe
|
|
8
|
-
// onto the highest-precedence strategy's candidate, exactly as the legacy
|
|
9
|
-
// merge returned "either" result on agreement; multiple DISTINCT parses
|
|
10
|
-
// surviving in the winning class are the legacy {ambiguousParse, candidates}
|
|
11
|
-
// surface, rendered by ask.mjs as "this could mean more than one thing: …".
|
|
12
|
-
// · DISTINCT-class groups produce the ambiguity SURROUND: the winning class
|
|
13
|
-
// answers, and each other class's best candidate is listed as an
|
|
14
|
-
// "if you mean X then …" line (alternateLines below) — the generalization of
|
|
15
|
-
// the engine's existing announced-correction shapes ("assuming you meant …").
|
|
16
|
-
//
|
|
17
|
-
// Winner selection is deterministic: the class holding the highest-confidence
|
|
18
|
-
// candidate wins; a confidence tie goes to the earlier-registered strategy. The
|
|
19
|
-
// two legacy strategies share one class ("graph-query"), so their merge reduces
|
|
20
|
-
// exactly to the original behavior by construction.
|
|
1
|
+
// interpret/merge.mjs — class-grouped merging of strategy results: same-class
|
|
2
|
+
// candidates merge/rank (identical parses dedupe onto the highest-precedence
|
|
3
|
+
// one; distinct ones surface as an honest {ambiguousParse} tie); distinct-class
|
|
4
|
+
// groups surface each other's best candidate as an "if you mean X then …"
|
|
5
|
+
// alternate. Winner = highest-confidence class, ties go to the
|
|
6
|
+
// earlier-registered strategy.
|
|
21
7
|
|
|
22
8
|
const DEFAULT_CONFIDENCE = 0.5;
|
|
23
9
|
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// this the two strategies would "disagree" over a word that names no different thing.
|
|
28
|
-
// A leading DETERMINER is the same kind of non-difference (0.8.2 feel wave): the
|
|
29
|
-
// anchored grammar captures "the logger" while keyword-spot captures "logger", and
|
|
30
|
-
// the resulting "ambiguity" asked the user to choose between identical readings.
|
|
10
|
+
// Term equality ignores a leading "commit " on a bare SHA and a leading
|
|
11
|
+
// determiner ("the logger" == "logger") — surface differences the two
|
|
12
|
+
// strategies otherwise disagree over despite meaning the same thing.
|
|
31
13
|
const cmpTerm = (s) => String(s || "").trim().toLowerCase().replace(/\s+/g, " ")
|
|
32
14
|
.replace(/^(?:the|a|an)\s+/, "")
|
|
33
15
|
.replace(/^commit\s+(?=[0-9a-f]{7,40}$)/, "");
|