@polycode-projects/the-mechanical-code-talker 2.2.0 → 2.3.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.
Files changed (49) hide show
  1. package/bin/tmct.mjs +4 -5
  2. package/corpus/wordnet/generate.mjs +6 -7
  3. package/package.json +30 -2
  4. package/src/adapters/corpus/conceptnet.mjs +1 -1
  5. package/src/adapters/graph-build.mjs +3 -3
  6. package/src/adapters/memory/blocks.mjs +2 -2
  7. package/src/adapters/memory/core.mjs +5 -5
  8. package/src/adapters/providers/bootstrap.mjs +1 -1
  9. package/src/adapters/providers/fixture.mjs +1 -1
  10. package/src/adapters/wink-model.mjs +1 -1
  11. package/src/adapters/wordnet-source.mjs +70 -0
  12. package/src/domain/answer-variants.json +1 -1
  13. package/src/domain/ask-vocab.mjs +2 -2
  14. package/src/domain/ask.mjs +4 -4
  15. package/src/domain/codegraph.mjs +3 -3
  16. package/src/domain/corpus-matrix.mjs +87 -0
  17. package/src/domain/grammar/ace.mjs +11 -11
  18. package/src/domain/grammar/lexicon.mjs +3 -3
  19. package/src/domain/inflect.mjs +67 -0
  20. package/src/domain/interpret/fuzzy.mjs +1 -1
  21. package/src/domain/interpret/merge.mjs +1 -1
  22. package/src/domain/interpret/normalize.mjs +1 -1
  23. package/src/domain/licences.mjs +68 -0
  24. package/src/domain/memory/capability.mjs +1 -1
  25. package/src/domain/memory/trust.mjs +2 -2
  26. package/src/domain/persona/codegen.mjs +123 -0
  27. package/src/domain/persona/examples.mjs +26 -0
  28. package/src/domain/persona/tiers.mjs +270 -0
  29. package/src/domain/publish-gate.mjs +41 -0
  30. package/src/domain/router/call-validator.mjs +1 -1
  31. package/src/domain/router/drive.mjs +3 -4
  32. package/src/domain/router/registry.mjs +12 -13
  33. package/src/domain/router/resolver.mjs +18 -5
  34. package/src/domain/router/results.mjs +3 -3
  35. package/src/domain/router/taught.mjs +4 -3
  36. package/src/domain/schemaorg/turtle.mjs +25 -0
  37. package/src/domain/semcor/parse.mjs +87 -0
  38. package/src/domain/syllogise.mjs +6 -6
  39. package/src/domain/version-stamp.mjs +36 -0
  40. package/src/domain/wordnet/yaml.mjs +133 -0
  41. package/src/services/chat-session.mjs +2 -2
  42. package/src/services/chat.mjs +2 -2
  43. package/src/services/cli-args.mjs +4 -4
  44. package/src/services/finish.mjs +1 -1
  45. package/src/services/ledger-viz.mjs +2 -3
  46. package/src/services/sessions.mjs +4 -4
  47. package/src/services/viz-theme.mjs +3 -4
  48. package/src/surfaces/web/memory-ask-browser.bundle.js +1 -18
  49. package/src/domain/router/guardrail.mjs +0 -116
@@ -1,15 +1,15 @@
1
1
  // src/domain/router/registry.mjs — the capability registry.
2
2
  //
3
3
  // Each tmct tool is modelled as a STRIPS/PDDL operator declared as DATA: a `Capability` with
4
- // typed `Parameter`s, `Precondition`s, and `Effect`s (add-list/delete-list). Preconditions are
5
- // the safety gate guardrail.mjs checks before a call fires; resolver.mjs backward-chains from
6
- // a goal to a capability whose add-list achieves it.
4
+ // typed `Parameter`s, `Precondition`s, and `Effect`s (add-list/delete-list). resolver.mjs
5
+ // backward-chains from a goal to a capability whose add-list achieves it, and proves the
6
+ // preconditions bind before the call fires.
7
7
  //
8
8
  // Plain data + pure accessors, no I/O. Tool names + parameter arg keys are the
9
9
  // exact ones src/tools/server.mjs `dispatchTool` reads, so a bound call this registry validates is
10
10
  // directly dispatchable.
11
11
 
