@polycode-projects/the-mechanical-code-talker 0.2.0 → 0.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.
package/src/chat.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // chat.mjs — `tmct chat`: a full interactive client over the tmct code-graph.
2
2
  // Any BARE line is a plain-English question dispatched through the mechanical
3
- // tmct_ask engine (the EXACT path bin/cli.mjs's `cli tmct_ask` fallback uses),
3
+ // tmct_ask engine (the EXACT path bin/tmct.mjs's `cli tmct_ask` fallback uses),
4
4
  // so chat is the same zero-model engine with a readline shell around it, plus:
5
5
  //
6
6
  // - SLASH-COMMANDS to reach every richer tool dispatchTool (server.mjs) serves —
@@ -31,6 +31,11 @@
31
31
  // focus }) so tests exercise it directly; every ask.mjs import is LAZY and
32
32
  // failure-tolerated, so concurrent evolution of the engine can never crash a turn
33
33
  // (worst case a turn records fewer ids / an honest miss hint, never wrong data).
34
+ //
35
+ // createSession(…) is the SESSION SINK every shell shares: it owns the artifact
36
+ // files, the per-turn writeLog → writeSidecar → upsertGraph sequencing (order is
37
+ // load-bearing — see its docblock), telemetry, and the close. runChat is the
38
+ // readline shell over it; src/tui/app.mjs is the Ink shell over the same sink.
34
39
 
35
40
  import { join } from "node:path";
36
41
  import { createWriteStream } from "node:fs";
@@ -315,7 +320,7 @@ export function gitToplevel(cwd = process.cwd()) {
315
320
  return null;
316
321
  }
317
322
 
318
- /** Mirror bin/cli.mjs's configFor: an explicit repo pins the artifact path; no
323
+ /** Mirror bin/tmct.mjs's configFor: an explicit repo pins the artifact path; no
319
324
  * repo falls back to the cwd/env-derived default. */
