@polycode-projects/the-mechanical-code-talker 0.2.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/LICENSE +373 -0
- package/README.md +108 -0
- package/ROADMAP.md +209 -0
- package/bin/cli.mjs +226 -0
- package/bin/tmct.mjs +47 -0
- package/package.json +46 -0
- package/src/ask-nlp.mjs +73 -0
- package/src/ask-vocab.mjs +687 -0
- package/src/ask.mjs +2403 -0
- package/src/chat.mjs +642 -0
- package/src/codegraph.mjs +1972 -0
- package/src/config.mjs +27 -0
- package/src/embed.mjs +191 -0
- package/src/graph-build.mjs +428 -0
- package/src/index.mjs +25 -0
- package/src/prose-nlp.mjs +52 -0
- package/src/prose.mjs +187 -0
- package/src/schema-docs.mjs +254 -0
- package/src/server.mjs +393 -0
- package/src/sessions.mjs +220 -0
- package/src/source.mjs +54 -0
- package/src/telemetry.mjs +90 -0
- package/src/toml-config.mjs +183 -0
- package/src/uuid.mjs +16 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// telemetry.mjs — OPT-IN, fire-and-forget query telemetry (PLAN_TMCT_TELEMETRY.md).
|
|
2
|
+
//
|
|
3
|
+
// The measurement contract is absolute: telemetry is OFF by default and the OFF path
|
|
4
|
+
// must be BYTE-IDENTICAL — no file, no stdout/stderr change, ~zero cost. createTelemetry
|
|
5
|
+
// returns NULL when disabled, so every call site is a single falsy check: `tel?.record(…)`.
|
|
6
|
+
//
|
|
7
|
+
// When enabled it appends ONE JSONL line per event to `<graphDir>/tmct-<id>.log`
|
|
8
|
+
// (i.e. next to graph.json, in the repo's `.tmct/` artifact dir). Writes are
|
|
9
|
+
// fire-and-forget with a swallowed catch: telemetry must NEVER throw, block, or write
|
|
10
|
+
// to stdout/stderr (stdout belongs to the chat surface). A structural redact() keeps file
|
|
11
|
+
// CONTENTS out of the log — it records only ids/paths/scores/sizes/counts.
|
|
12
|
+
|
|
13
|
+
import { appendFile } from "node:fs/promises";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { uuidv7 } from "./uuid.mjs";
|
|
16
|
+
|
|
17
|
+
/** Field names whose VALUES are (or embed) raw source and must never be logged. */
|
|
18
|
+
const DROP_KEYS = new Set(["text", "content", "snippet"]);
|
|
19
|
+
/** String fields longer than this are truncated — except query.raw (the user's own
|
|
20
|
+
* question, which is the correlation key and is not file content). */
|
|
21
|
+
const MAX_STR = 500;
|
|
22
|
+
|
|
23
|
+
/** Enabled iff `TMCT_TELEMETRY==="1"` OR `[telemetry] enabled=true` in the toml.
|
|
24
|
+
* The env wins BOTH directions: `TMCT_TELEMETRY==="0"` force-disables even when the
|
|
25
|
+
* toml turns it on. Default OFF (anything but "1"/"0" in the env falls through to toml). */
|
|
26
|
+
export function telemetryEnabled(env = process.env, toml = null) {
|
|
27
|
+
const e = env?.TMCT_TELEMETRY;
|
|
28
|
+
if (e === "1") return true;
|
|
29
|
+
if (e === "0") return false;
|
|
30
|
+
return toml?.telemetry?.enabled === true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The invocation id that correlates a log ↔ a host process. A host (the bench rig,
|
|
34
|
+
* a wrapping agent) may stamp `TMCT_INVOCATION_ID` so its trace/record and this log
|
|
35
|
+
* share one id; absent, we mint a fresh time-sortable uuidv7. */
|
|
36
|
+
export function invocationId(env = process.env) {
|
|
37
|
+
return (env?.TMCT_INVOCATION_ID && String(env.TMCT_INVOCATION_ID)) || uuidv7();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Structural redaction: drop any field named text/content/snippet at any depth, and
|
|
41
|
+
* truncate string fields longer than MAX_STR — EXCEPT `query.raw`. Records only
|
|
42
|
+
* ids/paths/scores/sizes/counts, never file contents. Pure; returns a fresh value. */
|
|
43
|
+
export function redact(value, path = "") {
|
|
44
|
+
if (typeof value === "string") {
|
|
45
|
+
if (path === "query.raw") return value; // the correlation key — kept whole
|
|
46
|
+
return value.length > MAX_STR ? value.slice(0, MAX_STR) : value;
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(value)) return value.map((v) => redact(v, path));
|
|
49
|
+
if (value && typeof value === "object") {
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const [k, v] of Object.entries(value)) {
|
|
52
|
+
if (DROP_KEYS.has(k)) continue;
|
|
53
|
+
out[k] = redact(v, path ? `${path}.${k}` : k);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Build a telemetry sink for one surface, or NULL when telemetry is disabled (the
|
|
62
|
+
* common OFF path — the caller's `tel?.record(…)` then costs one falsy check).
|
|
63
|
+
*
|
|
64
|
+
* When enabled, returns { id, file, record(fields) }. `record` stamps the schema-v1
|
|
65
|
+
* envelope ({v, id, seq, ts, surface}) onto the redacted, sparse caller fields and
|
|
66
|
+
* appends ONE JSONL line, fire-and-forget with a swallowed catch. `seq` is strictly
|
|
67
|
+
* increasing per sink. Record schema v1 fields (all sparse/optional — only include
|
|
68
|
+
* what a surface actually has):
|
|
69
|
+
* { v, id, seq, ts, surface, tool,
|
|
70
|
+
* query: { raw, family, ambiguous, candidates },
|
|
71
|
+
* response: { node_ids, scores, count, truncated, tier, topup },
|
|
72
|
+
* perf: { ms_total, ms_load, cold_load, graph_modules, graph_edges },
|
|
73
|
+
* quality: {},
|
|
74
|
+
* cost: { returned_chars, returned_tokens_est, cache_channel } }
|
|
75
|
+
*/
|
|
76
|
+
export function createTelemetry({ env = process.env, config, toml = null, surface } = {}) {
|
|
77
|
+
if (!telemetryEnabled(env, toml)) return null;
|
|
78
|
+
const id = invocationId(env);
|
|
79
|
+
const file = join(dirname(config.graphFile), `tmct-${id}.log`);
|
|
80
|
+
let seq = 0;
|
|
81
|
+
const record = (fields = {}) => {
|
|
82
|
+
try {
|
|
83
|
+
const line = { v: 1, id, seq: seq++, ts: new Date().toISOString(), surface, ...redact(fields) };
|
|
84
|
+
// Fire-and-forget: never await, never let a write error surface. Telemetry must
|
|
85
|
+
// not block a query or corrupt stdout.
|
|
86
|
+
appendFile(file, JSON.stringify(line) + "\n").catch(() => {});
|
|
87
|
+
} catch { /* redaction/serialisation must never throw a caller's turn */ }
|
|
88
|
+
};
|
|
89
|
+
return { id, file, record };
|
|
90
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// tmct.toml loader (pure library — no cli wiring here).
|
|
2
|
+
//
|
|
3
|
+
// The runtime config still lives in config.mjs (`loadConfig`, the
|
|
4
|
+
// TMCT_GRAPH_FILE knob) and stays byte-identical. This module is the *product*
|
|
5
|
+
// config surface: an optional `tmct.toml` at a repo/estate root that a consumer
|
|
6
|
+
// checks in to steer indexing and scoring. Absent file = shipped defaults (today's
|
|
7
|
+
// behaviour, byte-for-byte).
|
|
8
|
+
//
|
|
9
|
+
// Three pure entry points, consumed later by the cli:
|
|
10
|
+
// loadTomlConfig(rootDir) → raw parsed TOML, or null when absent
|
|
11
|
+
// normalizeConfig(raw,{configDir}) → canonical sparse shape (present keys only)
|
|
12
|
+
// mergeEffective({args,toml,defaults}) → {effective, sources} (arg>toml>default)
|
|
13
|
+
|
|
14
|
+
import { readFile } from "node:fs/promises";
|
|
15
|
+
import { join, resolve } from "node:path";
|
|
16
|
+
import { parse } from "smol-toml";
|
|
17
|
+
|
|
18
|
+
export const CONFIG_FILE = "tmct.toml";
|
|
19
|
+
|
|
20
|
+
/** Read `<rootDir>/tmct.toml` and return the raw parsed table.
|
|
21
|
+
* - Absent file → `null` (the "today" signal: shipped defaults, byte-identical).
|
|
22
|
+
* - Present but unparseable → throws a clear error naming the file + parse cause
|
|
23
|
+
* (a `secret_exclude` misparse is security-relevant — never swallow it). */
|
|
24
|
+
export async function loadTomlConfig(rootDir) {
|
|
25
|
+
const file = join(rootDir, CONFIG_FILE);
|
|
26
|
+
let text;
|
|
27
|
+
try {
|
|
28
|
+
text = await readFile(file, "utf8");
|
|
29
|
+
} catch (err) {
|
|
30
|
+
if (err && err.code === "ENOENT") return null;
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return parse(text);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
throw new Error(`Invalid ${CONFIG_FILE} at ${file}: ${err && err.message ? err.message : err}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Resolve the `repositories` value → absolute paths.
|
|
41
|
+
* - array → each entry resolved relative to `configDir`
|
|
42
|
+
* - string → path to a newline-delimited file (relative to `configDir`); each
|
|
43
|
+
* non-blank, non-`#` line resolved relative to `configDir`. */
|
|
44
|
+
async function resolveRepositories(value, configDir) {
|
|
45
|
+
let entries;
|
|
46
|
+
if (Array.isArray(value)) {
|
|
47
|
+
entries = value;
|
|
48
|
+
} else {
|
|
49
|
+
const file = resolve(configDir, String(value));
|
|
50
|
+
const text = await readFile(file, "utf8");
|
|
51
|
+
entries = text.split("\n");
|
|
52
|
+
}
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const raw of entries) {
|
|
55
|
+
const line = String(raw).trim();
|
|
56
|
+
if (!line || line.startsWith("#")) continue;
|
|
57
|
+
out.push(resolve(configDir, line));
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Recognized keys that parse and normalize but are not yet honoured by any
|
|
63
|
+
// consumer. Surfaced in `normalizeConfig().unwired` so the cli can warn once
|
|
64
|
+
// rather than silently ignore them.
|
|
65
|
+
const UNWIRED_KEYS = [
|
|
66
|
+
["index", "include_text"],
|
|
67
|
+
["index", "include_structure"],
|
|
68
|
+
["index", "respect_gitignore"],
|
|
69
|
+
["index", "markdown_sections"],
|
|
70
|
+
["index", "vue"],
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
/** Map a raw parsed TOML table → a canonical, sparse shape. Only keys actually
|
|
74
|
+
* present in `raw` appear (so a consumer can tell "unset" from "set to a value
|
|
75
|
+
* that equals the default" — the tri-state that `mergeEffective` relies on).
|
|
76
|
+
* `configDir` anchors every relative path. Async because `repositories` may name
|
|
77
|
+
* a newline-delimited file that has to be read. */
|
|
78
|
+
export async function normalizeConfig(raw, { configDir } = {}) {
|
|
79
|
+
const src = raw || {};
|
|
80
|
+
const dir = configDir || process.cwd();
|
|
81
|
+
const cfg = {};
|
|
82
|
+
|
|
83
|
+
if (src.repositories !== undefined) {
|
|
84
|
+
cfg.repositories = await resolveRepositories(src.repositories, dir);
|
|
85
|
+
}
|
|
86
|
+
if (src.out_root !== undefined) {
|
|
87
|
+
cfg.outRoot = resolve(dir, String(src.out_root));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const idx = src.index || {};
|
|
91
|
+
const index = {};
|
|
92
|
+
if (idx.languages !== undefined) index.languages = idx.languages;
|
|
93
|
+
if (idx.exclude !== undefined) index.exclude = idx.exclude;
|
|
94
|
+
if (idx.secret_exclude !== undefined) index.secretExclude = idx.secret_exclude;
|
|
95
|
+
if (idx.history_depth !== undefined) index.historyDepth = idx.history_depth;
|
|
96
|
+
if (idx.include_text !== undefined) index.includeText = idx.include_text;
|
|
97
|
+
if (idx.include_structure !== undefined) index.includeStructure = idx.include_structure;
|
|
98
|
+
if (idx.respect_gitignore !== undefined) index.respectGitignore = idx.respect_gitignore;
|
|
99
|
+
if (idx.markdown_sections !== undefined) index.markdownSections = idx.markdown_sections;
|
|
100
|
+
if (idx.vue !== undefined) index.vue = idx.vue;
|
|
101
|
+
if (Object.keys(index).length) cfg.index = index;
|
|
102
|
+
|
|
103
|
+
const t = src.tune || {};
|
|
104
|
+
const tune = {};
|
|
105
|
+
if (t.score_gap_k !== undefined) tune.scoreGapK = t.score_gap_k;
|
|
106
|
+
if (t.literal_mention !== undefined) tune.literalMention = t.literal_mention;
|
|
107
|
+
if (t.demote_non_prod !== undefined) tune.demoteNonProd = t.demote_non_prod;
|
|
108
|
+
if (t.call_adjacency !== undefined) tune.callAdjacency = t.call_adjacency;
|
|
109
|
+
if (t.impl_of_interface !== undefined) tune.implOfInterface = t.impl_of_interface;
|
|
110
|
+
if (t.beam_search !== undefined) tune.beamSearch = t.beam_search;
|
|
111
|
+
if (t.beam_width !== undefined) tune.beamWidth = t.beam_width;
|
|
112
|
+
if (t.embed_rank !== undefined) tune.embedRank = t.embed_rank;
|
|
113
|
+
if (t.prose_layers !== undefined) tune.proseLayers = t.prose_layers;
|
|
114
|
+
const exp = t.expansion || {};
|
|
115
|
+
const expansion = {};
|
|
116
|
+
if (exp.strategy !== undefined) expansion.strategy = exp.strategy;
|
|
117
|
+
if (exp.nodes !== undefined) expansion.nodes = exp.nodes;
|
|
118
|
+
if (exp.q !== undefined) expansion.q = exp.q;
|
|
119
|
+
if (exp.depth !== undefined) expansion.depth = exp.depth;
|
|
120
|
+
if (Object.keys(expansion).length) tune.expansion = expansion;
|
|
121
|
+
if (Object.keys(tune).length) cfg.tune = tune;
|
|
122
|
+
|
|
123
|
+
const tel = src.telemetry || {};
|
|
124
|
+
if (tel.enabled !== undefined) cfg.telemetry = { enabled: tel.enabled };
|
|
125
|
+
|
|
126
|
+
const unwired = [];
|
|
127
|
+
for (const [a, b] of UNWIRED_KEYS) {
|
|
128
|
+
if (src[a] && src[a][b] !== undefined) unwired.push(`${a}.${b}`);
|
|
129
|
+
}
|
|
130
|
+
cfg.unwired = unwired;
|
|
131
|
+
|
|
132
|
+
return cfg;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Flatten a nested object into a dotted-key map. Arrays and scalars are leaves
|
|
136
|
+
* (never recursed into); `undefined`/`null` values are dropped so that "present
|
|
137
|
+
* but false" survives while "absent" does not. */
|
|
138
|
+
function flatten(obj, prefix = "", out = {}) {
|
|
139
|
+
for (const [k, v] of Object.entries(obj || {})) {
|
|
140
|
+
if (v === undefined || v === null) continue;
|
|
141
|
+
const key = prefix ? `${prefix}.${k}` : k;
|
|
142
|
+
if (Array.isArray(v) || typeof v !== "object") out[key] = v;
|
|
143
|
+
else flatten(v, key, out);
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Merge config sources by per-key precedence: explicit `args` > `toml` >
|
|
149
|
+
* `defaults`. Returns `{effective, sources}`, both keyed by the same sorted set
|
|
150
|
+
* of dotted keys (deterministic output). `sources[key]` is one of
|
|
151
|
+
* "arg" | "tmct.toml" | "default".
|
|
152
|
+
*
|
|
153
|
+
* Tri-state note: presence is tested with `in`, not truthiness, so a `toml`
|
|
154
|
+
* value of `false` (e.g. `literal_mention = false` disabling the shipped-ON
|
|
155
|
+
* default) is honoured, while an explicit arg still wins over it. */
|
|
156
|
+
export function mergeEffective({ args = {}, toml = null, defaults = {} } = {}) {
|
|
157
|
+
const dFlat = flatten(defaults);
|
|
158
|
+
// `unwired` is metadata, not a knob — keep it out of the effective config.
|
|
159
|
+
let tomlKnobs = null;
|
|
160
|
+
if (toml) {
|
|
161
|
+
const { unwired, ...rest } = toml;
|
|
162
|
+
tomlKnobs = rest;
|
|
163
|
+
}
|
|
164
|
+
const tFlat = tomlKnobs ? flatten(tomlKnobs) : {};
|
|
165
|
+
const aFlat = flatten(args);
|
|
166
|
+
|
|
167
|
+
const keys = [...new Set([...Object.keys(dFlat), ...Object.keys(tFlat), ...Object.keys(aFlat)])].sort();
|
|
168
|
+
const effective = {};
|
|
169
|
+
const sources = {};
|
|
170
|
+
for (const k of keys) {
|
|
171
|
+
if (k in aFlat) {
|
|
172
|
+
effective[k] = aFlat[k];
|
|
173
|
+
sources[k] = "arg";
|
|
174
|
+
} else if (k in tFlat) {
|
|
175
|
+
effective[k] = tFlat[k];
|
|
176
|
+
sources[k] = "tmct.toml";
|
|
177
|
+
} else {
|
|
178
|
+
effective[k] = dFlat[k];
|
|
179
|
+
sources[k] = "default";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { effective, sources };
|
|
183
|
+
}
|
package/src/uuid.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// uuid.mjs — RFC 9562 UUIDv7 (node:crypto ships v4 only). Lifted verbatim from
|
|
2
|
+
// chat.mjs so the chat session id, the telemetry invocation id, and the bench
|
|
3
|
+
// per-run stamp all share ONE time-sortable implementation (no second copy, no dep).
|
|
4
|
+
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
/** UUID v7 (RFC 9562): 48-bit big-endian unix-ms timestamp, then version/variant
|
|
8
|
+
* bits over crypto-random tail — time-sortable, unlike crypto.randomUUID()'s v4. */
|
|
9
|
+
export function uuidv7(now = Date.now()) {
|
|
10
|
+
const b = randomBytes(16);
|
|
11
|
+
b.writeUIntBE(now, 0, 6); // 48-bit unix-ms timestamp, big-endian
|
|
12
|
+
b[6] = (b[6] & 0x0f) | 0x70; // version 7
|
|
13
|
+
b[8] = (b[8] & 0x3f) | 0x80; // variant 10xx
|
|
14
|
+
const h = b.toString("hex");
|
|
15
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
16
|
+
}
|