12
- export const PREFIXES = Object.freeze({
12
+ const PREFIXES = Object.freeze({
13
13
  cap: "urn:tmct:cap#", // the capability/operator vocabulary (this module)
14
14
  mgx: "urn:tmct:mgx#", // tmct's code-graph predicates (imports/calls/tests/…)
15
15
  seon: "http://se-on.org/ontologies/seon.owl#", // software-evolution ontology classes
@@ -25,7 +25,7 @@ export const VOCAB = Object.freeze({
25
25
 
26
26
  // Parameter entity-KINDS — the seon/mgx classes a slot ranges over. `Query` and
27
27
  // `Kind`/`Package` are free-text / enum slots (no graph resolution); the rest
28
- // name a graph entity the guardrail must prove RESOLVES before the call fires.
28
+ // name a graph entity the resolver must prove RESOLVES before the call fires.
29
29
  export const KINDS = Object.freeze({
30
30
  Symbol: "seon:CodeEntity", // any code symbol: function/method/class/module/attribute
31
31
  Module: "seon:Module",
@@ -196,11 +196,10 @@ function deepFreeze(value) {
196
196
  }
197
197
 
198
198
  /** Register a capability at runtime (e.g. a taught action family bridged in by
199
- * src/domain/router/taught.mjs). `readOnly` must be an explicit boolean; a
200
- * `readOnly: false` record is forced `dispatchable: false` the guardrail's
201
- * candidate enrichment re-dispatches a tool once per tied candidate, which is
202
- * only safe when dispatch performs no writes. Returns an `unregister()`
203
- * disposer. */
199
+ * src/domain/router/taught.mjs). `readOnly` must be an explicit boolean: it is
200
+ * what resolver.mjs's dispatch gate reads, and a `readOnly: false` record is
201
+ * never dispatched. `dispatchable` is derived from it for callers that want the
202
+ * record to state the conclusion. Returns an `unregister()` disposer. */
204
203
  export function registerCapability(cap) {
205
204
  const name = cap && typeof cap.name === "string" ? cap.name.trim() : "";
206
205
  if (!name) throw new Error("registerCapability: a non-empty name is required");
@@ -260,14 +259,14 @@ export function isCapability(n) { return Boolean(byName[n]); }
260
259
  /** The parameter slots of capability `n` (empty array if unknown/no-arg). */
261
260
  export function parametersOf(n) { return byName[n]?.parameters ?? []; }
262
261
 
263
- /** The preconditions of capability `n` (the safety gate the guardrail checks). */
262
+ /** The preconditions of capability `n` (the safety gate the resolver checks). */
264
263
  export function preconditionsOf(n) { return byName[n]?.preconditions ?? []; }
265
264
 
266
265
  /** The effects of capability `n` — `{ add, del }` (the proof-chain contribution). */
267
266
  export function effectsOf(n) { return byName[n]?.effects ?? { add: [], del: [] }; }
268
267
 
269
- /** The set of arg keys capability `n` accepts (for the guardrail's unknown-arg
270
- * check). Returns a Set of strings. */
268
+ /** The set of arg keys capability `n` accepts (for call-validator.mjs's
269
+ * unknown-arg check). Returns a Set of strings. */
271
270
  export function argKeysOf(n) {
272
271
  return new Set(parametersOf(n).map((p) => p.arg));
273
272
  }
@@ -200,7 +200,7 @@ export function commandCapability(request, declaredNames, selectTool) {
200
200
 
201
201
  /** Build the glass-box proof chain for a grounded single call: its preconditions then the
202
202
  * epistemic add-effect. Dispatch has succeeded, so `resolves` steps are ok. */
203
- export function proofFor(name, input) {
203
+ function proofFor(name, input) {
204
204
  const steps = [];
205
205
  for (const pre of preconditionsOf(name)) {
206
206
  if (pre.pred === PRECOND.graphLoaded) steps.push({ step: "precondition", pred: pre.pred, ok: true });
@@ -213,11 +213,21 @@ export function proofFor(name, input) {
213
213
 
214
214
  const REFUSE = (why, extra) => ({ selected: null, refused: true, reason: why, ...(extra || {}) });
215
215
 
216
- /** Breadth-first ambiguity: dispatches the SAME tool once per tied candidate (safe since
217
- * every registered capability is read-only). Returns `[{candidate, result}, ...]`, or
218
- * undefined when there is no dispatcher to run it with. */
216
+ /** The gate both dispatch sites below go through. Dispatching is an OBSERVATION, so it
217
+ * may only ever run a capability whose own record says it performs no writes. A
218
+ * world-mutating record (the ones src/domain/router/taught.mjs registers carry
219
+ * `readOnly: false`) is planned over and simulated, never fired here. */
220
+ function dispatchPerformsNoWrites(capName) {
221
+ return capabilityByName(capName)?.readOnly === true;
222
+ }
223
+
224
+ /** Breadth-first ambiguity: dispatches the SAME tool once per tied candidate, which only
225
+ * stays safe while the tool writes nothing. Returns `[{candidate, result}, ...]`, or
226
+ * undefined when there is no dispatcher to run it with — or when the capability is not
227
+ * read-only. */
219
228
  async function dispatchEachCandidate(pool, capName, arg, ctx, execute) {
220
229
  if (!execute || !ctx.dispatch) return undefined;
230
+ if (!dispatchPerformsNoWrites(capName)) return undefined;
221
231
  const results = [];
222
232
  for (const c of pool) {
223
233
  const res = await ctx.dispatch(capName, { [arg]: c.label });
@@ -283,6 +293,9 @@ export async function resolveOne(request, declaredNames, ctx, { execute = true }
283
293
  if (problems.length) return REFUSE(`bound call did not validate: ${problems.map((p) => p.reason).join(",")}`);
284
294
 
285
295
  if (execute && ctx.dispatch) {
296
+ if (!dispatchPerformsNoWrites(pick.name)) {
297
+ return REFUSE(`${pick.name} is not read-only; the resolver observes, it never fires a world-mutating capability`);
298
+ }
286
299
  const res = await ctx.dispatch(pick.name, input);
287
300
  if (!res.ok) return REFUSE(`unresolvable at dispatch: ${res.error}`);
288
301
  return { selected: call, proof: proofFor(pick.name, input), why, resolved: res.resolved ?? resolved, observed: String(res.text ?? "").slice(0, 240) };
@@ -293,7 +306,7 @@ export async function resolveOne(request, declaredNames, ctx, { execute = true }
293
306
  // ---- reachability (used by the bidirectional conformance test + docs) ---------
294
307
 
295
308
  /** The epistemic topics some NL intent or imperative frame can reach. */
296
- export function nlReachableTopics() {
309
+ function nlReachableTopics() {
297
310
  const topics = new Set();
298
311
  for (const v of Object.values(NL_INTENTS)) topics.add(v.topic);
299
312
  for (const f of FRAMES) topics.add(f.topic);
@@ -147,7 +147,7 @@ export function callersLabels(graph, ind) {
147
147
  }
148
148
 
149
149
  /** Callees of a symbol (mirrors renderCallees). */
150
- export function calleesLabels(graph, ind) {
150
+ function calleesLabels(graph, ind) {
151
151
  if (CALL_SYMBOL_CLASSES.has(ind.class)) {
152
152
  return uniqSort(edgesOfKind(graph, "callsSymbol").filter((e) => e.subject === ind.id).map((e) => e.objectLabel || e.object));
153
153
  }
@@ -157,7 +157,7 @@ export function calleesLabels(graph, ind) {
157
157
  }
158
158
 
159
159
  /** Subclasses (transitive) of a class (mirrors renderSubclasses closure). */
160
- export function subclassesLabels(graph, ind) {
160
+ function subclassesLabels(graph, ind) {
161
161
  const inherits = edgesOfKind(graph, "inherits");
162
162
  const childrenOf = new Map();
163
163
  for (const e of inherits) {
@@ -198,7 +198,7 @@ export function cochangesLabels(graph, ind) {
198
198
  }
199
199
 
200
200
  /** A module's public exports (mirrors renderExports — the `reexports` edge). */
201
- export function exportsLabels(graph, ind) {
201
+ function exportsLabels(graph, ind) {
202
202
  const modId = moduleIdOf(graph, ind);
203
203
  if (!modId) return [];
204
204
  return uniqSort(edgesOfKind(graph, "reexports").filter((e) => e.subject === modId).map((e) => e.objectLabel || e.object));
@@ -3,9 +3,10 @@
3
3
  // A taught game action ("you can move a disk onto a peg" + its preconditions
4
4
  // and effect) becomes a registered capability record so the router's operator
5
5
  // model covers taught actions and built-in query tools alike. Registered
6
- // records carry readOnly: false, so the guardrail never dispatches them — the
7
- // resolver also never selects them on its own, because it backward-chains over
8
- // `knows` add-effects and these records carry world-triple effects instead.
6
+ // records carry readOnly: false, so the resolver's dispatch gate refuses to
7
+ // fire them — and it never selects one on its own anyway, because it
8
+ // backward-chains over `knows` add-effects and these carry world-triple
9
+ // effects instead. They are planned over and simulated.
9
10
 
10
11
  import { capabilityByName, registerCapability } from "./registry.mjs";
11
12
 
@@ -0,0 +1,25 @@
1
+ // turtle.mjs — a very small Turtle reader for schema.ttl's OWN regular shape:
2
+ // each class is one `:Name a rdfs:Class ;` block terminated by a line ending
3
+ // in ` .`, with `rdfs:label`/`rdfs:comment`/`rdfs:subClassOf` as `;`-separated
4
+ // predicate lines. Not a general Turtle parser — schema.ttl's own generator
5
+ // emits a single, very regular style (confirmed by direct inspection).
6
+ //
7
+ // Pure: text in, Map out, no imports.
8
+
9
+ /** Every rdfs:Class in `text`, as name -> { name, label, comment, subClassOf }.
10
+ * A class with no rdfs:label falls back to its own name; no rdfs:comment
11
+ * yields "". Blocks that are not classes (properties, say) are skipped. */
12
+ export function parseSchemaClasses(text) {
13
+ const classes = new Map();
14
+ const blocks = text.split(/\n(?=:[A-Za-z])/); // each class/property starts a new top-level block
15
+ for (const block of blocks) {
16
+ const head = /^:([A-Za-z0-9_]+)\s+a\s+rdfs:Class\s*;/.exec(block);
17
+ if (!head) continue;
18
+ const name = head[1];
19
+ const label = /rdfs:label\s+"([^"]*)"/.exec(block)?.[1] || name;
20
+ const comment = /rdfs:comment\s+"([^"]*)"/.exec(block)?.[1] || "";
21
+ const subClassOf = [...block.matchAll(/rdfs:subClassOf\s+:([A-Za-z0-9_]+)/g)].map((m) => m[1]);
22
+ classes.set(name, { name, label, comment, subClassOf });
23
+ }
24
+ return classes;
25
+ }
@@ -0,0 +1,87 @@
1
+ // parse.mjs — a targeted reader for SemCor's own regular YAML shape:
2
+ // flow-style lemmas/pos arrays and a folded single-quoted `text` scalar, one
3
+ // record per sentence. Not a general YAML parser (this repo has no YAML
4
+ // dependency), and not the same shape as the WordNet dump's reader in
5
+ // src/domain/wordnet/yaml.mjs — SemCor's flow style is JSON-compatible once
6
+ // isolated, which the WordNet subset never is.
7
+ //
8
+ // Pure: text in, arrays/strings out, no imports, so it runs with no SemCor
9
+ // clone present.
10
+
11
+ /** Split a SemCor YAML file into per-sentence record blocks (top-level
12
+ * "<key>:" lines, skipping the leading "_meta:" schema block). */
13
+ export function splitRecords(text) {
14
+ const lines = text.split("\n");
15
+ const blocks = [];
16
+ let i = 0;
17
+ while (i < lines.length && lines[i] !== "_meta:") i++;
18
+ i += 1;
19
+ while (i < lines.length && (lines[i].startsWith(" ") || lines[i].trim() === "")) i++; // skip rest of _meta
20
+ while (i < lines.length) {
21
+ if (/^[A-Za-z0-9_]+:$/.test(lines[i])) {
22
+ let j = i + 1;
23
+ const block = [];
24
+ while (j < lines.length && !/^[A-Za-z0-9_]+:$/.test(lines[j])) {
25
+ block.push(lines[j]);
26
+ j += 1;
27
+ }
28
+ blocks.push(block.join("\n"));
29
+ i = j;
30
+ } else {
31
+ i += 1;
32
+ }
33
+ }
34
+ return blocks;
35
+ }
36
+
37
+ /** Extract a flow-style JSON-compatible array value for `key` from one
38
+ * record block (lemmas/pos are double-quoted string arrays — valid JSON
39
+ * once isolated), balancing brackets across a line wrap if one occurs. */
40
+ export function extractArray(block, key) {
41
+ const re = new RegExp(`^\\s*${key}:\\s*(\\[.*)$`, "m");
42
+ const m = re.exec(block);
43
+ if (!m) return null;
44
+ let buf = m[1];
45
+ let depth = (buf.match(/\[/g) || []).length - (buf.match(/\]/g) || []).length;
46
+ const afterIdx = block.indexOf(m[0]) + m[0].length;
47
+ const rest = block.slice(afterIdx).split("\n");
48
+ let ri = 0;
49
+ while (depth > 0 && ri < rest.length) {
50
+ buf += `\n${rest[ri]}`;
51
+ depth += (rest[ri].match(/\[/g) || []).length - (rest[ri].match(/\]/g) || []).length;
52
+ ri += 1;
53
+ }
54
+ try { return JSON.parse(buf); } catch { return null; }
55
+ }
56
+
57
+ /** Extract the `text:` folded single-quoted scalar (YAML's own `''` ->
58
+ * literal `'` escape; line breaks folded to spaces). */
59
+ export function extractText(block) {
60
+ const m = /^\s*text:\s*'/m.exec(block);
61
+ if (!m) return null;
62
+ const start = block.indexOf("'", m.index);
63
+ let i = start + 1;
64
+ let raw = "";
65
+ while (i < block.length) {
66
+ if (block[i] === "'") {
67
+ if (block[i + 1] === "'") { raw += "'"; i += 2; continue; }
68
+ break;
69
+ }
70
+ raw += block[i];
71
+ i += 1;
72
+ }
73
+ return raw.replace(/\s+/g, " ").trim();
74
+ }
75
+
76
+ export const NOUN_POS = new Set(["NN", "NNS"]);
77
+
78
+ /** Simple-grammar filter: short, no semicolons/colons, no embedded quotes
79
+ * (which signal reported speech), no more than one comma — a rough proxy for
80
+ * "no complex embedded clauses". */
81
+ export function isSimpleSentence(text, wordCount) {
82
+ if (wordCount > 18) return false;
83
+ if (/[;:]/.test(text)) return false;
84
+ if ((text.match(/,/g) || []).length > 1) return false;
85
+ if (/"/.test(text)) return false;
86
+ return true;
87
+ }
@@ -42,13 +42,13 @@ function requireStore(store, needed, caller) {
42
42
  /** scm-sco: the subClassOf-transitivity rule, and the provenance tag its
43
43
  * conclusions carry. */
44
44
  export const SUBCLASS_PREDICATE = "rdfs:subClassOf";
45
- export const SYLLOGISE_RULE = "subClassOf";
45
+ const SYLLOGISE_RULE = "subClassOf";
46
46
  export const ENTAILED_PROVENANCE = `entailed:${SYLLOGISE_RULE}`;
47
47
 
48
48
  /** cax-sco: the type-propagation rule, and the provenance tag its conclusions
49
49
  * carry. */
50
50
  export const TYPE_PREDICATE = "rdf:type";
51
- export const CAX_SCO_RULE = "type";
51
+ const CAX_SCO_RULE = "type";
52
52
  export const ENTAILED_TYPE_PROVENANCE = `entailed:${CAX_SCO_RULE}`;
53
53
 
54
54
  /** cax-dw: x rdf:type C1, C1 owl:disjointWith C2 |= x is NOT of type C2 — a
@@ -396,7 +396,7 @@ export function deriveSomeValuesFromApplication(propertyEdges, typeEdges, subCla
396
396
  // deriveSomeValuesFromApplication reconstructs someValuesFrom restrictions.
397
397
  const HAS_PROPERTY_KEY = "has"; // synthetic marker parseCardinality always mints, never a real taught verb
398
398
  const CARDINALITY_KIND_OF = { "owl:cardinality": "exactly", "owl:mincardinality": "min", "owl:maxcardinality": "max" };
399
- export const ON_CLASS_PREDICATE = "owl:onClass";
399
+ const ON_CLASS_PREDICATE = "owl:onClass";
400
400
 
401
401
  /** Reconstructs pattern-5 cardinality restriction records from raw stored
402
402
  * rows touching a restriction node. A restriction is only admitted when its
@@ -525,7 +525,7 @@ function findOwnCardinalityRestriction(subClassEdges, cardinalityRestrictionEdge
525
525
  }
526
526
 
527
527
  // ---- cardinality monotonicity (outside OWL 2 RL's own decidable profile) ----
528
- export const SCM_CARD_RULE = "cardinalityMonotonicity";
528
+ const SCM_CARD_RULE = "cardinalityMonotonicity";
529
529
  /** Same sub-1 discount as CAX_DW_RULE_CONFIDENCE. No `syllogise()` call site
530
530
  * (this rule is query-rooted, never an enumerable Fact) — defined anyway so
531
531
  * chat.mjs's live proof chase can attach an auditable confidence figure
@@ -557,7 +557,7 @@ export function proveCardinalityAtLeast(subClassEdges, cardinalityRestrictionEdg
557
557
  // generalization — see this file's header comment; `cax-` prefix per this
558
558
  // ladder's "produces a provable no" naming convention, same epistemic status
559
559
  // as cax-dw) ----
560
- export const CAX_MAXC0_RULE = "maxCardinalityZero";
560
+ const CAX_MAXC0_RULE = "maxCardinalityZero";
561
561
  /** Same sub-1 discount and query-rooted caveat as CARDINALITY_RULE_CONFIDENCE. */
562
562
  export const CAX_MAXC0_RULE_CONFIDENCE = 0.95;
563
563
 
@@ -989,7 +989,7 @@ function buildSurvivorDerivabilityCheck(rows) {
989
989
  * A survivor keeps its stale, still-single justification as-is; a later
990
990
  * retraction of its OTHER supporting path therefore won't re-examine it.
991
991
  * Re-grounding survivors — or tracking every alternate justification set —
992
- * is the ATMS horizon (PLAN_SYLLOGIST.md §3), not this bounded slice.
992
+ * is the ATMS horizon, not this bounded slice.
993
993
  *
994
994
  * Returns { retracted, count, budget, depth, truncated, found } — `found` is
995
995
  * false when `subject ⊑ object` was never a stored fact.
@@ -0,0 +1,36 @@
1
+ // version-stamp.mjs — the home page's #pkg-version element, written and read
2
+ // from one place. Pure: strings in, strings out, no imports, so the deploy
3
+ // smoke check can reach it without npm ci.
4
+ //
5
+ // This existed three times and the copies had already drifted: the writer
6
+ // matched [^<]*, the smoke check demanded \d+\.\d+\.\d+, and the estate test
7
+ // accepted [^<\s]*. A writer that accepts what its reader rejects is a green
8
+ // build and a failed deploy, so the pattern lives here and all three call it.
9
+
10
+ /** The element that carries the version, and the value inside it. */
11
+ const STAMP = /(id="pkg-version"[^>]*>)\s*v?([^<]*?)\s*(<)/;
12
+
13
+ /** A semver core, which is what the deploy smoke check is entitled to expect. */
14
+ const SEMVER = /^\d+\.\d+\.\d+$/;
15
+
16
+ /** True iff `html` carries an element the stamp can be written into. */
17
+ export function hasVersionStamp(html) {
18
+ return STAMP.test(html);
19
+ }
20
+
21
+ /** The version `html` displays, or null when the element is absent or holds
22
+ * something that is not a semver core (an unstamped placeholder, say). */
23
+ export function parseVersionStamp(html) {
24
+ const found = STAMP.exec(html);
25
+ if (!found) return null;
26
+ const value = found[2].trim();
27
+ return SEMVER.test(value) ? value : null;
28
+ }
29
+
30
+ /** `html` with the stamp set to `version`. Throws when there is nothing to
31
+ * stamp — a page that lost its element would otherwise publish blank. */
32
+ export function stampVersion(html, version) {
33
+ if (!SEMVER.test(version)) throw new Error(`not a stampable version: "${version}"`);
34
+ if (!hasVersionStamp(html)) throw new Error("no #pkg-version element to stamp");
35
+ return html.replace(STAMP, `$1${version}$3`);
36
+ }
@@ -0,0 +1,133 @@
1
+ // yaml.mjs — a reader for the small YAML subset the Open English WordNet dump
2
+ // uses: 2-space-indented block mappings/sequences, quoted or bare scalars, and
3
+ // long scalar list-items that simply WRAP onto a further-indented continuation
4
+ // line. No block scalars, no anchors, no flow style — confirmed by direct
5
+ // inspection of the dump. This reads exactly that subset; it is not a general
6
+ // YAML parser.
7
+ //
8
+ // Pure: text in, object out, no imports, so it is testable without the WordNet
9
+ // clone the scripts that call it need.
10
+
11
+ /** Non-greedy key group so a MULTI-WORD entry key ("M-1 rifle", "ice cream")
12
+ * still matches — the first ": "/end-of-line colon wins, exactly as real
13
+ * YAML's block-mapping key/value split works. A plain wrapped scalar
14
+ * continuation line (a definition/example fragment) only coincidentally
15
+ * matches this if it ALSO happens to contain a bare "word: " sequence — rare
16
+ * in this corpus's prose, and this feeds a maintainer worksheet whose output
17
+ * is hand-reviewed, not a correctness-critical parser. */
18
+ const KEY_RE = /^(.+?):(\s+(.*)|)$/;
19
+
20
+ const isDash = (t) => t === "-" || t.startsWith("- ");
21
+
22
+ function parseScalar(s) {
23
+ const t = s.trim();
24
+ if ((t.startsWith("'") && t.endsWith("'") && t.length >= 2) || (t.startsWith('"') && t.endsWith('"') && t.length >= 2)) {
25
+ return t.slice(1, -1);
26
+ }
27
+ return t;
28
+ }
29
+
30
+ export function parseYaml(text) {
31
+ const rawLines = text.split("\n");
32
+ const lines = [];
33
+ for (const line of rawLines) {
34
+ if (!line.trim() || line.trim().startsWith("#")) continue;
35
+ const indent = line.length - line.trimStart().length;
36
+ lines.push({ indent, text: line.trimStart() });
37
+ }
38
+ let pos = 0;
39
+
40
+ // A scalar that may continue on subsequent MORE-indented lines with no
41
+ // "key:"/"- " marker of their own (WordNet's definition-wrapping style). A
42
+ // QUOTED scalar ('...' or "...") is handled separately: WordNet definitions
43
+ // routinely contain a literal ": " inside the quoted text itself (e.g. "…
44
+ // Matthew, Mark, Luke, and John" split across a line boundary right after a
45
+ // colon) — the bare-scalar heuristic below would misread that continuation
46
+ // line as a new "key:" line and truncate the string. Once inside an open
47
+ // quote, EVERY line is a continuation until one ends with the matching
48
+ // closing quote, full stop — the key/dash heuristic never applies inside it.
49
+ function parseScalarOrContinue(first, minContinIndent) {
50
+ const trimmed = first.trim();
51
+ const quote = trimmed[0] === "'" || trimmed[0] === '"' ? trimmed[0] : null;
52
+ if (quote) {
53
+ const closes = (s) => s.length >= 2 && s.endsWith(quote);
54
+ let buf = trimmed;
55
+ while (!closes(buf) && pos < lines.length && lines[pos].indent >= minContinIndent) {
56
+ buf += " " + lines[pos].text.trim();
57
+ pos += 1;
58
+ }
59
+ return closes(buf) ? buf.slice(1, -1) : buf;
60
+ }
61
+ let s = parseScalar(first);
62
+ while (pos < lines.length && lines[pos].indent >= minContinIndent
63
+ && !isDash(lines[pos].text) && !KEY_RE.test(lines[pos].text)) {
64
+ s += " " + lines[pos].text.trim();
65
+ pos += 1;
66
+ }
67
+ return s;
68
+ }
69
+
70
+ /** The value that follows a "key:" (bare, no inline scalar) — peeks at the
71
+ * next line to decide whether it's a nested sequence (which YAML allows to
72
+ * sit at the SAME indent as the key itself, not just deeper) or a nested
73
+ * mapping (which must be deeper) or simply absent (null). `parentIndent`
74
+ * is the indent of the "key:" line whose value this resolves. */
75
+ function parseValue(parentIndent) {
76
+ if (pos >= lines.length || lines[pos].indent < parentIndent) return null;
77
+ const line = lines[pos];
78
+ if (isDash(line.text)) return parseSeq(line.indent);
79
+ if (line.indent > parentIndent && KEY_RE.test(line.text)) return parseMap(line.indent);
80
+ return null;
81
+ }
82
+
83
+ function parseSeq(indent) {
84
+ const arr = [];
85
+ while (pos < lines.length && lines[pos].indent === indent && isDash(lines[pos].text)) {
86
+ const dashIndent = indent;
87
+ const rest = lines[pos].text === "-" ? "" : lines[pos].text.slice(2);
88
+ pos += 1;
89
+ if (rest === "") {
90
+ arr.push(parseValue(dashIndent));
91
+ continue;
92
+ }
93
+ // A quoted scalar is classified FIRST, unconditionally — WordNet
94
+ // definitions routinely contain a literal ": " (or a colon at the very
95
+ // end of a wrapped line, e.g. "…including:\n whales, …") inside quoted
96
+ // prose, which KEY_RE would otherwise misread as an inline "- key:"
97
+ // mapping. Only an UNQUOTED rest is even considered for that shape.
98
+ const quoted = rest[0] === "'" || rest[0] === '"';
99
+ const m = quoted ? null : KEY_RE.exec(rest);
100
+ if (m) {
101
+ // "- key: value" or "- key:" — an inline mapping for this list item;
102
+ // sibling keys of the SAME item are indented +2 from the dash.
103
+ const obj = {};
104
+ obj[m[1]] = m[3] !== undefined && m[3] !== "" ? parseScalarOrContinue(m[3], dashIndent + 2) : parseValue(dashIndent + 2);
105
+ while (pos < lines.length && lines[pos].indent === dashIndent + 2 && KEY_RE.test(lines[pos].text)) {
106
+ const mm = KEY_RE.exec(lines[pos].text);
107
+ pos += 1;
108
+ obj[mm[1]] = mm[3] !== undefined && mm[3] !== "" ? parseScalarOrContinue(mm[3], dashIndent + 4) : parseValue(dashIndent + 2);
109
+ }
110
+ arr.push(obj);
111
+ } else {
112
+ // a plain (or quoted) scalar list item — may wrap onto continuation lines
113
+ arr.push(parseScalarOrContinue(rest, dashIndent + 2));
114
+ }
115
+ }
116
+ return arr;
117
+ }
118
+
119
+ function parseMap(indent) {
120
+ const obj = {};
121
+ while (pos < lines.length && lines[pos].indent === indent && KEY_RE.test(lines[pos].text)) {
122
+ const m = KEY_RE.exec(lines[pos].text);
123
+ const key = parseScalar(m[1]);
124
+ pos += 1;
125
+ obj[key] = m[3] !== undefined && m[3] !== "" ? parseScalarOrContinue(m[3], indent + 2) : parseValue(indent);
126
+ // (parseValue(indent) — not indent+2 — so a same-indent sequence value
127
+ // is recognized; parseValue itself accepts child indent >= indent.)
128
+ }
129
+ return obj;
130
+ }
131
+
132
+ return parseMap(0);
133
+ }
@@ -9,8 +9,8 @@
9
9
  //
10
10
  // createSession(…) is the SESSION SINK every shell shares (runChat's
11
11
  // readline loop below, src/surfaces/tui/app.mjs's Ink shell). chat.mjs re-exports
12
- // createSession/runChat/gitToplevel and the session constants so existing
13
- // import sites keep working.
12
+ // createSession/runChat and the session constants so existing import sites
13
+ // keep working.
14
14
 
15
15
  import { join, resolve } from "node:path";
16
16
  import { createWriteStream } from "node:fs";
@@ -62,8 +62,8 @@ export { uuidv7 };
62
62
  // in the session layer so runTurn and the fact engine below stay free of
63
63
  // node:fs/child_process/os/readline. Re-exported here (services → services) so
64
64
  // every existing import site — bin, tui, server-http, index, tests — keeps
65
- // importing createSession/runChat/gitToplevel/SESSION_LOG_DIR/PROMPT from chat.mjs.
66
- export { createSession, runChat, gitToplevel, SESSION_LOG_DIR, PROMPT } from "./chat-session.mjs";
65
+ // importing createSession/runChat/SESSION_LOG_DIR/PROMPT from chat.mjs.
66
+ export { createSession, runChat, SESSION_LOG_DIR, PROMPT } from "./chat-session.mjs";
67
67
 
68
68
  /** dispatchTool("tmct_ask", …) returns the prose answer plus a delimited
69
69
  * machine-readable envelope; the TUI shows the prose only. Reused verbatim
@@ -15,8 +15,8 @@
15
15
  // 3. tmct.toml's `graph_file` / `graph_files`
16
16
  // 4. <repo>/.tmct/graph.json, where repo is --repo, else git root, else cwd
17
17
  //
18
- // Deliberately does NOT import chat.mjs (would be circular) — the git-root lookup below
19
- // is a small, self-contained copy of chat.mjs's own gitToplevel().
18
+ // Deliberately does NOT import chat-session.mjs (would be circular) — the git-root lookup
19
+ // below is a small, self-contained copy of chat-session.mjs's own gitToplevel().
20
20
 
21
21
  import { spawnSync } from "node:child_process";
22
22
  import { dirname, resolve } from "node:path";
@@ -26,8 +26,8 @@ import { loadTomlConfig, normalizeConfig, mergeEffective, CONFIG_FILE } from "..
26
26
  import { DEFAULT_GRAPH_REL } from "../adapters/config.mjs";
27
27
 
28
28
  /** The git top-level for `cwd`, or null if not in a repo (or git is
29
- * unavailable). A deliberate re-declaration of chat.mjs's gitToplevel (not an
30
- * import) — see the file docblock for why. */
29
+ * unavailable). A deliberate re-declaration of chat-session.mjs's gitToplevel
30
+ * (not an import) — see the file docblock for why. */
31
31
  function defaultGitRoot(cwd) {
32
32
  try {
33
33
  const r = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
@@ -18,7 +18,7 @@ export { flatten };
18
18
 
19
19
  const GRAMMAR_DIR = dirname(fileURLToPath(import.meta.url));
20
20
  /** The data-driven grammar-rule table. */
21
- export const GRAMMAR_RULES_FILE = join(GRAMMAR_DIR, "..", "..", "data", "templates", "grammar-rules.toml");
21
+ const GRAMMAR_RULES_FILE = join(GRAMMAR_DIR, "..", "..", "data", "templates", "grammar-rules.toml");
22
22
 
23
23
  /** The segment type vocabulary. `prose` is the only unprotected type. */
24
24
  export const SEGMENT_TYPES = Object.freeze([
@@ -1,6 +1,5 @@
1
1
  // ledger-viz.mjs — `tmct viz`: the memory graph as a readable ledger of
2
- // fact-sentences around one focus term, with the in-page chat dock
3
- // (PLAN_VIZ_LEDGER.md).
2
+ // fact-sentences around one focus term, with the in-page chat dock.
4
3
  //
5
4
  // Three pure/impure-separated pieces:
6
5
  // - computeLedgerData(repoDir, opts) — I/O (loadMemory) + derivation
@@ -16,7 +15,7 @@ import { readFile } from "node:fs/promises";
16
15
  import { fileURLToPath } from "node:url";
17
16
  import { dirname, join } from "node:path";
18
17
 
19
- export const LEDGER_ROW_LIMIT_DEFAULT = 20000;
18
+ const LEDGER_ROW_LIMIT_DEFAULT = 20000;
20
19
 
21
20
  /** Read the checked-in browser memory-ask-engine bundle
22
21
  * (`src/surfaces/web/memory-ask-browser.bundle.js`) — the real memory-graph answer engine
@@ -24,7 +24,7 @@ import { turnKey } from "../domain/memory/session-turns.mjs";
24
24
  export const SESSIONS_DIR_REL = join(".tmct", "sessions");
25
25
 
26
26
  export const SESSION_CLASS = "Session";
27
- export const ASKS_ABOUT_PREDICATE = "asksAbout";
27
+ const ASKS_ABOUT_PREDICATE = "asksAbout";
28
28
  export const ASKS_ABOUT_PROP = "mgx:asksAbout";
29
29
 
30
30
  const QUERIES_ATTR_CAP = 500; // joined-queries attribute cap (mirrors commitMessage's cap idea)
@@ -111,9 +111,9 @@ export function upsertSession(entities, record) {
111
111
  attributes: [
112
112
  // referenced via the imported constant (single-sourced from memory/core.mjs)
113
113
  { prop: CREATED_AT_PROP, key: "createdAt", value: priorCreatedAt || started || new Date().toISOString() },
114
- // this IS a genuinely-mutated individual (re-written every chat turn, PLAN_VIZ.md §2's
115
- // concrete "Session" case) — stamp updatedAt explicitly since the derived "max over edges"
116
- // rule can't see an own-attribute rewrite that touches no edge.
114
+ // a Session really is mutated re-written every chat turn so stamp updatedAt
115
+ // explicitly: the derived "max over edges" rule cannot see an own-attribute rewrite
116
+ // that touches no edge.
117
117
  { prop: UPDATED_AT_PROP, key: "updatedAt", value: new Date().toISOString() },
118
118
  { prop: "mgx:sessionStarted", key: "started", value: started },
119
119
  { prop: "mgx:sessionEnded", key: "ended", value: ended },
@@ -1,7 +1,6 @@
1
1
  // viz-theme.mjs — the shared assets for tmct's generated HTML pages
2
- // (the ledger explorer and the plan player): the visual token table
3
- // (PLAN_VIZ_LEDGER.md's reference values) plus the escaping helpers every
4
- // page builder needs.
2
+ // (the ledger explorer and the plan player): the visual token table plus the
3
+ // escaping helpers every page builder needs.
5
4
  //
6
5
  // Trust tiers are precomputed rgba() values per provenance color so pages
7
6
  // render identically on browsers without color-mix() support.
@@ -30,7 +29,7 @@ function rgba(hex, alpha) {
30
29
  return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;
31
30
  }
32
31
 
33
- export const TOKENS = Object.freeze({
32
+ const TOKENS = Object.freeze({
34
33
  light: Object.freeze({
35
34
  bg: "#F7F6F2", ink: "#23272B", muted: "#6E7168", line: "#DDD9D0", card: "#FFFFFF",
36
35
  taught: "#2E7D4F", corpus: "#5A80AC", entail: "#B07C2E", alert: "#B0503F",