@polycode-projects/the-mechanical-code-talker 1.5.5 → 1.8.4
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 +123 -14
- package/ROADMAP.md +233 -1392
- package/bin/tmct.mjs +479 -98
- package/corpus/README.md +3 -0
- package/corpus/generated/README.md +43 -0
- package/corpus/generated/ace-surface-variants.jsonl +17 -0
- package/corpus/generated/manifest.json +9 -0
- package/corpus/tier2/generate.mjs +14668 -0
- package/corpus/tier2/human-examples-large.jsonl +1928 -0
- package/corpus/tier2/human-examples-medium.jsonl +356 -0
- package/corpus/tier2/human-examples.jsonl +120 -0
- package/corpus/tier2/human-large.jsonl +12001 -0
- package/corpus/tier2/human-medium.jsonl +944 -0
- package/corpus/tier2/human.jsonl +664 -0
- package/corpus/tier2/manifest.json +42 -0
- package/package.json +14 -8
- package/src/answer-variants.json +47 -0
- package/src/answer-variants.mjs +67 -0
- package/src/ask-browser-entry.mjs +34 -0
- package/src/ask-browser.bundle.js +5095 -0
- package/src/ask-vocab.mjs +93 -8
- package/src/ask.mjs +451 -49
- package/src/chat.mjs +1273 -137
- package/src/cli-args.mjs +164 -0
- package/src/codegraph.mjs +170 -32
- package/src/extensions.mjs +100 -19
- package/src/grammar/ace.mjs +85 -3
- package/src/grammar/lexicon-core.json +9531 -63
- package/src/grammar/lexicon.mjs +58 -8
- package/src/graph-merge.mjs +114 -0
- package/src/index.mjs +14 -0
- package/src/init.mjs +40 -14
- package/src/interpret/normalize.mjs +75 -1
- package/src/interpret/strategies/grammar.mjs +10 -0
- package/src/interpret/strategies/keywords.mjs +20 -0
- package/src/interpret/strategies/noise-strip.mjs +73 -4
- package/src/memory/core.mjs +466 -8
- package/src/router/goal-reasoner.mjs +41 -7
- package/src/router/guardrail.mjs +37 -7
- package/src/router/resolver.mjs +50 -4
- package/src/sessions.mjs +5 -1
- package/src/source.mjs +54 -1
- package/src/syllogise.mjs +398 -27
- package/src/toml-config.mjs +13 -4
- package/src/viz.mjs +541 -0
package/src/cli-args.mjs
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// cli-args.mjs — the ONE shared argv/config resolver for bin/tmct.mjs's
|
|
2
|
+
// subcommands. Replaces four independently hand-rolled `configFor` copies
|
|
3
|
+
// (bin/tmct.mjs's cli-mode and serve-mode ones, chat.mjs's own, and the richer
|
|
4
|
+
// 4-tier version inlined in chat.mjs's createSession) with a single, tested
|
|
5
|
+
// precedence chain — built on top of toml-config.mjs's already-tested
|
|
6
|
+
// mergeEffective/normalizeConfig (arg > toml > default), not a rebuild of it.
|
|
7
|
+
//
|
|
8
|
+
// Three tiny flag helpers (pure, no I/O) plus the one async resolver:
|
|
9
|
+
// strFlag(rest, names, dflt) → single value, last flag occurrence wins
|
|
10
|
+
// repeatedFlag(rest, names) → every value for a repeatable flag (e.g. --graph)
|
|
11
|
+
// boolFlag(rest, names) → true if any of `names` appears at all
|
|
12
|
+
// resolveRuntimeConfig({argv, cwd, env, gitRoot}) → the resolved repo/config
|
|
13
|
+
//
|
|
14
|
+
// Graph-path precedence (documented once, here — every subcommand shares it):
|
|
15
|
+
// 1. --graph <path> (repeatable; --graph wins outright, even over env)
|
|
16
|
+
// 2. TMCT_GRAPH_FILE (env)
|
|
17
|
+
// 3. tmct.toml's `graph_file` / `graph_files`
|
|
18
|
+
// 4. <repo>/.tmct/graph.json, where repo is --repo, else git root, else cwd
|
|
19
|
+
//
|
|
20
|
+
// Deliberately does NOT import chat.mjs (that would be circular once chat.mjs
|
|
21
|
+
// itself is rewired to call resolveRuntimeConfig) — the git-root lookup below
|
|
22
|
+
// is a small, self-contained copy of chat.mjs's own gitToplevel().
|
|
23
|
+
|
|
24
|
+
import { spawnSync } from "node:child_process";
|
|
25
|
+
import { dirname, resolve } from "node:path";
|
|
26
|
+
import { stat } from "node:fs/promises";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import { loadTomlConfig, normalizeConfig, mergeEffective, CONFIG_FILE } from "./toml-config.mjs";
|
|
29
|
+
import { DEFAULT_GRAPH_REL } from "./config.mjs";
|
|
30
|
+
|
|
31
|
+
/** The git top-level for `cwd`, or null if not in a repo (or git is
|
|
32
|
+
* unavailable). A deliberate re-declaration of chat.mjs's gitToplevel (not an
|
|
33
|
+
* import) — see the file docblock for why. */
|
|
34
|
+
function defaultGitRoot(cwd) {
|
|
35
|
+
try {
|
|
36
|
+
const r = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
|
|
37
|
+
if (r.status === 0) { const p = String(r.stdout || "").trim(); return p || null; }
|
|
38
|
+
} catch { /* git missing / not a repo — fall back to cwd */ }
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const asList = (names) => (Array.isArray(names) ? names : [names]);
|
|
43
|
+
|
|
44
|
+
/** Single-value flag, LAST occurrence wins (so `--repo a --repo b` → "b").
|
|
45
|
+
* `names` may be a single flag string or an array of aliases. */
|
|
46
|
+
export function strFlag(rest, names, dflt) {
|
|
47
|
+
const list = asList(names);
|
|
48
|
+
let val = dflt;
|
|
49
|
+
for (let i = 0; i < rest.length; i++) {
|
|
50
|
+
if (list.includes(rest[i]) && rest[i + 1] !== undefined) val = rest[i + 1];
|
|
51
|
+
}
|
|
52
|
+
return val;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Repeatable flag — every value across every occurrence, in argv order
|
|
56
|
+
* (`--graph a --graph b` → `["a","b"]`). Empty array when absent. */
|
|
57
|
+
export function repeatedFlag(rest, names) {
|
|
58
|
+
const list = asList(names);
|
|
59
|
+
const out = [];
|
|
60
|
+
for (let i = 0; i < rest.length; i++) {
|
|
61
|
+
if (list.includes(rest[i]) && rest[i + 1] !== undefined) out.push(rest[i + 1]);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Presence flag — true if any of `names` appears anywhere in `rest`. */
|
|
67
|
+
export function boolFlag(rest, names) {
|
|
68
|
+
const list = asList(names);
|
|
69
|
+
return rest.some((r) => list.includes(r));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve one subcommand invocation's repo root, tmct.toml, and graph
|
|
74
|
+
* path(s) — the shared precedence chain every subcommand (chat/memory/init/
|
|
75
|
+
* import/syllogise/serve) now funnels through instead of re-deriving it.
|
|
76
|
+
*
|
|
77
|
+
* @param {object} opts
|
|
78
|
+
* @param {string[]} [opts.argv] the subcommand's own argv tail (already past
|
|
79
|
+
* `tmct <mode>` — the same slice each subcommand hand-parses today).
|
|
80
|
+
* @param {string} [opts.cwd]
|
|
81
|
+
* @param {object} [opts.env]
|
|
82
|
+
* @param {(cwd:string)=>string|null} [opts.gitRoot] injectable for tests,
|
|
83
|
+
* mirrors createSession's own `gitRoot` param.
|
|
84
|
+
* @param {object} [opts.args] extra already-parsed args to fold into
|
|
85
|
+
* mergeEffective's "arg" tier (e.g. a subcommand's own flags) — optional,
|
|
86
|
+
* defaults to {}.
|
|
87
|
+
* @returns {Promise<{
|
|
88
|
+
* repo: string, configPath: string, toml: object,
|
|
89
|
+
* config: {graphFile: string, graphFiles: string[]},
|
|
90
|
+
* effective: object, sources: object,
|
|
91
|
+
* }>}
|
|
92
|
+
*/
|
|
93
|
+
export async function resolveRuntimeConfig({
|
|
94
|
+
argv = [],
|
|
95
|
+
cwd = process.cwd(),
|
|
96
|
+
env = process.env,
|
|
97
|
+
gitRoot = defaultGitRoot,
|
|
98
|
+
args = {},
|
|
99
|
+
} = {}) {
|
|
100
|
+
const configFlag = strFlag(argv, ["--config"]);
|
|
101
|
+
const repoFlag = strFlag(argv, ["--repo"]);
|
|
102
|
+
const graphFlags = repeatedFlag(argv, ["--graph"]);
|
|
103
|
+
|
|
104
|
+
const root = typeof gitRoot === "function" ? gitRoot(cwd) : null;
|
|
105
|
+
const repo = repoFlag ? resolve(cwd, repoFlag) : (root || cwd);
|
|
106
|
+
|
|
107
|
+
// --config <path>: a file OR a directory. A directory means "look for
|
|
108
|
+
// tmct.toml under here"; a file is the tmct.toml itself, wherever it lives.
|
|
109
|
+
let configDir = repo;
|
|
110
|
+
let configFileOverride;
|
|
111
|
+
if (configFlag) {
|
|
112
|
+
const abs = resolve(cwd, configFlag);
|
|
113
|
+
let isDir = false;
|
|
114
|
+
try { isDir = (await stat(abs)).isDirectory(); } catch { /* missing path — treat as a file target */ }
|
|
115
|
+
if (isDir) configDir = abs;
|
|
116
|
+
else { configFileOverride = abs; configDir = dirname(abs); }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const raw = await loadTomlConfig(configDir, configFileOverride ? { file: configFileOverride } : {});
|
|
120
|
+
const toml = await normalizeConfig(raw, { configDir });
|
|
121
|
+
|
|
122
|
+
const defaultGraphFile = join(repo, DEFAULT_GRAPH_REL);
|
|
123
|
+
const envGraph = env && env.TMCT_GRAPH_FILE && String(env.TMCT_GRAPH_FILE).trim();
|
|
124
|
+
|
|
125
|
+
let graphFiles;
|
|
126
|
+
let graphSource; // "arg" | "env" | "tmct.toml" | "default"
|
|
127
|
+
if (graphFlags.length) {
|
|
128
|
+
graphFiles = graphFlags.map((g) => resolve(cwd, g));
|
|
129
|
+
graphSource = "arg";
|
|
130
|
+
} else if (envGraph) {
|
|
131
|
+
graphFiles = [resolve(cwd, envGraph)];
|
|
132
|
+
graphSource = "env";
|
|
133
|
+
} else if (toml.graphFiles && toml.graphFiles.length) {
|
|
134
|
+
graphFiles = toml.graphFiles;
|
|
135
|
+
graphSource = "tmct.toml";
|
|
136
|
+
} else if (toml.graphFile) {
|
|
137
|
+
graphFiles = [toml.graphFile];
|
|
138
|
+
graphSource = "tmct.toml";
|
|
139
|
+
} else {
|
|
140
|
+
graphFiles = [defaultGraphFile];
|
|
141
|
+
graphSource = "default";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// mergeEffective wires in toml-config.mjs's already-tested arg>toml>default
|
|
145
|
+
// precedence for every OTHER knob (index/tune/corpus/…); graphFile's own
|
|
146
|
+
// precedence is richer (it has an env tier mergeEffective doesn't model), so
|
|
147
|
+
// it's resolved above and folded back in below rather than re-derived here.
|
|
148
|
+
const defaults = { graphFile: defaultGraphFile };
|
|
149
|
+
const { effective, sources } = mergeEffective({ args, toml, defaults });
|
|
150
|
+
effective.graphFile = graphFiles[0];
|
|
151
|
+
sources.graphFile = graphSource;
|
|
152
|
+
if (graphFiles.length > 1) effective.graphFiles = graphFiles;
|
|
153
|
+
|
|
154
|
+
const configPath = configFileOverride || join(configDir, CONFIG_FILE);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
repo,
|
|
158
|
+
configPath,
|
|
159
|
+
toml,
|
|
160
|
+
config: { graphFile: graphFiles[0], graphFiles },
|
|
161
|
+
effective,
|
|
162
|
+
sources,
|
|
163
|
+
};
|
|
164
|
+
}
|
package/src/codegraph.mjs
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { lookupByProseTokens, proseLayerHits } from "./prose.mjs";
|
|
2
2
|
import { cosine } from "./embed.mjs";
|
|
3
|
+
// Single-sourced predicate strings (memory/core.mjs owns these constants) — no
|
|
4
|
+
// circular-import risk: core.mjs imports trust.mjs/shacl.mjs/planning.mjs, never
|
|
5
|
+
// codegraph.mjs, in either direction.
|
|
6
|
+
import { CREATED_AT_PROP, UPDATED_AT_PROP } from "./memory/core.mjs";
|
|
3
7
|
|
|
4
8
|
// Pure (no-network, no-fs) query logic over the typed `entities` payload that the
|
|
5
9
|
// deterministic indexer writes to <repo>/.tmct/graph.json (shape produced by
|
|
@@ -91,6 +95,13 @@ const PROP_KIND = {
|
|
|
91
95
|
"mg:defines": "defines",
|
|
92
96
|
"mg:tests": "tests",
|
|
93
97
|
"mg:touches": "touches",
|
|
98
|
+
// memory-graph predicates (src/memory/core.mjs, src/sessions.mjs) — each maps to
|
|
99
|
+
// itself as its own kind name (no module-rollup abbreviation needed, unlike
|
|
100
|
+
// imports/calls) so adjacencyForKinds/edgesOfKind can walk the memory graph too.
|
|
101
|
+
"mgx:saidinsession": "saidInSession",
|
|
102
|
+
"mgx:inreplyto": "inReplyTo",
|
|
103
|
+
"mgx:statedby": "statedBy",
|
|
104
|
+
"mgx:canonicalisedfrom": "canonicalisedFrom",
|
|
94
105
|
};
|
|
95
106
|
|
|
96
107
|
export function relationKind(group) {
|
|
@@ -579,6 +590,12 @@ const SPIRAL_DEPTH_DEFAULT = 3;
|
|
|
579
590
|
const SPIRAL_NODE_LIMIT_DEFAULT = 12;
|
|
580
591
|
const SPIRAL_Q_DEFAULT = 0.9; // mild hub pruning (drop only the densest 10%) — the centre point
|
|
581
592
|
const SPIRAL_EXPAND_KINDS = ["imports", "calls", "callsSymbol", "inherits"]; // cochange dropped
|
|
593
|
+
// The memory graph's real edge-kind inventory (traced via every objectProperties.push/.find
|
|
594
|
+
// site in src/memory/*.mjs and src/sessions.mjs) — the `kinds` a memory-graph spiralExpand call
|
|
595
|
+
// passes so it walks Session/Fact/Source/Utterance individuals rather than code-graph Modules.
|
|
596
|
+
// NOTE: mgx:asksAbout (src/sessions.mjs) is deliberately EXCLUDED — that predicate lives in the
|
|
597
|
+
// CODE graph (Session ↔ code entities a chat turn resolved/answered), not the memory graph.
|
|
598
|
+
export const MEMORY_SPIRAL_EXPAND_KINDS = ["saidInSession", "inReplyTo", "statedBy", "canonicalisedFrom"];
|
|
582
599
|
const SPIRAL_EMIT_FRAC = 0.5; // a newly-surfaced node's base score = maxSeed × this …
|
|
583
600
|
const SPIRAL_HOP_DECAY = 0.6; // … decayed by this per hop from the seeds (bounded < maxSeed, so a walked-in node never dominates rank 1)
|
|
584
601
|
const SPIRAL_PROX_FRAC = 0.2; // an ALREADY-matched module the spiral re-reaches gets a bounded nudge …
|
|
@@ -623,8 +640,13 @@ function identComponents(name) {
|
|
|
623
640
|
/** For one edge-kind group, the depth-1 successor of `fromId` reachable via any edge in `kinds`,
|
|
624
641
|
* as a Map<moduleId, neighbourModuleId> adjacency (undirected — a module's neighbours via that
|
|
625
642
|
* kind, in either edge direction). Endpoints are mapped to their containing module first (call
|
|
626
|
-
* edges live at function granularity), matching the existing E1a call-adjacency convention.
|
|
627
|
-
|
|
643
|
+
* edges live at function granularity), matching the existing E1a call-adjacency convention.
|
|
644
|
+
* `idNormalizer` (default null) lets a caller fold edge endpoints some OTHER way — the memory
|
|
645
|
+
* graph has no "containing module" concept, so a memory-graph caller passes `(id) => id` to walk
|
|
646
|
+
* its raw individual ids unchanged. Defaulting to null (rather than `moduleIdOfId` directly)
|
|
647
|
+
* keeps the sole existing caller (`adjacencyForKinds(graph, kinds)` in beamExpand) byte-identical. */
|
|
648
|
+
export function adjacencyForKinds(graph, kinds, idNormalizer = null) {
|
|
649
|
+
const norm = idNormalizer || ((id) => moduleIdOfId(graph, id));
|
|
628
650
|
const adj = new Map();
|
|
629
651
|
const link = (a, b) => {
|
|
630
652
|
if (!a || !b || a === b) return;
|
|
@@ -635,7 +657,7 @@ function adjacencyForKinds(graph, kinds) {
|
|
|
635
657
|
};
|
|
636
658
|
for (const kind of kinds) {
|
|
637
659
|
for (const e of edgesOfKind(graph, kind)) {
|
|
638
|
-
link(
|
|
660
|
+
link(norm(e.subject), norm(e.object));
|
|
639
661
|
}
|
|
640
662
|
}
|
|
641
663
|
return adj;
|
|
@@ -707,24 +729,55 @@ function beamExpand(graph, scored, beamWidth) {
|
|
|
707
729
|
}
|
|
708
730
|
|
|
709
731
|
/** SPIRAL expansion (opt-in; see the SPIRAL_* constants' comment above for the full design).
|
|
710
|
-
* A deterministic bounded-radius ego walk from the lexical seeds (`scored
|
|
711
|
-
* fewest-arcs-first via a min-heap keyed (hop ASC,
|
|
712
|
-
* degree-quantile hub gate at each expansion step. Emits
|
|
713
|
-
* in pop order, scoring each seed-relative and bounded so
|
|
732
|
+
* A deterministic bounded-radius ego walk from the lexical seeds (`scored`, or an explicit
|
|
733
|
+
* `seeds` override — see below), popped fewest-arcs-first via a min-heap keyed (hop ASC,
|
|
734
|
+
* in-graph degree ASC, id ASC), with a degree-quantile hub gate at each expansion step. Emits
|
|
735
|
+
* up to `nodeLimit` newly-reached nodes in pop order, scoring each seed-relative and bounded so
|
|
736
|
+
* a hub can't dominate rank 1.
|
|
714
737
|
* CRITICAL vs beamExpand: it deliberately OMITS the `if (!baseScore.has) continue` guard, so it
|
|
715
738
|
* MAY push modules that had NO lexical match into `scored` — the one path to breaking the lexical
|
|
716
|
-
* ceiling. Mutates `scored` (nudges re-reached matches in place; APPENDS newly-surfaced modules)
|
|
717
|
-
* Pure otherwise (no fs/network); deterministic total
|
|
718
|
-
|
|
719
|
-
|
|
739
|
+
* ceiling. Mutates `scored` (nudges re-reached matches in place; APPENDS newly-surfaced modules)
|
|
740
|
+
* when the score-nudge machinery is active. Pure otherwise (no fs/network); deterministic total
|
|
741
|
+
* ordering throughout.
|
|
742
|
+
*
|
|
743
|
+
* Generalised (2026-07-11) past its original code-graph-only, `scored`-only shape so a pure
|
|
744
|
+
* graph-visualisation walk (no lexical match list at all) can reuse the exact same traversal:
|
|
745
|
+
* - `scored` is now OPTIONAL (default `[]`) — a bare walk with no ranking machinery.
|
|
746
|
+
* - `kinds` (default `SPIRAL_EXPAND_KINDS`) — the edge-kind set to walk; a memory-graph caller
|
|
747
|
+
* passes `MEMORY_SPIRAL_EXPAND_KINDS`.
|
|
748
|
+
* - `classPredicate` (default `(ind) => (ind.class || "") === "Module"`) — replaces the two
|
|
749
|
+
* hardcoded `"Module"` checks below, so a memory-graph caller can pass `() => true` (every
|
|
750
|
+
* class walkable) or any other individual filter.
|
|
751
|
+
* - `idNormalizer` (default `null`) — threaded straight into the internal `adjacencyForKinds`
|
|
752
|
+
* call; a memory-graph caller passes `(id) => id` (no module-folding).
|
|
753
|
+
* - `seeds` (default derived from `scored`, as before) — an explicit id iterable, so a caller
|
|
754
|
+
* with no `scored` list at all (e.g. `mostRecentIndividual`'s single seed) can still drive
|
|
755
|
+
* the walk.
|
|
756
|
+
* The score-nudge machinery (mutating `scored`/introducing newly-surfaced individuals into it)
|
|
757
|
+
* is gated behind `scored.length > 0 && maxSeed > 0` — the exact condition the original early
|
|
758
|
+
* return checked — so an empty `scored` degrades gracefully into a pure walk rather than erroring.
|
|
759
|
+
* Returns `[{id, hop}]` for every node the walk actually pops (seeds included, at hop 0) — this
|
|
760
|
+
* used to return `undefined`; safe, since the sole caller (`scoreModules`) already discards the
|
|
761
|
+
* return value (confirmed by inspection, not assumed). */
|
|
762
|
+
export function spiralExpand(graph, scored = [], {
|
|
763
|
+
depth = SPIRAL_DEPTH_DEFAULT,
|
|
764
|
+
q = SPIRAL_Q_DEFAULT,
|
|
765
|
+
nodeLimit = SPIRAL_NODE_LIMIT_DEFAULT,
|
|
766
|
+
kinds = SPIRAL_EXPAND_KINDS,
|
|
767
|
+
classPredicate = (ind) => (ind.class || "") === "Module",
|
|
768
|
+
idNormalizer = null,
|
|
769
|
+
seeds: seedsOpt = null,
|
|
770
|
+
} = {}) {
|
|
720
771
|
const byId = new Map(scored.map((s) => [s.ind.id, s]));
|
|
721
|
-
const seeds = new Set(byId.keys());
|
|
722
772
|
let maxSeed = 0;
|
|
723
773
|
for (const s of scored) maxSeed = Math.max(maxSeed, s.score);
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
774
|
+
const nudgeActive = scored.length > 0 && maxSeed > 0; // the original "!(maxSeed > 0) → return" guard, now a gate rather than a bail-out
|
|
775
|
+
const seeds = new Set(seedsOpt != null ? seedsOpt : byId.keys());
|
|
776
|
+
if (!seeds.size) return [];
|
|
777
|
+
// Combined undirected adjacency over the expansion kinds (cochange dropped for the code-graph
|
|
778
|
+
// default). In-graph degree = neighbour count over these kinds — the "arcs" the frontier orders
|
|
779
|
+
// and the quantile gate reads.
|
|
780
|
+
const adj = adjacencyForKinds(graph, kinds, idNormalizer);
|
|
728
781
|
const degree = (id) => (adj.get(id)?.size || 0);
|
|
729
782
|
// Binary min-heap over the frontier, keyed (hop ASC, degree ASC, id ASC) — pop the closest,
|
|
730
783
|
// least-connected node first, so expansion fans through the sparse surroundings and fizzles at
|
|
@@ -757,35 +810,41 @@ function spiralExpand(graph, scored, { depth = SPIRAL_DEPTH_DEFAULT, q = SPIRAL_
|
|
|
757
810
|
};
|
|
758
811
|
const visited = new Set(seeds);
|
|
759
812
|
for (const id of seeds) push({ id, hop: 0, deg: degree(id) });
|
|
760
|
-
|
|
813
|
+
let defIdx = null; // lazy: only the nudge-active path (newly-surfaced individuals) needs this
|
|
761
814
|
let emitted = 0;
|
|
815
|
+
const results = [];
|
|
762
816
|
while (heap.length && emitted < nodeLimit) {
|
|
763
817
|
const node = pop();
|
|
818
|
+
results.push({ id: node.id, hop: node.hop });
|
|
764
819
|
if (!seeds.has(node.id)) {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
existing
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
820
|
+
if (nudgeActive) {
|
|
821
|
+
// Slot this newly-reached node: seed-relative base, hop-decayed and bounded below maxSeed.
|
|
822
|
+
const emitScore = maxSeed * SPIRAL_EMIT_FRAC * Math.pow(SPIRAL_HOP_DECAY, node.hop - 1);
|
|
823
|
+
const existing = byId.get(node.id);
|
|
824
|
+
if (existing) { // already lexically matched (below-k) → bounded nudge, never a replacement
|
|
825
|
+
existing.score += Math.min(emitScore * SPIRAL_PROX_FRAC, existing.score * SPIRAL_PROX_CAP_FRAC);
|
|
826
|
+
} else { // lexically INVISIBLE → introduce it (the beam structurally cannot)
|
|
827
|
+
const ind = graph.byId?.get?.(node.id);
|
|
828
|
+
if (ind && classPredicate(ind)) {
|
|
829
|
+
if (!defIdx) defIdx = definesIndex(graph);
|
|
830
|
+
const defines = defIdx.get(ind.id) || [];
|
|
831
|
+
const entry = { ind, score: emitScore, defineCount: defines.length, matching: [], density: 0 };
|
|
832
|
+
scored.push(entry);
|
|
833
|
+
byId.set(node.id, entry);
|
|
834
|
+
}
|
|
777
835
|
}
|
|
778
836
|
}
|
|
779
837
|
emitted++;
|
|
780
838
|
}
|
|
781
839
|
if (node.hop >= depth) continue;
|
|
782
|
-
// This step's candidate set = the popped node's unvisited
|
|
783
|
-
// degree, keeping the lowest-degree ⌊q·n⌋ (drop the densest hubs), never
|
|
840
|
+
// This step's candidate set = the popped node's unvisited neighbours matching classPredicate;
|
|
841
|
+
// quantile-gate by degree, keeping the lowest-degree ⌊q·n⌋ (drop the densest hubs), never
|
|
842
|
+
// fewer than one.
|
|
784
843
|
const cands = [];
|
|
785
844
|
for (const nid of adj.get(node.id) || []) {
|
|
786
845
|
if (visited.has(nid)) continue;
|
|
787
846
|
const ind = graph.byId?.get?.(nid);
|
|
788
|
-
if (!ind || (ind
|
|
847
|
+
if (!ind || !classPredicate(ind)) continue; // no phantom (fn-fallback) ids
|
|
789
848
|
cands.push({ id: nid, deg: degree(nid) });
|
|
790
849
|
}
|
|
791
850
|
if (!cands.length) continue;
|
|
@@ -797,6 +856,24 @@ function spiralExpand(graph, scored, { depth = SPIRAL_DEPTH_DEFAULT, q = SPIRAL_
|
|
|
797
856
|
push({ id: c.id, hop: node.hop + 1, deg: c.deg });
|
|
798
857
|
}
|
|
799
858
|
}
|
|
859
|
+
return results;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/** The individual with the most recent `createdAtProp` attribute — item 1's ("Traversal") seed
|
|
863
|
+
* default: "sort memory-graph individuals by mgx:createdAt descending, seed from the single most
|
|
864
|
+
* recent." Deterministic tie-break by id (lowest id wins) when two individuals share the exact
|
|
865
|
+
* same timestamp — same total-order convention `spiralExpand`'s own heap uses. Null when no
|
|
866
|
+
* individual carries the attribute at all (empty graph, or a graph that predates timestamps).
|
|
867
|
+
* ISO-8601 timestamps compare correctly as plain strings (same zero-padded width throughout this
|
|
868
|
+
* codebase), so no Date parsing is needed. */
|
|
869
|
+
export function mostRecentIndividual(graph, createdAtProp = CREATED_AT_PROP) {
|
|
870
|
+
let best = null; // { ind, v }
|
|
871
|
+
for (const ind of graph?.individuals || []) {
|
|
872
|
+
const v = (ind?.attributes || []).find((a) => a?.prop === createdAtProp)?.value;
|
|
873
|
+
if (!v) continue;
|
|
874
|
+
if (!best || v > best.v || (v === best.v && String(ind.id) < String(best.ind.id))) best = { ind, v };
|
|
875
|
+
}
|
|
876
|
+
return best ? best.ind : null;
|
|
800
877
|
}
|
|
801
878
|
|
|
802
879
|
/** The shared module-ranking core behind renderSearch (text) and searchModulesRanked (path+score).
|
|
@@ -1203,6 +1280,67 @@ export function edgesOfKind(graph, kind) {
|
|
|
1203
1280
|
return out;
|
|
1204
1281
|
}
|
|
1205
1282
|
|
|
1283
|
+
/** A node's "last touched" moment, DERIVED rather than stored (PLAN_VIZ.md §2): the node's own
|
|
1284
|
+
* `updatedAtProp`/`createdAtProp` attribute, or the max `createdAt` over every edge (in
|
|
1285
|
+
* `graph.relations`, ACROSS every kind, not just classified ones) touching it as either
|
|
1286
|
+
* subject or object — whichever is newer. `""` when nothing carries a timestamp at all. Compares
|
|
1287
|
+
* ISO-8601 strings directly (correct for same-width zero-padded timestamps, no Date parsing).
|
|
1288
|
+
* Tolerates edges with no `createdAt` field (pre-dating `upsertEdge`'s own stamp, or written by
|
|
1289
|
+
* a path that bypasses `upsertEdge` entirely) by simply skipping them, never throwing. Operates
|
|
1290
|
+
* on the shared parsed-graph shape (`graph.relations`/`graph.individuals`), not memory-specific —
|
|
1291
|
+
* same reasoning `edgesOfKind`/`moduleIdOf` already document. */
|
|
1292
|
+
export function derivedUpdatedAt(graph, ind, { createdAtProp = CREATED_AT_PROP, updatedAtProp = UPDATED_AT_PROP } = {}) {
|
|
1293
|
+
if (!ind) return "";
|
|
1294
|
+
const attrs = ind.attributes || [];
|
|
1295
|
+
const own = attrs.find((a) => a?.prop === updatedAtProp)?.value || attrs.find((a) => a?.prop === createdAtProp)?.value || "";
|
|
1296
|
+
let best = own || "";
|
|
1297
|
+
for (const g of graph?.relations || []) {
|
|
1298
|
+
for (const e of g.edges || []) {
|
|
1299
|
+
if (!e || (e.subject !== ind.id && e.object !== ind.id)) continue;
|
|
1300
|
+
const c = e.createdAt;
|
|
1301
|
+
if (!c) continue; // no timestamp on this edge — skip, never throw
|
|
1302
|
+
if (!best || c > best) best = c;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return best;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/** Turn a `spiralExpand` walk (`[{id, hop}]`) into the `{nodes, edges}` shape
|
|
1309
|
+
* `tmct viz` renders — pure, no I/O, shared verbatim between the CLI
|
|
1310
|
+
* (`src/viz.mjs`'s `computeVizGraph`) and the browser bundle's client-side
|
|
1311
|
+
* re-walk/recentre (PLAN_BREADTH_FIRST_NLU.md §5 follow-on, operator
|
|
1312
|
+
* directive 2026-07-11) so both paths render byte-identically from the same
|
|
1313
|
+
* logic, never two hand-maintained copies. `nodes` enrich each walked id with
|
|
1314
|
+
* its real label/class/timestamps (`derivedUpdatedAt`, above); `edges` are
|
|
1315
|
+
* every relation-group edge connecting two walked nodes (not just the kinds
|
|
1316
|
+
* the walk itself traversed through — an incidental edge between two reached
|
|
1317
|
+
* nodes still renders), de-duped on (subject, object, predicate) across
|
|
1318
|
+
* relation groups. */
|
|
1319
|
+
export function buildVizNodesAndEdges(graph, walked, { createdAtProp = CREATED_AT_PROP, updatedAtProp = UPDATED_AT_PROP } = {}) {
|
|
1320
|
+
const nodeIds = new Set(walked.map((w) => w.id));
|
|
1321
|
+
const nodes = walked.map(({ id, hop }) => {
|
|
1322
|
+
const ind = graph.byId.get(id) || null;
|
|
1323
|
+
const attrs = ind?.attributes || [];
|
|
1324
|
+
const createdAt = attrs.find((a) => a?.prop === createdAtProp)?.value || "";
|
|
1325
|
+
return {
|
|
1326
|
+
id, hop, label: ind?.label || id, class: ind?.class || "", createdAt,
|
|
1327
|
+
updatedAt: derivedUpdatedAt(graph, ind, { createdAtProp, updatedAtProp }),
|
|
1328
|
+
};
|
|
1329
|
+
});
|
|
1330
|
+
const edges = [];
|
|
1331
|
+
const seen = new Set();
|
|
1332
|
+
for (const group of graph.relations || []) {
|
|
1333
|
+
for (const e of group.edges || []) {
|
|
1334
|
+
if (!nodeIds.has(e.subject) || !nodeIds.has(e.object)) continue;
|
|
1335
|
+
const key = `${e.subject} ${e.object} ${group.predicate}`;
|
|
1336
|
+
if (seen.has(key)) continue;
|
|
1337
|
+
seen.add(key);
|
|
1338
|
+
edges.push({ source: e.subject, target: e.object, kind: group.predicate });
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
return { nodes, edges };
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1206
1344
|
/** moduleIdOf by raw edge-endpoint id: resolves through byId when the individual exists,
|
|
1207
1345
|
* else falls back to parsing an `fn:<path>#name` id directly (callsSymbol objects may name
|
|
1208
1346
|
* symbols with no individual of their own). Null if it cannot be mapped. */
|
package/src/extensions.mjs
CHANGED
|
@@ -5,23 +5,41 @@
|
|
|
5
5
|
//
|
|
6
6
|
// resolveExtensions(repoRoot) → { entries: Map<name, ResolvedEntry>, biasByBundle }
|
|
7
7
|
//
|
|
8
|
-
// BUILTIN_EXTENSIONS
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// BUILTIN_EXTENSIONS' DEFAULT ACTIVE BUNDLE IS `human` (PLAN_SEED.md, the
|
|
9
|
+
// persona flip): a fresh repo now seeds an everyday-world vocabulary (people,
|
|
10
|
+
// places, objects, nature, time/events, body/food, mind — hand-curated from
|
|
11
|
+
// Open English WordNet + Schema.org) rather than the old implicit code-domain
|
|
12
|
+
// default. `seon` and `conceptnet` are SHIPPED but now INACTIVE — both are
|
|
13
|
+
// equally code/tech-domain-biased (conceptnet's committed slice was filtered
|
|
14
|
+
// via a tech-domain seed-term match, PLAN_SEED.md §2), so BOTH flip together,
|
|
15
|
+
// not just seon — a repo that wants the old behavior asks for it explicitly
|
|
16
|
+
// (`tmct init --with-persona code`, or `[extensions.seon]`/`[extensions.
|
|
17
|
+
// conceptnet]` `active = true`). Four more shipped-but-INACTIVE tier-2 bundles
|
|
18
|
+
// (`tier2-aws` / `tier2-python` / `tier2-java` / `tier2-general`) round out the
|
|
19
|
+
// catalog. Activating any of these is a config-only edit (`tmct init --corpus
|
|
12
20
|
// aws`, or a `[extensions.tier2-aws] active = true` in tmct.toml) — zero code
|
|
13
|
-
// change. `tier2-general` (PLAN_AGENTS.md
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
21
|
+
// change. `tier2-general`'s own 49-fact animal/weather set (PLAN_AGENTS.md
|
|
22
|
+
// Phase 1) is superseded IN DEFAULT ROLE by `human`'s much larger
|
|
23
|
+
// `human-nature` clump, but stays shipped/selectable on its own for a caller
|
|
24
|
+
// that wants that narrow slice without the rest of the human persona.
|
|
25
|
+
//
|
|
26
|
+
// `human-medium`/`human-large` (also shipped-but-INACTIVE) are SIZE TIERS of
|
|
27
|
+
// the SAME `human` bundle, not separate personas (PLAN_SEED.md §3) — each
|
|
28
|
+
// holds only the facts that size adds beyond the previous one. Activated
|
|
29
|
+
// together via `tmct init --persona-size medium|large` (bin/tmct.mjs), which
|
|
30
|
+
// resolves them through this same registry.
|
|
17
31
|
//
|
|
18
32
|
// A `tmct.toml` may carry a top-level `[extensions]` table-of-tables
|
|
19
33
|
// (`[extensions.tier2-aws]`, …): a RECOGNIZED name (one of the builtins above)
|
|
20
34
|
// may override `active`/paths/etc; an UNRECOGNIZED name declares a brand new
|
|
21
|
-
// host entry and MUST carry a `kind` (corpus | lexicon | templates | pack
|
|
22
|
-
// `pack` entry may combine any of corpus_path/lexicon_path/
|
|
23
|
-
// phrasebook_path under one `active` flag and one provenance
|
|
24
|
-
// third-party vocabulary package hands tmct.
|
|
35
|
+
// host entry and MUST carry a `kind` (corpus | lexicon | templates | pack |
|
|
36
|
+
// ontology) — a `pack` entry may combine any of corpus_path/lexicon_path/
|
|
37
|
+
// templates_path/phrasebook_path under one `active` flag and one provenance
|
|
38
|
+
// name, the shape a third-party vocabulary package hands tmct. `ontology` is a
|
|
39
|
+
// DISTINCT, nameable kind for an ontology bundle (as opposed to a plain
|
|
40
|
+
// `corpus` bundle) — its `ontology_path` key is just an alias populating the
|
|
41
|
+
// SAME internal `corpusPath` field a corpus entry uses, so every downstream
|
|
42
|
+
// seeder/loader needs zero branching by kind name.
|
|
25
43
|
//
|
|
26
44
|
// A SEPARATE top-level `[bias]` table (flat: bundle-name → number) feeds
|
|
27
45
|
// src/memory/bias.mjs's ranking — never nested under `[extensions.*]`.
|
|
@@ -45,7 +63,7 @@ import {
|
|
|
45
63
|
toFacts,
|
|
46
64
|
} from "./corpus/conceptnet.mjs";
|
|
47
65
|
|
|
48
|
-
export const EXTENSION_KINDS = Object.freeze(["corpus", "lexicon", "templates", "pack"]);
|
|
66
|
+
export const EXTENSION_KINDS = Object.freeze(["corpus", "lexicon", "templates", "pack", "ontology"]);
|
|
49
67
|
|
|
50
68
|
// The definitional-band-first predicate order chat.mjs's bootstrap has always
|
|
51
69
|
// passed for the ConceptNet seed (SEED_PREFER) — re-declared here (not
|
|
@@ -57,15 +75,20 @@ const CONCEPTNET_PREFER = ["rdfs:subClassOf", "rdf:type", "mgx:usedFor", "mgx:pa
|
|
|
57
75
|
* accidentally mutate a module-level singleton. */
|
|
58
76
|
function builtinExtensions() {
|
|
59
77
|
return {
|
|
78
|
+
// WAS active:true (the implicit code-domain default) — now opt-in.
|
|
79
|
+
// PLAN_SEED.md §2: re-activate explicitly (`tmct init --with-persona
|
|
80
|
+
// code`, or `[extensions.seon] active = true`) for the old behavior.
|
|
60
81
|
seon: {
|
|
61
82
|
kind: "corpus",
|
|
62
|
-
active:
|
|
83
|
+
active: false,
|
|
63
84
|
corpusPath: SEON_CONCEPTS_FILE,
|
|
64
85
|
provenancePrefix: "corpus:seon",
|
|
65
86
|
},
|
|
87
|
+
// WAS active:true — now opt-in too, not just seon (PLAN_SEED.md §2: the
|
|
88
|
+
// committed slice is itself tech-domain-filtered, equally biased).
|
|
66
89
|
conceptnet: {
|
|
67
90
|
kind: "corpus",
|
|
68
|
-
active:
|
|
91
|
+
active: false,
|
|
69
92
|
corpusPath: CONCEPTNET_SLICE_FILE,
|
|
70
93
|
provenancePrefix: "corpus:conceptnet",
|
|
71
94
|
// matches chat.mjs's seedBootstrapMemory exactly: uncapped, definitional
|
|
@@ -73,6 +96,40 @@ function builtinExtensions() {
|
|
|
73
96
|
limit: undefined,
|
|
74
97
|
prefer: CONCEPTNET_PREFER,
|
|
75
98
|
},
|
|
99
|
+
// NEW — the default active bundle (PLAN_SEED.md). Everyday-world
|
|
100
|
+
// vocabulary: people, places, objects, nature, time/events, body/food,
|
|
101
|
+
// mind, plus the human-base/human-bridge scaffolding connecting WordNet's
|
|
102
|
+
// and Schema.org's independently-built taxonomies (PLAN_SEED.md §3, §8).
|
|
103
|
+
human: {
|
|
104
|
+
kind: "corpus",
|
|
105
|
+
active: true,
|
|
106
|
+
corpusPath: join(TIER2_DIR, "human.jsonl"),
|
|
107
|
+
provenancePrefix: "corpus:human",
|
|
108
|
+
},
|
|
109
|
+
// NEW — Medium/Large SIZE tiers of the SAME `human` bundle (PLAN_SEED.md
|
|
110
|
+
// §3), not separate personas: each file holds ONLY the facts that size
|
|
111
|
+
// adds beyond the previous one (Medium beyond Small, Large beyond
|
|
112
|
+
// Medium), so activating them is purely ADDITIVE alongside `human`
|
|
113
|
+
// (never a replacement for it). Both ship INACTIVE — Small stays the
|
|
114
|
+
// unconditional default — and are activated together via `tmct init
|
|
115
|
+
// --persona-size medium|large` (bin/tmct.mjs), which resolves them
|
|
116
|
+
// through this SAME BUILTIN_EXTENSIONS lookup and the ordinary
|
|
117
|
+
// `--corpus <id>` activation seam (activatePluggableInput). "large"
|
|
118
|
+
// activates BOTH human-medium and human-large (Large's facts are
|
|
119
|
+
// Medium's plus its own — both bundles must be active to reach the
|
|
120
|
+
// full ~13,600-fact total).
|
|
121
|
+
"human-medium": {
|
|
122
|
+
kind: "corpus",
|
|
123
|
+
active: false,
|
|
124
|
+
corpusPath: join(TIER2_DIR, "human-medium.jsonl"),
|
|
125
|
+
provenancePrefix: "corpus:human-medium",
|
|
126
|
+
},
|
|
127
|
+
"human-large": {
|
|
128
|
+
kind: "corpus",
|
|
129
|
+
active: false,
|
|
130
|
+
corpusPath: join(TIER2_DIR, "human-large.jsonl"),
|
|
131
|
+
provenancePrefix: "corpus:human-large",
|
|
132
|
+
},
|
|
76
133
|
"tier2-aws": {
|
|
77
134
|
kind: "corpus",
|
|
78
135
|
active: false,
|
|
@@ -118,6 +175,9 @@ export function validateExtensionEntry(name, entry) {
|
|
|
118
175
|
if (entry.kind === "corpus" && !entry.corpusPath) {
|
|
119
176
|
throw new Error(`extension "${name}": a "corpus" entry needs corpus_path`);
|
|
120
177
|
}
|
|
178
|
+
if (entry.kind === "ontology" && !entry.corpusPath) {
|
|
179
|
+
throw new Error(`extension "${name}": an "ontology" entry needs ontology_path`);
|
|
180
|
+
}
|
|
121
181
|
if (entry.kind === "lexicon" && !entry.lexiconPath) {
|
|
122
182
|
throw new Error(`extension "${name}": a "lexicon" entry needs lexicon_path`);
|
|
123
183
|
}
|
|
@@ -154,6 +214,7 @@ function mergeExtensionEntry(name, builtin, override, repoRoot) {
|
|
|
154
214
|
};
|
|
155
215
|
const paths = [
|
|
156
216
|
["corpus_path", "corpusPath"],
|
|
217
|
+
["ontology_path", "corpusPath"], // alias: an "ontology" entry's own path key, same internal field as "corpus"
|
|
157
218
|
["lexicon_path", "lexiconPath"],
|
|
158
219
|
["templates_path", "templatesPath"],
|
|
159
220
|
["phrasebook_path", "phrasebookPath"],
|
|
@@ -184,9 +245,13 @@ function mergeExtensionEntry(name, builtin, override, repoRoot) {
|
|
|
184
245
|
* table (default {} — every bundle then ranks at bias 1, see bias.mjs).
|
|
185
246
|
* No `tmct.toml` (or one with no `[extensions]`/`[bias]` tables) resolves to
|
|
186
247
|
* exactly today's implicit seon+conceptnet default, byte-identical.
|
|
248
|
+
*
|
|
249
|
+
* `configFile` (optional): an explicit tmct.toml path override — `tmct extend
|
|
250
|
+
* --validate <dir> --config <path>` — read INSTEAD of `<repoRoot>/tmct.toml`;
|
|
251
|
+
* `repoRoot` still anchors every resource path (unchanged).
|
|
187
252
|
*/
|
|
188
|
-
export async function resolveExtensions(repoRoot) {
|
|
189
|
-
const raw = repoRoot ? await loadTomlConfig(repoRoot) : null;
|
|
253
|
+
export async function resolveExtensions(repoRoot, { configFile } = {}) {
|
|
254
|
+
const raw = repoRoot ? await loadTomlConfig(repoRoot, configFile ? { file: configFile } : {}) : null;
|
|
190
255
|
const defs = builtinExtensions();
|
|
191
256
|
const rawExtensions = (raw && raw.extensions && typeof raw.extensions === "object") ? raw.extensions : {};
|
|
192
257
|
const rawBias = (raw && raw.bias && typeof raw.bias === "object") ? raw.bias : {};
|
|
@@ -216,12 +281,19 @@ export async function resolveExtensions(repoRoot) {
|
|
|
216
281
|
|
|
217
282
|
// ---- Part 2: the unified corpus loader loop ---------------------------------
|
|
218
283
|
|
|
219
|
-
/** Seed every ACTIVE `corpus`-kind entry
|
|
284
|
+
/** Seed every ACTIVE `corpus`/`ontology`-kind entry, plus any ACTIVE `pack`-kind
|
|
285
|
+
* entry that declares a `corpusPath` (in the Map's own fixed order — seon,
|
|
220
286
|
* conceptnet, then the rest sorted by name) into `repo`'s memory, ONE
|
|
221
287
|
* seedMemory() call per bundle. Shared by chat.mjs's first-run bootstrap,
|
|
222
288
|
* `tmct init`'s seed step and `tmct init --corpus <id>` — so all three read
|
|
223
289
|
* the SAME loop instead of three independent hardcoded call sites.
|
|
224
290
|
*
|
|
291
|
+
* BUGFIX (this batch): a `pack`-kind entry's `corpusPath` used to be silently
|
|
292
|
+
* skipped here despite this module's own docblock claiming pack entries
|
|
293
|
+
* combine corpus_path/lexicon_path/etc — a pack's corpus facts never made it
|
|
294
|
+
* into memory. Fixed by seeding any active pack entry that declares a
|
|
295
|
+
* corpusPath, alongside corpus/ontology entries.
|
|
296
|
+
*
|
|
225
297
|
* FAILURE-TOLERANT per bundle (init.mjs's own doctrine: a missing/broken
|
|
226
298
|
* corpus degrades to "not seeded", never a crash): one bad third-party pack's
|
|
227
299
|
* seedMemory throw is CAUGHT and recorded as `perBundle[name].error` — logged
|
|
@@ -235,7 +307,16 @@ export async function seedActiveCorpusEntries(repo, entries) {
|
|
|
235
307
|
let skipped = 0;
|
|
236
308
|
let total = 0;
|
|
237
309
|
for (const [name, entry] of entries instanceof Map ? entries : new Map()) {
|
|
238
|
-
|
|
310
|
+
// PLAN_SEED.md §2 bug fix: a "pack"-kind entry with its own corpusPath
|
|
311
|
+
// combines corpus/lexicon/templates under one active flag (this module's
|
|
312
|
+
// own docblock says so) but was previously never actually seeded here —
|
|
313
|
+
// only bare `kind: "corpus"` entries were. Broadened, not narrowed: every
|
|
314
|
+
// existing `kind: "corpus"`/`kind: "ontology"` entry (seon/conceptnet/
|
|
315
|
+
// human/tier2-*) behaves identically to before; only a pack entry that
|
|
316
|
+
// DOES carry a corpusPath newly qualifies.
|
|
317
|
+
if (!entry.active) continue;
|
|
318
|
+
const seedable = entry.kind === "corpus" || entry.kind === "ontology" || (entry.kind === "pack" && entry.corpusPath);
|
|
319
|
+
if (!seedable) continue;
|
|
239
320
|
try {
|
|
240
321
|
const res = await seedMemory(repo, {
|
|
241
322
|
slicePath: entry.corpusPath,
|