@polycode-projects/the-mechanical-code-talker 3.0.5 → 3.0.6
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 +6 -6
- package/corpus/child/README.md +2 -2
- package/corpus/child/index.json.gz +0 -0
- package/corpus/child/manifest.json +74 -74
- package/corpus/child/shards/child-00.jsonl.gz +0 -0
- package/corpus/child/shards/child-01.jsonl.gz +0 -0
- package/corpus/child/shards/child-02.jsonl.gz +0 -0
- package/corpus/child/shards/child-03.jsonl.gz +0 -0
- package/corpus/child/shards/child-04.jsonl.gz +0 -0
- package/corpus/child/shards/child-05.jsonl.gz +0 -0
- package/corpus/child/shards/child-06.jsonl.gz +0 -0
- package/corpus/child/shards/child-07.jsonl.gz +0 -0
- package/corpus/child/shards/child-08.jsonl.gz +0 -0
- package/corpus/child/shards/child-09.jsonl.gz +0 -0
- package/corpus/child/shards/child-0a.jsonl.gz +0 -0
- package/corpus/child/shards/child-0b.jsonl.gz +0 -0
- package/corpus/child/shards/child-0c.jsonl.gz +0 -0
- package/corpus/child/shards/child-0d.jsonl.gz +0 -0
- package/corpus/child/shards/child-0e.jsonl.gz +0 -0
- package/corpus/child/shards/child-0f.jsonl.gz +0 -0
- package/corpus/child/shards/child-10.jsonl.gz +0 -0
- package/corpus/child/shards/child-11.jsonl.gz +0 -0
- package/corpus/child/shards/child-12.jsonl.gz +0 -0
- package/corpus/child/shards/child-13.jsonl.gz +0 -0
- package/corpus/child/shards/child-14.jsonl.gz +0 -0
- package/corpus/child/shards/child-15.jsonl.gz +0 -0
- package/corpus/child/shards/child-16.jsonl.gz +0 -0
- package/corpus/child/shards/child-17.jsonl.gz +0 -0
- package/corpus/child/shards/child-18.jsonl.gz +0 -0
- package/corpus/child/shards/child-19.jsonl.gz +0 -0
- package/corpus/child/shards/child-1a.jsonl.gz +0 -0
- package/corpus/child/shards/child-1b.jsonl.gz +0 -0
- package/corpus/child/shards/child-1c.jsonl.gz +0 -0
- package/corpus/child/shards/child-1d.jsonl.gz +0 -0
- package/corpus/child/shards/child-1e.jsonl.gz +0 -0
- package/corpus/child/shards/child-1f.jsonl.gz +0 -0
- package/corpus/conceptnet/quality-filter.mjs +28 -4
- package/corpus/tier2/generate.mjs +4 -8
- package/corpus/tier2/human-large.jsonl +0 -2
- package/corpus/tier2/human-medium.jsonl +0 -2
- package/corpus/tier2/manifest.json +6 -6
- package/data/templates/constructions/digest-sentence-structures.toml +146 -1
- package/package.json +1 -1
- package/src/adapters/corpus/digest-bank.mjs +15 -3
- package/src/domain/digest/compose.mjs +142 -10
- package/src/domain/digest/config.json +5 -0
- package/src/domain/digest/structures.mjs +39 -14
- package/src/services/chat-page-viz.mjs +15 -4
- package/src/services/chat.mjs +118 -49
- package/src/services/extract-facts.mjs +46 -4
- package/src/services/research.mjs +80 -3
- package/src/surfaces/web/chat-browser-entry.mjs +23 -3
- package/src/surfaces/web/memory-ask-browser.bundle.js +108 -108
|
@@ -10,19 +10,30 @@
|
|
|
10
10
|
# reads only [[relation]] and [[construction]] tables, so it ignores this file's
|
|
11
11
|
# [[structure]] rows; the digest layer reads them through its own path.
|
|
12
12
|
#
|
|
13
|
-
# Each row is one (family, form) pairing
|
|
13
|
+
# Each row is one wording for a (family, form) pairing. A key may hold several
|
|
14
|
+
# rows: the renderer collects them all and picks one by a deterministic hash of
|
|
15
|
+
# term|family|form|variantSeed, so a key with one wording always renders that
|
|
16
|
+
# wording, while a key with several gives natural variety that stays stable
|
|
17
|
+
# across runs. An exact-string duplicate under a key is folded; a second, truly
|
|
18
|
+
# different wording is kept as another variant.
|
|
14
19
|
# family one of the digest families (isa | location | partOf | capableOf |
|
|
15
20
|
# usedFor) — the closed set stage 1 groups predicates under. Each is
|
|
16
21
|
# verb-coherent: every predicate in it shares this frame's verb.
|
|
17
22
|
# form single — one fact of the family
|
|
18
23
|
# several — two or more facts of the family, merged into one clause
|
|
19
24
|
# chained — one isa fact plus its rendered ancestry chain
|
|
25
|
+
# group — one body sentence of the multi-sentence isa digest: the
|
|
26
|
+
# facts that share an immediate parent class, named together
|
|
27
|
+
# closer — the closing sentence of the multi-sentence isa digest:
|
|
28
|
+
# the highest-level ontology roots the term sits under
|
|
20
29
|
# template the skeleton. Placeholders the renderer fills (words.mjs supplies
|
|
21
30
|
# the article, plural and casing):
|
|
22
31
|
# {TERM} {TERM_CAP} the term, lower / sentence-leading
|
|
23
32
|
# {A_TERM} {A_TERM_CAP} "an aardvark" / "An aardvark"
|
|
24
33
|
# {TERMS} {TERMS_CAP} "aardvarks" / "Aardvarks"
|
|
25
34
|
# {PRONOUN} {PRONOUN_CAP} "it" / "It" (after first mention)
|
|
35
|
+
# {PARENT} {A_PARENT} the shared immediate parent of a group
|
|
36
|
+
# — "mammal" / "a mammal"
|
|
26
37
|
# {OBJECT} the first object, raw
|
|
27
38
|
# {A_OBJECT} "a mammal" (article chosen per word)
|
|
28
39
|
# {OBJECTS_A} "a mammal, a burrowing animal, and …"
|
|
@@ -48,6 +59,50 @@ family = "isa"
|
|
|
48
59
|
form = "chained"
|
|
49
60
|
template = "{A_TERM_CAP} is {CHAIN}."
|
|
50
61
|
|
|
62
|
+
# ---- isa group: one body sentence for facts under a shared parent class -----
|
|
63
|
+
|
|
64
|
+
[[structure]]
|
|
65
|
+
family = "isa"
|
|
66
|
+
form = "group"
|
|
67
|
+
template = "As a kind of {PARENT}, {PRONOUN} is also {OBJECTS_A}."
|
|
68
|
+
|
|
69
|
+
[[structure]]
|
|
70
|
+
family = "isa"
|
|
71
|
+
form = "group"
|
|
72
|
+
template = "Being {A_PARENT}, {PRONOUN} is {OBJECTS_A} too."
|
|
73
|
+
|
|
74
|
+
[[structure]]
|
|
75
|
+
family = "isa"
|
|
76
|
+
form = "group"
|
|
77
|
+
template = "Within the {PARENT} family, {PRONOUN} counts as {OBJECTS_A}."
|
|
78
|
+
|
|
79
|
+
[[structure]]
|
|
80
|
+
family = "isa"
|
|
81
|
+
form = "group"
|
|
82
|
+
template = "As {A_PARENT}, {PRONOUN} is {OBJECTS_A}."
|
|
83
|
+
|
|
84
|
+
# ---- isa closer: the highest-level roots the term sits under ----------------
|
|
85
|
+
|
|
86
|
+
[[structure]]
|
|
87
|
+
family = "isa"
|
|
88
|
+
form = "closer"
|
|
89
|
+
template = "More broadly, {TERMS} sit within {OBJECTS_A} in the wider hierarchy."
|
|
90
|
+
|
|
91
|
+
[[structure]]
|
|
92
|
+
family = "isa"
|
|
93
|
+
form = "closer"
|
|
94
|
+
template = "More generally, {TERMS} fall under {OBJECTS_A}."
|
|
95
|
+
|
|
96
|
+
[[structure]]
|
|
97
|
+
family = "isa"
|
|
98
|
+
form = "closer"
|
|
99
|
+
template = "Higher up, {TERMS} are kinds of {OBJECTS_PLURAL}."
|
|
100
|
+
|
|
101
|
+
[[structure]]
|
|
102
|
+
family = "isa"
|
|
103
|
+
form = "closer"
|
|
104
|
+
template = "Zooming out, {TERMS} belong among {OBJECTS_PLURAL}."
|
|
105
|
+
|
|
51
106
|
# ---- location: where it is found (atLocation / locatedNear) ----------------
|
|
52
107
|
|
|
53
108
|
[[structure]]
|
|
@@ -60,6 +115,21 @@ family = "location"
|
|
|
60
115
|
form = "several"
|
|
61
116
|
template = "{PRONOUN_CAP} is found in {OBJECTS_RAW}."
|
|
62
117
|
|
|
118
|
+
[[structure]]
|
|
119
|
+
family = "location"
|
|
120
|
+
form = "several"
|
|
121
|
+
template = "{PRONOUN_CAP} lives in {OBJECTS_RAW}."
|
|
122
|
+
|
|
123
|
+
[[structure]]
|
|
124
|
+
family = "location"
|
|
125
|
+
form = "several"
|
|
126
|
+
template = "You'll find {PRONOUN} in {OBJECTS_RAW}."
|
|
127
|
+
|
|
128
|
+
[[structure]]
|
|
129
|
+
family = "location"
|
|
130
|
+
form = "several"
|
|
131
|
+
template = "{PRONOUN_CAP} turns up in {OBJECTS_RAW}."
|
|
132
|
+
|
|
63
133
|
# ---- partOf: what it belongs to -------------------------------------------
|
|
64
134
|
|
|
65
135
|
[[structure]]
|
|
@@ -67,11 +137,41 @@ family = "partOf"
|
|
|
67
137
|
form = "single"
|
|
68
138
|
template = "{PRONOUN_CAP} is part of {A_OBJECT}."
|
|
69
139
|
|
|
140
|
+
[[structure]]
|
|
141
|
+
family = "partOf"
|
|
142
|
+
form = "single"
|
|
143
|
+
template = "{PRONOUN_CAP} belongs to {A_OBJECT}."
|
|
144
|
+
|
|
145
|
+
[[structure]]
|
|
146
|
+
family = "partOf"
|
|
147
|
+
form = "single"
|
|
148
|
+
template = "{PRONOUN_CAP} forms part of {A_OBJECT}."
|
|
149
|
+
|
|
150
|
+
[[structure]]
|
|
151
|
+
family = "partOf"
|
|
152
|
+
form = "single"
|
|
153
|
+
template = "{PRONOUN_CAP} makes up part of {A_OBJECT}."
|
|
154
|
+
|
|
70
155
|
[[structure]]
|
|
71
156
|
family = "partOf"
|
|
72
157
|
form = "several"
|
|
73
158
|
template = "{PRONOUN_CAP} is part of {OBJECTS_A}."
|
|
74
159
|
|
|
160
|
+
[[structure]]
|
|
161
|
+
family = "partOf"
|
|
162
|
+
form = "several"
|
|
163
|
+
template = "{PRONOUN_CAP} belongs to {OBJECTS_A}."
|
|
164
|
+
|
|
165
|
+
[[structure]]
|
|
166
|
+
family = "partOf"
|
|
167
|
+
form = "several"
|
|
168
|
+
template = "{PRONOUN_CAP} forms part of {OBJECTS_A}."
|
|
169
|
+
|
|
170
|
+
[[structure]]
|
|
171
|
+
family = "partOf"
|
|
172
|
+
form = "several"
|
|
173
|
+
template = "{PRONOUN_CAP} is one part of {OBJECTS_A}."
|
|
174
|
+
|
|
75
175
|
# ---- capableOf: what it can do --------------------------------------------
|
|
76
176
|
|
|
77
177
|
[[structure]]
|
|
@@ -84,6 +184,21 @@ family = "capableOf"
|
|
|
84
184
|
form = "several"
|
|
85
185
|
template = "{PRONOUN_CAP} can {OBJECTS_RAW}."
|
|
86
186
|
|
|
187
|
+
[[structure]]
|
|
188
|
+
family = "capableOf"
|
|
189
|
+
form = "several"
|
|
190
|
+
template = "{PRONOUN_CAP} is able to {OBJECTS_RAW}."
|
|
191
|
+
|
|
192
|
+
[[structure]]
|
|
193
|
+
family = "capableOf"
|
|
194
|
+
form = "several"
|
|
195
|
+
template = "{PRONOUN_CAP} knows how to {OBJECTS_RAW}."
|
|
196
|
+
|
|
197
|
+
[[structure]]
|
|
198
|
+
family = "capableOf"
|
|
199
|
+
form = "several"
|
|
200
|
+
template = "{PRONOUN_CAP} tends to {OBJECTS_RAW}."
|
|
201
|
+
|
|
87
202
|
# ---- usedFor: what it is used for -----------------------------------------
|
|
88
203
|
|
|
89
204
|
[[structure]]
|
|
@@ -91,7 +206,37 @@ family = "usedFor"
|
|
|
91
206
|
form = "single"
|
|
92
207
|
template = "{PRONOUN_CAP} is used for {OBJECT}."
|
|
93
208
|
|
|
209
|
+
[[structure]]
|
|
210
|
+
family = "usedFor"
|
|
211
|
+
form = "single"
|
|
212
|
+
template = "{PRONOUN_CAP} helps with {OBJECT}."
|
|
213
|
+
|
|
214
|
+
[[structure]]
|
|
215
|
+
family = "usedFor"
|
|
216
|
+
form = "single"
|
|
217
|
+
template = "People use {PRONOUN} for {OBJECT}."
|
|
218
|
+
|
|
219
|
+
[[structure]]
|
|
220
|
+
family = "usedFor"
|
|
221
|
+
form = "single"
|
|
222
|
+
template = "{PRONOUN_CAP} works for {OBJECT}."
|
|
223
|
+
|
|
94
224
|
[[structure]]
|
|
95
225
|
family = "usedFor"
|
|
96
226
|
form = "several"
|
|
97
227
|
template = "{PRONOUN_CAP} is used for {OBJECTS_RAW}."
|
|
228
|
+
|
|
229
|
+
[[structure]]
|
|
230
|
+
family = "usedFor"
|
|
231
|
+
form = "several"
|
|
232
|
+
template = "{PRONOUN_CAP} helps with {OBJECTS_RAW}."
|
|
233
|
+
|
|
234
|
+
[[structure]]
|
|
235
|
+
family = "usedFor"
|
|
236
|
+
form = "several"
|
|
237
|
+
template = "People use {PRONOUN} for {OBJECTS_RAW}."
|
|
238
|
+
|
|
239
|
+
[[structure]]
|
|
240
|
+
family = "usedFor"
|
|
241
|
+
form = "several"
|
|
242
|
+
template = "{PRONOUN_CAP} works for {OBJECTS_RAW}."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; indexes a repo on request (tmct index) or reads any producer's graph.",
|
|
@@ -5,9 +5,15 @@
|
|
|
5
5
|
// in src/domain/digest; only the TOML read lives here, so every node surface
|
|
6
6
|
// (chat, `tmct digest`, the page generators) shares one seam.
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// build-chat-bundle.mjs swaps this module for a live in-memory twin (the
|
|
9
|
+
// chat dock DOES digest in-page there — chat-browser-entry.mjs feeds it the
|
|
10
|
+
// build-time-embedded [[structure]] rows via setDigestStructures); every
|
|
11
|
+
// other browser bundle (ask, ledger's static render) still stubs it out to
|
|
12
|
+
// null, so those consumers keep treating a null article as "fall back to the
|
|
13
|
+
// flat fact list". setDigestStructures below is a no-op on THIS, the real
|
|
14
|
+
// node-side module — kept only for interface parity with the live bundle
|
|
15
|
+
// twin, so a file that imports both under plain Node (as a chat-engine
|
|
16
|
+
// contract test does) resolves the same export set either way.
|
|
11
17
|
|
|
12
18
|
import { readFileSync } from "node:fs";
|
|
13
19
|
import { fileURLToPath } from "node:url";
|
|
@@ -19,6 +25,12 @@ import { digestStoreStats, chainsForObjects, isaObjectsOf } from "../../domain/d
|
|
|
19
25
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
20
26
|
const BANK_FILE = join(HERE, "..", "..", "..", "data", "templates", "constructions", "digest-sentence-structures.toml");
|
|
21
27
|
|
|
28
|
+
/** No-op here — the node-side adapter always reads its structures from the
|
|
29
|
+
* committed TOML bank (below), never from an externally-supplied table. Only
|
|
30
|
+
* the browser bundle's live twin (build-chat-bundle.mjs's stub) actually
|
|
31
|
+
* holds state behind this setter. */
|
|
32
|
+
export function setDigestStructures() {}
|
|
33
|
+
|
|
22
34
|
/** The raw [[structure]] rows parsed from the committed bank, once. A missing or
|
|
23
35
|
* unparseable bank yields an empty list — the digest degrades to the caller's
|
|
24
36
|
* flat fallback rather than throwing, the same posture the construction-bank
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { renderStructure } from "./structures.mjs";
|
|
9
9
|
import { FAMILY_PRIORITY } from "./select.mjs";
|
|
10
10
|
import { articleFor, capitalizeFirst } from "./words.mjs";
|
|
11
|
+
import DEFAULT_CONFIG from "./config.json" with { type: "json" };
|
|
11
12
|
|
|
12
13
|
const DESCRIPTION_FAMILIES = FAMILY_PRIORITY.filter((f) => f !== "isa" && f !== "other");
|
|
13
14
|
|
|
@@ -22,6 +23,137 @@ function rowsByFamily(selected) {
|
|
|
22
23
|
return by;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
/** Split `rows` into runs of at most `size`, keeping order. Never returns an
|
|
27
|
+
* empty run; a size below 1 is treated as 1. */
|
|
28
|
+
export function chunk(rows, size) {
|
|
29
|
+
const n = Math.max(1, Math.trunc(size) || 1);
|
|
30
|
+
const out = [];
|
|
31
|
+
for (let i = 0; i < (rows || []).length; i += n) out.push(rows.slice(i, i + n));
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Bucket isa rows by their object's immediate ancestor (`chains[object][1]`),
|
|
36
|
+
* so facts that share a parent class can be named in one coherent sentence.
|
|
37
|
+
* A parent with two or more rows is a real group; a parent with only one row,
|
|
38
|
+
* and any row whose object has no recorded ancestry, fold into one shared
|
|
39
|
+
* leftover pool. Groups come first in the order their earliest member ranked;
|
|
40
|
+
* the leftover pool, in rank order, comes last. Returns
|
|
41
|
+
* `[{ parent, rows }, …]` where `parent` is null for the leftover pool. */
|
|
42
|
+
export function groupIsaByParent(rows, chains) {
|
|
43
|
+
const parentOf = (row) => (chains?.[row.object] || [])[1] || null;
|
|
44
|
+
const counts = new Map();
|
|
45
|
+
for (const row of rows || []) {
|
|
46
|
+
const p = parentOf(row);
|
|
47
|
+
if (p) counts.set(p, (counts.get(p) || 0) + 1);
|
|
48
|
+
}
|
|
49
|
+
const groupRows = new Map();
|
|
50
|
+
const order = [];
|
|
51
|
+
const leftover = [];
|
|
52
|
+
for (const row of rows || []) {
|
|
53
|
+
const p = parentOf(row);
|
|
54
|
+
if (p && counts.get(p) >= 2) {
|
|
55
|
+
if (!groupRows.has(p)) { groupRows.set(p, []); order.push(p); }
|
|
56
|
+
groupRows.get(p).push(row);
|
|
57
|
+
} else {
|
|
58
|
+
leftover.push(row);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const groups = order.map((parent) => ({ parent, rows: groupRows.get(parent) }));
|
|
62
|
+
if (leftover.length) groups.push({ parent: null, rows: leftover });
|
|
63
|
+
return groups;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The highest-level ontology roots the spoken isa objects reach: for each row,
|
|
67
|
+
* the last (most general) node its ancestry chain walked. Roots rank by how
|
|
68
|
+
* many chains reach them (ties broken alphabetically for determinism); the top
|
|
69
|
+
* `count` are kept. Returns `{ roots, rows }` — `roots` names the concepts a
|
|
70
|
+
* closer sentence renders, `rows` are the real backing isa facts so the closer
|
|
71
|
+
* keeps its provenance without ever synthesising a row. */
|
|
72
|
+
export function closerRootsFor(chains, usedRows, count) {
|
|
73
|
+
const rootCount = new Map();
|
|
74
|
+
const rootRows = new Map();
|
|
75
|
+
for (const row of usedRows || []) {
|
|
76
|
+
const chain = chains?.[row.object];
|
|
77
|
+
if (!Array.isArray(chain) || chain.length < 2) continue;
|
|
78
|
+
const root = chain[chain.length - 1];
|
|
79
|
+
if (!root || root === row.object) continue;
|
|
80
|
+
rootCount.set(root, (rootCount.get(root) || 0) + 1);
|
|
81
|
+
if (!rootRows.has(root)) rootRows.set(root, []);
|
|
82
|
+
rootRows.get(root).push(row);
|
|
83
|
+
}
|
|
84
|
+
const roots = [...rootCount.keys()]
|
|
85
|
+
.sort((a, b) => (rootCount.get(b) - rootCount.get(a)) || a.localeCompare(b))
|
|
86
|
+
.slice(0, Math.max(0, count));
|
|
87
|
+
const rows = [];
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
for (const root of roots) for (const r of rootRows.get(root) || []) {
|
|
90
|
+
if (!seen.has(r.id)) { seen.add(r.id); rows.push(r); }
|
|
91
|
+
}
|
|
92
|
+
return { roots, rows };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Compose the isa (definition) sentences. Gated purely on how many isa facts
|
|
96
|
+
* survived selection: none says nothing; one renders the single or chained
|
|
97
|
+
* form; a handful (up to the split threshold) merges into one clause. Past the
|
|
98
|
+
* threshold the run-on breaks apart — an opener names the top concepts, body
|
|
99
|
+
* sentences group the rest by shared parent, and a closer names the roots they
|
|
100
|
+
* all sit under. Returns tagged sentence objects, or an empty list. */
|
|
101
|
+
function composeIsaSection(isaRows, chains, term, table, config) {
|
|
102
|
+
const out = [];
|
|
103
|
+
if (!isaRows.length) return out;
|
|
104
|
+
|
|
105
|
+
if (isaRows.length === 1) {
|
|
106
|
+
const chain = chains[isaRows[0].object];
|
|
107
|
+
const form = chain && chain.length > 1 ? "chained" : "single";
|
|
108
|
+
const s = renderStructure(table, "isa", isaRows, { term, form, chain });
|
|
109
|
+
if (s) out.push({ ...s, role: "names-term", paragraph: "definition" });
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (isaRows.length <= config.groupSplitThreshold) {
|
|
114
|
+
const s = renderStructure(table, "isa", isaRows, { term, form: "several" });
|
|
115
|
+
if (s) out.push({ ...s, role: "names-term", paragraph: "definition" });
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const openerRows = isaRows.slice(0, config.openerConceptCount);
|
|
120
|
+
const restRows = isaRows.slice(config.openerConceptCount);
|
|
121
|
+
const opener = renderStructure(table, "isa", openerRows, {
|
|
122
|
+
term, form: openerRows.length > 1 ? "several" : "single", variantSeed: term,
|
|
123
|
+
});
|
|
124
|
+
if (opener) out.push({ ...opener, role: "names-term", paragraph: "definition" });
|
|
125
|
+
|
|
126
|
+
let units = [];
|
|
127
|
+
for (const g of groupIsaByParent(restRows, chains)) {
|
|
128
|
+
for (const c of chunk(g.rows, config.maxObjectsPerBodySentence)) units.push({ parent: g.parent, rows: c });
|
|
129
|
+
}
|
|
130
|
+
// Cap the sentence count. Overflow folds into the last sentence rather than
|
|
131
|
+
// drop a selected fact — the merged sentence loses its single-parent coherence,
|
|
132
|
+
// so it renders as a plain "several" clause.
|
|
133
|
+
if (units.length > config.maxBodySentences) {
|
|
134
|
+
const kept = units.slice(0, config.maxBodySentences);
|
|
135
|
+
const last = kept[kept.length - 1];
|
|
136
|
+
for (const o of units.slice(config.maxBodySentences)) last.rows = last.rows.concat(o.rows);
|
|
137
|
+
last.parent = null;
|
|
138
|
+
units = kept;
|
|
139
|
+
}
|
|
140
|
+
for (const u of units) {
|
|
141
|
+
const s = u.parent
|
|
142
|
+
? renderStructure(table, "isa", u.rows, { term, form: "group", parent: u.parent, variantSeed: u.parent })
|
|
143
|
+
: renderStructure(table, "isa", u.rows, { term, form: "several", variantSeed: term });
|
|
144
|
+
if (s) out.push({ ...s, role: "describes-isa", paragraph: "definition" });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const { roots, rows: closerRows } = closerRootsFor(chains, isaRows, config.closerRootCount);
|
|
148
|
+
if (roots.length) {
|
|
149
|
+
const s = renderStructure(table, "isa", closerRows.length ? closerRows : isaRows, {
|
|
150
|
+
term, form: "closer", objectsOverride: roots, variantSeed: term,
|
|
151
|
+
});
|
|
152
|
+
if (s) out.push({ ...s, role: "closes-isa", paragraph: "definition" });
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
25
157
|
/** The first sentence must introduce the term. When no isa fact led (so the
|
|
26
158
|
* first sentence opens with a bare "It"), rewrite that pronoun into the term's
|
|
27
159
|
* own noun phrase so the pronoun has an antecedent. */
|
|
@@ -59,8 +191,11 @@ function paragraphsFrom(sentences, cap) {
|
|
|
59
191
|
*
|
|
60
192
|
* opts:
|
|
61
193
|
* - chains: { object -> [object, parent, …] } ancestry chains, so a lone isa
|
|
62
|
-
* fact can render as a chain ("a mammal, and so an animal") when one exists
|
|
194
|
+
* fact can render as a chain ("a mammal, and so an animal") when one exists,
|
|
195
|
+
* and a many-fact term can split into opener, grouped body and closer.
|
|
63
196
|
* - maxSentencesPerParagraph: the sentence cap (default 3).
|
|
197
|
+
* - config: digest tuning overrides (the group split threshold and the
|
|
198
|
+
* per-sentence / per-digest caps), defaulting to config.json.
|
|
64
199
|
*
|
|
65
200
|
* Returns { term, sentences, paragraphs, provenanceRows }. Every sentence in
|
|
66
201
|
* `sentences` carries { text, rows, family, role, paragraph }; `paragraphs`
|
|
@@ -71,22 +206,19 @@ export function composeTermDigest(selection, table, opts = {}) {
|
|
|
71
206
|
const term = selection?.term || "";
|
|
72
207
|
const cap = Number.isInteger(opts.maxSentencesPerParagraph) ? opts.maxSentencesPerParagraph : 3;
|
|
73
208
|
const chains = opts.chains || {};
|
|
209
|
+
const config = { ...DEFAULT_CONFIG, ...(opts.config || {}) };
|
|
74
210
|
const byFamily = rowsByFamily(selection?.selected || []);
|
|
75
211
|
const sentences = [];
|
|
76
212
|
|
|
77
|
-
const
|
|
78
|
-
if (isaRows.length) {
|
|
79
|
-
const chain = isaRows.length === 1 ? chains[isaRows[0].object] : null;
|
|
80
|
-
const form = chain && chain.length > 1 ? "chained" : (isaRows.length > 1 ? "several" : "single");
|
|
81
|
-
const s = renderStructure(table, "isa", isaRows, { term, form, chain });
|
|
82
|
-
if (s) sentences.push({ ...s, role: "names-term", paragraph: "definition" });
|
|
83
|
-
}
|
|
213
|
+
for (const s of composeIsaSection(byFamily.get("isa") || [], chains, term, table, config)) sentences.push(s);
|
|
84
214
|
|
|
85
215
|
for (const family of DESCRIPTION_FAMILIES) {
|
|
86
216
|
const rows = byFamily.get(family);
|
|
87
217
|
if (!rows || !rows.length) continue;
|
|
88
|
-
const
|
|
89
|
-
|
|
218
|
+
for (const c of chunk(rows, config.maxObjectsPerBodySentence)) {
|
|
219
|
+
const s = renderStructure(table, family, c, { term });
|
|
220
|
+
if (s) sentences.push({ ...s, role: "describes", paragraph: "description" });
|
|
221
|
+
}
|
|
90
222
|
}
|
|
91
223
|
|
|
92
224
|
ensureFirstNamesTerm(sentences, term);
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
"entailedDepthPenalty": 0.2,
|
|
5
5
|
"minoritySensePenalty": 0.5,
|
|
6
6
|
"minScore": 0.05,
|
|
7
|
+
"groupSplitThreshold": 4,
|
|
8
|
+
"openerConceptCount": 2,
|
|
9
|
+
"maxObjectsPerBodySentence": 4,
|
|
10
|
+
"maxBodySentences": 4,
|
|
11
|
+
"closerRootCount": 2,
|
|
7
12
|
"budget": {
|
|
8
13
|
"chatReply": 5,
|
|
9
14
|
"researchPanel": 10,
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
// sibling word helpers.
|
|
7
7
|
|
|
8
8
|
import { articleFor, pluralOf, capitalizeFirst, series } from "./words.mjs";
|
|
9
|
+
import { fnv1a32 } from "../hash.mjs";
|
|
9
10
|
|
|
10
11
|
const VALID_FAMILIES = new Set(["isa", "location", "partOf", "capableOf", "usedFor"]);
|
|
11
|
-
const VALID_FORMS = new Set(["single", "several", "chained"]);
|
|
12
|
+
const VALID_FORMS = new Set(["single", "several", "chained", "group", "closer"]);
|
|
12
13
|
|
|
13
14
|
const keyFor = (family, form) => `${family}:${form}`;
|
|
14
15
|
|
|
@@ -16,8 +17,10 @@ const keyFor = (family, form) => `${family}:${form}`;
|
|
|
16
17
|
* Validate and index the raw [[structure]] rows into a Map keyed by
|
|
17
18
|
* `family:form`. Closed-vocabulary discipline, same as the construction banks:
|
|
18
19
|
* an unknown family or form, or a missing template, drops the row rather than
|
|
19
|
-
* coercing it.
|
|
20
|
-
*
|
|
20
|
+
* coercing it. Every valid row for a (family, form) is collected into that
|
|
21
|
+
* key's `templates` list, so the renderer can pick one deterministically from
|
|
22
|
+
* the pool; an exact-string duplicate is folded, but a second distinct wording
|
|
23
|
+
* is kept as another variant.
|
|
21
24
|
*/
|
|
22
25
|
export function buildStructureTable(structures) {
|
|
23
26
|
const table = new Map();
|
|
@@ -26,8 +29,9 @@ export function buildStructureTable(structures) {
|
|
|
26
29
|
if (typeof s.form !== "string" || !VALID_FORMS.has(s.form)) continue;
|
|
27
30
|
if (typeof s.template !== "string" || !s.template.trim()) continue;
|
|
28
31
|
const key = keyFor(s.family, s.form);
|
|
29
|
-
if (table.has(key))
|
|
30
|
-
table.
|
|
32
|
+
if (!table.has(key)) table.set(key, { family: s.family, form: s.form, templates: [] });
|
|
33
|
+
const entry = table.get(key);
|
|
34
|
+
if (!entry.templates.includes(s.template)) entry.templates.push(s.template);
|
|
31
35
|
}
|
|
32
36
|
return table;
|
|
33
37
|
}
|
|
@@ -44,12 +48,15 @@ function renderChain(chain) {
|
|
|
44
48
|
return `${head}, and so ${series(rest.map(withArticle))}`;
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
/** The slot values for one render, derived from the term, its objects
|
|
48
|
-
* optional ancestry chain
|
|
49
|
-
|
|
51
|
+
/** The slot values for one render, derived from the term, its objects, an
|
|
52
|
+
* optional ancestry chain and an optional parent class (the shared immediate
|
|
53
|
+
* ancestor a `group` sentence hangs off). A placeholder with no derived value
|
|
54
|
+
* renders empty. */
|
|
55
|
+
function slotsFor(term, objects, chain, parent) {
|
|
50
56
|
const t = String(term || "").trim();
|
|
51
57
|
const objs = (objects || []).map((o) => String(o || "").trim()).filter(Boolean);
|
|
52
58
|
const first = objs[0] || "";
|
|
59
|
+
const p = String(parent || "").trim();
|
|
53
60
|
return {
|
|
54
61
|
TERM: t,
|
|
55
62
|
TERM_CAP: capitalizeFirst(t),
|
|
@@ -59,6 +66,8 @@ function slotsFor(term, objects, chain) {
|
|
|
59
66
|
TERMS_CAP: capitalizeFirst(pluralOf(t)),
|
|
60
67
|
PRONOUN: "it",
|
|
61
68
|
PRONOUN_CAP: "It",
|
|
69
|
+
PARENT: p,
|
|
70
|
+
A_PARENT: p ? withArticle(p) : "",
|
|
62
71
|
OBJECT: first,
|
|
63
72
|
A_OBJECT: first ? withArticle(first) : "",
|
|
64
73
|
OBJECTS_A: series(objs.map(withArticle)),
|
|
@@ -82,9 +91,22 @@ function fill(template, slots) {
|
|
|
82
91
|
* Render one clause for `facts` (all of one family) using the structure the
|
|
83
92
|
* table holds for (family, form). `form` defaults by fact count — one fact is
|
|
84
93
|
* "single", more is "several" — unless the caller names a form (e.g. "chained"
|
|
85
|
-
* with an ancestry chain
|
|
86
|
-
*
|
|
87
|
-
*
|
|
94
|
+
* with an ancestry chain, or "group"/"closer" for the multi-sentence isa body).
|
|
95
|
+
*
|
|
96
|
+
* When a key holds more than one wording, the one that renders is picked by a
|
|
97
|
+
* deterministic FNV-1a hash of `term|family|form|variantSeed` — the same
|
|
98
|
+
* paraphrase-pool tool answer-variants.mjs uses — so the choice is stable across
|
|
99
|
+
* runs but varies by term (and by `variantSeed`, e.g. the shared parent, so two
|
|
100
|
+
* group sentences under one term can read differently). A single-wording key
|
|
101
|
+
* always resolves to that one wording.
|
|
102
|
+
*
|
|
103
|
+
* `opts.objectsOverride` renders words other than the raw fact objects (a closer
|
|
104
|
+
* sentence names root concepts, not the facts) while the returned `rows` stays
|
|
105
|
+
* the real backing facts, so provenance survives. `opts.parent` fills the
|
|
106
|
+
* `{PARENT}`/`{A_PARENT}` slots a group sentence hangs off.
|
|
107
|
+
*
|
|
108
|
+
* Returns { text, rows, family, form } so the sentence traces to the exact fact
|
|
109
|
+
* rows behind it, or null when no structure matches or there are no facts.
|
|
88
110
|
*/
|
|
89
111
|
export function renderStructure(table, family, facts, opts = {}) {
|
|
90
112
|
const rows = (facts || []).filter(Boolean);
|
|
@@ -92,9 +114,12 @@ export function renderStructure(table, family, facts, opts = {}) {
|
|
|
92
114
|
const term = opts.term ?? rows[0].subject ?? "";
|
|
93
115
|
const form = opts.form || (rows.length > 1 ? "several" : "single");
|
|
94
116
|
const entry = table instanceof Map ? table.get(keyFor(family, form)) : null;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
117
|
+
const templates = entry ? entry.templates : null;
|
|
118
|
+
if (!templates || !templates.length) return null;
|
|
119
|
+
const variantSeed = opts.variantSeed ?? "";
|
|
120
|
+
const idx = fnv1a32(`${term}|${family}|${form}|${variantSeed}`) % templates.length;
|
|
121
|
+
const objects = opts.objectsOverride || rows.map((r) => r.object);
|
|
122
|
+
const text = fill(templates[idx], slotsFor(term, objects, opts.chain, opts.parent));
|
|
98
123
|
if (!text) return null;
|
|
99
124
|
return { text, rows, family, form };
|
|
100
125
|
}
|
|
@@ -172,10 +172,18 @@ export function transcriptMarkdown(turns, meta, headerMd, turnMd) {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
/** The self-contained "talk to it" full-screen page. Pure — the same output
|
|
175
|
-
* for the same `title` every time; every other piece of
|
|
176
|
-
* every message, every chip) is computed live in the
|
|
177
|
-
* sibling chat bundle loads, exactly as the embedded widget
|
|
178
|
-
|
|
175
|
+
* for the same `title`/`digestStructures` every time; every other piece of
|
|
176
|
+
* state (the session, every message, every chip) is computed live in the
|
|
177
|
+
* browser once the sibling chat bundle loads, exactly as the embedded widget
|
|
178
|
+
* already works. `digestStructures` are the pre-parsed [[structure]] rows of
|
|
179
|
+
* the digest sentence-structure bank, embedded so a long answer can lead
|
|
180
|
+
* with a composed digest instead of the flat fact list — the same table
|
|
181
|
+
* research.html/ledger.html already embed, fed here to the chat bundle's own
|
|
182
|
+
* live digest-bank twin (see chat-browser-entry.mjs) rather than to a
|
|
183
|
+
* client-side digest panel of this page's own; an empty list degrades to the
|
|
184
|
+
* flat list exactly as before this page could digest at all. */
|
|
185
|
+
export function renderChatHtml({ title = DEFAULT_TITLE, digestStructures = [] } = {}) {
|
|
186
|
+
const digestStructuresJson = JSON.stringify(Array.isArray(digestStructures) ? digestStructures : []);
|
|
179
187
|
const legendHtml = PROV_LEGEND.map(
|
|
180
188
|
([key, label]) => `<span class="legend-item"><i class="dot dot-${provKey(key)}"></i>${escapeHtml(label)}</span>`,
|
|
181
189
|
).join("");
|
|
@@ -421,6 +429,7 @@ ${THEME_TOKENS_CSS}
|
|
|
421
429
|
const renderStatsPanelInto = ${renderStatsPanelInto.toString()};
|
|
422
430
|
const createTicker = ${createTicker.toString()};
|
|
423
431
|
const prefersReducedMotion = ${prefersReducedMotion.toString()};
|
|
432
|
+
const DIGEST_STRUCTURES = ${digestStructuresJson};
|
|
424
433
|
const el = (id) => document.getElementById(id);
|
|
425
434
|
|
|
426
435
|
if ("serviceWorker" in navigator) navigator.serviceWorker.register("./tmct-sw.js").catch(() => {});
|
|
@@ -655,6 +664,7 @@ ${THEME_TOKENS_CSS}
|
|
|
655
664
|
liveReference: liveReferenceForMode(checkedWikiMode()),
|
|
656
665
|
synthesisBudget: readSynthBudget(),
|
|
657
666
|
onLiveLookup: function () { statusEl.textContent = "searching wikipedia\\u2026"; },
|
|
667
|
+
digestStructures: DIGEST_STRUCTURES,
|
|
658
668
|
});
|
|
659
669
|
}
|
|
660
670
|
|
|
@@ -1139,6 +1149,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1139
1149
|
liveReference: liveReferenceForMode(initialMode),
|
|
1140
1150
|
synthesisBudget: readSynthBudget(),
|
|
1141
1151
|
onLiveLookup: function () { statusEl.textContent = "searching wikipedia\\u2026"; },
|
|
1152
|
+
digestStructures: DIGEST_STRUCTURES,
|
|
1142
1153
|
});
|
|
1143
1154
|
} else {
|
|
1144
1155
|
window.tmctChatSession = newSession();
|