320
325
  function configFor(repoPath) {
321
326
  return repoPath ? { graphFile: join(repoPath, DEFAULT_GRAPH_REL) } : loadConfig();
@@ -475,6 +480,34 @@ async function runCommand(line, { config, source, graph, focus }) {
475
480
  return mk(answer);
476
481
  }
477
482
 
483
+ /** A declarative ACE-grammar sentence → assert into memory + confirm; null on
484
+ * any grammar miss / residue / import failure so the query engine keeps first
485
+ * refusal on everything else. Lazy imports + catch-all: the grammar layer can
486
+ * never crash a turn (chat.mjs ethos). Writes ONLY under memoryDir/.tmct/memory. */
487
+ async function assertTurn(line, { memoryDir, sessionId, focus }) {
488
+ try {
489
+ const { parseAce } = await import("./grammar/ace.mjs");
490
+ const { loadLexicon } = await import("./grammar/lexicon.mjs");
491
+ const parse = parseAce(line, loadLexicon());
492
+ if (!parse || !parse.triples?.length || parse.residue?.length) return null;
493
+ const { assertSentence } = await import("./grammar/assert.mjs");
494
+ const { normFactTerm } = await import("./memory/core.mjs");
495
+ const ts = new Date().toISOString();
496
+ const res = await assertSentence(memoryDir, line, {
497
+ provenance: { source: "chat", sessionId, ts },
498
+ });
499
+ if (!res || !res.ids?.length) return null;
500
+ const shown = res.triples
501
+ .map((t) => `${normFactTerm(t.subject)} ${t.predicate} ${normFactTerm(t.object)}`)
502
+ .join("; ");
503
+ const n = res.ids.length;
504
+ const answer = `noted — remembered ${n} fact${n === 1 ? "" : "s"}: ${shown}`;
505
+ return plainTurn(line, answer, { command: "assert", focus });
506
+ } catch {
507
+ return null; // grammar unavailable / write failed — fall through to the engine
508
+ }
509
+ }
510
+
478
511
  /**
479
512
  * One chat turn: input → { answer, logLines, record, focus }. Pure of any
480
513
  * TTY/stream concerns so tests exercise it directly. A leading `/` routes to a
@@ -489,9 +522,9 @@ async function runCommand(line, { config, source, graph, focus }) {
489
522
  * subject), `answeredIds` the entity ids an ask answer cited; a slash-command turn
490
523
  * also carries its `command` name. Both drive the mgx:asksAbout graph append.
491
524
  */
492
- export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null } = {}) {
525
+ export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "" } = {}) {
493
526
  const line = String(input ?? "").trim();
494
- const ctx = { config, source, graph, focus, last };
527
+ const ctx = { config, source, graph, focus, last, memoryDir, sessionId };
495
528
  // A DISPATCHED turn (count / slash-command / ask) becomes the new "last answer"
496
529
  // that why/say-more re-renders; a conversational turn does not (it preserves it).
497
530
  const withLast = (result) => ({ ...result, last: { query: line, answer: result.answer, detail: result.detail ?? null } });
@@ -502,6 +535,14 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
502
535
  if (convo) return convo;
503
536
 
504
537
  if (line.startsWith("/")) return withLast(await runCommand(line, ctx));
538
+ // Declarative ACE sentences ("every module is a artifact") ASSERT into tmct's
539
+ // own memory and confirm — they are statements to remember, not graph queries.
540
+ // Gated on memoryDir: only a session shell provides a write target, so a bare
541
+ // runTurn (tests, library callers) stays pure and falls through to the engine.
542
+ if (memoryDir) {
543
+ const asserted = await assertTurn(line, ctx);
544
+ if (asserted) return withLast(asserted);
545
+ }
505
546
  // Aggregate/count questions are answered mechanically off the loaded graph header,
506
547
  // BEFORE falling through to the ask engine (focus unchanged — a count names no entity).
507
548
  const count = answerCount(graph, line);
@@ -514,16 +555,28 @@ const shortLabel = (l) => { const s = String(l); return s.length > 40 ? "…" +
514
555
  const promptFor = (focus) => (focus ? `tmct(${shortLabel(focus.label)})> ` : PROMPT);
515
556
 
516
557
  /**
517
- * The interactive shell. Streams are injectable so tests run scripted sessions
518
- * without a TTY. A repo with NO graph artifact is not an error: the session
519
- * starts from the empty bootstrap graph (the banner says so honestly) and the
520
- * first turn's fold-in creates .tmct/graph.json from the conversation itself.
521
- * Returns { logFile, sidecarFile, turns } once the session ends.
558
+ * The SESSION SINK everything a chat shell (readline below, the Ink TUI, any
559
+ * future surface) must share so the on-disk session contract stays identical no
560
+ * matter what draws the screen:
561
+ *
562
+ * - repo/config resolution (git root default, --repo override) + the one-time
563
+ * graph load and banner strings;
564
+ * - the transcript log + structured sidecar file creation and per-turn
565
+ * writeLog → writeSidecar → upsertGraph sequencing. THE ORDER IS LOAD-BEARING:
566
+ * the memory side-write (sessions.mjs) recovers each turn's ANSWER text by
567
+ * re-reading the transcript keyed by turnKey(record.ts, query), so the log
568
+ * line must be flushed before the graph upsert runs, and logLines[0] must be
569
+ * the record's ts (runTurn guarantees that);
570
+ * - opt-in telemetry and the end-of-session close (end lines, final upsert,
571
+ * stream flush).
572
+ *
573
+ * Returns { repo, config, graph, moduleCount, version, sessionId, logFile,
574
+ * sidecarFile, bannerLines, empty, focus, turns, promptFor(), turn(line), close() }.
575
+ * `turn(line)` runs one dispatched turn through runTurn and the full sink
576
+ * sequencing, returning { answer, end, prompt }; `close()` is idempotent.
522
577
  */
523
- export async function runChat({
578
+ export async function createSession({
524
579
  repoPath,
525
- input = process.stdin,
526
- output = process.stdout,
527
580
  source = defaultSource,
528
581
  env = process.env,
529
582
  cwd = process.cwd(),
@@ -548,7 +601,7 @@ export async function runChat({
548
601
  const moduleCount = graph.individuals.filter((i) => (i.class || "") === "Module").length;
549
602
  const { version } = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
550
603
 
551
- // Opt-in telemetry (default OFF → null → the loop's `tel?.record` is a no-op, and
604
+ // Opt-in telemetry (default OFF → null → the sink's `tel?.record` is a no-op, and
552
605
  // nothing is written). The conversational session log + sidecar above stay the
553
606
  // authoritative chat record; this is the machine-readable query telemetry.
554
607
  const tel = createTelemetry({ env, config, surface: "chat" });
@@ -562,7 +615,7 @@ export async function runChat({
562
615
  const sidecarFile = join(sessionsDir, `session-${sessionId}.jsonl`);
563
616
  const stream = createWriteStream(logFile, { flags: "a" });
564
617
  const sidecar = createWriteStream(sidecarFile, { flags: "a" });
565
- // Awaited writes: each chunk is handed to the OS before the loop continues, so a
618
+ // Awaited writes: each chunk is handed to the OS before the turn completes, so a
566
619
  // killed session keeps everything up to the last completed turn — in both files.
567
620
  const flush = (s, text) =>
568
621
  new Promise((resolve, reject) => s.write(text, (e) => (e ? reject(e) : resolve())));
@@ -584,35 +637,36 @@ export async function runChat({
584
637
  catch { /* best-effort — see above */ }
585
638
  };
586
639
 
587
- const dim = (s) => (env.NO_COLOR || !output.isTTY ? s : `\x1b[2m${s}\x1b[0m`);
588
- if (graph.individuals.length === 0) {
589
- // Empty-graph bootstrap: honest-miss messaging, never an error before the prompt.
590
- output.write(dim(`tmct chat ${repo} no graph loaded starting empty; ` +
591
- `the conversation is remembered to ${DEFAULT_GRAPH_REL} — log ${logFile}`) + "\n");
592
- } else {
593
- output.write(dim(`tmct chat — ${repo} — ${moduleCount} module(s) — log ${logFile}`) + "\n");
594
- }
595
- output.write(dim("pass --repo <path> to target a different repo") + "\n");
596
- output.write(dim("ask a question, or /help for commands (/stats for an overview) — /exit to leave") + "\n");
597
-
598
- const rl = createInterface({ input, output, prompt: PROMPT });
599
- rl.on("SIGINT", () => rl.close()); // Ctrl+C behaves like /exit (clean close, log flushed)
600
- let closed = false;
601
- rl.on("close", () => { closed = true; });
602
- const prompt = () => { if (!closed) rl.prompt(); }; // input may end while a turn is in flight
640
+ const empty = graph.individuals.length === 0;
641
+ const bannerLines = [
642
+ empty
643
+ // Empty-graph bootstrap: honest-miss messaging, never an error before the prompt.
644
+ ? `tmct chat ${repo} — no graph loaded — starting empty; ` +
645
+ `the conversation is remembered to ${DEFAULT_GRAPH_REL} log ${logFile}`
646
+ : `tmct chat — ${repo} — ${moduleCount} module(s) — log ${logFile}`,
647
+ "pass --repo <path> to target a different repo",
648
+ "ask a question, or /help for commands (/stats for an overview) /exit to leave",
649
+ ];
603
650
 
604
651
  let turns = 0;
605
652
  let focus = null; // the current focus entity ({id,label}) — threaded turn to turn
606
653
  let last = null; // the last dispatched answer ({query,answer,detail}) — why/say-more re-renders it
607
- prompt();
608
- for await (const raw of rl) { // Ctrl+D / closed stdin ends the iteration cleanly
609
- const line = raw.trim();
610
- if (line === "/exit") break;
611
- if (line) {
612
- const { answer, logLines, record, focus: nextFocus, last: nextLast, end } = await runTurn(line, { config, source, graph, focus, last });
654
+ let closed = false;
655
+
656
+ return {
657
+ repo, config, graph, moduleCount, version, sessionId, logFile, sidecarFile,
658
+ bannerLines, empty,
659
+ get focus() { return focus; },
660
+ get turns() { return turns; },
661
+ promptFor: () => promptFor(focus),
662
+
663
+ /** One dispatched turn through the FULL sink sequencing (writeLog → writeSidecar
664
+ * → telemetry → upsertGraph, in that exact order). Returns { answer, end, prompt }. */
665
+ async turn(line) {
666
+ const { answer, logLines, record, focus: nextFocus, last: nextLast, end } =
667
+ await runTurn(line, { config, source, graph, focus, last, memoryDir: repo, sessionId });
613
668
  focus = nextFocus;
614
669
  last = nextLast;
615
- output.write(answer + "\n");
616
670
  await writeLog(logLines.join("\n") + "\n");
617
671
  await writeSidecar(record);
618
672
  turnRecords.push(record);
@@ -625,18 +679,66 @@ export async function runChat({
625
679
  });
626
680
  await upsertGraph(record.ts);
627
681
  turns += 1;
628
- rl.setPrompt(promptFor(focus));
682
+ return { answer, end: Boolean(end), prompt: promptFor(focus) };
683
+ },
684
+
685
+ /** End-of-session close: end lines in both artifacts, the final graph upsert
686
+ * (which also triggers the memory fold), stream flush. Idempotent. */
687
+ async close() {
688
+ if (closed) return;
689
+ closed = true;
690
+ const endIso = new Date().toISOString();
691
+ await writeLog(`${endIso}\n> /exit\nsession end ${endIso}\n`);
692
+ await writeSidecar({ type: "end", ts: endIso });
693
+ await upsertGraph(endIso);
694
+ await new Promise((resolve) => stream.end(resolve));
695
+ await new Promise((resolve) => sidecar.end(resolve));
696
+ },
697
+ };
698
+ }
699
+
700
+ /**
701
+ * The interactive readline shell over createSession — the `--plain` surface and
702
+ * the scripted-test surface. Streams are injectable so tests run sessions
703
+ * without a TTY. A repo with NO graph artifact is not an error: the session
704
+ * starts from the empty bootstrap graph (the banner says so honestly) and the
705
+ * first turn's fold-in creates .tmct/graph.json from the conversation itself.
706
+ * Returns { logFile, sidecarFile, turns } once the session ends.
707
+ */
708
+ export async function runChat({
709
+ repoPath,
710
+ input = process.stdin,
711
+ output = process.stdout,
712
+ source = defaultSource,
713
+ env = process.env,
714
+ cwd = process.cwd(),
715
+ gitRoot = gitToplevel,
716
+ } = {}) {
717
+ const session = await createSession({ repoPath, source, env, cwd, gitRoot });
718
+
719
+ const dim = (s) => (env.NO_COLOR || !output.isTTY ? s : `\x1b[2m${s}\x1b[0m`);
720
+ for (const line of session.bannerLines) output.write(dim(line) + "\n");
721
+
722
+ const rl = createInterface({ input, output, prompt: PROMPT });
723
+ rl.on("SIGINT", () => rl.close()); // Ctrl+C behaves like /exit (clean close, log flushed)
724
+ let closed = false;
725
+ rl.on("close", () => { closed = true; });
726
+ const prompt = () => { if (!closed) rl.prompt(); }; // input may end while a turn is in flight
727
+
728
+ prompt();
729
+ for await (const raw of rl) { // Ctrl+D / closed stdin ends the iteration cleanly
730
+ const line = raw.trim();
731
+ if (line === "/exit") break;
732
+ if (line) {
733
+ const { answer, end, prompt: nextPrompt } = await session.turn(line);
734
+ output.write(answer + "\n");
735
+ rl.setPrompt(nextPrompt);
629
736
  if (end) break; // a conversational "bye"/"goodbye" — clean end, same as /exit
630
737
  }
631
738
  prompt();
632
739
  }
633
740
  rl.close();
634
741
 
635
- const endIso = new Date().toISOString();
636
- await writeLog(`${endIso}\n> /exit\nsession end ${endIso}\n`);
637
- await writeSidecar({ type: "end", ts: endIso });
638
- await upsertGraph(endIso);
639
- await new Promise((resolve) => stream.end(resolve));
640
- await new Promise((resolve) => sidecar.end(resolve));
641
- return { logFile, sidecarFile, turns };
742
+ await session.close();
743
+ return { logFile: session.logFile, sidecarFile: session.sidecarFile, turns: session.turns };
642
744
  }
@@ -0,0 +1,251 @@
1
+ # conceptnet-map.toml — the relation → ACE-OWL-pattern mapping table
2
+ # (ROADMAP Phase 2, ConceptNet corpus slice).
3
+ #
4
+ # One row per ConceptNet relation (the canonical closed set of 34 —
5
+ # docs/references/schemas/conceptnet-relations.md is the reference list this
6
+ # table is drift-checked against; test/corpus-conceptnet.test.mjs fails if the
7
+ # committed slice contains a relation with no row here).
8
+ #
9
+ # Fields:
10
+ # rel the ConceptNet relation URI ("/r/IsA")
11
+ # surface the canonical surface template, "{start}"/"{end}" slotted —
12
+ # the "A dog is a kind of animal" sentence shape this relation reads as
13
+ # ace which ACE-OWL pattern it maps to (docs/references/schemas/
14
+ # ace-owl-fragment.md): subClassOf | type | ObjectProperty |
15
+ # someValuesFrom | disjointWith | property | none
16
+ # predicate the predicate URI src/corpus/conceptnet.mjs emits into memory
17
+ # facts (absent when ace = "none" — no fact is emitted)
18
+ # note why, and what a "none" row is still good for
19
+ #
20
+ # Loader contract (src/corpus/conceptnet.mjs): a relation in the slice that is
21
+ # MISSING here is an error (drift guard); a row with ace = "none" is a
22
+ # deliberate non-emission, silently skipped by toFacts().
23
+
24
+ [[relation]]
25
+ rel = "/r/IsA"
26
+ surface = "a {start} is a kind of {end}"
27
+ ace = "subClassOf"
28
+ predicate = "rdfs:subClassOf"
29
+ note = "hyponym → hypernym; ACE pattern 1 ('every N1 is a N2')"
30
+
31
+ [[relation]]
32
+ rel = "/r/DefinedAs"
33
+ surface = "a {start} is defined as {end}"
34
+ ace = "subClassOf"
35
+ predicate = "rdfs:subClassOf"
36
+ note = "definitional IsA; also ACE pattern 1"
37
+
38
+ [[relation]]
39
+ rel = "/r/PartOf"
40
+ surface = "a {start} is part of a {end}"
41
+ ace = "ObjectProperty"
42
+ predicate = "mgx:partOf"
43
+ note = "meronymy; ACE pattern 3 (N1 VERB N2)"
44
+
45
+ [[relation]]
46
+ rel = "/r/HasA"
47
+ surface = "a {start} has a {end}"
48
+ ace = "ObjectProperty"
49
+ predicate = "mgx:hasA"
50
+ note = "possession/holonymy; plain edge today — ACE pattern 5 cardinality is a later refinement"
51
+
52
+ [[relation]]
53
+ rel = "/r/UsedFor"
54
+ surface = "a {start} is used for {end}"
55
+ ace = "ObjectProperty"
56
+ predicate = "mgx:usedFor"
57
+ note = "typical purpose; ACE pattern 3"
58
+
59
+ [[relation]]
60
+ rel = "/r/CapableOf"
61
+ surface = "a {start} can {end}"
62
+ ace = "ObjectProperty"
63
+ predicate = "mgx:capableOf"
64
+ note = "typical capability; ACE pattern 3"
65
+
66
+ [[relation]]
67
+ rel = "/r/AtLocation"
68
+ surface = "you are likely to find a {start} in a {end}"
69
+ ace = "ObjectProperty"
70
+ predicate = "mgx:atLocation"
71
+ note = "typical location; ACE pattern 3"
72
+
73
+ [[relation]]
74
+ rel = "/r/Causes"
75
+ surface = "a {start} causes {end}"
76
+ ace = "ObjectProperty"
77
+ predicate = "mgx:causes"
78
+ note = "causation; ACE pattern 3"
79
+
80
+ [[relation]]
81
+ rel = "/r/HasSubevent"
82
+ surface = "when you {start}, you {end}"
83
+ ace = "ObjectProperty"
84
+ predicate = "mgx:hasSubevent"
85
+ note = "event decomposition; ACE pattern 3"
86
+
87
+ [[relation]]
88
+ rel = "/r/HasFirstSubevent"
89
+ surface = "the first thing you do when you {start} is {end}"
90
+ ace = "ObjectProperty"
91
+ predicate = "mgx:hasFirstSubevent"
92
+ note = "first step; ACE pattern 3"
93
+
94
+ [[relation]]
95
+ rel = "/r/HasLastSubevent"
96
+ surface = "the last thing you do when you {start} is {end}"
97
+ ace = "ObjectProperty"
98
+ predicate = "mgx:hasLastSubevent"
99
+ note = "last step; ACE pattern 3"
100
+
101
+ [[relation]]
102
+ rel = "/r/HasPrerequisite"
103
+ surface = "in order to {start}, you must {end}"
104
+ ace = "ObjectProperty"
105
+ predicate = "mgx:hasPrerequisite"
106
+ note = "dependency; ACE pattern 3"
107
+
108
+ [[relation]]
109
+ rel = "/r/HasProperty"
110
+ surface = "a {start} is {end}"
111
+ ace = "property"
112
+ predicate = "mgx:hasProperty"
113
+ note = "attribute/adjective; ACE pattern 8 ('N1 is ADJ')"
114
+
115
+ [[relation]]
116
+ rel = "/r/MotivatedByGoal"
117
+ surface = "you would {start} because you want to {end}"
118
+ ace = "ObjectProperty"
119
+ predicate = "mgx:motivatedByGoal"
120
+ note = "motivation; ACE pattern 3"
121
+
122
+ [[relation]]
123
+ rel = "/r/ObstructedBy"
124
+ surface = "{start} can be prevented by {end}"
125
+ ace = "ObjectProperty"
126
+ predicate = "mgx:obstructedBy"
127
+ note = "blocker; ACE pattern 3"
128
+
129
+ [[relation]]
130
+ rel = "/r/Desires"
131
+ surface = "a {start} wants {end}"
132
+ ace = "ObjectProperty"
133
+ predicate = "mgx:desires"
134
+ note = "typical desire; ACE pattern 3"
135
+
136
+ [[relation]]
137
+ rel = "/r/CausesDesire"
138
+ surface = "{start} makes you want to {end}"
139
+ ace = "ObjectProperty"
140
+ predicate = "mgx:causesDesire"
141
+ note = "evoked desire; ACE pattern 3"
142
+
143
+ [[relation]]
144
+ rel = "/r/CreatedBy"
145
+ surface = "a {start} is created by a {end}"
146
+ ace = "ObjectProperty"
147
+ predicate = "mgx:createdBy"
148
+ note = "provenance; ACE pattern 3"
149
+
150
+ [[relation]]
151
+ rel = "/r/MadeOf"
152
+ surface = "a {start} is made of {end}"
153
+ ace = "ObjectProperty"
154
+ predicate = "mgx:madeOf"
155
+ note = "material; ACE pattern 3"
156
+
157
+ [[relation]]
158
+ rel = "/r/ReceivesAction"
159
+ surface = "a {start} can be {end}"
160
+ ace = "ObjectProperty"
161
+ predicate = "mgx:receivesAction"
162
+ note = "typical patient role; ACE pattern 3"
163
+
164
+ [[relation]]
165
+ rel = "/r/LocatedNear"
166
+ surface = "a {start} is typically near a {end}"
167
+ ace = "ObjectProperty"
168
+ predicate = "mgx:locatedNear"
169
+ note = "proximity; ACE pattern 3"
170
+
171
+ [[relation]]
172
+ rel = "/r/MannerOf"
173
+ surface = "{start} is a way to {end}"
174
+ ace = "ObjectProperty"
175
+ predicate = "mgx:mannerOf"
176
+ note = "verb specialization — properly rdfs:subPropertyOf between verbs; stored as a plain edge until the grammar grows verb hierarchies"
177
+
178
+ [[relation]]
179
+ rel = "/r/DistinctFrom"
180
+ surface = "a {start} is not a {end}"
181
+ ace = "disjointWith"
182
+ predicate = "owl:disjointWith"
183
+ note = "mutual exclusion; ACE pattern 6 ('no N1 is a N2')"
184
+
185
+ # --- unmappable relations: no clean OWL-axiom fit; kept for other consumers ---
186
+
187
+ [[relation]]
188
+ rel = "/r/RelatedTo"
189
+ surface = "{start} is related to {end}"
190
+ ace = "none"
191
+ note = "weakest, undirected association — too vague for an axiom; useful later as a fuzzy-match hint, never a fact"
192
+
193
+ [[relation]]
194
+ rel = "/r/Synonym"
195
+ surface = "{start} means the same as {end}"
196
+ ace = "none"
197
+ note = "lexical alias, not an axiom — feeds the grammar lexicon / phrasebook synonym families instead"
198
+
199
+ [[relation]]
200
+ rel = "/r/Antonym"
201
+ surface = "{start} is the opposite of {end}"
202
+ ace = "none"
203
+ note = "lexical opposition; OWL disjointness would over-claim (hot/cold are not disjoint classes) — DistinctFrom carries the real disjointness"
204
+
205
+ [[relation]]
206
+ rel = "/r/FormOf"
207
+ surface = "{start} is a form of the word {end}"
208
+ ace = "none"
209
+ note = "inflection → root; lexicon normalization, not knowledge"
210
+
211
+ [[relation]]
212
+ rel = "/r/DerivedFrom"
213
+ surface = "the word {start} is derived from {end}"
214
+ ace = "none"
215
+ note = "word derivation; lexicon material, not an axiom"
216
+
217
+ [[relation]]
218
+ rel = "/r/SymbolOf"
219
+ surface = "{start} is a symbol of {end}"
220
+ ace = "none"
221
+ note = "symbolism — no OWL fit"
222
+
223
+ [[relation]]
224
+ rel = "/r/SimilarTo"
225
+ surface = "{start} is similar to {end}"
226
+ ace = "none"
227
+ note = "graded similarity — no crisp OWL fit; potential fuzzy-match hint only"
228
+
229
+ [[relation]]
230
+ rel = "/r/HasContext"
231
+ surface = "{start} is used in the context of {end}"
232
+ ace = "none"
233
+ note = "usage-domain tag — the slice FILTER signal (tech-domain selection), not a fact to store"
234
+
235
+ [[relation]]
236
+ rel = "/r/EtymologicallyRelatedTo"
237
+ surface = "{start} shares an origin with {end}"
238
+ ace = "none"
239
+ note = "etymology — filtered out of the slice by policy"
240
+
241
+ [[relation]]
242
+ rel = "/r/EtymologicallyDerivedFrom"
243
+ surface = "the word {start} comes from {end}"
244
+ ace = "none"
245
+ note = "etymology — filtered out of the slice by policy"
246
+
247
+ [[relation]]
248
+ rel = "/r/ExternalURL"
249
+ surface = "{start} is described at {end}"
250
+ ace = "none"
251
+ note = "link out of the graph — filtered out of the slice (end is a URL, not an en concept)"