@polycode-projects/the-mechanical-code-talker 0.3.0 → 0.5.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 +77 -3
- package/ROADMAP.md +412 -2
- package/bin/tmct.mjs +56 -1
- package/data/templates/grammar-rules.toml +97 -0
- package/data/templates/responses.jsonl +13 -0
- package/package.json +30 -2
- package/src/ask-nlp.mjs +8 -10
- package/src/ask-vocab.mjs +22 -0
- package/src/ask.mjs +170 -8
- package/src/chat.mjs +740 -53
- package/src/corpus/conceptnet.mjs +14 -2
- package/src/corpus/templates.mjs +94 -10
- package/src/finish.mjs +443 -0
- package/src/grammar/lexicon-core.json +8 -0
- package/src/hash.mjs +32 -0
- package/src/init.mjs +264 -0
- package/src/interpret/normalize.mjs +34 -0
- package/src/interpret/strategies/keywords.mjs +57 -1
- package/src/memory/blocks.mjs +23 -3
- package/src/memory/core.mjs +257 -16
- package/src/memory/fold.mjs +0 -0
- package/src/memory/inspect.mjs +141 -0
- package/src/memory/trust.mjs +113 -0
- package/src/prose-nlp.mjs +14 -16
- package/src/providers/bootstrap.mjs +24 -0
- package/src/providers/fixture.mjs +118 -0
- package/src/providers/graph-service.mjs +312 -0
- package/src/repository-interface.mjs +318 -0
- package/src/server.mjs +44 -28
- package/src/sessions.mjs +13 -2
- package/src/syllogise.mjs +0 -0
- package/src/toml-config.mjs +14 -0
- package/src/wink-model.mjs +74 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// The Repository Interface — tmct's OWNED, versioned contract between "interpret
|
|
2
|
+
// the query" (tmct, the brittle side) and "ask the graph for truth" (a provider,
|
|
3
|
+
// the stable side). PLAN_REPOSITORY_INTERFACE.md.
|
|
4
|
+
//
|
|
5
|
+
// tmct defines and versions this shape; a provider (seonix, a fixture, a browser
|
|
6
|
+
// page) IMPLEMENTS it over its native graph. Both sides already agree on the
|
|
7
|
+
// TYPES — every Individual.class is a `tmct:` class and every Edge.predicate a
|
|
8
|
+
// `tmct:` object property (ontology/tmct-core.ttl is the type dictionary) — so
|
|
9
|
+
// the interface only names the OPERATIONS over those shared types.
|
|
10
|
+
//
|
|
11
|
+
// The error contract is the load-bearing rule: a clean miss is a first-class
|
|
12
|
+
// RETURN VALUE from a small CLOSED set of reasons (MISS_REASONS), never a throw.
|
|
13
|
+
// Presentation (the render* layer, the chat surface) lives on tmct's side of the
|
|
14
|
+
// seam and turns these typed results into prose; the interface returns data.
|
|
15
|
+
//
|
|
16
|
+
// This module is pure data + tiny constructors — no fs, no graph, no LLM.
|
|
17
|
+
|
|
18
|
+
/** SemVer of the interface. Additive-by-default: new services / optional args are
|
|
19
|
+
* minor bumps; the suite for version N stays green under N+1. A breaking change
|
|
20
|
+
* is a new MAJOR with its own suite (see the versioning policy in the plan). */
|
|
21
|
+
export const INTERFACE_VERSION = "1.0.0";
|
|
22
|
+
|
|
23
|
+
/** The OWL vocabulary the types are grounded in. */
|
|
24
|
+
export const ONTOLOGY_IRI = "urn:tmct:core";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The CLOSED set of miss reasons. A service that cannot answer returns
|
|
28
|
+
* `miss(reason, detail)` with `reason` drawn from exactly these — the interpreter
|
|
29
|
+
* renders the reason, never free-text error prose.
|
|
30
|
+
*
|
|
31
|
+
* - UNRESOLVED_TERM the term named no individual in the graph.
|
|
32
|
+
* - CAPABILITY_ABSENT the provider does not implement this service (negotiated away).
|
|
33
|
+
* - TRUNCATED_GRAPH the answer exists but the provider shipped a truncated sample.
|
|
34
|
+
* - NO_SOURCE a source-reaching service, but the provider exposes no working tree.
|
|
35
|
+
*/
|
|
36
|
+
export const MISS_REASONS = Object.freeze({
|
|
37
|
+
UNRESOLVED_TERM: "UNRESOLVED_TERM",
|
|
38
|
+
CAPABILITY_ABSENT: "CAPABILITY_ABSENT",
|
|
39
|
+
TRUNCATED_GRAPH: "TRUNCATED_GRAPH",
|
|
40
|
+
NO_SOURCE: "NO_SOURCE",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** The closed vocabulary of edge kinds `edges(id, kind)` traverses, each aligned
|
|
44
|
+
* to a `tmct:` object property (ontology/tmct-core.ttl). Symbol-granular kinds
|
|
45
|
+
* (callsSymbol/touchesSymbol) stay separate from module-coarse calls/touches. */
|
|
46
|
+
export const EDGE_KINDS = Object.freeze([
|
|
47
|
+
"imports", "calls", "callsSymbol", "defines", "tests",
|
|
48
|
+
"touches", "touchesSymbol", "contains", "inherits", "cochange", "reexports",
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
/** edge-kind → the `tmct:` object property it realizes (the OWL grounding). */
|
|
52
|
+
export const EDGE_KIND_TO_TMCT = Object.freeze({
|
|
53
|
+
imports: "tmct:imports",
|
|
54
|
+
calls: "tmct:calls",
|
|
55
|
+
callsSymbol: "tmct:calls",
|
|
56
|
+
defines: "tmct:defines",
|
|
57
|
+
tests: "tmct:covers",
|
|
58
|
+
touches: "tmct:touches",
|
|
59
|
+
touchesSymbol: "tmct:touches",
|
|
60
|
+
contains: "tmct:contains",
|
|
61
|
+
inherits: "tmct:extends",
|
|
62
|
+
cochange: "tmct:dependsOn",
|
|
63
|
+
reexports: "tmct:exports",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/** The named services, grouped as in the plan's six-group inventory. Names are
|
|
67
|
+
* the interface's stable identifiers; a provider advertises which it implements
|
|
68
|
+
* as its `capabilities`. `source: true` marks the working-tree-reaching services
|
|
69
|
+
* a graph-only provider may satisfy with an honest NO_SOURCE miss. */
|
|
70
|
+
export const SERVICE_GROUPS = Object.freeze({
|
|
71
|
+
resolution: ["resolve", "describe", "members", "subclasses", "exports", "signature"],
|
|
72
|
+
traversal: ["edges", "impact"],
|
|
73
|
+
source: ["snippet", "context"],
|
|
74
|
+
aggregate: ["architecture", "untested", "stats"],
|
|
75
|
+
temporal: ["history"],
|
|
76
|
+
search: ["search", "ask"],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
/** Flat list of every service name. */
|
|
80
|
+
export const SERVICES = Object.freeze(
|
|
81
|
+
Object.values(SERVICE_GROUPS).flat(),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
/** The subset that reaches past the graph into a working tree. */
|
|
85
|
+
export const SOURCE_SERVICES = Object.freeze(new Set(SERVICE_GROUPS.source));
|
|
86
|
+
|
|
87
|
+
// ---- result constructors — the honest-miss ethos as tiny values --------------
|
|
88
|
+
|
|
89
|
+
/** A successful result carrying typed `value`. */
|
|
90
|
+
export function hit(value) {
|
|
91
|
+
return { ok: true, value };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A first-class miss: `reason` ∈ MISS_REASONS, optional `detail` (free text for
|
|
95
|
+
* the human render) and `term` (what was looked up). Never thrown. */
|
|
96
|
+
export function miss(reason, { detail = "", term = null } = {}) {
|
|
97
|
+
if (!MISS_REASONS[reason]) {
|
|
98
|
+
throw new TypeError(`miss(): unknown reason "${reason}" (not in MISS_REASONS)`);
|
|
99
|
+
}
|
|
100
|
+
return { ok: false, miss: { reason, detail, term } };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const isHit = (r) => Boolean(r && r.ok === true);
|
|
104
|
+
export const isMiss = (r) => Boolean(r && r.ok === false);
|
|
105
|
+
|
|
106
|
+
// ---- type projections — raw graph rows → the interface's shared shapes --------
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Project a raw graph individual to the interface `Individual`:
|
|
110
|
+
* { id, label, class, attributes: [{ key, value, prop }] }
|
|
111
|
+
* `class` is a `tmct:` class token; `attributes[].prop` is the SEON/mgx token the
|
|
112
|
+
* value was asserted under (OWL grounding preserved). Pure; tolerant of partials.
|
|
113
|
+
* @returns {Individual|null}
|
|
114
|
+
*/
|
|
115
|
+
export function toIndividual(ind) {
|
|
116
|
+
if (!ind || !ind.id) return null;
|
|
117
|
+
return {
|
|
118
|
+
id: String(ind.id),
|
|
119
|
+
label: ind.label != null ? String(ind.label) : String(ind.id),
|
|
120
|
+
class: ind.class || "Entity",
|
|
121
|
+
attributes: (Array.isArray(ind.attributes) ? ind.attributes : []).map((a) => ({
|
|
122
|
+
key: a.key,
|
|
123
|
+
value: a.value,
|
|
124
|
+
prop: a.prop || null,
|
|
125
|
+
})),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Project a raw relation edge to the interface `Edge`:
|
|
131
|
+
* { subject, object, predicate, prop, subjectLabel?, objectLabel?, weight? }
|
|
132
|
+
* The required quartet is subject/object/predicate/prop; labels/weight are
|
|
133
|
+
* additive conveniences a provider MAY carry.
|
|
134
|
+
* @returns {Edge}
|
|
135
|
+
*/
|
|
136
|
+
export function toEdge(rawEdge, { predicate = "", prop = null } = {}) {
|
|
137
|
+
const e = {
|
|
138
|
+
subject: rawEdge.subject,
|
|
139
|
+
object: rawEdge.object,
|
|
140
|
+
predicate,
|
|
141
|
+
prop: prop || null,
|
|
142
|
+
};
|
|
143
|
+
if (rawEdge.subjectLabel != null) e.subjectLabel = rawEdge.subjectLabel;
|
|
144
|
+
if (rawEdge.objectLabel != null) e.objectLabel = rawEdge.objectLabel;
|
|
145
|
+
if (rawEdge.weight != null) e.weight = rawEdge.weight;
|
|
146
|
+
return e;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---- capability negotiation — CAPABILITY_ABSENT as a value, not a wall --------
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Invoke a service through capability negotiation. If the provider does not
|
|
153
|
+
* advertise `service` in `svc.capabilities`, return `miss(CAPABILITY_ABSENT)`
|
|
154
|
+
* rather than throwing — the interpreter degrades the query, never errors. When
|
|
155
|
+
* present, calls `svc[service](...args)` and returns its result verbatim.
|
|
156
|
+
*
|
|
157
|
+
* This is how a host calls an interface it cannot assume is complete; the
|
|
158
|
+
* reference providers implement every service, so they never trip it.
|
|
159
|
+
*/
|
|
160
|
+
export function invoke(svc, service, ...args) {
|
|
161
|
+
const caps = svc && svc.capabilities;
|
|
162
|
+
const has = Array.isArray(caps) ? caps.includes(service) : caps instanceof Set ? caps.has(service) : false;
|
|
163
|
+
if (!has || typeof svc[service] !== "function") {
|
|
164
|
+
return miss(MISS_REASONS.CAPABILITY_ABSENT, { detail: `service "${service}" is not implemented by this provider` });
|
|
165
|
+
}
|
|
166
|
+
return svc[service](...args);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---- the MACHINE-READABLE SHAPE ----------------------------------------------
|
|
170
|
+
// A single JSON-serializable object enumerating every service, its args, result
|
|
171
|
+
// type, and possible misses. tmct OWNS and versions this; docs/repository-
|
|
172
|
+
// interface.md is the prose peer and docs/repository-interface.schema.json is the
|
|
173
|
+
// committed serialization (the contract suite asserts they agree — no drift).
|
|
174
|
+
|
|
175
|
+
const IND = "Individual";
|
|
176
|
+
const EDGE = "Edge";
|
|
177
|
+
|
|
178
|
+
/** Concurrency note shared by every read service: safe across handles — the graph
|
|
179
|
+
* is read-only truth and the only mutable state is the caller-owned session
|
|
180
|
+
* handle, never touched here. Proven by the suite's concurrent-session cases. */
|
|
181
|
+
const CONCURRENT_SAFE = "concurrent-safe: reads immutable graph truth; no shared mutable state";
|
|
182
|
+
|
|
183
|
+
export const REPOSITORY_INTERFACE = Object.freeze({
|
|
184
|
+
version: INTERFACE_VERSION,
|
|
185
|
+
ontology: ONTOLOGY_IRI,
|
|
186
|
+
missReasons: Object.values(MISS_REASONS),
|
|
187
|
+
edgeKinds: [...EDGE_KINDS],
|
|
188
|
+
types: {
|
|
189
|
+
Individual: {
|
|
190
|
+
fields: {
|
|
191
|
+
id: "string (opaque provider id)",
|
|
192
|
+
label: "string (human/display name)",
|
|
193
|
+
class: "string — a tmct: class token (module|class|function|method|attribute|variable|commit|test|…)",
|
|
194
|
+
attributes: "Array<{ key: string, value: string, prop: string|null }> — prop is the SEON/mgx token grounding the value",
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
Edge: {
|
|
198
|
+
fields: {
|
|
199
|
+
subject: "string (individual id)",
|
|
200
|
+
object: "string (individual id)",
|
|
201
|
+
predicate: "string — the relation predicate",
|
|
202
|
+
prop: "string|null — the SEON/mgx property token; see edgeKinds → tmct: mapping",
|
|
203
|
+
subjectLabel: "string? (additive)",
|
|
204
|
+
objectLabel: "string? (additive)",
|
|
205
|
+
weight: "number? (additive; e.g. cochange coupling)",
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
Result: { shape: "{ ok: true, value: T } | { ok: false, miss: Miss }" },
|
|
209
|
+
Miss: { shape: "{ reason: MISS_REASONS, detail: string, term: string|null }" },
|
|
210
|
+
},
|
|
211
|
+
capabilitiesModel:
|
|
212
|
+
"A provider advertises `capabilities: string[]` (service names it implements). " +
|
|
213
|
+
"A service outside the set is negotiated away to miss(CAPABILITY_ABSENT) — never an error. " +
|
|
214
|
+
"Source services (snippet, context) may be advertised yet answer miss(NO_SOURCE) when no working tree exists.",
|
|
215
|
+
services: {
|
|
216
|
+
resolve: {
|
|
217
|
+
group: "resolution", args: { term: "string" },
|
|
218
|
+
result: `{ match: ${IND}, candidates: ${IND}[] }`,
|
|
219
|
+
misses: ["UNRESOLVED_TERM"], concurrency: CONCURRENT_SAFE,
|
|
220
|
+
purpose: "The resolveSymbol seam: map a free term to an individual + runner-up candidates. Every id-taking service consumes a resolved id.",
|
|
221
|
+
},
|
|
222
|
+
describe: {
|
|
223
|
+
group: "resolution", args: { id: "string" },
|
|
224
|
+
result: `{ individual: ${IND}, out: ${EDGE}[], incoming: ${EDGE}[] }`,
|
|
225
|
+
misses: ["UNRESOLVED_TERM"], concurrency: CONCURRENT_SAFE,
|
|
226
|
+
purpose: "Full typed portrait of one individual: its attributes and its outgoing/incoming edges.",
|
|
227
|
+
},
|
|
228
|
+
members: {
|
|
229
|
+
group: "resolution", args: { classId: "string" },
|
|
230
|
+
result: `{ methods: ${IND}[], attributes: ${IND}[] }`,
|
|
231
|
+
misses: ["UNRESOLVED_TERM"], concurrency: CONCURRENT_SAFE,
|
|
232
|
+
purpose: "A class's methods + attributes (via the contains relation) — replaces reading the class body.",
|
|
233
|
+
},
|
|
234
|
+
subclasses: {
|
|
235
|
+
group: "resolution", args: { classId: "string" },
|
|
236
|
+
result: `{ bases: ${IND}[], subclasses: ${IND}[] }`,
|
|
237
|
+
misses: ["UNRESOLVED_TERM"], concurrency: CONCURRENT_SAFE,
|
|
238
|
+
purpose: "Forward bases + the transitive reverse inheritance closure (who extends this).",
|
|
239
|
+
},
|
|
240
|
+
exports: {
|
|
241
|
+
group: "resolution", args: { moduleId: "string" },
|
|
242
|
+
result: `{ exports: ${IND}[] }`,
|
|
243
|
+
misses: ["UNRESOLVED_TERM"], concurrency: CONCURRENT_SAFE,
|
|
244
|
+
purpose: "A module's curated public API (resolved __all__ / re-exports).",
|
|
245
|
+
},
|
|
246
|
+
signature: {
|
|
247
|
+
group: "resolution", args: { id: "string" },
|
|
248
|
+
result: "{ id, label, class, params, returns, raises, decorators, doc, selfFields, flags }",
|
|
249
|
+
misses: ["UNRESOLVED_TERM"], concurrency: CONCURRENT_SAFE,
|
|
250
|
+
purpose: "The compact API surface of a symbol without its body.",
|
|
251
|
+
},
|
|
252
|
+
edges: {
|
|
253
|
+
group: "traversal", args: { id: "string", kind: "EDGE_KINDS member" },
|
|
254
|
+
result: `{ kind: string, edges: ${EDGE}[] }`,
|
|
255
|
+
misses: ["UNRESOLVED_TERM"], concurrency: CONCURRENT_SAFE,
|
|
256
|
+
purpose: "Outgoing edges of one closed kind from an individual (an honest empty array is not a miss).",
|
|
257
|
+
note: "An unknown kind (outside EDGE_KINDS) is a programming error and throws TypeError, not a miss.",
|
|
258
|
+
},
|
|
259
|
+
impact: {
|
|
260
|
+
group: "traversal", args: { moduleId: "string" },
|
|
261
|
+
result: `{ total: number, levels: Array<Array<{ id, label, via, tests }>> }`,
|
|
262
|
+
misses: ["UNRESOLVED_TERM"], concurrency: CONCURRENT_SAFE,
|
|
263
|
+
purpose: "The transitive dependent closure the interpreter cannot compute without provider truth.",
|
|
264
|
+
},
|
|
265
|
+
snippet: {
|
|
266
|
+
group: "source", args: { id: "string" },
|
|
267
|
+
result: "{ path: string, span: { start, end }, body: string|null }",
|
|
268
|
+
misses: ["UNRESOLVED_TERM", "NO_SOURCE"], concurrency: CONCURRENT_SAFE,
|
|
269
|
+
purpose: "The exact source span of a symbol. A provider with no working tree returns miss(NO_SOURCE) honestly.",
|
|
270
|
+
},
|
|
271
|
+
context: {
|
|
272
|
+
group: "source", args: { symbol: "string", depth: "min|auto|full?" },
|
|
273
|
+
result: "{ text: string, tier: string } (a sized edit bundle)",
|
|
274
|
+
misses: ["UNRESOLVED_TERM", "NO_SOURCE"], concurrency: CONCURRENT_SAFE,
|
|
275
|
+
purpose: "The composed edit bundle (exemplar, siblings, registration, insertion region). Source-reaching.",
|
|
276
|
+
},
|
|
277
|
+
architecture: {
|
|
278
|
+
group: "aggregate", args: { package: "string?" },
|
|
279
|
+
result: "{ modules: number, packages: Array<[dir, count]>, hubs: Array<{ id, label, importers }> }",
|
|
280
|
+
misses: [], concurrency: CONCURRENT_SAFE,
|
|
281
|
+
purpose: "Package/module shape + the most-imported hub modules. Optional package prefix scopes it.",
|
|
282
|
+
},
|
|
283
|
+
untested: {
|
|
284
|
+
group: "aggregate", args: {},
|
|
285
|
+
result: `{ modules: ${IND}[] }`,
|
|
286
|
+
misses: [], concurrency: CONCURRENT_SAFE,
|
|
287
|
+
purpose: "Modules with no covering test module (via the tests relation).",
|
|
288
|
+
},
|
|
289
|
+
stats: {
|
|
290
|
+
group: "aggregate", args: {},
|
|
291
|
+
result: "{ total: number, classes: Array<{ class: string, count: number }>, truncated: boolean }",
|
|
292
|
+
misses: [], concurrency: CONCURRENT_SAFE,
|
|
293
|
+
purpose: "Per-tmct:class individual counts, read straight from the payload.",
|
|
294
|
+
},
|
|
295
|
+
history: {
|
|
296
|
+
group: "temporal", args: { id: "string" },
|
|
297
|
+
result: "{ commits: Array<{ id, label, author, date, message }> }",
|
|
298
|
+
misses: ["UNRESOLVED_TERM"], concurrency: CONCURRENT_SAFE,
|
|
299
|
+
purpose: "The commits that touched an individual (tmct:commit individuals via tmct:touches).",
|
|
300
|
+
},
|
|
301
|
+
search: {
|
|
302
|
+
group: "search", args: { query: "string", kind: "string?", name: "string?", decorator: "string?" },
|
|
303
|
+
result: `{ results: ${IND}[] }`,
|
|
304
|
+
misses: [], concurrency: CONCURRENT_SAFE,
|
|
305
|
+
purpose: "Lexical, provider-local locate. An empty result set is honest, not a miss.",
|
|
306
|
+
},
|
|
307
|
+
ask: {
|
|
308
|
+
group: "search", args: { query: "string" },
|
|
309
|
+
result: "{ content: string, tmct_ask: object } (the composed NL round-trip envelope)",
|
|
310
|
+
misses: [], concurrency: CONCURRENT_SAFE,
|
|
311
|
+
purpose: "The mechanical, zero-model NL query over the graph — tmct's whole reason to exist.",
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
/** Freeze-deep helper is unnecessary; REPOSITORY_INTERFACE is treated as read-only
|
|
317
|
+
* data. Exported so the contract suite and docs generator share one source. */
|
|
318
|
+
export default REPOSITORY_INTERFACE;
|
package/src/server.mjs
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
renderClassHistory,
|
|
49
49
|
} from "./codegraph.mjs";
|
|
50
50
|
import { ask } from "./ask.mjs";
|
|
51
|
+
import { createGraphService } from "./providers/graph-service.mjs";
|
|
51
52
|
|
|
52
53
|
const SNIPPET_MAX_LINES = 200;
|
|
53
54
|
|
|
@@ -66,7 +67,7 @@ export const TOOLS = [
|
|
|
66
67
|
type: "object",
|
|
67
68
|
required: ["symbol"],
|
|
68
69
|
properties: {
|
|
69
|
-
symbol: { type: "string", description: "Module path (
|
|
70
|
+
symbol: { type: "string", description: "Module path (e.g. path/to/module) or a sibling function/class name defined in it." },
|
|
70
71
|
depth: { type: "string", enum: ["min", "auto", "full"], default: "auto", description: "auto (sized to the task) | min (leanest) | full (every section)." },
|
|
71
72
|
},
|
|
72
73
|
},
|
|
@@ -78,7 +79,7 @@ export const TOOLS = [
|
|
|
78
79
|
type: "object",
|
|
79
80
|
required: ["symbol"],
|
|
80
81
|
properties: {
|
|
81
|
-
symbol: { type: "string", description: "function/class name
|
|
82
|
+
symbol: { type: "string", description: "function/class name, Class.method, or fn:<path>#name." },
|
|
82
83
|
},
|
|
83
84
|
},
|
|
84
85
|
},
|
|
@@ -110,12 +111,17 @@ async function loadGraph(config, source) {
|
|
|
110
111
|
return graph;
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
// Resolution + the miss→ToolError bridge, threaded through the typed service
|
|
115
|
+
// object (createGraphService). The service is the named seam; tmct's own
|
|
116
|
+
// presentation (render*) reads its raw graph (svc.graph) and formats. A clean
|
|
117
|
+
// miss on the interface becomes the instructive ToolError the CLI/chat expect —
|
|
118
|
+
// message-only, never a stack, no fabricated entity names (generic placeholder).
|
|
119
|
+
function resolveOrThrow(svc, symbol, what) {
|
|
120
|
+
const { match, candidates } = resolveSymbol(svc.graph, symbol);
|
|
115
121
|
if (!match) {
|
|
116
122
|
throw new ToolError(
|
|
117
123
|
`no entity matching ${what} "${symbol}" in the code-map graph. ` +
|
|
118
|
-
"Try a repo-relative path (e.g.
|
|
124
|
+
"Try a repo-relative path (e.g. path/to/module), a basename, or tmct_search for a fuzzy lookup.",
|
|
119
125
|
);
|
|
120
126
|
}
|
|
121
127
|
return { match, candidates };
|
|
@@ -146,7 +152,8 @@ export async function buildContextBundle(args, { config, source = defaultSource,
|
|
|
146
152
|
// by the tmct-max arm to test whether more injection re-bloats.
|
|
147
153
|
const max = Boolean(args?.max);
|
|
148
154
|
const graph = await loadGraph(config, source);
|
|
149
|
-
const
|
|
155
|
+
const svc = createGraphService(graph);
|
|
156
|
+
const { match } = resolveOrThrow(svc, symbol, "symbol");
|
|
150
157
|
const plan = contextPlan(graph, match);
|
|
151
158
|
// #6/B1/B6: pick the section mask by depth — min forces TINY, full/max forces everything, auto
|
|
152
159
|
// runs the size classifier (lean TINY default + one-tier top-up when the edit needs it).
|
|
@@ -256,29 +263,48 @@ export async function buildContextBundle(args, { config, source = defaultSource,
|
|
|
256
263
|
return { text: out.join("\n"), tier, topup };
|
|
257
264
|
}
|
|
258
265
|
|
|
266
|
+
// The full set of tool names dispatchTool serves (hot catalog + cold tools). Used
|
|
267
|
+
// to reject an unknown tool before any graph load.
|
|
268
|
+
const DISPATCH_TOOLS = new Set([
|
|
269
|
+
"tmct_context", "tmct_context_more", "tmct_describe", "tmct_snippet", "tmct_signature",
|
|
270
|
+
"tmct_impact", "tmct_search", "tmct_members", "tmct_subclasses", "tmct_architecture",
|
|
271
|
+
"tmct_exports", "tmct_untested", "tmct_ask", "tmct_tests_for", "tmct_history",
|
|
272
|
+
"tmct_callers", "tmct_callees", "tmct_cochanges", "tmct_calls",
|
|
273
|
+
"tmct_file_history", "tmct_method_history", "tmct_class_history",
|
|
274
|
+
]);
|
|
275
|
+
|
|
259
276
|
export async function dispatchTool(name, args, { config, source = defaultSource } = {}) {
|
|
277
|
+
// tmct_context builds (and loads) its own edit bundle — return early so we don't
|
|
278
|
+
// double-load the graph for it.
|
|
260
279
|
if (name === "tmct_context") {
|
|
261
280
|
return (await buildContextBundle(args, { config, source })).text;
|
|
262
281
|
}
|
|
282
|
+
// Reject an unknown tool BEFORE touching the graph — preserves the original
|
|
283
|
+
// ordering (an unknown name never triggers a load).
|
|
284
|
+
if (!DISPATCH_TOOLS.has(name)) throw new ToolError(`unknown tool: ${name}`);
|
|
285
|
+
// Every other tool reads graph truth: load once and build the typed service
|
|
286
|
+
// object (the Repository Interface). dispatchTool is the presentation adapter —
|
|
287
|
+
// it delegates resolution + the miss/error contract to the service and formats
|
|
288
|
+
// the result with tmct's own render* layer (which reads svc.graph). This is the
|
|
289
|
+
// switch's operations extracted into a named, typed seam without changing bytes.
|
|
290
|
+
const graph = await loadGraph(config, source);
|
|
291
|
+
const svc = createGraphService(graph);
|
|
263
292
|
if (name === "tmct_context_more") {
|
|
264
293
|
const symbol = String(args?.symbol || "").trim();
|
|
265
294
|
if (!symbol) throw new ToolError("symbol is required");
|
|
266
|
-
const
|
|
267
|
-
const { match } = resolveOrThrow(graph, symbol, "symbol");
|
|
295
|
+
const { match } = resolveOrThrow(svc, symbol, "symbol");
|
|
268
296
|
return renderContextMore(contextPlan(graph, match));
|
|
269
297
|
}
|
|
270
298
|
if (name === "tmct_describe") {
|
|
271
299
|
const symbol = String(args?.symbol || "").trim();
|
|
272
300
|
if (!symbol) throw new ToolError("symbol is required");
|
|
273
|
-
const
|
|
274
|
-
const { match, candidates } = resolveOrThrow(graph, symbol, "symbol");
|
|
301
|
+
const { match, candidates } = resolveOrThrow(svc, symbol, "symbol");
|
|
275
302
|
return renderDescribe(graph, match, { candidates });
|
|
276
303
|
}
|
|
277
304
|
if (name === "tmct_snippet") {
|
|
278
305
|
const symbol = String(args?.symbol || "").trim();
|
|
279
306
|
if (!symbol) throw new ToolError("symbol is required");
|
|
280
|
-
const
|
|
281
|
-
const { match, candidates } = resolveOrThrow(graph, symbol, "symbol");
|
|
307
|
+
const { match, candidates } = resolveOrThrow(svc, symbol, "symbol");
|
|
282
308
|
const site = siteOf(match);
|
|
283
309
|
if (!site) {
|
|
284
310
|
throw new ToolError(
|
|
@@ -308,22 +334,19 @@ export async function dispatchTool(name, args, { config, source = defaultSource
|
|
|
308
334
|
if (name === "tmct_signature") {
|
|
309
335
|
const symbol = String(args?.symbol || "").trim();
|
|
310
336
|
if (!symbol) throw new ToolError("symbol is required");
|
|
311
|
-
const
|
|
312
|
-
const { match } = resolveOrThrow(graph, symbol, "symbol");
|
|
337
|
+
const { match } = resolveOrThrow(svc, symbol, "symbol");
|
|
313
338
|
return renderSignature(graph, match);
|
|
314
339
|
}
|
|
315
340
|
if (name === "tmct_impact") {
|
|
316
341
|
const module = String(args?.module || "").trim();
|
|
317
342
|
if (!module) throw new ToolError("module is required");
|
|
318
|
-
const
|
|
319
|
-
const { match } = resolveOrThrow(graph, module, "module");
|
|
343
|
+
const { match } = resolveOrThrow(svc, module, "module");
|
|
320
344
|
return renderImpact(graph, match);
|
|
321
345
|
}
|
|
322
346
|
if (name === "tmct_search") {
|
|
323
347
|
const query = String(args?.query || "").trim();
|
|
324
348
|
const kind = String(args?.kind || "").trim();
|
|
325
349
|
if (!query && !kind) throw new ToolError("query is required");
|
|
326
|
-
const graph = await loadGraph(config, source);
|
|
327
350
|
return renderSearch(graph, query, {
|
|
328
351
|
kind,
|
|
329
352
|
decorator: String(args?.decorator || "").trim(),
|
|
@@ -333,36 +356,30 @@ export async function dispatchTool(name, args, { config, source = defaultSource
|
|
|
333
356
|
if (name === "tmct_members") {
|
|
334
357
|
const symbol = String(args?.class || "").trim();
|
|
335
358
|
if (!symbol) throw new ToolError("class is required");
|
|
336
|
-
const
|
|
337
|
-
const { match } = resolveOrThrow(graph, symbol, "class");
|
|
359
|
+
const { match } = resolveOrThrow(svc, symbol, "class");
|
|
338
360
|
return renderMembers(graph, match);
|
|
339
361
|
}
|
|
340
362
|
if (name === "tmct_subclasses") {
|
|
341
363
|
const symbol = String(args?.class || "").trim();
|
|
342
364
|
if (!symbol) throw new ToolError("class is required");
|
|
343
|
-
const
|
|
344
|
-
const { match } = resolveOrThrow(graph, symbol, "class");
|
|
365
|
+
const { match } = resolveOrThrow(svc, symbol, "class");
|
|
345
366
|
return renderSubclasses(graph, match);
|
|
346
367
|
}
|
|
347
368
|
if (name === "tmct_architecture") {
|
|
348
|
-
const graph = await loadGraph(config, source);
|
|
349
369
|
return renderArchitecture(graph, { pkg: String(args?.package || "").trim() });
|
|
350
370
|
}
|
|
351
371
|
if (name === "tmct_exports") {
|
|
352
372
|
const module = String(args?.module || "").trim();
|
|
353
373
|
if (!module) throw new ToolError("module is required");
|
|
354
|
-
const
|
|
355
|
-
const { match } = resolveOrThrow(graph, module, "module");
|
|
374
|
+
const { match } = resolveOrThrow(svc, module, "module");
|
|
356
375
|
return renderExports(graph, match);
|
|
357
376
|
}
|
|
358
377
|
if (name === "tmct_untested") {
|
|
359
|
-
const graph = await loadGraph(config, source);
|
|
360
378
|
return renderUntested(graph);
|
|
361
379
|
}
|
|
362
380
|
if (name === "tmct_ask") {
|
|
363
381
|
const query = String(args?.query || "").trim();
|
|
364
382
|
if (!query) throw new ToolError("query is required");
|
|
365
|
-
const graph = await loadGraph(config, source);
|
|
366
383
|
const { content, tmct_ask } = ask(graph, query);
|
|
367
384
|
// Every dispatchTool caller (the chat surface, the CLI fallback) expects a plain string —
|
|
368
385
|
// append the structured envelope as a delimited, machine-parseable block rather than
|
|
@@ -376,8 +393,7 @@ export async function dispatchTool(name, args, { config, source = defaultSource
|
|
|
376
393
|
) {
|
|
377
394
|
const symbol = String(args?.symbol || "").trim();
|
|
378
395
|
if (!symbol) throw new ToolError("symbol is required");
|
|
379
|
-
const
|
|
380
|
-
const { match } = resolveOrThrow(graph, symbol, "symbol");
|
|
396
|
+
const { match } = resolveOrThrow(svc, symbol, "symbol");
|
|
381
397
|
if (name === "tmct_tests_for") return renderTestsFor(graph, match);
|
|
382
398
|
if (name === "tmct_history") return renderHistory(graph, match);
|
|
383
399
|
if (name === "tmct_callers") return renderCallers(graph, match);
|
package/src/sessions.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// .tmct/session-<uuidv7>.log — the human-readable transcript (chat.mjs)
|
|
5
5
|
// .tmct/sessions/session-<uuidv7>.jsonl — the STRUCTURED sidecar this module owns:
|
|
6
6
|
// {"type":"session", id, started, repo, tmctVersion} (header line)
|
|
7
|
-
// {"type":"turn", ts, query, resolvedIds, answeredIds, miss} (one per turn, flushed)
|
|
7
|
+
// {"type":"turn", ts, query, via, resolvedIds, answeredIds, miss} (one per turn, flushed)
|
|
8
8
|
// {"type":"end", ts} (clean close marker)
|
|
9
9
|
//
|
|
10
10
|
// From the sidecar the session enters the typed graph twice:
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
27
27
|
import { basename, dirname, join } from "node:path";
|
|
28
|
-
import { appendUtterances } from "./memory/core.mjs";
|
|
28
|
+
import { appendUtterances, CREATED_AT_PROP } from "./memory/core.mjs";
|
|
29
29
|
|
|
30
30
|
export const SESSIONS_DIR_REL = join(".tmct", "sessions");
|
|
31
31
|
|
|
@@ -73,6 +73,10 @@ export function upsertSession(entities, record) {
|
|
|
73
73
|
entities.individuals ||= [];
|
|
74
74
|
entities.objectProperties ||= [];
|
|
75
75
|
|
|
76
|
+
// capture the prior copy's createdAt BEFORE we drop it — first-write-wins so
|
|
77
|
+
// mgx:createdAt records when the session was FIRST seen, not last re-appended.
|
|
78
|
+
const priorSession = entities.individuals.find((i) => i?.id === sid);
|
|
79
|
+
const priorCreatedAt = priorSession?.attributes?.find((a) => a?.prop === CREATED_AT_PROP)?.value || "";
|
|
76
80
|
// replace any prior copy of this session (read-time appends run once per turn)
|
|
77
81
|
entities.individuals = entities.individuals.filter((i) => i?.id !== sid);
|
|
78
82
|
let group = entities.objectProperties.find((g) => g?.prop === ASKS_ABOUT_PROP);
|
|
@@ -111,6 +115,8 @@ export function upsertSession(entities, record) {
|
|
|
111
115
|
id: sid, label, class: SESSION_CLASS,
|
|
112
116
|
derived_from: [], mentions: [],
|
|
113
117
|
attributes: [
|
|
118
|
+
// referenced via the imported constant (single-sourced from memory/core.mjs)
|
|
119
|
+
{ prop: CREATED_AT_PROP, key: "createdAt", value: priorCreatedAt || started || new Date().toISOString() },
|
|
114
120
|
{ prop: "mgx:sessionStarted", key: "started", value: started },
|
|
115
121
|
{ prop: "mgx:sessionEnded", key: "ended", value: ended },
|
|
116
122
|
{ prop: "mgx:sessionTurns", key: "turns", value: String(turns.length) },
|
|
@@ -216,6 +222,7 @@ async function recordSessionMemory(graphFile, record, repoDirOverride = null) {
|
|
|
216
222
|
if (t.answeredIds?.length) parsed.answeredIds = t.answeredIds;
|
|
217
223
|
if (t.command) parsed.command = t.command;
|
|
218
224
|
if (t.miss) parsed.miss = true;
|
|
225
|
+
if (t.via) parsed.via = t.via; // answer provenance (W1) — carried into memory
|
|
219
226
|
utterances.push({
|
|
220
227
|
role: "visitor", text: query, ts, sessionId: record.id, sessionStarted: record.started || "",
|
|
221
228
|
...(Object.keys(parsed).length ? { parsed } : {}),
|
|
@@ -264,6 +271,10 @@ export function parseSessionJsonl(text) {
|
|
|
264
271
|
// conversational filler are recorded but never folded into the corpus.
|
|
265
272
|
...(rec.command ? { command: String(rec.command) } : {}),
|
|
266
273
|
...(rec.conversational ? { conversational: true } : {}),
|
|
274
|
+
// answer provenance (W1): composed|template|count|command|conversational|
|
|
275
|
+
// assert|recall|fact|corpus — carried through so the memory side-write and
|
|
276
|
+
// any re-fold keep the banding signal the Phase-5 bench reads.
|
|
277
|
+
...(rec.via ? { via: String(rec.via) } : {}),
|
|
267
278
|
});
|
|
268
279
|
} else if (rec?.type === "end") ended = String(rec.ts || "") || ended;
|
|
269
280
|
}
|
|
Binary file
|
package/src/toml-config.mjs
CHANGED
|
@@ -87,6 +87,20 @@ export async function normalizeConfig(raw, { configDir } = {}) {
|
|
|
87
87
|
cfg.outRoot = resolve(dir, String(src.out_root));
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// `tmct init` onboarding keys (ROADMAP Phase 8). Sparse like the rest: only a
|
|
91
|
+
// key actually present appears, so "unset" stays distinguishable from "set to
|
|
92
|
+
// the default". `graph_file` is resolved against configDir to match outRoot.
|
|
93
|
+
if (src.graph_file !== undefined) {
|
|
94
|
+
cfg.graphFile = resolve(dir, String(src.graph_file));
|
|
95
|
+
}
|
|
96
|
+
const corpus = src.corpus || {};
|
|
97
|
+
if (corpus.tier !== undefined) cfg.corpus = { tier: corpus.tier };
|
|
98
|
+
const seed = src.seed || {};
|
|
99
|
+
const seedCfg = {};
|
|
100
|
+
if (seed.enabled !== undefined) seedCfg.enabled = seed.enabled;
|
|
101
|
+
if (seed.limit !== undefined) seedCfg.limit = seed.limit;
|
|
102
|
+
if (Object.keys(seedCfg).length) cfg.seed = seedCfg;
|
|
103
|
+
|
|
90
104
|
const idx = src.index || {};
|
|
91
105
|
const index = {};
|
|
92
106
|
if (idx.languages !== undefined) index.languages = idx.languages;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// wink-model.mjs — the ONE place tmct loads the wink-nlp engine + model.
|
|
2
|
+
//
|
|
3
|
+
// Two adapters sit on top of this leaf loader: ask-nlp.mjs (lemma/POS tier for the
|
|
4
|
+
// ask engine) and prose-nlp.mjs (lemma layer for the prose index). They used to
|
|
5
|
+
// each carry their own `createRequire(import.meta.url)` block — the same ~six lines
|
|
6
|
+
// twice, and both Node-only. That duplication is single-sourced here, and the
|
|
7
|
+
// Node-only limitation is lifted with a browser seam, WITHOUT eagerly bundling the
|
|
8
|
+
// ~1 MB model into anything.
|
|
9
|
+
//
|
|
10
|
+
// Why a registration seam instead of a static `import "wink-nlp"`:
|
|
11
|
+
// - The whole architecture keeps the model OUT of the base/viewer bundle; a static
|
|
12
|
+
// import would drag it in. `wink-eng-lite-web-model` is already the *browser*
|
|
13
|
+
// build, so the model can run in the page — what was missing is a load path a
|
|
14
|
+
// bundler can satisfy without a Node `require`. That path is `registerWinkModel`:
|
|
15
|
+
// a browser/bundler entry imports wink with its own `import` and hands the pair
|
|
16
|
+
// in ONCE, before any lemma/POS use. Node needs nothing — it falls back to
|
|
17
|
+
// `createRequire`. This is the Phase-8 browser-mode unblocker the dependency
|
|
18
|
+
// audit called for (a wiring fix; the model was always browser-capable).
|
|
19
|
+
//
|
|
20
|
+
// The loader stays SYNCHRONOUS (the adapters and their callers are sync): the
|
|
21
|
+
// browser host registers up front; Node resolves lazily via createRequire. Failure
|
|
22
|
+
// is cached as null — a checkout without the optional deps, or a page that never
|
|
23
|
+
// registered a model, simply runs adapter-less (lemma/POS tiers honestly off), it
|
|
24
|
+
// never throws.
|
|
25
|
+
|
|
26
|
+
import { createRequire } from "node:module";
|
|
27
|
+
|
|
28
|
+
let injected; // browser/bundler-supplied `() => ({ winkNLP, model })`, or undefined
|
|
29
|
+
let cached; // undefined = not tried yet; null = unavailable (tried once, honestly off)
|
|
30
|
+
|
|
31
|
+
/** Browser/bundler seam: register a factory returning `{ winkNLP, model }` (each the
|
|
32
|
+
* imported module) so the page's own bundler resolves wink instead of a Node
|
|
33
|
+
* `require`. Call once before any ask/prose lemma use. Resets the cache so a late
|
|
34
|
+
* registration still takes effect. */
|
|
35
|
+
export function registerWinkModel(factory) {
|
|
36
|
+
injected = factory;
|
|
37
|
+
cached = undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Load `{ winkNLP, model }` once, or null when wink isn't available. Prefers a
|
|
41
|
+
* registered browser factory; otherwise falls back to Node module resolution. */
|
|
42
|
+
export function loadWinkModel() {
|
|
43
|
+
if (cached !== undefined) return cached;
|
|
44
|
+
try {
|
|
45
|
+
const pair = injected ? injected() : nodeRequireWink();
|
|
46
|
+
cached = pair && pair.winkNLP && pair.model ? pair : null;
|
|
47
|
+
} catch {
|
|
48
|
+
cached = null;
|
|
49
|
+
}
|
|
50
|
+
return cached;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Node fallback: resolve wink through the module system (never a guessed path),
|
|
54
|
+
* exactly as the two adapters did inline before. CJS deps, so `createRequire`. */
|
|
55
|
+
function nodeRequireWink() {
|
|
56
|
+
const require = createRequire(import.meta.url);
|
|
57
|
+
return {
|
|
58
|
+
winkNLP: require("wink-nlp"),
|
|
59
|
+
model: require("wink-eng-lite-web-model"),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Convenience: the constructed `nlp` instance (`winkNLP(model)`) or null. Both
|
|
64
|
+
* adapters want exactly this. Not cached here — the adapters cache their own
|
|
65
|
+
* higher-level object; constructing `nlp` is cheap next to loading the model. */
|
|
66
|
+
export function winkInstance() {
|
|
67
|
+
const loaded = loadWinkModel();
|
|
68
|
+
if (!loaded) return null;
|
|
69
|
+
try {
|
|
70
|
+
return loaded.winkNLP(loaded.model);
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|